Compare commits

..

10 Commits

Author SHA1 Message Date
2990367e2f
fix export type 2024-02-09 20:20:15 +02:00
23388b5f30
fix defer in a loop 2024-02-09 18:52:57 +02:00
ea85d820eb
fix grammar 2024-02-09 18:36:10 +02:00
139acd864b
fix naming 2024-02-09 10:46:20 +02:00
07ba350d82
fix comments 2024-02-08 21:05:27 +02:00
84a7666b13
fix overlapping name 2024-02-08 20:56:06 +02:00
39d746bd45
update deps 2024-02-08 20:55:48 +02:00
ca73ac9682
rename splitted to split 2024-02-08 20:49:35 +02:00
b6db09aa9e
fix typo & grammar 2024-02-08 20:44:00 +02:00
2d15bc43c2
update deps 2024-02-06 17:38:41 +02:00
28 changed files with 374 additions and 357 deletions

View File

@ -2,7 +2,7 @@
Convert CBZ/CBR/Dir into EPUB for e-reader devices (Kindle Devices, ...) Convert CBZ/CBR/Dir into EPUB for e-reader devices (Kindle Devices, ...)
My goal is to make a simple, crossplatform, and fast tool to convert comics into EPUB. My goal is to make a simple, cross-platform, and fast tool to convert comics into EPUB.
EPUB is now support by Amazon through [SendToKindle](https://www.amazon.com/gp/sendtokindle/), by Email or by using the App. So I've made it simple to support the size limit constraint of those services. EPUB is now support by Amazon through [SendToKindle](https://www.amazon.com/gp/sendtokindle/), by Email or by using the App. So I've made it simple to support the size limit constraint of those services.
@ -16,7 +16,7 @@ EPUB is now support by Amazon through [SendToKindle](https://www.amazon.com/gp/s
- Auto contrast - Auto contrast
- Auto rotate (if reader mainly read on portrait) - Auto rotate (if reader mainly read on portrait)
- Auto split double page (for easy read on portrait) - Auto split double page (for easy read on portrait)
- Keep double page if splitted - Keep double page if split
- Remove blank image (empty image is removed) - Remove blank image (empty image is removed)
- Manga or Normal mode - Manga or Normal mode
- Support cover page or not (first page will be taken in that case) - Support cover page or not (first page will be taken in that case)
@ -91,7 +91,7 @@ Convert every supported image files found in the input directory:
$ go-comic-converter -profile SR -input ~/Download/MyComic $ go-comic-converter -profile SR -input ~/Download/MyComic
``` ```
By default it will output: ~/Download/MyComic.epub By default, it will output: ~/Download/MyComic.epub
## Convert CBZ, ZIP, CBR, RAR, PDF ## Convert CBZ, ZIP, CBR, RAR, PDF
@ -101,7 +101,7 @@ Convert every supported image files found in the input directory:
$ go-comic-converter -profile SR -input ~/Download/MyComic.[CBZ,ZIP,CBR,RAR,PDF] $ go-comic-converter -profile SR -input ~/Download/MyComic.[CBZ,ZIP,CBR,RAR,PDF]
``` ```
By default it will output: ~/Download/MyComic.epub By default, it will output: ~/Download/MyComic.epub
## Convert with size limit ## Convert with size limit
@ -130,7 +130,7 @@ If the total is above 1, then the title of the EPUB include:
## Dry run ## Dry run
If you want to preview what will be set during the convertion without running the conversion, then you can use the `-dry` option. If you want to preview what will be set during the conversion without running the conversion, then you can use the `-dry` option.
``` ```
$ go-comic-converter -input ~/Downloads/mymanga.cbr -profile SR -auto -manga -limitmb 200 -dry $ go-comic-converter -input ~/Downloads/mymanga.cbr -profile SR -auto -manga -limitmb 200 -dry
@ -176,7 +176,7 @@ TOC:
## Dry verbose ## 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`. You can choose different way to sort path and files, depending on 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. The option `sort` allow you to change the sorting order.
@ -418,7 +418,7 @@ Explanation:
- `-autocontrast`: automatically improve contrast - `-autocontrast`: automatically improve contrast
- `-manga`: manga mode, read right to left - `-manga`: manga mode, read right to left
- `-limitmb 200`: size limit to 200MB allowing upload from SendToKindle website - `-limitmb 200`: size limit to 200MB allowing upload from SendToKindle website
- `-strip`: remove first level if alone on TOC, as offen comics include a main directory with the title - `-strip`: remove first level if alone on TOC, as often comics include a main directory with the title
- `aspect-ratio`: ensure aspect ratio is 1:1.6, best for kindle devices. - `aspect-ratio`: ensure aspect ratio is 1:1.6, best for kindle devices.
# Help # Help

8
go.mod
View File

@ -21,9 +21,9 @@ require (
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/stretchr/testify v1.8.4 // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.16.0 // indirect golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.16.0 // indirect golang.org/x/term v0.17.0 // indirect
) )

15
go.sum
View File

@ -29,8 +29,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/raff/pdfreader v0.0.0-20220308062436-033e8ac577f0 h1:fuFvfwIc+cpySYurvDNTs5LIHXP9Cj3reVRplj9Whv4= github.com/raff/pdfreader v0.0.0-20220308062436-033e8ac577f0 h1:fuFvfwIc+cpySYurvDNTs5LIHXP9Cj3reVRplj9Whv4=
github.com/raff/pdfreader v0.0.0-20220308062436-033e8ac577f0/go.mod h1:Ql3QqeGiYGlPOtYz+F/L7J27spqDcdH9LhDHOrrdsD4= github.com/raff/pdfreader v0.0.0-20220308062436-033e8ac577f0/go.mod h1:Ql3QqeGiYGlPOtYz+F/L7J27spqDcdH9LhDHOrrdsD4=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.14.1 h1:VD+MJPCr4s3wdhTc7OEJ/Z3dAeBzJ7yKH/P4lC5yRTI= github.com/schollz/progressbar/v3 v3.14.1 h1:VD+MJPCr4s3wdhTc7OEJ/Z3dAeBzJ7yKH/P4lC5yRTI=
github.com/schollz/progressbar/v3 v3.14.1/go.mod h1:Zc9xXneTzWXF81TGoqL71u0sBPjULtEHYtj/WVgVy8E= github.com/schollz/progressbar/v3 v3.14.1/go.mod h1:Zc9xXneTzWXF81TGoqL71u0sBPjULtEHYtj/WVgVy8E=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -41,15 +42,15 @@ github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -1,10 +1,8 @@
/* // Package converter Helper to parse and prepare options for go-comic-converter.
Converter Helper to parse and prepare options for go-comic-converter. //
// It uses goflag with additional feature:
It use goflag with additional feature: // - Keep original order
- Keep original order // - Support section
- Support section
*/
package converter package converter
import ( import (
@ -32,12 +30,12 @@ type Converter struct {
startAt time.Time startAt time.Time
} }
// Create a new parser // New Create a new parser
func New() *Converter { func New() *Converter {
options := options.New() o := options.New()
cmd := flag.NewFlagSet("go-comic-converter", flag.ExitOnError) cmd := flag.NewFlagSet("go-comic-converter", flag.ExitOnError)
conv := &Converter{ conv := &Converter{
Options: options, Options: o,
Cmd: cmd, Cmd: cmd,
order: make([]converterOrder, 0), order: make([]converterOrder, 0),
startAt: time.Now(), startAt: time.Now(),
@ -63,41 +61,41 @@ func New() *Converter {
return conv return conv
} }
// Load default options (config + default) // LoadConfig Load default options (config + default)
func (c *Converter) LoadConfig() error { func (c *Converter) LoadConfig() error {
return c.Options.LoadConfig() return c.Options.LoadConfig()
} }
// Create a new section of config // AddSection Create a new section of config
func (c *Converter) AddSection(section string) { func (c *Converter) AddSection(section string) {
c.order = append(c.order, converterOrderSection{value: section}) c.order = append(c.order, converterOrderSection{value: section})
} }
// Add a string parameter // AddStringParam Add a string parameter
func (c *Converter) AddStringParam(p *string, name string, value string, usage string) { func (c *Converter) AddStringParam(p *string, name string, value string, usage string) {
c.Cmd.StringVar(p, name, value, usage) c.Cmd.StringVar(p, name, value, usage)
c.order = append(c.order, converterOrderName{value: name, isString: true}) c.order = append(c.order, converterOrderName{value: name, isString: true})
} }
// Add an integer parameter // AddIntParam Add an integer parameter
func (c *Converter) AddIntParam(p *int, name string, value int, usage string) { func (c *Converter) AddIntParam(p *int, name string, value int, usage string) {
c.Cmd.IntVar(p, name, value, usage) c.Cmd.IntVar(p, name, value, usage)
c.order = append(c.order, converterOrderName{value: name}) c.order = append(c.order, converterOrderName{value: name})
} }
// Add an float parameter // AddFloatParam Add an float parameter
func (c *Converter) AddFloatParam(p *float64, name string, value float64, usage string) { func (c *Converter) AddFloatParam(p *float64, name string, value float64, usage string) {
c.Cmd.Float64Var(p, name, value, usage) c.Cmd.Float64Var(p, name, value, usage)
c.order = append(c.order, converterOrderName{value: name}) c.order = append(c.order, converterOrderName{value: name})
} }
// Add a boolean parameter // AddBoolParam Add a boolean parameter
func (c *Converter) AddBoolParam(p *bool, name string, value bool, usage string) { func (c *Converter) AddBoolParam(p *bool, name string, value bool, usage string) {
c.Cmd.BoolVar(p, name, value, usage) c.Cmd.BoolVar(p, name, value, usage)
c.order = append(c.order, converterOrderName{value: name}) c.order = append(c.order, converterOrderName{value: name})
} }
// Initialize the parser with all section and parameter. // InitParse Initialize the parser with all section and parameter.
func (c *Converter) InitParse() { func (c *Converter) InitParse() {
c.AddSection("Output") c.AddSection("Output")
c.AddStringParam(&c.Options.Input, "input", "", "Source of comic to convert: directory, cbz, zip, cbr, rar, pdf") c.AddStringParam(&c.Options.Input, "input", "", "Source of comic to convert: directory, cbz, zip, cbr, rar, pdf")
@ -115,25 +113,25 @@ func (c *Converter) InitParse() {
c.AddIntParam(&c.Options.CropRatioUp, "crop-ratio-up", c.Options.CropRatioUp, "Crop ratio up: ratio of pixels allow to be non blank while cutting on the top.") c.AddIntParam(&c.Options.CropRatioUp, "crop-ratio-up", c.Options.CropRatioUp, "Crop ratio up: ratio of pixels allow to be non blank while cutting on the top.")
c.AddIntParam(&c.Options.CropRatioRight, "crop-ratio-right", c.Options.CropRatioRight, "Crop ratio right: ratio of pixels allow to be non blank while cutting on the right.") c.AddIntParam(&c.Options.CropRatioRight, "crop-ratio-right", c.Options.CropRatioRight, "Crop ratio right: ratio of pixels allow to be non blank while cutting on the right.")
c.AddIntParam(&c.Options.CropRatioBottom, "crop-ratio-bottom", c.Options.CropRatioBottom, "Crop ratio bottom: ratio of pixels allow to be non blank while cutting on the bottom.") c.AddIntParam(&c.Options.CropRatioBottom, "crop-ratio-bottom", c.Options.CropRatioBottom, "Crop ratio bottom: ratio of pixels allow to be non blank while cutting on the bottom.")
c.AddIntParam(&c.Options.Brightness, "brightness", c.Options.Brightness, "Brightness readjustement: between -100 and 100, > 0 lighter, < 0 darker") c.AddIntParam(&c.Options.Brightness, "brightness", c.Options.Brightness, "Brightness readjustment: between -100 and 100, > 0 lighter, < 0 darker")
c.AddIntParam(&c.Options.Contrast, "contrast", c.Options.Contrast, "Contrast readjustement: between -100 and 100, > 0 more contrast, < 0 less contrast") c.AddIntParam(&c.Options.Contrast, "contrast", c.Options.Contrast, "Contrast readjustment: between -100 and 100, > 0 more contrast, < 0 less contrast")
c.AddBoolParam(&c.Options.AutoContrast, "autocontrast", c.Options.AutoContrast, "Improve contrast automatically") c.AddBoolParam(&c.Options.AutoContrast, "autocontrast", c.Options.AutoContrast, "Improve contrast automatically")
c.AddBoolParam(&c.Options.AutoRotate, "autorotate", c.Options.AutoRotate, "Auto Rotate page when width > height") c.AddBoolParam(&c.Options.AutoRotate, "autorotate", c.Options.AutoRotate, "Auto Rotate page when width > height")
c.AddBoolParam(&c.Options.AutoSplitDoublePage, "autosplitdoublepage", c.Options.AutoSplitDoublePage, "Auto Split double page when width > height") c.AddBoolParam(&c.Options.AutoSplitDoublePage, "autosplitdoublepage", c.Options.AutoSplitDoublePage, "Auto Split double page when width > height")
c.AddBoolParam(&c.Options.KeepDoublePageIfSplitted, "keepdoublepageifsplitted", c.Options.KeepDoublePageIfSplitted, "Keep the double page if splitted") c.AddBoolParam(&c.Options.KeepDoublePageIfSplit, "keepdoublepageifsplit", c.Options.KeepDoublePageIfSplit, "Keep the double page if split")
c.AddBoolParam(&c.Options.NoBlankImage, "noblankimage", c.Options.NoBlankImage, "Remove blank image") c.AddBoolParam(&c.Options.NoBlankImage, "noblankimage", c.Options.NoBlankImage, "Remove blank image")
c.AddBoolParam(&c.Options.Manga, "manga", c.Options.Manga, "Manga mode (right to left)") c.AddBoolParam(&c.Options.Manga, "manga", c.Options.Manga, "Manga mode (right to left)")
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.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.AddIntParam(&c.Options.LimitMb, "limitmb", c.Options.LimitMb, "Limit size of the EPUB: Default nolimit (0), Minimum 20") 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.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.AddIntParam(&c.Options.SortPathMode, "sort", c.Options.SortPathMode, "Sort path mode\n0 = alpha for path and file\n1 = alphanumeric for path and alpha for file\n2 = alphanumeric for path and file")
c.AddStringParam(&c.Options.ForegroundColor, "foreground-color", c.Options.ForegroundColor, "Foreground color in hexa format RGB. Black=000, White=FFF") c.AddStringParam(&c.Options.ForegroundColor, "foreground-color", c.Options.ForegroundColor, "Foreground color in hexadecimal format RGB. Black=000, White=FFF")
c.AddStringParam(&c.Options.BackgroundColor, "background-color", c.Options.BackgroundColor, "Background color in hexa format RGB. Black=000, White=FFF, Light Gray=DDD, Dark Gray=777") c.AddStringParam(&c.Options.BackgroundColor, "background-color", c.Options.BackgroundColor, "Background color in hexadecimal format RGB. Black=000, White=FFF, Light Gray=DDD, Dark Gray=777")
c.AddBoolParam(&c.Options.NoResize, "noresize", c.Options.NoResize, "Do not reduce image size if exceed device size") c.AddBoolParam(&c.Options.NoResize, "noresize", c.Options.NoResize, "Do not reduce image size if exceed device size")
c.AddStringParam(&c.Options.Format, "format", c.Options.Format, "Format of output images: jpeg (lossy), png (lossless)") c.AddStringParam(&c.Options.Format, "format", c.Options.Format, "Format of output images: jpeg (lossy), png (lossless)")
c.AddFloatParam(&c.Options.AspectRatio, "aspect-ratio", c.Options.AspectRatio, "Aspect ratio (height/width) of the output\n -1 = same as device\n 0 = same as source\n1.6 = amazon advice for kindle") c.AddFloatParam(&c.Options.AspectRatio, "aspect-ratio", c.Options.AspectRatio, "Aspect ratio (height/width) of the output\n -1 = same as device\n 0 = same as source\n1.6 = amazon advice for kindle")
c.AddBoolParam(&c.Options.PortraitOnly, "portrait-only", c.Options.PortraitOnly, "Portrait only: force orientation to portrait only.") c.AddBoolParam(&c.Options.PortraitOnly, "portrait-only", c.Options.PortraitOnly, "Portrait only: force orientation to portrait only.")
c.AddIntParam(&c.Options.TitlePage, "titlepage", c.Options.TitlePage, "Title page\n0 = never\n1 = always\n2 = only if epub is splitted") c.AddIntParam(&c.Options.TitlePage, "titlepage", c.Options.TitlePage, "Title page\n0 = never\n1 = always\n2 = only if epub is split")
c.AddSection("Default config") c.AddSection("Default config")
c.AddBoolParam(&c.Options.Show, "show", false, "Show your default parameters") c.AddBoolParam(&c.Options.Show, "show", false, "Show your default parameters")
@ -161,7 +159,7 @@ func (c *Converter) InitParse() {
c.AddBoolParam(&c.Options.Help, "help", false, "Show this help message") c.AddBoolParam(&c.Options.Help, "help", false, "Show this help message")
} }
// Customize version of FlagSet.PrintDefaults // Usage Customize version of FlagSet.PrintDefaults
func (c *Converter) Usage(isString bool, f *flag.Flag) string { func (c *Converter) Usage(isString bool, f *flag.Flag) string {
var b strings.Builder var b strings.Builder
fmt.Fprintf(&b, " -%s", f.Name) // Two spaces before -; see next two comments. fmt.Fprintf(&b, " -%s", f.Name) // Two spaces before -; see next two comments.
@ -272,11 +270,11 @@ func (c *Converter) Parse() {
if c.Options.AppleBookCompatibility { if c.Options.AppleBookCompatibility {
c.Options.AutoSplitDoublePage = true c.Options.AutoSplitDoublePage = true
c.Options.KeepDoublePageIfSplitted = false c.Options.KeepDoublePageIfSplit = false
} }
} }
// Check parameters // Validate Check parameters
func (c *Converter) Validate() error { func (c *Converter) Validate() error {
// Check input // Check input
if c.Options.Input == "" { if c.Options.Input == "" {
@ -363,11 +361,11 @@ func (c *Converter) Validate() error {
// Color // Color
colorRegex := regexp.MustCompile("^[0-9A-F]{3}$") colorRegex := regexp.MustCompile("^[0-9A-F]{3}$")
if !colorRegex.MatchString(c.Options.ForegroundColor) { if !colorRegex.MatchString(c.Options.ForegroundColor) {
return errors.New("foreground color must have color format in hexa: [0-9A-F]{3}") return errors.New("foreground color must have color format in hexadecimal: [0-9A-F]{3}")
} }
if !colorRegex.MatchString(c.Options.BackgroundColor) { if !colorRegex.MatchString(c.Options.BackgroundColor) {
return errors.New("background color must have color format in hexa: [0-9A-F]{3}") return errors.New("background color must have color format in hexadecimal: [0-9A-F]{3}")
} }
// Format // Format
@ -393,7 +391,7 @@ func (c *Converter) Validate() error {
return nil return nil
} }
// Helper to show usage, err and exit 1 // Fatal Helper to show usage, err and exit 1
func (c *Converter) Fatal(err error) { func (c *Converter) Fatal(err error) {
c.Cmd.Usage() c.Cmd.Usage()
fmt.Fprintf(os.Stderr, "\nError: %s\n", err) fmt.Fprintf(os.Stderr, "\nError: %s\n", err)

View File

@ -1,6 +1,4 @@
/* // Package options manage options with default value from config.
Manage options with default value from config.
*/
package options package options
import ( import (
@ -36,7 +34,7 @@ type Options struct {
AutoContrast bool `yaml:"auto_contrast"` AutoContrast bool `yaml:"auto_contrast"`
AutoRotate bool `yaml:"auto_rotate"` AutoRotate bool `yaml:"auto_rotate"`
AutoSplitDoublePage bool `yaml:"auto_split_double_page"` AutoSplitDoublePage bool `yaml:"auto_split_double_page"`
KeepDoublePageIfSplitted bool `yaml:"keep_double_page_if_splitted"` KeepDoublePageIfSplit bool `yaml:"keep_double_page_if_split"`
NoBlankImage bool `yaml:"no_blank_image"` NoBlankImage bool `yaml:"no_blank_image"`
Manga bool `yaml:"manga"` Manga bool `yaml:"manga"`
HasCover bool `yaml:"has_cover"` HasCover bool `yaml:"has_cover"`
@ -78,26 +76,26 @@ type Options struct {
profiles profiles.Profiles profiles profiles.Profiles
} }
// Initialize default options. // New Initialize default options.
func New() *Options { func New() *Options {
return &Options{ return &Options{
Profile: "SR", Profile: "SR",
Quality: 85, Quality: 85,
Grayscale: true, Grayscale: true,
Crop: true, Crop: true,
CropRatioLeft: 1, CropRatioLeft: 1,
CropRatioUp: 1, CropRatioUp: 1,
CropRatioRight: 1, CropRatioRight: 1,
CropRatioBottom: 3, CropRatioBottom: 3,
NoBlankImage: true, NoBlankImage: true,
HasCover: true, HasCover: true,
KeepDoublePageIfSplitted: true, KeepDoublePageIfSplit: true,
SortPathMode: 1, SortPathMode: 1,
ForegroundColor: "000", ForegroundColor: "000",
BackgroundColor: "FFF", BackgroundColor: "FFF",
Format: "jpeg", Format: "jpeg",
TitlePage: 1, TitlePage: 1,
profiles: profiles.New(), profiles: profiles.New(),
} }
} }
@ -173,7 +171,7 @@ func (o *Options) MarshalJSON() ([]byte, error) {
if o.PortraitOnly || !o.AppleBookCompatibility { if o.PortraitOnly || !o.AppleBookCompatibility {
out["autosplitdoublepage"] = o.AutoSplitDoublePage out["autosplitdoublepage"] = o.AutoSplitDoublePage
if o.AutoSplitDoublePage { if o.AutoSplitDoublePage {
out["keepdoublepageifsplitted"] = o.KeepDoublePageIfSplitted out["keepdoublepageifsplit"] = o.KeepDoublePageIfSplit
} }
} }
if o.LimitMb != 0 { if o.LimitMb != 0 {
@ -185,13 +183,13 @@ func (o *Options) MarshalJSON() ([]byte, error) {
return json.Marshal(out) return json.Marshal(out)
} }
// Config file: ~/.go-comic-converter.yaml // FileName Config file: ~/.go-comic-converter.yaml
func (o *Options) FileName() string { func (o *Options) FileName() string {
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
return filepath.Join(home, ".go-comic-converter.yaml") return filepath.Join(home, ".go-comic-converter.yaml")
} }
// Load config files // LoadConfig Load config files
func (o *Options) LoadConfig() error { func (o *Options) LoadConfig() error {
f, err := os.Open(o.FileName()) f, err := os.Open(o.FileName())
if err != nil { if err != nil {
@ -206,7 +204,7 @@ func (o *Options) LoadConfig() error {
return nil return nil
} }
// Get current settings for fields that can be saved // ShowConfig Get current settings for fields that can be saved
func (o *Options) ShowConfig() string { func (o *Options) ShowConfig() string {
var profileDesc string var profileDesc string
profile := o.GetProfile() profile := o.GetProfile()
@ -225,9 +223,9 @@ func (o *Options) ShowConfig() string {
case 0: case 0:
sortpathmode = "path=alpha, file=alpha" sortpathmode = "path=alpha, file=alpha"
case 1: case 1:
sortpathmode = "path=alphanum, file=alpha" sortpathmode = "path=alphanumeric, file=alpha"
case 2: case 2:
sortpathmode = "path=alphanum, file=alphanum" sortpathmode = "path=alphanumeric, file=alphanumeric"
} }
aspectRatio := "auto" aspectRatio := "auto"
@ -244,7 +242,7 @@ func (o *Options) ShowConfig() string {
case 1: case 1:
titlePage = "always" titlePage = "always"
case 2: case 2:
titlePage = "when epub is splitted" titlePage = "when epub is split"
} }
grayscaleMode := "normal" grayscaleMode := "normal"
@ -265,28 +263,28 @@ func (o *Options) ShowConfig() string {
{"Format", o.Format, true}, {"Format", o.Format, true},
{"Quality", o.Quality, o.Format == "jpeg"}, {"Quality", o.Quality, o.Format == "jpeg"},
{"Grayscale", o.Grayscale, true}, {"Grayscale", o.Grayscale, true},
{"Grayscale Mode", grayscaleMode, o.Grayscale}, {"Grayscale mode", grayscaleMode, o.Grayscale},
{"Crop", o.Crop, true}, {"Crop", o.Crop, true},
{"Crop Ratio", fmt.Sprintf("%d Left - %d Up - %d Right - %d Bottom", o.CropRatioLeft, o.CropRatioUp, o.CropRatioRight, o.CropRatioBottom), o.Crop}, {"Crop ratio", fmt.Sprintf("%d Left - %d Up - %d Right - %d Bottom", o.CropRatioLeft, o.CropRatioUp, o.CropRatioRight, o.CropRatioBottom), o.Crop},
{"Brightness", o.Brightness, o.Brightness != 0}, {"Brightness", o.Brightness, o.Brightness != 0},
{"Contrast", o.Contrast, o.Contrast != 0}, {"Contrast", o.Contrast, o.Contrast != 0},
{"Auto Contrast", o.AutoContrast, true}, {"Auto contrast", o.AutoContrast, true},
{"Auto Rotate", o.AutoRotate, true}, {"Auto rotate", o.AutoRotate, true},
{"Auto Split DoublePage", o.AutoSplitDoublePage, o.PortraitOnly || !o.AppleBookCompatibility}, {"Auto split double page", o.AutoSplitDoublePage, o.PortraitOnly || !o.AppleBookCompatibility},
{"Keep DoublePage If Splitted", o.KeepDoublePageIfSplitted, (o.PortraitOnly || !o.AppleBookCompatibility) && o.AutoSplitDoublePage}, {"Keep double page if split", o.KeepDoublePageIfSplit, (o.PortraitOnly || !o.AppleBookCompatibility) && o.AutoSplitDoublePage},
{"No Blank Image", o.NoBlankImage, true}, {"No blank image", o.NoBlankImage, true},
{"Manga", o.Manga, true}, {"Manga", o.Manga, true},
{"Has Cover", o.HasCover, true}, {"Has cover", o.HasCover, true},
{"Limit", fmt.Sprintf("%d Mb", o.LimitMb), o.LimitMb != 0}, {"Limit", fmt.Sprintf("%d Mb", o.LimitMb), o.LimitMb != 0},
{"Strip First Directory From Toc", o.StripFirstDirectoryFromToc, true}, {"Strip first directory from toc", o.StripFirstDirectoryFromToc, true},
{"Sort Path Mode", sortpathmode, true}, {"Sort path mode", sortpathmode, true},
{"Foreground Color", fmt.Sprintf("#%s", o.ForegroundColor), true}, {"Foreground color", fmt.Sprintf("#%s", o.ForegroundColor), true},
{"Background Color", fmt.Sprintf("#%s", o.BackgroundColor), true}, {"Background color", fmt.Sprintf("#%s", o.BackgroundColor), true},
{"Resize", !o.NoResize, true}, {"Resize", !o.NoResize, true},
{"Aspect Ratio", aspectRatio, true}, {"Aspect ratio", aspectRatio, true},
{"Portrait Only", o.PortraitOnly, true}, {"Portrait only", o.PortraitOnly, true},
{"Title Page", titlePage, true}, {"Title page", titlePage, true},
{"Apple Book Compatibility", o.AppleBookCompatibility, !o.PortraitOnly}, {"Apple book compatibility", o.AppleBookCompatibility, !o.PortraitOnly},
} { } {
if v.Condition { if v.Condition {
b.WriteString(fmt.Sprintf("\n %-32s: %v", v.Key, v.Value)) b.WriteString(fmt.Sprintf("\n %-32s: %v", v.Key, v.Value))
@ -295,13 +293,13 @@ func (o *Options) ShowConfig() string {
return b.String() return b.String()
} }
// reset all settings to default value // ResetConfig reset all settings to default value
func (o *Options) ResetConfig() error { func (o *Options) ResetConfig() error {
New().SaveConfig() New().SaveConfig()
return o.LoadConfig() return o.LoadConfig()
} }
// save all current settings as futur default value // SaveConfig save all current settings as default value
func (o *Options) SaveConfig() error { func (o *Options) SaveConfig() error {
f, err := os.Create(o.FileName()) f, err := os.Create(o.FileName())
if err != nil { if err != nil {
@ -311,12 +309,12 @@ func (o *Options) SaveConfig() error {
return yaml.NewEncoder(f).Encode(o) return yaml.NewEncoder(f).Encode(o)
} }
// shortcut to get current profile // GetProfile shortcut to get current profile
func (o *Options) GetProfile() *profiles.Profile { func (o *Options) GetProfile() *profiles.Profile {
return o.profiles.Get(o.Profile) return o.profiles.Get(o.Profile)
} }
// all available profiles // AvailableProfiles all available profiles
func (o *Options) AvailableProfiles() string { func (o *Options) AvailableProfiles() string {
return o.profiles.String() return o.profiles.String()
} }

View File

@ -1,6 +1,4 @@
/* // Package profiles manage supported profiles for go-comic-converter.
Manage supported profiles for go-comic-converter.
*/
package profiles package profiles
import ( import (
@ -17,10 +15,10 @@ type Profile struct {
type Profiles []Profile type Profiles []Profile
// Initialize list of all supported profiles. // New Initialize list of all supported profiles.
func New() Profiles { func New() Profiles {
return []Profile{ return []Profile{
// High Resolution for Tablette // High Resolution for Tablet
{"HR", "High Resolution", 2400, 3840}, {"HR", "High Resolution", 2400, 3840},
{"SR", "Standard Resolution", 1200, 1920}, {"SR", "Standard Resolution", 1200, 1920},
//Kindle //Kindle
@ -68,7 +66,7 @@ func (p Profiles) String() string {
return strings.Join(s, "\n") return strings.Join(s, "\n")
} }
// Lookup profile by code // Get Lookup profile by code
func (p Profiles) Get(name string) *Profile { func (p Profiles) Get(name string) *Profile {
for _, profile := range p { for _, profile := range p {
if profile.Code == name { if profile.Code == name {

View File

@ -1,6 +1,4 @@
/* // Package epub Tools to create EPUB from images.
Tools to create EPUB from images.
*/
package epub package epub
import ( import (
@ -25,7 +23,7 @@ import (
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
) )
type ePub struct { type EPub struct {
*epuboptions.Options *epuboptions.Options
UID string UID string
Publisher string Publisher string
@ -41,8 +39,8 @@ type epubPart struct {
Reader *zip.ReadCloser Reader *zip.ReadCloser
} }
// initialize EPUB // New initialize EPUB
func New(options *epuboptions.Options) *ePub { func New(options *epuboptions.Options) *EPub {
uid := uuid.Must(uuid.NewV4()) uid := uuid.Must(uuid.NewV4())
tmpl := template.New("parser") tmpl := template.New("parser")
tmpl.Funcs(template.FuncMap{ tmpl.Funcs(template.FuncMap{
@ -50,7 +48,7 @@ func New(options *epuboptions.Options) *ePub {
"zoom": func(s int, z float32) int { return int(float32(s) * z) }, "zoom": func(s int, z float32) int { return int(float32(s) * z) },
}) })
return &ePub{ return &EPub{
Options: options, Options: options,
UID: uid.String(), UID: uid.String(),
Publisher: "GO Comic Converter", Publisher: "GO Comic Converter",
@ -61,7 +59,7 @@ func New(options *epuboptions.Options) *ePub {
} }
// render templates // render templates
func (e *ePub) render(templateString string, data map[string]any) string { func (e *EPub) render(templateString string, data map[string]any) string {
var result strings.Builder var result strings.Builder
tmpl := template.Must(e.templateProcessor.Parse(templateString)) tmpl := template.Must(e.templateProcessor.Parse(templateString))
if err := tmpl.Execute(&result, data); err != nil { if err := tmpl.Execute(&result, data); err != nil {
@ -71,7 +69,7 @@ func (e *ePub) render(templateString string, data map[string]any) string {
} }
// write image to the zip // write image to the zip
func (e *ePub) writeImage(wz *epubzip.EPUBZip, img *epubimage.Image, zipImg *zip.File) error { func (e *EPub) writeImage(wz *epubzip.EPUBZip, img *epubimage.Image, zipImg *zip.File) error {
err := wz.WriteContent( err := wz.WriteContent(
img.EPUBPagePath(), img.EPUBPagePath(),
[]byte(e.render(epubtemplates.Text, map[string]any{ []byte(e.render(epubtemplates.Text, map[string]any{
@ -89,7 +87,7 @@ func (e *ePub) writeImage(wz *epubzip.EPUBZip, img *epubimage.Image, zipImg *zip
} }
// write blank page // write blank page
func (e *ePub) writeBlank(wz *epubzip.EPUBZip, img *epubimage.Image) error { func (e *EPub) writeBlank(wz *epubzip.EPUBZip, img *epubimage.Image) error {
return wz.WriteContent( return wz.WriteContent(
img.EPUBSpacePath(), img.EPUBSpacePath(),
[]byte(e.render(epubtemplates.Blank, map[string]any{ []byte(e.render(epubtemplates.Blank, map[string]any{
@ -100,7 +98,7 @@ func (e *ePub) writeBlank(wz *epubzip.EPUBZip, img *epubimage.Image) error {
} }
// write title image // write title image
func (e *ePub) writeCoverImage(wz *epubzip.EPUBZip, img *epubimage.Image, part, totalParts int) error { func (e *EPub) writeCoverImage(wz *epubzip.EPUBZip, img *epubimage.Image, part, totalParts int) error {
title := "Cover" title := "Cover"
text := "" text := ""
if totalParts > 1 { if totalParts > 1 {
@ -143,7 +141,7 @@ func (e *ePub) writeCoverImage(wz *epubzip.EPUBZip, img *epubimage.Image, part,
} }
// write title image // write title image
func (e *ePub) writeTitleImage(wz *epubzip.EPUBZip, img *epubimage.Image, title string) error { func (e *EPub) writeTitleImage(wz *epubzip.EPUBZip, img *epubimage.Image, title string) error {
titleAlign := "" titleAlign := ""
if !e.Image.View.PortraitOnly { if !e.Image.View.PortraitOnly {
if e.Image.Manga { if e.Image.Manga {
@ -199,7 +197,7 @@ func (e *ePub) writeTitleImage(wz *epubzip.EPUBZip, img *epubimage.Image, title
} }
// extract image and split it into part // extract image and split it into part
func (e *ePub) getParts() (parts []*epubPart, imgStorage *epubzip.EPUBZipStorageImageReader, err error) { func (e *EPub) getParts() (parts []*epubPart, imgStorage *epubzip.StorageImageReader, err error) {
images, err := e.imageProcessor.Load() images, err := e.imageProcessor.Load()
if err != nil { if err != nil {
@ -228,7 +226,7 @@ func (e *ePub) getParts() (parts []*epubPart, imgStorage *epubzip.EPUBZipStorage
return parts, nil, nil return parts, nil, nil
} }
imgStorage, err = epubzip.NewEPUBZipStorageImageReader(e.ImgStorage()) imgStorage, err = epubzip.NewStorageImageReader(e.ImgStorage())
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -270,24 +268,24 @@ func (e *ePub) getParts() (parts []*epubPart, imgStorage *epubzip.EPUBZipStorage
// create a tree from the directories. // create a tree from the directories.
// //
// this is used to simulate the toc. // this is used to simulate the toc.
func (e *ePub) getTree(images []*epubimage.Image, skip_files bool) string { func (e *EPub) getTree(images []*epubimage.Image, skipFiles bool) string {
t := epubtree.New() t := epubtree.New()
for _, img := range images { for _, img := range images {
if skip_files { if skipFiles {
t.Add(img.Path) t.Add(img.Path)
} else { } else {
t.Add(filepath.Join(img.Path, img.Name)) t.Add(filepath.Join(img.Path, img.Name))
} }
} }
c := t.Root() c := t.Root()
if skip_files && e.StripFirstDirectoryFromToc && len(c.Children) == 1 { if skipFiles && e.StripFirstDirectoryFromToc && c.ChildCount() == 1 {
c = c.Children[0] c = c.FirstChild()
} }
return c.WriteString("") return c.WriteString("")
} }
func (e *ePub) computeAspectRatio(epubParts []*epubPart) float64 { func (e *EPub) computeAspectRatio(epubParts []*epubPart) float64 {
var ( var (
bestAspectRatio float64 bestAspectRatio float64
bestAspectRatioCount int bestAspectRatioCount int
@ -314,7 +312,7 @@ func (e *ePub) computeAspectRatio(epubParts []*epubPart) float64 {
return bestAspectRatio return bestAspectRatio
} }
func (e *ePub) computeViewPort(epubParts []*epubPart) { func (e *EPub) computeViewPort(epubParts []*epubPart) {
if e.Image.View.AspectRatio == -1 { if e.Image.View.AspectRatio == -1 {
return //keep device size return //keep device size
} }
@ -333,13 +331,86 @@ func (e *ePub) computeViewPort(epubParts []*epubPart) {
} }
} }
// create the zip func (e *EPub) writePart(path string, currentPart, totalParts int, part *epubPart, imgStorage *epubzip.StorageImageReader) error {
func (e *ePub) Write() error { hasTitlePage := e.TitlePage == 1 || (e.TitlePage == 2 && totalParts > 1)
wz, err := epubzip.New(path)
if err != nil {
return err
}
defer wz.Close()
title := e.Title
if totalParts > 1 {
title = fmt.Sprintf("%s [%d/%d]", title, currentPart, totalParts)
}
type zipContent struct { type zipContent struct {
Name string Name string
Content string Content string
} }
content := []zipContent{
{"META-INF/container.xml", epubtemplates.Container},
{"META-INF/com.apple.ibooks.display-options.xml", epubtemplates.AppleBooks},
{"OEBPS/content.opf", epubtemplates.Content(&epubtemplates.ContentOptions{
Title: title,
HasTitlePage: hasTitlePage,
UID: e.UID,
Author: e.Author,
Publisher: e.Publisher,
UpdatedAt: e.UpdatedAt,
ImageOptions: e.Image,
Cover: part.Cover,
Images: part.Images,
Current: currentPart,
Total: totalParts,
})},
{"OEBPS/toc.xhtml", epubtemplates.Toc(title, hasTitlePage, e.StripFirstDirectoryFromToc, part.Images)},
{"OEBPS/Text/style.css", e.render(epubtemplates.Style, map[string]any{
"View": e.Image.View,
})},
}
if err = wz.WriteMagic(); err != nil {
return err
}
for _, c := range content {
if err := wz.WriteContent(c.Name, []byte(c.Content)); err != nil {
return err
}
}
if err = e.writeCoverImage(wz, part.Cover, currentPart, totalParts); err != nil {
return err
}
if hasTitlePage {
if err = e.writeTitleImage(wz, part.Cover, title); err != nil {
return err
}
}
lastImage := part.Images[len(part.Images)-1]
for _, img := range part.Images {
if err := e.writeImage(wz, img, imgStorage.Get(img.EPUBImgPath())); err != nil {
return err
}
// Double Page or Last Image that is not a double page
if !e.Image.View.PortraitOnly &&
(img.DoublePage ||
(!e.Image.KeepDoublePageIfSplit && img.Part == 1) ||
(img.Part == 0 && img == lastImage)) {
if err := e.writeBlank(wz, img); err != nil {
return err
}
}
}
return nil
}
// create the zip
func (e *EPub) Write() error {
epubParts, imgStorage, err := e.getParts() epubParts, imgStorage, err := e.getParts()
if err != nil { if err != nil {
return err return err
@ -373,7 +444,6 @@ func (e *ePub) Write() error {
}) })
e.computeViewPort(epubParts) e.computeViewPort(epubParts)
hasTitlePage := e.TitlePage == 1 || (e.TitlePage == 2 && totalParts > 1)
for i, part := range epubParts { for i, part := range epubParts {
ext := filepath.Ext(e.Output) ext := filepath.Ext(e.Output)
suffix := "" suffix := ""
@ -384,74 +454,17 @@ func (e *ePub) Write() error {
} }
path := fmt.Sprintf("%s%s%s", e.Output[0:len(e.Output)-len(ext)], suffix, ext) path := fmt.Sprintf("%s%s%s", e.Output[0:len(e.Output)-len(ext)], suffix, ext)
wz, err := epubzip.New(path)
if err != nil {
return err
}
defer wz.Close()
title := e.Title if err := e.writePart(
if totalParts > 1 { path,
title = fmt.Sprintf("%s [%d/%d]", title, i+1, totalParts) i+1,
} totalParts,
part,
content := []zipContent{ imgStorage,
{"META-INF/container.xml", epubtemplates.Container}, ); err != nil {
{"META-INF/com.apple.ibooks.display-options.xml", epubtemplates.AppleBooks},
{"OEBPS/content.opf", epubtemplates.Content(&epubtemplates.ContentOptions{
Title: title,
HasTitlePage: hasTitlePage,
UID: e.UID,
Author: e.Author,
Publisher: e.Publisher,
UpdatedAt: e.UpdatedAt,
ImageOptions: e.Image,
Cover: part.Cover,
Images: part.Images,
Current: i + 1,
Total: totalParts,
})},
{"OEBPS/toc.xhtml", epubtemplates.Toc(title, hasTitlePage, e.StripFirstDirectoryFromToc, part.Images)},
{"OEBPS/Text/style.css", e.render(epubtemplates.Style, map[string]any{
"View": e.Image.View,
})},
}
if err = wz.WriteMagic(); err != nil {
return err
}
for _, c := range content {
if err := wz.WriteContent(c.Name, []byte(c.Content)); err != nil {
return err
}
}
if err = e.writeCoverImage(wz, part.Cover, i+1, totalParts); err != nil {
return err return err
} }
if hasTitlePage {
if err = e.writeTitleImage(wz, part.Cover, title); err != nil {
return err
}
}
lastImage := part.Images[len(part.Images)-1]
for _, img := range part.Images {
if err := e.writeImage(wz, img, imgStorage.Get(img.EPUBImgPath())); err != nil {
return err
}
// Double Page or Last Image that is not a double page
if !e.Image.View.PortraitOnly &&
(img.DoublePage ||
(!e.Image.KeepDoublePageIfSplitted && img.Part == 1) ||
(img.Part == 0 && img == lastImage)) {
if err := e.writeBlank(wz, img); err != nil {
return err
}
}
}
bar.Add(1) bar.Add(1)
} }
bar.Close() bar.Close()

View File

@ -1,6 +1,4 @@
/* // Package epubimage Image helpers to transform image.
Image helpers to transform image.
*/
package epubimage package epubimage
import ( import (
@ -25,60 +23,60 @@ type Image struct {
Error error Error error
} }
// key name of the blank plage after the image // SpaceKey key name of the blank page after the image
func (i *Image) SpaceKey() string { func (i *Image) SpaceKey() string {
return fmt.Sprintf("space_%d", i.Id) return fmt.Sprintf("space_%d", i.Id)
} }
// path of the blank page // SpacePath path of the blank page
func (i *Image) SpacePath() string { func (i *Image) SpacePath() string {
return fmt.Sprintf("Text/%s.xhtml", i.SpaceKey()) return fmt.Sprintf("Text/%s.xhtml", i.SpaceKey())
} }
// path of the blank page into the EPUB // EPUBSpacePath path of the blank page into the EPUB
func (i *Image) EPUBSpacePath() string { func (i *Image) EPUBSpacePath() string {
return fmt.Sprintf("OEBPS/%s", i.SpacePath()) return fmt.Sprintf("OEBPS/%s", i.SpacePath())
} }
// key for page // PageKey key for page
func (i *Image) PageKey() string { func (i *Image) PageKey() string {
return fmt.Sprintf("page_%d_p%d", i.Id, i.Part) return fmt.Sprintf("page_%d_p%d", i.Id, i.Part)
} }
// page path linked to the image // PagePath page path linked to the image
func (i *Image) PagePath() string { func (i *Image) PagePath() string {
return fmt.Sprintf("Text/%s.xhtml", i.PageKey()) return fmt.Sprintf("Text/%s.xhtml", i.PageKey())
} }
// page path into the EPUB // EPUBPagePath page path into the EPUB
func (i *Image) EPUBPagePath() string { func (i *Image) EPUBPagePath() string {
return fmt.Sprintf("OEBPS/%s", i.PagePath()) return fmt.Sprintf("OEBPS/%s", i.PagePath())
} }
// key for image // ImgKey key for image
func (i *Image) ImgKey() string { func (i *Image) ImgKey() string {
return fmt.Sprintf("img_%d_p%d", i.Id, i.Part) return fmt.Sprintf("img_%d_p%d", i.Id, i.Part)
} }
// image path // ImgPath image path
func (i *Image) ImgPath() string { func (i *Image) ImgPath() string {
return fmt.Sprintf("Images/%s.%s", i.ImgKey(), i.Format) return fmt.Sprintf("Images/%s.%s", i.ImgKey(), i.Format)
} }
// image path into the EPUB // EPUBImgPath image path into the EPUB
func (i *Image) EPUBImgPath() string { func (i *Image) EPUBImgPath() string {
return fmt.Sprintf("OEBPS/%s", i.ImgPath()) return fmt.Sprintf("OEBPS/%s", i.ImgPath())
} }
// style to apply to the image. // ImgStyle style to apply to the image.
// //
// center by default. // center by default.
// align to left or right if it's part of the splitted double page. // align to left or right if it's part of the split double page.
func (i *Image) ImgStyle(viewWidth, viewHeight int, align string) string { func (i *Image) ImgStyle(viewWidth, viewHeight int, align string) string {
relWidth, relHeight := i.RelSize(viewWidth, viewHeight) relWidth, relHeight := i.RelSize(viewWidth, viewHeight)
marginW, marginH := float64(viewWidth-relWidth)/2, float64(viewHeight-relHeight)/2 marginW, marginH := float64(viewWidth-relWidth)/2, float64(viewHeight-relHeight)/2
style := []string{} var style []string
style = append(style, fmt.Sprintf("width:%dpx", relWidth)) style = append(style, fmt.Sprintf("width:%dpx", relWidth))
style = append(style, fmt.Sprintf("height:%dpx", relHeight)) style = append(style, fmt.Sprintf("height:%dpx", relHeight))

View File

@ -8,8 +8,8 @@ import (
"github.com/disintegration/gift" "github.com/disintegration/gift"
) )
// Automatically improve contrast // AutoContrast Automatically improve contrast
func AutoContrast() *autocontrast { func AutoContrast() gift.Filter {
return &autocontrast{} return &autocontrast{}
} }
@ -78,7 +78,7 @@ func (f *autocontrast) Draw(dst draw.Image, src image.Image, options *gift.Optio
// compute a curve from dark and light factor applying to the color // compute a curve from dark and light factor applying to the color
c := (1 - d) + (d+l)*y c := (1 - d) + (d+l)*y
// applying the coef // applying the ratio
return f.cap(r0 * c), f.cap(g0 * c), f.cap(b0 * c), a0 return f.cap(r0 * c), f.cap(g0 * c), f.cap(b0 * c), a0
}).Draw(dst, src, options) }).Draw(dst, src, options)
} }

View File

@ -7,7 +7,7 @@ import (
"github.com/disintegration/gift" "github.com/disintegration/gift"
) )
// Lookup for margin and crop // AutoCrop Lookup for margin and crop
func AutoCrop(img image.Image, bounds image.Rectangle, cutRatioLeft, cutRatioUp, cutRatioRight, cutRatioBottom int) gift.Filter { func AutoCrop(img image.Image, bounds image.Rectangle, cutRatioLeft, cutRatioUp, cutRatioRight, cutRatioBottom int) gift.Filter {
return gift.Crop( return gift.Crop(
findMargin(img, bounds, cutRatioOptions{cutRatioLeft, cutRatioUp, cutRatioRight, cutRatioBottom}), findMargin(img, bounds, cutRatioOptions{cutRatioLeft, cutRatioUp, cutRatioRight, cutRatioBottom}),

View File

@ -11,7 +11,7 @@ import (
"golang.org/x/image/font/gofont/gomonobold" "golang.org/x/image/font/gofont/gomonobold"
) )
// Create a title with the cover image // CoverTitle Create a title with the cover image
func CoverTitle(title string, align string, pctWidth int, pctMargin int, maxFontSize int, borderSize int) gift.Filter { func CoverTitle(title string, align string, pctWidth int, pctMargin int, maxFontSize int, borderSize int) gift.Filter {
return &coverTitle{title, align, pctWidth, pctMargin, maxFontSize, borderSize} return &coverTitle{title, align, pctWidth, pctMargin, maxFontSize, borderSize}
} }
@ -25,13 +25,13 @@ type coverTitle struct {
borderSize int borderSize int
} }
// size is the same as source // Bounds size is the same as source
func (p *coverTitle) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { func (p *coverTitle) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
return srcBounds return srcBounds
} }
// blur the src image, and create a box with the title in the middle // Draw blur the src image, and create a box with the title in the middle
func (p *coverTitle) Draw(dst draw.Image, src image.Image, options *gift.Options) { func (p *coverTitle) Draw(dst draw.Image, src image.Image, _ *gift.Options) {
draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src) draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
if p.title == "" { if p.title == "" {
return return

View File

@ -7,7 +7,8 @@ import (
"github.com/disintegration/gift" "github.com/disintegration/gift"
) )
// Cut a double page in 2 part: left and right. // CropSplitDoublePage Cut a double page in 2 part: left and right.
//
// This will cut in the middle of the page. // This will cut in the middle of the page.
func CropSplitDoublePage(right bool) gift.Filter { func CropSplitDoublePage(right bool) gift.Filter {
return &cropSplitDoublePage{right} return &cropSplitDoublePage{right}

View File

@ -8,7 +8,8 @@ import (
"github.com/disintegration/gift" "github.com/disintegration/gift"
) )
// Generate a blank pixel 1x1, if the size of the image is 0x0. // Pixel Generate a blank pixel 1x1, if the size of the image is 0x0.
//
// An image 0x0 is not a valid image, and failed to read. // An image 0x0 is not a valid image, and failed to read.
func Pixel() gift.Filter { func Pixel() gift.Filter {
return &pixel{} return &pixel{}
@ -26,7 +27,7 @@ func (p *pixel) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
return return
} }
func (p *pixel) Draw(dst draw.Image, src image.Image, options *gift.Options) { func (p *pixel) Draw(dst draw.Image, src image.Image, _ *gift.Options) {
if dst.Bounds().Dx() == 1 && dst.Bounds().Dy() == 1 { if dst.Bounds().Dx() == 1 && dst.Bounds().Dy() == 1 {
dst.Set(0, 0, color.White) dst.Set(0, 0, color.White)
return return

View File

@ -1,6 +1,4 @@
/* // Package epubimageprocessor extract and transform image into a compressed jpeg.
Extract and transform image into a compressed jpeg.
*/
package epubimageprocessor package epubimageprocessor
import ( import (
@ -27,7 +25,7 @@ func New(o *epuboptions.Options) *EPUBImageProcessor {
return &EPUBImageProcessor{o} return &EPUBImageProcessor{o}
} }
// extract and convert images // Load extract and convert images
func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) { func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
images = make([]*epubimage.Image, 0) images = make([]*epubimage.Image, 0)
imageCount, imageInput, err := e.load() imageCount, imageInput, err := e.load()
@ -35,7 +33,7 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
return nil, err return nil, err
} }
// dry run, skip convertion // dry run, skip conversion
if e.Dry { if e.Dry {
for img := range imageInput { for img := range imageInput {
images = append(images, &epubimage.Image{ images = append(images, &epubimage.Image{
@ -62,7 +60,7 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
}) })
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
imgStorage, err := epubzip.NewEPUBZipStorageImageWriter(e.ImgStorage(), e.Image.Format) imgStorage, err := epubzip.NewStorageImageWriter(e.ImgStorage(), e.Image.Format)
if err != nil { if err != nil {
bar.Close() bar.Close()
return nil, err return nil, err
@ -82,7 +80,7 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
// do not keep double page if requested // do not keep double page if requested
if !(img.DoublePage && input.Id > 0 && if !(img.DoublePage && input.Id > 0 &&
e.Options.Image.AutoSplitDoublePage && !e.Options.Image.KeepDoublePageIfSplitted) { e.Options.Image.AutoSplitDoublePage && !e.Options.Image.KeepDoublePageIfSplit) {
if err = imgStorage.Add(img.EPUBImgPath(), img.Raw, e.Image.Quality); err != nil { if err = imgStorage.Add(img.EPUBImgPath(), img.Raw, e.Image.Quality); err != nil {
bar.Close() bar.Close()
fmt.Fprintf(os.Stderr, "error with %s: %s", input.Name, err) fmt.Fprintf(os.Stderr, "error with %s: %s", input.Name, err)
@ -281,26 +279,26 @@ type CoverTitleDataOptions struct {
func (e *EPUBImageProcessor) Cover16LevelOfGray(bounds image.Rectangle) draw.Image { func (e *EPUBImageProcessor) Cover16LevelOfGray(bounds image.Rectangle) draw.Image {
return image.NewPaletted(bounds, color.Palette{ return image.NewPaletted(bounds, color.Palette{
color.Gray{0x00}, color.Gray{},
color.Gray{0x11}, color.Gray{Y: 0x11},
color.Gray{0x22}, color.Gray{Y: 0x22},
color.Gray{0x33}, color.Gray{Y: 0x33},
color.Gray{0x44}, color.Gray{Y: 0x44},
color.Gray{0x55}, color.Gray{Y: 0x55},
color.Gray{0x66}, color.Gray{Y: 0x66},
color.Gray{0x77}, color.Gray{Y: 0x77},
color.Gray{0x88}, color.Gray{Y: 0x88},
color.Gray{0x99}, color.Gray{Y: 0x99},
color.Gray{0xAA}, color.Gray{Y: 0xAA},
color.Gray{0xBB}, color.Gray{Y: 0xBB},
color.Gray{0xCC}, color.Gray{Y: 0xCC},
color.Gray{0xDD}, color.Gray{Y: 0xDD},
color.Gray{0xEE}, color.Gray{Y: 0xEE},
color.Gray{0xFF}, color.Gray{Y: 0xFF},
}) })
} }
// create a title page with the cover // CoverTitleData create a title page with the cover
func (e *EPUBImageProcessor) CoverTitleData(o *CoverTitleDataOptions) (*epubzip.ZipImage, error) { func (e *EPUBImageProcessor) CoverTitleData(o *CoverTitleDataOptions) (*epubzip.ZipImage, error) {
// Create a blur version of the cover // Create a blur version of the cover
g := gift.New(epubimagefilters.CoverTitle(o.Text, o.Align, o.PctWidth, o.PctMargin, o.MaxFontSize, o.BorderSize)) g := gift.New(epubimagefilters.CoverTitle(o.Text, o.Align, o.PctWidth, o.PctMargin, o.MaxFontSize, o.BorderSize))

View File

@ -208,7 +208,7 @@ func (e *EPUBImageProcessor) loadCbz() (totalImages int, output chan *task, err
return return
} }
names := []string{} var names []string
for _, img := range images { for _, img := range images {
names = append(names, img.Name) names = append(names, img.Name)
} }

View File

@ -1,6 +1,4 @@
/* // Package epuboptions Options for EPUB creation.
Options for EPUB creation.
*/
package epuboptions package epuboptions
import "fmt" import "fmt"
@ -22,23 +20,23 @@ type View struct {
} }
type Image struct { type Image struct {
Crop *Crop Crop *Crop
Quality int Quality int
Brightness int Brightness int
Contrast int Contrast int
AutoContrast bool AutoContrast bool
AutoRotate bool AutoRotate bool
AutoSplitDoublePage bool AutoSplitDoublePage bool
KeepDoublePageIfSplitted bool KeepDoublePageIfSplit bool
NoBlankImage bool NoBlankImage bool
Manga bool Manga bool
HasCover bool HasCover bool
View *View View *View
GrayScale bool GrayScale bool
GrayScaleMode int GrayScaleMode int
Resize bool Resize bool
Format string Format string
AppleBookCompatibility bool AppleBookCompatibility bool
} }
type Options struct { type Options struct {

View File

@ -1,6 +1,4 @@
/* // Package epubprogress create a progress bar with custom settings.
create a progress bar with custom settings.
*/
package epubprogress package epubprogress
import ( import (

View File

@ -5,20 +5,20 @@ import (
"os" "os"
) )
type EpubProgressJson struct { type Json struct {
o Options o Options
e *json.Encoder e *json.Encoder
current int current int
} }
func newEpubProgressJson(o Options) EpubProgress { func newEpubProgressJson(o Options) EpubProgress {
return &EpubProgressJson{ return &Json{
o: o, o: o,
e: json.NewEncoder(os.Stdout), e: json.NewEncoder(os.Stdout),
} }
} }
func (p *EpubProgressJson) Add(num int) error { func (p *Json) Add(num int) error {
p.current += num p.current += num
p.e.Encode(map[string]any{ p.e.Encode(map[string]any{
"type": "progress", "type": "progress",
@ -37,6 +37,6 @@ func (p *EpubProgressJson) Add(num int) error {
return nil return nil
} }
func (p *EpubProgressJson) Close() error { func (p *Json) Close() error {
return nil return nil
} }

View File

@ -1,6 +1,4 @@
/* // Package epubtemplates Templates use to create xml files of the EPUB.
Templates use to create xml files of the EPUB.
*/
package epubtemplates package epubtemplates
import _ "embed" import _ "embed"

View File

@ -30,7 +30,9 @@ type tag struct {
value string value string
} }
// create the content file // Content create the content file
//
//goland:noinspection HttpUrlsUsage,HttpUrlsUsage,HttpUrlsUsage,HttpUrlsUsage
func Content(o *ContentOptions) string { func Content(o *ContentOptions) string {
doc := etree.NewDocument() doc := etree.NewDocument()
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`) doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
@ -178,7 +180,7 @@ func getManifest(o *ContentOptions) []tag {
img, img,
!o.ImageOptions.View.PortraitOnly && !o.ImageOptions.View.PortraitOnly &&
(img.DoublePage || (img.DoublePage ||
(!o.ImageOptions.KeepDoublePageIfSplitted && img.Part == 1) || (!o.ImageOptions.KeepDoublePageIfSplit && img.Part == 1) ||
(img.Part == 0 && img == lastImage))) (img.Part == 0 && img == lastImage)))
} }
@ -212,7 +214,7 @@ func getSpineAuto(o *ContentOptions) []tag {
return fmt.Sprintf("%s layout-blank", getSpread(false)) return fmt.Sprintf("%s layout-blank", getSpread(false))
} }
spine := []tag{} var spine []tag
if o.HasTitlePage { if o.HasTitlePage {
if !o.ImageOptions.AppleBookCompatibility { if !o.ImageOptions.AppleBookCompatibility {
spine = append(spine, spine = append(spine,
@ -253,7 +255,7 @@ func getSpineAuto(o *ContentOptions) []tag {
} }
func getSpinePortrait(o *ContentOptions) []tag { func getSpinePortrait(o *ContentOptions) []tag {
spine := []tag{} var spine []tag
if o.HasTitlePage { if o.HasTitlePage {
spine = append(spine, spine = append(spine,
tag{"itemref", tagAttrs{"idref": "page_title"}, ""}, tag{"itemref", tagAttrs{"idref": "page_title"}, ""},
@ -269,7 +271,7 @@ func getSpinePortrait(o *ContentOptions) []tag {
return spine return spine
} }
// guide part of the content // getGuide Section guide of the content
func getGuide(o *ContentOptions) []tag { func getGuide(o *ContentOptions) []tag {
return []tag{ return []tag{
{"reference", tagAttrs{"type": "cover", "title": "cover", "href": "Text/cover.xhtml"}, ""}, {"reference", tagAttrs{"type": "cover", "title": "cover", "href": "Text/cover.xhtml"}, ""},

View File

@ -8,7 +8,9 @@ import (
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image" epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
) )
// create toc // Toc create toc
//
//goland:noinspection HttpUrlsUsage
func Toc(title string, hasTitle bool, stripFirstDirectoryFromToc bool, images []*epubimage.Image) string { func Toc(title string, hasTitle bool, stripFirstDirectoryFromToc bool, images []*epubimage.Image) string {
doc := etree.NewDocument() doc := etree.NewDocument()
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`) doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)

View File

@ -1,5 +1,5 @@
/* /*
Organize a list of filename with their path into a tree of directories. Package epubtree Organize a list of filename with their path into a tree of directories.
Example: Example:
- A/B/C/D.jpg - A/B/C/D.jpg
@ -23,52 +23,60 @@ import (
"strings" "strings"
) )
type tree struct { type Tree struct {
Nodes map[string]*node nodes map[string]*Node
} }
type node struct { type Node struct {
Value string value string
Children []*node children []*Node
} }
// initilize tree with a root node // New initialize tree with a root node
func New() *tree { func New() *Tree {
return &tree{map[string]*node{ return &Tree{map[string]*Node{
".": {".", []*node{}}, ".": {".", []*Node{}},
}} }}
} }
// root node // Root root node
func (n *tree) Root() *node { func (n *Tree) Root() *Node {
return n.Nodes["."] return n.nodes["."]
} }
// add the filename to the tree // Add the filename to the tree
func (n *tree) Add(filename string) { func (n *Tree) Add(filename string) {
cn := n.Root() cn := n.Root()
cp := "" cp := ""
for _, p := range strings.Split(filepath.Clean(filename), string(filepath.Separator)) { for _, p := range strings.Split(filepath.Clean(filename), string(filepath.Separator)) {
cp = filepath.Join(cp, p) cp = filepath.Join(cp, p)
if _, ok := n.Nodes[cp]; !ok { if _, ok := n.nodes[cp]; !ok {
n.Nodes[cp] = &node{Value: p, Children: []*node{}} n.nodes[cp] = &Node{value: p, children: []*Node{}}
cn.Children = append(cn.Children, n.Nodes[cp]) cn.children = append(cn.children, n.nodes[cp])
} }
cn = n.Nodes[cp] cn = n.nodes[cp]
} }
} }
// string version of the tree func (n *Node) ChildCount() int {
func (n *node) WriteString(indent string) string { return len(n.children)
}
func (n *Node) FirstChild() *Node {
return n.children[0]
}
// WriteString string version of the tree
func (n *Node) WriteString(indent string) string {
r := strings.Builder{} r := strings.Builder{}
if indent != "" { if indent != "" {
r.WriteString(indent) r.WriteString(indent)
r.WriteString("- ") r.WriteString("- ")
r.WriteString(n.Value) r.WriteString(n.value)
r.WriteString("\n") r.WriteString("\n")
} }
indent += " " indent += " "
for _, c := range n.Children { for _, c := range n.children {
r.WriteString(c.WriteString(indent)) r.WriteString(c.WriteString(indent))
} }
return r.String() return r.String()

View File

@ -1,5 +1,5 @@
/* /*
Helper to write EPUB files. Package epubzip Helper to write EPUB files.
We create a zip with the magic EPUB mimetype. We create a zip with the magic EPUB mimetype.
*/ */
@ -16,7 +16,7 @@ type EPUBZip struct {
wz *zip.Writer wz *zip.Writer
} }
// create a new EPUB // New create a new EPUB
func New(path string) (*EPUBZip, error) { func New(path string) (*EPUBZip, error) {
w, err := os.Create(path) w, err := os.Create(path)
if err != nil { if err != nil {
@ -26,7 +26,7 @@ func New(path string) (*EPUBZip, error) {
return &EPUBZip{w, wz}, nil return &EPUBZip{w, wz}, nil
} }
// close compress pipe and file. // Close compress pipe and file.
func (e *EPUBZip) Close() error { func (e *EPUBZip) Close() error {
if err := e.wz.Close(); err != nil { if err := e.wz.Close(); err != nil {
return err return err
@ -34,10 +34,11 @@ func (e *EPUBZip) Close() error {
return e.w.Close() return e.w.Close()
} }
// Write mimetype, in a very specific way. // WriteMagic Write mimetype, in a very specific way.
//
// This will be valid with epubcheck tools. // This will be valid with epubcheck tools.
func (e *EPUBZip) WriteMagic() error { func (e *EPUBZip) WriteMagic() error {
t := time.Now() t := time.Now().UTC()
fh := &zip.FileHeader{ fh := &zip.FileHeader{
Name: "mimetype", Name: "mimetype",
Method: zip.Store, Method: zip.Store,
@ -48,6 +49,8 @@ func (e *EPUBZip) WriteMagic() error {
UncompressedSize64: 20, UncompressedSize64: 20,
CRC32: 0x2cab616f, CRC32: 0x2cab616f,
} }
fh.CreatorVersion = fh.CreatorVersion&0xff00 | 20 // preserve compatibility byte
fh.ReaderVersion = 20
fh.SetMode(0600) fh.SetMode(0600)
m, err := e.wz.CreateRaw(fh) m, err := e.wz.CreateRaw(fh)
@ -62,7 +65,7 @@ func (e *EPUBZip) Copy(fz *zip.File) error {
return e.wz.Copy(fz) return e.wz.Copy(fz)
} }
// Write image. They are already compressed, so we write them down directly. // WriteRaw Write image. They are already compressed, so we write them down directly.
func (e *EPUBZip) WriteRaw(raw *ZipImage) error { func (e *EPUBZip) WriteRaw(raw *ZipImage) error {
m, err := e.wz.CreateRaw(raw.Header) m, err := e.wz.CreateRaw(raw.Header)
if err != nil { if err != nil {
@ -72,7 +75,7 @@ func (e *EPUBZip) WriteRaw(raw *ZipImage) error {
return err return err
} }
// Write file. Compressed it using deflate. // WriteContent Write file. Compressed it using deflate.
func (e *EPUBZip) WriteContent(file string, content []byte) error { func (e *EPUBZip) WriteContent(file string, content []byte) error {
m, err := e.wz.CreateHeader(&zip.FileHeader{ m, err := e.wz.CreateHeader(&zip.FileHeader{
Name: file, Name: file,

View File

@ -17,7 +17,7 @@ type ZipImage struct {
Data []byte Data []byte
} }
// create gzip encoded jpeg // CompressImage create gzip encoded jpeg
func CompressImage(filename string, format string, img image.Image, quality int) (*ZipImage, error) { func CompressImage(filename string, format string, img image.Image, quality int) (*ZipImage, error) {
var ( var (
data, cdata bytes.Buffer data, cdata bytes.Buffer

View File

@ -7,23 +7,23 @@ import (
"sync" "sync"
) )
type EPUBZipStorageImageWriter struct { type StorageImageWriter struct {
fh *os.File fh *os.File
fz *zip.Writer fz *zip.Writer
format string format string
mut *sync.Mutex mut *sync.Mutex
} }
func NewEPUBZipStorageImageWriter(filename string, format string) (*EPUBZipStorageImageWriter, error) { func NewStorageImageWriter(filename string, format string) (*StorageImageWriter, error) {
fh, err := os.Create(filename) fh, err := os.Create(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fz := zip.NewWriter(fh) fz := zip.NewWriter(fh)
return &EPUBZipStorageImageWriter{fh, fz, format, &sync.Mutex{}}, nil return &StorageImageWriter{fh, fz, format, &sync.Mutex{}}, nil
} }
func (e *EPUBZipStorageImageWriter) Close() error { func (e *StorageImageWriter) Close() error {
if err := e.fz.Close(); err != nil { if err := e.fz.Close(); err != nil {
e.fh.Close() e.fh.Close()
return err return err
@ -31,7 +31,7 @@ func (e *EPUBZipStorageImageWriter) Close() error {
return e.fh.Close() return e.fh.Close()
} }
func (e *EPUBZipStorageImageWriter) Add(filename string, img image.Image, quality int) error { func (e *StorageImageWriter) Add(filename string, img image.Image, quality int) error {
zipImage, err := CompressImage(filename, e.format, img, quality) zipImage, err := CompressImage(filename, e.format, img, quality)
if err != nil { if err != nil {
return err return err
@ -51,7 +51,7 @@ func (e *EPUBZipStorageImageWriter) Add(filename string, img image.Image, qualit
return nil return nil
} }
type EPUBZipStorageImageReader struct { type StorageImageReader struct {
filename string filename string
fh *os.File fh *os.File
fz *zip.Reader fz *zip.Reader
@ -59,7 +59,7 @@ type EPUBZipStorageImageReader struct {
files map[string]*zip.File files map[string]*zip.File
} }
func NewEPUBZipStorageImageReader(filename string) (*EPUBZipStorageImageReader, error) { func NewStorageImageReader(filename string) (*StorageImageReader, error) {
fh, err := os.Open(filename) fh, err := os.Open(filename)
if err != nil { if err != nil {
return nil, err return nil, err
@ -76,14 +76,14 @@ func NewEPUBZipStorageImageReader(filename string) (*EPUBZipStorageImageReader,
for _, z := range fz.File { for _, z := range fz.File {
files[z.Name] = z files[z.Name] = z
} }
return &EPUBZipStorageImageReader{filename, fh, fz, files}, nil return &StorageImageReader{filename, fh, fz, files}, nil
} }
func (e *EPUBZipStorageImageReader) Get(filename string) *zip.File { func (e *StorageImageReader) Get(filename string) *zip.File {
return e.files[filename] return e.files[filename]
} }
func (e *EPUBZipStorageImageReader) Size(filename string) uint64 { func (e *StorageImageReader) Size(filename string) uint64 {
img := e.Get(filename) img := e.Get(filename)
if img != nil { if img != nil {
return img.CompressedSize64 + 30 + uint64(len(img.Name)) return img.CompressedSize64 + 30 + uint64(len(img.Name))
@ -91,10 +91,10 @@ func (e *EPUBZipStorageImageReader) Size(filename string) uint64 {
return 0 return 0
} }
func (e *EPUBZipStorageImageReader) Close() error { func (e *StorageImageReader) Close() error {
return e.fh.Close() return e.fh.Close()
} }
func (e *EPUBZipStorageImageReader) Remove() error { func (e *StorageImageReader) Remove() error {
return os.Remove(e.filename) return os.Remove(e.filename)
} }

View File

@ -1,7 +1,7 @@
/* /*
sortpath support sorting of path that may include number. Package sortpath support sorting of path that may include number.
A series of path can looks like: A series of path can look like:
- Tome1/Chap1/Image1.jpg - Tome1/Chap1/Image1.jpg
- Tome1/Chap2/Image1.jpg - Tome1/Chap2/Image1.jpg
- Tome1/Chap10/Image2.jpg - Tome1/Chap10/Image2.jpg
@ -11,8 +11,8 @@ and compare them by decomposing the string and number part.
The module support 3 mode: The module support 3 mode:
- mode=0 alpha for path and file - mode=0 alpha for path and file
- mode=1 alphanum for path and alpha for file - mode=1 alphanumeric for path and alpha for file
- mode=2 alphanum for path and file - mode=2 alphanumeric for path and file
Example: Example:
@ -26,6 +26,8 @@ Example:
*/ */
package sortpath package sortpath
import "sort"
// struct that implement interface for sort.Sort // struct that implement interface for sort.Sort
type by struct { type by struct {
filenames []string filenames []string
@ -39,9 +41,9 @@ func (b by) Swap(i, j int) {
b.paths[i], b.paths[j] = b.paths[j], b.paths[i] b.paths[i], b.paths[j] = b.paths[j], b.paths[i]
} }
// use sortpath.By with sort.Sort // By use sortpath.By with sort.Sort
func By(filenames []string, mode int) by { func By(filenames []string, mode int) sort.Interface {
p := [][]part{} var p [][]part
for _, filename := range filenames { for _, filename := range filenames {
p = append(p, parse(filename, mode)) p = append(p, parse(filename, mode))
} }

View File

@ -8,7 +8,7 @@ import (
) )
// Strings follow with numbers like: s1, s1.2, s2-3, ... // Strings follow with numbers like: s1, s1.2, s2-3, ...
var split_path_regex = regexp.MustCompile(`^(.*?)(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?$`) var splitPathRegex = regexp.MustCompile(`^(.*?)(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?$`)
type part struct { type part struct {
fullname string fullname string
@ -31,7 +31,7 @@ func (a part) compare(b part) float64 {
// separate from the string the number part. // separate from the string the number part.
func parsePart(p string) part { func parsePart(p string) part {
r := split_path_regex.FindStringSubmatch(p) r := splitPathRegex.FindStringSubmatch(p)
if len(r) == 0 { if len(r) == 0 {
return part{p, p, 0} return part{p, p, 0}
} }
@ -43,23 +43,23 @@ func parsePart(p string) part {
} }
// mode=0 alpha for path and file // mode=0 alpha for path and file
// mode=1 alphanum for path and alpha for file // mode=1 alphanumeric for path and alpha for file
// mode=2 alphanum for path and file // mode=2 alphanumeric for path and file
func parse(filename string, mode int) []part { func parse(filename string, mode int) []part {
pathname, name := filepath.Split(strings.ToLower(filename)) pathname, name := filepath.Split(strings.ToLower(filename))
pathname = strings.TrimSuffix(pathname, string(filepath.Separator)) pathname = strings.TrimSuffix(pathname, string(filepath.Separator))
ext := filepath.Ext(name) ext := filepath.Ext(name)
name = name[0 : len(name)-len(ext)] name = name[0 : len(name)-len(ext)]
f := []part{} var f []part
for _, p := range strings.Split(pathname, string(filepath.Separator)) { for _, p := range strings.Split(pathname, string(filepath.Separator)) {
if mode > 0 { // alphanum for path if mode > 0 { // alphanumeric for path
f = append(f, parsePart(p)) f = append(f, parsePart(p))
} else { } else {
f = append(f, part{p, p, 0}) f = append(f, part{p, p, 0})
} }
} }
if mode == 2 { // alphanum for file if mode == 2 { // alphanumeric for file
f = append(f, parsePart(name)) f = append(f, parsePart(name))
} else { } else {
f = append(f, part{name, name, 0}) f = append(f, part{name, name, 0})
@ -67,7 +67,7 @@ func parse(filename string, mode int) []part {
return f return f
} }
// compare 2 fullpath splitted into parts // compare 2 full path split into parts
func compareParts(a, b []part) float64 { func compareParts(a, b []part) float64 {
m := len(a) m := len(a)
if m > len(b) { if m > len(b) {

44
main.go
View File

@ -1,7 +1,7 @@
/* /*
Convert CBZ/CBR/Dir into EPUB for e-reader devices (Kindle Devices, ...) Convert CBZ/CBR/Dir into EPUB for e-reader devices (Kindle Devices, ...)
My goal is to make a simple, crossplatform, and fast tool to convert comics into EPUB. My goal is to make a simple, cross-platform, and fast tool to convert comics into EPUB.
EPUB is now support by Amazon through [SendToKindle](https://www.amazon.com/gp/sendtokindle/), by Email or by using the App. So I've made it simple to support the size limit constraint of those services. EPUB is now support by Amazon through [SendToKindle](https://www.amazon.com/gp/sendtokindle/), by Email or by using the App. So I've made it simple to support the size limit constraint of those services.
*/ */
@ -43,7 +43,7 @@ func main() {
fmt.Fprintln(os.Stderr, "failed to fetch the latest version") fmt.Fprintln(os.Stderr, "failed to fetch the latest version")
os.Exit(1) os.Exit(1)
} }
latest_version := v.Versions[0] latestVersion := v.Versions[0]
fmt.Fprintf(os.Stderr, `go-comic-converter fmt.Fprintf(os.Stderr, `go-comic-converter
Path : %s Path : %s
@ -57,9 +57,9 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
bi.Main.Path, bi.Main.Path,
bi.Main.Sum, bi.Main.Sum,
bi.Main.Version, bi.Main.Version,
latest_version.Original(), latestVersion.Original(),
latest_version.Segments()[0], latestVersion.Segments()[0],
latest_version.Original(), latestVersion.Original(),
) )
return return
} }
@ -122,23 +122,23 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
Quiet: cmd.Options.Quiet, Quiet: cmd.Options.Quiet,
Json: cmd.Options.Json, Json: cmd.Options.Json,
Image: &epuboptions.Image{ Image: &epuboptions.Image{
Crop: &epuboptions.Crop{Enabled: cmd.Options.Crop, Left: cmd.Options.CropRatioLeft, Up: cmd.Options.CropRatioUp, Right: cmd.Options.CropRatioRight, Bottom: cmd.Options.CropRatioBottom}, Crop: &epuboptions.Crop{Enabled: cmd.Options.Crop, Left: cmd.Options.CropRatioLeft, Up: cmd.Options.CropRatioUp, Right: cmd.Options.CropRatioRight, Bottom: cmd.Options.CropRatioBottom},
Quality: cmd.Options.Quality, Quality: cmd.Options.Quality,
Brightness: cmd.Options.Brightness, Brightness: cmd.Options.Brightness,
Contrast: cmd.Options.Contrast, Contrast: cmd.Options.Contrast,
AutoContrast: cmd.Options.AutoContrast, AutoContrast: cmd.Options.AutoContrast,
AutoRotate: cmd.Options.AutoRotate, AutoRotate: cmd.Options.AutoRotate,
AutoSplitDoublePage: cmd.Options.AutoSplitDoublePage, AutoSplitDoublePage: cmd.Options.AutoSplitDoublePage,
KeepDoublePageIfSplitted: cmd.Options.KeepDoublePageIfSplitted, KeepDoublePageIfSplit: cmd.Options.KeepDoublePageIfSplit,
NoBlankImage: cmd.Options.NoBlankImage, NoBlankImage: cmd.Options.NoBlankImage,
Manga: cmd.Options.Manga, Manga: cmd.Options.Manga,
HasCover: cmd.Options.HasCover, HasCover: cmd.Options.HasCover,
View: &epuboptions.View{Width: profile.Width, Height: profile.Height, AspectRatio: cmd.Options.AspectRatio, PortraitOnly: cmd.Options.PortraitOnly, Color: epuboptions.Color{Foreground: cmd.Options.ForegroundColor, Background: cmd.Options.BackgroundColor}}, View: &epuboptions.View{Width: profile.Width, Height: profile.Height, AspectRatio: cmd.Options.AspectRatio, PortraitOnly: cmd.Options.PortraitOnly, Color: epuboptions.Color{Foreground: cmd.Options.ForegroundColor, Background: cmd.Options.BackgroundColor}},
GrayScale: cmd.Options.Grayscale, GrayScale: cmd.Options.Grayscale,
GrayScaleMode: cmd.Options.GrayscaleMode, GrayScaleMode: cmd.Options.GrayscaleMode,
Resize: !cmd.Options.NoResize, Resize: !cmd.Options.NoResize,
Format: cmd.Options.Format, Format: cmd.Options.Format,
AppleBookCompatibility: cmd.Options.AppleBookCompatibility, AppleBookCompatibility: cmd.Options.AppleBookCompatibility,
}, },
}).Write(); err != nil { }).Write(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)