improve dry and sort

dry verbose list files in sorted order
sort support 3 mode
	mode 0: path=alpha, file=alpha
	mode 1: path=alphanum, file=alpha
	mode 2: path=alphanum, file=alphanum
improve alphanum sort, supporting double page like "p51-52"
This commit is contained in:
Celogeek 2023-04-09 18:21:29 +02:00
parent 105c21f8d2
commit fbb48830d4
Signed by: celogeek
SSH Key Fingerprint: SHA256:njNJLzoLQdbV9PC6ehcruRb0QnEgxABoCYZ+0+aUIYc
8 changed files with 235 additions and 141 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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,
)
}

View File

@ -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
}

View File

@ -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),
}
}
}()

View File

@ -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}
}

View File

@ -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
}
}

View File

@ -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,