diff --git a/cbconvert.go b/cbconvert.go index 4e43b76..7d40125 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -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) - - files, err := os.ReadDir(c.Workdir) - if err != nil { - return fmt.Errorf("saveArchive: %w", err) + atomic.AddInt32(&c.CurrContent, 1) + if c.OnProgress != nil { + c.OnProgress() } - for _, file := range files { - r, err := os.ReadFile(filepath.Join(c.Workdir, file.Name())) - if err != nil { - return fmt.Errorf("saveArchive: %w", err) - } + 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)) + } - info, err := file.Info() - if err != nil { - return fmt.Errorf("saveArchive: %w", err) - } + img = c.imageTransform(img) - zipinfo, err := zip.FileInfoHeader(info) + 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) - } - - 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 } } diff --git a/cmd/cbconvert/main.go b/cmd/cbconvert/main.go index 66e0775..a32d8b4 100644 --- a/cmd/cbconvert/main.go +++ b/cmd/cbconvert/main.go @@ -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 [] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0])) diff --git a/go.mod b/go.mod index 1756f12..0c3b35d 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 156e4f3..2d7150e 100644 --- a/go.sum +++ b/go.sum @@ -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=