diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b42cc36..788fa80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,23 +10,9 @@ jobs: with: cmake-version: '3.27.x' - - name: Wget libaom - uses: wei/wget@v1 - with: - args: https://storage.googleapis.com/aom-releases/libaom-3.7.0.tar.gz - - name: Unpack libaom + - name: Update apt-get run: | - tar -xpf libaom-3.7.0.tar.gz && mkdir build - - name: Configure libaom - working-directory: build - run: | - cmake -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_DOCS=OFF \ - -DENABLE_EXAMPLES=OFF -DENABLE_NASM=OFF -DENABLE_TESTS=OFF -DENABLE_TOOLS=OFF -DENABLE_WERROR=OFF \ - -DAOM_TARGET_CPU=generic ../libaom-3.7.0 - - name: Install libaom - working-directory: build - run: | - make -j3 && sudo make install + sudo apt-get update - name: Wget libheif uses: wei/wget@v1 @@ -35,16 +21,72 @@ jobs: - name: Unpack libheif run: | tar -xpf libheif-1.15.2.tar.gz + - name: Install libheif dependencies + run: | + sudo apt-get install libaom-dev -y - name: Configure libheif working-directory: libheif-1.15.2 run: | ./configure --prefix=/usr --libdir=/usr/lib/x86_64-linux-gnu --enable-shared --disable-static --disable-libde265 \ - --disable-dav1d --disable-go --enable-aom --disable-gdk-pixbuf --disable-rav1e --disable-tests --disable-x265 + --disable-dav1d --disable-go --enable-aom --disable-gdk-pixbuf --disable-rav1e --disable-tests --disable-x265 --disable-examples - name: Install libheif working-directory: libheif-1.15.2 run: | make -j3 && sudo make install + - name: Wget lcms2 + uses: wei/wget@v1 + with: + args: https://github.com/mm2/Little-CMS/releases/download/lcms2.15/lcms2-2.15.tar.gz + - name: Unpack lcms2 + run: | + tar -xpf lcms2-2.15.tar.gz + - name: Configure lcms2 + working-directory: lcms2-2.15 + run: | + ./configure --prefix=/usr --libdir=/usr/lib/x86_64-linux-gnu --enable-shared --disable-static + - name: Install lcms2 + working-directory: lcms2-2.15 + run: | + make -j3 && sudo make install + + - name: Wget highway + uses: wei/wget@v1 + with: + args: -O highway-1.0.5.tar.gz https://github.com/google/highway/archive/refs/tags/1.0.5.tar.gz + - name: Unpack highway + run: | + tar -xpf highway-1.0.5.tar.gz && mkdir -p highway-1.0.5/build + - name: Configure highway + working-directory: highway-1.0.5/build + run: | + cmake -DCMAKE_INSTALL_PREFIX=/usr -DHWY_ENABLE_TESTS=OFF -DHWY_ENABLE_EXAMPLES=OFF -DHWY_WARNINGS_ARE_ERRORS=OFF ../ + - name: Install highway + working-directory: highway-1.0.5/build + run: | + make -j3 && sudo make install + + - name: Wget libjxl + uses: wei/wget@v1 + with: + args: -O libjxl-0.8.2.tar.gz https://github.com/libjxl/libjxl/archive/refs/tags/v0.8.2.tar.gz + - name: Unpack libjxl + run: | + tar -xpf libjxl-0.8.2.tar.gz && mkdir -p libjxl-0.8.2/build + - name: Configure libjxl + working-directory: libjxl-0.8.2/build + run: | + cmake -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DJPEGXL_ENABLE_BENCHMARK=OFF \ + -DJPEGXL_ENABLE_COVERAGE=OFF -DJPEGXL_ENABLE_FUZZERS=OFF -DJPEGXL_ENABLE_SJPEG=OFF -DJPEGXL_WARNINGS_AS_ERRORS=OFF \ + -DJPEGXL_ENABLE_SKCMS=OFF -DJPEGXL_ENABLE_VIEWERS=OFF -DJPEGXL_ENABLE_PLUGINS=OFF -DJPEGXL_ENABLE_DOXYGEN=OFF \ + -DJPEGXL_ENABLE_MANPAGES=OFF -DJPEGXL_ENABLE_JNI=OFF -DJPEGXL_ENABLE_JPEGLI_LIBJPEG=OFF -DJPEGXL_ENABLE_TCMALLOC=OFF \ + -DJPEGXL_ENABLE_EXAMPLES=OFF -DJPEGXL_ENABLE_TOOLS=OFF -DJPEGXL_ENABLE_OPENEXR=OFF -DBUILD_TESTING=OFF \ + -DJXL_HWY_DISABLED_TARGETS_FORCED=ON -DJPEGXL_FORCE_SYSTEM_BROTLI=ON -DJPEGXL_FORCE_SYSTEM_HWY=ON ../ + - name: Install libjxl + working-directory: libjxl-0.8.2/build + run: | + make -j3 && sudo make install + - name: Wget ImageMagick uses: wei/wget@v1 with: @@ -59,20 +101,20 @@ jobs: --without-frozenpaths --without-utilities --disable-hdri --disable-opencl --without-modules --without-magick-plus-plus --without-perl \ --without-bzlib --without-x --without-zip --with-zlib --without-dps --without-djvu --without-autotrace --without-fftw \ --without-fpx --without-fontconfig --without-freetype --without-gslib --without-gvc --without-jbig --without-openjp2 \ - --without-jxl --without-lcms --without-lqr --without-lzma --without-openexr --without-pango --without-raw \ - --without-rsvg --without-wmf --without-xml --disable-openmp --with-jpeg --with-heic --with-png --with-tiff --with-webp + --without-lcms --without-lqr --without-lzma --without-openexr --without-pango --without-raw --without-rsvg --without-wmf \ + --without-xml --disable-openmp --with-jpeg --with-heic --with-jxl --with-png --with-tiff --with-webp - name: Install ImageMagick working-directory: ImageMagick-7.1.1-15 run: | make -j3 && sudo make install - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: go-version: 1.21 - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Test run: go test diff --git a/README.md b/README.md index 403e00f..91f72ac 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It can convert comics to different formats to fit your various devices. * reads CBR (RAR), CBZ (ZIP), CB7 (7Z), CBT (TAR), PDF, XPS, EPUB, MOBI and plain directory * saves processed comics in ZIP archive format or TAR -* images can be converted to JPEG, PNG, TIFF, WEBP, AVIF, or 4-Bit BMP (16 colors) file format +* images can be converted to JPEG, PNG, TIFF, WEBP, AVIF, JXL, or 4-Bit BMP (16 colors) file format * rotate, adjust brightness/contrast, adjust levels (Photoshop-like) or grayscale images * resize filters (NearestNeighbor, Box, Linear, MitchellNetravali, CatmullRom, Gaussian, Lanczos) * export covers from comics @@ -58,13 +58,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, avif (default "jpeg") +            Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg") --archive Archive format, valid values are zip, tar (default "zip")         --quality             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 @@ -114,7 +112,7 @@ 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, avif (default "jpeg") +            Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg")         --quality             Image quality (default "75")         --filter @@ -198,6 +196,6 @@ This table maps quality settings for JPEG to the respective AVIF and WEBP qualit ### Compile -Install ImageMagick7, MuPDF and libheif (with libaom) libraries and headers and then install to GOBIN: +Install ImageMagick7 (with libheif/libjxl support) and MuPDF libraries and headers and then install to GOBIN: `go install -tags extlib github.com/gen2brain/cbconvert/cmd/cbconvert@latest` diff --git a/cbconvert.go b/cbconvert.go index 1baebc7..4bcee0b 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -27,7 +27,6 @@ import ( "image/png" "github.com/chai2010/webp" - "github.com/strukturag/libheif/go/heif" "golang.org/x/image/tiff" "github.com/disintegration/imaging" @@ -69,14 +68,12 @@ var filters = map[int]imaging.ResampleFilter{ // Options type. type Options struct { - // Image format, valid values are jpeg, png, tiff, bmp, webp, avif + // Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl Format string // Archive format, valid values are zip, tar Archive string // JPEG image quality Quality int - // Lossless compression (avif) - Lossless bool // Image width Width int // Image height @@ -179,6 +176,20 @@ type Image struct { SizeHuman string } +// NewOptions returns default options. +func NewOptions() Options { + o := Options{} + o.Format = "jpeg" + o.Archive = "zip" + o.Quality = 75 + o.Filter = 2 + o.LevelsGamma = 1.0 + o.LevelsInMax = 255 + o.LevelsOutMax = 255 + + return o +} + // New returns new convertor. func New(o Options) *Convertor { c := &Convertor{} @@ -479,12 +490,11 @@ func (c *Convertor) imageConvert(ctx context.Context, img image.Image, index int } switch c.Opts.Format { - case "jpeg", "png", "tiff", "webp", "avif": + case "jpeg", "png", "tiff", "webp": if err := c.imageEncode(img, fileName); err != nil { return fmt.Errorf("imageConvert: %w", err) } - case "bmp": - // convert image to 4-Bit BMP (16 colors) + case "bmp", "avif", "jxl": if err := c.imEncode(img, fileName); err != nil { return fmt.Errorf("imageConvert: %w", err) } @@ -643,18 +653,6 @@ func (c *Convertor) imageEncode(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, e := heif.EncodeFromImage(img, heif.CompressionAV1, c.Opts.Quality, lossLess, 0) - if e != nil { - return fmt.Errorf("imageEncode: %w", e) - } - err = ctx.WriteToFile(fileName) } if err != nil { @@ -680,16 +678,10 @@ func (c *Convertor) imEncode(i image.Image, fileName string) error { if err := mw.SetImageFormat("PNG"); err != nil { return fmt.Errorf("imEncode: %w", err) } - if err := mw.WriteImage(fileName); err != nil { - return fmt.Errorf("imEncode: %w", err) - } case ".tif", ".tiff": if err := mw.SetImageFormat("TIFF"); err != nil { return fmt.Errorf("imEncode: %w", err) } - if err := mw.WriteImage(fileName); err != nil { - return fmt.Errorf("imEncode: %w", err) - } case ".bmp": pw := imagick.NewPixelWand() pw.SetColor("black") @@ -713,10 +705,7 @@ func (c *Convertor) imEncode(i image.Image, fileName string) error { if err := mw.SetImageCompression(imagick.COMPRESSION_NO); err != nil { 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("imEncode: %w", err) - } - if err := mw.WriteImage(fileName); err != nil { + if err := mw.QuantizeImage(16, mw.GetImageColorspace(), 1, imagick.DITHER_METHOD_NO, true); err != nil { return fmt.Errorf("imEncode: %w", err) } case ".jpg", ".jpeg": @@ -726,9 +715,6 @@ func (c *Convertor) imEncode(i image.Image, fileName string) error { if err := mw.SetImageCompressionQuality(uint(c.Opts.Quality)); err != nil { return fmt.Errorf("imEncode: %w", err) } - if err := mw.WriteImage(fileName); err != nil { - return fmt.Errorf("imEncode: %w", err) - } case ".avif": if err := mw.SetImageFormat("AVIF"); err != nil { return fmt.Errorf("imEncode: %w", err) @@ -736,9 +722,17 @@ func (c *Convertor) imEncode(i image.Image, fileName string) error { if err := mw.SetImageCompressionQuality(uint(c.Opts.Quality)); err != nil { return fmt.Errorf("imEncode: %w", err) } - if err := mw.WriteImage(fileName); err != nil { + case ".jxl": + if err := mw.SetImageFormat("JXL"); err != nil { return fmt.Errorf("imEncode: %w", err) } + if err := mw.SetImageCompressionQuality(uint(c.Opts.Quality)); err != nil { + return fmt.Errorf("imEncode: %w", err) + } + } + + if err := mw.WriteImage(fileName); err != nil { + return fmt.Errorf("imEncode: %w", err) } return nil @@ -1409,7 +1403,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", ".avif"} + var types = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".webp", ".avif", ".jxl"} for _, t := range types { if strings.ToLower(filepath.Ext(f)) == t { return true @@ -1530,7 +1524,7 @@ func (c *Convertor) Files(args []string) ([]File, error) { count := 0 for _, fn := range fs { - if !fn.IsDir() { + if !fn.IsDir() && c.isImage(fn.Name()) { count++ } } @@ -1632,11 +1626,11 @@ func (c *Convertor) Cover(fileName string, fileInfo os.FileInfo) error { } switch c.Opts.Format { - case "jpeg", "png", "tiff", "webp", "avif": + case "jpeg", "png", "tiff", "webp": if err := c.imageEncode(cover, fName); err != nil { return fmt.Errorf("%s: %w", fileName, err) } - case "bmp": + case "bmp", "avif", "jxl": if err := c.imEncode(cover, fName); err != nil { return fmt.Errorf("%s: %w", fileName, err) } @@ -1791,11 +1785,11 @@ func (c *Convertor) Preview(fileName string, fileInfo os.FileInfo, width, height tmpName := c.tempName("cbc", "."+c.Opts.Format) switch c.Opts.Format { - case "jpeg", "png", "tiff", "webp", "avif": + case "jpeg", "png", "tiff", "webp": if err := c.imageEncode(i, tmpName); err != nil { return img, fmt.Errorf("%s: %w", fileName, err) } - case "bmp": + case "bmp", "avif", "jxl": if err := c.imEncode(i, tmpName); err != nil { return img, fmt.Errorf("%s: %w", fileName, err) } diff --git a/cbconvert_test.go b/cbconvert_test.go index bc4a9c4..6a68152 100644 --- a/cbconvert_test.go +++ b/cbconvert_test.go @@ -13,11 +13,8 @@ func TestConvert(t *testing.T) { t.Error(err) } - opts := Options{} + opts := NewOptions() opts.OutDir = tmpDir - opts.Archive = "zip" - opts.Quality = 75 - opts.Filter = 2 conv := New(opts) @@ -29,7 +26,7 @@ func TestConvert(t *testing.T) { t.Error(err) } - for _, format := range []string{"jpeg", "png", "tiff", "bmp", "webp", "avif"} { + for _, format := range []string{"jpeg", "png", "tiff", "bmp", "webp", "avif", "jxl"} { conv.Opts.Format = format for _, file := range files { @@ -54,11 +51,8 @@ func TestCover(t *testing.T) { t.Error(err) } - opts := Options{} + opts := NewOptions() opts.OutDir = tmpDir - opts.Quality = 75 - opts.Filter = 2 - opts.Format = "jpeg" conv := New(opts) @@ -89,9 +83,8 @@ func TestThumbnail(t *testing.T) { t.Error(err) } - opts := Options{} + opts := NewOptions() opts.OutDir = tmpDir - opts.Filter = 2 conv := New(opts) diff --git a/cmd/cbconvert/main.go b/cmd/cbconvert/main.go index 027df75..60f3680 100644 --- a/cmd/cbconvert/main.go +++ b/cmd/cbconvert/main.go @@ -169,10 +169,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, avif") + convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl") convert.StringVar(&opts.Archive, "archive", "zip", "Archive format, valid values are zip, tar") 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") @@ -255,7 +254,7 @@ func parseFlags() (cbconvert.Options, []string) { } if len(os.Args) < 2 { - convert.Usage() + flag.Usage() _, _ = fmt.Fprintf(os.Stderr, "no command\n") os.Exit(1) } diff --git a/go.mod b/go.mod index f03b7df..c0720ca 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,7 @@ require ( github.com/fvbommel/sortorder v1.1.0 github.com/gen2brain/go-fitz v1.23.1 github.com/gen2brain/go-unarr v0.1.7 - github.com/strukturag/libheif v1.15.2 - golang.org/x/image v0.11.0 + golang.org/x/image v0.12.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 50bdfd5..fe7066c 100644 --- a/go.sum +++ b/go.sum @@ -10,14 +10,12 @@ github.com/gen2brain/go-fitz v1.23.1 h1:x69/szWZXpI3jZ57mMqCg7WqqvtYnQG0lXts3L6M github.com/gen2brain/go-fitz v1.23.1/go.mod h1:HU04vc+RisUh/kvEd2pB0LAxmK1oyXdN4ftyshUr9rQ= github.com/gen2brain/go-unarr v0.1.7 h1:mEE7bPShJIsmAX67t6BW2ibpEUO7j5WK152KgNM9NbQ= github.com/gen2brain/go-unarr v0.1.7/go.mod h1:MK9a3hddpaIxjEtrE1f/LA5yJ7gA34cS7Oyr325sY9s= -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.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= -golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= +golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= +golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= 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= @@ -42,7 +40,7 @@ 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.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.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=