mirror of
https://github.com/celogeek/go-comic-converter.git
synced 2025-05-27 17:39:55 +02:00
Compare commits
No commits in common. "0263a643215f983030d52b03b79c43a0d56f92a6" and "d0d9af7f9beb8fb40d17b8f809cdf08a8c9a854c" have entirely different histories.
0263a64321
...
d0d9af7f9b
@ -1,10 +1,3 @@
|
|||||||
/*
|
|
||||||
Converter Helper to parse and prepare options for go-comic-converter.
|
|
||||||
|
|
||||||
It use goflag with additional feature:
|
|
||||||
- Keep original order
|
|
||||||
- Support section
|
|
||||||
*/
|
|
||||||
package converter
|
package converter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -24,18 +17,17 @@ type Converter struct {
|
|||||||
Options *options.Options
|
Options *options.Options
|
||||||
Cmd *flag.FlagSet
|
Cmd *flag.FlagSet
|
||||||
|
|
||||||
order []converterOrder
|
order []Order
|
||||||
isZeroValueErrs []error
|
isZeroValueErrs []error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new parser
|
|
||||||
func New() *Converter {
|
func New() *Converter {
|
||||||
options := options.New()
|
options := options.New()
|
||||||
cmd := flag.NewFlagSet("go-comic-converter", flag.ExitOnError)
|
cmd := flag.NewFlagSet("go-comic-converter", flag.ExitOnError)
|
||||||
conv := &Converter{
|
conv := &Converter{
|
||||||
Options: options,
|
Options: options,
|
||||||
Cmd: cmd,
|
Cmd: cmd,
|
||||||
order: make([]converterOrder, 0),
|
order: make([]Order, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdOutput strings.Builder
|
var cmdOutput strings.Builder
|
||||||
@ -44,9 +36,9 @@ func New() *Converter {
|
|||||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n", filepath.Base(os.Args[0]))
|
fmt.Fprintf(os.Stderr, "Usage of %s:\n", filepath.Base(os.Args[0]))
|
||||||
for _, o := range conv.order {
|
for _, o := range conv.order {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case converterOrderSection:
|
case OrderSection:
|
||||||
fmt.Fprintf(os.Stderr, "\n%s:\n", o.Value())
|
fmt.Fprintf(os.Stderr, "\n%s:\n", o.Value())
|
||||||
case converterOrderName:
|
case OrderName:
|
||||||
fmt.Fprintln(os.Stderr, conv.Usage(v.isString, cmd.Lookup(v.Value())))
|
fmt.Fprintln(os.Stderr, conv.Usage(v.isString, cmd.Lookup(v.Value())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,35 +50,29 @@ func New() *Converter {
|
|||||||
return conv
|
return conv
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load default options (config + default)
|
|
||||||
func (c *Converter) LoadConfig() error {
|
func (c *Converter) LoadConfig() error {
|
||||||
return c.Options.LoadConfig()
|
return c.Options.LoadDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new section of config
|
|
||||||
func (c *Converter) AddSection(section string) {
|
func (c *Converter) AddSection(section string) {
|
||||||
c.order = append(c.order, converterOrderSection{value: section})
|
c.order = append(c.order, OrderSection{value: section})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a string parameter
|
|
||||||
func (c *Converter) AddStringParam(p *string, name string, value string, usage string) {
|
func (c *Converter) AddStringParam(p *string, name string, value string, usage string) {
|
||||||
c.Cmd.StringVar(p, name, value, usage)
|
c.Cmd.StringVar(p, name, value, usage)
|
||||||
c.order = append(c.order, converterOrderName{value: name, isString: true})
|
c.order = append(c.order, OrderName{value: name, isString: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add an integer parameter
|
|
||||||
func (c *Converter) AddIntParam(p *int, name string, value int, usage string) {
|
func (c *Converter) AddIntParam(p *int, name string, value int, usage string) {
|
||||||
c.Cmd.IntVar(p, name, value, usage)
|
c.Cmd.IntVar(p, name, value, usage)
|
||||||
c.order = append(c.order, converterOrderName{value: name})
|
c.order = append(c.order, OrderName{value: name})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a boolean parameter
|
|
||||||
func (c *Converter) AddBoolParam(p *bool, name string, value bool, usage string) {
|
func (c *Converter) AddBoolParam(p *bool, name string, value bool, usage string) {
|
||||||
c.Cmd.BoolVar(p, name, value, usage)
|
c.Cmd.BoolVar(p, name, value, usage)
|
||||||
c.order = append(c.order, converterOrderName{value: name})
|
c.order = append(c.order, OrderName{value: name})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the parser with all section and parameter.
|
|
||||||
func (c *Converter) InitParse() {
|
func (c *Converter) InitParse() {
|
||||||
c.AddSection("Output")
|
c.AddSection("Output")
|
||||||
c.AddStringParam(&c.Options.Input, "input", "", "Source of comic to convert: directory, cbz, zip, cbr, rar, pdf")
|
c.AddStringParam(&c.Options.Input, "input", "", "Source of comic to convert: directory, cbz, zip, cbr, rar, pdf")
|
||||||
@ -124,7 +110,6 @@ func (c *Converter) InitParse() {
|
|||||||
c.AddBoolParam(&c.Options.Help, "help", false, "Show this help message")
|
c.AddBoolParam(&c.Options.Help, "help", false, "Show this help message")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Customize version of FlagSet.PrintDefaults
|
|
||||||
func (c *Converter) Usage(isString bool, f *flag.Flag) string {
|
func (c *Converter) Usage(isString bool, f *flag.Flag) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
fmt.Fprintf(&b, " -%s", f.Name) // Two spaces before -; see next two comments.
|
fmt.Fprintf(&b, " -%s", f.Name) // Two spaces before -; see next two comments.
|
||||||
@ -159,8 +144,6 @@ func (c *Converter) Usage(isString bool, f *flag.Flag) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Taken from flag package as it is private and needed for usage.
|
|
||||||
//
|
|
||||||
// isZeroValue determines whether the string represents the zero
|
// isZeroValue determines whether the string represents the zero
|
||||||
// value for a flag.
|
// value for a flag.
|
||||||
func (c *Converter) isZeroValue(f *flag.Flag, value string) (ok bool, err error) {
|
func (c *Converter) isZeroValue(f *flag.Flag, value string) (ok bool, err error) {
|
||||||
@ -188,7 +171,6 @@ func (c *Converter) isZeroValue(f *flag.Flag, value string) (ok bool, err error)
|
|||||||
return value == z.Interface().(flag.Value).String(), nil
|
return value == z.Interface().(flag.Value).String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse all parameters
|
|
||||||
func (c *Converter) Parse() {
|
func (c *Converter) Parse() {
|
||||||
c.Cmd.Parse(os.Args[1:])
|
c.Cmd.Parse(os.Args[1:])
|
||||||
if c.Options.Help {
|
if c.Options.Help {
|
||||||
@ -202,7 +184,6 @@ func (c *Converter) Parse() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check parameters
|
|
||||||
func (c *Converter) Validate() error {
|
func (c *Converter) Validate() error {
|
||||||
// Check input
|
// Check input
|
||||||
if c.Options.Input == "" {
|
if c.Options.Input == "" {
|
||||||
@ -281,7 +262,6 @@ func (c *Converter) Validate() error {
|
|||||||
return errors.New("contrast should be between -100 and 100")
|
return errors.New("contrast should be between -100 and 100")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortPathMode
|
|
||||||
if c.Options.SortPathMode < 0 || c.Options.SortPathMode > 2 {
|
if c.Options.SortPathMode < 0 || c.Options.SortPathMode > 2 {
|
||||||
return errors.New("sort should be 0, 1 or 2")
|
return errors.New("sort should be 0, 1 or 2")
|
||||||
}
|
}
|
||||||
@ -289,7 +269,6 @@ func (c *Converter) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to show usage, err and exit 1
|
|
||||||
func (c *Converter) Fatal(err error) {
|
func (c *Converter) Fatal(err error) {
|
||||||
c.Cmd.Usage()
|
c.Cmd.Usage()
|
||||||
fmt.Fprintf(os.Stderr, "\nError: %s\n", err)
|
fmt.Fprintf(os.Stderr, "\nError: %s\n", err)
|
||||||
|
@ -1,27 +1,22 @@
|
|||||||
package converter
|
package converter
|
||||||
|
|
||||||
// Name or Section
|
type Order interface {
|
||||||
type converterOrder interface {
|
|
||||||
Value() string
|
Value() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section
|
type OrderSection struct {
|
||||||
type converterOrderSection struct {
|
|
||||||
value string
|
value string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s converterOrderSection) Value() string {
|
func (s OrderSection) Value() string {
|
||||||
return s.value
|
return s.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name
|
type OrderName struct {
|
||||||
//
|
|
||||||
// isString is used to quote the default value.
|
|
||||||
type converterOrderName struct {
|
|
||||||
value string
|
value string
|
||||||
isString bool
|
isString bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s converterOrderName) Value() string {
|
func (s OrderName) Value() string {
|
||||||
return s.value
|
return s.value
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
/*
|
|
||||||
Manage options with default value from config.
|
|
||||||
*/
|
|
||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -53,7 +50,6 @@ type Options struct {
|
|||||||
profiles profiles.Profiles
|
profiles profiles.Profiles
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize default options.
|
|
||||||
func New() *Options {
|
func New() *Options {
|
||||||
return &Options{
|
return &Options{
|
||||||
Profile: "",
|
Profile: "",
|
||||||
@ -93,18 +89,16 @@ func (o *Options) String() string {
|
|||||||
o.Author,
|
o.Author,
|
||||||
o.Title,
|
o.Title,
|
||||||
o.Workers,
|
o.Workers,
|
||||||
o.ShowConfig(),
|
o.ShowDefault(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config file: ~/.go-comic-converter.yaml
|
|
||||||
func (o *Options) FileName() string {
|
func (o *Options) FileName() string {
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
return filepath.Join(home, ".go-comic-converter.yaml")
|
return filepath.Join(home, ".go-comic-converter.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config files
|
func (o *Options) LoadDefault() error {
|
||||||
func (o *Options) LoadConfig() error {
|
|
||||||
f, err := os.Open(o.FileName())
|
f, err := os.Open(o.FileName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
@ -118,8 +112,7 @@ func (o *Options) LoadConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current settings for fields that can be saved
|
func (o *Options) ShowDefault() string {
|
||||||
func (o *Options) ShowConfig() string {
|
|
||||||
var profileDesc, viewDesc string
|
var profileDesc, viewDesc string
|
||||||
profile := o.GetProfile()
|
profile := o.GetProfile()
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
@ -187,14 +180,12 @@ func (o *Options) ShowConfig() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset all settings to default value
|
func (o *Options) ResetDefault() error {
|
||||||
func (o *Options) ResetConfig() error {
|
New().SaveDefault()
|
||||||
New().SaveConfig()
|
return o.LoadDefault()
|
||||||
return o.LoadConfig()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// save all current settings as futur default value
|
func (o *Options) SaveDefault() error {
|
||||||
func (o *Options) SaveConfig() error {
|
|
||||||
f, err := os.Create(o.FileName())
|
f, err := os.Create(o.FileName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -203,12 +194,10 @@ func (o *Options) SaveConfig() error {
|
|||||||
return yaml.NewEncoder(f).Encode(o)
|
return yaml.NewEncoder(f).Encode(o)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcut to get current profile
|
|
||||||
func (o *Options) GetProfile() *profiles.Profile {
|
func (o *Options) GetProfile() *profiles.Profile {
|
||||||
return o.profiles.Get(o.Profile)
|
return o.profiles.Get(o.Profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// all available profiles
|
|
||||||
func (o *Options) AvailableProfiles() string {
|
func (o *Options) AvailableProfiles() string {
|
||||||
return o.profiles.String()
|
return o.profiles.String()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
/*
|
|
||||||
Manage supported profiles for go-comic-converter.
|
|
||||||
*/
|
|
||||||
package profiles
|
package profiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -15,10 +12,8 @@ type Profile struct {
|
|||||||
Height int
|
Height int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recommended ratio of image for perfect rendering Portrait or Landscape.
|
|
||||||
const PerfectRatio = 1.5
|
const PerfectRatio = 1.5
|
||||||
|
|
||||||
// Compute best dimension based on device size
|
|
||||||
func (p Profile) PerfectDim() (int, int) {
|
func (p Profile) PerfectDim() (int, int) {
|
||||||
width, height := float64(p.Width), float64(p.Height)
|
width, height := float64(p.Width), float64(p.Height)
|
||||||
perfectWidth, perfectHeight := height/PerfectRatio, width*PerfectRatio
|
perfectWidth, perfectHeight := height/PerfectRatio, width*PerfectRatio
|
||||||
@ -32,7 +27,6 @@ func (p Profile) PerfectDim() (int, int) {
|
|||||||
|
|
||||||
type Profiles []Profile
|
type Profiles []Profile
|
||||||
|
|
||||||
// Initialize list of all supported profiles.
|
|
||||||
func New() Profiles {
|
func New() Profiles {
|
||||||
return []Profile{
|
return []Profile{
|
||||||
{"K1", "Kindle 1", 600, 670},
|
{"K1", "Kindle 1", 600, 670},
|
||||||
@ -76,7 +70,6 @@ func (p Profiles) String() string {
|
|||||||
return strings.Join(s, "\n")
|
return strings.Join(s, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup profile by code
|
|
||||||
func (p Profiles) Get(name string) *Profile {
|
func (p Profiles) Get(name string) *Profile {
|
||||||
for _, profile := range p {
|
for _, profile := range p {
|
||||||
if profile.Code == name {
|
if profile.Code == name {
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
/*
|
|
||||||
Tools to create epub from images.
|
|
||||||
*/
|
|
||||||
package epub
|
package epub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -14,7 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
|
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
|
||||||
epubimageprocessing "github.com/celogeek/go-comic-converter/v2/internal/epub/imageprocessing"
|
epubimageprocessing "github.com/celogeek/go-comic-converter/v2/internal/epub/image_processing"
|
||||||
epubprogress "github.com/celogeek/go-comic-converter/v2/internal/epub/progress"
|
epubprogress "github.com/celogeek/go-comic-converter/v2/internal/epub/progress"
|
||||||
epubtemplates "github.com/celogeek/go-comic-converter/v2/internal/epub/templates"
|
epubtemplates "github.com/celogeek/go-comic-converter/v2/internal/epub/templates"
|
||||||
epubtree "github.com/celogeek/go-comic-converter/v2/internal/epub/tree"
|
epubtree "github.com/celogeek/go-comic-converter/v2/internal/epub/tree"
|
||||||
@ -51,7 +48,6 @@ type epubPart struct {
|
|||||||
Images []*epubimage.Image
|
Images []*epubimage.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize epub
|
|
||||||
func New(options *Options) *ePub {
|
func New(options *Options) *ePub {
|
||||||
uid := uuid.Must(uuid.NewV4())
|
uid := uuid.Must(uuid.NewV4())
|
||||||
tmpl := template.New("parser")
|
tmpl := template.New("parser")
|
||||||
@ -69,8 +65,7 @@ func New(options *Options) *ePub {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// render templates
|
func (e *ePub) render(templateString string, data any) string {
|
||||||
func (e *ePub) render(templateString string, data map[string]any) string {
|
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
tmpl := template.Must(e.templateProcessor.Parse(templateString))
|
tmpl := template.Must(e.templateProcessor.Parse(templateString))
|
||||||
if err := tmpl.Execute(&result, data); err != nil {
|
if err := tmpl.Execute(&result, data); err != nil {
|
||||||
@ -79,7 +74,6 @@ func (e *ePub) render(templateString string, data map[string]any) string {
|
|||||||
return regexp.MustCompile("\n+").ReplaceAllString(result.String(), "\n")
|
return regexp.MustCompile("\n+").ReplaceAllString(result.String(), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// write image to the zip
|
|
||||||
func (e *ePub) writeImage(wz *epubzip.EpubZip, img *epubimage.Image) error {
|
func (e *ePub) writeImage(wz *epubzip.EpubZip, img *epubimage.Image) error {
|
||||||
err := wz.WriteFile(
|
err := wz.WriteFile(
|
||||||
fmt.Sprintf("OEBPS/%s", img.TextPath()),
|
fmt.Sprintf("OEBPS/%s", img.TextPath()),
|
||||||
@ -98,7 +92,6 @@ func (e *ePub) writeImage(wz *epubzip.EpubZip, img *epubimage.Image) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// write blank page
|
|
||||||
func (e *ePub) writeBlank(wz *epubzip.EpubZip, img *epubimage.Image) error {
|
func (e *ePub) writeBlank(wz *epubzip.EpubZip, img *epubimage.Image) error {
|
||||||
return wz.WriteFile(
|
return wz.WriteFile(
|
||||||
fmt.Sprintf("OEBPS/Text/%d_sp.xhtml", img.Id),
|
fmt.Sprintf("OEBPS/Text/%d_sp.xhtml", img.Id),
|
||||||
@ -109,7 +102,6 @@ func (e *ePub) writeBlank(wz *epubzip.EpubZip, img *epubimage.Image) error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract image and split it into part
|
|
||||||
func (e *ePub) getParts() ([]*epubPart, error) {
|
func (e *ePub) getParts() ([]*epubPart, error) {
|
||||||
images, err := epubimageprocessing.LoadImages(&epubimageprocessing.Options{
|
images, err := epubimageprocessing.LoadImages(&epubimageprocessing.Options{
|
||||||
Input: e.Input,
|
Input: e.Input,
|
||||||
@ -124,12 +116,14 @@ func (e *ePub) getParts() ([]*epubPart, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort result by id and part
|
|
||||||
sort.Slice(images, func(i, j int) bool {
|
sort.Slice(images, func(i, j int) bool {
|
||||||
if images[i].Id == images[j].Id {
|
if images[i].Id < images[j].Id {
|
||||||
|
return true
|
||||||
|
} else if images[i].Id == images[j].Id {
|
||||||
return images[i].Part < images[j].Part
|
return images[i].Part < images[j].Part
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return images[i].Id < images[j].Id
|
|
||||||
})
|
})
|
||||||
|
|
||||||
parts := make([]*epubPart, 0)
|
parts := make([]*epubPart, 0)
|
||||||
@ -146,8 +140,8 @@ func (e *ePub) getParts() ([]*epubPart, error) {
|
|||||||
return parts, nil
|
return parts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// compute size of the epub part and try to be as close as possible of the target
|
|
||||||
maxSize := uint64(e.LimitMb * 1024 * 1024)
|
maxSize := uint64(e.LimitMb * 1024 * 1024)
|
||||||
|
|
||||||
xhtmlSize := uint64(1024)
|
xhtmlSize := uint64(1024)
|
||||||
// descriptor files + title
|
// descriptor files + title
|
||||||
baseSize := uint64(16*1024) + cover.Data.CompressedSize()
|
baseSize := uint64(16*1024) + cover.Data.CompressedSize()
|
||||||
@ -186,9 +180,6 @@ func (e *ePub) getParts() ([]*epubPart, error) {
|
|||||||
return parts, nil
|
return parts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a tree from the directories.
|
|
||||||
//
|
|
||||||
// this is used to simulate the toc.
|
|
||||||
func (e *ePub) getTree(images []*epubimage.Image, skip_files bool) string {
|
func (e *ePub) getTree(images []*epubimage.Image, skip_files bool) string {
|
||||||
t := epubtree.New()
|
t := epubtree.New()
|
||||||
for _, img := range images {
|
for _, img := range images {
|
||||||
@ -206,7 +197,6 @@ func (e *ePub) getTree(images []*epubimage.Image, skip_files bool) string {
|
|||||||
return c.WriteString("")
|
return c.WriteString("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the zip
|
|
||||||
func (e *ePub) Write() error {
|
func (e *ePub) Write() error {
|
||||||
type zipContent struct {
|
type zipContent struct {
|
||||||
Name string
|
Name string
|
||||||
@ -295,7 +285,7 @@ func (e *ePub) Write() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := wz.WriteImage(epubimageprocessing.LoadCoverTitleData(part.Cover, title, e.Image.Quality)); err != nil {
|
if err := wz.WriteImage(epubimageprocessing.LoadCoverData(part.Cover, title, e.Image.Quality)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
/*
|
|
||||||
Rotate image if the source width > height.
|
|
||||||
*/
|
|
||||||
package epubfilters
|
package epubfilters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -10,15 +7,29 @@ import (
|
|||||||
"github.com/disintegration/gift"
|
"github.com/disintegration/gift"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AutoRotate() gift.Filter {
|
func AutoRotate(viewWidth, viewHeight int) gift.Filter {
|
||||||
return &autoRotateFilter{}
|
return &autoRotateFilter{
|
||||||
|
viewWidth, viewHeight,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type autoRotateFilter struct {
|
type autoRotateFilter struct {
|
||||||
|
viewWidth, viewHeight int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *autoRotateFilter) needRotate(srcBounds image.Rectangle) bool {
|
||||||
|
width, height := srcBounds.Dx(), srcBounds.Dy()
|
||||||
|
if width <= height {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if width <= p.viewWidth && height <= p.viewHeight {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *autoRotateFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
|
func (p *autoRotateFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
|
||||||
if srcBounds.Dx() > srcBounds.Dy() {
|
if p.needRotate(srcBounds) {
|
||||||
dstBounds = gift.Rotate90().Bounds(srcBounds)
|
dstBounds = gift.Rotate90().Bounds(srcBounds)
|
||||||
} else {
|
} else {
|
||||||
dstBounds = srcBounds
|
dstBounds = srcBounds
|
||||||
@ -27,7 +38,7 @@ func (p *autoRotateFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *autoRotateFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
|
func (p *autoRotateFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
|
||||||
if src.Bounds().Dx() > src.Bounds().Dy() {
|
if p.needRotate(src.Bounds()) {
|
||||||
gift.Rotate90().Draw(dst, src, options)
|
gift.Rotate90().Draw(dst, src, options)
|
||||||
} else {
|
} else {
|
||||||
draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
|
draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
/*
|
|
||||||
Create a title with the cover image
|
|
||||||
*/
|
|
||||||
package epubfilters
|
package epubfilters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -22,12 +19,10 @@ type coverTitle struct {
|
|||||||
title string
|
title string
|
||||||
}
|
}
|
||||||
|
|
||||||
// size is the same as source
|
|
||||||
func (p *coverTitle) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
|
func (p *coverTitle) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
|
||||||
return srcBounds
|
return srcBounds
|
||||||
}
|
}
|
||||||
|
|
||||||
// blur the src image, and create a box with the title in the middle
|
|
||||||
func (p *coverTitle) Draw(dst draw.Image, src image.Image, options *gift.Options) {
|
func (p *coverTitle) Draw(dst draw.Image, src image.Image, options *gift.Options) {
|
||||||
// Create a blur version of the cover
|
// Create a blur version of the cover
|
||||||
g := gift.New(gift.GaussianBlur(4))
|
g := gift.New(gift.GaussianBlur(4))
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
/*
|
|
||||||
cut a double page in 2 part: left and right.
|
|
||||||
|
|
||||||
this will cut in the middle of the page.
|
|
||||||
*/
|
|
||||||
package epubfilters
|
package epubfilters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
/*
|
|
||||||
generate a blank pixel 1x1, if the size of the image is 0x0.
|
|
||||||
|
|
||||||
An image 0x0 is not a valid image, and failed to read.
|
|
||||||
*/
|
|
||||||
package epubfilters
|
package epubfilters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -32,7 +27,6 @@ func (p *pixel) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
|
|||||||
func (p *pixel) Draw(dst draw.Image, src image.Image, options *gift.Options) {
|
func (p *pixel) Draw(dst draw.Image, src image.Image, options *gift.Options) {
|
||||||
if dst.Bounds().Dx() == 1 && dst.Bounds().Dy() == 1 {
|
if dst.Bounds().Dx() == 1 && dst.Bounds().Dy() == 1 {
|
||||||
dst.Set(0, 0, color.White)
|
dst.Set(0, 0, color.White)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
|
draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
/*
|
|
||||||
Resize image by keeping aspect ratio.
|
|
||||||
|
|
||||||
This will reduce or enlarge image to fit into the viewWidth and viewHeight.
|
|
||||||
*/
|
|
||||||
package epubfilters
|
package epubfilters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -47,5 +42,6 @@ func (p *resizeFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *resizeFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
|
func (p *resizeFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
|
||||||
gift.Resize(dst.Bounds().Dx(), dst.Bounds().Dy(), p.resampling).Draw(dst, src, options)
|
b := p.Bounds(src.Bounds())
|
||||||
|
gift.Resize(b.Dx(), b.Dy(), p.resampling).Draw(dst, src, options)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
/*
|
|
||||||
Image helpers to transform image.
|
|
||||||
*/
|
|
||||||
package epubimage
|
package epubimage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -23,35 +20,22 @@ type Image struct {
|
|||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// key name of the image
|
|
||||||
func (i *Image) Key(prefix string) string {
|
func (i *Image) Key(prefix string) string {
|
||||||
return fmt.Sprintf("%s_%d_p%d", prefix, i.Id, i.Part)
|
return fmt.Sprintf("%s_%d_p%d", prefix, i.Id, i.Part)
|
||||||
}
|
}
|
||||||
|
|
||||||
// key name of the blank plage after the image
|
|
||||||
func (i *Image) SpaceKey(prefix string) string {
|
func (i *Image) SpaceKey(prefix string) string {
|
||||||
return fmt.Sprintf("%s_%d_sp", prefix, i.Id)
|
return fmt.Sprintf("%s_%d_sp", prefix, i.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// path of the blank page
|
|
||||||
func (i *Image) SpacePath() string {
|
|
||||||
return fmt.Sprintf("Text/%d_sp.xhtml", i.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// text path linked to the image
|
|
||||||
func (i *Image) TextPath() string {
|
func (i *Image) TextPath() string {
|
||||||
return fmt.Sprintf("Text/%d_p%d.xhtml", i.Id, i.Part)
|
return fmt.Sprintf("Text/%d_p%d.xhtml", i.Id, i.Part)
|
||||||
}
|
}
|
||||||
|
|
||||||
// image path
|
|
||||||
func (i *Image) ImgPath() string {
|
func (i *Image) ImgPath() string {
|
||||||
return fmt.Sprintf("Images/%d_p%d.jpg", i.Id, i.Part)
|
return fmt.Sprintf("Images/%d_p%d.jpg", i.Id, i.Part)
|
||||||
}
|
}
|
||||||
|
|
||||||
// style to apply to the image.
|
|
||||||
//
|
|
||||||
// center by default.
|
|
||||||
// align to left or right if it's part of the splitted double page.
|
|
||||||
func (i *Image) ImgStyle(viewWidth, viewHeight int, manga bool) string {
|
func (i *Image) ImgStyle(viewWidth, viewHeight int, manga bool) string {
|
||||||
marginW, marginH := float64(viewWidth-i.Width)/2, float64(viewHeight-i.Height)/2
|
marginW, marginH := float64(viewWidth-i.Width)/2, float64(viewHeight-i.Height)/2
|
||||||
left, top := marginW*100/float64(viewWidth), marginH*100/float64(viewHeight)
|
left, top := marginW*100/float64(viewWidth), marginH*100/float64(viewHeight)
|
||||||
@ -81,3 +65,7 @@ func (i *Image) ImgStyle(viewWidth, viewHeight int, manga bool) string {
|
|||||||
align,
|
align,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Image) SpacePath() string {
|
||||||
|
return fmt.Sprintf("Text/%d_sp.xhtml", i.Id)
|
||||||
|
}
|
||||||
|
@ -5,13 +5,12 @@ import (
|
|||||||
"github.com/disintegration/gift"
|
"github.com/disintegration/gift"
|
||||||
)
|
)
|
||||||
|
|
||||||
// create filter to apply to the source
|
|
||||||
func NewGift(options *Options) *gift.GIFT {
|
func NewGift(options *Options) *gift.GIFT {
|
||||||
g := gift.New()
|
g := gift.New()
|
||||||
g.SetParallelization(false)
|
g.SetParallelization(false)
|
||||||
|
|
||||||
if options.AutoRotate {
|
if options.AutoRotate {
|
||||||
g.Add(epubfilters.AutoRotate())
|
g.Add(epubfilters.AutoRotate(options.ViewWidth, options.ViewHeight))
|
||||||
}
|
}
|
||||||
if options.Contrast != 0 {
|
if options.Contrast != 0 {
|
||||||
g.Add(gift.Contrast(float32(options.Contrast)))
|
g.Add(gift.Contrast(float32(options.Contrast)))
|
||||||
@ -26,7 +25,6 @@ func NewGift(options *Options) *gift.GIFT {
|
|||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
// create filters to cut image into 2 equal pieces
|
|
||||||
func NewGiftSplitDoublePage(options *Options) []*gift.GIFT {
|
func NewGiftSplitDoublePage(options *Options) []*gift.GIFT {
|
||||||
gifts := make([]*gift.GIFT, 2)
|
gifts := make([]*gift.GIFT, 2)
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package epubimage
|
package epubimage
|
||||||
|
|
||||||
// options for image transformation
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Crop bool
|
Crop bool
|
||||||
ViewWidth int
|
ViewWidth int
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
/*
|
|
||||||
Extract and transform image into a compressed jpeg.
|
|
||||||
*/
|
|
||||||
package epubimageprocessing
|
package epubimageprocessing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -29,7 +26,6 @@ type tasks struct {
|
|||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract and convert images
|
|
||||||
func LoadImages(o *Options) ([]*epubimage.Image, error) {
|
func LoadImages(o *Options) ([]*epubimage.Image, error) {
|
||||||
images := make([]*epubimage.Image, 0)
|
images := make([]*epubimage.Image, 0)
|
||||||
|
|
||||||
@ -43,7 +39,6 @@ func LoadImages(o *Options) ([]*epubimage.Image, error) {
|
|||||||
imageInput chan *tasks
|
imageInput chan *tasks
|
||||||
)
|
)
|
||||||
|
|
||||||
// get all images though a channel of bytes
|
|
||||||
if fi.IsDir() {
|
if fi.IsDir() {
|
||||||
imageCount, imageInput, err = o.loadDir()
|
imageCount, imageInput, err = o.loadDir()
|
||||||
} else {
|
} else {
|
||||||
@ -62,7 +57,6 @@ func LoadImages(o *Options) ([]*epubimage.Image, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// dry run, skip convertion
|
|
||||||
if o.Dry {
|
if o.Dry {
|
||||||
for img := range imageInput {
|
for img := range imageInput {
|
||||||
images = append(images, &epubimage.Image{
|
images = append(images, &epubimage.Image{
|
||||||
@ -184,8 +178,7 @@ func LoadImages(o *Options) ([]*epubimage.Image, error) {
|
|||||||
return images, nil
|
return images, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a title page with the cover
|
func LoadCoverData(img *epubimage.Image, title string, quality int) *epubimagedata.ImageData {
|
||||||
func LoadCoverTitleData(img *epubimage.Image, title string, quality int) *epubimagedata.ImageData {
|
|
||||||
// Create a blur version of the cover
|
// Create a blur version of the cover
|
||||||
g := gift.New(epubfilters.CoverTitle(title))
|
g := gift.New(epubfilters.CoverTitle(title))
|
||||||
dst := image.NewGray(g.Bounds(img.Raw.Bounds()))
|
dst := image.NewGray(g.Bounds(img.Raw.Bounds()))
|
@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// only accept jpg, png and webp as source file
|
|
||||||
func isSupportedImage(path string) bool {
|
func isSupportedImage(path string) bool {
|
||||||
switch strings.ToLower(filepath.Ext(path)) {
|
switch strings.ToLower(filepath.Ext(path)) {
|
||||||
case ".jpg", ".jpeg", ".png", ".webp":
|
case ".jpg", ".jpeg", ".png", ".webp":
|
||||||
@ -18,13 +17,11 @@ func isSupportedImage(path string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the color is blank enough
|
|
||||||
func colorIsBlank(c color.Color) bool {
|
func colorIsBlank(c color.Color) bool {
|
||||||
g := color.GrayModel.Convert(c).(color.Gray)
|
g := color.GrayModel.Convert(c).(color.Gray)
|
||||||
return g.Y >= 0xf0
|
return g.Y >= 0xf0
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup for margin (blank) around the image
|
|
||||||
func findMarging(img image.Image) image.Rectangle {
|
func findMarging(img image.Image) image.Rectangle {
|
||||||
imgArea := img.Bounds()
|
imgArea := img.Bounds()
|
||||||
|
|
@ -30,7 +30,6 @@ type Options struct {
|
|||||||
|
|
||||||
var errNoImagesFound = errors.New("no images found")
|
var errNoImagesFound = errors.New("no images found")
|
||||||
|
|
||||||
// ensure copy image into a buffer
|
|
||||||
func (o *Options) mustExtractImage(imageOpener func() (io.ReadCloser, error)) *bytes.Buffer {
|
func (o *Options) mustExtractImage(imageOpener func() (io.ReadCloser, error)) *bytes.Buffer {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
if o.Dry {
|
if o.Dry {
|
||||||
@ -52,7 +51,6 @@ func (o *Options) mustExtractImage(imageOpener func() (io.ReadCloser, error)) *b
|
|||||||
return &b
|
return &b
|
||||||
}
|
}
|
||||||
|
|
||||||
// load a directory of images
|
|
||||||
func (o *Options) loadDir() (totalImages int, output chan *tasks, err error) {
|
func (o *Options) loadDir() (totalImages int, output chan *tasks, err error) {
|
||||||
images := make([]string, 0)
|
images := make([]string, 0)
|
||||||
|
|
||||||
@ -103,7 +101,6 @@ func (o *Options) loadDir() (totalImages int, output chan *tasks, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// load a zip file that include images
|
|
||||||
func (o *Options) loadCbz() (totalImages int, output chan *tasks, err error) {
|
func (o *Options) loadCbz() (totalImages int, output chan *tasks, err error) {
|
||||||
r, err := zip.OpenReader(o.Input)
|
r, err := zip.OpenReader(o.Input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -153,7 +150,6 @@ func (o *Options) loadCbz() (totalImages int, output chan *tasks, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// load a rar file that include images
|
|
||||||
func (o *Options) loadCbr() (totalImages int, output chan *tasks, err error) {
|
func (o *Options) loadCbr() (totalImages int, output chan *tasks, err error) {
|
||||||
// listing and indexing
|
// listing and indexing
|
||||||
rl, err := rardecode.OpenReader(o.Input, "")
|
rl, err := rardecode.OpenReader(o.Input, "")
|
||||||
@ -237,7 +233,6 @@ func (o *Options) loadCbr() (totalImages int, output chan *tasks, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract image from a pdf
|
|
||||||
func (o *Options) loadPdf() (totalImages int, output chan *tasks, err error) {
|
func (o *Options) loadPdf() (totalImages int, output chan *tasks, err error) {
|
||||||
pdf := pdfread.Load(o.Input)
|
pdf := pdfread.Load(o.Input)
|
||||||
if pdf == nil {
|
if pdf == nil {
|
@ -1,6 +1,3 @@
|
|||||||
/*
|
|
||||||
prepare image to be store in a zip file.
|
|
||||||
*/
|
|
||||||
package epubimagedata
|
package epubimagedata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -20,7 +17,6 @@ type ImageData struct {
|
|||||||
Data []byte
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// compressed size of the image with the header
|
|
||||||
func (img *ImageData) CompressedSize() uint64 {
|
func (img *ImageData) CompressedSize() uint64 {
|
||||||
return img.Header.CompressedSize64 + 30 + uint64(len(img.Header.Name))
|
return img.Header.CompressedSize64 + 30 + uint64(len(img.Header.Name))
|
||||||
}
|
}
|
||||||
@ -30,13 +26,11 @@ func exitWithError(err error) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new data image with file name based on id and part
|
|
||||||
func New(id int, part int, img image.Image, quality int) *ImageData {
|
func New(id int, part int, img image.Image, quality int) *ImageData {
|
||||||
name := fmt.Sprintf("OEBPS/Images/%d_p%d.jpg", id, part)
|
name := fmt.Sprintf("OEBPS/Images/%d_p%d.jpg", id, part)
|
||||||
return NewRaw(name, img, quality)
|
return NewRaw(name, img, quality)
|
||||||
}
|
}
|
||||||
|
|
||||||
// create gzip encoded jpeg
|
|
||||||
func NewRaw(name string, img image.Image, quality int) *ImageData {
|
func NewRaw(name string, img image.Image, quality int) *ImageData {
|
||||||
var (
|
var (
|
||||||
data, cdata bytes.Buffer
|
data, cdata bytes.Buffer
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
/*
|
|
||||||
create a progress bar with custom settings.
|
|
||||||
*/
|
|
||||||
package epubprogress
|
package epubprogress
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
/*
|
|
||||||
Templates use to create xml files of the epub.
|
|
||||||
*/
|
|
||||||
package epubtemplates
|
package epubtemplates
|
||||||
|
|
||||||
import _ "embed"
|
import _ "embed"
|
||||||
|
@ -28,7 +28,6 @@ type tag struct {
|
|||||||
value string
|
value string
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the content file
|
|
||||||
func Content(o *ContentOptions) string {
|
func Content(o *ContentOptions) string {
|
||||||
doc := etree.NewDocument()
|
doc := etree.NewDocument()
|
||||||
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
|
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
|
||||||
@ -77,7 +76,6 @@ func Content(o *ContentOptions) string {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// metadata part of the content
|
|
||||||
func getMeta(o *ContentOptions) []tag {
|
func getMeta(o *ContentOptions) []tag {
|
||||||
metas := []tag{
|
metas := []tag{
|
||||||
{"meta", tagAttrs{"property": "dcterms:modified"}, o.UpdatedAt},
|
{"meta", tagAttrs{"property": "dcterms:modified"}, o.UpdatedAt},
|
||||||
@ -154,7 +152,6 @@ func getManifest(o *ContentOptions) []tag {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
// spine part of the content
|
|
||||||
func getSpine(o *ContentOptions) []tag {
|
func getSpine(o *ContentOptions) []tag {
|
||||||
isOnTheRight := !o.ImageOptions.Manga
|
isOnTheRight := !o.ImageOptions.Manga
|
||||||
getSpread := func(doublePageNoBlank bool) string {
|
getSpread := func(doublePageNoBlank bool) string {
|
||||||
@ -199,7 +196,6 @@ func getSpine(o *ContentOptions) []tag {
|
|||||||
return spine
|
return spine
|
||||||
}
|
}
|
||||||
|
|
||||||
// guide part of the content
|
|
||||||
func getGuide(o *ContentOptions) []tag {
|
func getGuide(o *ContentOptions) []tag {
|
||||||
guide := []tag{}
|
guide := []tag{}
|
||||||
if o.Cover != nil {
|
if o.Cover != nil {
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
|
epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image"
|
||||||
)
|
)
|
||||||
|
|
||||||
// create toc
|
|
||||||
func Toc(title string, stripFirstDirectoryFromToc bool, images []*epubimage.Image) string {
|
func Toc(title string, stripFirstDirectoryFromToc bool, images []*epubimage.Image) string {
|
||||||
doc := etree.NewDocument()
|
doc := etree.NewDocument()
|
||||||
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
|
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
|
@ -1,21 +1,3 @@
|
|||||||
/*
|
|
||||||
Organize a list of filename with their path into a tree of directories.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
- A/B/C/D.jpg
|
|
||||||
- A/B/C/E.jpg
|
|
||||||
- A/B/F/G.jpg
|
|
||||||
|
|
||||||
This is transformed like:
|
|
||||||
|
|
||||||
A
|
|
||||||
B
|
|
||||||
C
|
|
||||||
D.jpg
|
|
||||||
E.jpg
|
|
||||||
F
|
|
||||||
G.jpg
|
|
||||||
*/
|
|
||||||
package epubtree
|
package epubtree
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -32,19 +14,16 @@ type node struct {
|
|||||||
Children []*node
|
Children []*node
|
||||||
}
|
}
|
||||||
|
|
||||||
// initilize tree with a root node
|
|
||||||
func New() *tree {
|
func New() *tree {
|
||||||
return &tree{map[string]*node{
|
return &tree{map[string]*node{
|
||||||
".": {".", []*node{}},
|
".": {".", []*node{}},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// root node
|
|
||||||
func (n *tree) Root() *node {
|
func (n *tree) Root() *node {
|
||||||
return n.Nodes["."]
|
return n.Nodes["."]
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the filename to the tree
|
|
||||||
func (n *tree) Add(filename string) {
|
func (n *tree) Add(filename string) {
|
||||||
cn := n.Root()
|
cn := n.Root()
|
||||||
cp := ""
|
cp := ""
|
||||||
@ -58,7 +37,6 @@ func (n *tree) Add(filename string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// string version of the tree
|
|
||||||
func (n *node) WriteString(indent string) string {
|
func (n *node) WriteString(indent string) string {
|
||||||
r := strings.Builder{}
|
r := strings.Builder{}
|
||||||
if indent != "" {
|
if indent != "" {
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
/*
|
|
||||||
Helper to write epub files.
|
|
||||||
|
|
||||||
We create a zip with the magic epub mimetype.
|
|
||||||
*/
|
|
||||||
package epubzip
|
package epubzip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -18,7 +13,6 @@ type EpubZip struct {
|
|||||||
wz *zip.Writer
|
wz *zip.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new epub
|
|
||||||
func New(path string) (*EpubZip, error) {
|
func New(path string) (*EpubZip, error) {
|
||||||
w, err := os.Create(path)
|
w, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -28,7 +22,6 @@ func New(path string) (*EpubZip, error) {
|
|||||||
return &EpubZip{w, wz}, nil
|
return &EpubZip{w, wz}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// close compress pipe and file.
|
|
||||||
func (e *EpubZip) Close() error {
|
func (e *EpubZip) Close() error {
|
||||||
if err := e.wz.Close(); err != nil {
|
if err := e.wz.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -36,8 +29,6 @@ func (e *EpubZip) Close() error {
|
|||||||
return e.w.Close()
|
return e.w.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write mimetype, in a very specific way.
|
|
||||||
// This will be valid with epubcheck tools.
|
|
||||||
func (e *EpubZip) WriteMagic() error {
|
func (e *EpubZip) WriteMagic() error {
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
fh := &zip.FileHeader{
|
fh := &zip.FileHeader{
|
||||||
@ -60,7 +51,6 @@ func (e *EpubZip) WriteMagic() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write image. They are already compressed, so we write them down directly.
|
|
||||||
func (e *EpubZip) WriteImage(image *epubimagedata.ImageData) error {
|
func (e *EpubZip) WriteImage(image *epubimagedata.ImageData) error {
|
||||||
m, err := e.wz.CreateRaw(image.Header)
|
m, err := e.wz.CreateRaw(image.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -70,7 +60,6 @@ func (e *EpubZip) WriteImage(image *epubimagedata.ImageData) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write file. Compressed it using deflate.
|
|
||||||
func (e *EpubZip) WriteFile(file string, content []byte) error {
|
func (e *EpubZip) WriteFile(file string, content []byte) error {
|
||||||
m, err := e.wz.CreateHeader(&zip.FileHeader{
|
m, err := e.wz.CreateHeader(&zip.FileHeader{
|
||||||
Name: file,
|
Name: file,
|
||||||
|
@ -1,45 +1,95 @@
|
|||||||
/*
|
|
||||||
sortpath support sorting of path that may include number.
|
|
||||||
|
|
||||||
A series of path can looks like:
|
|
||||||
- Tome1/Chap1/Image1.jpg
|
|
||||||
- Tome1/Chap2/Image1.jpg
|
|
||||||
- Tome1/Chap10/Image2.jpg
|
|
||||||
|
|
||||||
The module will split the string by path,
|
|
||||||
and compare them by decomposing the string and number part.
|
|
||||||
|
|
||||||
The module support 3 mode:
|
|
||||||
- mode=0 alpha for path and file
|
|
||||||
- mode=1 alphanum for path and alpha for file
|
|
||||||
- mode=2 alphanum for path and file
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
files := []string{
|
|
||||||
'T1/C1/Img1.jpg',
|
|
||||||
'T1/C2/Img1.jpg',
|
|
||||||
'T1/C10/Img1.jpg',
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(sortpath.By(files, 1))
|
|
||||||
*/
|
|
||||||
package sortpath
|
package sortpath
|
||||||
|
|
||||||
// struct that implement interface for sort.Sort
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Strings follow with numbers like: s1, s1.2, s2-3, ...
|
||||||
|
var split_path_regex = regexp.MustCompile(`^(.*?)(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?$`)
|
||||||
|
|
||||||
|
type part struct {
|
||||||
|
fullname string
|
||||||
|
name string
|
||||||
|
number float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a part) compare(b part) float64 {
|
||||||
|
if a.number == 0 || b.number == 0 {
|
||||||
|
return float64(strings.Compare(a.fullname, b.fullname))
|
||||||
|
}
|
||||||
|
if a.name == b.name {
|
||||||
|
return a.number - b.number
|
||||||
|
} else {
|
||||||
|
return float64(strings.Compare(a.name, b.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePart(p string) part {
|
||||||
|
r := split_path_regex.FindStringSubmatch(p)
|
||||||
|
if len(r) == 0 {
|
||||||
|
return part{p, p, 0}
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseFloat(r[2], 64)
|
||||||
|
if err != nil {
|
||||||
|
return part{p, p, 0}
|
||||||
|
}
|
||||||
|
return part{p, r[1], n}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mode=0 alpha for path and file
|
||||||
|
// mode=1 alphanum for path and alpha for file
|
||||||
|
// mode=2 alphanum for path and file
|
||||||
|
func parse(filename string, mode int) []part {
|
||||||
|
pathname, name := filepath.Split(strings.ToLower(filename))
|
||||||
|
pathname = strings.TrimSuffix(pathname, string(filepath.Separator))
|
||||||
|
ext := filepath.Ext(name)
|
||||||
|
name = name[0 : len(name)-len(ext)]
|
||||||
|
|
||||||
|
f := []part{}
|
||||||
|
for _, p := range strings.Split(pathname, string(filepath.Separator)) {
|
||||||
|
if mode > 0 { // alphanum for path
|
||||||
|
f = append(f, parsePart(p))
|
||||||
|
} else {
|
||||||
|
f = append(f, part{p, p, 0})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mode == 2 { // alphanum for file
|
||||||
|
f = append(f, parsePart(name))
|
||||||
|
} else {
|
||||||
|
f = append(f, part{name, name, 0})
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparePart(a, b []part) float64 {
|
||||||
|
m := len(a)
|
||||||
|
if m > len(b) {
|
||||||
|
m = len(b)
|
||||||
|
}
|
||||||
|
for i := 0; i < m; i++ {
|
||||||
|
c := a[i].compare(b[i])
|
||||||
|
if c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return float64(len(a) - len(b))
|
||||||
|
}
|
||||||
|
|
||||||
type by struct {
|
type by struct {
|
||||||
filenames []string
|
filenames []string
|
||||||
paths [][]part
|
paths [][]part
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b by) Len() int { return len(b.filenames) }
|
func (b by) Len() int { return len(b.filenames) }
|
||||||
func (b by) Less(i, j int) bool { return compareParts(b.paths[i], b.paths[j]) < 0 }
|
func (b by) Less(i, j int) bool { return comparePart(b.paths[i], b.paths[j]) < 0 }
|
||||||
func (b by) Swap(i, j int) {
|
func (b by) Swap(i, j int) {
|
||||||
b.filenames[i], b.filenames[j] = b.filenames[j], b.filenames[i]
|
b.filenames[i], b.filenames[j] = b.filenames[j], b.filenames[i]
|
||||||
b.paths[i], b.paths[j] = b.paths[j], b.paths[i]
|
b.paths[i], b.paths[j] = b.paths[j], b.paths[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// use sortpath.By with sort.Sort
|
|
||||||
func By(filenames []string, mode int) by {
|
func By(filenames []string, mode int) by {
|
||||||
p := [][]part{}
|
p := [][]part{}
|
||||||
for _, filename := range filenames {
|
for _, filename := range filenames {
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
package sortpath
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Strings follow with numbers like: s1, s1.2, s2-3, ...
|
|
||||||
var split_path_regex = regexp.MustCompile(`^(.*?)(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?$`)
|
|
||||||
|
|
||||||
type part struct {
|
|
||||||
fullname string
|
|
||||||
name string
|
|
||||||
number float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare part, first check if both include a number,
|
|
||||||
// if so, compare string part then number part, else compare there as string.
|
|
||||||
func (a part) compare(b part) float64 {
|
|
||||||
if a.number == 0 || b.number == 0 {
|
|
||||||
return float64(strings.Compare(a.fullname, b.fullname))
|
|
||||||
}
|
|
||||||
if a.name == b.name {
|
|
||||||
return a.number - b.number
|
|
||||||
} else {
|
|
||||||
return float64(strings.Compare(a.name, b.name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// separate from the string the number part.
|
|
||||||
func parsePart(p string) part {
|
|
||||||
r := split_path_regex.FindStringSubmatch(p)
|
|
||||||
if len(r) == 0 {
|
|
||||||
return part{p, p, 0}
|
|
||||||
}
|
|
||||||
n, err := strconv.ParseFloat(r[2], 64)
|
|
||||||
if err != nil {
|
|
||||||
return part{p, p, 0}
|
|
||||||
}
|
|
||||||
return part{p, r[1], n}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mode=0 alpha for path and file
|
|
||||||
// mode=1 alphanum for path and alpha for file
|
|
||||||
// mode=2 alphanum for path and file
|
|
||||||
func parse(filename string, mode int) []part {
|
|
||||||
pathname, name := filepath.Split(strings.ToLower(filename))
|
|
||||||
pathname = strings.TrimSuffix(pathname, string(filepath.Separator))
|
|
||||||
ext := filepath.Ext(name)
|
|
||||||
name = name[0 : len(name)-len(ext)]
|
|
||||||
|
|
||||||
f := []part{}
|
|
||||||
for _, p := range strings.Split(pathname, string(filepath.Separator)) {
|
|
||||||
if mode > 0 { // alphanum for path
|
|
||||||
f = append(f, parsePart(p))
|
|
||||||
} else {
|
|
||||||
f = append(f, part{p, p, 0})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if mode == 2 { // alphanum for file
|
|
||||||
f = append(f, parsePart(name))
|
|
||||||
} else {
|
|
||||||
f = append(f, part{name, name, 0})
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare 2 fullpath splitted into parts
|
|
||||||
func compareParts(a, b []part) float64 {
|
|
||||||
m := len(a)
|
|
||||||
if m > len(b) {
|
|
||||||
m = len(b)
|
|
||||||
}
|
|
||||||
for i := 0; i < m; i++ {
|
|
||||||
c := a[i].compare(b[i])
|
|
||||||
if c != 0 {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return float64(len(a) - len(b))
|
|
||||||
}
|
|
17
main.go
17
main.go
@ -1,10 +1,3 @@
|
|||||||
/*
|
|
||||||
Convert CBZ/CBR/Dir into Epub for e-reader devices (Kindle Devices, ...)
|
|
||||||
|
|
||||||
My goal is to make a simple, crossplatform, and fast tool to convert comics into epub.
|
|
||||||
|
|
||||||
Epub is now support by Amazon through [SendToKindle](https://www.amazon.com/gp/sendtokindle/), by Email or by using the App. So I've made it simple to support the size limit constraint of those services.
|
|
||||||
*/
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -64,29 +57,29 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cmd.Options.Save {
|
if cmd.Options.Save {
|
||||||
cmd.Options.SaveConfig()
|
cmd.Options.SaveDefault()
|
||||||
fmt.Fprintf(
|
fmt.Fprintf(
|
||||||
os.Stderr,
|
os.Stderr,
|
||||||
"%s%s\n\nSaving to %s\n",
|
"%s%s\n\nSaving to %s\n",
|
||||||
cmd.Options.Header(),
|
cmd.Options.Header(),
|
||||||
cmd.Options.ShowConfig(),
|
cmd.Options.ShowDefault(),
|
||||||
cmd.Options.FileName(),
|
cmd.Options.FileName(),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.Options.Show {
|
if cmd.Options.Show {
|
||||||
fmt.Fprintln(os.Stderr, cmd.Options.Header(), cmd.Options.ShowConfig())
|
fmt.Fprintln(os.Stderr, cmd.Options.Header(), cmd.Options.ShowDefault())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.Options.Reset {
|
if cmd.Options.Reset {
|
||||||
cmd.Options.ResetConfig()
|
cmd.Options.ResetDefault()
|
||||||
fmt.Fprintf(
|
fmt.Fprintf(
|
||||||
os.Stderr,
|
os.Stderr,
|
||||||
"%s%s\n\nReset default to %s\n",
|
"%s%s\n\nReset default to %s\n",
|
||||||
cmd.Options.Header(),
|
cmd.Options.Header(),
|
||||||
cmd.Options.ShowConfig(),
|
cmd.Options.ShowDefault(),
|
||||||
cmd.Options.FileName(),
|
cmd.Options.FileName(),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
Loading…
x
Reference in New Issue
Block a user