diff --git a/internal/converter/core.go b/internal/converter/core.go index 5fa42b5..e02cdca 100644 --- a/internal/converter/core.go +++ b/internal/converter/core.go @@ -96,6 +96,7 @@ func (c *Converter) InitParse() { c.AddBoolParam(&c.Options.HasCover, "hascover", c.Options.HasCover, "Has cover. Indicate if your comic have a cover. The first page will be used as a cover and include after the title.") c.AddBoolParam(&c.Options.AddPanelView, "addpanelview", c.Options.AddPanelView, "Add an embeded panel view. On kindle you may not need this option as it is handled by the kindle.") c.AddIntParam(&c.Options.LimitMb, "limitmb", c.Options.LimitMb, "Limit size of the ePub: Default nolimit (0), Minimum 20") + c.AddBoolParam(&c.Options.StripFirstDirectoryFromToc, "strip", c.Options.StripFirstDirectoryFromToc, "Strip first directory from the TOC if only 1") c.AddSection("Default config") c.AddBoolParam(&c.Options.Show, "show", false, "Show your default parameters") diff --git a/internal/converter/options/core.go b/internal/converter/options/core.go index 26cc750..f5d27c9 100644 --- a/internal/converter/options/core.go +++ b/internal/converter/options/core.go @@ -20,18 +20,19 @@ type Options struct { Dry bool `yaml:"-"` // Config - Profile string `yaml:"profile"` - Quality int `yaml:"quality"` - Crop bool `yaml:"crop"` - Brightness int `yaml:"brightness"` - Contrast int `yaml:"contrast"` - AutoRotate bool `yaml:"auto_rotate"` - AutoSplitDoublePage bool `yaml:"auto_split_double_page"` - NoBlankPage bool `yaml:"no_blank_page"` - Manga bool `yaml:"manga"` - HasCover bool `yaml:"has_cover"` - AddPanelView bool `yaml:"add_panel_view"` - LimitMb int `yaml:"limit_mb"` + Profile string `yaml:"profile"` + Quality int `yaml:"quality"` + Crop bool `yaml:"crop"` + Brightness int `yaml:"brightness"` + Contrast int `yaml:"contrast"` + AutoRotate bool `yaml:"auto_rotate"` + AutoSplitDoublePage bool `yaml:"auto_split_double_page"` + NoBlankPage bool `yaml:"no_blank_page"` + Manga bool `yaml:"manga"` + HasCover bool `yaml:"has_cover"` + AddPanelView bool `yaml:"add_panel_view"` + LimitMb int `yaml:"limit_mb"` + StripFirstDirectoryFromToc bool `yaml:"strip_first_directory_from_toc"` // Default Config Show bool `yaml:"-"` @@ -48,19 +49,20 @@ type Options struct { func New() *Options { return &Options{ - Profile: "", - Quality: 85, - Crop: true, - Brightness: 0, - Contrast: 0, - AutoRotate: false, - AutoSplitDoublePage: false, - NoBlankPage: false, - Manga: false, - HasCover: true, - AddPanelView: false, - LimitMb: 0, - profiles: profiles.New(), + Profile: "", + Quality: 85, + Crop: true, + Brightness: 0, + Contrast: 0, + AutoRotate: false, + AutoSplitDoublePage: false, + NoBlankPage: false, + Manga: false, + HasCover: true, + AddPanelView: false, + LimitMb: 0, + StripFirstDirectoryFromToc: false, + profiles: profiles.New(), } } @@ -72,11 +74,11 @@ Options:` func (o *Options) String() string { return fmt.Sprintf(`%s - Input : %s - Output : %s - Author : %s - Title : %s - Workers : %d%s + Input : %s + Output : %s + Author : %s + Title : %s + Workers : %d%s `, o.Header(), o.Input, @@ -126,18 +128,19 @@ func (o *Options) ShowDefault() string { } return fmt.Sprintf(` - Profile : %s - Quality : %d - Crop : %v - Brightness : %d - Contrast : %d - AutoRotate : %v - AutoSplitDoublePage: %v - NoBlankPage : %v - Manga : %v - HasCover : %v - AddPanelView : %v - LimitMb : %s`, + Profile : %s + Quality : %d + Crop : %v + Brightness : %d + Contrast : %d + AutoRotate : %v + AutoSplitDoublePage : %v + NoBlankPage : %v + Manga : %v + HasCover : %v + AddPanelView : %v + LimitMb : %s + StripFirstDirectoryFromToc: %v`, profileDesc, o.Quality, o.Crop, @@ -150,6 +153,7 @@ func (o *Options) ShowDefault() string { o.HasCover, o.AddPanelView, limitmb, + o.StripFirstDirectoryFromToc, ) } diff --git a/internal/epub/core.go b/internal/epub/core.go index bcb1582..c23a008 100644 --- a/internal/epub/core.go +++ b/internal/epub/core.go @@ -1,8 +1,10 @@ package epub import ( + "encoding/xml" "fmt" "image/color" + "os" "path/filepath" "regexp" "strings" @@ -10,6 +12,7 @@ import ( "time" "github.com/gofrs/uuid" + "gopkg.in/yaml.v3" ) type ImageOptions struct { @@ -31,11 +34,13 @@ type ImageOptions struct { } type EpubOptions struct { - Input string - Output string - Title string - Author string - LimitMb int + Input string + Output string + Title string + Author string + LimitMb int + StripFirstDirectoryFromToc bool + Dry bool *ImageOptions } @@ -91,7 +96,8 @@ func (e *ePub) render(templateString string, data any) string { } func (e *ePub) getParts() ([]*epubPart, error) { - images, err := LoadImages(e.Input, e.ImageOptions) + images, err := LoadImages(e.Input, e.ImageOptions, e.Dry) + if err != nil { return nil, err } @@ -101,6 +107,15 @@ func (e *ePub) getParts() ([]*epubPart, error) { if e.HasCover { images = images[1:] } + + if e.Dry { + parts = append(parts, &epubPart{ + Cover: cover, + Images: images, + }) + return parts, nil + } + maxSize := uint64(e.LimitMb * 1024 * 1024) xhtmlSize := uint64(1024) @@ -139,6 +154,42 @@ func (e *ePub) getParts() ([]*epubPart, error) { return parts, nil } +func (e *ePub) getToc(images []*Image) *TocChildren { + paths := map[string]*TocPart{ + ".": {}, + } + for _, img := range images { + currentPath := "." + for _, path := range strings.Split(img.Path, string(filepath.Separator)) { + parentPath := currentPath + currentPath = filepath.Join(currentPath, path) + if _, ok := paths[currentPath]; ok { + continue + } + part := &TocPart{ + Title: TocTitle{ + Value: path, + Link: fmt.Sprintf("Text/%d_p%d.xhtml", img.Id, img.Part), + }, + } + paths[currentPath] = part + if paths[parentPath].Children == nil { + paths[parentPath].Children = &TocChildren{} + } + paths[parentPath].Children.Tags = append(paths[parentPath].Children.Tags, part) + } + } + + children := paths["."].Children + + if children != nil && e.StripFirstDirectoryFromToc && len(children.Tags) == 1 { + children = children.Tags[0].Children + } + + return children + +} + func (e *ePub) Write() error { type zipContent struct { Name string @@ -149,6 +200,16 @@ func (e *ePub) Write() error { if err != nil { return err } + + if e.Dry { + tocChildren := e.getToc(epubParts[0].Images) + fmt.Fprintf(os.Stderr, "TOC:\n- %s\n", e.Title) + if tocChildren != nil { + yaml.NewEncoder(os.Stderr).Encode(tocChildren) + } + return nil + } + totalParts := len(epubParts) bar := NewBar(totalParts, "Writing Part", 2, 2) @@ -170,6 +231,16 @@ func (e *ePub) Write() error { if totalParts > 1 { title = fmt.Sprintf("%s [%d/%d]", title, i+1, totalParts) } + + tocChildren := e.getToc(part.Images) + toc := []byte{} + if tocChildren != nil { + toc, err = xml.MarshalIndent(tocChildren.Tags, " ", " ") + if err != nil { + return err + } + } + content := []zipContent{ {"META-INF/container.xml", containerTmpl}, {"OEBPS/content.opf", e.render(contentTmpl, map[string]any{ @@ -180,8 +251,14 @@ func (e *ePub) Write() error { "Part": i + 1, "Total": totalParts, })}, - {"OEBPS/toc.ncx", e.render(tocTmpl, map[string]any{"Info": e})}, - {"OEBPS/nav.xhtml", e.render(navTmpl, map[string]any{"Info": e})}, + {"OEBPS/toc.ncx", e.render(tocTmpl, map[string]any{ + "Info": e, + "Title": title, + })}, + {"OEBPS/nav.xhtml", e.render(navTmpl, map[string]any{ + "Title": title, + "TOC": string(toc), + })}, {"OEBPS/Text/style.css", styleTmpl}, {"OEBPS/Text/part.xhtml", e.render(partTmpl, map[string]any{ "Info": e, diff --git a/internal/epub/image_processing.go b/internal/epub/image_processing.go index 6a60e51..678d427 100644 --- a/internal/epub/image_processing.go +++ b/internal/epub/image_processing.go @@ -31,11 +31,13 @@ type Image struct { Height int IsCover bool NeedSpace bool + Path string } type imageTask struct { Id int Reader io.ReadCloser + Path string Filename string } @@ -90,7 +92,7 @@ BOTTOM: return imgArea } -func LoadImages(path string, options *ImageOptions) ([]*Image, error) { +func LoadImages(path string, options *ImageOptions, dry bool) ([]*Image, error) { images := make([]*Image, 0) fi, err := os.Stat(path) @@ -121,6 +123,22 @@ func LoadImages(path string, options *ImageOptions) ([]*Image, error) { return nil, err } + if dry { + for img := range imageInput { + images = append(images, &Image{ + img.Id, + 0, + nil, + 0, + 0, + false, + false, // NeedSpace reajust during parts computation + img.Path, + }) + } + return images, nil + } + imageOutput := make(chan *Image) // processing @@ -162,6 +180,7 @@ func LoadImages(path string, options *ImageOptions) ([]*Image, error) { dst.Bounds().Dy(), img.Id == 0, false, + img.Path, } // Auto split double page @@ -185,6 +204,7 @@ func LoadImages(path string, options *ImageOptions) ([]*Image, error) { dst.Bounds().Dy(), false, false, // NeedSpace reajust during parts computation + img.Path, } } } @@ -235,6 +255,7 @@ func isSupportedImage(path string) bool { func loadDir(input string) (int, chan *imageTask, error) { images := make([]string, 0) + input = filepath.Clean(input) err := filepath.WalkDir(input, func(path string, d fs.DirEntry, err error) error { if err != nil { return err @@ -263,9 +284,16 @@ func loadDir(input string) (int, chan *imageTask, error) { fmt.Fprintln(os.Stderr, err) os.Exit(1) } + p := filepath.Dir(img) + if p == input { + p = "" + } else { + p = p[len(input)+1:] + } output <- &imageTask{ Id: i, Reader: f, + Path: p, Filename: img, } } @@ -306,6 +334,7 @@ func loadCbz(input string) (int, chan *imageTask, error) { output <- &imageTask{ Id: i, Reader: f, + Path: filepath.Dir(filepath.Clean(img.Name)), Filename: img.Name, } } @@ -374,6 +403,7 @@ func loadCbr(input string) (int, chan *imageTask, error) { output <- &imageTask{ Id: idx, Reader: io.NopCloser(b), + Path: filepath.Dir(filepath.Clean(f.Name)), Filename: f.Name, } } @@ -409,6 +439,7 @@ func loadPdf(input string) (int, chan *imageTask, error) { output <- &imageTask{ Id: i, Reader: io.NopCloser(b), + Path: "/", Filename: fmt.Sprintf("page %d", i+1), } } diff --git a/internal/epub/templates/nav.xhtml.tmpl b/internal/epub/templates/nav.xhtml.tmpl index 0492d4f..5403194 100644 --- a/internal/epub/templates/nav.xhtml.tmpl +++ b/internal/epub/templates/nav.xhtml.tmpl @@ -2,18 +2,15 @@ - {{ .Info.Title }} + {{ .Title }} -