26 Commits

Author SHA1 Message Date
Milan Nikolic 28ef6910e4 new release 2015-11-05 07:51:47 +01:00
Milan Nikolic 35b8bc340f man page info 2015-11-05 07:46:27 +01:00
Milan Nikolic a5a45306c6 update version 2015-11-05 07:38:48 +01:00
Milan Nikolic b063367110 add progressbar for cover 2015-11-05 07:06:03 +01:00
Milan Nikolic 7831e487e6 add fit for cover and thumbnail 2015-11-05 06:41:09 +01:00
Milan Nikolic ce32d48469 Update README.md 2015-11-05 06:16:39 +01:00
Milan Nikolic 4d1e0a44d2 add brightness and contrast options 2015-11-05 06:11:40 +01:00
Milan Nikolic 574e364954 add support for tiff format 2015-11-05 05:50:11 +01:00
Milan Nikolic 97aae5e0c7 best fit option 2015-11-05 05:06:45 +01:00
Milan Nikolic bbcaf0f9dd add encodeImage and encodeImageMagick 2015-11-05 04:53:35 +01:00
Milan Nikolic d70de9f4db add options to rotate and flip images 2015-11-04 14:20:37 +01:00
Milan Nikolic d7be2f4d5e add kingpin commands 2015-11-04 12:58:26 +01:00
Milan Nikolic 82e8a6cbac add option to remove non-image files from archive 2015-11-04 00:28:18 +01:00
Milan Nikolic 85818eec5f add grayscale option 2015-11-03 22:13:21 +01:00
Milan Nikolic 0c6b9fcaa8 use imaging instead of resize 2015-11-03 21:39:30 +01:00
Milan Nikolic 55b53efcca rename interp. to resize 2015-11-03 20:13:25 +01:00
Milan Nikolic 4abf99a644 add gif support 2015-11-03 19:47:24 +01:00
Milan Nikolic ca4f13ebfe Update README.md 2015-11-03 14:44:42 +01:00
Milan Nikolic a4c7b87dc0 add tiff and webp support 2015-11-03 13:37:02 +01:00
Milan Nikolic dd4c3a2dbf Update README.md 2015-11-02 07:58:35 +01:00
Milan Nikolic 8e3f7962d1 linux static build 2015-11-02 07:55:29 +01:00
Milan Nikolic 1c3421028b Update README.md 2015-11-02 05:53:37 +01:00
Milan Nikolic 5ddb4d92aa Update README.md 2015-11-02 05:47:46 +01:00
Milan Nikolic b1e9763b0b use MuPDF instead of Poppler 2015-11-02 05:40:40 +01:00
Milan Nikolic 14bfaf7b6e Update README.md 2015-11-01 14:05:08 +01:00
Milan Nikolic 3dcfc1dd5e Update README.md 2015-11-01 14:03:54 +01:00
3 changed files with 477 additions and 218 deletions
+113 -52
View File
@@ -4,78 +4,92 @@ CBconvert
Introduction Introduction
------------ ------------
CBconvert is a [Comic Book](http://en.wikipedia.org/wiki/Comic_Book_Archive_file) convert tool. CBconvert is a [Comic Book](http://en.wikipedia.org/wiki/Comic_Book_Archive_file) converter.
It allows you to convert individual comics or bulk convert comics to different formats to fit your various devices.
Features Features
-------- --------
- reads rar, zip, 7z, gz, bz2, cbr, cbz, cb7, cbt, pdf and plain directory - reads RAR, ZIP, 7Z, GZ, BZ2, CBR, CBZ, CB7, CBT, PDF, EPUB, XPS and plain directory
- always saves processed comic in cbz (zip) format - always saves processed comic in CBZ (ZIP) archive format
- images can be converted to JPEG, PNG or 4-Bit BMP (16 colors) format - images can be converted to JPEG, PNG, GIF, TIFF or 4-Bit BMP (16 colors) file format
- choose resize algorithm (NearestNeighbor, Bilinear, Bicubic, MitchellNetravali, Lanczos2/3) - choose resize algorithm (NearestNeighbor, Box, Linear, MitchellNetravali, CatmullRom, Gaussian, Lanczos)
- rotate, flip, adjust brightness/contrast or grayscale images
- export covers from comics - export covers from comics
- create thumbnails from covers by [freedesktop](http://www.freedesktop.org/wiki/) specification - create thumbnails from covers by [freedesktop](http://www.freedesktop.org/wiki/) specification
Download Download
-------- --------
- [Windows static build](https://github.com/gen2brain/cbconvert/releases/download/0.1.0/cbconvert-0.1.0.zip) - [Windows binary](https://github.com/gen2brain/cbconvert/releases/download/0.4.0/cbconvert-0.4.0.zip)
- [Linux 64bit build](https://github.com/gen2brain/cbconvert/releases/download/0.1.0/cbconvert-0.1.0.tar.gz)
Compile - [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)
Install poppler, poppler-glib, cairo and imagemagick dev packages:
apt-get install libpoppler-glib-dev libcairo2-dev libmagickcore-dev libmagickwand-dev
Install go package:
go get github.com/gen2brain/cbconvert
go install github.com/gen2brain/cbconvert && cbconvert
Dependencies
------------
go get github.com/cheggaaa/go-poppler
go get github.com/cheggaaa/pb
go get github.com/gen2brain/go-unarr
go get github.com/gographics/imagick/imagick
go get github.com/hotei/bmp
go get github.com/nfnt/resize
go get github.com/skarademir/naturalsort
go get github.com/ungerik/go-cairo
go get gopkg.in/alecthomas/kingpin.v2
Using Using
----- -----
usage: cbconvert [<flags>] <args>... usage: cbconvert [<flags>] <command> [<args> ...]
Comic Book convert tool. Comic Book convert tool.
Flags: Flags:
--help Show context-sensitive help (also try --help-long and --help-man). --help Show context-sensitive help (also try --help-long and --help-man).
--version Show application version. --version Show application version.
-p, --png encode images to PNG instead of JPEG --outdir="." Output directory
-b, --bmp encode images to 4-Bit BMP instead of JPEG --size=0 Process only files larger then size (in MB)
-w, --width=0 image width --recursive Process subdirectories recursively
-h, --height=0 image height --quiet Hide console output
-q, --quality=75 JPEG image quality
-n, --norgb do not convert images with RGB colorspace
-i, --interpolation=1 0=NearestNeighbor, 1=Bilinear, 2=Bicubic, 3=MitchellNetravali, 4=Lanczos2, 5=Lanczos3
-s, --suffix=SUFFIX add suffix to file basename
-c, --cover extract cover
-t, --thumbnail extract cover thumbnail (freedesktop spec.)
-o, --outdir="." output directory
-m, --size=0 process only files larger then size (in MB)
-r, --recursive process subdirectories recursively
-Q, --quiet hide console output
Args: Args:
<args> filename or directory <args> filename or directory
Commands:
help [<command>...]
Show help.
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
cover [<flags>] <args>...
Extract cover
--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
thumbnail [<flags>] <args>...
Extract cover thumbnail (freedesktop spec.)
--width=0 Image width
--height=0 Image height
--fit Best fit for required width and height
--filter=2 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos
[man page](https://en.wikipedia.org/wiki/Man_page) is also available:
cbconvert --help-man | man /dev/stdin
Examples Examples
-------- --------
@@ -84,12 +98,59 @@ Rescale images to 1200px for all supported files found in directory with size la
cbconvert --recursive --width 1200 --size 60 /media/comics/Thorgal/ cbconvert --recursive --width 1200 --size 60 /media/comics/Thorgal/
Convert all images in archive to 4bit BMP image and save result in ~/comics directory: Convert all images in pdf to 4bit BMP image and save result in ~/comics directory:
cbconvert --bmp --outdir ~/comics /media/comics/Garfield/Garfield_01.cbz cbconvert --bmp --outdir ~/comics /media/comics/Garfield/Garfield_01.pdf
[BMP](http://en.wikipedia.org/wiki/BMP_file_format) format is uncompressed, for black&white pages very good choice. Archive size can be smaller 2-3x and file will be readable by comic readers. [BMP](http://en.wikipedia.org/wiki/BMP_file_format) format is very good choice for black&white pages. Archive size can be smaller 2-3x and file will be readable by comic readers.
Generate thumbnails by freedesktop specification in ~/.thumbnails/normal directory, Lanczos3 algorithm is used for resizing: Generate thumbnails by freedesktop specification in ~/.thumbnails/normal directory with width 512:
cbconvert --interpolation=5 --outdir ~/.thumbnails/normal --thumbnail /media/comics/GrooTheWanderer/ cbconvert thumbnail --width 512 --outdir ~/.thumbnails/normal /media/comics/GrooTheWanderer/
Extract covers to ~/covers dir for all supported files found in directory, Lanczos algorithm is used for resizing:
cbconvert cover --outdir ~/covers --filter=7 /media/comics/GrooTheWanderer/
Compile
-------
Install imagemagick dev packages:
apt-get install libmagickcore-dev libmagickwand-dev
Compile latest MuPDF:
git clone git://git.ghostscript.com/mupdf.git && cd mupdf
git submodule update --init --recursive
curl -L https://gist.githubusercontent.com/gen2brain/7869ac4c6db5933f670f/raw/1619394dc957ae10bcd73c713760993466b4bfea/mupdf-openssl-curl.patch | patch -p1
sed -e "1iHAVE_X11 = no" -e "1iWANT_OPENSSL = no" -e "1iWANT_CURL = no" -i Makerules
HAVE_X11=no HAVE_GLFW=no HAVE_GLUT=no WANT_OPENSSL=no WANT_CURL=no HAVE_MUJS=yes HAVE_JSCORE=no HAVE_V8=no make && make install
Compile unarr library:
git clone https://github.com/zeniko/unarr && cd unarr
mkdir lzma920 && cd lzma920 && curl -L http://www.7-zip.org/a/lzma920.tar.bz2 | tar -xjvp && cd ..
curl -L http://zlib.net/zlib-1.2.8.tar.gz | tar -xzvp
curl -L http://www.bzip.org/1.0.6/bzip2-1.0.6.tar.gz | tar -xzvp
curl -L https://gist.githubusercontent.com/gen2brain/89fe506863be3fb139e8/raw/8783a7d81e22ad84944d146c5e33beab6dffc641/unarr-makefile.patch | patch -p1
CFLAGS="-DHAVE_7Z -DHAVE_ZLIB -DHAVE_BZIP2 -I./lzma920/C -I./zlib-1.2.8 -I./bzip2-1.0.6" make
cp build/debug/libunarr.a /usr/lib64/ && cp unarr.h /usr/include
Install dependencies:
go get github.com/cheggaaa/pb
go get github.com/disintegration/imaging
go get github.com/gen2brain/go-fitz
go get github.com/gen2brain/go-unarr
go get github.com/gographics/imagick/imagick
go get github.com/hotei/bmp
go get github.com/skarademir/naturalsort
go get golang.org/x/image/tiff
go get golang.org/x/image/webp
go get gopkg.in/alecthomas/kingpin.v2
Install go package:
go get github.com/gen2brain/cbconvert
go install github.com/gen2brain/cbconvert
+344 -149
View File
@@ -23,7 +23,7 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
_ "image/gif" "image/gif"
"image/jpeg" "image/jpeg"
"image/png" "image/png"
"io" "io"
@@ -39,42 +39,76 @@ import (
"sync" "sync"
"syscall" "syscall"
"github.com/cheggaaa/go-poppler"
"github.com/cheggaaa/pb" "github.com/cheggaaa/pb"
"github.com/disintegration/imaging"
"github.com/gen2brain/go-fitz"
"github.com/gen2brain/go-unarr" "github.com/gen2brain/go-unarr"
"github.com/gographics/imagick/imagick" "github.com/gographics/imagick/imagick"
_ "github.com/hotei/bmp" _ "github.com/hotei/bmp"
"github.com/nfnt/resize"
"github.com/skarademir/naturalsort" "github.com/skarademir/naturalsort"
"golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
"gopkg.in/alecthomas/kingpin.v2" "gopkg.in/alecthomas/kingpin.v2"
) )
var ( // Resample filters
opts options const (
workdir string NearestNeighbor int = iota // Fastest resampling filter, no antialiasing
nfiles int Box // Box filter (averaging pixels)
current int Linear // Bilinear filter, smooth and reasonably fast
wg sync.WaitGroup MitchellNetravali // А smooth bicubic filter
CatmullRom // A sharp bicubic filter
Gaussian // Blurring filter that uses gaussian function, useful for noise removal
Lanczos // High-quality resampling filter, it's slower than cubic filters
) )
// Command line options var filters = map[int]imaging.ResampleFilter{
type options struct { NearestNeighbor: imaging.NearestNeighbor,
Box: imaging.Box,
Linear: imaging.Linear,
MitchellNetravali: imaging.MitchellNetravali,
CatmullRom: imaging.CatmullRom,
Gaussian: imaging.Gaussian,
Lanczos: imaging.Lanczos,
}
// Options
type Options struct {
ToPNG bool // encode images to PNG instead of JPEG ToPNG bool // encode images to PNG instead of JPEG
ToBMP bool // encode images to 4-Bit BMP 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 Quality int // JPEG image quality
NoRGB bool // do not convert images with RGB colorspace Width int // image width
Width uint // image width Height int // image height
Height uint // image height Fit bool // Best fit for required width and height
Interpolation int // 0=NearestNeighbor, 1=Bilinear, 2=Bicubic, 3=MitchellNetravali, 4=Lanczos2, 5=Lanczos3 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 Suffix string // add suffix to file basename
Cover bool // extract cover Cover bool // extract cover
Thumbnail bool // extract cover thumbnail (freedesktop spec.) Thumbnail bool // extract cover thumbnail (freedesktop spec.)
Outdir string // output directory 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 Recursive bool // process subdirectories recursively
Size int64 // process only files larger then size (in MB) Size int64 // process only files larger then size (in MB)
Quiet bool // hide console output Quiet bool // hide console output
} }
// Globals
var (
opts Options
workdir string
nfiles int
current int
bar *pb.ProgressBar
wg sync.WaitGroup
)
// Command line arguments // Command line arguments
var arguments []string var arguments []string
@@ -90,6 +124,10 @@ func convertImage(img image.Image, index int, pathName string) {
ext = "png" ext = "png"
} else if opts.ToBMP { } else if opts.ToBMP {
ext = "bmp" ext = "bmp"
} else if opts.ToGIF {
ext = "gif"
} else if opts.ToTIFF {
ext = "tiff"
} }
var filename string var filename string
@@ -99,74 +137,89 @@ func convertImage(img image.Image, index int, pathName string) {
filename = filepath.Join(workdir, fmt.Sprintf("%03d.%s", index, ext)) filename = filepath.Join(workdir, fmt.Sprintf("%03d.%s", index, ext))
} }
var i image.Image
if opts.Width > 0 || opts.Height > 0 {
i = resize.Resize(opts.Width, opts.Height, img,
resize.InterpolationFunction(opts.Interpolation))
} else {
i = img
}
if opts.ToPNG { if opts.ToPNG {
// convert image to PNG // convert image to PNG
f, err := os.Create(filename) if opts.Grayscale {
if err != nil { encodeImageMagick(img, filename)
fmt.Fprintf(os.Stderr, "Error Create: %v\n", err.Error()) } else {
encodeImage(img, filename)
} }
defer f.Close()
png.Encode(f, i)
} else if opts.ToBMP { } else if opts.ToBMP {
// convert image to 4-Bit - 16 colors BMP // convert image to 4-Bit BMP (16 colors)
imagick.Initialize() encodeImageMagick(img, filename)
} else if opts.ToGIF {
mw := imagick.NewMagickWand() // convert image to GIF
defer mw.Destroy() encodeImageMagick(img, filename)
} else if opts.ToTIFF {
b := new(bytes.Buffer) // convert image to TIFF
jpeg.Encode(b, i, &jpeg.Options{jpeg.DefaultQuality}) encodeImage(img, filename)
err := mw.ReadImageBlob(b.Bytes())
if err != nil {
fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error())
}
w := imagick.NewPixelWand()
w.SetColor("black")
defer w.Destroy()
mw.SetImageBackgroundColor(w)
mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_REMOVE)
mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_DEACTIVATE)
mw.SetImageMatte(false)
mw.SetImageCompression(imagick.COMPRESSION_NO)
mw.QuantizeImage(16, imagick.COLORSPACE_SRGB, 8, true, true)
mw.WriteImage(fmt.Sprintf("BMP3:%s", filename))
} else { } else {
// convert image to JPEG (default) // convert image to JPEG (default)
f, err := os.Create(filename) if opts.Grayscale {
if err != nil { encodeImageMagick(img, filename)
fmt.Fprintf(os.Stderr, "Error Create: %v\n", err.Error()) } else {
encodeImage(img, filename)
} }
defer f.Close()
jpeg.Encode(f, i, &jpeg.Options{opts.Quality})
} }
<-throttle <-throttle
} }
// Converts pdf file to cbz // Transforms image (resize, rotate, flip, brightness, contrast)
func convertPDF(file string) { func transformImage(img image.Image) image.Image {
var i image.Image = img
if opts.Width > 0 || opts.Height > 0 {
if opts.Fit {
i = imaging.Fit(i, opts.Width, opts.Height, filters[opts.Filter])
} else {
i = imaging.Resize(i, opts.Width, opts.Height, filters[opts.Filter])
}
}
if opts.Rotate > 0 {
switch opts.Rotate {
case 90:
i = imaging.Rotate90(i)
case 180:
i = imaging.Rotate180(i)
case 270:
i = imaging.Rotate270(i)
}
}
if opts.Flip != "none" {
switch opts.Flip {
case "horizontal":
i = imaging.FlipH(i)
case "vertical":
i = imaging.FlipV(i)
}
}
if opts.Brightness != 0 {
i = imaging.AdjustBrightness(i, opts.Brightness)
}
if opts.Contrast != 0 {
i = imaging.AdjustContrast(i, opts.Contrast)
}
return i
}
// Converts PDF/EPUB/XPS document to CBZ
func convertDocument(file string) {
workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") workdir, _ = ioutil.TempDir(os.TempDir(), "cbc")
doc, err := poppler.Open(file) doc, err := fitz.NewDocument(file)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Skipping %s, error: %v", file, err.Error()) fmt.Fprintf(os.Stderr, "Skipping %s, error: %v", file, err.Error())
return return
} }
npages := doc.GetNPages() npages := doc.Pages()
var bar *pb.ProgressBar
if !opts.Quiet { if !opts.Quiet {
bar = pb.New(npages) bar = pb.New(npages)
bar.ShowTimeLeft = false bar.ShowTimeLeft = false
@@ -179,23 +232,23 @@ func convertPDF(file string) {
bar.Increment() bar.Increment()
} }
page := doc.GetPage(n) img, err := doc.Image(n)
images := page.Images()
if len(images) == 1 { if err == nil {
img = transformImage(img)
}
if img != nil {
throttle <- 1 throttle <- 1
wg.Add(1) wg.Add(1)
surface := images[0].GetSurface() go convertImage(img, n, "")
go convertImage(surface.GetImage(), page.Index(), "")
} else {
// FIXME merge images?
} }
} }
wg.Wait() wg.Wait()
} }
// Converts archive to cbz // Converts archive to CBZ
func convertArchive(file string) { func convertArchive(file string) {
workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") workdir, _ = ioutil.TempDir(os.TempDir(), "cbc")
@@ -207,7 +260,6 @@ func convertArchive(file string) {
} }
defer archive.Close() defer archive.Close()
var bar *pb.ProgressBar
if !opts.Quiet { if !opts.Quiet {
bar = pb.New(ncontents) bar = pb.New(ncontents)
bar.ShowTimeLeft = false bar.ShowTimeLeft = false
@@ -254,30 +306,33 @@ func convertArchive(file string) {
continue continue
} }
if opts.NoRGB && !isGrayScale(img) { i := transformImage(img)
copyFile(bytes.NewReader(buf), filepath.Join(workdir, filepath.Base(pathname)))
if !opts.RGB && !isGrayScale(i) {
encodeImage(i, filepath.Join(workdir, filepath.Base(pathname)))
continue continue
} }
if img != nil { if i != nil {
throttle <- 1 throttle <- 1
wg.Add(1) wg.Add(1)
go convertImage(img, 0, pathname) go convertImage(i, 0, pathname)
} }
} else { } else {
if opts.NonImage {
copyFile(bytes.NewReader(buf), filepath.Join(workdir, filepath.Base(pathname))) copyFile(bytes.NewReader(buf), filepath.Join(workdir, filepath.Base(pathname)))
} }
} }
}
wg.Wait() wg.Wait()
} }
// Converts directory to cbz // Converts directory to CBZ
func convertDirectory(path string) { func convertDirectory(path string) {
workdir, _ = ioutil.TempDir(os.TempDir(), "cbc") workdir, _ = ioutil.TempDir(os.TempDir(), "cbc")
images := getImages(path) images := getImages(path)
var bar *pb.ProgressBar
if !opts.Quiet { if !opts.Quiet {
bar = pb.New(nfiles) bar = pb.New(nfiles)
bar.ShowTimeLeft = false bar.ShowTimeLeft = false
@@ -302,8 +357,10 @@ func convertDirectory(path string) {
continue continue
} }
if opts.NoRGB && !isGrayScale(i) { i = transformImage(i)
copyFile(f, filepath.Join(workdir, filepath.Base(img)))
if !opts.RGB && !isGrayScale(i) {
encodeImage(i, filepath.Join(workdir, filepath.Base(img)))
continue continue
} }
@@ -318,7 +375,7 @@ func convertDirectory(path string) {
wg.Wait() wg.Wait()
} }
// Saves workdir to cbz archive // Saves workdir to CBZ archive
func saveArchive(file string) { func saveArchive(file string) {
defer os.RemoveAll(workdir) defer os.RemoveAll(workdir)
@@ -333,7 +390,6 @@ func saveArchive(file string) {
z := zip.NewWriter(zipfile) z := zip.NewWriter(zipfile)
files, _ := ioutil.ReadDir(workdir) files, _ := ioutil.ReadDir(workdir)
var bar *pb.ProgressBar
if !opts.Quiet { if !opts.Quiet {
bar = pb.New(len(files)) bar = pb.New(len(files))
bar.ShowTimeLeft = false bar.ShowTimeLeft = false
@@ -362,6 +418,95 @@ func saveArchive(file string) {
z.Close() z.Close()
} }
// Decodes image from reader
func decodeImage(reader io.Reader, filename string) (i image.Image, err error) {
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "Recovered in decodeImage %s: %v\n", filename, r)
}
}()
i, _, err = image.Decode(reader)
return i, err
}
// Encode image to file
func encodeImage(i image.Image, filename string) (err error) {
f, err := os.Create(filename)
if err != nil {
return
}
switch filepath.Ext(filename) {
case ".png":
err = png.Encode(f, i)
case ".tif":
case ".tiff":
err = tiff.Encode(f, i, &tiff.Options{tiff.Uncompressed, false})
case ".gif":
err = gif.Encode(f, i, nil)
default:
err = jpeg.Encode(f, i, &jpeg.Options{opts.Quality})
}
f.Close()
return
}
// Encode image to file (ImageMagick)
func encodeImageMagick(i image.Image, filename string) (err error) {
imagick.Initialize()
mw := imagick.NewMagickWand()
defer mw.Destroy()
b := new(bytes.Buffer)
jpeg.Encode(b, i, &jpeg.Options{opts.Quality})
err = mw.ReadImageBlob(b.Bytes())
if err != nil {
fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error())
return
}
if opts.Grayscale {
c := mw.GetImageColors()
mw.QuantizeImage(c, imagick.COLORSPACE_GRAY, 8, true, true)
}
switch filepath.Ext(filename) {
case ".png":
mw.SetImageFormat("PNG")
mw.WriteImage(filename)
case ".gif":
mw.SetImageFormat("GIF")
mw.WriteImage(filename)
case ".bmp":
w := imagick.NewPixelWand()
w.SetColor("black")
defer w.Destroy()
cs := mw.GetImageColorspace()
if opts.Grayscale {
cs = imagick.COLORSPACE_GRAY
}
mw.SetImageFormat("BMP3")
mw.SetImageBackgroundColor(w)
mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_REMOVE)
mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_DEACTIVATE)
mw.SetImageMatte(false)
mw.SetImageCompression(imagick.COMPRESSION_NO)
mw.QuantizeImage(16, cs, 8, true, true)
mw.WriteImage(filename)
default:
mw.SetImageFormat("JPEG")
mw.WriteImage(filename)
}
return
}
// Lists contents of archive // Lists contents of archive
func listArchive(file string) []string { func listArchive(file string) []string {
var contents []string var contents []string
@@ -435,28 +580,23 @@ func coverArchive(file string) (image.Image, error) {
return img, nil return img, nil
} }
// Extracts cover from pdf // Extracts cover from document
func coverPDF(file string) (image.Image, error) { func coverDocument(file string) (image.Image, error) {
doc, err := poppler.Open(file) doc, err := fitz.NewDocument(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
page := doc.GetPage(0) img, err := doc.Image(0)
images := page.Images() if err != nil {
return nil, err
if len(images) == 1 { }
surface := images[0].GetSurface()
img := surface.GetImage()
if img == nil { if img == nil {
return nil, errors.New("Image is nil") return nil, errors.New("Image is nil")
} }
return img, nil return img, nil
}
return nil, nil
} }
// Extracts cover from directory // Extracts cover from directory
@@ -489,7 +629,7 @@ func getFiles() []string {
walkFiles := func(fp string, f os.FileInfo, err error) error { walkFiles := func(fp string, f os.FileInfo, err error) error {
if !f.IsDir() { if !f.IsDir() {
if isComic(fp) { if isArchive(fp) || isDocument(fp) {
if isSize(f.Size()) { if isSize(f.Size()) {
files = append(files, fp) files = append(files, fp)
} }
@@ -507,7 +647,7 @@ func getFiles() []string {
} }
if !stat.IsDir() { if !stat.IsDir() {
if isComic(path) { if isArchive(path) || isDocument(path) {
if isSize(stat.Size()) { if isSize(stat.Size()) {
files = append(files, path) files = append(files, path)
} }
@@ -518,9 +658,9 @@ func getFiles() []string {
} else { } else {
fs, _ := ioutil.ReadDir(path) fs, _ := ioutil.ReadDir(path)
for _, f := range fs { for _, f := range fs {
if isComic(f.Name()) { if isArchive(f.Name()) || isArchive(f.Name()) {
if isSize(f.Size()) { if isSize(f.Size()) {
files = append(files, f.Name()) files = append(files, filepath.Join(path, f.Name()))
} }
} }
} }
@@ -569,6 +709,10 @@ func getImages(path string) []string {
// Returns the filename that is the most likely to be the cover // Returns the filename that is the most likely to be the cover
func getCover(images []string) string { func getCover(images []string) string {
if len(images) == 0 {
return ""
}
for _, i := range images { for _, i := range images {
if strings.HasPrefix(i, "cover") || strings.HasPrefix(i, "front") { if strings.HasPrefix(i, "cover") || strings.HasPrefix(i, "front") {
return i return i
@@ -579,10 +723,21 @@ func getCover(images []string) string {
return images[0] return images[0]
} }
// Checks if file is comic // Checks if file is archive
func isComic(f string) bool { func isArchive(f string) bool {
var types = []string{".rar", ".zip", ".7z", ".gz", ".bz2", var types = []string{".rar", ".zip", ".7z", ".gz",
".cbr", ".cbz", ".cb7", ".cbt", ".pdf"} ".bz2", ".cbr", ".cbz", ".cb7", ".cbt"}
for _, t := range types {
if strings.ToLower(filepath.Ext(f)) == t {
return true
}
}
return false
}
// Checks if file is document
func isDocument(f string) bool {
var types = []string{".pdf", ".epub", ".xps"}
for _, t := range types { for _, t := range types {
if strings.ToLower(filepath.Ext(f)) == t { if strings.ToLower(filepath.Ext(f)) == t {
return true return true
@@ -593,8 +748,8 @@ func isComic(f string) bool {
// Checks if file is image // Checks if file is image
func isImage(f string) bool { func isImage(f string) bool {
var types = []string{".jpg", ".jpeg", ".jpe", var types = []string{".jpg", ".jpeg", ".jpe", ".png",
".png", ".gif", ".bmp"} ".gif", ".bmp", ".tiff", ".tif", ".webp"}
for _, t := range types { for _, t := range types {
if strings.ToLower(filepath.Ext(f)) == t { if strings.ToLower(filepath.Ext(f)) == t {
return true return true
@@ -622,18 +777,6 @@ func isGrayScale(img image.Image) bool {
return false return false
} }
// Decodes image from reader
func decodeImage(reader io.Reader, filename string) (i image.Image, err error) {
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "Recovered in decodeImage %s: %v\n", filename, r)
}
}()
i, _, err = image.Decode(reader)
return i, err
}
// Copies reader to file // Copies reader to file
func copyFile(reader io.Reader, filename string) error { func copyFile(reader io.Reader, filename string) error {
os.MkdirAll(filepath.Dir(filename), 0755) os.MkdirAll(filepath.Dir(filename), 0755)
@@ -665,8 +808,8 @@ func extractCover(file string, info os.FileInfo) {
var cover image.Image var cover image.Image
if info.IsDir() { if info.IsDir() {
cover, err = coverDirectory(file) cover, err = coverDirectory(file)
} else if strings.ToLower(filepath.Ext(file)) == ".pdf" { } else if isDocument(file) {
cover, err = coverPDF(file) cover, err = coverDocument(file)
} else { } else {
cover, err = coverArchive(file) cover, err = coverArchive(file)
} }
@@ -677,8 +820,11 @@ func extractCover(file string, info os.FileInfo) {
} }
if opts.Width > 0 || opts.Height > 0 { if opts.Width > 0 || opts.Height > 0 {
cover = resize.Resize(opts.Width, opts.Height, cover, if opts.Fit {
resize.InterpolationFunction(opts.Interpolation)) cover = imaging.Fit(cover, opts.Width, opts.Height, filters[opts.Filter])
} else {
cover = imaging.Resize(cover, opts.Width, opts.Height, filters[opts.Filter])
}
} }
filename := filepath.Join(opts.Outdir, fmt.Sprintf("%s.jpg", getBasename(file))) filename := filepath.Join(opts.Outdir, fmt.Sprintf("%s.jpg", getBasename(file)))
@@ -698,8 +844,8 @@ func extractThumbnail(file string, info os.FileInfo) {
var cover image.Image var cover image.Image
if info.IsDir() { if info.IsDir() {
cover, err = coverDirectory(file) cover, err = coverDirectory(file)
} else if strings.ToLower(filepath.Ext(file)) == ".pdf" { } else if isDocument(file) {
cover, err = coverPDF(file) cover, err = coverDocument(file)
} else { } else {
cover, err = coverArchive(file) cover, err = coverArchive(file)
} }
@@ -710,11 +856,13 @@ func extractThumbnail(file string, info os.FileInfo) {
} }
if opts.Width > 0 || opts.Height > 0 { if opts.Width > 0 || opts.Height > 0 {
cover = resize.Resize(opts.Width, opts.Height, cover, if opts.Fit {
resize.InterpolationFunction(opts.Interpolation)) cover = imaging.Fit(cover, opts.Width, opts.Height, filters[opts.Filter])
} else { } else {
cover = resize.Resize(256, 0, cover, cover = imaging.Resize(cover, opts.Width, opts.Height, filters[opts.Filter])
resize.InterpolationFunction(opts.Interpolation)) }
} else {
cover = imaging.Resize(cover, 256, 0, filters[opts.Filter])
} }
imagick.Initialize() imagick.Initialize()
@@ -733,7 +881,7 @@ func extractThumbnail(file string, info os.FileInfo) {
fileuri := "file://" + file fileuri := "file://" + file
filename := filepath.Join(opts.Outdir, fmt.Sprintf("%x.png", md5.Sum([]byte(fileuri)))) filename := filepath.Join(opts.Outdir, fmt.Sprintf("%x.png", md5.Sum([]byte(fileuri))))
mw.SetImageFormat("png") mw.SetImageFormat("PNG")
mw.SetImageProperty("Software", "cbconvert") mw.SetImageProperty("Software", "cbconvert")
mw.SetImageProperty("Description", "Thumbnail of "+fileuri) mw.SetImageProperty("Description", "Thumbnail of "+fileuri)
mw.SetImageProperty("Thumb::URI", fileuri) mw.SetImageProperty("Thumb::URI", fileuri)
@@ -749,8 +897,8 @@ func convertComic(file string, info os.FileInfo) {
if info.IsDir() { if info.IsDir() {
convertDirectory(file) convertDirectory(file)
saveArchive(file) saveArchive(file)
} else if strings.ToLower(filepath.Ext(file)) == ".pdf" { } else if isDocument(file) {
convertPDF(file) convertDocument(file)
saveArchive(file) saveArchive(file)
} else { } else {
convertArchive(file) convertArchive(file)
@@ -760,26 +908,57 @@ func convertComic(file string, info os.FileInfo) {
// Parses command line flags // Parses command line flags
func parseFlags() { func parseFlags() {
opts = options{} opts = Options{}
kingpin.Version("CBconvert 0.2.0") kingpin.Version("CBconvert 0.4.0")
kingpin.CommandLine.Help = "Comic Book convert tool." kingpin.CommandLine.Help = "Comic Book convert tool."
kingpin.Flag("png", "encode images to PNG instead of JPEG").Short('p').BoolVar(&opts.ToPNG) kingpin.UsageTemplate(kingpin.CompactUsageTemplate)
kingpin.Flag("bmp", "encode images to 4-Bit BMP instead of JPEG").Short('b').BoolVar(&opts.ToBMP)
kingpin.Flag("width", "image width").Default(strconv.Itoa(0)).Short('w').UintVar(&opts.Width) kingpin.Flag("outdir", "Output directory").Default(".").StringVar(&opts.Outdir)
kingpin.Flag("height", "image height").Default(strconv.Itoa(0)).Short('h').UintVar(&opts.Height) kingpin.Flag("size", "Process only files larger then size (in MB)").Default(strconv.Itoa(0)).Int64Var(&opts.Size)
kingpin.Flag("quality", "JPEG image quality").Short('q').Default(strconv.Itoa(jpeg.DefaultQuality)).IntVar(&opts.Quality) kingpin.Flag("recursive", "Process subdirectories recursively").BoolVar(&opts.Recursive)
kingpin.Flag("norgb", "do not convert images with RGB colorspace").Short('n').BoolVar(&opts.NoRGB) kingpin.Flag("quiet", "Hide console output").BoolVar(&opts.Quiet)
kingpin.Flag("interpolation", "0=NearestNeighbor, 1=Bilinear, 2=Bicubic, 3=MitchellNetravali, 4=Lanczos2, 5=Lanczos3").Short('i').
Default(strconv.Itoa(int(resize.Bilinear))).IntVar(&opts.Interpolation) convert := kingpin.Command("convert", "Convert archive or document (default command)").Default()
kingpin.Flag("suffix", "add suffix to file basename").Short('s').StringVar(&opts.Suffix) convert.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&arguments)
kingpin.Flag("cover", "extract cover").Short('c').BoolVar(&opts.Cover) convert.Flag("width", "Image width").Default(strconv.Itoa(0)).IntVar(&opts.Width)
kingpin.Flag("thumbnail", "extract cover thumbnail (freedesktop spec.)").Short('t').BoolVar(&opts.Thumbnail) convert.Flag("height", "Image height").Default(strconv.Itoa(0)).IntVar(&opts.Height)
kingpin.Flag("outdir", "output directory").Default(".").Short('o').StringVar(&opts.Outdir) convert.Flag("fit", "Best fit for required width and height").BoolVar(&opts.Fit)
kingpin.Flag("size", "process only files larger then size (in MB)").Short('m').Default(strconv.Itoa(0)).Int64Var(&opts.Size) convert.Flag("quality", "JPEG image quality").Default(strconv.Itoa(jpeg.DefaultQuality)).IntVar(&opts.Quality)
kingpin.Flag("recursive", "process subdirectories recursively").Short('r').BoolVar(&opts.Recursive) convert.Flag("filter", "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos").Default(strconv.Itoa(Linear)).IntVar(&opts.Filter)
kingpin.Flag("quiet", "hide console output").Short('Q').BoolVar(&opts.Quiet) convert.Flag("png", "Encode images to PNG instead of JPEG").BoolVar(&opts.ToPNG)
kingpin.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&arguments) convert.Flag("bmp", "Encode images to 4-Bit BMP (16 colors) instead of JPEG").BoolVar(&opts.ToBMP)
kingpin.Parse() convert.Flag("gif", "Encode images to GIF instead of JPEG").BoolVar(&opts.ToGIF)
convert.Flag("tiff", "Encode images to TIFF instead of JPEG").BoolVar(&opts.ToTIFF)
convert.Flag("rgb", "Convert images that have RGB colorspace (use --no-rgb if you only want to convert grayscale images)").Default("true").BoolVar(&opts.RGB)
convert.Flag("nonimage", "Leave non image files in archive (use --no-nonimage to remove non image files from archive)").Default("true").BoolVar(&opts.NonImage)
convert.Flag("grayscale", "Convert images to grayscale (monochromatic)").BoolVar(&opts.Grayscale)
convert.Flag("rotate", "Rotate images, valid values are 0, 90, 180, 270").Default(strconv.Itoa(0)).IntVar(&opts.Rotate)
convert.Flag("flip", "Flip images, valid values are none, horizontal, vertical").Default("none").StringVar(&opts.Flip)
convert.Flag("brightness", "Adjust brightness of the images, must be in range (-100, 100)").Default(strconv.Itoa(0)).Float64Var(&opts.Brightness)
convert.Flag("contrast", "Adjust contrast of the images, must be in range (-100, 100)").Default(strconv.Itoa(0)).Float64Var(&opts.Contrast)
convert.Flag("suffix", "Add suffix to file basename").StringVar(&opts.Suffix)
cover := kingpin.Command("cover", "Extract cover")
cover.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&arguments)
cover.Flag("width", "Image width").Default(strconv.Itoa(0)).IntVar(&opts.Width)
cover.Flag("height", "Image height").Default(strconv.Itoa(0)).IntVar(&opts.Height)
cover.Flag("fit", "Best fit for required width and height").BoolVar(&opts.Fit)
cover.Flag("quality", "JPEG image quality").Default(strconv.Itoa(jpeg.DefaultQuality)).IntVar(&opts.Quality)
cover.Flag("filter", "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos").Default(strconv.Itoa(Linear)).IntVar(&opts.Filter)
thumbnail := kingpin.Command("thumbnail", "Extract cover thumbnail (freedesktop spec.)")
thumbnail.Arg("args", "filename or directory").Required().ExistingFilesOrDirsVar(&arguments)
thumbnail.Flag("width", "Image width").Default(strconv.Itoa(0)).IntVar(&opts.Width)
thumbnail.Flag("height", "Image height").Default(strconv.Itoa(0)).IntVar(&opts.Height)
thumbnail.Flag("fit", "Best fit for required width and height").BoolVar(&opts.Fit)
thumbnail.Flag("filter", "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos").Default(strconv.Itoa(Linear)).IntVar(&opts.Filter)
switch kingpin.Parse() {
case "cover":
opts.Cover = true
case "thumbnail":
opts.Thumbnail = true
}
} }
func main() { func main() {
@@ -789,6 +968,7 @@ func main() {
signal.Notify(c, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM) signal.Notify(c, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM)
go func() { go func() {
for _ = range c { for _ = range c {
fmt.Fprintf(os.Stderr, "Aborting\n")
os.RemoveAll(workdir) os.RemoveAll(workdir)
os.Exit(1) os.Exit(1)
} }
@@ -800,6 +980,15 @@ func main() {
files := getFiles() files := getFiles()
nfiles = len(files) nfiles = len(files)
if opts.Cover || opts.Thumbnail {
if !opts.Quiet {
bar = pb.New(nfiles)
bar.ShowTimeLeft = false
bar.Start()
}
}
for n, file := range files { for n, file := range files {
current = n + 1 current = n + 1
@@ -811,9 +1000,15 @@ func main() {
if opts.Cover { if opts.Cover {
extractCover(file, stat) extractCover(file, stat)
if !opts.Quiet {
bar.Increment()
}
continue continue
} else if opts.Thumbnail { } else if opts.Thumbnail {
extractThumbnail(file, stat) extractThumbnail(file, stat)
if !opts.Quiet {
bar.Increment()
}
continue continue
} }
+7 -4
View File
@@ -5,14 +5,17 @@ mkdir -p build
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/cbconvert CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/cbconvert
strip 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_LDFLAGS="-L/usr/i686-pc-mingw32/usr/lib" \
CGO_CFLAGS="-I/usr/i686-pc-mingw32/usr/include -I/usr/i686-pc-mingw32/usr/include/poppler/glib -I/usr/i686-pc-mingw32/usr/include/glib-2.0 -I/usr/i686-pc-mingw32/usr/include/cairo" \ CGO_CFLAGS="-I/usr/i686-pc-mingw32/usr/include -Wno-poison-system-directories" \
CGO_CXXFLAGS="-I/usr/i686-pc-mingw32/usr/include -I/usr/i686-pc-mingw32/usr/include/poppler/glib -I/usr/i686-pc-mingw32/usr/include/glib-2.0 -I/usr/i686-pc-mingw32/usr/include/cairo" \ CGO_CXXFLAGS="-I/usr/i686-pc-mingw32/usr/include -Wno-poison-system-directories" \
CGO_CPPFLAGS="-I/usr/i686-pc-mingw32/usr/include -I/usr/i686-pc-mingw32/usr/include/poppler/glib -I/usr/i686-pc-mingw32/usr/include/glib-2.0 -I/usr/i686-pc-mingw32/usr/include/cairo" \ CGO_CPPFLAGS="-I/usr/i686-pc-mingw32/usr/include -Wno-poison-system-directories" \
PKG_CONFIG=/usr/bin/i686-pc-mingw32-pkg-config \ PKG_CONFIG=/usr/bin/i686-pc-mingw32-pkg-config \
PKG_CONFIG_PATH=/usr/i686-pc-mingw32/usr/lib/pkgconfig \ PKG_CONFIG_PATH=/usr/i686-pc-mingw32/usr/lib/pkgconfig \
PKG_CONFIG_LIBDIR=/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="i686-pc-mingw32-gcc" CXX="i686-pc-mingw32-g++" \
CC_FOR_TARGET=i686-pc-mingw32-gcc CXX_FOR_TARGET=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 -x -work -o build/cbconvert.exe -ldflags "-linkmode external -extldflags -static" 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 i686-pc-mingw32-strip build/cbconvert.exe