diff --git a/README.md b/README.md index b079c83..d1f051b 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ Usage of go-comic-converter: Author of the epub (default "GO Comic Converter") -autorotate Auto Rotate page when width > height + -autosplitdoublepage + Auto Split double page when width > height -brightness int Brightness readjustement: between -100 and 100, > 0 lighter, < 0 darker -contrast int diff --git a/internal/epub/core.go b/internal/epub/core.go index 09602f5..4eb742f 100644 --- a/internal/epub/core.go +++ b/internal/epub/core.go @@ -12,16 +12,17 @@ import ( ) type ImageOptions struct { - Crop bool - ViewWidth int - ViewHeight int - Quality int - Algo string - Palette color.Palette - Brightness int - Contrast int - AutoRotate bool - Workers int + Crop bool + ViewWidth int + ViewHeight int + Quality int + Algo string + Palette color.Palette + Brightness int + Contrast int + AutoRotate bool + AutoSplitDoublePage bool + Workers int } type EpubOptions struct { @@ -184,7 +185,7 @@ func (e *ePub) Write() error { wz.WriteImage(part.Cover.Data) for _, img := range part.Images { - text := fmt.Sprintf("OEBPS/Text/%d.xhtml", img.Id) + text := fmt.Sprintf("OEBPS/Text/%d_p%d.xhtml", img.Id, img.Part) if err := wz.WriteFile(text, e.render(textTmpl, img)); err != nil { return err } diff --git a/internal/epub/filters/crop.go b/internal/epub/filters/crop.go new file mode 100644 index 0000000..59fa577 --- /dev/null +++ b/internal/epub/filters/crop.go @@ -0,0 +1,35 @@ +package filters + +import ( + "image" + "image/draw" + + "github.com/disintegration/gift" +) + +func CropSplitDoublePage(right bool) *cropSplitDoublePage { + return &cropSplitDoublePage{right} +} + +type cropSplitDoublePage struct { + right bool +} + +func (p *cropSplitDoublePage) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { + if p.right { + dstBounds = image.Rectangle{ + Min: image.Point{srcBounds.Max.X / 2, srcBounds.Min.Y}, + Max: srcBounds.Max, + } + } else { + dstBounds = image.Rectangle{ + Min: srcBounds.Min, + Max: image.Point{srcBounds.Max.X / 2, srcBounds.Max.Y}, + } + } + return +} + +func (p *cropSplitDoublePage) Draw(dst draw.Image, src image.Image, options *gift.Options) { + gift.Crop(dst.Bounds()).Draw(dst, src, options) +} diff --git a/internal/epub/image_data.go b/internal/epub/image_data.go index a07a707..be8226b 100644 --- a/internal/epub/image_data.go +++ b/internal/epub/image_data.go @@ -4,7 +4,10 @@ import ( "archive/zip" "bytes" "compress/flate" + "fmt" "hash/crc32" + "image" + "image/jpeg" "time" ) @@ -17,13 +20,23 @@ func (img *ImageData) CompressedSize() uint64 { return img.Header.CompressedSize64 + 30 + uint64(len(img.Header.Name)) } -func newImageData(name string, data []byte) *ImageData { +func newImageData(id int, part int, img image.Image, quality int) *ImageData { + name := fmt.Sprintf("OEBPS/Images/%d_p%d.jpg", id, part) + if id == 0 { + name = "OEBPS/Images/cover.jpg" + } + + data := bytes.NewBuffer([]byte{}) + if err := jpeg.Encode(data, img, &jpeg.Options{Quality: quality}); err != nil { + panic(err) + } + cdata := bytes.NewBuffer([]byte{}) wcdata, err := flate.NewWriter(cdata, flate.BestCompression) if err != nil { panic(err) } - wcdata.Write(data) + wcdata.Write(data.Bytes()) wcdata.Close() if err != nil { panic(err) @@ -33,8 +46,8 @@ func newImageData(name string, data []byte) *ImageData { &zip.FileHeader{ Name: name, CompressedSize64: uint64(cdata.Len()), - UncompressedSize64: uint64(len(data)), - CRC32: crc32.Checksum(data, crc32.IEEETable), + UncompressedSize64: uint64(data.Len()), + CRC32: crc32.Checksum(data.Bytes(), crc32.IEEETable), Method: zip.Deflate, ModifiedTime: uint16(t.Second()/2 + t.Minute()<<5 + t.Hour()<<11), ModifiedDate: uint16(t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9), diff --git a/internal/epub/image_filters.go b/internal/epub/image_filters.go index 5345c77..c2ec6e5 100644 --- a/internal/epub/image_filters.go +++ b/internal/epub/image_filters.go @@ -23,3 +23,29 @@ func NewGift(options *ImageOptions) *gift.GIFT { ) return g } + +func NewGiftSplitDoublePage(options *ImageOptions) []*gift.GIFT { + gifts := make([]*gift.GIFT, 2) + + gifts[0] = gift.New( + filters.CropSplitDoublePage(false), + ) + + gifts[1] = gift.New( + filters.CropSplitDoublePage(true), + ) + + for _, g := range gifts { + if options.Contrast != 0 { + g.Add(gift.Contrast(float32(options.Contrast))) + } + if options.Brightness != 0 { + g.Add(gift.Brightness(float32(options.Brightness))) + } + g.Add( + gift.ResizeToFit(options.ViewWidth, options.ViewHeight, gift.LanczosResampling), + ) + } + + return gifts +} diff --git a/internal/epub/image_processing.go b/internal/epub/image_processing.go index 8f1d4eb..9421703 100644 --- a/internal/epub/image_processing.go +++ b/internal/epub/image_processing.go @@ -6,7 +6,6 @@ import ( "fmt" "image" "image/color" - "image/jpeg" "io" "io/fs" "os" @@ -24,6 +23,7 @@ import ( type Image struct { Id int + Part int Data *ImageData Width int Height int @@ -154,23 +154,29 @@ func LoadImages(path string, options *ImageOptions) ([]*Image, error) { g.Draw(dst, src) } - // Encode image - b := bytes.NewBuffer([]byte{}) - err = jpeg.Encode(b, dst, &jpeg.Options{Quality: options.Quality}) - if err != nil { - panic(err) - } - - name := fmt.Sprintf("OEBPS/Images/%d.jpg", img.Id) - if img.Id == 0 { - name = "OEBPS/Images/cover.jpg" - } imageOutput <- &Image{ img.Id, - newImageData(name, b.Bytes()), + 0, + newImageData(img.Id, 0, dst, options.Quality), dst.Bounds().Dx(), dst.Bounds().Dy(), } + + if options.AutoSplitDoublePage && src.Bounds().Dx() > src.Bounds().Dy() { + gifts := NewGiftSplitDoublePage(options) + for i, g := range gifts { + part := i + 1 + dst := image.NewPaletted(g.Bounds(src.Bounds()), options.Palette) + g.Draw(dst, src) + imageOutput <- &Image{ + img.Id, + part, + newImageData(img.Id, part, dst, options.Quality), + dst.Bounds().Dx(), + dst.Bounds().Dy(), + } + } + } } }() } @@ -183,7 +189,9 @@ func LoadImages(path string, options *ImageOptions) ([]*Image, error) { bar := NewBar(imageCount, "Processing", 1, 2) for image := range imageOutput { images = append(images, image) - bar.Add(1) + if image.Part == 0 { + bar.Add(1) + } } bar.Close() @@ -192,7 +200,12 @@ func LoadImages(path string, options *ImageOptions) ([]*Image, error) { } sort.Slice(images, func(i, j int) bool { - return images[i].Id < images[j].Id + if images[i].Id < images[j].Id { + return true + } else if images[i].Id == images[j].Id && images[i].Part < images[j].Part { + return true + } + return false }) return images, nil diff --git a/internal/epub/templates/content.opf.tmpl b/internal/epub/templates/content.opf.tmpl index 0ab3aeb..a212c0b 100644 --- a/internal/epub/templates/content.opf.tmpl +++ b/internal/epub/templates/content.opf.tmpl @@ -26,18 +26,14 @@ {{ range .Images }} - - + + {{ end }} {{ range $idx, $ := .Images }} -{{ if mod $idx 2 }} - -{{ else }} - -{{ end }} + {{ end }} diff --git a/internal/epub/templates/text.xhtml.tmpl b/internal/epub/templates/text.xhtml.tmpl index be1fcc6..921eda8 100644 --- a/internal/epub/templates/text.xhtml.tmpl +++ b/internal/epub/templates/text.xhtml.tmpl @@ -2,13 +2,13 @@ -Page {{ .Id }} +Page {{ .Id }}_p{{ .Part}}
- +
@@ -25,16 +25,16 @@
- +
- +
- +
- +
\ No newline at end of file diff --git a/main.go b/main.go index 0234099..f40193c 100644 --- a/main.go +++ b/main.go @@ -57,18 +57,19 @@ func init() { } type Option struct { - Input string - Output string - Profile string - Author string - Title string - Quality int - NoCrop bool - Brightness int - Contrast int - AutoRotate bool - Workers int - LimitMb int + Input string + Output string + Profile string + Author string + Title string + Quality int + NoCrop bool + Brightness int + Contrast int + AutoRotate bool + AutoSplitDoublePage bool + Workers int + LimitMb int } func (o *Option) String() string { @@ -89,18 +90,19 @@ func (o *Option) String() string { return fmt.Sprintf(`Go Comic Converter Options: - Input : %s - Output : %s - Profile : %s - %s - %dx%d - %d levels of gray - Author : %s - Title : %s - Quality : %d - Crop : %v - Brightness: %d - Contrast : %d - AutoRotate: %v - LimitMb : %s - Workers : %d + Input : %s + Output : %s + Profile : %s - %s - %dx%d - %d levels of gray + Author : %s + Title : %s + Quality : %d + Crop : %v + Brightness : %d + Contrast : %d + AutoRotate : %v + AutoSplitDoublePage: %v + LimitMb : %s + Workers : %d `, o.Input, o.Output, @@ -112,6 +114,7 @@ Options: o.Brightness, o.Contrast, o.AutoRotate, + o.AutoSplitDoublePage, limitmb, o.Workers, ) @@ -140,6 +143,7 @@ func main() { flag.IntVar(&opt.Brightness, "brightness", 0, "Brightness readjustement: between -100 and 100, > 0 lighter, < 0 darker") flag.IntVar(&opt.Contrast, "contrast", 0, "Contrast readjustement: between -100 and 100, > 0 more contrast, < 0 less contrast") flag.BoolVar(&opt.AutoRotate, "autorotate", false, "Auto Rotate page when width > height") + flag.BoolVar(&opt.AutoSplitDoublePage, "autosplitdoublepage", false, "Auto Split double page when width > height") flag.IntVar(&opt.LimitMb, "limitmb", 0, "Limit size of the ePub: Default nolimit (0), Minimum 20") flag.IntVar(&opt.Workers, "workers", runtime.NumCPU(), "Number of workers") flag.Usage = func() { @@ -231,15 +235,16 @@ func main() { Title: opt.Title, Author: opt.Author, ImageOptions: &epub.ImageOptions{ - ViewWidth: profile.Width, - ViewHeight: profile.Height, - Quality: opt.Quality, - Crop: !opt.NoCrop, - Palette: profile.Palette, - Brightness: opt.Brightness, - Contrast: opt.Contrast, - AutoRotate: opt.AutoRotate, - Workers: opt.Workers, + ViewWidth: profile.Width, + ViewHeight: profile.Height, + Quality: opt.Quality, + Crop: !opt.NoCrop, + Palette: profile.Palette, + Brightness: opt.Brightness, + Contrast: opt.Contrast, + AutoRotate: opt.AutoRotate, + AutoSplitDoublePage: opt.AutoSplitDoublePage, + Workers: opt.Workers, }, }).Write(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err)