add auto split double page

This commit is contained in:
Celogeek 2023-01-14 18:11:22 +01:00
parent 991e95f02e
commit 7adea5ddbd
Signed by: celogeek
GPG Key ID: E6B7BDCFC446233A
9 changed files with 167 additions and 76 deletions

View File

@ -80,6 +80,8 @@ Usage of go-comic-converter:
Author of the epub (default "GO Comic Converter") Author of the epub (default "GO Comic Converter")
-autorotate -autorotate
Auto Rotate page when width > height Auto Rotate page when width > height
-autosplitdoublepage
Auto Split double page when width > height
-brightness int -brightness int
Brightness readjustement: between -100 and 100, > 0 lighter, < 0 darker Brightness readjustement: between -100 and 100, > 0 lighter, < 0 darker
-contrast int -contrast int

View File

@ -12,16 +12,17 @@ import (
) )
type ImageOptions struct { type ImageOptions struct {
Crop bool Crop bool
ViewWidth int ViewWidth int
ViewHeight int ViewHeight int
Quality int Quality int
Algo string Algo string
Palette color.Palette Palette color.Palette
Brightness int Brightness int
Contrast int Contrast int
AutoRotate bool AutoRotate bool
Workers int AutoSplitDoublePage bool
Workers int
} }
type EpubOptions struct { type EpubOptions struct {
@ -184,7 +185,7 @@ func (e *ePub) Write() error {
wz.WriteImage(part.Cover.Data) wz.WriteImage(part.Cover.Data)
for _, img := range part.Images { 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 { if err := wz.WriteFile(text, e.render(textTmpl, img)); err != nil {
return err return err
} }

View File

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

View File

@ -4,7 +4,10 @@ import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"compress/flate" "compress/flate"
"fmt"
"hash/crc32" "hash/crc32"
"image"
"image/jpeg"
"time" "time"
) )
@ -17,13 +20,23 @@ func (img *ImageData) CompressedSize() uint64 {
return img.Header.CompressedSize64 + 30 + uint64(len(img.Header.Name)) 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{}) cdata := bytes.NewBuffer([]byte{})
wcdata, err := flate.NewWriter(cdata, flate.BestCompression) wcdata, err := flate.NewWriter(cdata, flate.BestCompression)
if err != nil { if err != nil {
panic(err) panic(err)
} }
wcdata.Write(data) wcdata.Write(data.Bytes())
wcdata.Close() wcdata.Close()
if err != nil { if err != nil {
panic(err) panic(err)
@ -33,8 +46,8 @@ func newImageData(name string, data []byte) *ImageData {
&zip.FileHeader{ &zip.FileHeader{
Name: name, Name: name,
CompressedSize64: uint64(cdata.Len()), CompressedSize64: uint64(cdata.Len()),
UncompressedSize64: uint64(len(data)), UncompressedSize64: uint64(data.Len()),
CRC32: crc32.Checksum(data, crc32.IEEETable), CRC32: crc32.Checksum(data.Bytes(), crc32.IEEETable),
Method: zip.Deflate, Method: zip.Deflate,
ModifiedTime: uint16(t.Second()/2 + t.Minute()<<5 + t.Hour()<<11), ModifiedTime: uint16(t.Second()/2 + t.Minute()<<5 + t.Hour()<<11),
ModifiedDate: uint16(t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9), ModifiedDate: uint16(t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9),

View File

@ -23,3 +23,29 @@ func NewGift(options *ImageOptions) *gift.GIFT {
) )
return g 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
}

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"image/jpeg"
"io" "io"
"io/fs" "io/fs"
"os" "os"
@ -24,6 +23,7 @@ import (
type Image struct { type Image struct {
Id int Id int
Part int
Data *ImageData Data *ImageData
Width int Width int
Height int Height int
@ -154,23 +154,29 @@ func LoadImages(path string, options *ImageOptions) ([]*Image, error) {
g.Draw(dst, src) 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{ imageOutput <- &Image{
img.Id, img.Id,
newImageData(name, b.Bytes()), 0,
newImageData(img.Id, 0, dst, options.Quality),
dst.Bounds().Dx(), dst.Bounds().Dx(),
dst.Bounds().Dy(), 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) bar := NewBar(imageCount, "Processing", 1, 2)
for image := range imageOutput { for image := range imageOutput {
images = append(images, image) images = append(images, image)
bar.Add(1) if image.Part == 0 {
bar.Add(1)
}
} }
bar.Close() bar.Close()
@ -192,7 +200,12 @@ func LoadImages(path string, options *ImageOptions) ([]*Image, error) {
} }
sort.Slice(images, func(i, j int) bool { 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 return images, nil

View File

@ -26,18 +26,14 @@
<item id="css" href="Text/style.css" media-type="text/css"/> <item id="css" href="Text/style.css" media-type="text/css"/>
<item id="page_part" href="Text/part.xhtml" media-type="application/xhtml+xml"/> <item id="page_part" href="Text/part.xhtml" media-type="application/xhtml+xml"/>
{{ range .Images }} {{ range .Images }}
<item id="page_{{ .Id }}" href="Text/{{ .Id }}.xhtml" media-type="application/xhtml+xml"/> <item id="page_{{ .Id }}_p{{ .Part}}" href="Text/{{ .Id }}_p{{ .Part}}.xhtml" media-type="application/xhtml+xml"/>
<item id="img_{{ .Id }}" href="Images/{{ .Id }}.jpg" media-type="image/jpeg"/> <item id="img_{{ .Id }}_p{{ .Part}}" href="Images/{{ .Id }}_p{{ .Part}}.jpg" media-type="image/jpeg"/>
{{ end }} {{ end }}
</manifest> </manifest>
<spine page-progression-direction="ltr" toc="ncx"> <spine page-progression-direction="ltr" toc="ncx">
<itemref idref="page_part" linear="yes" properties="page-spread-right"/> <itemref idref="page_part" linear="yes" properties="page-spread-right"/>
{{ range $idx, $ := .Images }} {{ range $idx, $ := .Images }}
{{ if mod $idx 2 }} <itemref idref="page_{{ $.Id }}_p{{ $.Part }}" linear="yes" properties="page-spread-{{ if mod $idx 2 }}left{{ else }}right{{ end }}"/>
<itemref idref="page_{{ $.Id }}" linear="yes" properties="page-spread-left"/>
{{ else }}
<itemref idref="page_{{ $.Id }}" linear="yes" properties="page-spread-right"/>
{{ end }}
{{ end }} {{ end }}
</spine> </spine>
</package> </package>

View File

@ -2,13 +2,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head> <head>
<title>Page {{ .Id }}</title> <title>Page {{ .Id }}_p{{ .Part}}</title>
<link href="style.css" type="text/css" rel="stylesheet"/> <link href="style.css" type="text/css" rel="stylesheet"/>
<meta name="viewport" content="width={{ .Width }}, height={{ .Height }}"/> <meta name="viewport" content="width={{ .Width }}, height={{ .Height }}"/>
</head> </head>
<body style=""> <body style="">
<div style="text-align:center;top:0.0%;"> <div style="text-align:center;top:0.0%;">
<img width="{{ .Width }}" height="{{ .Height }}" src="../Images/{{ .Id }}.jpg"/> <img width="{{ .Width }}" height="{{ .Height }}" src="../Images/{{ .Id }}_p{{ .Part}}.jpg"/>
</div> </div>
<div id="PV"> <div id="PV">
<div id="PV-TL"> <div id="PV-TL">
@ -25,16 +25,16 @@
</div> </div>
</div> </div>
<div class="PV-P" id="PV-TL-P" style=""> <div class="PV-P" id="PV-TL-P" style="">
<img style="position:absolute;left:0;top:0;" src="../Images/{{ .Id }}.jpg" width="{{ zoom .Width 1.5 }}" height="{{ zoom .Height 1.5 }}"/> <img style="position:absolute;left:0;top:0;" src="../Images/{{ .Id }}_p{{ .Part}}.jpg" width="{{ zoom .Width 1.5 }}" height="{{ zoom .Height 1.5 }}"/>
</div> </div>
<div class="PV-P" id="PV-TR-P" style=""> <div class="PV-P" id="PV-TR-P" style="">
<img style="position:absolute;right:0;top:0;" src="../Images/{{ .Id }}.jpg" width="{{ zoom .Width 1.5 }}" height="{{ zoom .Height 1.5 }}"/> <img style="position:absolute;right:0;top:0;" src="../Images/{{ .Id }}_p{{ .Part}}.jpg" width="{{ zoom .Width 1.5 }}" height="{{ zoom .Height 1.5 }}"/>
</div> </div>
<div class="PV-P" id="PV-BL-P" style=""> <div class="PV-P" id="PV-BL-P" style="">
<img style="position:absolute;left:0;bottom:0;" src="../Images/{{ .Id }}.jpg" width="{{ zoom .Width 1.5 }}" height="{{ zoom .Height 1.5 }}"/> <img style="position:absolute;left:0;bottom:0;" src="../Images/{{ .Id }}_p{{ .Part}}.jpg" width="{{ zoom .Width 1.5 }}" height="{{ zoom .Height 1.5 }}"/>
</div> </div>
<div class="PV-P" id="PV-BR-P" style=""> <div class="PV-P" id="PV-BR-P" style="">
<img style="position:absolute;right:0;bottom:0;" src="../Images/{{ .Id }}.jpg" width="{{ zoom .Width 1.5 }}" height="{{ zoom .Height 1.5 }}"/> <img style="position:absolute;right:0;bottom:0;" src="../Images/{{ .Id }}_p{{ .Part}}.jpg" width="{{ zoom .Width 1.5 }}" height="{{ zoom .Height 1.5 }}"/>
</div> </div>
</body> </body>
</html> </html>

71
main.go
View File

@ -57,18 +57,19 @@ func init() {
} }
type Option struct { type Option struct {
Input string Input string
Output string Output string
Profile string Profile string
Author string Author string
Title string Title string
Quality int Quality int
NoCrop bool NoCrop bool
Brightness int Brightness int
Contrast int Contrast int
AutoRotate bool AutoRotate bool
Workers int AutoSplitDoublePage bool
LimitMb int Workers int
LimitMb int
} }
func (o *Option) String() string { func (o *Option) String() string {
@ -89,18 +90,19 @@ func (o *Option) String() string {
return fmt.Sprintf(`Go Comic Converter return fmt.Sprintf(`Go Comic Converter
Options: Options:
Input : %s Input : %s
Output : %s Output : %s
Profile : %s - %s - %dx%d - %d levels of gray Profile : %s - %s - %dx%d - %d levels of gray
Author : %s Author : %s
Title : %s Title : %s
Quality : %d Quality : %d
Crop : %v Crop : %v
Brightness: %d Brightness : %d
Contrast : %d Contrast : %d
AutoRotate: %v AutoRotate : %v
LimitMb : %s AutoSplitDoublePage: %v
Workers : %d LimitMb : %s
Workers : %d
`, `,
o.Input, o.Input,
o.Output, o.Output,
@ -112,6 +114,7 @@ Options:
o.Brightness, o.Brightness,
o.Contrast, o.Contrast,
o.AutoRotate, o.AutoRotate,
o.AutoSplitDoublePage,
limitmb, limitmb,
o.Workers, 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.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.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.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.LimitMb, "limitmb", 0, "Limit size of the ePub: Default nolimit (0), Minimum 20")
flag.IntVar(&opt.Workers, "workers", runtime.NumCPU(), "Number of workers") flag.IntVar(&opt.Workers, "workers", runtime.NumCPU(), "Number of workers")
flag.Usage = func() { flag.Usage = func() {
@ -231,15 +235,16 @@ func main() {
Title: opt.Title, Title: opt.Title,
Author: opt.Author, Author: opt.Author,
ImageOptions: &epub.ImageOptions{ ImageOptions: &epub.ImageOptions{
ViewWidth: profile.Width, ViewWidth: profile.Width,
ViewHeight: profile.Height, ViewHeight: profile.Height,
Quality: opt.Quality, Quality: opt.Quality,
Crop: !opt.NoCrop, Crop: !opt.NoCrop,
Palette: profile.Palette, Palette: profile.Palette,
Brightness: opt.Brightness, Brightness: opt.Brightness,
Contrast: opt.Contrast, Contrast: opt.Contrast,
AutoRotate: opt.AutoRotate, AutoRotate: opt.AutoRotate,
Workers: opt.Workers, AutoSplitDoublePage: opt.AutoSplitDoublePage,
Workers: opt.Workers,
}, },
}).Write(); err != nil { }).Write(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)