Split files

This commit is contained in:
Milan Nikolic
2024-11-06 05:10:12 +01:00
parent 85059d7df3
commit eff4730014
2 changed files with 401 additions and 390 deletions

View File

@@ -4,30 +4,16 @@ import (
"bytes"
"context"
"crypto/md5"
"errors"
"fmt"
"image"
_ "image/gif"
"image/png"
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"sync/atomic"
"github.com/gen2brain/avif"
"github.com/gen2brain/jpegli"
"github.com/gen2brain/jpegxl"
"github.com/gen2brain/webp"
"github.com/jsummers/gobmp"
"golang.org/x/image/tiff"
pngstructure "github.com/dsoprea/go-png-image-structure"
"github.com/dustin/go-humanize"
"github.com/gen2brain/go-fitz"
"github.com/gen2brain/go-unarr"
"golang.org/x/sync/errgroup"
)
// Options type.
@@ -151,382 +137,6 @@ func New(o Options) *Converter {
return c
}
// convertDocument converts PDF/EPUB document to CBZ.
func (c *Converter) convertDocument(ctx context.Context, fileName string) error {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
doc, err := fitz.New(fileName)
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
defer doc.Close()
c.Ncontents = doc.NumPage()
c.CurrContent = 0
if c.OnStart != nil {
c.OnStart()
}
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
for n := 0; n < c.Ncontents; n++ {
if ctx.Err() != nil {
return fmt.Errorf("convertDocument: %w", ctx.Err())
}
img, err := doc.Image(n)
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
if img != nil {
eg.Go(func() error {
return c.imageConvert(ctx, img, n, "")
})
}
}
err = eg.Wait()
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
return nil
}
// convertArchive converts archive to CBZ.
func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
contents, err := c.archiveList(fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
images := imagesFromSlice(contents)
c.Ncontents = len(images)
c.CurrContent = 0
if c.OnStart != nil {
c.OnStart()
}
cover := c.coverName(images)
archive, err := unarr.NewArchive(fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
defer archive.Close()
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
for {
if ctx.Err() != nil {
return fmt.Errorf("convertArchive: %w", ctx.Err())
}
err := archive.Entry()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return fmt.Errorf("convertArchive: %w", err)
}
data, err := archive.ReadAll()
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
pathName := archive.Name()
if isImage(pathName) {
if c.Opts.NoConvert {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
}
if cover == pathName && c.Opts.NoCover {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
}
var img image.Image
img, err = c.imageDecode(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
if c.Opts.NoRGB && !isGrayScale(img) {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
}
if img != nil {
eg.Go(func() error {
return c.imageConvert(ctx, img, 0, pathName)
})
}
} else if !c.Opts.NoNonImage {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
}
}
err = eg.Wait()
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
return nil
}
// convertDirectory converts directory to CBZ.
func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
contents, err := imagesFromPath(dirPath)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
images := imagesFromSlice(contents)
c.Ncontents = len(images)
c.CurrContent = 0
if c.OnStart != nil {
c.OnStart()
}
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
for index, img := range contents {
if ctx.Err() != nil {
return fmt.Errorf("convertDirectory: %w", ctx.Err())
}
file, err := os.Open(img)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if isNonImage(img) && !c.Opts.NoNonImage {
if err = copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
continue
} else if isImage(img) {
if c.Opts.NoConvert {
if err = copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
continue
}
var i image.Image
i, err = c.imageDecode(file)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if c.Opts.NoRGB && !isGrayScale(i) {
if err = copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
continue
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if i != nil {
eg.Go(func() error {
return c.imageConvert(ctx, i, index, img)
})
}
}
}
err = eg.Wait()
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
return nil
}
// imageConvert converts image.Image.
func (c *Converter) imageConvert(ctx context.Context, img image.Image, index int, pathName string) error {
err := ctx.Err()
if err != nil {
return fmt.Errorf("imageConvert: %w", err)
}
atomic.AddInt32(&c.CurrContent, 1)
if c.OnProgress != nil {
c.OnProgress()
}
ext := c.Opts.Format
if ext == "jpeg" {
ext = "jpg"
}
var fileName string
if pathName != "" {
fileName = filepath.Join(c.Workdir, fmt.Sprintf("%s.%s", baseNoExt(pathName), ext))
} else {
fileName = filepath.Join(c.Workdir, fmt.Sprintf("%03d.%s", index, ext))
}
img = c.imageTransform(img)
w, err := os.Create(fileName)
if err != nil {
return fmt.Errorf("imageConvert: %w", err)
}
defer w.Close()
if err := c.imageEncode(img, w); err != nil {
return fmt.Errorf("imageConvert: %w", err)
}
return nil
}
// imageTransform transforms image (resize, rotate, brightness, contrast).
func (c *Converter) imageTransform(img image.Image) image.Image {
var i = img
if c.Opts.Width > 0 || c.Opts.Height > 0 {
if c.Opts.Fit {
i = fit(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
} else {
i = resize(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
}
}
if c.Opts.Rotate > 0 {
switch c.Opts.Rotate {
case 90:
i = rotate(i, 90)
case 180:
i = rotate(i, 180)
case 270:
i = rotate(i, 270)
}
}
if c.Opts.Brightness != 0 {
i = brightness(i, float64(c.Opts.Brightness))
}
if c.Opts.Contrast != 0 {
i = contrast(i, float64(c.Opts.Contrast))
}
if c.Opts.Grayscale {
i = imageToGray(i)
}
return i
}
// imageDecode decodes image from reader.
func (c *Converter) imageDecode(reader io.Reader) (image.Image, error) {
img, _, err := image.Decode(reader)
if err != nil {
return img, fmt.Errorf("imageDecode: %w", err)
}
return img, nil
}
// imageEncode encodes image to file.
func (c *Converter) imageEncode(img image.Image, w io.Writer) error {
var err error
switch c.Opts.Format {
case "png":
err = png.Encode(w, img)
case "tiff":
err = tiff.Encode(w, img, &tiff.Options{Compression: tiff.Uncompressed})
case "jpeg":
opts := &jpegli.EncodingOptions{}
opts.Quality = c.Opts.Quality
opts.ChromaSubsampling = image.YCbCrSubsampleRatio420
opts.ProgressiveLevel = 2
opts.AdaptiveQuantization = true
opts.DCTMethod = jpegli.DefaultDCTMethod
err = jpegli.Encode(w, img, opts)
case "webp":
err = webp.Encode(w, img, webp.Options{Quality: c.Opts.Quality, Method: webp.DefaultMethod})
case "avif":
err = avif.Encode(w, img, avif.Options{Quality: c.Opts.Quality, Speed: avif.DefaultSpeed})
case "jxl":
err = jpegxl.Encode(w, img, jpegxl.Options{Quality: c.Opts.Quality, Effort: jpegxl.DefaultEffort})
case "bmp":
opts := &gobmp.EncoderOptions{}
opts.SupportTransparency(false)
err = gobmp.EncodeWithOptions(w, imageToPaletted(img), opts)
}
if err != nil {
return fmt.Errorf("imageEncode: %w", err)
}
return nil
}
// Cancel cancels the operation.
func (c *Converter) Cancel() {
if c.OnCancel != nil {

401
cbconvert_convert.go Normal file
View File

@@ -0,0 +1,401 @@
package cbconvert
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/png"
"io"
"os"
"path/filepath"
"runtime"
"sync/atomic"
"github.com/gen2brain/avif"
"github.com/gen2brain/go-fitz"
"github.com/gen2brain/go-unarr"
"github.com/gen2brain/jpegli"
"github.com/gen2brain/jpegxl"
"github.com/gen2brain/webp"
"github.com/jsummers/gobmp"
"golang.org/x/image/tiff"
"golang.org/x/sync/errgroup"
)
// convertDocument converts PDF/EPUB document to CBZ.
func (c *Converter) convertDocument(ctx context.Context, fileName string) error {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
doc, err := fitz.New(fileName)
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
defer doc.Close()
c.Ncontents = doc.NumPage()
c.CurrContent = 0
if c.OnStart != nil {
c.OnStart()
}
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
for n := 0; n < c.Ncontents; n++ {
if ctx.Err() != nil {
return fmt.Errorf("convertDocument: %w", ctx.Err())
}
img, err := doc.Image(n)
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
if img != nil {
eg.Go(func() error {
return c.imageConvert(ctx, img, n, "")
})
}
}
err = eg.Wait()
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
return nil
}
// convertArchive converts archive to CBZ.
func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
contents, err := c.archiveList(fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
images := imagesFromSlice(contents)
c.Ncontents = len(images)
c.CurrContent = 0
if c.OnStart != nil {
c.OnStart()
}
cover := c.coverName(images)
archive, err := unarr.NewArchive(fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
defer archive.Close()
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
for {
if ctx.Err() != nil {
return fmt.Errorf("convertArchive: %w", ctx.Err())
}
err := archive.Entry()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return fmt.Errorf("convertArchive: %w", err)
}
data, err := archive.ReadAll()
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
pathName := archive.Name()
if isImage(pathName) {
if c.Opts.NoConvert {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
}
if cover == pathName && c.Opts.NoCover {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
}
var img image.Image
img, err = c.imageDecode(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
if c.Opts.NoRGB && !isGrayScale(img) {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
}
if img != nil {
eg.Go(func() error {
return c.imageConvert(ctx, img, 0, pathName)
})
}
} else if !c.Opts.NoNonImage {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
}
}
err = eg.Wait()
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
return nil
}
// convertDirectory converts directory to CBZ.
func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
contents, err := imagesFromPath(dirPath)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
images := imagesFromSlice(contents)
c.Ncontents = len(images)
c.CurrContent = 0
if c.OnStart != nil {
c.OnStart()
}
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
for index, img := range contents {
if ctx.Err() != nil {
return fmt.Errorf("convertDirectory: %w", ctx.Err())
}
file, err := os.Open(img)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if isNonImage(img) && !c.Opts.NoNonImage {
if err = copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
continue
} else if isImage(img) {
if c.Opts.NoConvert {
if err = copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
continue
}
var i image.Image
i, err = c.imageDecode(file)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if c.Opts.NoRGB && !isGrayScale(i) {
if err = copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
continue
}
if err = file.Close(); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if i != nil {
eg.Go(func() error {
return c.imageConvert(ctx, i, index, img)
})
}
}
}
err = eg.Wait()
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
return nil
}
// imageConvert converts image.Image.
func (c *Converter) imageConvert(ctx context.Context, img image.Image, index int, pathName string) error {
err := ctx.Err()
if err != nil {
return fmt.Errorf("imageConvert: %w", err)
}
atomic.AddInt32(&c.CurrContent, 1)
if c.OnProgress != nil {
c.OnProgress()
}
ext := c.Opts.Format
if ext == "jpeg" {
ext = "jpg"
}
var fileName string
if pathName != "" {
fileName = filepath.Join(c.Workdir, fmt.Sprintf("%s.%s", baseNoExt(pathName), ext))
} else {
fileName = filepath.Join(c.Workdir, fmt.Sprintf("%03d.%s", index, ext))
}
img = c.imageTransform(img)
w, err := os.Create(fileName)
if err != nil {
return fmt.Errorf("imageConvert: %w", err)
}
defer w.Close()
if err := c.imageEncode(img, w); err != nil {
return fmt.Errorf("imageConvert: %w", err)
}
return nil
}
// imageTransform transforms image (resize, rotate, brightness, contrast).
func (c *Converter) imageTransform(img image.Image) image.Image {
var i = img
if c.Opts.Width > 0 || c.Opts.Height > 0 {
if c.Opts.Fit {
i = fit(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
} else {
i = resize(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
}
}
if c.Opts.Rotate > 0 {
switch c.Opts.Rotate {
case 90:
i = rotate(i, 90)
case 180:
i = rotate(i, 180)
case 270:
i = rotate(i, 270)
}
}
if c.Opts.Brightness != 0 {
i = brightness(i, float64(c.Opts.Brightness))
}
if c.Opts.Contrast != 0 {
i = contrast(i, float64(c.Opts.Contrast))
}
if c.Opts.Grayscale {
i = imageToGray(i)
}
return i
}
// imageDecode decodes image from reader.
func (c *Converter) imageDecode(reader io.Reader) (image.Image, error) {
img, _, err := image.Decode(reader)
if err != nil {
return img, fmt.Errorf("imageDecode: %w", err)
}
return img, nil
}
// imageEncode encodes image to file.
func (c *Converter) imageEncode(img image.Image, w io.Writer) error {
var err error
switch c.Opts.Format {
case "png":
err = png.Encode(w, img)
case "tiff":
err = tiff.Encode(w, img, &tiff.Options{Compression: tiff.Uncompressed})
case "jpeg":
opts := &jpegli.EncodingOptions{}
opts.Quality = c.Opts.Quality
opts.ChromaSubsampling = image.YCbCrSubsampleRatio420
opts.ProgressiveLevel = 2
opts.AdaptiveQuantization = true
opts.DCTMethod = jpegli.DefaultDCTMethod
err = jpegli.Encode(w, img, opts)
case "webp":
err = webp.Encode(w, img, webp.Options{Quality: c.Opts.Quality, Method: webp.DefaultMethod})
case "avif":
err = avif.Encode(w, img, avif.Options{Quality: c.Opts.Quality, Speed: avif.DefaultSpeed})
case "jxl":
err = jpegxl.Encode(w, img, jpegxl.Options{Quality: c.Opts.Quality, Effort: jpegxl.DefaultEffort})
case "bmp":
opts := &gobmp.EncoderOptions{}
opts.SupportTransparency(false)
err = gobmp.EncodeWithOptions(w, imageToPaletted(img), opts)
}
if err != nil {
return fmt.Errorf("imageEncode: %w", err)
}
return nil
}