mirror of
https://github.com/celogeek/go-comic-converter.git
synced 2025-06-27 23:59:55 +02:00
add auto split double page
This commit is contained in:
parent
991e95f02e
commit
7adea5ddbd
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
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"
|
"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),
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
5
main.go
5
main.go
@ -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
|
||||||
}
|
}
|
||||||
@ -99,6 +100,7 @@ Options:
|
|||||||
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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user