mirror of
https://github.com/gen2brain/cbconvert
synced 2025-10-13 18:18:52 +02:00
918 lines
21 KiB
Go
918 lines
21 KiB
Go
// 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 cbconvert
|
||
|
||
import (
|
||
"archive/zip"
|
||
"bytes"
|
||
"crypto/md5"
|
||
"errors"
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/gif"
|
||
"image/jpeg"
|
||
"image/png"
|
||
"io"
|
||
"io/ioutil"
|
||
"mime"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
|
||
"github.com/cheggaaa/pb"
|
||
"github.com/disintegration/imaging"
|
||
"github.com/gen2brain/go-fitz"
|
||
"github.com/gen2brain/go-unarr"
|
||
"github.com/gographics/imagick/imagick"
|
||
_ "github.com/hotei/bmp"
|
||
"github.com/skarademir/naturalsort"
|
||
"golang.org/x/image/tiff"
|
||
_ "golang.org/x/image/webp"
|
||
)
|
||
|
||
// Resample filters
|
||
const (
|
||
NearestNeighbor int = iota // Fastest resampling filter, no antialiasing
|
||
Box // Box filter (averaging pixels)
|
||
Linear // Bilinear filter, smooth and reasonably fast
|
||
MitchellNetravali // А smooth bicubic filter
|
||
CatmullRom // A sharp bicubic filter
|
||
Gaussian // Blurring filter that uses gaussian function, useful for noise removal
|
||
Lanczos // High-quality resampling filter, it's slower than cubic filters
|
||
)
|
||
|
||
var filters = map[int]imaging.ResampleFilter{
|
||
NearestNeighbor: imaging.NearestNeighbor,
|
||
Box: imaging.Box,
|
||
Linear: imaging.Linear,
|
||
MitchellNetravali: imaging.MitchellNetravali,
|
||
CatmullRom: imaging.CatmullRom,
|
||
Gaussian: imaging.Gaussian,
|
||
Lanczos: imaging.Lanczos,
|
||
}
|
||
|
||
var (
|
||
bar *pb.ProgressBar
|
||
wg sync.WaitGroup
|
||
)
|
||
|
||
// Limits go routines to number of CPUs + 1
|
||
var throttle = make(chan int, runtime.NumCPU()+1)
|
||
|
||
// Options
|
||
type Options struct {
|
||
ToPNG bool // encode images to PNG instead of JPEG
|
||
ToBMP bool // encode images to 4-Bit BMP (16 colors) instead of JPEG
|
||
ToGIF bool // encode images to GIF instead of JPEG
|
||
ToTIFF bool // encode images to TIFF instead of JPEG
|
||
Quality int // JPEG image quality
|
||
Width int // image width
|
||
Height int // image height
|
||
Fit bool // Best fit for required width and height
|
||
Filter int // 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos
|
||
RGB bool // convert images that have RGB colorspace
|
||
NonImage bool // Leave non image files in archive
|
||
Suffix string // add suffix to file basename
|
||
Cover bool // extract cover
|
||
Thumbnail bool // extract cover thumbnail (freedesktop spec.)
|
||
Outdir string // output directory
|
||
Grayscale bool // convert images to grayscale (monochromatic)
|
||
Rotate int // Rotate images, valid values are 0, 90, 180, 270
|
||
Flip string // Flip images, valid values are none, horizontal, vertical
|
||
Brightness float64 // Adjust brightness of the images, must be in range (-100, 100)
|
||
Contrast float64 // Adjust contrast of the images, must be in range (-100, 100)
|
||
Recursive bool // process subdirectories recursively
|
||
Size int64 // process only files larger then size (in MB)
|
||
Quiet bool // hide console output
|
||
}
|
||
|
||
// Convertor struct
|
||
type Convertor struct {
|
||
Opts Options // Options struct
|
||
Workdir string // Current working directory
|
||
Nfiles int // Number of files
|
||
Current int // Index of current file
|
||
}
|
||
|
||
// NewConvertor returns new convertor
|
||
func NewConvertor(o Options) *Convertor {
|
||
c := &Convertor{}
|
||
c.Opts = o
|
||
return c
|
||
}
|
||
|
||
// Converts image
|
||
func (c *Convertor) convertImage(img image.Image, index int, pathName string) {
|
||
defer wg.Done()
|
||
|
||
var ext string = "jpg"
|
||
if c.Opts.ToPNG {
|
||
ext = "png"
|
||
} else if c.Opts.ToBMP {
|
||
ext = "bmp"
|
||
} else if c.Opts.ToGIF {
|
||
ext = "gif"
|
||
} else if c.Opts.ToTIFF {
|
||
ext = "tiff"
|
||
}
|
||
|
||
var filename string
|
||
if pathName != "" {
|
||
filename = filepath.Join(c.Workdir, fmt.Sprintf("%s.%s", c.getBasename(pathName), ext))
|
||
} else {
|
||
filename = filepath.Join(c.Workdir, fmt.Sprintf("%03d.%s", index, ext))
|
||
}
|
||
|
||
if c.Opts.ToPNG {
|
||
// convert image to PNG
|
||
if c.Opts.Grayscale {
|
||
c.encodeImageMagick(img, filename)
|
||
} else {
|
||
c.encodeImage(img, filename)
|
||
}
|
||
} else if c.Opts.ToBMP {
|
||
// convert image to 4-Bit BMP (16 colors)
|
||
c.encodeImageMagick(img, filename)
|
||
} else if c.Opts.ToGIF {
|
||
// convert image to GIF
|
||
c.encodeImageMagick(img, filename)
|
||
} else if c.Opts.ToTIFF {
|
||
// convert image to TIFF
|
||
c.encodeImage(img, filename)
|
||
} else {
|
||
// convert image to JPEG (default)
|
||
if c.Opts.Grayscale {
|
||
c.encodeImageMagick(img, filename)
|
||
} else {
|
||
c.encodeImage(img, filename)
|
||
}
|
||
}
|
||
|
||
<-throttle
|
||
}
|
||
|
||
// Transforms image (resize, rotate, flip, brightness, contrast)
|
||
func (c *Convertor) transformImage(img image.Image) image.Image {
|
||
var i image.Image = img
|
||
|
||
if c.Opts.Width > 0 || c.Opts.Height > 0 {
|
||
if c.Opts.Fit {
|
||
i = imaging.Fit(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
|
||
} else {
|
||
i = imaging.Resize(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
|
||
}
|
||
}
|
||
|
||
if c.Opts.Rotate > 0 {
|
||
switch c.Opts.Rotate {
|
||
case 90:
|
||
i = imaging.Rotate90(i)
|
||
case 180:
|
||
i = imaging.Rotate180(i)
|
||
case 270:
|
||
i = imaging.Rotate270(i)
|
||
}
|
||
}
|
||
|
||
if c.Opts.Flip != "none" {
|
||
switch c.Opts.Flip {
|
||
case "horizontal":
|
||
i = imaging.FlipH(i)
|
||
case "vertical":
|
||
i = imaging.FlipV(i)
|
||
}
|
||
}
|
||
|
||
if c.Opts.Brightness != 0 {
|
||
i = imaging.AdjustBrightness(i, c.Opts.Brightness)
|
||
}
|
||
|
||
if c.Opts.Contrast != 0 {
|
||
i = imaging.AdjustContrast(i, c.Opts.Contrast)
|
||
}
|
||
|
||
return i
|
||
}
|
||
|
||
// Converts PDF/EPUB/XPS document to CBZ
|
||
func (c *Convertor) convertDocument(file string) {
|
||
c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc")
|
||
|
||
doc, err := fitz.NewDocument(file)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Skipping %s, error: %v", file, err.Error())
|
||
return
|
||
}
|
||
|
||
npages := doc.Pages()
|
||
|
||
if !c.Opts.Quiet {
|
||
bar = pb.New(npages)
|
||
bar.ShowTimeLeft = false
|
||
bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.Current, c.Nfiles))
|
||
bar.Start()
|
||
}
|
||
|
||
for n := 0; n < npages; n++ {
|
||
if !c.Opts.Quiet {
|
||
bar.Increment()
|
||
}
|
||
|
||
img, err := doc.Image(n)
|
||
|
||
if err == nil {
|
||
img = c.transformImage(img)
|
||
}
|
||
|
||
if img != nil {
|
||
throttle <- 1
|
||
wg.Add(1)
|
||
|
||
go c.convertImage(img, n, "")
|
||
}
|
||
}
|
||
wg.Wait()
|
||
}
|
||
|
||
// Converts archive to CBZ
|
||
func (c *Convertor) convertArchive(file string) {
|
||
c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc")
|
||
|
||
ncontents := len(c.listArchive(file))
|
||
|
||
archive, err := unarr.NewArchive(file)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error NewReader: %v\n", err.Error())
|
||
}
|
||
defer archive.Close()
|
||
|
||
if !c.Opts.Quiet {
|
||
bar = pb.New(ncontents)
|
||
bar.ShowTimeLeft = false
|
||
bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.Current, c.Nfiles))
|
||
bar.Start()
|
||
}
|
||
|
||
for {
|
||
err := archive.Entry()
|
||
if err != nil {
|
||
if err == io.EOF {
|
||
break
|
||
} else {
|
||
fmt.Fprintf(os.Stderr, "Error Entry: %v\n", err.Error())
|
||
continue
|
||
}
|
||
}
|
||
|
||
if !c.Opts.Quiet {
|
||
bar.Increment()
|
||
}
|
||
|
||
size := archive.Size()
|
||
pathname := archive.Name()
|
||
|
||
buf := make([]byte, size)
|
||
for size > 0 {
|
||
n, err := archive.Read(buf)
|
||
if err != nil && err != io.EOF {
|
||
break
|
||
}
|
||
size -= n
|
||
}
|
||
|
||
if size > 0 {
|
||
fmt.Printf("Error Read\n")
|
||
continue
|
||
}
|
||
|
||
if c.isImage(pathname) {
|
||
img, err := c.decodeImage(bytes.NewReader(buf), pathname)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Decode: %v\n", err.Error())
|
||
continue
|
||
}
|
||
|
||
i := c.transformImage(img)
|
||
|
||
if !c.Opts.RGB && !c.isGrayScale(i) {
|
||
c.encodeImage(i, filepath.Join(c.Workdir, filepath.Base(pathname)))
|
||
continue
|
||
}
|
||
|
||
if i != nil {
|
||
throttle <- 1
|
||
wg.Add(1)
|
||
go c.convertImage(i, 0, pathname)
|
||
}
|
||
} else {
|
||
if c.Opts.NonImage {
|
||
c.copyFile(bytes.NewReader(buf), filepath.Join(c.Workdir, filepath.Base(pathname)))
|
||
}
|
||
}
|
||
}
|
||
wg.Wait()
|
||
}
|
||
|
||
// Converts directory to CBZ
|
||
func (c *Convertor) convertDirectory(path string) {
|
||
c.Workdir, _ = ioutil.TempDir(os.TempDir(), "cbc")
|
||
|
||
images := c.getImages(path)
|
||
|
||
if !c.Opts.Quiet {
|
||
bar = pb.New(c.Nfiles)
|
||
bar.ShowTimeLeft = false
|
||
bar.Prefix(fmt.Sprintf("Converting %d of %d: ", c.Current, c.Nfiles))
|
||
bar.Start()
|
||
}
|
||
|
||
for index, img := range images {
|
||
if c.Opts.Quiet {
|
||
bar.Increment()
|
||
}
|
||
|
||
f, err := os.Open(img)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Open: %v\n", err.Error())
|
||
continue
|
||
}
|
||
|
||
i, err := c.decodeImage(f, img)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Decode: %v\n", err.Error())
|
||
continue
|
||
}
|
||
|
||
i = c.transformImage(i)
|
||
|
||
if !c.Opts.RGB && !c.isGrayScale(i) {
|
||
c.encodeImage(i, filepath.Join(c.Workdir, filepath.Base(img)))
|
||
continue
|
||
}
|
||
|
||
f.Close()
|
||
|
||
if i != nil {
|
||
throttle <- 1
|
||
wg.Add(1)
|
||
go c.convertImage(i, index, img)
|
||
}
|
||
}
|
||
wg.Wait()
|
||
}
|
||
|
||
// Saves workdir to CBZ archive
|
||
func (c *Convertor) saveArchive(file string) {
|
||
defer os.RemoveAll(c.Workdir)
|
||
|
||
zipname := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%s%s.cbz", c.getBasename(file), c.Opts.Suffix))
|
||
zipfile, err := os.Create(zipname)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Create: %v\n", err.Error())
|
||
return
|
||
}
|
||
defer zipfile.Close()
|
||
|
||
z := zip.NewWriter(zipfile)
|
||
files, _ := ioutil.ReadDir(c.Workdir)
|
||
|
||
if !c.Opts.Quiet {
|
||
bar = pb.New(len(files))
|
||
bar.ShowTimeLeft = false
|
||
bar.Prefix(fmt.Sprintf("Compressing %d of %d: ", c.Current, c.Nfiles))
|
||
bar.Start()
|
||
}
|
||
|
||
for _, file := range files {
|
||
if !c.Opts.Quiet {
|
||
bar.Increment()
|
||
}
|
||
|
||
r, err := ioutil.ReadFile(filepath.Join(c.Workdir, file.Name()))
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error ReadFile: %v\n", err.Error())
|
||
continue
|
||
}
|
||
|
||
w, err := z.Create(file.Name())
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Create: %v\n", err.Error())
|
||
continue
|
||
}
|
||
w.Write(r)
|
||
}
|
||
z.Close()
|
||
}
|
||
|
||
// Decodes image from reader
|
||
func (c *Convertor) decodeImage(reader io.Reader, filename string) (i image.Image, err error) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
fmt.Fprintf(os.Stderr, "Recovered in decodeImage %s: %v\n", filename, r)
|
||
}
|
||
}()
|
||
|
||
i, _, err = image.Decode(reader)
|
||
return i, err
|
||
}
|
||
|
||
// Encode image to file
|
||
func (c *Convertor) encodeImage(i image.Image, filename string) (err error) {
|
||
f, err := os.Create(filename)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
switch filepath.Ext(filename) {
|
||
case ".png":
|
||
err = png.Encode(f, i)
|
||
case ".tif":
|
||
case ".tiff":
|
||
err = tiff.Encode(f, i, &tiff.Options{tiff.Uncompressed, false})
|
||
case ".gif":
|
||
err = gif.Encode(f, i, nil)
|
||
default:
|
||
err = jpeg.Encode(f, i, &jpeg.Options{c.Opts.Quality})
|
||
}
|
||
|
||
f.Close()
|
||
return
|
||
}
|
||
|
||
// Encode image to file (ImageMagick)
|
||
func (c *Convertor) encodeImageMagick(i image.Image, filename string) (err error) {
|
||
imagick.Initialize()
|
||
|
||
mw := imagick.NewMagickWand()
|
||
defer mw.Destroy()
|
||
|
||
b := new(bytes.Buffer)
|
||
jpeg.Encode(b, i, &jpeg.Options{c.Opts.Quality})
|
||
|
||
err = mw.ReadImageBlob(b.Bytes())
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error())
|
||
return
|
||
}
|
||
|
||
if c.Opts.Grayscale {
|
||
c := mw.GetImageColors()
|
||
mw.QuantizeImage(c, imagick.COLORSPACE_GRAY, 8, true, true)
|
||
}
|
||
|
||
switch filepath.Ext(filename) {
|
||
case ".png":
|
||
mw.SetImageFormat("PNG")
|
||
mw.WriteImage(filename)
|
||
case ".gif":
|
||
mw.SetImageFormat("GIF")
|
||
mw.WriteImage(filename)
|
||
case ".bmp":
|
||
w := imagick.NewPixelWand()
|
||
w.SetColor("black")
|
||
defer w.Destroy()
|
||
|
||
cs := mw.GetImageColorspace()
|
||
if c.Opts.Grayscale {
|
||
cs = imagick.COLORSPACE_GRAY
|
||
}
|
||
|
||
mw.SetImageFormat("BMP3")
|
||
mw.SetImageBackgroundColor(w)
|
||
mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_REMOVE)
|
||
mw.SetImageAlphaChannel(imagick.ALPHA_CHANNEL_DEACTIVATE)
|
||
mw.SetImageMatte(false)
|
||
mw.SetImageCompression(imagick.COMPRESSION_NO)
|
||
mw.QuantizeImage(16, cs, 8, true, true)
|
||
mw.WriteImage(filename)
|
||
default:
|
||
mw.SetImageFormat("JPEG")
|
||
mw.WriteImage(filename)
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// Lists contents of archive
|
||
func (c *Convertor) listArchive(file string) []string {
|
||
var contents []string
|
||
archive, err := unarr.NewArchive(file)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error NewReader: %v\n", err.Error())
|
||
}
|
||
defer archive.Close()
|
||
|
||
for {
|
||
err := archive.Entry()
|
||
if err != nil {
|
||
if err == io.EOF {
|
||
break
|
||
} else {
|
||
fmt.Fprintf(os.Stderr, "Error Entry: %v\n", err.Error())
|
||
continue
|
||
}
|
||
}
|
||
|
||
pathname := archive.Name()
|
||
contents = append(contents, pathname)
|
||
}
|
||
|
||
return contents
|
||
}
|
||
|
||
// Extracts cover from archive
|
||
func (c *Convertor) coverArchive(file string) (image.Image, error) {
|
||
var images []string
|
||
|
||
contents := c.listArchive(file)
|
||
for _, ct := range contents {
|
||
if c.isImage(ct) {
|
||
images = append(images, ct)
|
||
}
|
||
}
|
||
|
||
cover := c.getCover(images)
|
||
|
||
archive, err := unarr.NewArchive(file)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer archive.Close()
|
||
|
||
err = archive.EntryFor(cover)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
size := archive.Size()
|
||
buf := make([]byte, size)
|
||
for size > 0 {
|
||
n, err := archive.Read(buf)
|
||
if err != nil && err != io.EOF {
|
||
break
|
||
}
|
||
size -= n
|
||
}
|
||
|
||
if size > 0 {
|
||
return nil, errors.New("Error Read")
|
||
}
|
||
|
||
img, err := c.decodeImage(bytes.NewReader(buf), cover)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return img, nil
|
||
}
|
||
|
||
// Extracts cover from document
|
||
func (c *Convertor) coverDocument(file string) (image.Image, error) {
|
||
doc, err := fitz.NewDocument(file)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
img, err := doc.Image(0)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if img == nil {
|
||
return nil, errors.New("Image is nil")
|
||
}
|
||
|
||
return img, nil
|
||
}
|
||
|
||
// Extracts cover from directory
|
||
func (c *Convertor) coverDirectory(dir string) (image.Image, error) {
|
||
images := c.getImages(dir)
|
||
cover := c.getCover(images)
|
||
|
||
p, err := os.Open(cover)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer p.Close()
|
||
|
||
img, err := c.decodeImage(p, cover)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Decode: %v\n", err.Error())
|
||
return nil, err
|
||
}
|
||
|
||
if img == nil {
|
||
return nil, errors.New("Image is nil")
|
||
}
|
||
|
||
return img, nil
|
||
}
|
||
|
||
// Returns list of found comic files
|
||
func (c *Convertor) GetFiles(args []string) []string {
|
||
var files []string
|
||
|
||
walkFiles := func(fp string, f os.FileInfo, err error) error {
|
||
if !f.IsDir() {
|
||
if c.isArchive(fp) || c.isDocument(fp) {
|
||
if c.isSize(f.Size()) {
|
||
files = append(files, fp)
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
for _, arg := range args {
|
||
path, _ := filepath.Abs(arg)
|
||
stat, err := os.Stat(path)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Stat: %v\n", err.Error())
|
||
continue
|
||
}
|
||
|
||
if !stat.IsDir() {
|
||
if c.isArchive(path) || c.isDocument(path) {
|
||
if c.isSize(stat.Size()) {
|
||
files = append(files, path)
|
||
}
|
||
}
|
||
} else {
|
||
if c.Opts.Recursive {
|
||
filepath.Walk(path, walkFiles)
|
||
} else {
|
||
fs, _ := ioutil.ReadDir(path)
|
||
for _, f := range fs {
|
||
if c.isArchive(f.Name()) || c.isArchive(f.Name()) {
|
||
if c.isSize(f.Size()) {
|
||
files = append(files, filepath.Join(path, f.Name()))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(files) == 0 {
|
||
// append plain directory with images
|
||
files = append(files, path)
|
||
}
|
||
}
|
||
}
|
||
|
||
c.Nfiles = len(files)
|
||
return files
|
||
}
|
||
|
||
// Returns list of found image files for given directory
|
||
func (c *Convertor) getImages(path string) []string {
|
||
var images []string
|
||
|
||
walkFiles := func(fp string, f os.FileInfo, err error) error {
|
||
if !f.IsDir() && f.Mode()&os.ModeType == 0 {
|
||
if f.Size() > 0 && c.isImage(fp) {
|
||
images = append(images, fp)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
f, _ := filepath.Abs(path)
|
||
stat, err := os.Stat(f)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Stat: %v\n", err.Error())
|
||
return images
|
||
}
|
||
|
||
if !stat.IsDir() && stat.Mode()&os.ModeType == 0 {
|
||
if c.isImage(f) {
|
||
images = append(images, f)
|
||
}
|
||
} else {
|
||
filepath.Walk(f, walkFiles)
|
||
}
|
||
|
||
return images
|
||
}
|
||
|
||
// Returns the filename that is the most likely to be the cover
|
||
func (c *Convertor) getCover(images []string) string {
|
||
if len(images) == 0 {
|
||
return ""
|
||
}
|
||
|
||
for _, i := range images {
|
||
if strings.HasPrefix(i, "cover") || strings.HasPrefix(i, "front") {
|
||
return i
|
||
}
|
||
}
|
||
|
||
sort.Sort(naturalsort.NaturalSort(images))
|
||
return images[0]
|
||
}
|
||
|
||
// Checks if file is archive
|
||
func (c *Convertor) isArchive(f string) bool {
|
||
var types = []string{".rar", ".zip", ".7z", ".gz",
|
||
".bz2", ".cbr", ".cbz", ".cb7", ".cbt"}
|
||
for _, t := range types {
|
||
if strings.ToLower(filepath.Ext(f)) == t {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Checks if file is document
|
||
func (c *Convertor) isDocument(f string) bool {
|
||
var types = []string{".pdf", ".epub", ".xps"}
|
||
for _, t := range types {
|
||
if strings.ToLower(filepath.Ext(f)) == t {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Checks if file is image
|
||
func (c *Convertor) isImage(f string) bool {
|
||
var types = []string{".jpg", ".jpeg", ".jpe", ".png",
|
||
".gif", ".bmp", ".tiff", ".tif", ".webp"}
|
||
for _, t := range types {
|
||
if strings.ToLower(filepath.Ext(f)) == t {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Checks size of file
|
||
func (c *Convertor) isSize(size int64) bool {
|
||
if c.Opts.Size > 0 {
|
||
if size < c.Opts.Size*(1024*1024) {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// Checks if image is grayscale
|
||
func (c *Convertor) isGrayScale(img image.Image) bool {
|
||
model := img.ColorModel()
|
||
if model == color.GrayModel || model == color.Gray16Model {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Copies reader to file
|
||
func (c *Convertor) copyFile(reader io.Reader, filename string) error {
|
||
os.MkdirAll(filepath.Dir(filename), 0755)
|
||
|
||
file, err := os.Create(filename)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer file.Close()
|
||
|
||
_, err = io.Copy(file, reader)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Returns basename without extension
|
||
func (c *Convertor) getBasename(file string) string {
|
||
basename := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file))
|
||
basename = strings.TrimSuffix(basename, ".tar")
|
||
return basename
|
||
}
|
||
|
||
// Extracts cover
|
||
func (c *Convertor) ExtractCover(file string, info os.FileInfo) {
|
||
var err error
|
||
var cover image.Image
|
||
c.Current += 1
|
||
|
||
if info.IsDir() {
|
||
cover, err = c.coverDirectory(file)
|
||
} else if c.isDocument(file) {
|
||
cover, err = c.coverDocument(file)
|
||
} else {
|
||
cover, err = c.coverArchive(file)
|
||
}
|
||
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Cover: %v\n", err.Error())
|
||
return
|
||
}
|
||
|
||
if c.Opts.Width > 0 || c.Opts.Height > 0 {
|
||
if c.Opts.Fit {
|
||
cover = imaging.Fit(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
|
||
} else {
|
||
cover = imaging.Resize(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
|
||
}
|
||
}
|
||
|
||
filename := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%s.jpg", c.getBasename(file)))
|
||
f, err := os.Create(filename)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Create: %v\n", err.Error())
|
||
return
|
||
}
|
||
defer f.Close()
|
||
|
||
jpeg.Encode(f, cover, &jpeg.Options{c.Opts.Quality})
|
||
}
|
||
|
||
// Extracts thumbnail
|
||
func (c *Convertor) ExtractThumbnail(file string, info os.FileInfo) {
|
||
var err error
|
||
var cover image.Image
|
||
c.Current += 1
|
||
|
||
if info.IsDir() {
|
||
cover, err = c.coverDirectory(file)
|
||
} else if c.isDocument(file) {
|
||
cover, err = c.coverDocument(file)
|
||
} else {
|
||
cover, err = c.coverArchive(file)
|
||
}
|
||
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error Thumbnail: %v\n", err.Error())
|
||
return
|
||
}
|
||
|
||
if c.Opts.Width > 0 || c.Opts.Height > 0 {
|
||
if c.Opts.Fit {
|
||
cover = imaging.Fit(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
|
||
} else {
|
||
cover = imaging.Resize(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
|
||
}
|
||
} else {
|
||
cover = imaging.Resize(cover, 256, 0, filters[c.Opts.Filter])
|
||
}
|
||
|
||
imagick.Initialize()
|
||
|
||
mw := imagick.NewMagickWand()
|
||
defer mw.Destroy()
|
||
|
||
b := new(bytes.Buffer)
|
||
png.Encode(b, cover)
|
||
|
||
err = mw.ReadImageBlob(b.Bytes())
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error ReadImageBlob: %v\n", err.Error())
|
||
}
|
||
|
||
fileuri := "file://" + file
|
||
filename := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%x.png", md5.Sum([]byte(fileuri))))
|
||
|
||
mw.SetImageFormat("PNG")
|
||
mw.SetImageProperty("Software", "cbconvert")
|
||
mw.SetImageProperty("Description", "Thumbnail of "+fileuri)
|
||
mw.SetImageProperty("Thumb::URI", fileuri)
|
||
mw.SetImageProperty("Thumb::MTime", strconv.FormatInt(info.ModTime().Unix(), 10))
|
||
mw.SetImageProperty("Thumb::Size", strconv.FormatInt(info.Size(), 10))
|
||
mw.SetImageProperty("Thumb::Mimetype", mime.TypeByExtension(filepath.Ext(file)))
|
||
|
||
mw.WriteImage(filename)
|
||
}
|
||
|
||
// Converts comic book
|
||
func (c *Convertor) ConvertComic(file string, info os.FileInfo) {
|
||
c.Current += 1
|
||
if info.IsDir() {
|
||
c.convertDirectory(file)
|
||
c.saveArchive(file)
|
||
} else if c.isDocument(file) {
|
||
c.convertDocument(file)
|
||
c.saveArchive(file)
|
||
} else {
|
||
c.convertArchive(file)
|
||
c.saveArchive(file)
|
||
}
|
||
}
|