mirror of
https://github.com/celogeek/go-comic-converter.git
synced 2025-06-27 15:49:55 +02:00
add auto split double page
This commit is contained in:
parent
991e95f02e
commit
7adea5ddbd
README.md
internal/epub
main.go@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
35
internal/epub/filters/crop.go
Normal file
35
internal/epub/filters/crop.go
Normal 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)
|
||||
}
|
@ -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),
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
71
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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user