From b930b06a7dbff0673d10f4470c62f92afdeef473 Mon Sep 17 00:00:00 2001 From: Milan Nikolic Date: Wed, 2 Dec 2015 20:46:42 +0100 Subject: [PATCH] add GUI --- README.md | 75 ++-- cbconvert.go | 313 +++++++++----- cmd/main.go | 17 +- cmd/make.bash | 39 +- cmd/versioninfo.json | 44 ++ gui/assets/icon.ico | Bin 0 -> 9662 bytes gui/assets/icon.png | Bin 0 -> 1935 bytes gui/assets/main.qml | 965 +++++++++++++++++++++++++++++++++++++++++++ gui/main.go | 549 ++++++++++++++++++++++++ gui/make.bash | 32 ++ gui/versioninfo.json | 44 ++ 11 files changed, 1922 insertions(+), 156 deletions(-) create mode 100644 cmd/versioninfo.json create mode 100644 gui/assets/icon.ico create mode 100644 gui/assets/icon.png create mode 100644 gui/assets/main.qml create mode 100644 gui/main.go create mode 100755 gui/make.bash create mode 100644 gui/versioninfo.json diff --git a/README.md b/README.md index 3b5c82b..abefda1 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Features - 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 or grayscale images + - 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 @@ -22,25 +22,26 @@ Features Download -------- - - [Windows binary](https://github.com/gen2brain/cbconvert/releases/download/0.4.0/cbconvert-0.4.0.zip) + - [Windows GUI](https://github.com/gen2brain/cbconvert/releases/download/0.5.0/cbconvert-0.5.0.zip) + - [Windows CMD](https://github.com/gen2brain/cbconvert/releases/download/0.5.0/cbconvert-cmd-0.5.0.zip) - - [Linux 64bit binary](https://github.com/gen2brain/cbconvert/releases/download/0.4.0/cbconvert-0.4.0.tar.gz) - - [Linux 64bit static binary](https://github.com/gen2brain/cbconvert/releases/download/0.4.0/cbconvert-0.4.0-static.tar.gz) + - [Linux 64bit GUI](https://github.com/gen2brain/cbconvert/releases/download/0.5.0/cbconvert-0.5.0.tar.gz) + - [Linux 64bit CMD](https://github.com/gen2brain/cbconvert/releases/download/0.5.0/cbconvert-cmd-0.5.0.tar.gz) -Using ------ +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 + --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 @@ -53,23 +54,26 @@ Using convert [] ... Convert archive or document (default command) - --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 - --png Encode images to PNG instead of JPEG - --bmp Encode images to 4-Bit BMP (16 colors) instead of JPEG - --gif Encode images to GIF instead of JPEG - --tiff Encode images to TIFF instead of JPEG - --rgb Convert images that have RGB colorspace (use --no-rgb if you only want to convert grayscale 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 + --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 cover [] ... Extract cover @@ -89,7 +93,7 @@ Using --filter=2 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos [man page](https://en.wikipedia.org/wiki/Man_page) is also available: - + cbconvert --help-man | man /dev/stdin Examples @@ -105,7 +109,7 @@ Convert all images in pdf to 4bit BMP image and save result in ~/comics director [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 in ~/.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 ~/.thumbnails/normal /media/comics/GrooTheWanderer/ @@ -148,10 +152,15 @@ Install dependencies: 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 golang.org/x/image/webp go get gopkg.in/alecthomas/kingpin.v2 -Install go package: +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 github.com/gen2brain/cbconvert + go build -o $GOPATH/bin/cbconvert github.com/gen2brain/cbconvert/gui diff --git a/cbconvert.go b/cbconvert.go index 8dbdf5a..eeba03c 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -79,37 +79,42 @@ 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 + 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.) + 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 } // 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 + 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 } // NewConvertor returns new convertor @@ -123,15 +128,19 @@ func NewConvertor(o Options) *Convertor { func (c *Convertor) convertImage(img image.Image, index int, pathName string) { defer wg.Done() - var ext string = "jpg" - if c.Opts.ToPNG { + var ext string + switch c.Opts.Format { + case "jpeg": + ext = "jpg" + case "png": ext = "png" - } else if c.Opts.ToBMP { - ext = "bmp" - } else if c.Opts.ToGIF { + case "gif": ext = "gif" - } else if c.Opts.ToTIFF { + case "tiff": ext = "tiff" + case "bmp": + ext = "bmp" + } var filename string @@ -141,36 +150,48 @@ func (c *Convertor) convertImage(img image.Image, index int, pathName string) { 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 { + img = c.TransformImage(img) + + if c.Opts.LevelsInMin != 0 || c.Opts.LevelsInMax != 255 || c.Opts.LevelsGamma != 1.00 || + c.Opts.LevelsOutMin != 0 || c.Opts.LevelsOutMax != 255 { + img = c.LevelImage(img) + } + + switch c.Opts.Format { + case "jpeg": // convert image to JPEG (default) if c.Opts.Grayscale { c.encodeImageMagick(img, filename) } else { c.encodeImage(img, filename) } + case "png": + // convert image to PNG + if c.Opts.Grayscale { + c.encodeImageMagick(img, filename) + } else { + c.encodeImage(img, filename) + } + 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) + } + case "bmp": + // convert image to 4-Bit BMP (16 colors) + c.encodeImageMagick(img, filename) } <-throttle } // Transforms image (resize, rotate, flip, brightness, contrast) -func (c *Convertor) transformImage(img image.Image) image.Image { +func (c *Convertor) TransformImage(img image.Image) image.Image { var i image.Image = img if c.Opts.Width > 0 || c.Opts.Height > 0 { @@ -212,6 +233,49 @@ 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 { + imagick.Initialize() + + mw := imagick.NewMagickWand() + defer mw.Destroy() + + err := mw.ReadImageBlob(c.GetImageBytes(img)) + if err != nil { + fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error()) + return img + } + + _, qrange := imagick.GetQuantumRange() + quantumRange := float64(qrange) + + inmin := (quantumRange * c.Opts.LevelsInMin) / 255 + inmax := (quantumRange * c.Opts.LevelsInMax) / 255 + outmin := (quantumRange * c.Opts.LevelsOutMin) / 255 + outmax := (quantumRange * c.Opts.LevelsOutMax) / 255 + + err = mw.LevelImage(inmin, c.Opts.LevelsGamma, inmax) + if err != nil { + fmt.Fprintf(os.Stderr, "Error LevelImageChannel Input: %v\n", err.Error()) + return img + } + + 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 + } + + 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 i +} + // Converts PDF/EPUB/XPS document to CBZ func (c *Convertor) convertDocument(file string) { c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") @@ -222,24 +286,25 @@ func (c *Convertor) convertDocument(file string) { return } - npages := doc.Pages() + c.Ncontents = doc.Pages() + c.CurrContent = 0 if !c.Opts.Quiet { - bar = pb.New(npages) + bar = pb.New(c.Ncontents) bar.ShowTimeLeft = false - bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.Current, c.Nfiles)) + bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.CurrFile, c.Nfiles)) bar.Start() } - for n := 0; n < npages; n++ { + for n := 0; n < c.Ncontents; n++ { + c.CurrContent++ if !c.Opts.Quiet { bar.Increment() } img, err := doc.Image(n) - - if err == nil { - img = c.transformImage(img) + if err != nil { + fmt.Fprintf(os.Stderr, "Error Image: %v\n", err.Error()) } if img != nil { @@ -256,18 +321,23 @@ func (c *Convertor) convertDocument(file string) { func (c *Convertor) convertArchive(file string) { c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") - ncontents := len(c.listArchive(file)) + contents := c.listArchive(file) + c.Ncontents = len(contents) + c.CurrContent = 0 + + cover := c.getCover(c.getImagesFromSlice(contents)) archive, err := unarr.NewArchive(file) if err != nil { fmt.Fprintf(os.Stderr, "Error NewReader: %v\n", err.Error()) + return } defer archive.Close() if !c.Opts.Quiet { - bar = pb.New(ncontents) + bar = pb.New(c.Ncontents) bar.ShowTimeLeft = false - bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.Current, c.Nfiles)) + bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.CurrFile, c.Nfiles)) bar.Start() } @@ -282,6 +352,7 @@ func (c *Convertor) convertArchive(file string) { } } + c.CurrContent++ if !c.Opts.Quiet { bar.Increment() } @@ -310,17 +381,24 @@ func (c *Convertor) convertArchive(file string) { continue } - i := c.transformImage(img) + if !c.Opts.ConvertCover { + if cover == pathname { + img = c.TransformImage(img) + c.encodeImage(img, filepath.Join(c.Workdir, filepath.Base(pathname))) + continue + } + } - if !c.Opts.RGB && !c.isGrayScale(i) { - c.encodeImage(i, filepath.Join(c.Workdir, filepath.Base(pathname))) + if !c.Opts.RGB && !c.isGrayScale(img) { + img = c.TransformImage(img) + c.encodeImage(img, filepath.Join(c.Workdir, filepath.Base(pathname))) continue } - if i != nil { + if img != nil { throttle <- 1 wg.Add(1) - go c.convertImage(i, 0, pathname) + go c.convertImage(img, 0, pathname) } } else { if c.Opts.NonImage { @@ -335,17 +413,20 @@ func (c *Convertor) convertArchive(file string) { func (c *Convertor) convertDirectory(path string) { c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") - images := c.getImages(path) + images := c.getImagesFromPath(path) + c.Ncontents = len(images) + c.CurrContent = 0 if !c.Opts.Quiet { - bar = pb.New(c.Nfiles) + bar = pb.New(c.Ncontents) bar.ShowTimeLeft = false - bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.Current, c.Nfiles)) + bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.CurrFile, c.Nfiles)) bar.Start() } for index, img := range images { - if c.Opts.Quiet { + c.CurrContent++ + if !c.Opts.Quiet { bar.Increment() } @@ -361,9 +442,8 @@ func (c *Convertor) convertDirectory(path string) { continue } - i = c.transformImage(i) - if !c.Opts.RGB && !c.isGrayScale(i) { + i = c.TransformImage(i) c.encodeImage(i, filepath.Join(c.Workdir, filepath.Base(img))) continue } @@ -394,10 +474,12 @@ func (c *Convertor) saveArchive(file string) { z := zip.NewWriter(zipfile) files, _ := ioutil.ReadDir(c.Workdir) + ncontents := len(files) + if !c.Opts.Quiet { - bar = pb.New(len(files)) + bar = pb.New(ncontents) bar.ShowTimeLeft = false - bar.Prefix(fmt.Sprintf("Compressing %d of %d: ", c.Current, c.Nfiles)) + bar.Prefix(fmt.Sprintf("Compressing %d of %d: ", c.CurrFile, c.Nfiles)) bar.Start() } @@ -464,10 +546,7 @@ func (c *Convertor) encodeImageMagick(i image.Image, filename string) (err error mw := imagick.NewMagickWand() defer mw.Destroy() - b := new(bytes.Buffer) - jpeg.Encode(b, i, &jpeg.Options{c.Opts.Quality}) - - err = mw.ReadImageBlob(b.Bytes()) + err = mw.ReadImageBlob(c.GetImageBytes(i)) if err != nil { fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error()) return @@ -482,6 +561,10 @@ func (c *Convertor) encodeImageMagick(i image.Image, filename string) (err error 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) @@ -516,7 +599,7 @@ 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()) + return contents } defer archive.Close() @@ -526,7 +609,6 @@ func (c *Convertor) listArchive(file string) []string { if err == io.EOF { break } else { - fmt.Fprintf(os.Stderr, "Error Entry: %v\n", err.Error()) continue } } @@ -605,7 +687,7 @@ func (c *Convertor) coverDocument(file string) (image.Image, error) { // Extracts cover from directory func (c *Convertor) coverDirectory(dir string) (image.Image, error) { - images := c.getImages(dir) + images := c.getImagesFromPath(dir) cover := c.getCover(images) p, err := os.Open(cover) @@ -646,7 +728,7 @@ func (c *Convertor) GetFiles(args []string) []string { path, _ := filepath.Abs(arg) stat, err := os.Stat(path) if err != nil { - fmt.Fprintf(os.Stderr, "Error Stat: %v\n", err.Error()) + fmt.Fprintf(os.Stderr, "Error Stat GetFiles: %v\n", err.Error()) continue } @@ -662,7 +744,7 @@ func (c *Convertor) GetFiles(args []string) []string { } else { fs, _ := ioutil.ReadDir(path) for _, f := range fs { - if c.isArchive(f.Name()) || c.isArchive(f.Name()) { + if c.isArchive(f.Name()) || c.isDocument(f.Name()) { if c.isSize(f.Size()) { files = append(files, filepath.Join(path, f.Name())) } @@ -682,7 +764,7 @@ func (c *Convertor) GetFiles(args []string) []string { } // Returns list of found image files for given directory -func (c *Convertor) getImages(path string) []string { +func (c *Convertor) getImagesFromPath(path string) []string { var images []string walkFiles := func(fp string, f os.FileInfo, err error) error { @@ -697,7 +779,7 @@ func (c *Convertor) getImages(path string) []string { f, _ := filepath.Abs(path) stat, err := os.Stat(f) if err != nil { - fmt.Fprintf(os.Stderr, "Error Stat: %v\n", err.Error()) + fmt.Fprintf(os.Stderr, "Error Stat getImagesFromPath: %v\n", err.Error()) return images } @@ -712,6 +794,26 @@ func (c *Convertor) getImages(path string) []string { 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 { @@ -719,7 +821,9 @@ func (c *Convertor) getCover(images []string) string { } for _, i := range images { - if strings.HasPrefix(i, "cover") || strings.HasPrefix(i, "front") { + e := c.getBasename(i) + if strings.HasPrefix(i, "cover") || strings.HasPrefix(i, "front") || + strings.HasSuffix(e, "cover") || strings.HasSuffix(e, "front") { return i } } @@ -807,11 +911,10 @@ func (c *Convertor) getBasename(file string) string { return basename } -// Extracts cover -func (c *Convertor) ExtractCover(file string, info os.FileInfo) { +// Returns cover image.Image +func (c *Convertor) GetCoverImage(file string, info os.FileInfo) (image.Image, error) { var err error var cover image.Image - c.Current += 1 if info.IsDir() { cover, err = c.coverDirectory(file) @@ -822,7 +925,19 @@ func (c *Convertor) ExtractCover(file string, info os.FileInfo) { } if err != nil { - fmt.Fprintf(os.Stderr, "Error Cover: %v\n", err.Error()) + 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 } @@ -847,16 +962,12 @@ func (c *Convertor) ExtractCover(file string, info os.FileInfo) { // Extracts thumbnail func (c *Convertor) ExtractThumbnail(file string, info os.FileInfo) { - var err error - var cover image.Image - c.Current += 1 + c.CurrFile += 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) + cover, err := c.GetCoverImage(file, info) + if err != nil { + fmt.Fprintf(os.Stderr, "Error GetCoverImage: %v\n", err.Error()) + return } if err != nil { @@ -891,7 +1002,7 @@ func (c *Convertor) ExtractThumbnail(file string, info os.FileInfo) { filename := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%x.png", md5.Sum([]byte(fileuri)))) mw.SetImageFormat("PNG") - mw.SetImageProperty("Software", "cbconvert") + 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)) @@ -903,7 +1014,7 @@ func (c *Convertor) ExtractThumbnail(file string, info os.FileInfo) { // Converts comic book func (c *Convertor) ConvertComic(file string, info os.FileInfo) { - c.Current += 1 + c.CurrFile += 1 if info.IsDir() { c.convertDirectory(file) c.saveArchive(file) diff --git a/cmd/main.go b/cmd/main.go index 4ccd1fc..e96a8e4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -15,6 +15,8 @@ package main +//go:generate goversioninfo + import ( "fmt" "image/jpeg" @@ -33,7 +35,7 @@ func parseFlags() (cbconvert.Options, []string) { opts := cbconvert.Options{} var args []string - kingpin.Version("CBconvert 0.4.0") + kingpin.Version("CBconvert 0.5.0") kingpin.CommandLine.Help = "Comic Book convert tool." kingpin.UsageTemplate(kingpin.CompactUsageTemplate) @@ -47,13 +49,11 @@ func parseFlags() (cbconvert.Options, []string) { 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("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("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) @@ -61,6 +61,11 @@ func parseFlags() (cbconvert.Options, []string) { 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) diff --git a/cmd/make.bash b/cmd/make.bash index 36256a8..519a603 100755 --- a/cmd/make.bash +++ b/cmd/make.bash @@ -1,21 +1,28 @@ #!/usr/bin/env bash -mkdir -p build +CHROOT="/home/milann/chroot" +MINGW="/usr/i686-w64-mingw32" -CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/cbconvert +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 -CGO_LDFLAGS="-ldl -lltdl -lfreetype -lm -lz -static-libgcc" CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/cbconvert-static --ldflags '-extldflags "-static"' -strip build/cbconvert-static - -CGO_LDFLAGS="-L/usr/i686-pc-mingw32/usr/lib" \ -CGO_CFLAGS="-I/usr/i686-pc-mingw32/usr/include -Wno-poison-system-directories" \ -CGO_CXXFLAGS="-I/usr/i686-pc-mingw32/usr/include -Wno-poison-system-directories" \ -CGO_CPPFLAGS="-I/usr/i686-pc-mingw32/usr/include -Wno-poison-system-directories" \ -PKG_CONFIG=/usr/bin/i686-pc-mingw32-pkg-config \ -PKG_CONFIG_PATH=/usr/i686-pc-mingw32/usr/lib/pkgconfig \ -PKG_CONFIG_LIBDIR=/usr/i686-pc-mingw32/usr/lib/pkgconfig \ -CC="i686-pc-mingw32-gcc" CXX="i686-pc-mingw32-g++" \ -CC_FOR_TARGET=i686-pc-mingw32-gcc CXX_FOR_TARGET=i686-pc-mingw32-g++ \ -CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -o build/cbconvert.exe -ldflags "-linkmode external -extldflags -static" -i686-pc-mingw32-strip build/cbconvert.exe +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 new file mode 100644 index 0000000..6d757fa --- /dev/null +++ b/cmd/versioninfo.json @@ -0,0 +1,44 @@ +{ + "FixedFileInfo": + { + "FileVersion": { + "Major": 0, + "Minor": 5, + "Patch": 0, + "Build": 0 + }, + "ProductVersion": { + "Major": 0, + "Minor": 5, + "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.5.0", + "InternalName": "", + "LegalCopyright": "", + "LegalTrademarks": "", + "OriginalFilename": "cbconvert.exe", + "PrivateBuild": "", + "ProductName": "CBconvert", + "ProductVersion": "0.5.0", + "SpecialBuild": "" + }, + "VarFileInfo": + { + "Translation": { + "LangID": "0409", + "CharsetID": "04B0" + } + } +} diff --git a/gui/assets/icon.ico b/gui/assets/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..20805da5c807352bdde66fc654786c93911b179a GIT binary patch literal 9662 zcmeI0d2H0j701_@8&!set0F+SDiCKYZXq?CQlO<=rK*s)1qeg|K~oTi4TT0MS0Evz zM7SI+M~qB40ty9e2XmCGK!P~}IDie<1{-60cfD6X-(_d@`|U0eq~?!Ge(1yee)F4o z^XAQ)H?wv);`rC8lY{@?aqMd4aMX7=9Ib&W$5>E(P+B@1SwDKgi;9Z>D?Cu)feH^) zc;E;2KxOpD{lH`VQ0gn8ct7O8Y1julLwrt|3Y%d()PnCTjcJ%xrAn2{jT<+<+`M`7 zQ%##T&8%L%x^#X5E8qhd3mqXI-c;()VYjGSwd&b1W5&4B)6+d=Wo1R><>dv2Vk9Lc z#XV@ypn}@9Yj2}{n^z3<6TZ!X;>wjPd!9Ub;toWJ&*$^z=H~jYT)E=jyLYd1@#4iL zy?gh*Py5g4*Wneqt)FDWCHS;;>(-?=Z{GAo3={M9@#DvyAw!1b)31e9S?IYl`}W_U%PPxwkzb7d0DH@S@7_IId_H~p)Hi9;q@qfd zDy6h&(IO`^Gc)Ge96EGJy84F(7#Gs(w(95E_weCEoBr=Rk2!PZl+wQ;NR&4&L)dnn zJ-vGMii-Kcg9jJ!?Vs=z?sn|h@e*T3twmN=mY?r`1jzzW?5NI+k&h4dJDpCe{r;gt zhq}lE2L`=%3`aT5|AGr$_EQlGuckbNLS}q4k z4LA(xVC)K9xNsq?TlVGW=j+?IU~CAsCErBF5O(?E5wIgp8^Q|zVUGb<APo7>&Tgj)OGvOJOgW@QsTD5BBR{OKFvlUbF zz1kRCT>bj>E4Av@^ZNDcwT}HjG6XJx>2s}4ojNxn`{^J%gc)kdJ?hCg4`Rv&DjYqF*!AlyN3Ly`%)K>tOMoK%(Gd1U3vm5Sf7U_|zZZQ(h;e*OA+tZk(EfkurQ?dH4o{S`|e zje7b)cfmSe^Q?W52$6Sf<)7P;ea4L&XY(uh!MkhMF4xA5 z8@=bxpLemRZ2lWLa-{O>$50ilBKx<(b&xIYdiClRKX~xKt^jSEY4VZw;oVSMW4rd} zM2IC?mm)B2gJqX4UCL|*zUCz_FVEYxYges%?TA5?e`Js1K>PZ0xX0bqw!fr)>eQ*) zCw;&gKRVOi1|!n{bLbwbSVVaPZ1!`O+xDf^e9jMB?la%6U%y_?-B4>_m7npF2FhL9 zm&zRh&hl{Vnf~k@ed__*`;9`p#XdX1nq}ahuk|}&_3G75?(DgIS1f72Xx`Ry!$mAc z<;4RB4(R^vBz|4z&Yg1-Ls2oXeED+O_%0Zc9p=mn`)KLXrBVJrc<`X?H*q4{eh0an zrEb>OHWt4-lymC9U*T&A>(=w+$&+pIFCWdHKi`^z0^~Cnzhi86C_j@QrPIh;Jeb%w zV|e95<9AbX#z_B0_!XF(9%$FDT~sV@+qO;nS+SN4@>wQagJO6vYu2oMqt}$&E49Cl zgZj!AKff&>>CSB0sO;0HkMf8S*>Bpb&v)?QqD6~r^Hx9YAnmSz zzxgUOKq}=HSO-S`p3R##+x81xeBHWrE9JiL-oAZ%Nk&G7hdEjO9$bPoYu4zDY6n*N z4KGHwO5LAMsBXrL8KvCiY-5qbeEdF;&u#U=0r>COv11K*VjfcCL{1GY2)9Ik!wbpTh&-|^Un0srKFJg_QxwUQER{6dPybal)c=Lnwsr&|s zin&<&Or6$!`0(MnyR?8E%4}%$ql_ob$e%`MCI7WSRbM z2xZC1$u54sxcRNA+?xcilVvo}7&_mqb5TyvzA@wfE8@F+u5(0Zp;ffjI&+oJOke3$ zd-HBe*;o|9)Tvb4_d&Vrb)q}XbkKg&IN})m9jrDO{rbKSI>O8313sAcH+k0F^>+aE zHT8-ujjQ;!&P}@Cfj5-~bkMnW7%qZhPiLdnK=aVw2X$t2g4anG8YF}Emwcf)d=kO3z^S+SFDKc$vrwjm?k-88GVrI&!dZ$En$UgK3>nXEUSxqG`37QWt!Ln(R;}@D|Vv=qSf%K3c KROtN;%l`s2OOj~- literal 0 HcmV?d00001 diff --git a/gui/assets/icon.png b/gui/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6e2eb741dddeb7193de8efc4f5f15d2e5cfdd841 GIT binary patch literal 1935 zcmV;A2XOd_P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 zVoOIv0RM-N%)bBt010qNS#tmY4c7nw4c7reD4Tcy000McNliru-wO*74i@;{`e=|GlhwM0ZoW#z8Jrz*wxwS;K zw9G7*Hc(qu zBkvK@UDr;O<=nuGzcvEhtvfAT71DBQk z*lAnlkkRboV^aVJxTajL+$`cgM6DJ?gl`J+!4<1$%v&^7vq6$bv~8k)4~QmOEt~$tgkQ9ABfQhl$ypv z12Gq1X|(A5pmTz9K8ykMV63vwMSPcjx4~tMBc$-MB>})vSVIAuu+h0pO>s2%Vk}?@ z0Cm>TU&Qb9ZFhlAn*I2S2n}D7J<27Ao+zc%bB3&Fo^QcI7jWp~3O#IQU^qQ(paM!+ zTB)q{BG3Wo24Drd=1bs^*4*dW$#p(W?jl!LLpW$K!FDQ0kzs<(RwnFp0}}ycRb;E- z>*A&lkVio2*gl0BOHm!@?xuEKO8|Ca6hCIM(a|71d>ebul zwG&dEqpKLt*ybPn)qV8flHn-5FJ*#;e0YLS(1sv1vmAh8s@V$sh^}Dfcg~;*d)iQ5 zwF{mNlC9@3R}rj5$v!tQ_=rt{9u;VqZ%iYWy&-Q(O?vh@9&)y{Q4D^G;LF4SC!*(v zQQR&HLuOydZ&=0?s@qB!WxoGmy=v;1d)A2?TbvK(`t|vi_0QKyPwhbExp-)bRSxhE-{G2i03_T=KgUCV8S=g&&I&?B&>+yEtCd$tS48AS*0_m{AY|@M1xYq!xK{^;SmkfU(Xwiw5-^F+bvf&* zbaj*kt`q_lyg?CBbmEb#idami5T9i1GCQb!jYhQ&bTL4)uj*1%5rO$6{PpsX&?0&y z3S46??O4G2oRvO_?1?f;A0QO&%nR&45M{Lvm@MaGzN%O4pp^0fMIiR$*}%cZ9S7c_ zn6-=p&Sab80{qIOkbQLW?hls3t_cOM$pn=1SNKJEHH z>98I68iJ*I#vPpF+7}VoouWJd`#2X6x@p=q?HW&dGaYsfu>e>TKDfMvvAM%L-iD58 z1Ve;JNpm*uWVbWB!$7uRzp~xID1yV_>Kk(d{Cr~O+X$eFRu0~k>dwB`JeI8=p7zq& zOeey%ikq_sf7|?ysr`gmq=ZHqb1r-(b1s`6w@2&|dnB1`vOYCZ`~p=oESIpGq>&vB z&%kvC+&1eabMNFmH;v!15!EiYiq3KFrk5BdU|=vp3yq{^NvDD%(UNkaDOX|xnGuqB zbkRyXF8@J{IsPrVEw|szH##viIx;dVFflqXFrN7s+5i9m8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuU Va%Y?FJQ@H1002ovPDHLkV1nZRdh`GQ literal 0 HcmV?d00001 diff --git a/gui/assets/main.qml b/gui/assets/main.qml new file mode 100644 index 0000000..fe62e2b --- /dev/null +++ b/gui/assets/main.qml @@ -0,0 +1,965 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.2 +import Qt.labs.settings 1.0 + +ApplicationWindow { + id: applicationWindow + visible: true + width: 800 + height: 600 + title: "CBconvert" + + property int margin: 15 + property int screenWidth: Screen.width + property int screenHeight: Screen.height + + function updateImage() { + imagePreview.source = "" + sizePreview.text = "" + if(groupBoxPreview.checked) { + if(c.len > 0) { + imagePreview.source = "image://cover/" + c.get(listView.currentIndex).path + } + } + } + + ColumnLayout { + id: splitView1 + anchors.rightMargin: margin + anchors.leftMargin: margin + anchors.bottomMargin: margin + anchors.topMargin: margin + anchors.fill: parent + + RowLayout { + id: rowLayout1 + anchors.bottom: rowLayout2.top + anchors.right: parent.right + anchors.left: parent.left + anchors.top: parent.top + anchors.bottomMargin: 15 + Layout.minimumHeight: 250 + + ScrollView { + id: scrollview + anchors.right: columnButtons.left + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.top: parent.top + anchors.rightMargin: margin + frameVisible: true + highlightOnFocus: false + verticalScrollBarPolicy: 2 + flickableItem.interactive: true + focus: true + + ListView { + id: listView + anchors.topMargin: 0 + anchors.fill: parent + spacing: 1 + focus: true + + model: c.len + + header: Rectangle { + height: 20 + width: ListView.view.width + color: "#DCDCDC" + + Text { + id: nameHeader + text: 'Name' + anchors.left: parent.left + anchors.leftMargin: 5 + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + onClicked: { + c.byName() + } + } + } + + Text { + id: typeHeader + text: 'Type' + width: 40 + anchors.right: sizeHeader.left + anchors.rightMargin: 40 + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + + MouseArea { + anchors.fill: parent + onClicked: { + c.byType() + } + } + } + + Text { + id: sizeHeader + text: 'Size' + width: 40 + anchors.right: parent.right + anchors.rightMargin: 15 + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + + MouseArea { + anchors.fill: parent + onClicked: { + c.bySize() + } + } + } + } + + delegate: Item { + id: item1 + width: ListView.view.width + height: 20 + + Text { + id: nameItem + text: c.get(index).name + anchors.left: parent.left + anchors.leftMargin: 5 + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 170 + elide: Text.ElideRight + } + + Text { + id: typeItem + width: 40 + text: c.get(index).type + anchors.right: sizeItem.left + anchors.rightMargin: 40 + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + } + + Text { + id: sizeItem + width: 40 + text: c.get(index).sizeHuman + anchors.right: parent.right + anchors.rightMargin: 15 + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + } + + MouseArea { + anchors.fill: parent + onClicked: { + listView.currentIndex = index + listView.forceActiveFocus() + } + } + } + + highlight: Rectangle { + width: ListView.view ? ListView.view.width : undefined + color: "#326686" + opacity: 0.2 + y: listView.currentItem.y + Behavior on y { + SpringAnimation { + spring: 3 + damping: 0.2 + } + } + } + + onCurrentItemChanged: updateImage() + } + } + + ColumnLayout { + id: columnButtons + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + + ColumnLayout { + id: columnButtonsFiles + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + spacing: 5 + + Button { + id: buttonAddFile + objectName: "buttonAddFile" + text: "Add &Files" + onClicked: { + fileDialogFile.open() + } + } + + Button { + id: buttonAddDir + objectName: "buttonAddDir" + text: "Add &Dir" + onClicked: { + fileDialogDir.open() + } + } + + Button { + id: buttonRemove + objectName: "buttonRemove" + text: "Remove" + enabled: (c.len !== 0) ? true : false + onClicked: { + c.remove(listView.currentIndex) + updateImage() + } + } + + Button { + id: buttonRemoveAll + objectName: "buttonRemoveAll" + text: "Remove All" + enabled: (c.len !== 0) ? true : false + onClicked: { + c.removeAll() + updateImage() + } + } + } + + ColumnLayout { + id: columnButtonsActions + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + spacing: 5 + + Button { + id: buttonThumbnail + objectName: "buttonThumbnail" + text: "Thumbnail" + anchors.bottom: buttonCover.top + anchors.bottomMargin: 5 + tooltip: "Extract Thumbnail (freedesktop spec.)" + enabled: (textFieldOutDir.text != "" && c.len !== 0) ? true : false + } + + Button { + id: buttonCover + objectName: "buttonCover" + text: "Cover" + anchors.bottom: buttonConvert.top + anchors.bottomMargin: 15 + tooltip: "Extract Cover" + enabled: (textFieldOutDir.text != "" && c.len !== 0) ? true : false + } + + Button { + id: buttonConvert + objectName: "buttonConvert" + text: "&Convert" + tooltip: "Convert archives and documents" + enabled: (textFieldOutDir.text != "" && c.len !== 0) ? true : false + } + } + } + } + + RowLayout { + id: rowLayout2 + spacing: 0 + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.left: parent.left + + ColumnLayout { + id: columnLeft + anchors.right: columnMiddle.left + anchors.rightMargin: margin + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + Layout.minimumWidth: 200 + + GroupBox { + id: groupBoxPreview + checkable: true + flat: true + anchors.fill: parent + title: "Preview" + + Image { + id: imagePreview + anchors.fill: parent + fillMode: Image.PreserveAspectFit + asynchronous: true + cache: false + } + + BusyIndicator { + running: imagePreview.status === Image.Loading + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + } + + onCheckedChanged: updateImage() + } + + Text { + id: sizePreview + objectName: "sizePreview" + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: groupBoxPreview.bottom + anchors.topMargin: -5 + } + } + + ColumnLayout { + id: columnMiddle + anchors.right: columnRight.left + anchors.rightMargin: margin + anchors.top: parent.top + anchors.topMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + Layout.fillHeight: true + + GroupBox { + id: groupBoxInput + flat: true + anchors.right: parent.right + anchors.rightMargin: 0 + title: "Input" + anchors.top: parent.top + anchors.topMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + + ColumnLayout { + id: columnInput + anchors.bottomMargin: 0 + anchors.fill: parent + spacing: 5 + + CheckBox { + id: checkBoxRecursive + objectName: "checkBoxRecursive" + text: "Recurse SubDirectories" + } + + CheckBox { + id: checkBoxNoRGB + objectName: "checkBoxNoRGB" + text: "Only Grayscaled Images" + } + + CheckBox { + id: checkBoxConvertCover + objectName: "checkBoxConvertCover" + text: "Exclude Cover" + } + + RowLayout { + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + spacing: 5 + + SpinBox { + id: spinboxSize + objectName: "spinboxSize" + stepSize: 10 + prefix: "" + maximumValue: 1000 + suffix: " MiB" + } + + Text { + text: "Minimum Size" + } + } + } + } + + GroupBox { + id: groupBoxTransform + flat: true + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + title: "Transform" + + ColumnLayout { + id: columnTransform + anchors.fill: parent + spacing: 5 + + Button { + id: buttonLevels + text: "Levels..." + enabled: (c.len > 0) ? true : false + onClicked: { + levelsDialog.open() + } + } + + Text { + text: "Brightness: " + sliderBrightness.value + } + + Slider { + id: sliderBrightness + objectName: "sliderBrightness" + value: 0 + stepSize: 1 + minimumValue: -100 + maximumValue: 100 + activeFocusOnPress: true + updateValueWhileDragging: false + enabled: (c.len > 0) ? true : false + onValueChanged: updateImage() + } + + Text { + text: "Contrast: " + sliderContrast.value + } + + Slider { + id: sliderContrast + objectName: "sliderContrast" + value: 0 + maximumValue: 100 + stepSize: 1 + minimumValue: -100 + activeFocusOnPress: true + updateValueWhileDragging: false + enabled: (c.len > 0) ? true : false + onValueChanged: updateImage() + } + + Text { + text: "Flip:" + } + + ComboBox { + id: comboBoxFlip + objectName: "comboBoxFlip" + enabled: (c.len > 0) ? true : false + + model: ListModel { + ListElement { + text: "None" + } + + ListElement { + text: "Horizontal" + } + + ListElement { + text: "Vertical" + } + } + + onActivated: updateImage() + } + + Text { + text: "Rotate:" + } + + ComboBox { + id: comboBoxRotate + objectName: "comboBoxRotate" + enabled: (c.len > 0) ? true : false + + model: ListModel { + ListElement { + text: "0" + } + + ListElement { + text: "90" + } + + ListElement { + text: "180" + } + + ListElement { + text: "270" + } + } + + onActivated: updateImage() + } + } + } + } + + ColumnLayout { + id: columnRight + anchors.top: parent.top + anchors.topMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + Layout.fillHeight: true + + GroupBox { + id: groupBoxOutput + flat: true + title: "Output" + anchors.top: parent.top + anchors.topMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + + ColumnLayout { + id: columnOutput + anchors.bottomMargin: 0 + anchors.fill: parent + spacing: 5 + + RowLayout { + id: rowLayoutOutput + anchors.top: parent.top + anchors.topMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + spacing: 5 + + TextField { + id: textFieldOutDir + objectName: "textFieldOutDir" + anchors.right: buttonBrowse.left + anchors.rightMargin: 5 + anchors.left: parent.left + anchors.leftMargin: 0 + placeholderText: "Output Directory" + + Settings { + id: settingsOutDir + property alias text: textFieldOutDir.text + } + } + + Button { + id: buttonBrowse + text: "..." + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.verticalCenter: parent.verticalCenter + onClicked: { + fileDialogOutput.open() + } + } + } + + TextField { + id: textFieldSuffix + objectName: "textFieldSuffix" + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + placeholderText: "Add Suffix to Output File" + } + + CheckBox { + id: checkBoxNonImage + objectName: "checkBoxNonImage" + text: "Remove Non-Image Files" + } + } + } + + + GroupBox { + id: groupBoxImage + flat: true + anchors.left: parent.left + anchors.leftMargin: 0 + title: "Image" + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + + ColumnLayout { + id: columnFormat + anchors.fill: parent + spacing: 5 + + Text { + text: "Format:" + } + + ComboBox { + id: comboBoxFormat + objectName: "comboBoxFormat" + enabled: (c.len > 0) ? true : false + + model: ListModel { + ListElement { + text: "JPEG" + } + + ListElement { + text: "PNG" + } + + ListElement { + text: "GIF" + } + + ListElement { + text: "BMP" + } + + ListElement { + text: "TIFF" + } + } + + onActivated: updateImage() + } + + Text { + text: "Size:" + } + + RowLayout { + spacing: 5 + + TextField { + id: width + objectName: "width" + placeholderText: "width" + maximumLength: 4 + implicitWidth: 50 + onAccepted: updateImage() + } + + Text { + id: x + text: "x" + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + } + + TextField { + id: height + objectName: "height" + placeholderText: "height" + maximumLength: 4 + implicitWidth: 50 + onAccepted: updateImage() + } + } + + CheckBox { + id: checkBoxFit + objectName: "checkBoxFit" + text: "Best Fit" + enabled: (c.len > 0) ? true : false + onClicked: updateImage() + } + + Text { + text: "Resize Algorithm:" + } + + ComboBox { + id: comboBoxFilter + objectName: "comboBoxFilter" + currentIndex: 2 + enabled: (c.len > 0) ? true : false + + model: ListModel { + ListElement { + text: "NearestNeighbor" + } + + ListElement { + text: "Box" + } + + ListElement { + text: "Linear" + } + + ListElement { + text: "MitchellNetravali" + } + + ListElement { + text: "CatmullRom" + } + + ListElement { + text: "Gaussian" + } + + ListElement { + text: "Lanczos" + } + } + + onActivated: updateImage() + + Settings { + id: settingsFilter + property alias currentIndex: comboBoxFilter.currentIndex + } + } + + Text { + text: "Quality: " + sliderQuality.value + } + + Slider { + id: sliderQuality + objectName: "sliderQuality" + stepSize: 1 + value: 75 + maximumValue: 100 + activeFocusOnPress: true + updateValueWhileDragging: false + enabled: (c.len > 0 && comboBoxFormat.currentText == "JPEG") ? true : false + onValueChanged: updateImage() + } + + CheckBox { + id: checkBoxGrayscale + objectName: "checkBoxGrayscale" + text: "Convert to Grayscale" + enabled: (c.len > 0) ? true : false + onClicked: updateImage() + } + + Settings { + id: settingsQuality + property alias value: sliderQuality.value + } + } + } + } + } + } + + FileDialog { + id: fileDialogFile + modality: Qt.WindowModal + title: "Add Files" + selectFolder: false + selectMultiple: true + selectExisting: true + sidebarVisible: true + nameFilters: [ "Comic files (*.rar *.zip *.7z *.gz *.bz2 *.cbr *.cbz *.cb7 *.cbt *.pdf *.epub *.xps)" ] + onAccepted: { + c.addUrls(decodeURIComponent(fileUrls.join("_CBSEP_"))) + } + } + + FileDialog { + id: fileDialogDir + modality: Qt.WindowModal + title: "Add Directory" + selectFolder: true + sidebarVisible: true + onAccepted: { + c.addUrls(decodeURIComponent(fileUrl.toString())) + } + } + + FileDialog { + id: fileDialogOutput + modality: Qt.WindowModal + title: "Output Directory" + selectFolder: true + sidebarVisible: true + onAccepted: { + textFieldOutDir.text = decodeURIComponent(fileUrl.toString().replace("file://", "")) + } + } + + Dialog { + id: levelsDialog + objectName: "levelsDialog" + title: "Levels" + standardButtons: StandardButton.Close + width: 230 + height: 150 + + ColumnLayout { + anchors.fill: parent + + Text { + text: "Input Levels:" + } + + RowLayout { + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + spacing: 5 + + SpinBox { + id: spinboxLevelsInMin + objectName: "spinboxLevelsInMin" + anchors.left: parent.left + anchors.leftMargin: 0 + stepSize: 1 + maximumValue: 255 + value: 0 + onEditingFinished: updateImage() + Keys.onReturnPressed: { + event.accepted = true + } + } + + SpinBox { + id: spinboxLevelsGamma + objectName: "spinboxLevelsGamma" + anchors.horizontalCenter: parent.horizontalCenter + decimals: 2 + stepSize: 0.01 + maximumValue: 10.00 + value: 1.00 + onEditingFinished: updateImage() + } + + SpinBox { + id: spinboxLevelsInMax + objectName: "spinboxLevelsInMax" + anchors.right: parent.right + anchors.rightMargin: 0 + stepSize: 1 + maximumValue: 255 + value: 255 + onEditingFinished: updateImage() + } + } + + Text { + text: "Output Levels:" + } + + RowLayout { + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + spacing: 5 + + SpinBox { + id: spinboxLevelsOutMin + objectName: "spinboxLevelsOutMin" + anchors.left: parent.left + anchors.leftMargin: 0 + stepSize: 1 + maximumValue: 255 + value: 0 + onEditingFinished: updateImage() + } + + SpinBox { + id: spinboxLevelsOutMax + objectName: "spinboxLevelsOutMax" + anchors.right: parent.right + anchors.rightMargin: 0 + stepSize: 1 + maximumValue: 255 + value: 255 + onEditingFinished: updateImage() + } + } + } + } + + statusBar: StatusBar { + + RowLayout { + anchors.fill: parent + + Label { + id: labelStatus + objectName: "labelStatus" + text: "Ready" + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + id: rectangle1 + Layout.fillWidth: true + anchors.verticalCenter: parent.verticalCenter + + Label { + id: labelPercent + objectName: "labelPercent" + font.pointSize: 9 + anchors.right: progressBar.left + anchors.rightMargin: 5 + anchors.verticalCenter: parent.verticalCenter + } + + ProgressBar { + id: progressBar + objectName: "progressBar" + visible: false + value: 0.0 + minimumValue : 0.0 + maximumValue : 100.0 + anchors.bottom: parent.bottom + anchors.bottomMargin: -10 + anchors.top: parent.top + anchors.topMargin: -10 + anchors.right: labelProgress.left + anchors.rightMargin: 5 + } + + Label { + id: labelProgress + objectName: "labelProgress" + font.pointSize: 9 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + +} diff --git a/gui/main.go b/gui/main.go new file mode 100644 index 0000000..5253979 --- /dev/null +++ b/gui/main.go @@ -0,0 +1,549 @@ +// 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 genqrc assets + +import ( + "bytes" + "compress/gzip" + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "mime" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "github.com/disintegration/imaging" + "github.com/dustin/go-humanize" + "github.com/gen2brain/cbconvert" + "github.com/gographics/imagick/imagick" + "github.com/hotei/bmp" + "golang.org/x/image/tiff" + "gopkg.in/qml.v1" +) + +// Model +type Comics struct { + Root qml.Object + Conv *cbconvert.Convertor + List []Comic + Len int +} + +// Comic Element +type Comic struct { + Name string + Path string + Type string + Size int64 + SizeHuman string +} + +// Sorts by name +type ByName []Comic + +func (c ByName) Len() int { return len(c) } +func (c ByName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c ByName) Less(i, j int) bool { return c[i].Name < c[j].Name } + +// Sorts by size +type BySize []Comic + +func (c BySize) Len() int { return len(c) } +func (c BySize) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c BySize) Less(i, j int) bool { return c[i].Size < c[j].Size } + +// Sorts by type +type ByType []Comic + +func (c ByType) Len() int { return len(c) } +func (c ByType) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c ByType) Less(i, j int) bool { return c[i].Type < c[j].Type } + +// Adds element to list +func (c *Comics) Add(comic Comic) { + c.List = append(c.List, comic) + c.Len = len(c.List) + qml.Changed(c, &c.Len) +} + +// Removes element from list +func (c *Comics) Remove(i int) { + l := c.List + l = append(l[:i], l[i+1:]...) + c.List = l + c.Len = len(c.List) + qml.Changed(c, &c.Len) +} + +// Removes all elements from list +func (c *Comics) RemoveAll() { + c.Len = 0 + c.List = make([]Comic, 0) + qml.Changed(c, &c.Len) +} + +// Sorts by name +func (c *Comics) ByName() { + sort.Sort(ByName(c.List)) + c.Len++ + qml.Changed(c, &c.Len) + c.Len-- + qml.Changed(c, &c.Len) +} + +// Sorts by size +func (c *Comics) BySize() { + sort.Sort(BySize(c.List)) + c.Len++ + qml.Changed(c, &c.Len) + c.Len-- + qml.Changed(c, &c.Len) +} + +// Sorts by type +func (c *Comics) ByType() { + sort.Sort(ByType(c.List)) + c.Len++ + qml.Changed(c, &c.Len) + c.Len-- + qml.Changed(c, &c.Len) +} + +// Returns element for given index +func (c *Comics) Get(i int) Comic { + return c.List[i] +} + +// Adds elements from fileUrls to list +func (c *Comics) AddUrls(u string) { + var args []string + l := strings.Split(u, "_CBSEP_") + re := regexp.MustCompile(`^[a-zA-Z]:`) + + for _, f := range l { + f = strings.Replace(f, "file://", "", -1) + f = re.ReplaceAllString(f, "") + f = re.ReplaceAllString(f, "") + args = append(args, f) + } + + c.Conv.Opts = c.GetOptions() + files := c.Conv.GetFiles(args) + + for _, file := range files { + stat, err := os.Stat(file) + if err != nil { + fmt.Fprintf(os.Stderr, "Error Stat AddUrls: %v\n", err.Error()) + continue + } + + m := mime.TypeByExtension(filepath.Ext(file)) + if m == "" && stat.IsDir() { + m = "inode/directory" + } + + c.Add(Comic{ + filepath.Base(file), + file, + m, + stat.Size(), + humanize.IBytes(uint64(stat.Size())), + }) + } +} + +// Returns cbconvert options from qml +func (c *Comics) GetOptions() cbconvert.Options { + var o cbconvert.Options + o.Quiet = true + + r := c.Root.ObjectByName("checkBoxRecursive") + o.Recursive = r.Bool("checked") + + r = c.Root.ObjectByName("checkBoxNoRGB") + o.RGB = !r.Bool("checked") + + r = c.Root.ObjectByName("checkBoxConvertCover") + o.ConvertCover = !r.Bool("checked") + + r = c.Root.ObjectByName("spinboxSize") + o.Size = r.Int64("value") + + r = c.Root.ObjectByName("sliderBrightness") + o.Brightness = r.Float64("value") + + r = c.Root.ObjectByName("sliderContrast") + o.Contrast = r.Float64("value") + + r = c.Root.ObjectByName("checkBoxGrayscale") + o.Grayscale = r.Bool("checked") + + r = c.Root.ObjectByName("comboBoxFlip") + o.Flip = strings.ToLower(r.String("currentText")) + + r = c.Root.ObjectByName("comboBoxRotate") + o.Rotate, _ = strconv.Atoi(r.String("currentText")) + + r = c.Root.ObjectByName("textFieldOutDir") + o.Outdir = r.String("text") + + r = c.Root.ObjectByName("textFieldSuffix") + o.Suffix = r.String("text") + + r = c.Root.ObjectByName("checkBoxNonImage") + o.NonImage = !r.Bool("checked") + + r = c.Root.ObjectByName("comboBoxFormat") + o.Format = strings.ToLower(r.String("currentText")) + + r = c.Root.ObjectByName("width") + o.Width, _ = strconv.Atoi(r.String("text")) + + r = c.Root.ObjectByName("height") + o.Height, _ = strconv.Atoi(r.String("text")) + + r = c.Root.ObjectByName("checkBoxFit") + o.Fit = r.Bool("checked") + + r = c.Root.ObjectByName("comboBoxFilter") + o.Filter = r.Int("currentIndex") + + r = c.Root.ObjectByName("sliderQuality") + o.Quality = int(r.Float64("value")) + + r = c.Root.ObjectByName("spinboxLevelsInMin") + o.LevelsInMin = r.Float64("value") + + r = c.Root.ObjectByName("spinboxLevelsInMax") + o.LevelsInMax = r.Float64("value") + + r = c.Root.ObjectByName("spinboxLevelsGamma") + o.LevelsGamma = r.Float64("value") + + r = c.Root.ObjectByName("spinboxLevelsOutMin") + o.LevelsOutMin = r.Float64("value") + + r = c.Root.ObjectByName("spinboxLevelsOutMax") + o.LevelsOutMax = r.Float64("value") + + return o +} + +// Sets "enabled" property +func (c *Comics) SetEnabled(b bool) { + c.Root.ObjectByName("checkBoxRecursive").Set("enabled", b) + c.Root.ObjectByName("checkBoxNoRGB").Set("enabled", b) + c.Root.ObjectByName("checkBoxConvertCover").Set("enabled", b) + c.Root.ObjectByName("spinboxSize").Set("enabled", b) + c.Root.ObjectByName("sliderBrightness").Set("enabled", b) + c.Root.ObjectByName("sliderContrast").Set("enabled", b) + c.Root.ObjectByName("checkBoxGrayscale").Set("enabled", b) + c.Root.ObjectByName("comboBoxFlip").Set("enabled", b) + c.Root.ObjectByName("comboBoxRotate").Set("enabled", b) + c.Root.ObjectByName("textFieldOutDir").Set("enabled", b) + c.Root.ObjectByName("textFieldSuffix").Set("enabled", b) + c.Root.ObjectByName("checkBoxNonImage").Set("enabled", b) + c.Root.ObjectByName("comboBoxFormat").Set("enabled", b) + c.Root.ObjectByName("width").Set("enabled", b) + c.Root.ObjectByName("height").Set("enabled", b) + c.Root.ObjectByName("checkBoxFit").Set("enabled", b) + c.Root.ObjectByName("comboBoxFilter").Set("enabled", b) + c.Root.ObjectByName("sliderQuality").Set("enabled", b) + c.Root.ObjectByName("buttonAddFile").Set("enabled", b) + c.Root.ObjectByName("buttonAddDir").Set("enabled", b) + c.Root.ObjectByName("buttonRemove").Set("enabled", b) + c.Root.ObjectByName("buttonRemoveAll").Set("enabled", b) + c.Root.ObjectByName("buttonThumbnail").Set("enabled", b) + c.Root.ObjectByName("buttonCover").Set("enabled", b) + c.Root.ObjectByName("buttonConvert").Set("enabled", b) +} + +// Converts comic +func (c *Comics) Convert() { + c.Conv.Opts = c.GetOptions() + c.Conv.Nfiles = c.Len + c.Conv.CurrFile = 0 + + c.SetEnabled(false) + + go func() { + for _, e := range c.List { + stat, err := os.Stat(e.Path) + if err != nil { + fmt.Fprintf(os.Stderr, "Error Stat Convert: %v\n", err.Error()) + continue + } + c.Conv.ConvertComic(e.Path, stat) + } + }() + + go c.showProgress(true, "Converting...") +} + +// Extracts cover +func (c *Comics) Cover() { + c.Conv.Opts = c.GetOptions() + c.Conv.Nfiles = c.Len + c.Conv.CurrFile = 0 + + c.SetEnabled(false) + + go func() { + for _, e := range c.List { + stat, err := os.Stat(e.Path) + if err != nil { + fmt.Fprintf(os.Stderr, "Error Stat Cover: %v\n", err.Error()) + continue + } + c.Conv.ExtractCover(e.Path, stat) + } + }() + + go c.showProgress(false, "Extracting...") +} + +// Extracts thumbnail +func (c *Comics) Thumbnail() { + c.Conv.Opts = c.GetOptions() + c.Conv.Nfiles = c.Len + c.Conv.CurrFile = 0 + + c.SetEnabled(false) + + go func() { + for _, e := range c.List { + stat, err := os.Stat(e.Path) + if err != nil { + fmt.Fprintf(os.Stderr, "Error Stat Thumbnail: %v\n", err.Error()) + continue + } + c.Conv.ExtractThumbnail(e.Path, stat) + } + }() + + go c.showProgress(false, "Extracting...") +} + +// Shows progress +func (c *Comics) showProgress(cn bool, text string) { + c.Root.ObjectByName("labelStatus").Set("text", text) + c.Root.ObjectByName("progressBar").Set("visible", true) + + for { + if c.Conv.CurrFile == c.Conv.Nfiles { + if c.Conv.CurrContent == c.Conv.Ncontents { + c.Root.ObjectByName("progressBar").Set("value", 0) + c.Root.ObjectByName("labelProgress").Set("text", "") + c.Root.ObjectByName("labelStatus").Set("text", "Ready") + c.Root.ObjectByName("labelPercent").Set("text", "") + c.Root.ObjectByName("progressBar").Set("visible", false) + c.SetEnabled(true) + break + } + } + + var count, current int + if cn { + count = c.Conv.Ncontents + current = c.Conv.CurrContent + } else { + count = c.Conv.Nfiles + current = c.Conv.CurrFile + } + + value := float64(current) / float64(count) * 100 + c.Root.ObjectByName("progressBar").Set("value", float64(value)) + c.Root.ObjectByName("labelPercent").Set("text", + fmt.Sprintf("%d/%d %.0f%%", current, count, float64(value))) + c.Root.ObjectByName("labelProgress").Set("text", + fmt.Sprintf("File %d of %d", c.Conv.CurrFile, c.Conv.Nfiles)) + + time.Sleep(500 * time.Millisecond) + } +} + +// Provides image://cover/ +func (c *Comics) CoverProvider(file string, width int, height int) image.Image { + c.Conv.Opts = c.GetOptions() + + stat, err := os.Stat(file) + if err != nil { + fmt.Fprintf(os.Stderr, "Error Stat CoverProvider: %v\n", err.Error()) + return image.NewRGBA(image.Rect(0, 0, width, height)) + } + + cover, err := c.Conv.GetCoverImage(file, stat) + if err != nil { + fmt.Fprintf(os.Stderr, "Error GetCoverImage: %v\n", err.Error()) + return image.NewRGBA(image.Rect(0, 0, width, height)) + } + + cover = c.Conv.TransformImage(cover) + + if c.Conv.Opts.LevelsInMin != 0 || c.Conv.Opts.LevelsInMax != 255 || c.Conv.Opts.LevelsGamma != 1.00 || + c.Conv.Opts.LevelsOutMin != 0 || c.Conv.Opts.LevelsOutMax != 255 { + cover = c.Conv.LevelImage(cover) + } + + // imaging is used for preview only + if c.Conv.Opts.Grayscale { + cover = imaging.Grayscale(cover) + } + + // size preview + s := 0 + b := new(bytes.Buffer) + + w := 0 + h := 0 + + switch c.Conv.Opts.Format { + case "jpeg": + jpeg.Encode(b, cover, &jpeg.Options{c.Conv.Opts.Quality}) + s = len(b.Bytes()) + cover, _ = jpeg.Decode(bytes.NewReader(b.Bytes())) + config, _, _ := image.DecodeConfig(bytes.NewReader(b.Bytes())) + w = config.Width + h = config.Height + case "png": + png.Encode(b, cover) + s = len(b.Bytes()) + cover, _ = png.Decode(bytes.NewReader(b.Bytes())) + config, _, _ := image.DecodeConfig(bytes.NewReader(b.Bytes())) + w = config.Width + h = config.Height + case "gif": + mw := imagick.NewMagickWand() + defer mw.Destroy() + + mw.ReadImageBlob(c.Conv.GetImageBytes(cover)) + mw.SetImageFormat("GIF") + blob := mw.GetImageBlob() + + s = len(blob) + cover, _ = gif.Decode(bytes.NewReader(blob)) + config, _, _ := image.DecodeConfig(bytes.NewReader(blob)) + w = config.Width + h = config.Height + case "tiff": + tiff.Encode(b, cover, &tiff.Options{tiff.Uncompressed, false}) + + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + gz.Write(b.Bytes()) + gz.Close() + + s = buf.Len() + cover, _ = tiff.Decode(bytes.NewReader(b.Bytes())) + config, _, _ := image.DecodeConfig(bytes.NewReader(b.Bytes())) + w = config.Width + h = config.Height + case "bmp": + mw := imagick.NewMagickWand() + defer mw.Destroy() + + bb := c.Conv.GetImageBytes(cover) + mw.ReadImageBlob(bb) + + wand := imagick.NewPixelWand() + wand.SetColor("black") + defer wand.Destroy() + + mw.SetImageFormat("BMP3") + mw.SetImageBackgroundColor(wand) + 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(), 8, true, true) + + var buf bytes.Buffer + blob := mw.GetImageBlob() + gz := gzip.NewWriter(&buf) + gz.Write(blob) + gz.Close() + + s = buf.Len() + cover, _ = bmp.Decode(bytes.NewReader(blob)) + config, _, _ := image.DecodeConfig(bytes.NewReader(bb)) + w = config.Width + h = config.Height + } + + if cover == nil { + return image.NewRGBA(image.Rect(0, 0, width, height)) + } + + human := humanize.IBytes(uint64(s)) + c.Root.ObjectByName("sizePreview").Set("text", fmt.Sprintf("%s (%dx%d)", human, w, h)) + + return cover +} + +func run() error { + qml.SetWindowIcon(":///assets/icon.png") + + engine := qml.NewEngine() + + c := &Comics{} + engine.Context().SetVar("c", c) + + engine.AddImportPath("qrc:/assets") + engine.AddImageProvider("cover", c.CoverProvider) + + q, err := engine.LoadFile("qrc:///assets/main.qml") + if err != nil { + return err + } + + window := q.CreateWindow(nil) + c.Root = window.Root() + c.Conv = cbconvert.NewConvertor(c.GetOptions()) + + c.Root.On("closing", func(o qml.Object) { + os.RemoveAll(c.Conv.Workdir) + }) + + c.Root.ObjectByName("buttonConvert").On("clicked", c.Convert) + c.Root.ObjectByName("buttonCover").On("clicked", c.Cover) + c.Root.ObjectByName("buttonThumbnail").On("clicked", c.Thumbnail) + + // center window + x := c.Root.Int("screenWidth")/2 - c.Root.Int("width")/2 + y := c.Root.Int("screenHeight")/2 - c.Root.Int("height")/2 + window.Set("x", x) + window.Set("y", y) + + window.Show() + window.Wait() + return nil +} + +func main() { + if err := qml.Run(run); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} diff --git a/gui/make.bash b/gui/make.bash new file mode 100755 index 0000000..c24962e --- /dev/null +++ b/gui/make.bash @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +CHROOT="/home/milann/chroot" +MINGW="/usr/i686-w64-mingw32" + +mkdir -p build +rm -f resource.syso + +go generate + +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 -L$CHROOT/usr/plugins/generic -L$CHROOT/usr/plugins/platforms -L$CHROOT/usr/plugins/qmltooling -L$CHROOT/usr/qml/QtQuick.2 -L$CHROOT/usr/qml/QtQuick/Controls -L$CHROOT/usr/qml/QtQuick/Dialogs -L$CHROOT/usr/qml/QtQuick/Layouts -L$CHROOT/usr/qml/QtQuick/Window.2 -L$CHROOT/usr/qml/Qt/labs/settings -L$CHROOT/usr/qml/QtQuick/PrivateWidgets -L$CHROOT/usr/plugins/xcbglintegrations" \ +CGO_LDFLAGS="$CGO_LDFLAGS -lqxcb -lqtquick2plugin -lqtquickcontrolsplugin -ldialogplugin -lqquicklayoutsplugin -lwindowplugin -lqmlsettingsplugin -lwidgetsplugin -lqxcb-glx-integration -lqevdevkeyboardplugin -lqevdevmouseplugin" \ +CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -v -x -o build/cbconvert +strip build/cbconvert + +goversioninfo -icon=assets/icon.ico + +PKG_CONFIG="/usr/bin/i686-w64-mingw32-pkg-config" \ +PKG_CONFIG_PATH="$MINGW/usr/lib/pkgconfig:$MINGW/usr/lib/pkgconfig" \ +PKG_CONFIG_LIBDIR="$MINGW/usr/lib/pkgconfig:$MINGW/usr/lib/pkgconfig" \ +CGO_LDFLAGS="-L$MINGW/usr/lib -L$MINGW/lib -L$MINGW/usr/plugins/generic -L$MINGW/usr/plugins/platforms -L$MINGW/usr/qml/Qt/labs/folderlistmodel -L$MINGW/usr/plugins/qmltooling -L$MINGW/usr/qml/QtQuick.2 -L$MINGW/usr/qml/QtQuick/Controls -L$MINGW/usr/qml/QtQuick/Dialogs -L$MINGW/usr/qml/QtQuick/Dialogs/Private -L$MINGW/usr/qml/QtQuick/Layouts -L$MINGW/usr/qml/QtQuick/Window.2 -L$MINGW/usr/qml/Qt/labs/settings -L$MINGW/usr/qml/QtQuick/PrivateWidgets" \ +CGO_LDFLAGS="$CGO_LDFLAGS -lqwindows -lqtquick2plugin -lqtquickcontrolsplugin -lqmlfolderlistmodelplugin -ldialogplugin -ldialogsprivateplugin -lqquicklayoutsplugin -lwindowplugin -lqmlsettingsplugin -lwidgetsplugin" \ +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 "-H=windowsgui -linkmode external '-extldflags=-static -Wl,--allow-multiple-definition'" +i686-w64-mingw32-strip build/cbconvert.exe diff --git a/gui/versioninfo.json b/gui/versioninfo.json new file mode 100644 index 0000000..ca5454c --- /dev/null +++ b/gui/versioninfo.json @@ -0,0 +1,44 @@ +{ + "FixedFileInfo": + { + "FileVersion": { + "Major": 0, + "Minor": 5, + "Patch": 0, + "Build": 0 + }, + "ProductVersion": { + "Major": 0, + "Minor": 5, + "Patch": 0, + "Build": 0 + }, + "FileFlagsMask": "3f", + "FileFlags ": "00", + "FileOS": "040004", + "FileType": "01", + "FileSubType": "00" + }, + "StringFileInfo": + { + "Comments": "Comic Book converter", + "CompanyName": "", + "FileDescription": "CBconvert GUI", + "FileVersion": "0.5.0", + "InternalName": "", + "LegalCopyright": "", + "LegalTrademarks": "", + "OriginalFilename": "cbconvert.exe", + "PrivateBuild": "", + "ProductName": "CBconvert", + "ProductVersion": "0.5.0", + "SpecialBuild": "" + }, + "VarFileInfo": + { + "Translation": { + "LangID": "0409", + "CharsetID": "04B0" + } + } +}