// 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 import ( "archive/zip" "bytes" "crypto/md5" "errors" "fmt" "image" "image/color" "image/gif" "image/jpeg" "image/png" "io" "io/ioutil" "mime" "os" "os/signal" "path/filepath" "runtime" "sort" "strconv" "strings" "sync" "syscall" "github.com/cheggaaa/pb" "github.com/disintegration/imaging" "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" "gopkg.in/alecthomas/kingpin.v2" ) // 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 ) var filters = map[int]imaging.ResampleFilter{ NearestNeighbor: imaging.NearestNeighbor, Box: imaging.Box, Linear: imaging.Linear, MitchellNetravali: imaging.MitchellNetravali, CatmullRom: imaging.CatmullRom, Gaussian: imaging.Gaussian, 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 type Options struct { ToPNG bool // encode images to PNG instead of JPEG ToBMP bool // encode images to 4-Bit BMP (16 colors) instead of JPEG ToGIF bool // encode images to GIF instead of JPEG ToTIFF bool // encode images to TIFF instead of JPEG 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 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.) 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 } // Convertor struct type Convertor struct { Opts Options // Options struct Workdir string // Current working directory Nfiles int // Number of files Current int // Index of current file } // NewConvertor returns new convertor func NewConvertor(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 = "jpg" if c.Opts.ToPNG { ext = "png" } else if c.Opts.ToBMP { ext = "bmp" } else if c.Opts.ToGIF { ext = "gif" } else if c.Opts.ToTIFF { ext = "tiff" } var filename string if pathName != "" { filename = filepath.Join(c.Workdir, fmt.Sprintf("%s.%s", c.getBasename(pathName), ext)) } else { filename = filepath.Join(c.Workdir, fmt.Sprintf("%03d.%s", index, ext)) } if c.Opts.ToPNG { // convert image to PNG if c.Opts.Grayscale { c.encodeImageMagick(img, filename) } else { c.encodeImage(img, filename) } } else if c.Opts.ToBMP { // convert image to 4-Bit BMP (16 colors) c.encodeImageMagick(img, filename) } else if c.Opts.ToGIF { // convert image to GIF c.encodeImageMagick(img, filename) } else if c.Opts.ToTIFF { // convert image to TIFF c.encodeImage(img, filename) } else { // convert image to JPEG (default) if c.Opts.Grayscale { c.encodeImageMagick(img, filename) } else { c.encodeImage(img, filename) } } <-throttle } // Transforms image (resize, rotate, flip, brightness, contrast) func (c *Convertor) transformImage(img image.Image) image.Image { var i image.Image = img if c.Opts.Width > 0 || c.Opts.Height > 0 { if c.Opts.Fit { i = imaging.Fit(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) } else { i = imaging.Resize(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) } } if c.Opts.Rotate > 0 { switch c.Opts.Rotate { case 90: i = imaging.Rotate90(i) case 180: i = imaging.Rotate180(i) case 270: i = imaging.Rotate270(i) } } if c.Opts.Flip != "none" { switch c.Opts.Flip { case "horizontal": i = imaging.FlipH(i) case "vertical": i = imaging.FlipV(i) } } if c.Opts.Brightness != 0 { i = imaging.AdjustBrightness(i, c.Opts.Brightness) } if c.Opts.Contrast != 0 { i = imaging.AdjustContrast(i, c.Opts.Contrast) } return i } // Converts PDF/EPUB/XPS document to CBZ func (c *Convertor) convertDocument(file string) { c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") doc, err := fitz.NewDocument(file) if err != nil { fmt.Fprintf(os.Stderr, "Skipping %s, error: %v", file, err.Error()) return } npages := doc.Pages() if !c.Opts.Quiet { bar = pb.New(npages) bar.ShowTimeLeft = false bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.Current, c.Nfiles)) bar.Start() } for n := 0; n < npages; n++ { if !c.Opts.Quiet { bar.Increment() } img, err := doc.Image(n) if err == nil { img = c.transformImage(img) } if img != nil { throttle <- 1 wg.Add(1) go c.convertImage(img, n, "") } } wg.Wait() } // Converts archive to CBZ func (c *Convertor) convertArchive(file string) { c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") ncontents := len(c.listArchive(file)) archive, err := unarr.NewArchive(file) if err != nil { fmt.Fprintf(os.Stderr, "Error NewReader: %v\n", err.Error()) } defer archive.Close() if !c.Opts.Quiet { bar = pb.New(ncontents) bar.ShowTimeLeft = false bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.Current, c.Nfiles)) bar.Start() } for { err := archive.Entry() if err != nil { if err == io.EOF { break } else { fmt.Fprintf(os.Stderr, "Error Entry: %v\n", err.Error()) continue } } if !c.Opts.Quiet { bar.Increment() } size := archive.Size() 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 } i := c.transformImage(img) if !c.Opts.RGB && !c.isGrayScale(i) { c.encodeImage(i, filepath.Join(c.Workdir, filepath.Base(pathname))) continue } if i != nil { throttle <- 1 wg.Add(1) go c.convertImage(i, 0, pathname) } } else { if c.Opts.NonImage { c.copyFile(bytes.NewReader(buf), filepath.Join(c.Workdir, filepath.Base(pathname))) } } } wg.Wait() } // Converts directory to CBZ func (c *Convertor) convertDirectory(path string) { c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") images := c.getImages(path) if !c.Opts.Quiet { bar = pb.New(c.Nfiles) bar.ShowTimeLeft = false bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.Current, c.Nfiles)) bar.Start() } for index, img := range images { if c.Opts.Quiet { bar.Increment() } f, err := os.Open(img) if err != nil { fmt.Fprintf(os.Stderr, "Error Open: %v\n", err.Error()) continue } i, err := c.decodeImage(f, img) if err != nil { fmt.Fprintf(os.Stderr, "Error Decode: %v\n", err.Error()) continue } i = c.transformImage(i) if !c.Opts.RGB && !c.isGrayScale(i) { c.encodeImage(i, filepath.Join(c.Workdir, filepath.Base(img))) continue } f.Close() if i != nil { throttle <- 1 wg.Add(1) go c.convertImage(i, index, img) } } wg.Wait() } // Saves workdir to CBZ archive func (c *Convertor) saveArchive(file string) { defer os.RemoveAll(c.Workdir) 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 } defer zipfile.Close() z := zip.NewWriter(zipfile) files, _ := ioutil.ReadDir(c.Workdir) if !c.Opts.Quiet { bar = pb.New(len(files)) bar.ShowTimeLeft = false bar.Prefix(fmt.Sprintf("Compressing %d of %d: ", c.Current, c.Nfiles)) bar.Start() } for _, file := range files { if !c.Opts.Quiet { bar.Increment() } r, err := ioutil.ReadFile(filepath.Join(c.Workdir, file.Name())) if err != nil { fmt.Fprintf(os.Stderr, "Error ReadFile: %v\n", err.Error()) continue } w, err := z.Create(file.Name()) if err != nil { fmt.Fprintf(os.Stderr, "Error Create: %v\n", err.Error()) continue } 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) if err != nil { return } 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}) } f.Close() return } // Encode image to file (ImageMagick) func (c *Convertor) encodeImageMagick(i image.Image, filename string) (err error) { imagick.Initialize() mw := imagick.NewMagickWand() defer mw.Destroy() b := new(bytes.Buffer) jpeg.Encode(b, i, &jpeg.Options{c.Opts.Quality}) err = mw.ReadImageBlob(b.Bytes()) if err != nil { fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error()) return } if c.Opts.Grayscale { c := mw.GetImageColors() mw.QuantizeImage(c, imagick.COLORSPACE_GRAY, 8, true, true) } switch filepath.Ext(filename) { case ".png": mw.SetImageFormat("PNG") mw.WriteImage(filename) case ".gif": mw.SetImageFormat("GIF") mw.WriteImage(filename) case ".bmp": w := imagick.NewPixelWand() w.SetColor("black") defer w.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) } return } // Lists contents of archive func (c *Convertor) listArchive(file string) []string { var contents []string archive, err := unarr.NewArchive(file) if err != nil { fmt.Fprintf(os.Stderr, "Error NewReader: %v\n", err.Error()) } defer archive.Close() for { err := archive.Entry() if err != nil { if err == io.EOF { break } else { fmt.Fprintf(os.Stderr, "Error Entry: %v\n", err.Error()) continue } } pathname := archive.Name() contents = append(contents, pathname) } return contents } // Extracts cover from archive func (c *Convertor) coverArchive(file string) (image.Image, error) { var images []string contents := c.listArchive(file) for _, ct := range contents { if c.isImage(ct) { images = append(images, ct) } } cover := c.getCover(images) archive, err := unarr.NewArchive(file) if err != nil { return nil, err } defer archive.Close() err = archive.EntryFor(cover) if err != nil { 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) if err != nil { return nil, err } return img, nil } // Extracts cover from document func (c *Convertor) coverDocument(file string) (image.Image, error) { doc, err := fitz.NewDocument(file) if err != nil { return nil, err } img, err := doc.Image(0) if err != nil { return nil, err } if img == nil { return nil, errors.New("Image is nil") } return img, nil } // Extracts cover from directory func (c *Convertor) coverDirectory(dir string) (image.Image, error) { images := c.getImages(dir) cover := c.getCover(images) p, err := os.Open(cover) 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 } if img == nil { return nil, errors.New("Image is nil") } return img, nil } // Returns list of found comic files func (c *Convertor) GetFiles(args []string) []string { 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) } } } return nil } for _, arg := range args { path, _ := filepath.Abs(arg) stat, err := os.Stat(path) if err != nil { fmt.Fprintf(os.Stderr, "Error Stat: %v\n", err.Error()) continue } if !stat.IsDir() { if c.isArchive(path) || c.isDocument(path) { if c.isSize(stat.Size()) { files = append(files, path) } } } else { if c.Opts.Recursive { filepath.Walk(path, walkFiles) } else { fs, _ := ioutil.ReadDir(path) for _, f := range fs { if c.isArchive(f.Name()) || c.isArchive(f.Name()) { if c.isSize(f.Size()) { files = append(files, filepath.Join(path, f.Name())) } } } } if len(files) == 0 { // append plain directory with images files = append(files, path) } } } c.Nfiles = len(files) return files } // Returns list of found image files for given directory func (c *Convertor) getImages(path string) []string { 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) { images = append(images, fp) } } return nil } f, _ := filepath.Abs(path) stat, err := os.Stat(f) if err != nil { fmt.Fprintf(os.Stderr, "Error Stat: %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 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 { if strings.HasPrefix(i, "cover") || strings.HasPrefix(i, "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 } // Extracts cover func (c *Convertor) ExtractCover(file string, info os.FileInfo) { var err error var cover image.Image c.Current += 1 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 { fmt.Fprintf(os.Stderr, "Error Cover: %v\n", err.Error()) return } if c.Opts.Width > 0 || c.Opts.Height > 0 { if c.Opts.Fit { cover = imaging.Fit(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) } else { cover = imaging.Resize(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) } } filename := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%s.jpg", c.getBasename(file))) f, err := os.Create(filename) if err != nil { fmt.Fprintf(os.Stderr, "Error Create: %v\n", err.Error()) return } defer f.Close() jpeg.Encode(f, cover, &jpeg.Options{c.Opts.Quality}) } // Extracts thumbnail func (c *Convertor) ExtractThumbnail(file string, info os.FileInfo) { var err error var cover image.Image c.Current += 1 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 { fmt.Fprintf(os.Stderr, "Error Thumbnail: %v\n", err.Error()) return } if c.Opts.Width > 0 || c.Opts.Height > 0 { if c.Opts.Fit { cover = imaging.Fit(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) } else { cover = imaging.Resize(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) } } else { cover = imaging.Resize(cover, 256, 0, filters[c.Opts.Filter]) } imagick.Initialize() mw := imagick.NewMagickWand() defer mw.Destroy() b := new(bytes.Buffer) png.Encode(b, cover) err = mw.ReadImageBlob(b.Bytes()) if err != nil { fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error()) } fileuri := "file://" + file filename := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%x.png", md5.Sum([]byte(fileuri)))) 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.WriteImage(filename) } // Converts comic book func (c *Convertor) ConvertComic(file string, info os.FileInfo) { c.Current += 1 if info.IsDir() { c.convertDirectory(file) c.saveArchive(file) } else if c.isDocument(file) { c.convertDocument(file) c.saveArchive(file) } else { c.convertArchive(file) c.saveArchive(file) } } // Parses command line flags func parseFlags() (Options, []string) { opts := Options{} var args []string kingpin.Version("CBconvert 0.4.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("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(Linear)).IntVar(&opts.Filter) convert.Flag("png", "Encode images to PNG instead of JPEG").BoolVar(&opts.ToPNG) convert.Flag("bmp", "Encode images to 4-Bit BMP (16 colors) instead of JPEG").BoolVar(&opts.ToBMP) convert.Flag("gif", "Encode images to GIF instead of JPEG").BoolVar(&opts.ToGIF) convert.Flag("tiff", "Encode images to TIFF instead of JPEG").BoolVar(&opts.ToTIFF) convert.Flag("rgb", "Convert images that have RGB colorspace (use --no-rgb if you only want to convert grayscale 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) 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(Linear)).IntVar(&opts.Filter) thumbnail := kingpin.Command("thumbnail", "Extract cover thumbnail (freedesktop spec.)") thumbnail.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&args) 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(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 := NewConvertor(opts) 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) } }