diff --git a/README.md b/README.md index ff788eb..901b916 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ Options: AddPanelView : false LimitMb : 200 Mb StripFirstDirectoryFromToc: true + SortPathMode : path=alphanum, file=alpha TOC: - mymanga @@ -134,6 +135,58 @@ TOC: - Chapter 2 ``` +## Dry verbose + +You can choose different way to sort path and files, depending of your source. You can preview the sorted result with the option `dry-verbose` associated with `dry`. + +The option `sort` allow you to change the sorting order. + +``` +$ go-comic-converter -input ~/Downloads/mymanga.cbr -profile KS -auto -manga -limitmb 200 -dry -dry-verbose -sort 2 +Go Comic Converter + +Options: + Input : ~/Downloads/mymanga.cbr + Output : ~/Downloads/mymanga.epub + Author : GO Comic Converter + Title : mymanga + Workers : 8 + Profile : KS - Kindle Scribe - 1860x2480 - 16 levels of gray + Quality : 85 + Crop : true + Brightness : 0 + Contrast : 0 + AutoRotate : true + AutoSplitDoublePage : true + NoBlankPage : false + Manga : true + HasCover : true + AddPanelView : false + LimitMb : 200 Mb + StripFirstDirectoryFromToc: true + SortPathMode : path=alphanum, file=alphanum + +TOC: +- mymanga +- Chapter 1 +- Chapter 2 +- Chapter 3 + +Files: +- Chapter 1 + - img1.jpg + - img2.jpg + - img10.jpg +- Chapter 2 + - img01.jpg + - img02.jpg + - img03.jpg +- Chapter 3 + - img1.jpg + - img2-3.jpg + - img4.jpg +``` + ## Change default settings ### Show current default option @@ -156,6 +209,7 @@ Options: AddPanelView : false LimitMb : nolimit StripFirstDirectoryFromToc: false + SortPathMode : path=alphanum, file=alpha ``` ### Change default settings @@ -178,6 +232,7 @@ Options: AddPanelView : false LimitMb : 200 Mb StripFirstDirectoryFromToc: false + SortPathMode : path=alphanum, file=alpha Saving to /Users/vincent/.go-comic-converter.yaml ``` @@ -202,43 +257,11 @@ Options: AddPanelView : false LimitMb : 200 Mb StripFirstDirectoryFromToc: false + SortPathMode : path=alphanum, file=alpha Saving to /Users/vincent/.go-comic-converter.yaml ``` -### Check -You can test the command dry above like -``` -$ go-comic-converter -input ~/Downloads/mymanga.cbr -dry - -Go Comic Converter - -Options: - Input : ~/Downloads/mymanga.cbr - Output : ~/Downloads/mymanga.epub - Author : GO Comic Converter - Title : mymanga - Workers : 8 - Profile : KS - Kindle Scribe - 1860x2480 - 16 levels of gray - Quality : 85 - Crop : true - Brightness : 0 - Contrast : 0 - AutoRotate : true - AutoSplitDoublePage : true - NoBlankPage : false - Manga : false - HasCover : true - AddPanelView : false - LimitMb : 200 Mb - StripFirstDirectoryFromToc: false - -TOC: -- mymanga -- Chapter 1 -- Chapter 2 -``` - ### Reset default To reset all value to default: @@ -283,6 +306,8 @@ Output: Number of workers -dry Dry run to show all options + -dry-verbose + Display also sorted files after the TOC Config: -profile string @@ -337,6 +362,11 @@ Config: Limit size of the ePub: Default nolimit (0), Minimum 20 -strip Strip first directory from the TOC if only 1 + -sort int (default 1) + Sort path mode + 0 = alpha for path and file + 1 = alphanum for path and alpha for file + 2 = alphanum for path and file Default config: -show diff --git a/internal/converter/core.go b/internal/converter/core.go index e02cdca..0a01c78 100644 --- a/internal/converter/core.go +++ b/internal/converter/core.go @@ -81,6 +81,7 @@ func (c *Converter) InitParse() { c.AddStringParam(&c.Options.Title, "title", "", "Title of the epub") c.AddIntParam(&c.Options.Workers, "workers", runtime.NumCPU(), "Number of workers") c.AddBoolParam(&c.Options.Dry, "dry", false, "Dry run to show all options") + c.AddBoolParam(&c.Options.DryVerbose, "dry-verbose", false, "Display also sorted files after the TOC") c.AddSection("Config") c.AddStringParam(&c.Options.Profile, "profile", c.Options.Profile, fmt.Sprintf("Profile to use: \n%s", c.Options.AvailableProfiles())) @@ -97,6 +98,7 @@ func (c *Converter) InitParse() { 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.AddIntParam(&c.Options.SortPathMode, "sort", c.Options.SortPathMode, "Sort path mode\n0 = alpha for path and file\n1 = alphanum for path and alpha for file\n2 = alphanum for path and file") c.AddSection("Default config") c.AddBoolParam(&c.Options.Show, "show", false, "Show your default parameters") @@ -260,6 +262,10 @@ func (c *Converter) Validate() error { return errors.New("contrast should be between -100 and 100") } + if c.Options.SortPathMode < 0 || c.Options.SortPathMode > 2 { + return errors.New("sort should be 0, 1 or 2") + } + return nil } diff --git a/internal/converter/options/core.go b/internal/converter/options/core.go index f5d27c9..66e5dac 100644 --- a/internal/converter/options/core.go +++ b/internal/converter/options/core.go @@ -11,13 +11,14 @@ import ( type Options struct { // Output - Input string `yaml:"-"` - Output string `yaml:"-"` - Author string `yaml:"-"` - Title string `yaml:"-"` - Auto bool `yaml:"-"` - Workers int `yaml:"-"` - Dry bool `yaml:"-"` + Input string `yaml:"-"` + Output string `yaml:"-"` + Author string `yaml:"-"` + Title string `yaml:"-"` + Auto bool `yaml:"-"` + Workers int `yaml:"-"` + Dry bool `yaml:"-"` + DryVerbose bool `yaml:"-"` // Config Profile string `yaml:"profile"` @@ -33,6 +34,7 @@ type Options struct { AddPanelView bool `yaml:"add_panel_view"` LimitMb int `yaml:"limit_mb"` StripFirstDirectoryFromToc bool `yaml:"strip_first_directory_from_toc"` + SortPathMode int `yaml:"sort_path_mode"` // Default Config Show bool `yaml:"-"` @@ -62,6 +64,7 @@ func New() *Options { AddPanelView: false, LimitMb: 0, StripFirstDirectoryFromToc: false, + SortPathMode: 1, profiles: profiles.New(), } } @@ -127,6 +130,16 @@ func (o *Options) ShowDefault() string { limitmb = fmt.Sprintf("%d Mb", o.LimitMb) } + sortpathmode := "" + switch o.SortPathMode { + case 0: + sortpathmode = "path=alpha, file=alpha" + case 1: + sortpathmode = "path=alphanum, file=alpha" + case 2: + sortpathmode = "path=alphanum, file=alphanum" + } + return fmt.Sprintf(` Profile : %s Quality : %d @@ -140,7 +153,8 @@ func (o *Options) ShowDefault() string { HasCover : %v AddPanelView : %v LimitMb : %s - StripFirstDirectoryFromToc: %v`, + StripFirstDirectoryFromToc: %v + SortPathMode : %s`, profileDesc, o.Quality, o.Crop, @@ -154,6 +168,7 @@ func (o *Options) ShowDefault() string { o.AddPanelView, limitmb, o.StripFirstDirectoryFromToc, + sortpathmode, ) } diff --git a/internal/epub/core.go b/internal/epub/core.go index f91e140..558ed46 100644 --- a/internal/epub/core.go +++ b/internal/epub/core.go @@ -13,7 +13,6 @@ import ( "time" "github.com/gofrs/uuid" - "gopkg.in/yaml.v3" ) type ImageOptions struct { @@ -42,6 +41,8 @@ type EpubOptions struct { LimitMb int StripFirstDirectoryFromToc bool Dry bool + DryVerbose bool + SortPathMode int *ImageOptions } @@ -97,7 +98,7 @@ func (e *ePub) render(templateString string, data any) string { } func (e *ePub) getParts() ([]*epubPart, error) { - images, err := LoadImages(e.Input, e.ImageOptions, e.Dry) + images, err := e.LoadImages() if err != nil { return nil, err @@ -201,6 +202,32 @@ func (e *ePub) getToc(images []*Image) *TocChildren { } +func (e *ePub) getTree(images []*Image, skip_files bool) string { + r := []string{} + + c := []string{} + for _, img := range images { + n := []string{} + if len(img.Path) > 0 { + n = strings.Split(filepath.Clean(img.Path), string(filepath.Separator)) + } + for l, p := range n { + f := fmt.Sprintf("%%%ds- %%s", l*2) + if len(c) > l && c[l] == p { + continue + } + r = append(r, fmt.Sprintf(f, "", p)) + } + c = n + if skip_files { + continue + } + f := fmt.Sprintf("%%%ds- %%s", len(n)*2+2) + r = append(r, fmt.Sprintf(f, "", img.Name)) + } + return strings.Join(r, "\n") +} + func (e *ePub) Write() error { type zipContent struct { Name string @@ -213,10 +240,9 @@ func (e *ePub) Write() error { } 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) + fmt.Fprintf(os.Stderr, "TOC:\n- %s\n%s\n", e.Title, e.getTree(epubParts[0].Images, true)) + if e.DryVerbose { + fmt.Fprintf(os.Stderr, "\nFiles:\n%s\n", e.getTree(epubParts[0].Images, false)) } return nil } diff --git a/internal/epub/image_processing.go b/internal/epub/image_processing.go index 8365530..80c1c95 100644 --- a/internal/epub/image_processing.go +++ b/internal/epub/image_processing.go @@ -34,13 +34,14 @@ type Image struct { IsCover bool NeedSpace bool Path string + Name string } type imageTask struct { - Id int - Reader io.ReadCloser - Path string - Filename string + Id int + Reader io.ReadCloser + Path string + Name string } func colorIsBlank(c color.Color) bool { @@ -94,10 +95,10 @@ BOTTOM: return imgArea } -func LoadImages(path string, options *ImageOptions, dry bool) ([]*Image, error) { +func (e *ePub) LoadImages() ([]*Image, error) { images := make([]*Image, 0) - fi, err := os.Stat(path) + fi, err := os.Stat(e.Input) if err != nil { return nil, err } @@ -108,15 +109,15 @@ func LoadImages(path string, options *ImageOptions, dry bool) ([]*Image, error) ) if fi.IsDir() { - imageCount, imageInput, err = loadDir(path) + imageCount, imageInput, err = loadDir(e.Input, e.SortPathMode) } else { - switch ext := strings.ToLower(filepath.Ext(path)); ext { + switch ext := strings.ToLower(filepath.Ext(e.Input)); ext { case ".cbz", ".zip": - imageCount, imageInput, err = loadCbz(path) + imageCount, imageInput, err = loadCbz(e.Input, e.SortPathMode) case ".cbr", ".rar": - imageCount, imageInput, err = loadCbr(path) + imageCount, imageInput, err = loadCbr(e.Input, e.SortPathMode) case ".pdf": - imageCount, imageInput, err = loadPdf(path) + imageCount, imageInput, err = loadPdf(e.Input) default: err = fmt.Errorf("unknown file format (%s): support .cbz, .zip, .cbr, .rar, .pdf", ext) } @@ -125,17 +126,19 @@ func LoadImages(path string, options *ImageOptions, dry bool) ([]*Image, error) return nil, err } - if dry { + if e.Dry { for img := range imageInput { + img.Reader.Close() images = append(images, &Image{ - img.Id, - 0, - nil, - 0, - 0, - false, - false, // NeedSpace reajust during parts computation - img.Path, + Id: img.Id, + Part: 0, + Data: nil, + Width: 0, + Height: 0, + IsCover: false, + NeedSpace: false, // NeedSpace reajust during parts computation + Path: img.Path, + Name: img.Name, }) } @@ -148,7 +151,7 @@ func LoadImages(path string, options *ImageOptions, dry bool) ([]*Image, error) bar := NewBar(imageCount, "Processing", 1, 2) wg := &sync.WaitGroup{} - for i := 0; i < options.Workers; i++ { + for i := 0; i < e.ImageOptions.Workers; i++ { wg.Add(1) go func() { defer wg.Done() @@ -156,58 +159,61 @@ func LoadImages(path string, options *ImageOptions, dry bool) ([]*Image, error) for img := range imageInput { // Decode image src, _, err := image.Decode(img.Reader) + img.Reader.Close() if err != nil { bar.Clear() - fmt.Fprintf(os.Stderr, "error processing image %s: %s\n", img.Filename, err) + fmt.Fprintf(os.Stderr, "error processing image %s%s: %s\n", img.Path, img.Name, err) os.Exit(1) } - if options.Crop { + if e.ImageOptions.Crop { g := gift.New(gift.Crop(findMarging(src))) newSrc := image.NewNRGBA(g.Bounds(src.Bounds())) g.Draw(newSrc, src) src = newSrc } - g := NewGift(options) + g := NewGift(e.ImageOptions) // Convert image - dst := image.NewPaletted(g.Bounds(src.Bounds()), options.Palette) + dst := image.NewPaletted(g.Bounds(src.Bounds()), e.ImageOptions.Palette) g.Draw(dst, src) imageOutput <- &Image{ - img.Id, - 0, - newImageData(img.Id, 0, dst, options.Quality), - dst.Bounds().Dx(), - dst.Bounds().Dy(), - img.Id == 0, - false, - img.Path, + Id: img.Id, + Part: 0, + Data: newImageData(img.Id, 0, dst, e.ImageOptions.Quality), + Width: dst.Bounds().Dx(), + Height: dst.Bounds().Dy(), + IsCover: img.Id == 0, + NeedSpace: false, + Path: img.Path, + Name: img.Name, } // Auto split double page // Except for cover // Only if the src image have width > height and is bigger than the view - if (!options.HasCover || img.Id > 0) && - options.AutoSplitDoublePage && + if (!e.ImageOptions.HasCover || img.Id > 0) && + e.ImageOptions.AutoSplitDoublePage && src.Bounds().Dx() > src.Bounds().Dy() && - src.Bounds().Dx() > options.ViewHeight && - src.Bounds().Dy() > options.ViewWidth { - gifts := NewGiftSplitDoublePage(options) + src.Bounds().Dx() > e.ImageOptions.ViewHeight && + src.Bounds().Dy() > e.ImageOptions.ViewWidth { + gifts := NewGiftSplitDoublePage(e.ImageOptions) for i, g := range gifts { part := i + 1 - dst := image.NewPaletted(g.Bounds(src.Bounds()), options.Palette) + dst := image.NewPaletted(g.Bounds(src.Bounds()), e.ImageOptions.Palette) g.Draw(dst, src) imageOutput <- &Image{ - img.Id, - part, - newImageData(img.Id, part, dst, options.Quality), - dst.Bounds().Dx(), - dst.Bounds().Dy(), - false, - false, // NeedSpace reajust during parts computation - img.Path, + Id: img.Id, + Part: part, + Data: newImageData(img.Id, part, dst, e.ImageOptions.Quality), + Width: dst.Bounds().Dx(), + Height: dst.Bounds().Dy(), + IsCover: false, + NeedSpace: false, // NeedSpace reajust during parts computation + Path: img.Path, + Name: img.Name, } } } @@ -221,7 +227,7 @@ func LoadImages(path string, options *ImageOptions, dry bool) ([]*Image, error) }() for image := range imageOutput { - if !(options.NoBlankPage && image.Width == 1 && image.Height == 1) { + if !(e.ImageOptions.NoBlankPage && image.Width == 1 && image.Height == 1) { images = append(images, image) } if image.Part == 0 { @@ -247,7 +253,7 @@ func isSupportedImage(path string) bool { return false } -func loadDir(input string) (int, chan *imageTask, error) { +func loadDir(input string, sortpathmode int) (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 { @@ -267,7 +273,7 @@ func loadDir(input string) (int, chan *imageTask, error) { return 0, nil, fmt.Errorf("image not found") } - sort.Sort(sortpath.By(images)) + sort.Sort(sortpath.By(images, sortpathmode)) output := make(chan *imageTask) go func() { @@ -278,24 +284,25 @@ func loadDir(input string) (int, chan *imageTask, error) { fmt.Fprintln(os.Stderr, err) os.Exit(1) } - p := filepath.Dir(img) + + p, fn := filepath.Split(img) if p == input { p = "" } else { p = p[len(input)+1:] } output <- &imageTask{ - Id: i, - Reader: f, - Path: p, - Filename: img, + Id: i, + Reader: f, + Path: p, + Name: fn, } } }() return len(images), output, nil } -func loadCbz(input string) (int, chan *imageTask, error) { +func loadCbz(input string, sortpathmode int) (int, chan *imageTask, error) { r, err := zip.OpenReader(input) if err != nil { return 0, nil, err @@ -316,7 +323,7 @@ func loadCbz(input string) (int, chan *imageTask, error) { for _, img := range images { names = append(names, img.Name) } - sort.Sort(sortpath.By(names)) + sort.Sort(sortpath.By(names, sortpathmode)) indexedNames := make(map[string]int) for i, name := range names { @@ -332,18 +339,19 @@ func loadCbz(input string) (int, chan *imageTask, error) { fmt.Fprintln(os.Stderr, err) os.Exit(1) } + p, fn := filepath.Split(filepath.Clean(img.Name)) output <- &imageTask{ - Id: indexedNames[img.Name], - Reader: f, - Path: filepath.Dir(filepath.Clean(img.Name)), - Filename: img.Name, + Id: indexedNames[img.Name], + Reader: f, + Path: p, + Name: fn, } } }() return len(images), output, nil } -func loadCbr(input string) (int, chan *imageTask, error) { +func loadCbr(input string, sortpathmode int) (int, chan *imageTask, error) { // listing and indexing rl, err := rardecode.OpenReader(input, "") if err != nil { @@ -372,7 +380,7 @@ func loadCbr(input string) (int, chan *imageTask, error) { return 0, nil, fmt.Errorf("no images found") } - sort.Sort(sortpath.By(names)) + sort.Sort(sortpath.By(names, sortpathmode)) indexedNames := make(map[string]int) for i, name := range names { @@ -401,11 +409,13 @@ func loadCbr(input string) (int, chan *imageTask, error) { b := bytes.NewBuffer([]byte{}) io.Copy(b, r) + p, fn := filepath.Split(filepath.Clean(f.Name)) + output <- &imageTask{ - Id: idx, - Reader: io.NopCloser(b), - Path: filepath.Dir(filepath.Clean(f.Name)), - Filename: f.Name, + Id: idx, + Reader: io.NopCloser(b), + Path: p, + Name: fn, } } } @@ -421,6 +431,7 @@ func loadPdf(input string) (int, chan *imageTask, error) { } nbPages := len(pdf.Pages()) + pageFmt := fmt.Sprintf("page %%0%dd", len(fmt.Sprintf("%d", nbPages))) output := make(chan *imageTask) go func() { defer close(output) @@ -438,10 +449,10 @@ func loadPdf(input string) (int, chan *imageTask, error) { } output <- &imageTask{ - Id: i, - Reader: io.NopCloser(b), - Path: "/", - Filename: fmt.Sprintf("page %d", i+1), + Id: i, + Reader: io.NopCloser(b), + Path: "", + Name: fmt.Sprintf(pageFmt, i+1), } } }() diff --git a/internal/epub/sortpath/core.go b/internal/epub/sortpath/core.go index fa0042c..e89f616 100644 --- a/internal/epub/sortpath/core.go +++ b/internal/epub/sortpath/core.go @@ -7,14 +7,19 @@ import ( "strings" ) -var split_path_regex = regexp.MustCompile(`^(.*?)(\d+(?:\.\d+)?)$`) +// Strings follow with numbers like: s1, s1.2, s2-3, ... +var split_path_regex = regexp.MustCompile(`^(.*?)(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?$`) type part struct { - name string - number float64 + fullname string + name string + number float64 } func (a part) Compare(b part) float64 { + if a.number == 0 || b.number == 0 { + return float64(strings.Compare(a.fullname, b.fullname)) + } if a.name == b.name { return a.number - b.number } else { @@ -25,16 +30,19 @@ func (a part) Compare(b part) float64 { func parsePart(p string) part { r := split_path_regex.FindStringSubmatch(p) if len(r) == 0 { - return part{p, 0} + return part{p, p, 0} } n, err := strconv.ParseFloat(r[2], 64) if err != nil { - return part{p, 0} + return part{p, p, 0} } - return part{r[1], n} + return part{p, r[1], n} } -func parse(filename string) []part { +// mode=0 alpha for path and file +// mode=1 alphanum for path and alpha for file +// mode=2 alphanum for path and file +func parse(filename string, mode int) []part { pathname, name := filepath.Split(strings.ToLower(filename)) pathname = strings.TrimSuffix(pathname, string(filepath.Separator)) ext := filepath.Ext(name) @@ -42,9 +50,17 @@ func parse(filename string) []part { f := []part{} for _, p := range strings.Split(pathname, string(filepath.Separator)) { - f = append(f, parsePart(p)) + if mode > 0 { // alphanum for path + f = append(f, parsePart(p)) + } else { + f = append(f, part{p, p, 0}) + } + } + if mode == 2 { // alphanum for file + f = append(f, parsePart(name)) + } else { + f = append(f, part{name, name, 0}) } - f = append(f, parsePart(name)) return f } @@ -74,10 +90,10 @@ func (b by) Swap(i, j int) { b.paths[i], b.paths[j] = b.paths[j], b.paths[i] } -func By(filenames []string) by { +func By(filenames []string, mode int) by { p := [][]part{} for _, filename := range filenames { - p = append(p, parse(filename)) + p = append(p, parse(filename, mode)) } return by{filenames, p} } diff --git a/internal/epub/toc.go b/internal/epub/toc.go index 8f9758b..7bda2ef 100644 --- a/internal/epub/toc.go +++ b/internal/epub/toc.go @@ -15,20 +15,8 @@ type TocChildren struct { Tags []*TocPart } -func (t *TocChildren) MarshalYAML() (any, error) { - return t.Tags, nil -} - type TocPart struct { XMLName xml.Name `xml:"li"` Title TocTitle Children *TocChildren `xml:",omitempty"` } - -func (t *TocPart) MarshalYAML() (any, error) { - if t.Children == nil { - return t.Title.Value, nil - } else { - return map[string]any{t.Title.Value: t.Children}, nil - } -} diff --git a/main.go b/main.go index 225dd86..1354150 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,8 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s Author: cmd.Options.Author, StripFirstDirectoryFromToc: cmd.Options.StripFirstDirectoryFromToc, Dry: cmd.Options.Dry, + DryVerbose: cmd.Options.DryVerbose, + SortPathMode: cmd.Options.SortPathMode, ImageOptions: &epub.ImageOptions{ ViewWidth: profile.Width, ViewHeight: profile.Height,