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

@ -21,6 +21,7 @@ type ImageOptions struct {
Brightness int Brightness int
Contrast int Contrast int
AutoRotate bool AutoRotate bool
AutoSplitDoublePage bool
Workers int Workers int
} }
@ -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,8 +189,10 @@ 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)
if image.Part == 0 {
bar.Add(1) bar.Add(1)
} }
}
bar.Close() bar.Close()
if len(images) == 0 { if len(images) == 0 {
@ -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>

View File

@ -67,6 +67,7 @@ type Option struct {
Brightness int Brightness int
Contrast int Contrast int
AutoRotate bool AutoRotate bool
AutoSplitDoublePage bool
Workers int Workers int
LimitMb int LimitMb int
} }
@ -96,9 +97,10 @@ Options:
Title : %s Title : %s
Quality : %d Quality : %d
Crop : %v Crop : %v
Brightness: %d Brightness : %d
Contrast : %d Contrast : %d
AutoRotate: %v AutoRotate : %v
AutoSplitDoublePage: %v
LimitMb : %s LimitMb : %s
Workers : %d Workers : %d
`, `,
@ -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() {
@ -239,6 +243,7 @@ func main() {
Brightness: opt.Brightness, Brightness: opt.Brightness,
Contrast: opt.Contrast, Contrast: opt.Contrast,
AutoRotate: opt.AutoRotate, AutoRotate: opt.AutoRotate,
AutoSplitDoublePage: opt.AutoSplitDoublePage,
Workers: opt.Workers, Workers: opt.Workers,
}, },
}).Write(); err != nil { }).Write(); err != nil {