mirror of
https://github.com/celogeek/go-comic-converter.git
synced 2025-05-25 00:02:37 +02:00
In order to let third parties use the package of this library, we moved the pkg folder out of the locked internal folder
435 lines
16 KiB
Go
435 lines
16 KiB
Go
// Package converter Helper to parse and prepare options for go-comic-converter.
|
|
//
|
|
// It uses goflag with additional feature:
|
|
// - Keep original order
|
|
// - Support section
|
|
package converter
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/celogeek/go-comic-converter/v2/pkg/utils"
|
|
)
|
|
|
|
type Converter struct {
|
|
Options *Options
|
|
Cmd *flag.FlagSet
|
|
|
|
order []order
|
|
isZeroValueErrs []error
|
|
startAt time.Time
|
|
}
|
|
|
|
// New Create a new parser
|
|
func New() *Converter {
|
|
o := NewOptions()
|
|
cmd := flag.NewFlagSet("go-comic-converter", flag.ExitOnError)
|
|
conv := &Converter{
|
|
Options: o,
|
|
Cmd: cmd,
|
|
order: make([]order, 0),
|
|
startAt: time.Now(),
|
|
}
|
|
|
|
var cmdOutput strings.Builder
|
|
cmd.SetOutput(&cmdOutput)
|
|
cmd.Usage = func() {
|
|
utils.Printf("Usage of %s:\n", filepath.Base(os.Args[0]))
|
|
for _, o := range conv.order {
|
|
switch v := o.(type) {
|
|
case orderSection:
|
|
utils.Printf("\n%s:\n", o.Value())
|
|
case orderName:
|
|
utils.Println(conv.Usage(v.isString, cmd.Lookup(v.Value())))
|
|
}
|
|
}
|
|
if cmdOutput.Len() > 0 {
|
|
utils.Printf("\nError: %s", cmdOutput.String())
|
|
}
|
|
}
|
|
|
|
return conv
|
|
}
|
|
|
|
// LoadConfig Load default options (config + default)
|
|
func (c *Converter) LoadConfig() error {
|
|
return c.Options.LoadConfig()
|
|
}
|
|
|
|
// AddSection Create a new section of config
|
|
func (c *Converter) AddSection(section string) {
|
|
c.order = append(c.order, orderSection{value: section})
|
|
}
|
|
|
|
// AddStringParam Add a string parameter
|
|
func (c *Converter) AddStringParam(p *string, name string, value string, usage string) {
|
|
c.Cmd.StringVar(p, name, value, usage)
|
|
c.order = append(c.order, orderName{value: name, isString: true})
|
|
}
|
|
|
|
// AddIntParam Add an integer parameter
|
|
func (c *Converter) AddIntParam(p *int, name string, value int, usage string) {
|
|
c.Cmd.IntVar(p, name, value, usage)
|
|
c.order = append(c.order, orderName{value: name})
|
|
}
|
|
|
|
// AddFloatParam Add an float parameter
|
|
func (c *Converter) AddFloatParam(p *float64, name string, value float64, usage string) {
|
|
c.Cmd.Float64Var(p, name, value, usage)
|
|
c.order = append(c.order, orderName{value: name})
|
|
}
|
|
|
|
// AddBoolParam Add a boolean parameter
|
|
func (c *Converter) AddBoolParam(p *bool, name string, value bool, usage string) {
|
|
c.Cmd.BoolVar(p, name, value, usage)
|
|
c.order = append(c.order, orderName{value: name})
|
|
}
|
|
|
|
// InitParse Initialize the parser with all section and parameter.
|
|
func (c *Converter) InitParse() {
|
|
c.AddSection("Output")
|
|
c.AddStringParam(&c.Options.Input, "input", "", "Source of comic to convert: directory, cbz, zip, cbr, rar, pdf")
|
|
c.AddStringParam(&c.Options.Output, "output", "", "Output of the EPUB (directory or EPUB): (default [INPUT].epub)")
|
|
c.AddStringParam(&c.Options.Author, "author", "GO Comic Converter", "Author of the EPUB")
|
|
c.AddStringParam(&c.Options.Title, "title", "", "Title of the EPUB")
|
|
|
|
c.AddSection("Config")
|
|
c.AddStringParam(&c.Options.Profile, "profile", c.Options.Profile, "Profile to use: \n"+c.Options.AvailableProfiles())
|
|
c.AddIntParam(&c.Options.Quality, "quality", c.Options.Quality, "Quality of the image")
|
|
c.AddBoolParam(&c.Options.Grayscale, "grayscale", c.Options.Grayscale, "Grayscale image. Ideal for eInk devices.")
|
|
c.AddIntParam(&c.Options.GrayscaleMode, "grayscale-mode", c.Options.GrayscaleMode, "Grayscale Mode\n0 = normal\n1 = average\n2 = luminance")
|
|
c.AddBoolParam(&c.Options.Crop, "crop", c.Options.Crop, "Crop images")
|
|
c.AddIntParam(&c.Options.CropRatioLeft, "crop-ratio-left", c.Options.CropRatioLeft, "Crop ratio left: ratio of pixels allow to be non blank while cutting on the left.")
|
|
c.AddIntParam(&c.Options.CropRatioUp, "crop-ratio-up", c.Options.CropRatioUp, "Crop ratio up: ratio of pixels allow to be non blank while cutting on the top.")
|
|
c.AddIntParam(&c.Options.CropRatioRight, "crop-ratio-right", c.Options.CropRatioRight, "Crop ratio right: ratio of pixels allow to be non blank while cutting on the right.")
|
|
c.AddIntParam(&c.Options.CropRatioBottom, "crop-ratio-bottom", c.Options.CropRatioBottom, "Crop ratio bottom: ratio of pixels allow to be non blank while cutting on the bottom.")
|
|
c.AddIntParam(&c.Options.CropLimit, "crop-limit", c.Options.CropLimit, "Crop limit: maximum number of cropping in percentage allowed. 0 mean unlimited.")
|
|
c.AddBoolParam(&c.Options.CropSkipIfLimitReached, "crop-skip-if-limit-reached", c.Options.CropSkipIfLimitReached, "Crop skip if limit reached.")
|
|
c.AddIntParam(&c.Options.Brightness, "brightness", c.Options.Brightness, "Brightness readjustment: between -100 and 100, > 0 lighter, < 0 darker")
|
|
c.AddIntParam(&c.Options.Contrast, "contrast", c.Options.Contrast, "Contrast readjustment: between -100 and 100, > 0 more contrast, < 0 less contrast")
|
|
c.AddBoolParam(&c.Options.AutoContrast, "autocontrast", c.Options.AutoContrast, "Improve contrast automatically")
|
|
c.AddBoolParam(&c.Options.AutoRotate, "autorotate", c.Options.AutoRotate, "Auto Rotate page when width > height")
|
|
c.AddBoolParam(&c.Options.AutoSplitDoublePage, "autosplitdoublepage", c.Options.AutoSplitDoublePage, "Auto Split double page when width > height")
|
|
c.AddBoolParam(&c.Options.KeepDoublePageIfSplit, "keepdoublepageifsplit", c.Options.KeepDoublePageIfSplit, "Keep the double page if split")
|
|
c.AddBoolParam(&c.Options.KeepSplitDoublePageAspect, "keepsplitdoublepageaspect", c.Options.KeepSplitDoublePageAspect, "Keep aspect of split part of a double page (best for landscape rendering)")
|
|
c.AddBoolParam(&c.Options.NoBlankImage, "noblankimage", c.Options.NoBlankImage, "Remove blank image")
|
|
c.AddBoolParam(&c.Options.Manga, "manga", c.Options.Manga, "Manga mode (right to left)")
|
|
c.AddBoolParam(&c.Options.HasCover, "hascover", c.Options.HasCover, "Has cover. Indicate if your comic have a cover. The first page will be used as a cover and include after the title.")
|
|
c.AddIntParam(&c.Options.LimitMb, "limitmb", c.Options.LimitMb, "Limit size of the EPUB: Default nolimit (0), Minimum 20")
|
|
c.AddBoolParam(&c.Options.StripFirstDirectoryFromToc, "strip", c.Options.StripFirstDirectoryFromToc, "Strip first directory from the TOC if only 1")
|
|
c.AddIntParam(&c.Options.SortPathMode, "sort", c.Options.SortPathMode, "Sort path mode\n0 = alpha for path and file\n1 = alphanumeric for path and alpha for file\n2 = alphanumeric for path and file")
|
|
c.AddStringParam(&c.Options.ForegroundColor, "foreground-color", c.Options.ForegroundColor, "Foreground color in hexadecimal format RGB. Black=000, White=FFF")
|
|
c.AddStringParam(&c.Options.BackgroundColor, "background-color", c.Options.BackgroundColor, "Background color in hexadecimal format RGB. Black=000, White=FFF, Light Gray=DDD, Dark Gray=777")
|
|
c.AddBoolParam(&c.Options.NoResize, "noresize", c.Options.NoResize, "Do not reduce image size if exceed device size")
|
|
c.AddStringParam(&c.Options.Format, "format", c.Options.Format, "Format of output images: jpeg (lossy), png (lossless)")
|
|
c.AddFloatParam(&c.Options.AspectRatio, "aspect-ratio", c.Options.AspectRatio, "Aspect ratio (height/width) of the output\n -1 = same as device\n 0 = same as source\n1.6 = amazon advice for kindle")
|
|
c.AddBoolParam(&c.Options.PortraitOnly, "portrait-only", c.Options.PortraitOnly, "Portrait only: force orientation to portrait only.")
|
|
c.AddIntParam(&c.Options.TitlePage, "titlepage", c.Options.TitlePage, "Title page\n0 = never\n1 = always\n2 = only if epub is split")
|
|
|
|
c.AddSection("Default config")
|
|
c.AddBoolParam(&c.Options.Show, "show", false, "Show your default parameters")
|
|
c.AddBoolParam(&c.Options.Save, "save", false, "Save your parameters as default")
|
|
c.AddBoolParam(&c.Options.Reset, "reset", false, "Reset your parameters to default")
|
|
|
|
c.AddSection("Shortcut")
|
|
c.AddBoolParam(&c.Options.Auto, "auto", false, "Activate all automatic options")
|
|
c.AddBoolParam(&c.Options.NoFilter, "nofilter", false, "Deactivate all filters")
|
|
c.AddBoolParam(&c.Options.MaxQuality, "maxquality", false, "Max quality: color png + noresize")
|
|
c.AddBoolParam(&c.Options.BestQuality, "bestquality", false, "Max quality: color jpg q100 + noresize")
|
|
c.AddBoolParam(&c.Options.GreatQuality, "greatquality", false, "Max quality: grayscale jpg q90 + noresize")
|
|
c.AddBoolParam(&c.Options.GoodQuality, "goodquality", false, "Max quality: grayscale jpg q90")
|
|
|
|
c.AddSection("Compatibility")
|
|
c.AddBoolParam(&c.Options.AppleBookCompatibility, "applebookcompatibility", c.Options.AppleBookCompatibility, "Apple book compatibility")
|
|
|
|
c.AddSection("Other")
|
|
c.AddIntParam(&c.Options.Workers, "workers", runtime.NumCPU(), "Number of workers")
|
|
c.AddBoolParam(&c.Options.Dry, "dry", false, "Dry run to show all options")
|
|
c.AddBoolParam(&c.Options.DryVerbose, "dry-verbose", false, "Display also sorted files after the TOC")
|
|
c.AddBoolParam(&c.Options.Quiet, "quiet", false, "Disable progress bar")
|
|
c.AddBoolParam(&c.Options.Json, "json", false, "Output progression and information in Json format")
|
|
c.AddBoolParam(&c.Options.Version, "version", false, "Show current and available version")
|
|
c.AddBoolParam(&c.Options.Help, "help", false, "Show this help message")
|
|
}
|
|
|
|
// Usage Customize version of FlagSet.PrintDefaults
|
|
func (c *Converter) Usage(isString bool, f *flag.Flag) string {
|
|
var b strings.Builder
|
|
b.WriteString(" -" + f.Name)
|
|
name, usage := flag.UnquoteUsage(f)
|
|
if len(name) > 0 {
|
|
b.WriteString(" ")
|
|
b.WriteString(name)
|
|
}
|
|
// Print the default value only if it differs to the zero value
|
|
// for this flag type.
|
|
if isZero, err := c.isZeroValue(f, f.DefValue); err != nil {
|
|
c.isZeroValueErrs = append(c.isZeroValueErrs, err)
|
|
} else if !isZero {
|
|
if isString {
|
|
b.WriteString(fmt.Sprintf(" (default %q)", f.DefValue))
|
|
} else {
|
|
b.WriteString(fmt.Sprintf(" (default %v)", f.DefValue))
|
|
}
|
|
}
|
|
|
|
// Boolean flags of one ASCII letter are so common we
|
|
// treat them specially, putting their usage on the same line.
|
|
if b.Len() <= 4 { // space, space, '-', 'x'.
|
|
b.WriteString("\t")
|
|
} else {
|
|
// Four spaces before the tab triggers good alignment
|
|
// for both 4- and 8-space tab stops.
|
|
b.WriteString("\n \t")
|
|
}
|
|
b.WriteString(strings.ReplaceAll(usage, "\n", "\n \t"))
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// Taken from flag package as it is private and needed for usage.
|
|
//
|
|
// isZeroValue determines whether the string represents the zero
|
|
// value for a flag.
|
|
func (c *Converter) isZeroValue(f *flag.Flag, value string) (ok bool, err error) {
|
|
// Build a zero value of the flag's Value type, and see if the
|
|
// result of calling its String method equals the value passed in.
|
|
// This works unless the Value type is itself an interface type.
|
|
typ := reflect.TypeOf(f.Value)
|
|
var z reflect.Value
|
|
if typ.Kind() == reflect.Pointer {
|
|
z = reflect.New(typ.Elem())
|
|
} else {
|
|
z = reflect.Zero(typ)
|
|
}
|
|
// Catch panics calling the String method, which shouldn't prevent the
|
|
// usage message from being printed, but that we should report to the
|
|
// user so that they know to fix their code.
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
if typ.Kind() == reflect.Pointer {
|
|
typ = typ.Elem()
|
|
}
|
|
err = fmt.Errorf("panic calling String method on zero %v for flag %s: %v", typ, f.Name, e)
|
|
}
|
|
}()
|
|
return value == z.Interface().(flag.Value).String(), nil
|
|
}
|
|
|
|
// Parse all parameters
|
|
func (c *Converter) Parse() {
|
|
_ = c.Cmd.Parse(os.Args[1:])
|
|
if c.Options.Help {
|
|
c.Cmd.Usage()
|
|
os.Exit(0)
|
|
}
|
|
|
|
if c.Options.Auto {
|
|
c.Options.AutoContrast = true
|
|
c.Options.AutoRotate = true
|
|
c.Options.AutoSplitDoublePage = true
|
|
}
|
|
|
|
if c.Options.MaxQuality {
|
|
c.Options.Format = "png"
|
|
c.Options.Grayscale = false
|
|
c.Options.NoResize = true
|
|
} else if c.Options.BestQuality {
|
|
c.Options.Format = "jpeg"
|
|
c.Options.Quality = 100
|
|
c.Options.Grayscale = false
|
|
c.Options.NoResize = true
|
|
} else if c.Options.GreatQuality {
|
|
c.Options.Format = "jpeg"
|
|
c.Options.Quality = 90
|
|
c.Options.Grayscale = true
|
|
c.Options.NoResize = true
|
|
} else if c.Options.GoodQuality {
|
|
c.Options.Format = "jpeg"
|
|
c.Options.Quality = 90
|
|
c.Options.Grayscale = true
|
|
c.Options.NoResize = false
|
|
}
|
|
|
|
if c.Options.NoFilter {
|
|
c.Options.Crop = false
|
|
c.Options.Brightness = 0
|
|
c.Options.Contrast = 0
|
|
c.Options.AutoContrast = false
|
|
c.Options.AutoRotate = false
|
|
c.Options.NoBlankImage = false
|
|
c.Options.NoResize = true
|
|
}
|
|
|
|
if c.Options.AppleBookCompatibility {
|
|
c.Options.AutoSplitDoublePage = true
|
|
c.Options.KeepDoublePageIfSplit = false
|
|
c.Options.KeepSplitDoublePageAspect = true
|
|
}
|
|
|
|
if c.Options.PortraitOnly {
|
|
c.Options.KeepSplitDoublePageAspect = false
|
|
}
|
|
}
|
|
|
|
// Validate Check parameters
|
|
func (c *Converter) Validate() error {
|
|
// Check input
|
|
if c.Options.Input == "" {
|
|
return errors.New("missing input")
|
|
}
|
|
|
|
fi, err := os.Stat(c.Options.Input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check Output
|
|
var defaultOutput string
|
|
inputBase := filepath.Clean(c.Options.Input)
|
|
if fi.IsDir() {
|
|
defaultOutput = inputBase + ".epub"
|
|
} else {
|
|
ext := filepath.Ext(inputBase)
|
|
defaultOutput = inputBase[0:len(inputBase)-len(ext)] + ".epub"
|
|
}
|
|
|
|
if c.Options.Output == "" {
|
|
c.Options.Output = defaultOutput
|
|
}
|
|
|
|
c.Options.Output = filepath.Clean(c.Options.Output)
|
|
if filepath.Ext(c.Options.Output) == ".epub" {
|
|
fo, err := os.Stat(filepath.Dir(c.Options.Output))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !fo.IsDir() {
|
|
return errors.New("parent of the output is not a directory")
|
|
}
|
|
} else {
|
|
fo, err := os.Stat(c.Options.Output)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !fo.IsDir() {
|
|
return errors.New("output must be an existing dir or end with .epub")
|
|
}
|
|
c.Options.Output = filepath.Join(
|
|
c.Options.Output,
|
|
filepath.Base(defaultOutput),
|
|
)
|
|
}
|
|
|
|
// Title
|
|
if c.Options.Title == "" {
|
|
ext := filepath.Ext(defaultOutput)
|
|
c.Options.Title = filepath.Base(defaultOutput[0 : len(defaultOutput)-len(ext)])
|
|
}
|
|
|
|
// Profile
|
|
if c.Options.Profile == "" {
|
|
return errors.New("profile missing")
|
|
}
|
|
|
|
if p := c.Options.GetProfile(); p == nil {
|
|
return fmt.Errorf("profile %q doesn't exists", c.Options.Profile)
|
|
}
|
|
|
|
// LimitMb
|
|
if c.Options.LimitMb < 20 && c.Options.LimitMb != 0 {
|
|
return errors.New("limitmb should be 0 or >= 20")
|
|
}
|
|
|
|
// Brightness
|
|
if c.Options.Brightness < -100 || c.Options.Brightness > 100 {
|
|
return errors.New("brightness should be between -100 and 100")
|
|
}
|
|
|
|
// Contrast
|
|
if c.Options.Contrast < -100 || c.Options.Contrast > 100 {
|
|
return errors.New("contrast should be between -100 and 100")
|
|
}
|
|
|
|
// SortPathMode
|
|
if c.Options.SortPathMode < 0 || c.Options.SortPathMode > 2 {
|
|
return errors.New("sort should be 0, 1 or 2")
|
|
}
|
|
|
|
// Color
|
|
colorRegex := regexp.MustCompile("^[0-9A-F]{3}$")
|
|
if !colorRegex.MatchString(c.Options.ForegroundColor) {
|
|
return errors.New("foreground color must have color format in hexadecimal: [0-9A-F]{3}")
|
|
}
|
|
|
|
if !colorRegex.MatchString(c.Options.BackgroundColor) {
|
|
return errors.New("background color must have color format in hexadecimal: [0-9A-F]{3}")
|
|
}
|
|
|
|
// Format
|
|
if !(c.Options.Format == "jpeg" || c.Options.Format == "png") {
|
|
return errors.New("format should be jpeg or png")
|
|
}
|
|
|
|
// Aspect Ratio
|
|
if c.Options.AspectRatio < 0 && c.Options.AspectRatio != -1 {
|
|
return errors.New("aspect ratio should be -1, 0 or > 0")
|
|
}
|
|
|
|
// Title Page
|
|
if c.Options.TitlePage < 0 || c.Options.TitlePage > 2 {
|
|
return errors.New("title page should be 0, 1 or 2")
|
|
}
|
|
|
|
// Grayscale Mode
|
|
if c.Options.GrayscaleMode < 0 || c.Options.GrayscaleMode > 2 {
|
|
return errors.New("grayscale mode should be 0, 1 or 2")
|
|
}
|
|
|
|
// crop
|
|
if c.Options.CropLimit < 0 || c.Options.CropLimit > 100 {
|
|
return errors.New("crop limit should be between 0 and 100")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Fatal Helper to show usage, err and exit 1
|
|
func (c *Converter) Fatal(err error) {
|
|
c.Cmd.Usage()
|
|
utils.Fatalf("\nError: %s\n", err)
|
|
}
|
|
|
|
func (c *Converter) Stats() {
|
|
// Display elapse time and memory usage
|
|
elapse := time.Since(c.startAt).Round(time.Millisecond)
|
|
var mem runtime.MemStats
|
|
runtime.ReadMemStats(&mem)
|
|
|
|
if c.Options.Json {
|
|
_ = json.NewEncoder(os.Stdout).Encode(map[string]any{
|
|
"type": "stats",
|
|
"data": map[string]any{
|
|
"elapse_ms": elapse.Milliseconds(),
|
|
"memory_usage_mb": mem.Sys / 1024 / 1024,
|
|
},
|
|
})
|
|
} else {
|
|
utils.Printf(
|
|
"Completed in %s, Memory usage %d Mb\n",
|
|
elapse,
|
|
mem.Sys/1024/1024,
|
|
)
|
|
}
|
|
}
|