This commit is contained in:
Milan Nikolic
2023-08-21 17:52:57 +02:00
parent dfea3dc6a3
commit 7b6c766dcc
4 changed files with 344 additions and 313 deletions

View File

@@ -96,6 +96,10 @@ type Options struct {
Thumbnail bool
// CBZ metadata
Meta bool
// ZIP comment
Comment bool
// ZIP comment body
CommentBody string
// Output file
Outfile string
// Output directory
@@ -157,134 +161,6 @@ func New(o Options) *Convertor {
return c
}
// convertImage converts image.Image.
func (c *Convertor) convertImage(ctx context.Context, img image.Image, index int, pathName string) error {
err := ctx.Err()
if err != nil {
return err
}
atomic.AddInt32(&c.CurrContent, 1)
if c.OnProgress != nil {
c.OnProgress()
}
var fileName string
if pathName != "" {
fileName = filepath.Join(c.Workdir, fmt.Sprintf("%s.%s", c.baseNoExt(pathName), c.Opts.Format))
} else {
fileName = filepath.Join(c.Workdir, fmt.Sprintf("%03d.%s", index, c.Opts.Format))
}
img = c.transformImage(img)
if c.Opts.LevelsInMin != 0 || c.Opts.LevelsInMax != 255 || c.Opts.LevelsGamma != 1.0 ||
c.Opts.LevelsOutMin != 0 || c.Opts.LevelsOutMax != 255 {
img, err = c.levelImage(img)
if err != nil {
return err
}
}
switch c.Opts.Format {
case "jpeg", "png", "tiff", "webp", "avif":
if err := c.encodeImage(img, fileName); err != nil {
return err
}
case "bmp":
// convert image to 4-Bit BMP (16 colors)
if err := c.encodeIM(img, fileName); err != nil {
return err
}
}
return nil
}
// transformImage transforms image (resize, rotate, flip, brightness, contrast).
func (c *Convertor) transformImage(img image.Image) image.Image {
var i = img
if c.Opts.Width > 0 || c.Opts.Height > 0 {
if c.Opts.Fit {
i = imaging.Fit(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
} else {
i = imaging.Resize(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
}
}
if c.Opts.Rotate > 0 {
switch c.Opts.Rotate {
case 90:
i = imaging.Rotate90(i)
case 180:
i = imaging.Rotate180(i)
case 270:
i = imaging.Rotate270(i)
}
}
if c.Opts.Flip != "none" {
switch c.Opts.Flip {
case "horizontal":
i = imaging.FlipH(i)
case "vertical":
i = imaging.FlipV(i)
}
}
if c.Opts.Brightness != 0 {
i = imaging.AdjustBrightness(i, c.Opts.Brightness)
}
if c.Opts.Contrast != 0 {
i = imaging.AdjustContrast(i, c.Opts.Contrast)
}
return i
}
// levelImage applies a Photoshop-like levels operation on an image.
func (c *Convertor) levelImage(img image.Image) (image.Image, error) {
mw := imagick.NewMagickWand()
defer mw.Destroy()
rgba := imageToRGBA(img)
err := mw.ConstituteImage(uint(img.Bounds().Dx()), uint(img.Bounds().Dy()), "RGBA", imagick.PIXEL_CHAR, rgba.Pix)
if err != nil {
return img, fmt.Errorf("levelImage: %w", err)
}
_, qrange := imagick.GetQuantumRange()
quantumRange := float64(qrange)
inmin := (quantumRange * c.Opts.LevelsInMin) / 255
inmax := (quantumRange * c.Opts.LevelsInMax) / 255
outmin := (quantumRange * c.Opts.LevelsOutMin) / 255
outmax := (quantumRange * c.Opts.LevelsOutMax) / 255
if err := mw.LevelImage(inmin, c.Opts.LevelsGamma, inmax); err != nil {
return img, fmt.Errorf("levelImage: %w", err)
}
if err := mw.LevelImage(-outmin, 1.0, quantumRange+(quantumRange-outmax)); err != nil {
return img, fmt.Errorf("levelImage: %w", err)
}
blob := mw.GetImageBlob()
var i image.Image
i, err = c.decodeImage(bytes.NewReader(blob), "levels")
if err != nil {
i, err = c.decodeIM(bytes.NewReader(blob), "levels")
if err != nil {
return nil, fmt.Errorf("levelImage: %w", err)
}
}
return i, nil
}
// convertDocument converts PDF/EPUB document to CBZ.
func (c *Convertor) convertDocument(fileName string) error {
var err error
@@ -319,7 +195,7 @@ func (c *Convertor) convertDocument(fileName string) error {
if img != nil {
eg.Go(func() error {
return c.convertImage(ctx, img, n, "")
return c.imageConvert(ctx, img, n, "")
})
}
}
@@ -336,7 +212,7 @@ func (c *Convertor) convertArchive(fileName string) error {
return fmt.Errorf("convertArchive: %w", err)
}
contents, err := c.listArchive(fileName)
contents, err := c.archiveList(fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
@@ -388,17 +264,17 @@ func (c *Convertor) convertArchive(fileName string) error {
}
var img image.Image
img, err = c.decodeImage(bytes.NewReader(data), pathName)
img, err = c.imageDecode(bytes.NewReader(data), pathName)
if err != nil {
img, err = c.decodeIM(bytes.NewReader(data), pathName)
img, err = c.imDecode(bytes.NewReader(data), pathName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
}
if cover == pathName && c.Opts.NoCover {
img = c.transformImage(img)
if err = c.encodeImage(img, filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
img = c.imageTransform(img)
if err = c.imageEncode(img, filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
@@ -406,8 +282,8 @@ func (c *Convertor) convertArchive(fileName string) error {
}
if c.Opts.NoRGB && !c.isGrayScale(img) {
img = c.transformImage(img)
if err = c.encodeImage(img, filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
img = c.imageTransform(img)
if err = c.imageEncode(img, filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
@@ -416,7 +292,7 @@ func (c *Convertor) convertArchive(fileName string) error {
if img != nil {
eg.Go(func() error {
return c.convertImage(ctx, img, 0, pathName)
return c.imageConvert(ctx, img, 0, pathName)
})
}
} else {
@@ -486,17 +362,17 @@ func (c *Convertor) convertDirectory(dirPath string) error {
}
var i image.Image
i, err = c.decodeImage(file, img)
i, err = c.imageDecode(file, img)
if err != nil {
i, err = c.decodeIM(file, img)
i, err = c.imDecode(file, img)
if err != nil {
return fmt.Errorf("coverDirectory: %w", err)
}
}
if c.Opts.NoRGB && !c.isGrayScale(i) {
i = c.transformImage(i)
if err = c.encodeImage(i, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
i = c.imageTransform(i)
if err = c.imageEncode(i, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
@@ -513,7 +389,7 @@ func (c *Convertor) convertDirectory(dirPath string) error {
if i != nil {
eg.Go(func() error {
return c.convertImage(ctx, i, index, img)
return c.imageConvert(ctx, i, index, img)
})
}
}
@@ -522,90 +398,146 @@ func (c *Convertor) convertDirectory(dirPath string) error {
return eg.Wait()
}
// saveArchive saves workdir to CBZ archive.
func (c *Convertor) saveArchive(fileName string) error {
if c.OnCompress != nil {
c.OnCompress()
}
zipname := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%s%s.cbz", c.baseNoExt(fileName), c.Opts.Suffix))
zipfile, err := os.Create(zipname)
// imageConvert converts image.Image.
func (c *Convertor) imageConvert(ctx context.Context, img image.Image, index int, pathName string) error {
err := ctx.Err()
if err != nil {
return fmt.Errorf("saveArchive: %w", err)
return err
}
z := zip.NewWriter(zipfile)
atomic.AddInt32(&c.CurrContent, 1)
if c.OnProgress != nil {
c.OnProgress()
}
files, err := os.ReadDir(c.Workdir)
var fileName string
if pathName != "" {
fileName = filepath.Join(c.Workdir, fmt.Sprintf("%s.%s", c.baseNoExt(pathName), c.Opts.Format))
} else {
fileName = filepath.Join(c.Workdir, fmt.Sprintf("%03d.%s", index, c.Opts.Format))
}
img = c.imageTransform(img)
if c.Opts.LevelsInMin != 0 || c.Opts.LevelsInMax != 255 || c.Opts.LevelsGamma != 1.0 ||
c.Opts.LevelsOutMin != 0 || c.Opts.LevelsOutMax != 255 {
img, err = c.imageLevel(img)
if err != nil {
return fmt.Errorf("saveArchive: %w", err)
}
for _, file := range files {
r, err := os.ReadFile(filepath.Join(c.Workdir, file.Name()))
if err != nil {
return fmt.Errorf("saveArchive: %w", err)
}
info, err := file.Info()
if err != nil {
return fmt.Errorf("saveArchive: %w", err)
}
zipinfo, err := zip.FileInfoHeader(info)
if err != nil {
return fmt.Errorf("saveArchive: %w", err)
}
zipinfo.Method = zip.Deflate
w, err := z.CreateHeader(zipinfo)
if err != nil {
return fmt.Errorf("saveArchive: %w", err)
}
_, err = w.Write(r)
if err != nil {
return fmt.Errorf("saveArchive: %w", err)
return err
}
}
if err = z.Close(); err != nil {
return fmt.Errorf("saveArchive: %w", err)
switch c.Opts.Format {
case "jpeg", "png", "tiff", "webp", "avif":
if err := c.imageEncode(img, fileName); err != nil {
return err
}
case "bmp":
// convert image to 4-Bit BMP (16 colors)
if err := c.imEncode(img, fileName); err != nil {
return err
}
}
if err = zipfile.Close(); err != nil {
return fmt.Errorf("saveArchive: %w", err)
}
return os.RemoveAll(c.Workdir)
return nil
}
// listArchive lists contents of archive.
func (c *Convertor) listArchive(fileName string) ([]string, error) {
var contents []string
// imageTransform transforms image (resize, rotate, flip, brightness, contrast).
func (c *Convertor) imageTransform(img image.Image) image.Image {
var i = img
archive, err := unarr.NewArchive(fileName)
if err != nil {
return contents, err
if c.Opts.Width > 0 || c.Opts.Height > 0 {
if c.Opts.Fit {
i = imaging.Fit(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
} else {
i = imaging.Resize(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
}
}
defer archive.Close()
return archive.List()
if c.Opts.Rotate > 0 {
switch c.Opts.Rotate {
case 90:
i = imaging.Rotate90(i)
case 180:
i = imaging.Rotate180(i)
case 270:
i = imaging.Rotate270(i)
}
}
if c.Opts.Flip != "none" {
switch c.Opts.Flip {
case "horizontal":
i = imaging.FlipH(i)
case "vertical":
i = imaging.FlipV(i)
}
}
if c.Opts.Brightness != 0 {
i = imaging.AdjustBrightness(i, c.Opts.Brightness)
}
if c.Opts.Contrast != 0 {
i = imaging.AdjustContrast(i, c.Opts.Contrast)
}
return i
}
// decodeImage decodes image from reader.
func (c *Convertor) decodeImage(reader io.Reader, fileName string) (img image.Image, err error) {
// imageLevel applies a Photoshop-like levels operation on an image.
func (c *Convertor) imageLevel(img image.Image) (image.Image, error) {
mw := imagick.NewMagickWand()
defer mw.Destroy()
rgba := imageToRGBA(img)
err := mw.ConstituteImage(uint(img.Bounds().Dx()), uint(img.Bounds().Dy()), "RGBA", imagick.PIXEL_CHAR, rgba.Pix)
if err != nil {
return img, fmt.Errorf("imageLevel: %w", err)
}
_, qrange := imagick.GetQuantumRange()
quantumRange := float64(qrange)
inmin := (quantumRange * c.Opts.LevelsInMin) / 255
inmax := (quantumRange * c.Opts.LevelsInMax) / 255
outmin := (quantumRange * c.Opts.LevelsOutMin) / 255
outmax := (quantumRange * c.Opts.LevelsOutMax) / 255
if err := mw.LevelImage(inmin, c.Opts.LevelsGamma, inmax); err != nil {
return img, fmt.Errorf("imageLevel: %w", err)
}
if err := mw.LevelImage(-outmin, 1.0, quantumRange+(quantumRange-outmax)); err != nil {
return img, fmt.Errorf("imageLevel: %w", err)
}
blob := mw.GetImageBlob()
var i image.Image
i, err = c.imageDecode(bytes.NewReader(blob), "levels")
if err != nil {
i, err = c.imDecode(bytes.NewReader(blob), "levels")
if err != nil {
return nil, fmt.Errorf("imageLevel: %w", err)
}
}
return i, nil
}
// imageDecode decodes image from reader.
func (c *Convertor) imageDecode(reader io.Reader, fileName string) (img image.Image, err error) {
img, _, err = image.Decode(reader)
if err != nil {
err = fmt.Errorf("decodeImage: %s: %w", fileName, err)
err = fmt.Errorf("imageDecode: %s: %w", fileName, err)
}
return
}
// decodeIM decodes image from reader (ImageMagick).
func (c *Convertor) decodeIM(reader io.Reader, fileName string) (img image.Image, err error) {
// imDecode decodes image from reader (ImageMagick).
func (c *Convertor) imDecode(reader io.Reader, fileName string) (img image.Image, err error) {
mw := imagick.NewMagickWand()
defer mw.Destroy()
@@ -614,15 +546,15 @@ func (c *Convertor) decodeIM(reader io.Reader, fileName string) (img image.Image
data, err = io.ReadAll(reader)
if err != nil {
return img, fmt.Errorf("decodeIM: %w", err)
return img, fmt.Errorf("imDecode: %w", err)
}
if err = mw.SetFilename(fileName); err != nil {
return img, fmt.Errorf("decodeIM: %w", err)
return img, fmt.Errorf("imDecode: %w", err)
}
if err = mw.ReadImageBlob(data); err != nil {
return img, fmt.Errorf("decodeIM: %w", err)
return img, fmt.Errorf("imDecode: %w", err)
}
w := mw.GetImageWidth()
@@ -630,7 +562,7 @@ func (c *Convertor) decodeIM(reader io.Reader, fileName string) (img image.Image
out, err = mw.ExportImagePixels(0, 0, w, h, "RGBA", imagick.PIXEL_CHAR)
if err != nil {
return img, fmt.Errorf("decodeIM: %w", err)
return img, fmt.Errorf("imDecode: %w", err)
}
b := image.Rect(0, 0, int(w), int(h))
@@ -641,11 +573,11 @@ func (c *Convertor) decodeIM(reader io.Reader, fileName string) (img image.Image
return
}
// encodeImage encodes image to file.
func (c *Convertor) encodeImage(img image.Image, fileName string) error {
// imageEncode encodes image to file.
func (c *Convertor) imageEncode(img image.Image, fileName string) error {
file, err := os.Create(fileName)
if err != nil {
return fmt.Errorf("encodeImage: %w", err)
return fmt.Errorf("imageEncode: %w", err)
}
defer file.Close()
@@ -671,47 +603,47 @@ func (c *Convertor) encodeImage(img image.Image, fileName string) error {
ctx, err := heif.EncodeFromImage(img, heif.CompressionAV1, c.Opts.Quality, lossLess, 0)
if err != nil {
return fmt.Errorf("encodeImage: %w", err)
return fmt.Errorf("imageEncode: %w", err)
}
err = ctx.WriteToFile(fileName)
}
if err != nil {
return fmt.Errorf("encodeImage: %w", err)
return fmt.Errorf("imageEncode: %w", err)
}
return nil
}
// encodeIM encodes image to file (ImageMagick).
func (c *Convertor) encodeIM(i image.Image, fileName string) error {
// imEncode encodes image to file (ImageMagick).
func (c *Convertor) imEncode(i image.Image, fileName string) error {
mw := imagick.NewMagickWand()
defer mw.Destroy()
rgba := imageToRGBA(i)
if err := mw.ConstituteImage(uint(i.Bounds().Dx()), uint(i.Bounds().Dy()), "RGBA", imagick.PIXEL_CHAR, rgba.Pix); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if c.Opts.Grayscale {
if err := mw.TransformImageColorspace(imagick.COLORSPACE_GRAY); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
}
switch filepath.Ext(fileName) {
case ".png":
if err := mw.SetImageFormat("PNG"); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.WriteImage(fileName); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
case ".tif", ".tiff":
if err := mw.SetImageFormat("TIFF"); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.WriteImage(fileName); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
case ".bmp":
pw := imagick.NewPixelWand()
@@ -719,59 +651,146 @@ func (c *Convertor) encodeIM(i image.Image, fileName string) error {
defer pw.Destroy()
if err := mw.SetImageFormat("BMP3"); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.SetImageBackgroundColor(pw); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_REMOVE); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_DEACTIVATE); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.SetImageMatte(false); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.SetImageCompression(imagick.COMPRESSION_NO); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.QuantizeImage(16, mw.GetImageColorspace(), 1, imagick.DITHER_METHOD_FLOYD_STEINBERG, true); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.WriteImage(fileName); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
case ".jpg", ".jpeg":
if err := mw.SetImageFormat("JPEG"); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.SetImageCompressionQuality(uint(c.Opts.Quality)); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.WriteImage(fileName); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
case ".avif":
if err := mw.SetImageFormat("AVIF"); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.SetImageCompressionQuality(uint(c.Opts.Quality)); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
if err := mw.WriteImage(fileName); err != nil {
return fmt.Errorf("encodeIM: %w", err)
return fmt.Errorf("imEncode: %w", err)
}
}
return nil
}
// archiveSave saves workdir to CBZ archive.
func (c *Convertor) archiveSave(fileName string) error {
if c.OnCompress != nil {
c.OnCompress()
}
zipName := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%s%s.cbz", c.baseNoExt(fileName), c.Opts.Suffix))
zipFile, err := os.Create(zipName)
if err != nil {
return fmt.Errorf("archiveSave: %w", err)
}
z := zip.NewWriter(zipFile)
files, err := os.ReadDir(c.Workdir)
if err != nil {
return fmt.Errorf("archiveSave: %w", err)
}
for _, file := range files {
r, err := os.ReadFile(filepath.Join(c.Workdir, file.Name()))
if err != nil {
return fmt.Errorf("archiveSave: %w", err)
}
info, err := file.Info()
if err != nil {
return fmt.Errorf("archiveSave: %w", err)
}
zipInfo, err := zip.FileInfoHeader(info)
if err != nil {
return fmt.Errorf("archiveSave: %w", err)
}
zipInfo.Method = zip.Deflate
w, err := z.CreateHeader(zipInfo)
if err != nil {
return fmt.Errorf("archiveSave: %w", err)
}
_, err = w.Write(r)
if err != nil {
return fmt.Errorf("archiveSave: %w", err)
}
}
if err = z.Close(); err != nil {
return fmt.Errorf("archiveSave: %w", err)
}
if err = zipFile.Close(); err != nil {
return fmt.Errorf("archiveSave: %w", err)
}
return os.RemoveAll(c.Workdir)
}
// archiveList lists contents of archive.
func (c *Convertor) archiveList(fileName string) ([]string, error) {
var contents []string
archive, err := unarr.NewArchive(fileName)
if err != nil {
return contents, err
}
defer archive.Close()
return archive.List()
}
// archiveComment returns ZIP comment.
func (c *Convertor) archiveComment(fileName string) (string, error) {
r, err := zip.OpenReader(fileName)
if err != nil {
return "", err
}
return r.Comment, nil
}
// archiveSetComment sets ZIP comment.
func (c *Convertor) archiveSetComment(fileName, commentBody string) error {
return nil
}
// coverArchive extracts cover from archive.
func (c *Convertor) coverArchive(fileName string) (image.Image, error) {
var images []string
contents, err := c.listArchive(fileName)
contents, err := c.archiveList(fileName)
if err != nil {
return nil, fmt.Errorf("coverArchive: %w", err)
}
@@ -800,9 +819,9 @@ func (c *Convertor) coverArchive(fileName string) (image.Image, error) {
}
var img image.Image
img, err = c.decodeImage(bytes.NewReader(data), cover)
img, err = c.imageDecode(bytes.NewReader(data), cover)
if err != nil {
img, err = c.decodeIM(bytes.NewReader(data), cover)
img, err = c.imDecode(bytes.NewReader(data), cover)
if err != nil {
return nil, fmt.Errorf("coverArchive: %w", err)
}
@@ -845,9 +864,9 @@ func (c *Convertor) coverDirectory(dir string) (image.Image, error) {
defer file.Close()
var img image.Image
img, err = c.decodeImage(file, cover)
img, err = c.imageDecode(file, cover)
if err != nil {
img, err = c.decodeIM(file, cover)
img, err = c.imDecode(file, cover)
if err != nil {
return nil, fmt.Errorf("coverDirectory: %w", err)
}
@@ -856,6 +875,61 @@ func (c *Convertor) coverDirectory(dir string) (image.Image, error) {
return img, nil
}
// coverName returns the filename that is the most likely to be the cover.
func (c *Convertor) coverName(images []string) string {
if len(images) == 0 {
return ""
}
lower := make([]string, 0)
for idx, img := range images {
img = strings.ToLower(img)
lower = append(lower, img)
ext := c.baseNoExt(img)
if strings.HasPrefix(img, "cover") || strings.HasPrefix(img, "front") ||
strings.HasSuffix(ext, "cover") || strings.HasSuffix(ext, "front") {
return filepath.ToSlash(images[idx])
}
}
sort.Sort(sortorder.Natural(lower))
cover := lower[0]
for idx, img := range images {
img = strings.ToLower(img)
if img == cover {
return filepath.ToSlash(images[idx])
}
}
return ""
}
// coverImage returns cover as image.Image.
func (c *Convertor) coverImage(fileName string, fileInfo os.FileInfo) (image.Image, error) {
var err error
var cover image.Image
if fileInfo.IsDir() {
cover, err = c.coverDirectory(fileName)
} else if c.isDocument(fileName) {
cover, err = c.coverDocument(fileName)
} else {
cover, err = c.coverArchive(fileName)
}
if c.OnProgress != nil {
c.OnProgress()
}
if err != nil {
return nil, fmt.Errorf("coverImage: %w", err)
}
return cover, nil
}
// imagesFromPath returns list of found image files for given directory.
func (c *Convertor) imagesFromPath(path string) ([]string, error) {
var images []string
@@ -970,7 +1044,7 @@ func (c *Convertor) isImage(f string) bool {
// isNonImage checks for allowed files in archive.
func (c *Convertor) isNonImage(f string) bool {
var types = []string{".nfo", ".xml"}
var types = []string{".nfo", ".xml", ".txt"}
for _, t := range types {
if strings.ToLower(filepath.Ext(f)) == t {
return true
@@ -1027,61 +1101,6 @@ func (c *Convertor) copyFile(reader io.Reader, filename string) error {
return nil
}
// coverName returns the filename that is the most likely to be the cover.
func (c *Convertor) coverName(images []string) string {
if len(images) == 0 {
return ""
}
lower := make([]string, 0)
for idx, img := range images {
img = strings.ToLower(img)
lower = append(lower, img)
ext := c.baseNoExt(img)
if strings.HasPrefix(img, "cover") || strings.HasPrefix(img, "front") ||
strings.HasSuffix(ext, "cover") || strings.HasSuffix(ext, "front") {
return filepath.ToSlash(images[idx])
}
}
sort.Sort(sortorder.Natural(lower))
cover := lower[0]
for idx, img := range images {
img = strings.ToLower(img)
if img == cover {
return filepath.ToSlash(images[idx])
}
}
return ""
}
// coverImage returns cover as image.Image.
func (c *Convertor) coverImage(fileName string, fileInfo os.FileInfo) (image.Image, error) {
var err error
var cover image.Image
if fileInfo.IsDir() {
cover, err = c.coverDirectory(fileName)
} else if c.isDocument(fileName) {
cover, err = c.coverDocument(fileName)
} else {
cover, err = c.coverArchive(fileName)
}
if c.OnProgress != nil {
c.OnProgress()
}
if err != nil {
return nil, fmt.Errorf("coverImage: %w", err)
}
return cover, nil
}
// Initialize inits ImageMagick.
func (c *Convertor) Initialize() {
imagick.Initialize()
@@ -1166,7 +1185,7 @@ func (c *Convertor) Cover(fileName string, fileInfo os.FileInfo) error {
cover, err := c.coverImage(fileName, fileInfo)
if err != nil {
return fmt.Errorf("extractCover: %w", err)
return fmt.Errorf("cover: %w", err)
}
if c.Opts.Width > 0 || c.Opts.Height > 0 {
@@ -1181,11 +1200,11 @@ func (c *Convertor) Cover(fileName string, fileInfo os.FileInfo) error {
switch c.Opts.Format {
case "jpeg", "png", "tiff", "webp", "avif":
if err := c.encodeImage(cover, fName); err != nil {
if err := c.imageEncode(cover, fName); err != nil {
return err
}
case "bmp":
if err := c.encodeIM(cover, fName); err != nil {
if err := c.imEncode(cover, fName); err != nil {
return err
}
}
@@ -1264,7 +1283,7 @@ func (c *Convertor) Meta(fileName string, info os.FileInfo) (any, error) {
if c.Opts.Cover {
var images []string
contents, err := c.listArchive(fileName)
contents, err := c.archiveList(fileName)
if err != nil {
return nil, err
}
@@ -1276,6 +1295,14 @@ func (c *Convertor) Meta(fileName string, info os.FileInfo) (any, error) {
}
return c.coverName(images), nil
} else if c.Opts.Comment {
comment, err := c.archiveComment(fileName)
if err != nil {
return nil, err
}
return comment, nil
} else if c.Opts.CommentBody != "" {
}
return nil, nil
@@ -1289,21 +1316,21 @@ func (c *Convertor) Convert(filename string, info os.FileInfo) error {
if err := c.convertDirectory(filename); err != nil {
return err
}
if err := c.saveArchive(filename); err != nil {
if err := c.archiveSave(filename); err != nil {
return err
}
} else if c.isDocument(filename) {
if err := c.convertDocument(filename); err != nil {
return err
}
if err := c.saveArchive(filename); err != nil {
if err := c.archiveSave(filename); err != nil {
return err
}
} else {
} else if c.isArchive(filename) {
if err := c.convertArchive(filename); err != nil {
return err
}
if err := c.saveArchive(filename); err != nil {
if err := c.archiveSave(filename); err != nil {
return err
}
}

View File

@@ -96,6 +96,8 @@ func main() {
if opts.Cover {
fmt.Println(ret)
} else if opts.Comment {
fmt.Println(ret)
}
continue
@@ -182,6 +184,8 @@ func parseFlags() (cbconvert.Options, []string) {
meta := flag.NewFlagSet("meta", flag.ExitOnError)
meta.SortFlags = false
meta.BoolVar(&opts.Cover, "cover", false, "Print cover name")
meta.BoolVar(&opts.Comment, "comment", false, "Print comment")
meta.StringVar(&opts.CommentBody, "comment-body", "", "Set comment body (file or string)")
convert.Usage = func() {
_, _ = fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))

10
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/gen2brain/cbconvert
go 1.19
go 1.21
require (
github.com/chai2010/webp v1.1.1
@@ -9,8 +9,8 @@ require (
github.com/gen2brain/go-fitz v1.22.2
github.com/gen2brain/go-unarr v0.1.7
github.com/hotei/bmp v0.0.0-20150430041436-f620cebab0c7
github.com/strukturag/libheif v1.13.0
golang.org/x/image v0.7.0
golang.org/x/sync v0.2.0
gopkg.in/gographics/imagick.v3 v3.4.2
github.com/strukturag/libheif v1.15.2
golang.org/x/image v0.11.0
golang.org/x/sync v0.3.0
gopkg.in/gographics/imagick.v3 v3.4.3
)

18
go.sum
View File

@@ -10,14 +10,14 @@ github.com/gen2brain/go-unarr v0.1.7 h1:mEE7bPShJIsmAX67t6BW2ibpEUO7j5WK152KgNM9
github.com/gen2brain/go-unarr v0.1.7/go.mod h1:MK9a3hddpaIxjEtrE1f/LA5yJ7gA34cS7Oyr325sY9s=
github.com/hotei/bmp v0.0.0-20150430041436-f620cebab0c7 h1:NlUATi3cllRJhpM4mfR9BxiLRXT83bcSLcOa+S8lrME=
github.com/hotei/bmp v0.0.0-20150430041436-f620cebab0c7/go.mod h1:Hku3FQ2laCEwSv7Z8YkC0er38jLaUycUCbsFkWMr+z4=
github.com/strukturag/libheif v1.13.0 h1:SuLo/Fl/Nzbw0ixOya1YZSl0Xd27X4fgofGnJdvOHqI=
github.com/strukturag/libheif v1.13.0/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks=
github.com/strukturag/libheif v1.15.2 h1:pgdcpDHqtLKRXL9ETSTeht0CsJODB3BojpTsb3S/3Wg=
github.com/strukturag/libheif v1.15.2/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -27,8 +27,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -42,11 +42,11 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/gographics/imagick.v3 v3.4.2 h1:vk6oildvhRBVSBfQ4X3raJstApYSeK6CZsyzoSOZk58=
gopkg.in/gographics/imagick.v3 v3.4.2/go.mod h1:+Q9nyA2xRZXrDyTtJ/eko+8V/5E7bWYs08ndkZp8UmA=
gopkg.in/gographics/imagick.v3 v3.4.3 h1:9plKFE/Us913jBN6KohtLG9FNW8LPvfpjiGAORIiEHg=
gopkg.in/gographics/imagick.v3 v3.4.3/go.mod h1:+Q9nyA2xRZXrDyTtJ/eko+8V/5E7bWYs08ndkZp8UmA=