mirror of
https://github.com/celogeek/go-comic-converter.git
synced 2025-05-25 00:02:37 +02:00
revamp command line
This commit is contained in:
parent
db4ba5ad5f
commit
49056e71d9
267
internal/converter/core.go
Normal file
267
internal/converter/core.go
Normal file
@ -0,0 +1,267 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type converterOrder struct {
|
||||
name string
|
||||
is_string bool
|
||||
|
||||
section string
|
||||
is_section bool
|
||||
}
|
||||
|
||||
type Converter struct {
|
||||
Options *Options
|
||||
Cmd *flag.FlagSet
|
||||
|
||||
order []converterOrder
|
||||
isZeroValueErrs []error
|
||||
}
|
||||
|
||||
func New() *Converter {
|
||||
options := NewOptions()
|
||||
cmd := flag.NewFlagSet("go-comic-converter", flag.ExitOnError)
|
||||
conv := &Converter{
|
||||
Options: options,
|
||||
Cmd: cmd,
|
||||
order: make([]converterOrder, 0),
|
||||
}
|
||||
|
||||
cmd.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n", filepath.Base(os.Args[0]))
|
||||
for _, o := range conv.order {
|
||||
if o.is_section {
|
||||
fmt.Fprintf(os.Stderr, "\n%s:\n", o.section)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, conv.Usage(o.is_string, cmd.Lookup(o.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conv
|
||||
}
|
||||
|
||||
func (c *Converter) LoadConfig() error {
|
||||
return c.Options.LoadDefault()
|
||||
}
|
||||
|
||||
func (c *Converter) AddSection(section string) {
|
||||
c.order = append(c.order, converterOrder{section: section, is_section: true})
|
||||
}
|
||||
|
||||
func (c *Converter) AddStringParam(p *string, name string, value string, usage string) {
|
||||
c.Cmd.StringVar(p, name, value, usage)
|
||||
c.order = append(c.order, converterOrder{name: name, is_string: true})
|
||||
}
|
||||
|
||||
func (c *Converter) AddIntParam(p *int, name string, value int, usage string) {
|
||||
c.Cmd.IntVar(p, name, value, usage)
|
||||
c.order = append(c.order, converterOrder{name: name})
|
||||
}
|
||||
|
||||
func (c *Converter) AddBoolParam(p *bool, name string, value bool, usage string) {
|
||||
c.Cmd.BoolVar(p, name, value, usage)
|
||||
c.order = append(c.order, converterOrder{name: name})
|
||||
}
|
||||
|
||||
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.AddIntParam(&c.Options.Workers, "workers", runtime.NumCPU(), "Number of workers")
|
||||
c.AddBoolParam(&c.Options.Dry, "dry", false, "Dry run to show all options")
|
||||
|
||||
c.AddSection("Config")
|
||||
c.AddStringParam(&c.Options.Profile, "profile", c.Options.Profile, fmt.Sprintf("Profile to use: \n%s", c.Options.profiles))
|
||||
c.AddIntParam(&c.Options.Quality, "quality", c.Options.Quality, "Quality of the image")
|
||||
c.AddBoolParam(&c.Options.Crop, "crop", c.Options.Crop, "Crop images")
|
||||
c.AddIntParam(&c.Options.Brightness, "brightness", c.Options.Brightness, "Brightness readjustement: between -100 and 100, > 0 lighter, < 0 darker")
|
||||
c.AddIntParam(&c.Options.Contrast, "contrast", c.Options.Contrast, "Contrast readjustement: between -100 and 100, > 0 more contrast, < 0 less contrast")
|
||||
c.AddBoolParam(&c.Options.AutoRotate, "autorotate", c.Options.AutoRotate, "Auto Rotate page when width > height")
|
||||
c.AddBoolParam(&c.Options.Auto, "auto", false, "Activate all automatic options")
|
||||
c.AddBoolParam(&c.Options.AutoSplitDoublePage, "autosplitdoublepage", c.Options.AutoSplitDoublePage, "Auto Split double page when width > height")
|
||||
c.AddBoolParam(&c.Options.NoBlankPage, "noblankpage", c.Options.NoBlankPage, "Remove blank pages")
|
||||
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.AddBoolParam(&c.Options.AddPanelView, "addpanelview", c.Options.AddPanelView, "Add an embeded panel view. On kindle you may not need this option as it is handled by the kindle.")
|
||||
c.AddIntParam(&c.Options.LimitMb, "limitmb", c.Options.LimitMb, "Limit size of the ePub: Default nolimit (0), Minimum 20")
|
||||
|
||||
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.AddSection("Other")
|
||||
c.AddBoolParam(&c.Options.Help, "help", false, "Show this help message")
|
||||
}
|
||||
|
||||
func (c *Converter) Usage(isString bool, f *flag.Flag) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, " -%s", f.Name) // Two spaces before -; see next two comments.
|
||||
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 {
|
||||
fmt.Fprintf(&b, " (default %q)", f.DefValue)
|
||||
} else {
|
||||
fmt.Fprintf(&b, " (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()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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.AutoRotate = true
|
||||
c.Options.AutoSplitDoublePage = true
|
||||
}
|
||||
}
|
||||
|
||||
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 = fmt.Sprintf("%s.epub", inputBase)
|
||||
} else {
|
||||
ext := filepath.Ext(inputBase)
|
||||
defaultOutput = fmt.Sprintf("%s.epub", inputBase[0:len(inputBase)-len(ext)])
|
||||
}
|
||||
|
||||
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 _, ok := c.Options.profiles[c.Options.Profile]; !ok {
|
||||
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")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Converter) Fatal(err error) {
|
||||
c.Cmd.Usage()
|
||||
fmt.Fprintf(os.Stderr, "\nError: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
160
internal/converter/option.go
Normal file
160
internal/converter/option.go
Normal file
@ -0,0 +1,160 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Input string `yaml:"-"`
|
||||
Output string `yaml:"-"`
|
||||
Author string `yaml:"-"`
|
||||
Title string `yaml:"-"`
|
||||
Auto bool `yaml:"-"`
|
||||
Workers int `yaml:"-"`
|
||||
Dry bool `yaml:"-"`
|
||||
Show bool `yaml:"-"`
|
||||
Save bool `yaml:"-"`
|
||||
Help bool `yaml:"-"`
|
||||
Profile string `yaml:"profile"`
|
||||
Quality int `yaml:"quality"`
|
||||
Crop bool `yaml:"crop"`
|
||||
Brightness int `yaml:"brightness"`
|
||||
Contrast int `yaml:"contrast"`
|
||||
AutoRotate bool `yaml:"auto_rotate"`
|
||||
AutoSplitDoublePage bool `yaml:"auto_split_double_page"`
|
||||
NoBlankPage bool `yaml:"no_blank_page"`
|
||||
Manga bool `yaml:"manga"`
|
||||
HasCover bool `yaml:"has_cover"`
|
||||
AddPanelView bool `yaml:"add_panel_view"`
|
||||
LimitMb int `yaml:"limit_mb"`
|
||||
|
||||
profiles Profiles
|
||||
}
|
||||
|
||||
func NewOptions() *Options {
|
||||
return &Options{
|
||||
Profile: "",
|
||||
Quality: 85,
|
||||
Crop: true,
|
||||
Brightness: 0,
|
||||
Contrast: 0,
|
||||
AutoRotate: false,
|
||||
AutoSplitDoublePage: false,
|
||||
NoBlankPage: false,
|
||||
Manga: false,
|
||||
HasCover: true,
|
||||
AddPanelView: false,
|
||||
LimitMb: 0,
|
||||
|
||||
profiles: NewProfile(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Options) Header() string {
|
||||
return `Go Comic Converter
|
||||
|
||||
Options:`
|
||||
}
|
||||
|
||||
func (o *Options) String() string {
|
||||
return fmt.Sprintf(`%s
|
||||
Input : %s
|
||||
Output : %s
|
||||
Author : %s
|
||||
Title : %s
|
||||
Workers : %d%s
|
||||
`,
|
||||
o.Header(),
|
||||
o.Input,
|
||||
o.Output,
|
||||
o.Author,
|
||||
o.Title,
|
||||
o.Workers,
|
||||
o.ShowDefault(),
|
||||
)
|
||||
}
|
||||
|
||||
func (o *Options) FileName() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".go-comic-converter.yaml")
|
||||
}
|
||||
|
||||
func (o *Options) LoadDefault() error {
|
||||
f, err := os.Open(o.FileName())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
err = yaml.NewDecoder(f).Decode(o)
|
||||
if err != nil && err.Error() != "EOF" {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Options) GetProfile() *Profile {
|
||||
if profile, ok := o.profiles[o.Profile]; ok {
|
||||
return &profile
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Options) ShowDefault() string {
|
||||
var profileDesc string
|
||||
profile := o.GetProfile()
|
||||
if profile != nil {
|
||||
profileDesc = fmt.Sprintf(
|
||||
"%s - %s - %dx%d - %d levels of gray",
|
||||
o.Profile,
|
||||
profile.Description,
|
||||
profile.Width,
|
||||
profile.Height,
|
||||
len(profile.Palette),
|
||||
)
|
||||
}
|
||||
limitmb := "nolimit"
|
||||
if o.LimitMb > 0 {
|
||||
limitmb = fmt.Sprintf("%d Mb", o.LimitMb)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
Profile : %s
|
||||
Quality : %d
|
||||
Crop : %v
|
||||
Brightness : %d
|
||||
Contrast : %d
|
||||
AutoRotate : %v
|
||||
AutoSplitDoublePage: %v
|
||||
NoBlankPage : %v
|
||||
Manga : %v
|
||||
HasCover : %v
|
||||
AddPanelView : %v
|
||||
LimitMb : %s`,
|
||||
profileDesc,
|
||||
o.Quality,
|
||||
o.Crop,
|
||||
o.Brightness,
|
||||
o.Contrast,
|
||||
o.AutoRotate,
|
||||
o.AutoSplitDoublePage,
|
||||
o.NoBlankPage,
|
||||
o.Manga,
|
||||
o.HasCover,
|
||||
o.AddPanelView,
|
||||
limitmb,
|
||||
)
|
||||
}
|
||||
|
||||
func (o *Options) SaveDefault() error {
|
||||
f, err := os.Create(o.FileName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return yaml.NewEncoder(f).Encode(o)
|
||||
}
|
69
internal/converter/profile.go
Normal file
69
internal/converter/profile.go
Normal file
@ -0,0 +1,69 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
"github.com/celogeek/go-comic-converter/internal/epub"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
Code string
|
||||
Description string
|
||||
Width int
|
||||
Height int
|
||||
Palette color.Palette
|
||||
}
|
||||
|
||||
type Profiles map[string]Profile
|
||||
|
||||
var profiles = []Profile{
|
||||
{"KS", "Kindle 1", 600, 670, epub.PALETTE_4},
|
||||
{"K11", "Kindle 11", 1072, 1448, epub.PALETTE_16},
|
||||
{"K2", "Kindle 2", 600, 670, epub.PALETTE_15},
|
||||
{"K34", "Kindle Keyboard/Touch", 600, 800, epub.PALETTE_16},
|
||||
{"K578", "Kindle", 600, 800, epub.PALETTE_16},
|
||||
{"KDX", "Kindle DX/DXG", 824, 1000, epub.PALETTE_16},
|
||||
{"KPW", "Kindle Paperwhite 1/2", 758, 1024, epub.PALETTE_16},
|
||||
{"KV", "Kindle Paperwhite 3/4/Voyage/Oasis", 1072, 1448, epub.PALETTE_16},
|
||||
{"KPW5", "Kindle Paperwhite 5/Signature Edition", 1236, 1648, epub.PALETTE_16},
|
||||
{"KO", "Kindle Oasis 2/3", 1264, 1680, epub.PALETTE_16},
|
||||
{"KS", "Kindle Scribe", 1860, 2480, epub.PALETTE_16},
|
||||
// Kobo
|
||||
{"KoMT", "Kobo Mini/Touch", 600, 800, epub.PALETTE_16},
|
||||
{"KoG", "Kobo Glo", 768, 1024, epub.PALETTE_16},
|
||||
{"KoGHD", "Kobo Glo HD", 1072, 1448, epub.PALETTE_16},
|
||||
{"KoA", "Kobo Aura", 758, 1024, epub.PALETTE_16},
|
||||
{"KoAHD", "Kobo Aura HD", 1080, 1440, epub.PALETTE_16},
|
||||
{"KoAH2O", "Kobo Aura H2O", 1080, 1430, epub.PALETTE_16},
|
||||
{"KoAO", "Kobo Aura ONE", 1404, 1872, epub.PALETTE_16},
|
||||
{"KoN", "Kobo Nia", 758, 1024, epub.PALETTE_16},
|
||||
{"KoC", "Kobo Clara HD/Kobo Clara 2E", 1072, 1448, epub.PALETTE_16},
|
||||
{"KoL", "Kobo Libra H2O/Kobo Libra 2", 1264, 1680, epub.PALETTE_16},
|
||||
{"KoF", "Kobo Forma", 1440, 1920, epub.PALETTE_16},
|
||||
{"KoS", "Kobo Sage", 1440, 1920, epub.PALETTE_16},
|
||||
{"KoE", "Kobo Elipsa", 1404, 1872, epub.PALETTE_16},
|
||||
}
|
||||
|
||||
func NewProfile() Profiles {
|
||||
r := Profiles{}
|
||||
for _, profile := range profiles {
|
||||
r[profile.Code] = profile
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (p Profiles) String() string {
|
||||
s := make([]string, 0)
|
||||
for _, v := range profiles {
|
||||
s = append(s, fmt.Sprintf(
|
||||
" - %-7s ( %9s ) - %2d levels of gray - %s",
|
||||
v.Code,
|
||||
fmt.Sprintf("%dx%d", v.Width, v.Height),
|
||||
len(v.Palette),
|
||||
v.Description,
|
||||
))
|
||||
}
|
||||
return strings.Join(s, "\n")
|
||||
}
|
360
main.go
360
main.go
@ -1,348 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/celogeek/go-comic-converter/internal/converter"
|
||||
"github.com/celogeek/go-comic-converter/internal/epub"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
Code string
|
||||
Description string
|
||||
Width int
|
||||
Height int
|
||||
Palette color.Palette
|
||||
}
|
||||
|
||||
var Profiles = []Profile{
|
||||
// Kindle
|
||||
{"K1", "Kindle 1", 600, 670, epub.PALETTE_4},
|
||||
{"K11", "Kindle 11", 1072, 1448, epub.PALETTE_16},
|
||||
{"K2", "Kindle 2", 600, 670, epub.PALETTE_15},
|
||||
{"K34", "Kindle Keyboard/Touch", 600, 800, epub.PALETTE_16},
|
||||
{"K578", "Kindle", 600, 800, epub.PALETTE_16},
|
||||
{"KDX", "Kindle DX/DXG", 824, 1000, epub.PALETTE_16},
|
||||
{"KPW", "Kindle Paperwhite 1/2", 758, 1024, epub.PALETTE_16},
|
||||
{"KV", "Kindle Paperwhite 3/4/Voyage/Oasis", 1072, 1448, epub.PALETTE_16},
|
||||
{"KPW5", "Kindle Paperwhite 5/Signature Edition", 1236, 1648, epub.PALETTE_16},
|
||||
{"KO", "Kindle Oasis 2/3", 1264, 1680, epub.PALETTE_16},
|
||||
{"KS", "Kindle Scribe", 1860, 2480, epub.PALETTE_16},
|
||||
// Kobo
|
||||
{"KoMT", "Kobo Mini/Touch", 600, 800, epub.PALETTE_16},
|
||||
{"KoG", "Kobo Glo", 768, 1024, epub.PALETTE_16},
|
||||
{"KoGHD", "Kobo Glo HD", 1072, 1448, epub.PALETTE_16},
|
||||
{"KoA", "Kobo Aura", 758, 1024, epub.PALETTE_16},
|
||||
{"KoAHD", "Kobo Aura HD", 1080, 1440, epub.PALETTE_16},
|
||||
{"KoAH2O", "Kobo Aura H2O", 1080, 1430, epub.PALETTE_16},
|
||||
{"KoAO", "Kobo Aura ONE", 1404, 1872, epub.PALETTE_16},
|
||||
{"KoN", "Kobo Nia", 758, 1024, epub.PALETTE_16},
|
||||
{"KoC", "Kobo Clara HD/Kobo Clara 2E", 1072, 1448, epub.PALETTE_16},
|
||||
{"KoL", "Kobo Libra H2O/Kobo Libra 2", 1264, 1680, epub.PALETTE_16},
|
||||
{"KoF", "Kobo Forma", 1440, 1920, epub.PALETTE_16},
|
||||
{"KoS", "Kobo Sage", 1440, 1920, epub.PALETTE_16},
|
||||
{"KoE", "Kobo Elipsa", 1404, 1872, epub.PALETTE_16},
|
||||
}
|
||||
var ProfilesIdx = map[string]int{}
|
||||
|
||||
func init() {
|
||||
for i, p := range Profiles {
|
||||
ProfilesIdx[p.Code] = i
|
||||
}
|
||||
}
|
||||
|
||||
var Home, _ = os.UserHomeDir()
|
||||
var ConfigFile = filepath.Join(Home, ".go-comic-converter.yaml")
|
||||
|
||||
type Config struct {
|
||||
Profile string `yaml:"profile"`
|
||||
Quality int `yaml:"quality"`
|
||||
Crop bool `yaml:"crop"`
|
||||
Brightness int `yaml:"brightness"`
|
||||
Contrast int `yaml:"contrast"`
|
||||
AutoRotate bool `yaml:"auto_rotate"`
|
||||
AutoSplitDoublePage bool `yaml:"auto_split_double_page"`
|
||||
NoBlankPage bool `yaml:"no_blank_page"`
|
||||
Manga bool `yaml:"manga"`
|
||||
HasCover bool `yaml:"has_cover"`
|
||||
AddPanelView bool `yaml:"add_panel_view"`
|
||||
LimitMb int `yaml:"limit_mb"`
|
||||
}
|
||||
|
||||
type Option struct {
|
||||
Input string
|
||||
Output string
|
||||
Profile string
|
||||
Author string
|
||||
Title string
|
||||
Quality int
|
||||
Crop bool
|
||||
Brightness int
|
||||
Contrast int
|
||||
Auto bool
|
||||
AutoRotate bool
|
||||
AutoSplitDoublePage bool
|
||||
NoBlankPage bool
|
||||
Manga bool
|
||||
HasCover bool
|
||||
AddPanelView bool
|
||||
Workers int
|
||||
LimitMb int
|
||||
Save bool
|
||||
}
|
||||
|
||||
func (o *Option) String() string {
|
||||
var desc string
|
||||
var width, height, level int
|
||||
if i, ok := ProfilesIdx[o.Profile]; ok {
|
||||
profile := Profiles[i]
|
||||
desc = profile.Description
|
||||
width = profile.Width
|
||||
height = profile.Height
|
||||
level = len(profile.Palette)
|
||||
}
|
||||
limitmb := "nolimit"
|
||||
if o.LimitMb > 0 {
|
||||
limitmb = fmt.Sprintf("%d Mb", o.LimitMb)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`Go Comic Converter
|
||||
|
||||
Options:
|
||||
Input : %s
|
||||
Output : %s
|
||||
Profile : %s - %s - %dx%d - %d levels of gray
|
||||
Author : %s
|
||||
Title : %s
|
||||
Quality : %d
|
||||
Crop : %v
|
||||
Brightness : %d
|
||||
Contrast : %d
|
||||
AutoRotate : %v
|
||||
AutoSplitDoublePage: %v
|
||||
NoBlankPage : %v
|
||||
Manga : %v
|
||||
HasCover : %v
|
||||
AddPanelView : %v
|
||||
LimitMb : %s
|
||||
Workers : %d
|
||||
`,
|
||||
o.Input,
|
||||
o.Output,
|
||||
o.Profile, desc, width, height, level,
|
||||
o.Author,
|
||||
o.Title,
|
||||
o.Quality,
|
||||
o.Crop,
|
||||
o.Brightness,
|
||||
o.Contrast,
|
||||
o.AutoRotate,
|
||||
o.AutoSplitDoublePage,
|
||||
o.NoBlankPage,
|
||||
o.Manga,
|
||||
o.HasCover,
|
||||
o.AddPanelView,
|
||||
limitmb,
|
||||
o.Workers,
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
availableProfiles := make([]string, 0)
|
||||
for _, p := range Profiles {
|
||||
availableProfiles = append(availableProfiles, fmt.Sprintf(
|
||||
" - %-7s ( %9s ) - %2d levels of gray - %s",
|
||||
p.Code,
|
||||
fmt.Sprintf("%dx%d", p.Width, p.Height),
|
||||
len(p.Palette),
|
||||
p.Description,
|
||||
))
|
||||
cmd := converter.New()
|
||||
if err := cmd.LoadConfig(); err != nil {
|
||||
cmd.Fatal(err)
|
||||
}
|
||||
cmd.InitParse()
|
||||
cmd.Parse()
|
||||
|
||||
defaultOpt := &Config{
|
||||
Profile: "",
|
||||
Quality: 85,
|
||||
Crop: true,
|
||||
Brightness: 0,
|
||||
Contrast: 0,
|
||||
AutoRotate: false,
|
||||
AutoSplitDoublePage: false,
|
||||
NoBlankPage: false,
|
||||
Manga: false,
|
||||
HasCover: true,
|
||||
AddPanelView: false,
|
||||
LimitMb: 0,
|
||||
}
|
||||
configHandler, err := os.Open(ConfigFile)
|
||||
if err == nil {
|
||||
defer configHandler.Close()
|
||||
err = yaml.NewDecoder(configHandler).Decode(defaultOpt)
|
||||
if err != nil && err.Error() != "EOF" {
|
||||
fmt.Fprintf(os.Stderr, "Error detected in your config %q\n", ConfigFile)
|
||||
fmt.Fprint(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
opt := &Option{}
|
||||
flag.StringVar(&opt.Input, "input", "", "Source of comic to convert: directory, cbz, zip, cbr, rar, pdf")
|
||||
flag.StringVar(&opt.Output, "output", "", "Output of the epub (directory or epub): (default [INPUT].epub)")
|
||||
flag.StringVar(&opt.Profile, "profile", defaultOpt.Profile, fmt.Sprintf("Profile to use: \n%s\n", strings.Join(availableProfiles, "\n")))
|
||||
flag.StringVar(&opt.Author, "author", "GO Comic Converter", "Author of the epub")
|
||||
flag.StringVar(&opt.Title, "title", "", "Title of the epub")
|
||||
flag.IntVar(&opt.Quality, "quality", defaultOpt.Quality, "Quality of the image")
|
||||
flag.BoolVar(&opt.Crop, "crop", defaultOpt.Crop, "Crop images")
|
||||
flag.IntVar(&opt.Brightness, "brightness", defaultOpt.Brightness, "Brightness readjustement: between -100 and 100, > 0 lighter, < 0 darker")
|
||||
flag.IntVar(&opt.Contrast, "contrast", defaultOpt.Contrast, "Contrast readjustement: between -100 and 100, > 0 more contrast, < 0 less contrast")
|
||||
flag.BoolVar(&opt.Auto, "auto", false, "Activate all automatic options")
|
||||
flag.BoolVar(&opt.AutoRotate, "autorotate", defaultOpt.AutoRotate, "Auto Rotate page when width > height")
|
||||
flag.BoolVar(&opt.AutoSplitDoublePage, "autosplitdoublepage", defaultOpt.AutoSplitDoublePage, "Auto Split double page when width > height")
|
||||
flag.BoolVar(&opt.NoBlankPage, "noblankpage", defaultOpt.NoBlankPage, "Remove blank pages")
|
||||
flag.BoolVar(&opt.Manga, "manga", defaultOpt.Manga, "Manga mode (right to left)")
|
||||
flag.BoolVar(&opt.HasCover, "hascover", defaultOpt.HasCover, "Has cover. Indicate if your comic have a cover. The first page will be used as a cover and include after the title.")
|
||||
flag.BoolVar(&opt.AddPanelView, "addpanelview", defaultOpt.AddPanelView, "Add an embeded panel view. On kindle you may not need this option as it is handled by the kindle.")
|
||||
flag.IntVar(&opt.LimitMb, "limitmb", defaultOpt.LimitMb, "Limit size of the ePub: Default nolimit (0), Minimum 20")
|
||||
flag.IntVar(&opt.Workers, "workers", runtime.NumCPU(), "Number of workers")
|
||||
flag.BoolVar(&opt.Save, "save", false, "Save your parameters as default.")
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n", filepath.Base(os.Args[0]))
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
if opt.Auto {
|
||||
opt.AutoRotate = true
|
||||
opt.AutoSplitDoublePage = true
|
||||
}
|
||||
|
||||
if opt.Save {
|
||||
f, err := os.Create(ConfigFile)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
yaml.NewEncoder(f).Encode(&Config{
|
||||
Profile: opt.Profile,
|
||||
Quality: opt.Quality,
|
||||
Crop: opt.Crop,
|
||||
Brightness: opt.Brightness,
|
||||
Contrast: opt.Contrast,
|
||||
AutoRotate: opt.AutoRotate,
|
||||
AutoSplitDoublePage: opt.AutoSplitDoublePage,
|
||||
NoBlankPage: opt.NoBlankPage,
|
||||
Manga: opt.Manga,
|
||||
HasCover: opt.HasCover,
|
||||
AddPanelView: opt.AddPanelView,
|
||||
LimitMb: opt.LimitMb,
|
||||
})
|
||||
fmt.Fprintf(os.Stderr, "Default settings saved in %q\n", ConfigFile)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if opt.Input == "" {
|
||||
fmt.Fprintln(os.Stderr, "Missing input or output!")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var defaultOutput string
|
||||
fi, err := os.Stat(opt.Input)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
inputBase := filepath.Clean(opt.Input)
|
||||
if fi.IsDir() {
|
||||
defaultOutput = fmt.Sprintf("%s.epub", inputBase)
|
||||
} else {
|
||||
ext := filepath.Ext(inputBase)
|
||||
defaultOutput = fmt.Sprintf("%s.epub", inputBase[0:len(inputBase)-len(ext)])
|
||||
}
|
||||
|
||||
if opt.Output == "" {
|
||||
opt.Output = defaultOutput
|
||||
}
|
||||
|
||||
if filepath.Ext(opt.Output) != ".epub" {
|
||||
fo, err := os.Stat(opt.Output)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if !fo.IsDir() {
|
||||
fmt.Fprintln(os.Stderr, "output must be an existing dir or end with .epub")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
opt.Output = filepath.Join(
|
||||
opt.Output,
|
||||
filepath.Base(defaultOutput),
|
||||
if cmd.Options.Save {
|
||||
cmd.Options.SaveDefault()
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"%s%s\n\nSaving to %s.\n",
|
||||
cmd.Options.Header(),
|
||||
cmd.Options.ShowDefault(),
|
||||
cmd.Options.FileName(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
profileIdx, profileMatch := ProfilesIdx[opt.Profile]
|
||||
if !profileMatch {
|
||||
fmt.Fprintf(os.Stderr, "Profile %q doesn't exists!\n", opt.Profile)
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
profile := Profiles[profileIdx]
|
||||
|
||||
if opt.LimitMb > 0 && opt.LimitMb < 20 {
|
||||
fmt.Fprintln(os.Stderr, "LimitMb should be 0 or >= 20")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
if cmd.Options.Show {
|
||||
fmt.Fprintln(os.Stderr, cmd.Options.Header(), cmd.Options.ShowDefault())
|
||||
return
|
||||
}
|
||||
|
||||
if opt.Brightness < -100 || opt.Brightness > 100 {
|
||||
fmt.Fprintln(os.Stderr, "Brightness should be between -100 and 100")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
if err := cmd.Validate(); err != nil {
|
||||
cmd.Fatal(err)
|
||||
}
|
||||
|
||||
if opt.Contrast < -100 || opt.Contrast > 100 {
|
||||
fmt.Fprintln(os.Stderr, "Contrast should be between -100 and 100")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
fmt.Fprintln(os.Stderr, cmd.Options)
|
||||
|
||||
if cmd.Options.Dry {
|
||||
return
|
||||
}
|
||||
|
||||
if opt.Title == "" {
|
||||
ext := filepath.Ext(defaultOutput)
|
||||
opt.Title = filepath.Base(defaultOutput[0 : len(defaultOutput)-len(ext)])
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, opt)
|
||||
|
||||
profile := cmd.Options.GetProfile()
|
||||
if err := epub.NewEpub(&epub.EpubOptions{
|
||||
Input: opt.Input,
|
||||
Output: opt.Output,
|
||||
LimitMb: opt.LimitMb,
|
||||
Title: opt.Title,
|
||||
Author: opt.Author,
|
||||
Input: cmd.Options.Input,
|
||||
Output: cmd.Options.Output,
|
||||
LimitMb: cmd.Options.LimitMb,
|
||||
Title: cmd.Options.Title,
|
||||
Author: cmd.Options.Author,
|
||||
ImageOptions: &epub.ImageOptions{
|
||||
ViewWidth: profile.Width,
|
||||
ViewHeight: profile.Height,
|
||||
Quality: opt.Quality,
|
||||
Crop: opt.Crop,
|
||||
Quality: cmd.Options.Quality,
|
||||
Crop: cmd.Options.Crop,
|
||||
Palette: profile.Palette,
|
||||
Brightness: opt.Brightness,
|
||||
Contrast: opt.Contrast,
|
||||
AutoRotate: opt.AutoRotate,
|
||||
AutoSplitDoublePage: opt.AutoSplitDoublePage,
|
||||
NoBlankPage: opt.NoBlankPage,
|
||||
Manga: opt.Manga,
|
||||
HasCover: opt.HasCover,
|
||||
AddPanelView: opt.AddPanelView,
|
||||
Workers: opt.Workers,
|
||||
Brightness: cmd.Options.Brightness,
|
||||
Contrast: cmd.Options.Contrast,
|
||||
AutoRotate: cmd.Options.AutoRotate,
|
||||
AutoSplitDoublePage: cmd.Options.AutoSplitDoublePage,
|
||||
NoBlankPage: cmd.Options.NoBlankPage,
|
||||
Manga: cmd.Options.Manga,
|
||||
HasCover: cmd.Options.HasCover,
|
||||
AddPanelView: cmd.Options.AddPanelView,
|
||||
Workers: cmd.Options.Workers,
|
||||
},
|
||||
}).Write(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
|
Loading…
x
Reference in New Issue
Block a user