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")
-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

View File

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

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"
"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),

View File

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

View File

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

View File

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

View File

@ -2,13 +2,13 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Page {{ .Id }}</title>
<title>Page {{ .Id }}_p{{ .Part}}</title>
<link href="style.css" type="text/css" rel="stylesheet"/>
<meta name="viewport" content="width={{ .Width }}, height={{ .Height }}"/>
</head>
<body style="">
<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 id="PV">
<div id="PV-TL">
@ -25,16 +25,16 @@
</div>
</div>
<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 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 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 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>
</body>
</html>

71
main.go
View File

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