From 9da5b97f15851d8d639d5ff76328a1341dcb966b Mon Sep 17 00:00:00 2001 From: Milan Nikolic Date: Thu, 8 Sep 2022 23:38:58 +0200 Subject: [PATCH] Add support for AVIF --- README.md | 14 +++++---- cbconvert.go | 70 +++++++++++++++++++++++++++++++++++++++++-- cmd/cbconvert/go.mod | 1 + cmd/cbconvert/go.sum | 2 ++ cmd/cbconvert/main.go | 3 +- go.mod | 1 + go.sum | 2 ++ 7 files changed, 85 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e300b25..41a39e8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It can convert comics to different formats to fit your various devices. * reads RAR, ZIP, 7Z, CBR, CBZ, CB7, CBT, PDF, EPUB, and plain directory * always saves processed comics in CBZ (ZIP) archive format -* images can be converted to JPEG, PNG, TIFF, WEBP, or 4-Bit BMP (16 colors) file format +* images can be converted to JPEG, PNG, TIFF, WEBP, AVIF, or 4-Bit BMP (16 colors) file format * rotate, flip, adjust brightness/contrast, adjust levels (Photoshop-like) or grayscale images * resize algorithms (NearestNeighbor, Box, Linear, MitchellNetravali, CatmullRom, Gaussian, Lanczos) * export covers from comics @@ -18,8 +18,10 @@ It can convert comics to different formats to fit your various devices. ### Download -* [Windows](https://github.com/gen2brain/cbconvert/releases/download/0.7.0/cbconvert-0.7.0-windows-i686.zip) -* [Linux](https://github.com/gen2brain/cbconvert/releases/download/0.7.0/cbconvert-0.7.0-linux-x86_64.tar.gz) +* [Windows x86_64](https://github.com/gen2brain/cbconvert/releases/download/0.8.0/cbconvert-0.8.0-windows-x86_64.zip) +* [Linux x86_64](https://github.com/gen2brain/cbconvert/releases/download/0.8.0/cbconvert-0.8.0-linux-x86_64.tar.gz) +* [macOS x86_64](https://github.com/gen2brain/cbconvert/releases/download/0.8.0/cbconvert-0.8.0-darwin-x86_64.tar.gz) +* [macOS aarch64](https://github.com/gen2brain/cbconvert/releases/download/0.8.0/cbconvert-0.8.0-darwin-aarch64.tar.gz) ### Using cbconvert in file managers to generate FreeDesktop thumbnails @@ -55,9 +57,11 @@ This is what it looks like in the PCManFM file manager:         --fit             Best fit for required width and height (default "false")         --format -            Image format, valid values are jpeg, png, tiff, bmp, webp (default "jpeg") +            Image format, valid values are jpeg, png, tiff, bmp, webp, avif (default "jpeg")         --quality -            JPEG image quality (default "75") +            Image quality (default "75") +        --lossless +            Lossless compression (avif) (default "false")         --filter             0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")         --no-cover diff --git a/cbconvert.go b/cbconvert.go index 4f17a9f..45d1fd0 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -24,6 +24,7 @@ import ( "github.com/chai2010/webp" _ "github.com/hotei/bmp" + "github.com/strukturag/libheif/go/heif" "golang.org/x/image/tiff" "github.com/disintegration/imaging" @@ -64,10 +65,12 @@ var filters = map[int]imaging.ResampleFilter{ // Options type. type Options struct { - // Image format, valid values are jpeg, png, tiff, bmp, webp + // Image format, valid values are jpeg, png, tiff, bmp, webp, avif Format string // JPEG image quality Quality int + // Lossless compression (avif) + Lossless bool // Image width Width int // Image height @@ -208,6 +211,11 @@ func (c *Convertor) convertImage(ctx context.Context, img image.Image, index int if err != nil { return err } + case "avif": + err = c.encodeImage(img, fileName) + if err != nil { + return err + } } return nil @@ -579,6 +587,47 @@ func (c *Convertor) decodeImage(reader io.Reader, fileName string) (img image.Im return } +// decodeIM decodes image from reader (ImageMagick). +func (c *Convertor) decodeIM(reader io.Reader, fileName string) (img image.Image, err error) { + imagick.Initialize() + + mw := imagick.NewMagickWand() + defer mw.Destroy() + + var data []byte + var out interface{} + + data, err = io.ReadAll(reader) + if err != nil { + return img, fmt.Errorf("decodeIM: %w", err) + } + + err = mw.SetFilename(fileName) + if err != nil { + return img, fmt.Errorf("decodeIM: %w", err) + } + + err = mw.ReadImageBlob(data) + if err != nil { + return img, fmt.Errorf("decodeIM: %w", err) + } + + w := mw.GetImageWidth() + h := mw.GetImageHeight() + + out, err = mw.ExportImagePixels(0, 0, w, h, "RGBA", imagick.PIXEL_CHAR) + if err != nil { + return img, fmt.Errorf("decodeIM: %w", err) + } + + b := image.Rect(0, 0, int(w), int(h)) + rgba := image.NewRGBA(b) + rgba.Pix = out.([]byte) + img = rgba + + return +} + // encodeImage encodes image to file. func (c *Convertor) encodeImage(img image.Image, fileName string) error { file, err := os.Create(fileName) @@ -600,6 +649,18 @@ func (c *Convertor) encodeImage(img image.Image, fileName string) error { err = jpeg.Encode(file, img, &jpeg.Options{Quality: c.Opts.Quality}) case ".webp": err = webp.Encode(file, img, &webp.Options{Quality: float32(c.Opts.Quality)}) + case ".avif": + img = imageToRGBA(img) + lossLess := heif.LosslessModeDisabled + if c.Opts.Lossless { + lossLess = heif.LosslessModeEnabled + } + + ctx, err := heif.EncodeFromImage(img, heif.CompressionAV1, c.Opts.Quality, lossLess, 0) + if err != nil { + return fmt.Errorf("encodeImage: %w", err) + } + err = ctx.WriteToFile(fileName) } if err != nil { return fmt.Errorf("encodeImage: %w", err) @@ -647,6 +708,11 @@ func (c *Convertor) encodeIM(i image.Image, fileName string) error { _ = mw.WriteImage(fileName) case ".jpg", ".jpeg": _ = mw.SetImageFormat("JPEG") + _ = mw.SetImageCompressionQuality(uint(c.Opts.Quality)) + _ = mw.WriteImage(fileName) + case ".avif": + _ = mw.SetImageFormat("AVIF") + _ = mw.SetImageCompressionQuality(uint(c.Opts.Quality)) _ = mw.WriteImage(fileName) } @@ -846,7 +912,7 @@ func (c *Convertor) isDocument(f string) bool { // isImage checks if file is image. func (c *Convertor) isImage(f string) bool { - var types = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".webp"} + var types = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".webp", ".avif"} for _, t := range types { if strings.ToLower(filepath.Ext(f)) == t { return true diff --git a/cmd/cbconvert/go.mod b/cmd/cbconvert/go.mod index 20ca1c4..59af14b 100644 --- a/cmd/cbconvert/go.mod +++ b/cmd/cbconvert/go.mod @@ -20,6 +20,7 @@ require ( github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/rivo/uniseg v0.3.4 // indirect + github.com/strukturag/libheif v1.13.0 // indirect golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect diff --git a/cmd/cbconvert/go.sum b/cmd/cbconvert/go.sum index a7ff35b..57465e0 100644 --- a/cmd/cbconvert/go.sum +++ b/cmd/cbconvert/go.sum @@ -31,6 +31,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/strukturag/libheif v1.13.0 h1:SuLo/Fl/Nzbw0ixOya1YZSl0Xd27X4fgofGnJdvOHqI= +github.com/strukturag/libheif v1.13.0/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= diff --git a/cmd/cbconvert/main.go b/cmd/cbconvert/main.go index 1e75392..5e5d6a3 100644 --- a/cmd/cbconvert/main.go +++ b/cmd/cbconvert/main.go @@ -120,8 +120,9 @@ func parseFlags() (cbconvert.Options, []string) { convert.IntVar(&opts.Width, "width", 0, "Image width") convert.IntVar(&opts.Height, "height", 0, "Image height") convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height") - convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp") + convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif") convert.IntVar(&opts.Quality, "quality", 75, "Image quality") + convert.BoolVar(&opts.Lossless, "lossless", false, "Lossless compression (avif)") convert.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos") convert.BoolVar(&opts.NoCover, "no-cover", false, "Do not convert the cover image") convert.BoolVar(&opts.NoRGB, "no-rgb", false, "Do not convert images that have RGB colorspace") diff --git a/go.mod b/go.mod index ca7315f..16697dd 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/gen2brain/go-fitz v1.20.1 github.com/gen2brain/go-unarr v0.1.6 github.com/hotei/bmp v0.0.0-20150430041436-f620cebab0c7 + github.com/strukturag/libheif v1.13.0 golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde gopkg.in/gographics/imagick.v3 v3.4.1 diff --git a/go.sum b/go.sum index dda665f..25a2ba7 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/gen2brain/go-unarr v0.1.6 h1:2TtfIQ2dGuCkgEYa+vPE1ydcpkB3CtBbdYMfRSGL github.com/gen2brain/go-unarr v0.1.6/go.mod h1:P05CsEe8jVEXhxqXqp9mFKUKFV0BKpFmtgNWf8Mcoos= 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= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=