diff --git a/internal/converter/core.go b/internal/converter/core.go new file mode 100644 index 0000000..d432517 --- /dev/null +++ b/internal/converter/core.go @@ -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) +} diff --git a/internal/converter/option.go b/internal/converter/option.go new file mode 100644 index 0000000..968eb57 --- /dev/null +++ b/internal/converter/option.go @@ -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) +} diff --git a/internal/converter/profile.go b/internal/converter/profile.go new file mode 100644 index 0000000..e6a8bbe --- /dev/null +++ b/internal/converter/profile.go @@ -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") +} diff --git a/main.go b/main.go index 363fdb5..187ba9c 100644 --- a/main.go +++ b/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)