diff --git a/README.md b/README.md index 449489f..7319ccb 100644 --- a/README.md +++ b/README.md @@ -1,184 +1,171 @@ -CBconvert -========= +## CBconvert -Introduction ------------- +### Introduction -CBconvert is a [Comic Book](http://en.wikipedia.org/wiki/Comic_Book_Archive_file) converter written in [Go language](https://golang.org/). +CBconvert is a [Comic Book](http://en.wikipedia.org/wiki/Comic_Book_Archive_file) converter. -It can convert one comic at a time or bulk convert comics to different formats to fit your various devices. +It can convert comics to different formats to fit your various devices. -![screenshot](https://goo.gl/AxsWsA) +### Features -Features --------- + - 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 + - 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 + - create thumbnails from covers by [FreeDesktop](http://www.freedesktop.org/wiki/) specification - - reads RAR, ZIP, 7Z, GZ, BZ2, CBR, CBZ, CB7, CBT, PDF, EPUB, XPS and plain directory - - always saves processed comic in CBZ (ZIP) archive format - - images can be converted to JPEG, PNG, GIF, TIFF or 4-Bit BMP (16 colors) file format - - rotate, flip, adjust brightness/contrast, adjust levels (Photoshop like) or grayscale images - - choose resize algorithm (NearestNeighbor, Box, Linear, MitchellNetravali, CatmullRom, Gaussian, Lanczos) - - export covers from comics - - create thumbnails from covers by [freedesktop](http://www.freedesktop.org/wiki/) specification +### Download -Download --------- + - [Windows](https://github.com/gen2brain/cbconvert/releases/download/0.7.0/cbconvert-0.7.zip) + - [Linux](https://github.com/gen2brain/cbconvert/releases/download/0.7.0/cbconvert-0.7.tar.gz) - - [Windows GUI](https://github.com/gen2brain/cbconvert/releases/download/0.6.0/cbconvert-0.6.zip) - - [Windows CMD](https://github.com/gen2brain/cbconvert/releases/download/0.6.0/cbconvert-cmd-0.6.zip) +### Using cbconvert in file managers to generate FreeDesktop thumbnails - - [Linux 64bit GUI](https://github.com/gen2brain/cbconvert/releases/download/0.6.0/cbconvert-0.6.tar.gz) - - [Linux 64bit CMD](https://github.com/gen2brain/cbconvert/releases/download/0.6.0/cbconvert-cmd-0.6.tar.gz) +Copy cbconvert cli binary to your PATH and create file ~/.local/share/thumbnailers/cbconvert.thumbnailer : +    +    [Thumbnailer Entry] +    TryExec=cbconvert +    Exec=cbconvert thumbnail --quiet --width %s --outfile %o %i +    MimeType=application/pdf;application/x-pdf;image/pdf;application/x-cbz;application/x-cbr;application/x-cb7;application/x-cbt;application/epub+zip; -Using cbconvert in file managers to generate freedesktop thumbnails -------------------------------------------------------------------- +This is what it looks like in the PCManFM file manager: -Just copy cbconvert cmd binary to your PATH and create file ~/.local/share/thumbnailers/cbconvert.thumbnailer : - - [Thumbnailer Entry] - TryExec=cbconvert - Exec=cbconvert thumbnail --quiet --width %s --outfile %o %i - MimeType=application/pdf;application/x-pdf;image/pdf;application/x-cbz;application/x-cbr;application/x-cb7;application/x-cbt;application/oxps;application/vnd.ms-xpsdocument;application/epub+zip; - -This is how it looks like in PCManFM file manager: - -![thumbnails](https://goo.gl/I39Otm) +![thumbnails](https://bit.ly/3BaTvTV) -Using command line app ----------------------- +### Using command line app - usage: cbconvert [] [ ...] - - Comic Book convert tool. - - Flags: - --help Show context-sensitive help (also try --help-long and --help-man). - --version Show application version. - --outdir="." Output directory - --size=0 Process only files larger then size (in MB) - --recursive Process subdirectories recursively - --quiet Hide console output - - Args: - filename or directory - - Commands: - help [...] - Show help. +    Usage: cbconvert [] [file1 dir1 ... fileOrDirN] - convert [] ... - Convert archive or document (default command) +    Commands: - --width=0 Image width - --height=0 Image height - --fit Best fit for required width and height - --format="jpeg" Image format, valid values are jpeg, png, gif, tiff, bmp - --quality=75 JPEG image quality - --filter=2 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos - --cover Convert cover image (use --no-cover if you want to exclude cover) - --rgb Convert images that have RGB colorspace (use --no-rgb if you only want to convert grayscaled images) - --nonimage Leave non image files in archive (use --no-nonimage to remove non image files from archive) - --grayscale Convert images to grayscale (monochromatic) - --rotate=0 Rotate images, valid values are 0, 90, 180, 270 - --flip="none" Flip images, valid values are none, horizontal, vertical - --brightness=0 Adjust brightness of the images, must be in range (-100, 100) - --contrast=0 Adjust contrast of the images, must be in range (-100, 100) - --suffix=SUFFIX Add suffix to file basename - --levels-inmin=0 Shadow input value - --levels-inmax=255 Highlight input value - --levels-gamma=1 Midpoint/Gamma - --levels-outmin=0 Shadow output value - --levels-outmax=255 Highlight output value +      convert* +            Convert archive or document (default command) - cover [] ... - Extract cover +        --width +            Image width (default "0") +        --height +            Image height (default "0") +        --fit +            Best fit for required width and height (default "false") +        --format +            Image format, valid values are jpeg, png, tiff, bmp, webp (default "jpeg") +        --quality +            JPEG image quality (default "75") +        --filter +            0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2") +        --no-cover +            Do not convert the cover image (default "false") +        --no-rgb +            Do not convert images that have RGB colorspace (default "false") +        --no-nonimage +            Remove non-image files from the archive (default "false") +        --no-convert +       Do not transform or convert images (default "false") +        --grayscale +            Convert images to grayscale (monochromatic) (default "false") +        --rotate +            Rotate images, valid values are 0, 90, 180, 270 (default "0") +        --flip +            Flip images, valid values are none, horizontal, vertical (default "none") +        --brightness +            Adjust the brightness of the images, must be in the range (-100, 100) (default "0") +        --contrast +            Adjust the contrast of the images, must be in the range (-100, 100) (default "0") +        --suffix +            Add suffix to file basename (default "") +        --levels-inmin +            Shadow input value (default "0") +        --levels-gamma +            Midpoint/Gamma (default "1") +        --levels-inmax +            Highlight input value (default "255") +        --levels-outmin +            Shadow output value (default "0") +        --levels-outmax +            Highlight output value (default "255") +        --outdir +            Output directory (default ".") +        --size +            Process only files larger than size (in MB) (default "0") +        --recursive +            Process subdirectories recursively (default "false") +        --quiet +            Hide console output (default "false") - --width=0 Image width - --height=0 Image height - --fit Best fit for required width and height - --quality=75 JPEG image quality - --filter=2 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos +      cover +            Extract cover - thumbnail [] ... - Extract cover thumbnail (freedesktop spec.) +        --width +            Image width (default "0") +        --height +            Image height (default "0") +        --fit +            Best fit for required width and height (default "false") +        --quality +            JPEG image quality (default "75") +        --filter +            0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2") +        --outdir +            Output directory (default ".") +        --size +            Process only files larger than size (in MB) (default "0") +        --recursive +            Process subdirectories recursively (default "false") +        --quiet +            Hide console output (default "false") - --width=0 Image width - --height=0 Image height - --fit Best fit for required width and height - --filter=2 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos +      thumbnail +            Extract cover thumbnail (freedesktop spec.) -[man page](https://en.wikipedia.org/wiki/Man_page) is also available: +        --width +            Image width (default "0") +        --height +            Image height (default "0") +        --fit +            Best fit for required width and height (default "false") +        --filter +            0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2") +        --outdir +            Output directory (default ".") +        --outfile +            Output file (default "") +        --size +            Process only files larger than size (in MB) (default "0") +        --recursive +            Process subdirectories recursively (default "false") +        --quiet +            Hide console output (default "false") - cbconvert --help-man | man /dev/stdin +### Examples -Examples --------- +Rescale images to 1200px for all supported files found in a directory with a size larger than 60MB: -Rescale images to 1200px for all supported files found in directory with size larger then 60MB: +    cbconvert --recursive --width 1200 --size 60 /media/comics/Thorgal/ - cbconvert --recursive --width 1200 --size 60 /media/comics/Thorgal/ +Convert all images in pdf to 4bit BMP images and save the result in ~/comics directory: -Convert all images in pdf to 4bit BMP image and save result in ~/comics directory: +    cbconvert --bmp --outdir ~/comics /media/comics/Garfield/Garfield_01.pdf - cbconvert --bmp --outdir ~/comics /media/comics/Garfield/Garfield_01.pdf +[BMP](http://en.wikipedia.org/wiki/BMP_file_format) format is a very good choice for black&white pages. Archive size can be smaller 2-3x and the file will be readable by comic readers. -[BMP](http://en.wikipedia.org/wiki/BMP_file_format) format is very good choice for black&white pages. Archive size can be smaller 2-3x and file will be readable by comic readers. +Generate thumbnails by [freedesktop specification](http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html) in ~/.cache/thumbnails/normal directory with width 512: -Generate thumbnails by [freedesktop specification](http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html) in ~/.thumbnails/normal directory with width 512: +    cbconvert thumbnail --width 512 --outdir ~/.cache/thumbnails/normal /media/comics/GrooTheWanderer/ - cbconvert thumbnail --width 512 --outdir ~/.thumbnails/normal /media/comics/GrooTheWanderer/ +Extract covers to ~/covers dir for all supported files found in the directory, Lanczos algorithm is used for resizing: -Extract covers to ~/covers dir for all supported files found in directory, Lanczos algorithm is used for resizing: +    cbconvert cover --outdir ~/covers --filter=7 /media/comics/GrooTheWanderer/ - cbconvert cover --outdir ~/covers --filter=7 /media/comics/GrooTheWanderer/ +### Compile -Compile -------- +Install ImageMagick development packages, e.g. on Ubuntu: -Install imagemagick dev packages: +    apt-get install libmagickcore-dev libmagickwand-dev - apt-get install libmagickcore-dev libmagickwand-dev +Install to GOBIN: -Compile latest MuPDF: - - git clone git://git.ghostscript.com/mupdf.git && cd mupdf - git submodule update --init --recursive - curl -L https://gist.githubusercontent.com/gen2brain/7869ac4c6db5933f670f/raw/1619394dc957ae10bcd73c713760993466b4bfea/mupdf-openssl-curl.patch | patch -p1 - sed -e "1iHAVE_X11 = no" -e "1iWANT_OPENSSL = no" -e "1iWANT_CURL = no" -i Makerules - HAVE_X11=no HAVE_GLFW=no HAVE_GLUT=no WANT_OPENSSL=no WANT_CURL=no HAVE_MUJS=yes HAVE_JSCORE=no HAVE_V8=no make && make install - -Compile unarr library: - - git clone https://github.com/zeniko/unarr && cd unarr - mkdir lzma920 && cd lzma920 && curl -L http://www.7-zip.org/a/lzma920.tar.bz2 | tar -xjvp && cd .. - curl -L http://zlib.net/zlib-1.2.8.tar.gz | tar -xzvp - curl -L http://www.bzip.org/1.0.6/bzip2-1.0.6.tar.gz | tar -xzvp - curl -L https://gist.githubusercontent.com/gen2brain/89fe506863be3fb139e8/raw/8783a7d81e22ad84944d146c5e33beab6dffc641/unarr-makefile.patch | patch -p1 - CFLAGS="-DHAVE_7Z -DHAVE_ZLIB -DHAVE_BZIP2 -I./lzma920/C -I./zlib-1.2.8 -I./bzip2-1.0.6" make - cp build/debug/libunarr.a /usr/lib64/ && cp unarr.h /usr/include - -Install dependencies: - - go get github.com/cheggaaa/pb - go get github.com/disintegration/imaging - go get github.com/gen2brain/go-fitz - go get github.com/gen2brain/go-unarr - go get github.com/gographics/imagick/imagick - go get github.com/hotei/bmp - go get github.com/skarademir/naturalsort - go get golang.org/x/image/tiff - go get golang.org/x/image/webp - go get gopkg.in/alecthomas/kingpin.v2 - -For command line app: - - go get github.com/gen2brain/cbconvert - go build -o $GOPATH/bin/cbconvert github.com/gen2brain/cbconvert/cmd - -For GUI app: - - go get gopkg.in/qml.v1 - go get github.com/gen2brain/cbconvert - go build -o $GOPATH/bin/cbconvert github.com/gen2brain/cbconvert/gui +    go install github.com/gen2brain/cbconvert/cmd/cbconvert@latest diff --git a/cbconvert.go b/cbconvert.go index 32a42b7..22f4f3d 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -1,33 +1,18 @@ -// Author: Milan Nikolic -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - package cbconvert import ( "archive/zip" "bytes" + "context" "crypto/md5" - "errors" "fmt" "image" "image/color" - "image/gif" + "image/draw" + _ "image/gif" "image/jpeg" "image/png" "io" - "io/ioutil" "mime" "os" "path/filepath" @@ -35,28 +20,36 @@ import ( "sort" "strconv" "strings" - "sync" + "sync/atomic" + + "github.com/chai2010/webp" + _ "github.com/hotei/bmp" + "golang.org/x/image/tiff" - "github.com/cheggaaa/pb" "github.com/disintegration/imaging" + "github.com/fvbommel/sortorder" "github.com/gen2brain/go-fitz" "github.com/gen2brain/go-unarr" - "github.com/gographics/imagick/imagick" - _ "github.com/hotei/bmp" - "github.com/skarademir/naturalsort" - "golang.org/x/image/tiff" - _ "golang.org/x/image/webp" + "golang.org/x/sync/errgroup" + "gopkg.in/gographics/imagick.v3/imagick" ) -// Resample filters +// Resample filters. const ( - NearestNeighbor int = iota // Fastest resampling filter, no antialiasing - Box // Box filter (averaging pixels) - Linear // Bilinear filter, smooth and reasonably fast - MitchellNetravali // А smooth bicubic filter - CatmullRom // A sharp bicubic filter - Gaussian // Blurring filter that uses gaussian function, useful for noise removal - Lanczos // High-quality resampling filter, it's slower than cubic filters + // NearestNeighbor is the fastest resampling filter, no antialiasing + NearestNeighbor int = iota + // Box filter (averaging pixels) + Box + // Linear is the bilinear filter, smooth and reasonably fast + Linear + // MitchellNetravali is a smooth bicubic filter + MitchellNetravali + // CatmullRom is a sharp bicubic filter + CatmullRom + // Gaussian is a blurring filter that uses gaussian function, useful for noise removal + Gaussian + // Lanczos is a high-quality resampling filter, it's slower than cubic filters + Lanczos ) var filters = map[int]imaging.ResampleFilter{ @@ -69,131 +62,160 @@ var filters = map[int]imaging.ResampleFilter{ Lanczos: imaging.Lanczos, } -var ( - bar *pb.ProgressBar - wg sync.WaitGroup -) - -// Limits go routines to number of CPUs + 1 -var throttle = make(chan int, runtime.NumCPU()+1) - -// Options +// Options type. type Options struct { - Format string // Image format, valid values are jpeg, png, gif, tiff, bmp - Quality int // JPEG image quality - Width int // image width - Height int // image height - Fit bool // Best fit for required width and height - Filter int // 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos - ConvertCover bool // convert cover image - RGB bool // convert images that have RGB colorspace - NonImage bool // Leave non image files in archive - Suffix string // add suffix to file basename - Cover bool // extract cover - Thumbnail bool // extract cover thumbnail (freedesktop spec.) - Outfile string // output file - Outdir string // output directory - Grayscale bool // convert images to grayscale (monochromatic) - Rotate int // Rotate images, valid values are 0, 90, 180, 270 - Flip string // Flip images, valid values are none, horizontal, vertical - Brightness float64 // Adjust brightness of the images, must be in range (-100, 100) - Contrast float64 // Adjust contrast of the images, must be in range (-100, 100) - Recursive bool // process subdirectories recursively - Size int64 // process only files larger then size (in MB) - Quiet bool // hide console output - LevelsInMin float64 // shadow input value - LevelsInMax float64 // highlight input value - LevelsGamma float64 // midpoint/gamma - LevelsOutMin float64 // shadow output value - LevelsOutMax float64 // highlight output value + // Image format, valid values are jpeg, png, tiff, bmp, webp + Format string + // JPEG image quality + Quality int + // Image width + Width int + // Image height + Height int + // Best fit for required width and height + Fit bool + // 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos + Filter int + // Do not convert the cover image + NoCover bool + // Do not convert images that have RGB colorspace + NoRGB bool + // Remove non-image files from the archive + NoNonImage bool + // Do not transform or convert images + NoConvert bool + // Add suffix to file baseNoExt + Suffix string + // Extract cover + Cover bool + // Extract cover thumbnail (freedesktop spec.) + Thumbnail bool + // Output file + Outfile string + // Output directory + Outdir string + // Convert images to grayscale (monochromatic) + Grayscale bool + // Rotate images, valid values are 0, 90, 180, 270 + Rotate int + // Flip images, valid values are none, horizontal, vertical + Flip string + // Adjust the brightness of the images, must be in the range (-100, 100) + Brightness float64 + // Adjust the contrast of the images, must be in the range (-100, 100) + Contrast float64 + // Process subdirectories recursively + Recursive bool + // Process only files larger than size (in MB) + Size int64 + // Hide console output + Quiet bool + // Shadow input value + LevelsInMin float64 + // Highlight input value + LevelsInMax float64 + // Midpoint/gamma + LevelsGamma float64 + // Shadow output value + LevelsOutMin float64 + // Highlight output value + LevelsOutMax float64 } -// Convertor struct +// Convertor type. type Convertor struct { - Opts Options // Options struct - Workdir string // Current working directory - Nfiles int // Number of files - CurrFile int // Index of current file - Ncontents int // Number of contents in archive/document - CurrContent int // Index of current content + // Options struct + Opts Options + // Current working directory + Workdir string + // Number of files + Nfiles int + // Index of current file + CurrFile int + // Number of contents in archive/document + Ncontents int + // Index of current content + CurrContent int32 + // Start function + OnStart func() + // Progress function + OnProgress func() + // Compress function + OnCompress func() } -// NewConvertor returns new convertor -func NewConvertor(o Options) *Convertor { +// New returns new convertor. +func New(o Options) *Convertor { c := &Convertor{} c.Opts = o return c } -// Converts image -func (c *Convertor) convertImage(img image.Image, index int, pathName string) { - defer wg.Done() - - var ext string - switch c.Opts.Format { - case "jpeg": - ext = "jpg" - case "png": - ext = "png" - case "gif": - ext = "gif" - case "tiff": - ext = "tiff" - case "bmp": - ext = "bmp" - +// 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 } - var filename string + atomic.AddInt32(&c.CurrContent, 1) + if c.OnProgress != nil { + c.OnProgress() + } + + var ext = c.Opts.Format + var fileName string if pathName != "" { - filename = filepath.Join(c.Workdir, fmt.Sprintf("%s.%s", c.getBasename(pathName), ext)) + fileName = filepath.Join(c.Workdir, fmt.Sprintf("%s.%s", c.baseNoExt(pathName), ext)) } else { - filename = filepath.Join(c.Workdir, fmt.Sprintf("%03d.%s", index, ext)) + fileName = filepath.Join(c.Workdir, fmt.Sprintf("%03d.%s", index, ext)) } - img = c.TransformImage(img) + img = c.transformImage(img) - if c.Opts.LevelsInMin != 0 || c.Opts.LevelsInMax != 255 || c.Opts.LevelsGamma != 1.00 || + if c.Opts.LevelsInMin != 0 || c.Opts.LevelsInMax != 255 || c.Opts.LevelsGamma != 1.0 || c.Opts.LevelsOutMin != 0 || c.Opts.LevelsOutMax != 255 { - img = c.LevelImage(img) + img, err = c.levelImage(img) + if err != nil { + return err + } } switch c.Opts.Format { case "jpeg": - // convert image to JPEG (default) - if c.Opts.Grayscale { - c.encodeImageMagick(img, filename) - } else { - c.encodeImage(img, filename) + err = c.encodeImage(img, fileName) + if err != nil { + return err } case "png": - // convert image to PNG - if c.Opts.Grayscale { - c.encodeImageMagick(img, filename) - } else { - c.encodeImage(img, filename) + err = c.encodeImage(img, fileName) + if err != nil { + return err } - case "gif": - // convert image to GIF - c.encodeImageMagick(img, filename) case "tiff": - // convert image to TIFF - if c.Opts.Grayscale { - c.encodeImageMagick(img, filename) - } else { - c.encodeImage(img, filename) + err = c.encodeImage(img, fileName) + if err != nil { + return err } case "bmp": // convert image to 4-Bit BMP (16 colors) - c.encodeImageMagick(img, filename) + err = c.encodeIM(img, fileName) + if err != nil { + return err + } + case "webp": + err = c.encodeImage(img, fileName) + if err != nil { + return err + } } - <-throttle + return nil } -// Transforms image (resize, rotate, flip, brightness, contrast) -func (c *Convertor) TransformImage(img image.Image) image.Image { - var i image.Image = img +// 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 { @@ -234,17 +256,17 @@ func (c *Convertor) TransformImage(img image.Image) image.Image { return i } -// Applies a Photoshop-like levels operation on an image -func (c *Convertor) LevelImage(img image.Image) image.Image { +// levelImage applies a Photoshop-like levels operation on an image. +func (c *Convertor) levelImage(img image.Image) (image.Image, error) { imagick.Initialize() mw := imagick.NewMagickWand() defer mw.Destroy() - err := mw.ReadImageBlob(c.GetImageBytes(img)) + rgba := imageToRGBA(img) + err := mw.ConstituteImage(uint(img.Bounds().Dx()), uint(img.Bounds().Dy()), "RGBA", imagick.PIXEL_CHAR, rgba.Pix) if err != nil { - fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error()) - return img + return img, fmt.Errorf("levelImage: %w", err) } _, qrange := imagick.GetQuantumRange() @@ -257,90 +279,88 @@ func (c *Convertor) LevelImage(img image.Image) image.Image { err = mw.LevelImage(inmin, c.Opts.LevelsGamma, inmax) if err != nil { - fmt.Fprintf(os.Stderr, "Error LevelImageChannel Input: %v\n", err.Error()) - return img + return img, fmt.Errorf("levelImage: %w", err) } err = mw.LevelImage(-outmin, 1.0, quantumRange+(quantumRange-outmax)) if err != nil { - fmt.Fprintf(os.Stderr, "Error LevelImageChannel Output: %v\n", err.Error()) - return img + return img, fmt.Errorf("levelImage: %w", err) } blob := mw.GetImageBlob() i, err := c.decodeImage(bytes.NewReader(blob), "levels") if err != nil { - fmt.Fprintf(os.Stderr, "Error decodeImage: %v\n", err.Error()) - return img + return img, fmt.Errorf("levelImage: %w", err) } - return i + return i, nil } -// Converts PDF/EPUB/XPS document to CBZ -func (c *Convertor) convertDocument(file string) { - c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") +// convertDocument converts PDF/EPUB/XPS document to CBZ. +func (c *Convertor) convertDocument(fileName string) error { + c.Workdir, _ = os.MkdirTemp(os.TempDir(), "cbc") - doc, err := fitz.NewDocument(file) + doc, err := fitz.New(fileName) if err != nil { - fmt.Fprintf(os.Stderr, "Skipping %s, error: %v", file, err.Error()) - return + return fmt.Errorf("convertDocument: %w", err) } - c.Ncontents = doc.Pages() + defer doc.Close() + + c.Ncontents = doc.NumPage() c.CurrContent = 0 - if !c.Opts.Quiet { - bar = pb.New(c.Ncontents) - bar.ShowTimeLeft = false - bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.CurrFile, c.Nfiles)) - bar.Start() + if c.OnStart != nil { + c.OnStart() } - for n := 0; n < c.Ncontents; n++ { - c.CurrContent++ - if !c.Opts.Quiet { - bar.Increment() - } + eg, ctx := errgroup.WithContext(context.Background()) + eg.SetLimit(runtime.NumCPU() + 1) + for n := 0; n < c.Ncontents; n++ { img, err := doc.Image(n) if err != nil { - fmt.Fprintf(os.Stderr, "Error Image: %v\n", err.Error()) + return fmt.Errorf("convertDocument: %w", err) } if img != nil { - throttle <- 1 - wg.Add(1) - - go c.convertImage(img, n, "") + eg.Go(func() error { + return c.convertImage(ctx, img, n, "") + }) } } - wg.Wait() + + return eg.Wait() } -// Converts archive to CBZ -func (c *Convertor) convertArchive(file string) { - c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") +// convertArchive converts archive to CBZ. +func (c *Convertor) convertArchive(fileName string) error { + c.Workdir, _ = os.MkdirTemp(os.TempDir(), "cbc") - contents := c.listArchive(file) - c.Ncontents = len(contents) + contents, err := c.listArchive(fileName) + if err != nil { + return fmt.Errorf("convertArchive: %w", err) + } + + images := c.imagesFromSlice(contents) + + c.Ncontents = len(images) c.CurrContent = 0 - cover := c.getCover(c.getImagesFromSlice(contents)) + if c.OnStart != nil { + c.OnStart() + } - archive, err := unarr.NewArchive(file) + cover := c.coverName(images) + + archive, err := unarr.NewArchive(fileName) if err != nil { - fmt.Fprintf(os.Stderr, "Error NewReader: %v\n", err.Error()) - return + return fmt.Errorf("convertArchive: %w", err) } defer archive.Close() - if !c.Opts.Quiet { - bar = pb.New(c.Ncontents) - bar.ShowTimeLeft = false - bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.CurrFile, c.Nfiles)) - bar.Start() - } + eg, ctx := errgroup.WithContext(context.Background()) + eg.SetLimit(runtime.NumCPU() + 1) for { err := archive.Entry() @@ -348,293 +368,317 @@ func (c *Convertor) convertArchive(file string) { if err == io.EOF { break } else { - fmt.Fprintf(os.Stderr, "Error Entry: %v\n", err.Error()) - continue + return fmt.Errorf("convertArchive: %w", err) } } - c.CurrContent++ - if !c.Opts.Quiet { - bar.Increment() + data, err := archive.ReadAll() + if err != nil { + return fmt.Errorf("convertArchive: %w", err) } - size := archive.Size() - pathname := archive.Name() + pathName := archive.Name() - buf := make([]byte, size) - for size > 0 { - n, err := archive.Read(buf) - if err != nil && err != io.EOF { - break - } - size -= n - } - - if size > 0 { - fmt.Printf("Error Read\n") - continue - } - - if c.isImage(pathname) { - img, err := c.decodeImage(bytes.NewReader(buf), pathname) - if err != nil { - fmt.Fprintf(os.Stderr, "Error Decode: %v\n", err.Error()) - continue - } - - if !c.Opts.ConvertCover { - if cover == pathname { - img = c.TransformImage(img) - c.encodeImage(img, filepath.Join(c.Workdir, filepath.Base(pathname))) - continue + if c.isImage(pathName) { + if c.Opts.NoConvert { + err = c.copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))) + if err != nil { + return fmt.Errorf("convertArchive: %w", err) } + + continue } - if !c.Opts.RGB && !c.isGrayScale(img) { - img = c.TransformImage(img) - c.encodeImage(img, filepath.Join(c.Workdir, filepath.Base(pathname))) + img, err := c.decodeImage(bytes.NewReader(data), pathName) + if err != nil { + return fmt.Errorf("convertArchive: %w", err) + } + + if cover == pathName && c.Opts.NoCover { + img = c.transformImage(img) + err = c.encodeImage(img, filepath.Join(c.Workdir, filepath.Base(pathName))) + if err != nil { + return fmt.Errorf("convertArchive: %w", err) + } + + continue + } + + if c.Opts.NoRGB && !c.isGrayScale(img) { + img = c.transformImage(img) + err = c.encodeImage(img, filepath.Join(c.Workdir, filepath.Base(pathName))) + if err != nil { + return fmt.Errorf("convertArchive: %w", err) + } + continue } if img != nil { - throttle <- 1 - wg.Add(1) - go c.convertImage(img, 0, pathname) + eg.Go(func() error { + return c.convertImage(ctx, img, 0, pathName) + }) } } else { - if c.Opts.NonImage { - c.copyFile(bytes.NewReader(buf), filepath.Join(c.Workdir, filepath.Base(pathname))) + if !c.Opts.NoNonImage { + err = c.copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))) + if err != nil { + return fmt.Errorf("convertArchive: %w", err) + } } } } - wg.Wait() + + return eg.Wait() } -// Converts directory to CBZ -func (c *Convertor) convertDirectory(path string) { - c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") +// convertDirectory converts directory to CBZ. +func (c *Convertor) convertDirectory(dirPath string) error { + c.Workdir, _ = os.MkdirTemp(os.TempDir(), "cbc") - images := c.getImagesFromPath(path) + contents, err := c.imagesFromPath(dirPath) + if err != nil { + return fmt.Errorf("convertDirectory: %w", err) + } + + images := c.imagesFromSlice(contents) c.Ncontents = len(images) c.CurrContent = 0 - if !c.Opts.Quiet { - bar = pb.New(c.Ncontents) - bar.ShowTimeLeft = false - bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.CurrFile, c.Nfiles)) - bar.Start() + if c.OnStart != nil { + c.OnStart() } - for index, img := range images { - c.CurrContent++ - if !c.Opts.Quiet { - bar.Increment() - } + eg, ctx := errgroup.WithContext(context.Background()) + eg.SetLimit(runtime.NumCPU() + 1) - f, err := os.Open(img) + for index, img := range contents { + file, err := os.Open(img) if err != nil { - fmt.Fprintf(os.Stderr, "Error Open: %v\n", err.Error()) - continue + return fmt.Errorf("convertDirectory: %w", err) } - i, err := c.decodeImage(f, img) - if err != nil { - fmt.Fprintf(os.Stderr, "Error Decode: %v\n", err.Error()) + if c.isNonImage(img) && !c.Opts.NoNonImage { + err = c.copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))) + if err != nil { + return fmt.Errorf("convertArchive: %w", err) + } + + err = file.Close() + if err != nil { + return fmt.Errorf("convertDirectory: %w", err) + } + continue - } + } else if c.isImage(img) { - if !c.Opts.RGB && !c.isGrayScale(i) { - i = c.TransformImage(i) - c.encodeImage(i, filepath.Join(c.Workdir, filepath.Base(img))) - continue - } + i, err := c.decodeImage(file, img) + if err != nil { + return fmt.Errorf("convertDirectory: %w", err) + } - f.Close() + if c.Opts.NoRGB && !c.isGrayScale(i) { + i = c.transformImage(i) + err = c.encodeImage(i, filepath.Join(c.Workdir, filepath.Base(img))) + if err != nil { + return fmt.Errorf("convertDirectory: %w", err) + } - if i != nil { - throttle <- 1 - wg.Add(1) - go c.convertImage(i, index, img) + continue + } + + err = file.Close() + if err != nil { + return fmt.Errorf("convertDirectory: %w", err) + } + + if i != nil { + eg.Go(func() error { + return c.convertImage(ctx, i, index, img) + }) + } } } - wg.Wait() + + return eg.Wait() } -// Saves workdir to CBZ archive -func (c *Convertor) saveArchive(file string) { - defer os.RemoveAll(c.Workdir) +// 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)) - zipname := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%s%s.cbz", c.getBasename(file), c.Opts.Suffix)) zipfile, err := os.Create(zipname) if err != nil { - fmt.Fprintf(os.Stderr, "Error Create: %v\n", err.Error()) - return + return fmt.Errorf("saveArchive: %w", err) } - defer zipfile.Close() z := zip.NewWriter(zipfile) - files, _ := ioutil.ReadDir(c.Workdir) - ncontents := len(files) - - if !c.Opts.Quiet { - bar = pb.New(ncontents) - bar.ShowTimeLeft = false - bar.Prefix(fmt.Sprintf("Compressing %d of %d: ", c.CurrFile, c.Nfiles)) - bar.Start() + files, err := os.ReadDir(c.Workdir) + if err != nil { + return fmt.Errorf("saveArchive: %w", err) } for _, file := range files { - if !c.Opts.Quiet { - bar.Increment() + r, err := os.ReadFile(filepath.Join(c.Workdir, file.Name())) + if err != nil { + return fmt.Errorf("saveArchive: %w", err) } - r, err := ioutil.ReadFile(filepath.Join(c.Workdir, file.Name())) + info, err := file.Info() if err != nil { - fmt.Fprintf(os.Stderr, "Error ReadFile: %v\n", err.Error()) - continue + return fmt.Errorf("saveArchive: %w", err) } - w, err := z.Create(file.Name()) + zipinfo, err := zip.FileInfoHeader(info) if err != nil { - fmt.Fprintf(os.Stderr, "Error Create: %v\n", err.Error()) - continue + 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) } - w.Write(r) } - z.Close() -} -// Decodes image from reader -func (c *Convertor) decodeImage(reader io.Reader, filename string) (i image.Image, err error) { - defer func() { - if r := recover(); r != nil { - fmt.Fprintf(os.Stderr, "Recovered in decodeImage %s: %v\n", filename, r) - } - }() - - i, _, err = image.Decode(reader) - return i, err -} - -// Encode image to file -func (c *Convertor) encodeImage(i image.Image, filename string) (err error) { - f, err := os.Create(filename) + err = z.Close() if err != nil { - return + return fmt.Errorf("saveArchive: %w", err) } - switch filepath.Ext(filename) { - case ".png": - err = png.Encode(f, i) - case ".tif": - case ".tiff": - err = tiff.Encode(f, i, &tiff.Options{tiff.Uncompressed, false}) - case ".gif": - err = gif.Encode(f, i, nil) - default: - err = jpeg.Encode(f, i, &jpeg.Options{c.Opts.Quality}) + err = zipfile.Close() + if err != nil { + return fmt.Errorf("saveArchive: %w", err) + } + + return os.RemoveAll(c.Workdir) +} + +// decodeImage decodes image from reader. +func (c *Convertor) decodeImage(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) } - f.Close() return } -// Encode image to file (ImageMagick) -func (c *Convertor) encodeImageMagick(i image.Image, filename string) (err error) { +// encodeImage encodes image to file. +func (c *Convertor) encodeImage(img image.Image, fileName string) error { + file, err := os.Create(fileName) + if err != nil { + return fmt.Errorf("encodeImage: %w", err) + } + defer file.Close() + + if c.Opts.Grayscale { + img = imageToGray(img) + } + + switch filepath.Ext(fileName) { + case ".png": + err = png.Encode(file, img) + case ".tif", ".tiff": + err = tiff.Encode(file, img, &tiff.Options{Compression: tiff.Uncompressed}) + case ".jpg", ".jpeg": + 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)}) + } + if err != nil { + return fmt.Errorf("encodeImage: %w", err) + } + + return nil +} + +// encodeIM encodes image to file (ImageMagick). +func (c *Convertor) encodeIM(i image.Image, fileName string) error { imagick.Initialize() mw := imagick.NewMagickWand() defer mw.Destroy() - err = mw.ReadImageBlob(c.GetImageBytes(i)) + rgba := imageToRGBA(i) + err := mw.ConstituteImage(uint(i.Bounds().Dx()), uint(i.Bounds().Dy()), "RGBA", imagick.PIXEL_CHAR, rgba.Pix) if err != nil { - fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error()) - return + return fmt.Errorf("encodeIM: %w", err) } if c.Opts.Grayscale { - c := mw.GetImageColors() - mw.QuantizeImage(c, imagick.COLORSPACE_GRAY, 8, true, true) + _ = mw.TransformImageColorspace(imagick.COLORSPACE_GRAY) } - switch filepath.Ext(filename) { + switch filepath.Ext(fileName) { case ".png": - mw.SetImageFormat("PNG") - mw.WriteImage(filename) - case ".tif": - case ".tiff": - mw.SetImageFormat("TIFF") - mw.WriteImage(filename) - case ".gif": - mw.SetImageFormat("GIF") - mw.WriteImage(filename) + _ = mw.SetImageFormat("PNG") + _ = mw.WriteImage(fileName) + case ".tif", ".tiff": + _ = mw.SetImageFormat("TIFF") + _ = mw.WriteImage(fileName) case ".bmp": - w := imagick.NewPixelWand() - w.SetColor("black") - defer w.Destroy() + pw := imagick.NewPixelWand() + pw.SetColor("black") + defer pw.Destroy() - cs := mw.GetImageColorspace() - if c.Opts.Grayscale { - cs = imagick.COLORSPACE_GRAY - } - - mw.SetImageFormat("BMP3") - mw.SetImageBackgroundColor(w) - mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_REMOVE) - mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_DEACTIVATE) - mw.SetImageMatte(false) - mw.SetImageCompression(imagick.COMPRESSION_NO) - mw.QuantizeImage(16, cs, 8, true, true) - mw.WriteImage(filename) - default: - mw.SetImageFormat("JPEG") - mw.WriteImage(filename) + _ = mw.SetImageFormat("BMP3") + _ = mw.SetImageBackgroundColor(pw) + _ = mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_REMOVE) + _ = mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_DEACTIVATE) + _ = mw.SetImageMatte(false) + _ = mw.SetImageCompression(imagick.COMPRESSION_NO) + _ = mw.QuantizeImage(16, mw.GetImageColorspace(), 1, imagick.DITHER_METHOD_FLOYD_STEINBERG, true) + _ = mw.WriteImage(fileName) + case ".jpg", ".jpeg": + _ = mw.SetImageFormat("JPEG") + _ = mw.WriteImage(fileName) } - return + return nil } -// Lists contents of archive -func (c *Convertor) listArchive(file string) []string { +// listArchive lists contents of archive. +func (c *Convertor) listArchive(fileName string) ([]string, error) { var contents []string - archive, err := unarr.NewArchive(file) + + archive, err := unarr.NewArchive(fileName) if err != nil { - return contents + return contents, err } defer archive.Close() - for { - err := archive.Entry() - if err != nil { - if err == io.EOF { - break - } else { - continue - } - } - - pathname := archive.Name() - contents = append(contents, pathname) - } - - return contents + return archive.List() } -// Extracts cover from archive -func (c *Convertor) coverArchive(file string) (image.Image, error) { +// coverArchive extracts coverName from archive. +func (c *Convertor) coverArchive(fileName string) (image.Image, error) { var images []string - contents := c.listArchive(file) + contents, err := c.listArchive(fileName) + if err != nil { + return nil, fmt.Errorf("coverArchive: %w", err) + } + for _, ct := range contents { if c.isImage(ct) { images = append(images, ct) } } - cover := c.getCover(images) + cover := c.coverName(images) - archive, err := unarr.NewArchive(file) + archive, err := unarr.NewArchive(fileName) if err != nil { return nil, err } @@ -645,92 +689,290 @@ func (c *Convertor) coverArchive(file string) (image.Image, error) { return nil, err } - size := archive.Size() - buf := make([]byte, size) - for size > 0 { - n, err := archive.Read(buf) - if err != nil && err != io.EOF { - break - } - size -= n - } - - if size > 0 { - return nil, errors.New("Error Read") - } - - img, err := c.decodeImage(bytes.NewReader(buf), cover) + data, err := archive.ReadAll() if err != nil { - return nil, err + return nil, fmt.Errorf("coverArchive: %w", err) + } + + img, err := c.decodeImage(bytes.NewReader(data), cover) + if err != nil { + return nil, fmt.Errorf("coverArchive: %w", err) } return img, nil } -// Extracts cover from document -func (c *Convertor) coverDocument(file string) (image.Image, error) { - doc, err := fitz.NewDocument(file) +// coverDocument extracts coverName from document. +func (c *Convertor) coverDocument(fileName string) (image.Image, error) { + doc, err := fitz.New(fileName) if err != nil { - return nil, err + return nil, fmt.Errorf("coverDocument: %w", err) } + defer doc.Close() + img, err := doc.Image(0) if err != nil { - return nil, err - } - - if img == nil { - return nil, errors.New("Image is nil") + return nil, fmt.Errorf("coverDocument: %w", err) } return img, nil } -// Extracts cover from directory +// coverDirectory extracts coverName from directory. func (c *Convertor) coverDirectory(dir string) (image.Image, error) { - images := c.getImagesFromPath(dir) - cover := c.getCover(images) - - p, err := os.Open(cover) + contents, err := c.imagesFromPath(dir) if err != nil { - return nil, err - } - defer p.Close() - - img, err := c.decodeImage(p, cover) - if err != nil { - fmt.Fprintf(os.Stderr, "Error Decode: %v\n", err.Error()) - return nil, err + return nil, fmt.Errorf("coverDirectory: %w", err) } - if img == nil { - return nil, errors.New("Image is nil") + images := c.imagesFromSlice(contents) + cover := c.coverName(images) + + file, err := os.Open(cover) + if err != nil { + return nil, fmt.Errorf("coverDirectory: %w", err) + } + defer file.Close() + + img, err := c.decodeImage(file, cover) + if err != nil { + return nil, fmt.Errorf("coverDirectory: %w", err) } return img, nil } -// Returns list of found comic files -func (c *Convertor) GetFiles(args []string) []string { +// imagesFromPath returns list of found image files for given directory. +func (c *Convertor) imagesFromPath(path string) ([]string, error) { + var images []string + + walkFiles := func(fp string, f os.FileInfo, err error) error { + if !f.IsDir() && f.Mode()&os.ModeType == 0 { + if f.Size() > 0 && (c.isImage(fp) || c.isNonImage(fp)) { + images = append(images, fp) + } + } + return nil + } + + f, err := filepath.Abs(path) + if err != nil { + return images, fmt.Errorf("imagesFromPath: %w", err) + } + + stat, err := os.Stat(f) + if err != nil { + return images, fmt.Errorf("imagesFromPath: %w", err) + } + + if !stat.IsDir() && stat.Mode()&os.ModeType == 0 { + if c.isImage(f) { + images = append(images, f) + } + } else { + err = filepath.Walk(f, walkFiles) + if err != nil { + return images, fmt.Errorf("imagesFromPath: %w", err) + } + } + + return images, nil +} + +// imagesFromSlice returns list of found image files for given slice of files. +func (c *Convertor) imagesFromSlice(files []string) []string { + var images []string + + for _, f := range files { + if c.isImage(f) { + images = append(images, f) + } + } + + return images +} + +// imageToRGBA converts an image.Image to *image.RGBA. +func imageToRGBA(src image.Image) *image.RGBA { + if dst, ok := src.(*image.RGBA); ok { + return dst + } + + b := src.Bounds() + dst := image.NewRGBA(b) + draw.Draw(dst, dst.Bounds(), src, b.Min, draw.Src) + return dst +} + +// imageToGray converts an image.Image to *image.Gray. +func imageToGray(src image.Image) *image.Gray { + if dst, ok := src.(*image.Gray); ok { + return dst + } + + b := src.Bounds() + dst := image.NewGray(b) + draw.Draw(dst, dst.Bounds(), src, b.Min, draw.Src) + return dst +} + +// isArchive checks if file is archive. +func (c *Convertor) isArchive(f string) bool { + var types = []string{".rar", ".zip", ".7z", ".tar", ".cbr", ".cbz", ".cb7", ".cbt"} + for _, t := range types { + if strings.ToLower(filepath.Ext(f)) == t { + return true + } + } + return false +} + +// isDocument checks if file is document. +func (c *Convertor) isDocument(f string) bool { + var types = []string{".pdf", ".epub"} + for _, t := range types { + if strings.ToLower(filepath.Ext(f)) == t { + return true + } + } + return false +} + +// isImage checks if file is image. +func (c *Convertor) isImage(f string) bool { + var types = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".webp"} + for _, t := range types { + if strings.ToLower(filepath.Ext(f)) == t { + return true + } + } + return false +} + +// isNonImage checks for allowed files in archive. +func (c *Convertor) isNonImage(f string) bool { + var types = []string{".nfo", ".xml"} + for _, t := range types { + if strings.ToLower(filepath.Ext(f)) == t { + return true + } + } + return false +} + +// isSize checks size of file. +func (c *Convertor) isSize(size int64) bool { + if c.Opts.Size > 0 { + if size < c.Opts.Size*(1024*1024) { + return false + } + } + return true +} + +// isGrayScale checks if image is grayscale. +func (c *Convertor) isGrayScale(img image.Image) bool { + model := img.ColorModel() + if model == color.GrayModel || model == color.Gray16Model { + return true + } + return false +} + +// baseNoExt returns base name without extension. +func (c *Convertor) baseNoExt(filename string) string { + return strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename)) +} + +// copyFile copies reader to file. +func (c *Convertor) copyFile(reader io.Reader, filename string) error { + err := os.MkdirAll(filepath.Dir(filename), 0755) + if err != nil { + return fmt.Errorf("copyFile: %w", err) + } + + file, err := os.Create(filename) + if err != nil { + return fmt.Errorf("copyFile: %w", err) + } + defer file.Close() + + _, err = io.Copy(file, reader) + if err != nil { + return fmt.Errorf("copyFile: %w", err) + } + + return nil +} + +// coverName returns the filename that is the most likely to be the coverName. +func (c *Convertor) coverName(images []string) string { + if len(images) == 0 { + return "" + } + + for _, i := range images { + e := c.baseNoExt(i) + if strings.HasPrefix(i, "coverName") || strings.HasPrefix(i, "front") || + strings.HasSuffix(e, "coverName") || strings.HasSuffix(e, "front") { + return i + } + } + + sort.Sort(sortorder.Natural(images)) + return images[0] +} + +// coverImage returns coverName 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 +} + +// Files returns list of found comic files. +func (c *Convertor) Files(args []string) ([]string, error) { var files []string walkFiles := func(fp string, f os.FileInfo, err error) error { - if !f.IsDir() { - if c.isArchive(fp) || c.isDocument(fp) { - if c.isSize(f.Size()) { - files = append(files, fp) - } + if f.IsDir() { + return nil + } + if c.isArchive(fp) || c.isDocument(fp) { + if c.isSize(f.Size()) { + files = append(files, fp) } } return nil } for _, arg := range args { - path, _ := filepath.Abs(arg) + path, err := filepath.Abs(arg) + if err != nil { + return files, fmt.Errorf("files: %w", err) + } + stat, err := os.Stat(path) if err != nil { - fmt.Fprintf(os.Stderr, "Error Stat GetFiles: %v\n", err.Error()) - continue + return files, fmt.Errorf("files: %w", err) } if !stat.IsDir() { @@ -741,12 +983,23 @@ func (c *Convertor) GetFiles(args []string) []string { } } else { if c.Opts.Recursive { - filepath.Walk(path, walkFiles) + err = filepath.Walk(path, walkFiles) + if err != nil { + return files, fmt.Errorf("files: %w", err) + } } else { - fs, _ := ioutil.ReadDir(path) + fs, err := os.ReadDir(path) + if err != nil { + return files, fmt.Errorf("files: %w", err) + } + for _, f := range fs { if c.isArchive(f.Name()) || c.isDocument(f.Name()) { - if c.isSize(f.Size()) { + info, err := f.Info() + if err != nil { + return files, fmt.Errorf("files: %w", err) + } + if c.isSize(info.Size()) { files = append(files, filepath.Join(path, f.Name())) } } @@ -761,185 +1014,16 @@ func (c *Convertor) GetFiles(args []string) []string { } c.Nfiles = len(files) - return files + return files, nil } -// Returns list of found image files for given directory -func (c *Convertor) getImagesFromPath(path string) []string { - var images []string +// ExtractCover extracts coverName. +func (c *Convertor) ExtractCover(fileName string, fileInfo os.FileInfo) error { + c.CurrFile++ - walkFiles := func(fp string, f os.FileInfo, err error) error { - if !f.IsDir() && f.Mode()&os.ModeType == 0 { - if f.Size() > 0 && c.isImage(fp) { - images = append(images, fp) - } - } - return nil - } - - f, _ := filepath.Abs(path) - stat, err := os.Stat(f) + cover, err := c.coverImage(fileName, fileInfo) if err != nil { - fmt.Fprintf(os.Stderr, "Error Stat getImagesFromPath: %v\n", err.Error()) - return images - } - - if !stat.IsDir() && stat.Mode()&os.ModeType == 0 { - if c.isImage(f) { - images = append(images, f) - } - } else { - filepath.Walk(f, walkFiles) - } - - return images -} - -// Returns list of found image files for given slice of files -func (c *Convertor) getImagesFromSlice(files []string) []string { - var images []string - - for _, f := range files { - if c.isImage(f) { - images = append(images, f) - } - } - - return images -} - -// Returns image bytes/blob to be used with ImageMagick -func (c *Convertor) GetImageBytes(i image.Image) []byte { - b := new(bytes.Buffer) - jpeg.Encode(b, i, &jpeg.Options{c.Opts.Quality}) - return b.Bytes() -} - -// Returns the filename that is the most likely to be the cover -func (c *Convertor) getCover(images []string) string { - if len(images) == 0 { - return "" - } - - for _, i := range images { - e := c.getBasename(i) - if strings.HasPrefix(i, "cover") || strings.HasPrefix(i, "front") || - strings.HasSuffix(e, "cover") || strings.HasSuffix(e, "front") { - return i - } - } - - sort.Sort(naturalsort.NaturalSort(images)) - return images[0] -} - -// Checks if file is archive -func (c *Convertor) isArchive(f string) bool { - var types = []string{".rar", ".zip", ".7z", ".gz", - ".bz2", ".cbr", ".cbz", ".cb7", ".cbt"} - for _, t := range types { - if strings.ToLower(filepath.Ext(f)) == t { - return true - } - } - return false -} - -// Checks if file is document -func (c *Convertor) isDocument(f string) bool { - var types = []string{".pdf", ".epub", ".xps"} - for _, t := range types { - if strings.ToLower(filepath.Ext(f)) == t { - return true - } - } - return false -} - -// Checks if file is image -func (c *Convertor) isImage(f string) bool { - var types = []string{".jpg", ".jpeg", ".jpe", ".png", - ".gif", ".bmp", ".tiff", ".tif", ".webp"} - for _, t := range types { - if strings.ToLower(filepath.Ext(f)) == t { - return true - } - } - return false -} - -// Checks size of file -func (c *Convertor) isSize(size int64) bool { - if c.Opts.Size > 0 { - if size < c.Opts.Size*(1024*1024) { - return false - } - } - return true -} - -// Checks if image is grayscale -func (c *Convertor) isGrayScale(img image.Image) bool { - model := img.ColorModel() - if model == color.GrayModel || model == color.Gray16Model { - return true - } - return false -} - -// Copies reader to file -func (c *Convertor) copyFile(reader io.Reader, filename string) error { - os.MkdirAll(filepath.Dir(filename), 0755) - - file, err := os.Create(filename) - if err != nil { - return err - } - defer file.Close() - - _, err = io.Copy(file, reader) - if err != nil { - return err - } - - return nil -} - -// Returns basename without extension -func (c *Convertor) getBasename(file string) string { - basename := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) - basename = strings.TrimSuffix(basename, ".tar") - return basename -} - -// Returns cover image.Image -func (c *Convertor) GetCoverImage(file string, info os.FileInfo) (image.Image, error) { - var err error - var cover image.Image - - if info.IsDir() { - cover, err = c.coverDirectory(file) - } else if c.isDocument(file) { - cover, err = c.coverDocument(file) - } else { - cover, err = c.coverArchive(file) - } - - if err != nil { - return nil, err - } - - return cover, nil -} - -// Extracts cover -func (c *Convertor) ExtractCover(file string, info os.FileInfo) { - c.CurrFile += 1 - - cover, err := c.GetCoverImage(file, info) - if err != nil { - fmt.Fprintf(os.Stderr, "Error GetCoverImage: %v\n", err.Error()) - return + return fmt.Errorf("extractCover: %w", err) } if c.Opts.Width > 0 || c.Opts.Height > 0 { @@ -950,30 +1034,32 @@ func (c *Convertor) ExtractCover(file string, info os.FileInfo) { } } - filename := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%s.jpg", c.getBasename(file))) - f, err := os.Create(filename) + fname := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%s.jpg", c.baseNoExt(fileName))) + file, err := os.Create(fname) if err != nil { - fmt.Fprintf(os.Stderr, "Error Create: %v\n", err.Error()) - return + return fmt.Errorf("extractCover: %w", err) } - defer f.Close() - jpeg.Encode(f, cover, &jpeg.Options{c.Opts.Quality}) + err = jpeg.Encode(file, cover, &jpeg.Options{Quality: c.Opts.Quality}) + if err != nil { + return fmt.Errorf("extractCover: %w", err) + } + + err = file.Close() + if err != nil { + return fmt.Errorf("extractCover: %w", err) + } + + return nil } -// Extracts thumbnail -func (c *Convertor) ExtractThumbnail(file string, info os.FileInfo) { - c.CurrFile += 1 +// ExtractThumbnail extracts thumbnail. +func (c *Convertor) ExtractThumbnail(filename string, info os.FileInfo) error { + c.CurrFile++ - cover, err := c.GetCoverImage(file, info) + cover, err := c.coverImage(filename, info) if err != nil { - fmt.Fprintf(os.Stderr, "Error GetCoverImage: %v\n", err.Error()) - return - } - - if err != nil { - fmt.Fprintf(os.Stderr, "Error Thumbnail: %v\n", err.Error()) - return + return err } if c.Opts.Width > 0 || c.Opts.Height > 0 { @@ -991,48 +1077,68 @@ func (c *Convertor) ExtractThumbnail(file string, info os.FileInfo) { mw := imagick.NewMagickWand() defer mw.Destroy() - b := new(bytes.Buffer) - png.Encode(b, cover) - - err = mw.ReadImageBlob(b.Bytes()) + rgba := imageToRGBA(cover) + err = mw.ConstituteImage(uint(cover.Bounds().Dx()), uint(cover.Bounds().Dy()), "RGBA", imagick.PIXEL_CHAR, rgba.Pix) if err != nil { - fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error()) + return fmt.Errorf("extractThumbnail: %w", err) } - var fileuri string - var filename string + var fname string + var furi string if c.Opts.Outfile == "" { - fileuri = "file://" + file - filename = filepath.Join(c.Opts.Outdir, fmt.Sprintf("%x.png", md5.Sum([]byte(fileuri)))) + furi = "file://" + filename + fname = filepath.Join(c.Opts.Outdir, fmt.Sprintf("%x.png", md5.Sum([]byte(furi)))) } else { abs, _ := filepath.Abs(c.Opts.Outfile) - fileuri = "file://" + abs - filename = abs + furi = "file://" + abs + fname = abs } - mw.SetImageFormat("PNG") - mw.SetImageProperty("Software", "CBconvert") - mw.SetImageProperty("Description", "Thumbnail of "+fileuri) - mw.SetImageProperty("Thumb::URI", fileuri) - mw.SetImageProperty("Thumb::MTime", strconv.FormatInt(info.ModTime().Unix(), 10)) - mw.SetImageProperty("Thumb::Size", strconv.FormatInt(info.Size(), 10)) - mw.SetImageProperty("Thumb::Mimetype", mime.TypeByExtension(filepath.Ext(file))) + _ = mw.SetImageFormat("PNG") + _ = mw.SetImageProperty("Software", "CBconvert") + _ = mw.SetImageProperty("Description", "Thumbnail of "+furi) + _ = mw.SetImageProperty("Thumb::URI", furi) + _ = mw.SetImageProperty("Thumb::MTime", strconv.FormatInt(info.ModTime().Unix(), 10)) + _ = mw.SetImageProperty("Thumb::Size", strconv.FormatInt(info.Size(), 10)) + _ = mw.SetImageProperty("Thumb::Mimetype", mime.TypeByExtension(filepath.Ext(filename))) - mw.WriteImage(filename) + _ = mw.WriteImage(fname) + return nil } -// Converts comic book -func (c *Convertor) ConvertComic(file string, info os.FileInfo) { - c.CurrFile += 1 +// Convert converts comic book. +func (c *Convertor) Convert(filename string, info os.FileInfo) error { + c.CurrFile++ + if info.IsDir() { - c.convertDirectory(file) - c.saveArchive(file) - } else if c.isDocument(file) { - c.convertDocument(file) - c.saveArchive(file) + err := c.convertDirectory(filename) + if err != nil { + return err + } + err = c.saveArchive(filename) + if err != nil { + return err + } + } else if c.isDocument(filename) { + err := c.convertDocument(filename) + if err != nil { + return err + } + err = c.saveArchive(filename) + if err != nil { + return err + } } else { - c.convertArchive(file) - c.saveArchive(file) + err := c.convertArchive(filename) + if err != nil { + return err + } + err = c.saveArchive(filename) + if err != nil { + return err + } } + + return nil } diff --git a/cmd/cbconvert/go.mod b/cmd/cbconvert/go.mod new file mode 100644 index 0000000..20ca1c4 --- /dev/null +++ b/cmd/cbconvert/go.mod @@ -0,0 +1,28 @@ +module cbconvert + +go 1.19 + +replace github.com/gen2brain/cbconvert => ../../ + +require ( + github.com/gen2brain/cbconvert v0.0.0-20170124143008-5df10a58ee74 + github.com/schollz/progressbar/v3 v3.10.0 + github.com/spf13/pflag v1.0.5 +) + +require ( + github.com/chai2010/webp v1.1.1 // indirect + github.com/disintegration/imaging v1.6.2 // indirect + github.com/fvbommel/sortorder v1.0.2 // indirect + github.com/gen2brain/go-fitz v1.20.1 // indirect + github.com/gen2brain/go-unarr v0.1.6 // indirect + github.com/hotei/bmp v0.0.0-20150430041436-f620cebab0c7 // indirect + 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 + 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 + golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect + gopkg.in/gographics/imagick.v3 v3.4.1 // indirect +) diff --git a/cmd/cbconvert/go.sum b/cmd/cbconvert/go.sum new file mode 100644 index 0000000..a7ff35b --- /dev/null +++ b/cmd/cbconvert/go.sum @@ -0,0 +1,49 @@ +github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= +github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo= +github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/gen2brain/go-fitz v1.20.1 h1:i5GPe/58q/gbNqa2mO+ZcTymwowbJEsDOXp7D0JwxgU= +github.com/gen2brain/go-fitz v1.20.1/go.mod h1:UZAxMETTDK4UPpuh80HaRpPzgkSibUihXVzwj2ip5oQ= +github.com/gen2brain/go-unarr v0.1.6 h1:2TtfIQ2dGuCkgEYa+vPE1ydcpkB3CtBbdYMfRSGLdA8= +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/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= +github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/schollz/progressbar/v3 v3.10.0 h1:pOab0roS2jf6zkEBKAe9EyEdmRKJvhbbuxqVp9/Qjyw= +github.com/schollz/progressbar/v3 v3.10.0/go.mod h1:0N6zRwbDVLFCFy5chxuukVlRkoHWYFFLzlxQrw/sf3M= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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= +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= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/gographics/imagick.v3 v3.4.1 h1:CClNBnd1UGxH9KAl4Vuwx+jgNRkyKN+cHlbuFPyt+KU= +gopkg.in/gographics/imagick.v3 v3.4.1/go.mod h1:+Q9nyA2xRZXrDyTtJ/eko+8V/5E7bWYs08ndkZp8UmA= diff --git a/cmd/cbconvert/main.go b/cmd/cbconvert/main.go new file mode 100644 index 0000000..ac735e5 --- /dev/null +++ b/cmd/cbconvert/main.go @@ -0,0 +1,222 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "path/filepath" + "syscall" + + "github.com/gen2brain/cbconvert" + "github.com/schollz/progressbar/v3" + flag "github.com/spf13/pflag" +) + +func main() { + opts, args := parseFlags() + conv := cbconvert.New(opts) + + c := make(chan os.Signal, 2) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + for range c { + fmt.Println("\naborting") + err := os.RemoveAll(conv.Workdir) + if err != nil { + fmt.Println(err) + } + os.Exit(1) + } + }() + + if _, err := os.Stat(opts.Outdir); err != nil { + err = os.MkdirAll(opts.Outdir, 0775) + if err != nil { + fmt.Println(err) + } + os.Exit(1) + } + + files, err := conv.Files(args) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + var bar *progressbar.ProgressBar + if opts.Cover || opts.Thumbnail { + if !opts.Quiet { + bar = progressbar.NewOptions(conv.Nfiles, + progressbar.OptionShowCount(), + progressbar.OptionClearOnFinish(), + progressbar.OptionUseANSICodes(true), + progressbar.OptionSetPredictTime(false), + ) + } + } + + conv.OnStart = func() { + if !opts.Quiet { + bar = progressbar.NewOptions(conv.Ncontents, + progressbar.OptionShowCount(), + progressbar.OptionClearOnFinish(), + progressbar.OptionUseANSICodes(true), + progressbar.OptionSetDescription(fmt.Sprintf("Converting %d of %d:", conv.CurrFile, conv.Nfiles)), + progressbar.OptionSetPredictTime(false), + ) + } + } + + conv.OnProgress = func() { + if !opts.Quiet { + _ = bar.Add(1) + } + } + + conv.OnCompress = func() { + if !opts.Quiet { + _, _ = fmt.Fprintf(os.Stderr, "Compressing %d of %d...\r", conv.CurrFile, conv.Nfiles) + } + } + + for _, file := range files { + stat, err := os.Stat(file) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if opts.Cover { + err = conv.ExtractCover(file, stat) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } else if opts.Thumbnail { + err = conv.ExtractThumbnail(file, stat) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + + err = conv.Convert(file, stat) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } +} + +// parseFlags parses command line flags +func parseFlags() (cbconvert.Options, []string) { + opts := cbconvert.Options{} + var args []string + + convert := flag.NewFlagSet("convert", flag.ExitOnError) + convert.SortFlags = false + 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.IntVar(&opts.Quality, "quality", 75, "Image quality") + 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") + convert.BoolVar(&opts.NoNonImage, "no-nonimage", false, "Remove non-image files from the archive") + convert.BoolVar(&opts.NoConvert, "no-convert", false, "Do not transform or convert images") + convert.BoolVar(&opts.Grayscale, "grayscale", false, "Convert images to grayscale (monochromatic)") + convert.IntVar(&opts.Rotate, "rotate", 0, "Rotate images, valid values are 0, 90, 180, 270") + convert.StringVar(&opts.Flip, "flip", "none", "Flip images, valid values are none, horizontal, vertical") + convert.Float64Var(&opts.Brightness, "brightness", 0, "Adjust the brightness of the images, must be in the range (-100, 100)") + convert.Float64Var(&opts.Contrast, "contrast", 0, "Adjust the contrast of the images, must be in the range (-100, 100)") + convert.StringVar(&opts.Suffix, "suffix", "", "Add suffix to file basename") + convert.Float64Var(&opts.LevelsInMin, "levels-inmin", 0, "Shadow input value") + convert.Float64Var(&opts.LevelsGamma, "levels-gamma", 1.0, "Midpoint/Gamma") + convert.Float64Var(&opts.LevelsInMax, "levels-inmax", 255, "Highlight input value") + convert.Float64Var(&opts.LevelsOutMin, "levels-outmin", 0, "Shadow output value") + convert.Float64Var(&opts.LevelsOutMax, "levels-outmax", 255, "Highlight output value") + convert.StringVar(&opts.Outdir, "outdir", ".", "Output directory") + convert.Int64Var(&opts.Size, "size", 0, "Process only files larger than size (in MB)") + convert.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively") + convert.BoolVar(&opts.Quiet, "quiet", false, "Hide console output") + + cover := flag.NewFlagSet("cover", flag.ExitOnError) + cover.SortFlags = false + cover.IntVar(&opts.Width, "width", 0, "Image width") + cover.IntVar(&opts.Height, "height", 0, "Image height") + cover.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height") + cover.IntVar(&opts.Quality, "quality", 75, "Image quality") + cover.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos") + cover.StringVar(&opts.Outdir, "outdir", ".", "Output directory") + cover.Int64Var(&opts.Size, "size", 0, "Process only files larger than size (in MB)") + cover.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively") + cover.BoolVar(&opts.Quiet, "quiet", false, "Hide console output") + + thumbnail := flag.NewFlagSet("thumbnail", flag.ExitOnError) + thumbnail.SortFlags = false + thumbnail.IntVar(&opts.Width, "width", 0, "Image width") + thumbnail.IntVar(&opts.Height, "height", 0, "Image height") + thumbnail.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height") + thumbnail.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos") + thumbnail.StringVar(&opts.Outdir, "outdir", ".", "Output directory") + thumbnail.StringVar(&opts.Outfile, "outfile", "", "Output file") + thumbnail.Int64Var(&opts.Size, "size", 0, "Process only files larger than size (in MB)") + thumbnail.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively") + thumbnail.BoolVar(&opts.Quiet, "quiet", false, "Hide console output") + + convert.Usage = func() { + _, _ = fmt.Fprintf(os.Stderr, "Usage: %s [] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0])) + _, _ = fmt.Fprintf(os.Stderr, "\nCommands:\n") + _, _ = fmt.Fprintf(os.Stderr, "\n convert*\n \tConvert archive or document (default command)\n\n") + convert.VisitAll(func(f *flag.Flag) { + _, _ = fmt.Fprintf(os.Stderr, " --%s", f.Name) + _, _ = fmt.Fprintf(os.Stderr, "\n \t") + _, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) + }) + _, _ = fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n") + cover.VisitAll(func(f *flag.Flag) { + _, _ = fmt.Fprintf(os.Stderr, " --%s", f.Name) + _, _ = fmt.Fprintf(os.Stderr, "\n \t") + _, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) + }) + _, _ = fmt.Fprintf(os.Stderr, "\n thumbnail\n \tExtract cover thumbnail (freedesktop spec.)\n\n") + thumbnail.VisitAll(func(f *flag.Flag) { + _, _ = fmt.Fprintf(os.Stderr, " --%s", f.Name) + _, _ = fmt.Fprintf(os.Stderr, "\n \t") + _, _ = fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) + }) + _, _ = fmt.Fprintf(os.Stderr, "\n") + } + + if len(os.Args) < 2 { + convert.Usage() + _, _ = fmt.Fprintf(os.Stderr, "no arguments\n") + os.Exit(1) + } + + switch os.Args[1] { + case "convert": + _ = convert.Parse(os.Args[2:]) + args = convert.Args() + case "cover": + opts.Cover = true + _ = cover.Parse(os.Args[2:]) + args = cover.Args() + case "thumbnail": + opts.Thumbnail = true + _ = thumbnail.Parse(os.Args[2:]) + args = thumbnail.Args() + default: + _ = convert.Parse(os.Args[1:]) + args = convert.Args() + } + + if len(args) == 0 { + convert.Usage() + _, _ = fmt.Fprintf(os.Stderr, "no arguments\n") + os.Exit(1) + } + + return opts, args +} diff --git a/cmd/cbconvert/make.bash b/cmd/cbconvert/make.bash new file mode 100755 index 0000000..9434595 --- /dev/null +++ b/cmd/cbconvert/make.bash @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +MUSL="/usr/x86_64-pc-linux-musl" +MINGW="/usr/i686-w64-mingw32" + +VERSION="`git --git-dir ../../.git describe --tags --abbrev=0 >/dev/null 2>&1 || echo '0.0.0'`" + +BUILDDIR="cbconvert-${VERSION}" +mkdir -p ${BUILDDIR} + +CC=x86_64-pc-linux-musl-gcc \ +PKG_CONFIG="x86_64-pc-linux-musl-pkg-config" \ +PKG_CONFIG_PATH="$MUSL/usr/lib/pkgconfig" \ +PKG_CONFIG_LIBDIR="$MUSL/usr/lib/pkgconfig" \ +CGO_CFLAGS="-I$MUSL/usr/include" \ +CGO_LDFLAGS="-L$MUSL/usr/lib" \ +CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \ +go build -tags 'extlib static' -v -o ${BUILDDIR}/cbconvert -ldflags "-linkmode external -s -w '-extldflags=-static'" + +cp ../../README.md ../../AUTHORS ../../COPYING ${BUILDDIR} && tar -czf "${BUILDDIR}-linux-x86_64.tar.gz" ${BUILDDIR} +rm -rf ${BUILDDIR} + + +BUILDDIR="cbconvert-${VERSION}" +mkdir -p ${BUILDDIR} + +CC=i686-w64-mingw32-gcc \ +PKG_CONFIG="/usr/bin/i686-w64-mingw32-pkg-config" \ +PKG_CONFIG_PATH="$MINGW/usr/lib/pkgconfig" \ +PKG_CONFIG_LIBDIR="$MINGW/usr/lib/pkgconfig" \ +CGO_CFLAGS="-I$MINGW/usr/include" \ +CGO_LDFLAGS="-L$MINGW/usr/lib" \ +CGO_ENABLED=1 GOOS=windows GOARCH=386 \ +go build -tags 'extlib static' -v -o ${BUILDDIR}/cbconvert.exe -ldflags "-s -w '-extldflags=-static -Wl,--allow-multiple-definition'" + +cp ../../README.md ../../AUTHORS ../../COPYING ${BUILDDIR} && zip -rq "${BUILDDIR}-windows-i686.zip" ${BUILDDIR} +rm -rf ${BUILDDIR} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index f784f50..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,149 +0,0 @@ -// Author: Milan Nikolic -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package main - -//go:generate goversioninfo - -import ( - "fmt" - "image/jpeg" - "os" - "os/signal" - "strconv" - "syscall" - - "github.com/cheggaaa/pb" - "github.com/gen2brain/cbconvert" - "gopkg.in/alecthomas/kingpin.v2" -) - -// Parses command line flags -func parseFlags() (cbconvert.Options, []string) { - opts := cbconvert.Options{} - var args []string - - kingpin.Version("CBconvert 0.5.0") - kingpin.CommandLine.Help = "Comic Book convert tool." - kingpin.UsageTemplate(kingpin.CompactUsageTemplate) - - kingpin.Flag("outdir", "Output directory").Default(".").StringVar(&opts.Outdir) - kingpin.Flag("size", "Process only files larger then size (in MB)").Default(strconv.Itoa(0)).Int64Var(&opts.Size) - kingpin.Flag("recursive", "Process subdirectories recursively").BoolVar(&opts.Recursive) - kingpin.Flag("quiet", "Hide console output").BoolVar(&opts.Quiet) - - convert := kingpin.Command("convert", "Convert archive or document (default command)").Default() - convert.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&args) - convert.Flag("width", "Image width").Default(strconv.Itoa(0)).IntVar(&opts.Width) - convert.Flag("height", "Image height").Default(strconv.Itoa(0)).IntVar(&opts.Height) - convert.Flag("fit", "Best fit for required width and height").BoolVar(&opts.Fit) - convert.Flag("format", "Image format, valid values are jpeg, png, gif, tiff, bmp").Default("jpeg").StringVar(&opts.Format) - convert.Flag("quality", "JPEG image quality").Default(strconv.Itoa(jpeg.DefaultQuality)).IntVar(&opts.Quality) - convert.Flag("filter", "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos").Default(strconv.Itoa(cbconvert.Linear)).IntVar(&opts.Filter) - convert.Flag("cover", "Convert cover image (use --no-cover if you want to exclude cover)").Default("true").BoolVar(&opts.ConvertCover) - convert.Flag("rgb", "Convert images that have RGB colorspace (use --no-rgb if you only want to convert grayscaled images)").Default("true").BoolVar(&opts.RGB) - convert.Flag("nonimage", "Leave non image files in archive (use --no-nonimage to remove non image files from archive)").Default("true").BoolVar(&opts.NonImage) - convert.Flag("grayscale", "Convert images to grayscale (monochromatic)").BoolVar(&opts.Grayscale) - convert.Flag("rotate", "Rotate images, valid values are 0, 90, 180, 270").Default(strconv.Itoa(0)).IntVar(&opts.Rotate) - convert.Flag("flip", "Flip images, valid values are none, horizontal, vertical").Default("none").StringVar(&opts.Flip) - convert.Flag("brightness", "Adjust brightness of the images, must be in range (-100, 100)").Default(strconv.Itoa(0)).Float64Var(&opts.Brightness) - convert.Flag("contrast", "Adjust contrast of the images, must be in range (-100, 100)").Default(strconv.Itoa(0)).Float64Var(&opts.Contrast) - convert.Flag("suffix", "Add suffix to file basename").StringVar(&opts.Suffix) - convert.Flag("levels-inmin", "Shadow input value").Default(strconv.Itoa(0)).Float64Var(&opts.LevelsInMin) - convert.Flag("levels-gamma", "Midpoint/Gamma").Default(strconv.Itoa(1.00)).Float64Var(&opts.LevelsGamma) - convert.Flag("levels-inmax", "Highlight input value").Default(strconv.Itoa(255)).Float64Var(&opts.LevelsInMax) - convert.Flag("levels-outmin", "Shadow output value").Default(strconv.Itoa(0)).Float64Var(&opts.LevelsOutMin) - convert.Flag("levels-outmax", "Highlight output value").Default(strconv.Itoa(255)).Float64Var(&opts.LevelsOutMax) - - cover := kingpin.Command("cover", "Extract cover") - cover.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&args) - cover.Flag("width", "Image width").Default(strconv.Itoa(0)).IntVar(&opts.Width) - cover.Flag("height", "Image height").Default(strconv.Itoa(0)).IntVar(&opts.Height) - cover.Flag("fit", "Best fit for required width and height").BoolVar(&opts.Fit) - cover.Flag("quality", "JPEG image quality").Default(strconv.Itoa(jpeg.DefaultQuality)).IntVar(&opts.Quality) - cover.Flag("filter", "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos").Default(strconv.Itoa(cbconvert.Linear)).IntVar(&opts.Filter) - - thumbnail := kingpin.Command("thumbnail", "Extract cover thumbnail (freedesktop spec.)") - thumbnail.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&args) - thumbnail.Flag("outfile", "Output file").Default("").StringVar(&opts.Outfile) - thumbnail.Flag("width", "Image width").Default(strconv.Itoa(0)).IntVar(&opts.Width) - thumbnail.Flag("height", "Image height").Default(strconv.Itoa(0)).IntVar(&opts.Height) - thumbnail.Flag("fit", "Best fit for required width and height").BoolVar(&opts.Fit) - thumbnail.Flag("filter", "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos").Default(strconv.Itoa(cbconvert.Linear)).IntVar(&opts.Filter) - - switch kingpin.Parse() { - case "cover": - opts.Cover = true - case "thumbnail": - opts.Thumbnail = true - } - - return opts, args -} - -func main() { - opts, args := parseFlags() - conv := cbconvert.NewConvertor(opts) - - var bar *pb.ProgressBar - - c := make(chan os.Signal, 3) - signal.Notify(c, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM) - go func() { - for _ = range c { - fmt.Fprintf(os.Stderr, "Aborting\n") - os.RemoveAll(conv.Workdir) - os.Exit(1) - } - }() - - if _, err := os.Stat(opts.Outdir); err != nil { - os.MkdirAll(opts.Outdir, 0777) - } - - files := conv.GetFiles(args) - - if opts.Cover || opts.Thumbnail { - if !opts.Quiet { - bar = pb.New(conv.Nfiles) - bar.ShowTimeLeft = false - bar.Start() - } - } - - for _, file := range files { - stat, err := os.Stat(file) - if err != nil { - fmt.Fprintf(os.Stderr, "Error Stat: %v\n", err.Error()) - continue - } - - if opts.Cover { - conv.ExtractCover(file, stat) - if !opts.Quiet { - bar.Increment() - } - continue - } else if opts.Thumbnail { - conv.ExtractThumbnail(file, stat) - if !opts.Quiet { - bar.Increment() - } - continue - } - - conv.ConvertComic(file, stat) - } -} diff --git a/cmd/make.bash b/cmd/make.bash deleted file mode 100755 index 519a603..0000000 --- a/cmd/make.bash +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -CHROOT="/home/milann/chroot" -MINGW="/usr/i686-w64-mingw32" - -mkdir -p build -rm -f resource.syso - -LIBRARY_PATH="$CHROOT/usr/lib:$CHROOT/lib" \ -PKG_CONFIG_PATH="$CHROOT/usr/lib/pkgconfig" \ -PKG_CONFIG_LIBDIR="$CHROOT/usr/lib/pkgconfig" \ -CGO_LDFLAGS="-L$CHROOT/usr/lib -L$CHROOT/lib" \ -CC_FOR_TARGET="x86_64-pc-linux-gnu-gcc" CXX_FOR_TARGET="x86_64-pc-linux-gnu-g++" \ -CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -v -x -o build/cbconvert -strip build/cbconvert - -go generate -PKG_CONFIG="/usr/bin/i686-w64-mingw32-pkg-config" \ -PKG_CONFIG_PATH="$MINGW/usr/lib/pkgconfig" \ -PKG_CONFIG_LIBDIR="$MINGW/usr/lib/pkgconfig" \ -CGO_LDFLAGS="-L$MINGW/usr/lib" \ -CGO_CFLAGS="-I$MINGW/usr/include -Wno-poison-system-directories" \ -CGO_CXXFLAGS="-I$MINGW/usr/include -Wno-poison-system-directories" \ -CGO_CPPFLAGS="-I$MINGW/usr/include -Wno-poison-system-directories" \ -CC="i686-w64-mingw32-gcc" CXX="i686-w64-mingw32-g++" \ -CC_FOR_TARGET="i686-w64-mingw32-gcc" CXX_FOR_TARGET="i686-w64-mingw32-g++" \ -CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -v -x -o build/cbconvert.exe -ldflags "-linkmode external '-extldflags=-static -Wl,--allow-multiple-definition'" -i686-w64-mingw32-strip build/cbconvert.exe diff --git a/cmd/versioninfo.json b/cmd/versioninfo.json deleted file mode 100644 index 6caf6be..0000000 --- a/cmd/versioninfo.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "FixedFileInfo": - { - "FileVersion": { - "Major": 0, - "Minor": 6, - "Patch": 0, - "Build": 0 - }, - "ProductVersion": { - "Major": 0, - "Minor": 6, - "Patch": 0, - "Build": 0 - }, - "FileFlagsMask": "3f", - "FileFlags ": "00", - "FileOS": "040004", - "FileType": "01", - "FileSubType": "00" - }, - "StringFileInfo": - { - "Comments": "Comic Book converter", - "CompanyName": "", - "FileDescription": "CBconvert CLI", - "FileVersion": "0.6.0", - "InternalName": "", - "LegalCopyright": "", - "LegalTrademarks": "", - "OriginalFilename": "cbconvert.exe", - "PrivateBuild": "", - "ProductName": "CBconvert", - "ProductVersion": "0.6.0", - "SpecialBuild": "" - }, - "VarFileInfo": - { - "Translation": { - "LangID": "0409", - "CharsetID": "04B0" - } - } -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ca7315f --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/gen2brain/cbconvert + +go 1.19 + +require ( + github.com/chai2010/webp v1.1.1 + github.com/disintegration/imaging v1.6.2 + github.com/fvbommel/sortorder v1.0.2 + 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 + 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 new file mode 100644 index 0000000..dda665f --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= +github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo= +github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/gen2brain/go-fitz v1.20.1 h1:i5GPe/58q/gbNqa2mO+ZcTymwowbJEsDOXp7D0JwxgU= +github.com/gen2brain/go-fitz v1.20.1/go.mod h1:UZAxMETTDK4UPpuh80HaRpPzgkSibUihXVzwj2ip5oQ= +github.com/gen2brain/go-unarr v0.1.6 h1:2TtfIQ2dGuCkgEYa+vPE1ydcpkB3CtBbdYMfRSGLdA8= +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= +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= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/gographics/imagick.v3 v3.4.1 h1:CClNBnd1UGxH9KAl4Vuwx+jgNRkyKN+cHlbuFPyt+KU= +gopkg.in/gographics/imagick.v3 v3.4.1/go.mod h1:+Q9nyA2xRZXrDyTtJ/eko+8V/5E7bWYs08ndkZp8UmA=