Compare commits

...

5 Commits

Author SHA1 Message Date
0fdbf5baca
improve image processor 2023-04-27 19:00:27 +02:00
12cd279bb8
use img helper 2023-04-27 18:33:04 +02:00
06cc6d2788
fix duplicate blank page if last page is double 2023-04-27 18:32:48 +02:00
42eba9cb3b
move options to common package 2023-04-27 18:08:32 +02:00
ba538b431b
move compress image to zip package 2023-04-27 17:20:37 +02:00
11 changed files with 403 additions and 388 deletions

View File

@ -14,7 +14,8 @@ import (
"time"
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
epubimageprocessing "github.com/celogeek/go-comic-converter/v2/internal/epub/imageprocessing"
epubimageprocessor "github.com/celogeek/go-comic-converter/v2/internal/epub/imageprocessor"
epuboptions "github.com/celogeek/go-comic-converter/v2/internal/epub/options"
epubprogress "github.com/celogeek/go-comic-converter/v2/internal/epub/progress"
epubtemplates "github.com/celogeek/go-comic-converter/v2/internal/epub/templates"
epubtree "github.com/celogeek/go-comic-converter/v2/internal/epub/tree"
@ -22,37 +23,23 @@ import (
"github.com/gofrs/uuid"
)
type Options struct {
Input string
Output string
Title string
Author string
LimitMb int
StripFirstDirectoryFromToc bool
Dry bool
DryVerbose bool
SortPathMode int
Quiet bool
Workers int
Image *epubimage.Options
}
type ePub struct {
*Options
*epuboptions.Options
UID string
Publisher string
UpdatedAt string
templateProcessor *template.Template
imageProcessor *epubimageprocessor.EpubImageProcessor
}
type epubPart struct {
Cover *epubimage.Image
Images []*epubimage.Image
Cover *epubimageprocessor.LoadedImage
LoadedImages epubimageprocessor.LoadedImages
}
// initialize epub
func New(options *Options) *ePub {
func New(options *epuboptions.Options) *ePub {
uid := uuid.Must(uuid.NewV4())
tmpl := template.New("parser")
tmpl.Funcs(template.FuncMap{
@ -66,6 +53,7 @@ func New(options *Options) *ePub {
Publisher: "GO Comic Converter",
UpdatedAt: time.Now().UTC().Format("2006-01-02T15:04:05Z"),
templateProcessor: tmpl,
imageProcessor: epubimageprocessor.New(options),
}
}
@ -80,19 +68,19 @@ func (e *ePub) render(templateString string, data map[string]any) string {
}
// write image to the zip
func (e *ePub) writeImage(wz *epubzip.EpubZip, img *epubimage.Image) error {
err := wz.WriteFile(
fmt.Sprintf("OEBPS/%s", img.TextPath()),
func (e *ePub) writeImage(wz *epubzip.EpubZip, img *epubimageprocessor.LoadedImage) error {
err := wz.WriteContent(
fmt.Sprintf("OEBPS/%s", img.Image.TextPath()),
[]byte(e.render(epubtemplates.Text, map[string]any{
"Title": fmt.Sprintf("Image %d Part %d", img.Id, img.Part),
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.ViewWidth, e.Image.ViewHeight),
"ImagePath": img.ImgPath(),
"ImageStyle": img.ImgStyle(e.Image.ViewWidth, e.Image.ViewHeight, e.Image.Manga),
"Title": fmt.Sprintf("Image %d Part %d", img.Image.Id, img.Image.Part),
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.View.Width, e.Image.View.Height),
"ImagePath": img.Image.ImgPath(),
"ImageStyle": img.Image.ImgStyle(e.Image.View.Width, e.Image.View.Height, e.Image.Manga),
})),
)
if err == nil {
err = wz.WriteImage(img.Data)
err = wz.WriteRaw(img.ZipImage)
}
return err
@ -100,48 +88,41 @@ func (e *ePub) writeImage(wz *epubzip.EpubZip, img *epubimage.Image) error {
// write blank page
func (e *ePub) writeBlank(wz *epubzip.EpubZip, img *epubimage.Image) error {
return wz.WriteFile(
fmt.Sprintf("OEBPS/Text/%d_sp.xhtml", img.Id),
return wz.WriteContent(
fmt.Sprintf("OEBPS/%s", img.SpacePath()),
[]byte(e.render(epubtemplates.Blank, map[string]any{
"Title": fmt.Sprintf("Blank Page %d", img.Id),
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.ViewWidth, e.Image.ViewHeight),
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.View.Width, e.Image.View.Height),
})),
)
}
// extract image and split it into part
func (e *ePub) getParts() ([]*epubPart, error) {
images, err := epubimageprocessing.LoadImages(&epubimageprocessing.Options{
Input: e.Input,
SortPathMode: e.SortPathMode,
Quiet: e.Quiet,
Dry: e.Dry,
Workers: e.Workers,
Image: e.Image,
})
loadedImages, err := e.imageProcessor.Load()
if err != nil {
return nil, err
}
// sort result by id and part
sort.Slice(images, func(i, j int) bool {
if images[i].Id == images[j].Id {
return images[i].Part < images[j].Part
sort.Slice(loadedImages, func(i, j int) bool {
if loadedImages[i].Image.Id == loadedImages[j].Image.Id {
return loadedImages[i].Image.Part < loadedImages[j].Image.Part
}
return images[i].Id < images[j].Id
return loadedImages[i].Image.Id < loadedImages[j].Image.Id
})
parts := make([]*epubPart, 0)
cover := images[0]
cover := loadedImages[0]
if e.Image.HasCover {
images = images[1:]
loadedImages = loadedImages[1:]
}
if e.Dry {
parts = append(parts, &epubPart{
Cover: cover,
Images: images,
LoadedImages: loadedImages,
})
return parts, nil
}
@ -150,28 +131,28 @@ func (e *ePub) getParts() ([]*epubPart, error) {
maxSize := uint64(e.LimitMb * 1024 * 1024)
xhtmlSize := uint64(1024)
// descriptor files + title
baseSize := uint64(16*1024) + cover.Data.CompressedSize()
baseSize := uint64(16*1024) + cover.ZipImage.CompressedSize()
if e.Image.HasCover {
baseSize += cover.Data.CompressedSize()
baseSize += cover.ZipImage.CompressedSize()
}
currentSize := baseSize
currentImages := make([]*epubimage.Image, 0)
currentImages := make([]*epubimageprocessor.LoadedImage, 0)
part := 1
for _, img := range images {
imgSize := img.Data.CompressedSize() + xhtmlSize
for _, img := range loadedImages {
imgSize := img.ZipImage.CompressedSize() + xhtmlSize
if maxSize > 0 && len(currentImages) > 0 && currentSize+imgSize > maxSize {
parts = append(parts, &epubPart{
Cover: cover,
Images: currentImages,
LoadedImages: currentImages,
})
part += 1
currentSize = baseSize
if !e.Image.HasCover {
currentSize += cover.Data.CompressedSize()
currentSize += cover.ZipImage.CompressedSize()
}
currentImages = make([]*epubimage.Image, 0)
currentImages = make([]*epubimageprocessor.LoadedImage, 0)
}
currentSize += imgSize
currentImages = append(currentImages, img)
@ -179,7 +160,7 @@ func (e *ePub) getParts() ([]*epubPart, error) {
if len(currentImages) > 0 {
parts = append(parts, &epubPart{
Cover: cover,
Images: currentImages,
LoadedImages: currentImages,
})
}
@ -220,12 +201,12 @@ func (e *ePub) Write() error {
if e.Dry {
p := epubParts[0]
fmt.Fprintf(os.Stderr, "TOC:\n - %s\n%s\n", e.Title, e.getTree(p.Images, true))
fmt.Fprintf(os.Stderr, "TOC:\n - %s\n%s\n", e.Title, e.getTree(p.LoadedImages.Images(), true))
if e.DryVerbose {
if e.Image.HasCover {
fmt.Fprintf(os.Stderr, "Cover:\n%s\n", e.getTree([]*epubimage.Image{p.Cover}, false))
fmt.Fprintf(os.Stderr, "Cover:\n%s\n", e.getTree([]*epubimage.Image{p.Cover.Image}, false))
}
fmt.Fprintf(os.Stderr, "Files:\n%s\n", e.getTree(p.Images, false))
fmt.Fprintf(os.Stderr, "Files:\n%s\n", e.getTree(p.LoadedImages.Images(), false))
}
return nil
}
@ -270,21 +251,21 @@ func (e *ePub) Write() error {
Publisher: e.Publisher,
UpdatedAt: e.UpdatedAt,
ImageOptions: e.Image,
Cover: part.Cover,
Images: part.Images,
Cover: part.Cover.Image,
Images: part.LoadedImages.Images(),
Current: i + 1,
Total: totalParts,
})},
{"OEBPS/toc.xhtml", epubtemplates.Toc(title, e.StripFirstDirectoryFromToc, part.Images)},
{"OEBPS/toc.xhtml", epubtemplates.Toc(title, e.StripFirstDirectoryFromToc, part.LoadedImages.Images())},
{"OEBPS/Text/style.css", e.render(epubtemplates.Style, map[string]any{
"PageWidth": e.Image.ViewWidth,
"PageHeight": e.Image.ViewHeight,
"PageWidth": e.Image.View.Width,
"PageHeight": e.Image.View.Height,
})},
{"OEBPS/Text/title.xhtml", e.render(epubtemplates.Text, map[string]any{
"Title": title,
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.ViewWidth, e.Image.ViewHeight),
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.View.Width, e.Image.View.Height),
"ImagePath": "Images/title.jpg",
"ImageStyle": part.Cover.ImgStyle(e.Image.ViewWidth, e.Image.ViewHeight, e.Image.Manga),
"ImageStyle": part.Cover.Image.ImgStyle(e.Image.View.Width, e.Image.View.Height, e.Image.Manga),
})},
}
@ -292,11 +273,11 @@ func (e *ePub) Write() error {
return err
}
for _, c := range content {
if err := wz.WriteFile(c.Name, []byte(c.Content)); err != nil {
if err := wz.WriteContent(c.Name, []byte(c.Content)); err != nil {
return err
}
}
if err := wz.WriteImage(epubimageprocessing.CoverTitleData(part.Cover.Raw, title, e.Image.Quality)); err != nil {
if err := wz.WriteRaw(e.imageProcessor.CoverTitleData(part.Cover.Image.Raw, title)); err != nil {
return err
}
@ -308,14 +289,15 @@ func (e *ePub) Write() error {
}
}
for i, img := range part.Images {
for i, img := range part.LoadedImages {
if err := e.writeImage(wz, img); err != nil {
return err
}
// Double Page or Last Image
if img.DoublePage || (i+1 == len(part.Images)) {
if err := e.writeBlank(wz, img); err != nil {
// Double Page or Last Image that is not a double page
if img.Image.DoublePage ||
(img.Image.Part == 0 && i+1 == len(part.LoadedImages)) {
if err := e.writeBlank(wz, img.Image); err != nil {
return err
}
}

View File

@ -6,15 +6,12 @@ package epubimage
import (
"fmt"
"image"
epubimagedata "github.com/celogeek/go-comic-converter/v2/internal/epub/imagedata"
)
type Image struct {
Id int
Part int
Raw image.Image
Data *epubimagedata.ImageData
Width int
Height int
IsCover bool

View File

@ -1,20 +0,0 @@
package epubimage
// options for image transformation
type Options struct {
Crop bool
CropRatioLeft int
CropRatioUp int
CropRatioRight int
CropRatioBottom int
ViewWidth int
ViewHeight int
Quality int
Brightness int
Contrast int
AutoRotate bool
AutoSplitDoublePage bool
NoBlankPage bool
Manga bool
HasCover bool
}

View File

@ -1,203 +0,0 @@
/*
Extract and transform image into a compressed jpeg.
*/
package epubimageprocessing
import (
"image"
"path/filepath"
"strings"
"sync"
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
epubimagedata "github.com/celogeek/go-comic-converter/v2/internal/epub/imagedata"
epubimagefilters "github.com/celogeek/go-comic-converter/v2/internal/epub/imagefilters"
epubprogress "github.com/celogeek/go-comic-converter/v2/internal/epub/progress"
"github.com/disintegration/gift"
)
// only accept jpg, png and webp as source file
func isSupportedImage(path string) bool {
switch strings.ToLower(filepath.Ext(path)) {
case ".jpg", ".jpeg", ".png", ".webp":
{
return true
}
}
return false
}
// extract and convert images
func LoadImages(o *Options) ([]*epubimage.Image, error) {
images := make([]*epubimage.Image, 0)
imageCount, imageInput, err := o.Load()
if err != nil {
return nil, err
}
// dry run, skip convertion
if o.Dry {
for img := range imageInput {
images = append(images, &epubimage.Image{
Id: img.Id,
Path: img.Path,
Name: img.Name,
})
}
return images, nil
}
imageOutput := make(chan *epubimage.Image)
// processing
bar := epubprogress.New(epubprogress.Options{
Quiet: o.Quiet,
Max: imageCount,
Description: "Processing",
CurrentJob: 1,
TotalJob: 2,
})
wg := &sync.WaitGroup{}
for i := 0; i < o.WorkersRatio(50); i++ {
wg.Add(1)
go func() {
defer wg.Done()
for img := range imageInput {
src := img.Image
for part, dst := range TransformImage(src, img.Id, o.Image) {
var raw image.Image
if img.Id == 0 && part == 0 {
raw = dst
}
imageOutput <- &epubimage.Image{
Id: img.Id,
Part: part,
Raw: raw,
Data: epubimagedata.New(img.Id, part, dst, o.Image.Quality),
Width: dst.Bounds().Dx(),
Height: dst.Bounds().Dy(),
IsCover: img.Id == 0 && part == 0,
DoublePage: part == 0 && src.Bounds().Dx() > src.Bounds().Dy(),
Path: img.Path,
Name: img.Name,
}
}
}
}()
}
go func() {
wg.Wait()
close(imageOutput)
}()
for img := range imageOutput {
if img.Part == 0 {
bar.Add(1)
}
if o.Image.NoBlankPage && img.Width == 1 && img.Height == 1 {
continue
}
images = append(images, img)
}
bar.Close()
if len(images) == 0 {
return nil, errNoImagesFound
}
return images, nil
}
// create a title page with the cover
func CoverTitleData(img image.Image, title string, quality int) *epubimagedata.ImageData {
// Create a blur version of the cover
g := gift.New(epubimagefilters.CoverTitle(title))
dst := image.NewGray(g.Bounds(img.Bounds()))
g.Draw(dst, img)
return epubimagedata.NewRaw("OEBPS/Images/title.jpg", dst, quality)
}
// transform image into 1 or 3 images
// only doublepage with autosplit has 3 versions
func TransformImage(src image.Image, srcId int, o *epubimage.Options) []image.Image {
var filters, splitFilter []gift.Filter
var images []image.Image
if o.Crop {
f := epubimagefilters.AutoCrop(
src,
o.CropRatioLeft,
o.CropRatioUp,
o.CropRatioRight,
o.CropRatioBottom,
)
filters = append(filters, f)
splitFilter = append(splitFilter, f)
}
if o.AutoRotate && src.Bounds().Dx() > src.Bounds().Dy() {
filters = append(filters, gift.Rotate90())
}
if o.Contrast != 0 {
f := gift.Contrast(float32(o.Contrast))
filters = append(filters, f)
splitFilter = append(splitFilter, f)
}
if o.Brightness != 0 {
f := gift.Brightness(float32(o.Brightness))
filters = append(filters, f)
splitFilter = append(splitFilter, f)
}
filters = append(filters,
epubimagefilters.Resize(o.ViewWidth, o.ViewHeight, gift.LanczosResampling),
epubimagefilters.Pixel(),
)
// convert
{
g := gift.New(filters...)
dst := image.NewGray(g.Bounds(src.Bounds()))
g.Draw(dst, src)
images = append(images, dst)
}
// auto split off
if !o.AutoSplitDoublePage {
return images
}
// portrait, no need to split
if src.Bounds().Dx() <= src.Bounds().Dy() {
return images
}
// cover
if o.HasCover && srcId == 0 {
return images
}
// convert double page
for _, b := range []bool{o.Manga, !o.Manga} {
g := gift.New(splitFilter...)
g.Add(
epubimagefilters.CropSplitDoublePage(b),
epubimagefilters.Resize(o.ViewWidth, o.ViewHeight, gift.LanczosResampling),
)
dst := image.NewGray(g.Bounds(src.Bounds()))
g.Draw(dst, src)
images = append(images, dst)
}
return images
}

View File

@ -0,0 +1,220 @@
/*
Extract and transform image into a compressed jpeg.
*/
package epubimageprocessor
import (
"fmt"
"image"
"sync"
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
epubimagefilters "github.com/celogeek/go-comic-converter/v2/internal/epub/imagefilters"
epuboptions "github.com/celogeek/go-comic-converter/v2/internal/epub/options"
epubprogress "github.com/celogeek/go-comic-converter/v2/internal/epub/progress"
epubzip "github.com/celogeek/go-comic-converter/v2/internal/epub/zip"
"github.com/disintegration/gift"
)
type LoadedImage struct {
Image *epubimage.Image
ZipImage *epubzip.ZipImage
}
type LoadedImages []*LoadedImage
func (l LoadedImages) Images() []*epubimage.Image {
res := make([]*epubimage.Image, len(l))
for i, v := range l {
res[i] = v.Image
}
return res
}
type EpubImageProcessor struct {
*epuboptions.Options
}
func New(o *epuboptions.Options) *EpubImageProcessor {
return &EpubImageProcessor{o}
}
// extract and convert images
func (e *EpubImageProcessor) Load() (LoadedImages, error) {
images := make(LoadedImages, 0)
imageCount, imageInput, err := e.load()
if err != nil {
return nil, err
}
// dry run, skip convertion
if e.Dry {
for img := range imageInput {
images = append(images, &LoadedImage{
Image: &epubimage.Image{
Id: img.Id,
Path: img.Path,
Name: img.Name,
},
})
}
return images, nil
}
imageOutput := make(chan *LoadedImage)
// processing
bar := epubprogress.New(epubprogress.Options{
Quiet: e.Quiet,
Max: imageCount,
Description: "Processing",
CurrentJob: 1,
TotalJob: 2,
})
wg := &sync.WaitGroup{}
for i := 0; i < e.WorkersRatio(50); i++ {
wg.Add(1)
go func() {
defer wg.Done()
for input := range imageInput {
src := input.Image
for part, dst := range e.transformImage(src, input.Id) {
var raw image.Image
if input.Id == 0 && part == 0 {
raw = dst
}
img := &epubimage.Image{
Id: input.Id,
Part: part,
Raw: raw,
Width: dst.Bounds().Dx(),
Height: dst.Bounds().Dy(),
IsCover: input.Id == 0 && part == 0,
DoublePage: part == 0 && src.Bounds().Dx() > src.Bounds().Dy(),
Path: input.Path,
Name: input.Name,
}
imageOutput <- &LoadedImage{
Image: img,
ZipImage: epubzip.CompressImage(fmt.Sprintf("OEBPS/%s", img.ImgPath()), dst, e.Image.Quality),
}
}
}
}()
}
go func() {
wg.Wait()
close(imageOutput)
}()
for output := range imageOutput {
if output.Image.Part == 0 {
bar.Add(1)
}
if e.Image.NoBlankPage && output.Image.Width == 1 && output.Image.Height == 1 {
continue
}
images = append(images, output)
}
bar.Close()
if len(images) == 0 {
return nil, errNoImagesFound
}
return images, nil
}
// transform image into 1 or 3 images
// only doublepage with autosplit has 3 versions
func (e *EpubImageProcessor) transformImage(src image.Image, srcId int) []image.Image {
var filters, splitFilter []gift.Filter
var images []image.Image
if e.Image.Crop.Enabled {
f := epubimagefilters.AutoCrop(
src,
e.Image.Crop.Left,
e.Image.Crop.Up,
e.Image.Crop.Right,
e.Image.Crop.Bottom,
)
filters = append(filters, f)
splitFilter = append(splitFilter, f)
}
if e.Image.AutoRotate && src.Bounds().Dx() > src.Bounds().Dy() {
filters = append(filters, gift.Rotate90())
}
if e.Image.Contrast != 0 {
f := gift.Contrast(float32(e.Image.Contrast))
filters = append(filters, f)
splitFilter = append(splitFilter, f)
}
if e.Image.Brightness != 0 {
f := gift.Brightness(float32(e.Image.Brightness))
filters = append(filters, f)
splitFilter = append(splitFilter, f)
}
filters = append(filters,
epubimagefilters.Resize(e.Image.View.Width, e.Image.View.Height, gift.LanczosResampling),
epubimagefilters.Pixel(),
)
// convert
{
g := gift.New(filters...)
dst := image.NewGray(g.Bounds(src.Bounds()))
g.Draw(dst, src)
images = append(images, dst)
}
// auto split off
if !e.Image.AutoSplitDoublePage {
return images
}
// portrait, no need to split
if src.Bounds().Dx() <= src.Bounds().Dy() {
return images
}
// cover
if e.Image.HasCover && srcId == 0 {
return images
}
// convert double page
for _, b := range []bool{e.Image.Manga, !e.Image.Manga} {
g := gift.New(splitFilter...)
g.Add(
epubimagefilters.CropSplitDoublePage(b),
epubimagefilters.Resize(e.Image.View.Width, e.Image.View.Height, gift.LanczosResampling),
)
dst := image.NewGray(g.Bounds(src.Bounds()))
g.Draw(dst, src)
images = append(images, dst)
}
return images
}
// create a title page with the cover
func (e *EpubImageProcessor) CoverTitleData(img image.Image, title string) *epubzip.ZipImage {
// Create a blur version of the cover
g := gift.New(epubimagefilters.CoverTitle(title))
dst := image.NewGray(g.Bounds(img.Bounds()))
g.Draw(dst, img)
return epubzip.CompressImage("OEBPS/Images/title.jpg", dst, e.Image.Quality)
}

View File

@ -1,4 +1,4 @@
package epubimageprocessing
package epubimageprocessor
import (
"archive/zip"
@ -18,7 +18,6 @@ import (
_ "golang.org/x/image/webp"
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
"github.com/celogeek/go-comic-converter/v2/internal/sortpath"
"github.com/nwaples/rardecode/v2"
pdfimage "github.com/raff/pdfreader/image"
@ -32,43 +31,37 @@ type tasks struct {
Name string
}
type Options struct {
Input string
SortPathMode int
Quiet bool
Dry bool
Workers int
Image *epubimage.Options
}
var errNoImagesFound = errors.New("no images found")
func (o *Options) WorkersRatio(pct int) (nbWorkers int) {
nbWorkers = o.Workers * pct / 100
if nbWorkers < 1 {
nbWorkers = 1
// only accept jpg, png and webp as source file
func (e *EpubImageProcessor) isSupportedImage(path string) bool {
switch strings.ToLower(filepath.Ext(path)) {
case ".jpg", ".jpeg", ".png", ".webp":
{
return true
}
return
}
return false
}
// Load images from input
func (o *Options) Load() (totalImages int, output chan *tasks, err error) {
fi, err := os.Stat(o.Input)
func (e *EpubImageProcessor) load() (totalImages int, output chan *tasks, err error) {
fi, err := os.Stat(e.Input)
if err != nil {
return
}
// get all images though a channel of bytes
if fi.IsDir() {
return o.loadDir()
return e.loadDir()
} else {
switch ext := strings.ToLower(filepath.Ext(o.Input)); ext {
switch ext := strings.ToLower(filepath.Ext(e.Input)); ext {
case ".cbz", ".zip":
return o.loadCbz()
return e.loadCbz()
case ".cbr", ".rar":
return o.loadCbr()
return e.loadCbr()
case ".pdf":
return o.loadPdf()
return e.loadPdf()
default:
err = fmt.Errorf("unknown file format (%s): support .cbz, .zip, .cbr, .rar, .pdf", ext)
return
@ -77,15 +70,15 @@ func (o *Options) Load() (totalImages int, output chan *tasks, err error) {
}
// load a directory of images
func (o *Options) loadDir() (totalImages int, output chan *tasks, err error) {
func (e *EpubImageProcessor) loadDir() (totalImages int, output chan *tasks, err error) {
images := make([]string, 0)
input := filepath.Clean(o.Input)
input := filepath.Clean(e.Input)
err = filepath.WalkDir(input, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && isSupportedImage(path) {
if !d.IsDir() && e.isSupportedImage(path) {
images = append(images, path)
}
return nil
@ -103,7 +96,7 @@ func (o *Options) loadDir() (totalImages int, output chan *tasks, err error) {
return
}
sort.Sort(sortpath.By(images, o.SortPathMode))
sort.Sort(sortpath.By(images, e.SortPathMode))
// Queue all file with id
type job struct {
@ -119,15 +112,15 @@ func (o *Options) loadDir() (totalImages int, output chan *tasks, err error) {
}()
// read in parallel and get an image
output = make(chan *tasks, o.Workers)
output = make(chan *tasks, e.Workers)
wg := &sync.WaitGroup{}
for j := 0; j < o.WorkersRatio(50); j++ {
for j := 0; j < e.WorkersRatio(50); j++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
var img image.Image
if !o.Dry {
if !e.Dry {
f, err := os.Open(job.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "\nerror processing image %s: %s\n", job.Path, err)
@ -167,15 +160,15 @@ func (o *Options) loadDir() (totalImages int, output chan *tasks, err error) {
}
// load a zip file that include images
func (o *Options) loadCbz() (totalImages int, output chan *tasks, err error) {
r, err := zip.OpenReader(o.Input)
func (e *EpubImageProcessor) loadCbz() (totalImages int, output chan *tasks, err error) {
r, err := zip.OpenReader(e.Input)
if err != nil {
return
}
images := make([]*zip.File, 0)
for _, f := range r.File {
if !f.FileInfo().IsDir() && isSupportedImage(f.Name) {
if !f.FileInfo().IsDir() && e.isSupportedImage(f.Name) {
images = append(images, f)
}
}
@ -192,7 +185,7 @@ func (o *Options) loadCbz() (totalImages int, output chan *tasks, err error) {
for _, img := range images {
names = append(names, img.Name)
}
sort.Sort(sortpath.By(names, o.SortPathMode))
sort.Sort(sortpath.By(names, e.SortPathMode))
indexedNames := make(map[string]int)
for i, name := range names {
@ -211,15 +204,15 @@ func (o *Options) loadCbz() (totalImages int, output chan *tasks, err error) {
}
}()
output = make(chan *tasks, o.Workers)
output = make(chan *tasks, e.Workers)
wg := &sync.WaitGroup{}
for j := 0; j < o.WorkersRatio(50); j++ {
for j := 0; j < e.WorkersRatio(50); j++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
var img image.Image
if !o.Dry {
if !e.Dry {
f, err := job.F.Open()
if err != nil {
fmt.Fprintf(os.Stderr, "\nerror processing image %s: %s\n", job.F.Name, err)
@ -253,16 +246,16 @@ func (o *Options) loadCbz() (totalImages int, output chan *tasks, err error) {
}
// load a rar file that include images
func (o *Options) loadCbr() (totalImages int, output chan *tasks, err error) {
func (e *EpubImageProcessor) loadCbr() (totalImages int, output chan *tasks, err error) {
var isSolid bool
files, err := rardecode.List(o.Input)
files, err := rardecode.List(e.Input)
if err != nil {
return
}
names := make([]string, 0)
for _, f := range files {
if !f.IsDir && isSupportedImage(f.Name) {
if !f.IsDir && e.isSupportedImage(f.Name) {
if f.Solid {
isSolid = true
}
@ -276,7 +269,7 @@ func (o *Options) loadCbr() (totalImages int, output chan *tasks, err error) {
return
}
sort.Sort(sortpath.By(names, o.SortPathMode))
sort.Sort(sortpath.By(names, e.SortPathMode))
indexedNames := make(map[string]int)
for i, name := range names {
@ -292,10 +285,10 @@ func (o *Options) loadCbr() (totalImages int, output chan *tasks, err error) {
jobs := make(chan *job)
go func() {
defer close(jobs)
if isSolid && !o.Dry {
r, rerr := rardecode.OpenReader(o.Input)
if isSolid && !e.Dry {
r, rerr := rardecode.OpenReader(e.Input)
if rerr != nil {
fmt.Fprintf(os.Stderr, "\nerror processing image %s: %s\n", o.Input, rerr)
fmt.Fprintf(os.Stderr, "\nerror processing image %s: %s\n", e.Input, rerr)
os.Exit(1)
}
defer r.Close()
@ -330,15 +323,15 @@ func (o *Options) loadCbr() (totalImages int, output chan *tasks, err error) {
}()
// send file to the queue
output = make(chan *tasks, o.Workers)
output = make(chan *tasks, e.Workers)
wg := &sync.WaitGroup{}
for j := 0; j < o.WorkersRatio(50); j++ {
for j := 0; j < e.WorkersRatio(50); j++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
var img image.Image
if !o.Dry {
if !e.Dry {
f, err := job.Open()
if err != nil {
fmt.Fprintf(os.Stderr, "\nerror processing image %s: %s\n", job.Name, err)
@ -370,8 +363,8 @@ func (o *Options) loadCbr() (totalImages int, output chan *tasks, err error) {
}
// extract image from a pdf
func (o *Options) loadPdf() (totalImages int, output chan *tasks, err error) {
pdf := pdfread.Load(o.Input)
func (e *EpubImageProcessor) loadPdf() (totalImages int, output chan *tasks, err error) {
pdf := pdfread.Load(e.Input)
if pdf == nil {
err = fmt.Errorf("can't read pdf")
return
@ -385,7 +378,7 @@ func (o *Options) loadPdf() (totalImages int, output chan *tasks, err error) {
defer pdf.Close()
for i := 0; i < totalImages; i++ {
var img image.Image
if !o.Dry {
if !e.Dry {
img, err = pdfimage.Extract(pdf, i+1)
if err != nil {
fmt.Fprintln(os.Stderr, err)

View File

@ -0,0 +1,49 @@
/*
Options for epub creation.
*/
package epuboptions
type Crop struct {
Enabled bool
Left, Up, Right, Bottom int
}
type View struct {
Width, Height int
}
type Image struct {
Crop *Crop
Quality int
Brightness int
Contrast int
AutoRotate bool
AutoSplitDoublePage bool
NoBlankPage bool
Manga bool
HasCover bool
View *View
}
type Options struct {
Input string
Output string
Title string
Author string
LimitMb int
StripFirstDirectoryFromToc bool
Dry bool
DryVerbose bool
SortPathMode int
Quiet bool
Workers int
Image *Image
}
func (o *Options) WorkersRatio(pct int) (nbWorkers int) {
nbWorkers = o.Workers * pct / 100
if nbWorkers < 1 {
nbWorkers = 1
}
return
}

View File

@ -5,6 +5,7 @@ import (
"github.com/beevik/etree"
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
epuboptions "github.com/celogeek/go-comic-converter/v2/internal/epub/options"
)
type ContentOptions struct {
@ -13,7 +14,7 @@ type ContentOptions struct {
Author string
Publisher string
UpdatedAt string
ImageOptions *epubimage.Options
ImageOptions *epuboptions.Image
Cover *epubimage.Image
Images []*epubimage.Image
Current int
@ -91,7 +92,7 @@ func getMeta(o *ContentOptions) []tag {
{"meta", tagAttrs{"property": "schema:accessibilityHazard"}, "noSoundHazard"},
{"meta", tagAttrs{"name": "book-type", "content": "comic"}, ""},
{"opf:meta", tagAttrs{"name": "fixed-layout", "content": "true"}, ""},
{"opf:meta", tagAttrs{"name": "original-resolution", "content": fmt.Sprintf("%dx%d", o.ImageOptions.ViewWidth, o.ImageOptions.ViewHeight)}, ""},
{"opf:meta", tagAttrs{"name": "original-resolution", "content": fmt.Sprintf("%dx%d", o.ImageOptions.View.Width, o.ImageOptions.View.Height)}, ""},
{"dc:title", tagAttrs{}, o.Title},
{"dc:identifier", tagAttrs{"id": "ean"}, fmt.Sprintf("urn:uuid:%s", o.UID)},
{"dc:language", tagAttrs{}, "en"},
@ -149,7 +150,10 @@ func getManifest(o *ContentOptions) []tag {
}
items = append(items, itag(img), htag(img))
}
items = append(items, stag(o.Images[len(o.Images)-1]))
lastImage := o.Images[len(o.Images)-1]
if lastImage.Part == 0 {
items = append(items, stag(lastImage))
}
return items
}

View File

@ -9,8 +9,6 @@ import (
"archive/zip"
"os"
"time"
epubimagedata "github.com/celogeek/go-comic-converter/v2/internal/epub/imagedata"
)
type EpubZip struct {
@ -61,17 +59,17 @@ func (e *EpubZip) WriteMagic() error {
}
// Write image. They are already compressed, so we write them down directly.
func (e *EpubZip) WriteImage(image *epubimagedata.ImageData) error {
m, err := e.wz.CreateRaw(image.Header)
func (e *EpubZip) WriteRaw(raw *ZipImage) error {
m, err := e.wz.CreateRaw(raw.Header)
if err != nil {
return err
}
_, err = m.Write(image.Data)
_, err = m.Write(raw.Data)
return err
}
// Write file. Compressed it using deflate.
func (e *EpubZip) WriteFile(file string, content []byte) error {
func (e *EpubZip) WriteContent(file string, content []byte) error {
m, err := e.wz.CreateHeader(&zip.FileHeader{
Name: file,
Modified: time.Now(),

View File

@ -1,7 +1,4 @@
/*
prepare image to be store in a zip file.
*/
package epubimagedata
package epubzip
import (
"archive/zip"
@ -15,13 +12,13 @@ import (
"time"
)
type ImageData struct {
type ZipImage struct {
Header *zip.FileHeader
Data []byte
}
// compressed size of the image with the header
func (img *ImageData) CompressedSize() uint64 {
func (img *ZipImage) CompressedSize() uint64 {
return img.Header.CompressedSize64 + 30 + uint64(len(img.Header.Name))
}
@ -30,14 +27,8 @@ func exitWithError(err error) {
os.Exit(1)
}
// create a new data image with file name based on id and part
func New(id int, part int, img image.Image, quality int) *ImageData {
name := fmt.Sprintf("OEBPS/Images/%d_p%d.jpg", id, part)
return NewRaw(name, img, quality)
}
// create gzip encoded jpeg
func NewRaw(name string, img image.Image, quality int) *ImageData {
func CompressImage(filename string, img image.Image, quality int) *ZipImage {
var (
data, cdata bytes.Buffer
err error
@ -64,9 +55,9 @@ func NewRaw(name string, img image.Image, quality int) *ImageData {
}
t := time.Now()
return &ImageData{
return &ZipImage{
&zip.FileHeader{
Name: name,
Name: filename,
CompressedSize64: uint64(cdata.Len()),
UncompressedSize64: uint64(data.Len()),
CRC32: crc32.Checksum(data.Bytes(), crc32.IEEETable),

32
main.go
View File

@ -14,7 +14,7 @@ import (
"github.com/celogeek/go-comic-converter/v2/internal/converter"
"github.com/celogeek/go-comic-converter/v2/internal/epub"
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
epuboptions "github.com/celogeek/go-comic-converter/v2/internal/epub/options"
"github.com/tcnksm/go-latest"
)
@ -101,7 +101,7 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
profile := cmd.Options.GetProfile()
perfectWidth, perfectHeight := profile.PerfectDim()
if err := epub.New(&epub.Options{
if err := epub.New(&epuboptions.Options{
Input: cmd.Options.Input,
Output: cmd.Options.Output,
LimitMb: cmd.Options.LimitMb,
@ -109,15 +109,19 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
Author: cmd.Options.Author,
StripFirstDirectoryFromToc: cmd.Options.StripFirstDirectoryFromToc,
SortPathMode: cmd.Options.SortPathMode,
Image: &epubimage.Options{
ViewWidth: perfectWidth,
ViewHeight: perfectHeight,
Workers: cmd.Options.Workers,
Dry: cmd.Options.Dry,
DryVerbose: cmd.Options.DryVerbose,
Quiet: cmd.Options.Quiet,
Image: &epuboptions.Image{
Crop: &epuboptions.Crop{
Enabled: cmd.Options.Crop,
Left: cmd.Options.CropRatioLeft,
Up: cmd.Options.CropRatioUp,
Right: cmd.Options.CropRatioRight,
Bottom: cmd.Options.CropRatioBottom,
},
Quality: cmd.Options.Quality,
Crop: cmd.Options.Crop,
CropRatioLeft: cmd.Options.CropRatioLeft,
CropRatioUp: cmd.Options.CropRatioUp,
CropRatioRight: cmd.Options.CropRatioRight,
CropRatioBottom: cmd.Options.CropRatioBottom,
Brightness: cmd.Options.Brightness,
Contrast: cmd.Options.Contrast,
AutoRotate: cmd.Options.AutoRotate,
@ -125,11 +129,11 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
NoBlankPage: cmd.Options.NoBlankPage,
Manga: cmd.Options.Manga,
HasCover: cmd.Options.HasCover,
View: &epuboptions.View{
Width: perfectWidth,
Height: perfectHeight,
},
},
Workers: cmd.Options.Workers,
Dry: cmd.Options.Dry,
DryVerbose: cmd.Options.DryVerbose,
Quiet: cmd.Options.Quiet,
}).Write(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)