This commit is contained in:
Milan Nikolic
2015-12-02 20:46:42 +01:00
parent c8d472e576
commit b930b06a7d
11 changed files with 1922 additions and 156 deletions

View File

@@ -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 [<flags>] <command> [<args> ...]
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:
<args> filename or directory
@@ -53,23 +54,26 @@ Using
convert [<flags>] <args>...
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 [<flags>] <args>...
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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

44
cmd/versioninfo.json Normal file
View File

@@ -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"
}
}
}

BIN
gui/assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
gui/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

965
gui/assets/main.qml Normal file
View File

@@ -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
}
}
}
}
}

549
gui/main.go Normal file
View File

@@ -0,0 +1,549 @@
// Author: Milan Nikolic <gen2brain@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
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)
}
}

32
gui/make.bash Executable file
View File

@@ -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

44
gui/versioninfo.json Normal file
View File

@@ -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"
}
}
}