Compare commits

..

No commits in common. "003a91a07e73c4e143aaa1bda6f3971dc0966020" and "abf3b8facf7d3ce251e941ec4b405e0a925c7e4c" have entirely different histories.

9 changed files with 111 additions and 196 deletions

View File

@ -84,12 +84,6 @@ func (c *Converter) AddIntParam(p *int, name string, value int, usage string) {
c.order = append(c.order, converterOrderName{value: name}) c.order = append(c.order, converterOrderName{value: name})
} }
// Add an float parameter
func (c *Converter) AddFloatParam(p *float64, name string, value float64, usage string) {
c.Cmd.Float64Var(p, name, value, usage)
c.order = append(c.order, converterOrderName{value: name})
}
// Add a boolean parameter // 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)
@ -127,8 +121,6 @@ func (c *Converter) InitParse() {
c.AddStringParam(&c.Options.BackgroundColor, "background-color", c.Options.BackgroundColor, "Background color in hexa format RGB. Black=000, White=FFF, Light Gray=DDD, Dark Gray=777") c.AddStringParam(&c.Options.BackgroundColor, "background-color", c.Options.BackgroundColor, "Background color in hexa format RGB. Black=000, White=FFF, Light Gray=DDD, Dark Gray=777")
c.AddBoolParam(&c.Options.NoResize, "noresize", c.Options.NoResize, "Do not reduce image size if exceed device size") c.AddBoolParam(&c.Options.NoResize, "noresize", c.Options.NoResize, "Do not reduce image size if exceed device size")
c.AddStringParam(&c.Options.Format, "format", c.Options.Format, "Format of output images: jpeg (lossy), png (lossless)") c.AddStringParam(&c.Options.Format, "format", c.Options.Format, "Format of output images: jpeg (lossy), png (lossless)")
c.AddFloatParam(&c.Options.AspectRatio, "aspect-ratio", c.Options.AspectRatio, "Aspect ratio (height/width) of the output\n -1 = same as device\n 0 = same as source\n1.6 = amazon advice for kindle")
c.AddBoolParam(&c.Options.PortraitOnly, "portrait-only", c.Options.PortraitOnly, "Portrait only: force orientation to portrait only.")
c.AddSection("Default config") c.AddSection("Default config")
c.AddBoolParam(&c.Options.Show, "show", false, "Show your default parameters") c.AddBoolParam(&c.Options.Show, "show", false, "Show your default parameters")
@ -359,11 +351,6 @@ func (c *Converter) Validate() error {
return errors.New("format should be jpeg or png") return errors.New("format should be jpeg or png")
} }
// Aspect Ratio
if c.Options.AspectRatio < 0 && c.Options.AspectRatio != -1 {
return errors.New("aspect ratio should be: -1, 0, > 0")
}
return nil return nil
} }

View File

@ -21,30 +21,28 @@ type Options struct {
Title string `yaml:"-"` Title string `yaml:"-"`
// Config // Config
Profile string `yaml:"profile"` Profile string `yaml:"profile"`
Quality int `yaml:"quality"` Quality int `yaml:"quality"`
Grayscale bool `yaml:"grayscale"` Grayscale bool `yaml:"grayscale"`
Crop bool `yaml:"crop"` Crop bool `yaml:"crop"`
CropRatioLeft int `yaml:"crop_ratio_left"` CropRatioLeft int `yaml:"crop_ratio_left"`
CropRatioUp int `yaml:"crop_ratio_up"` CropRatioUp int `yaml:"crop_ratio_up"`
CropRatioRight int `yaml:"crop_ratio_right"` CropRatioRight int `yaml:"crop_ratio_right"`
CropRatioBottom int `yaml:"crop_ratio_bottom"` CropRatioBottom int `yaml:"crop_ratio_bottom"`
Brightness int `yaml:"brightness"` Brightness int `yaml:"brightness"`
Contrast int `yaml:"contrast"` Contrast int `yaml:"contrast"`
AutoRotate bool `yaml:"auto_rotate"` AutoRotate bool `yaml:"auto_rotate"`
AutoSplitDoublePage bool `yaml:"auto_split_double_page"` AutoSplitDoublePage bool `yaml:"auto_split_double_page"`
NoBlankImage bool `yaml:"no_blank_image"` NoBlankImage bool `yaml:"no_blank_image"`
Manga bool `yaml:"manga"` Manga bool `yaml:"manga"`
HasCover bool `yaml:"has_cover"` HasCover bool `yaml:"has_cover"`
LimitMb int `yaml:"limit_mb"` LimitMb int `yaml:"limit_mb"`
StripFirstDirectoryFromToc bool `yaml:"strip_first_directory_from_toc"` StripFirstDirectoryFromToc bool `yaml:"strip_first_directory_from_toc"`
SortPathMode int `yaml:"sort_path_mode"` SortPathMode int `yaml:"sort_path_mode"`
ForegroundColor string `yaml:"foreground_color"` ForegroundColor string `yaml:"foreground_color"`
BackgroundColor string `yaml:"background_color"` BackgroundColor string `yaml:"background_color"`
NoResize bool `yaml:"noresize"` NoResize bool `yaml:"noresize"`
Format string `yaml:"format"` Format string `yaml:"format"`
AspectRatio float64 `yaml:"aspect_ratio"`
PortraitOnly bool `yaml:"portrait_only"`
// Default Config // Default Config
Show bool `yaml:"-"` Show bool `yaml:"-"`
@ -74,20 +72,29 @@ type Options struct {
// Initialize default options. // Initialize default options.
func New() *Options { func New() *Options {
return &Options{ return &Options{
Quality: 85, Profile: "",
Grayscale: true, Quality: 85,
Crop: true, Grayscale: true,
CropRatioLeft: 1, Crop: true,
CropRatioUp: 1, CropRatioLeft: 1,
CropRatioRight: 1, CropRatioUp: 1,
CropRatioBottom: 3, CropRatioRight: 1,
NoBlankImage: true, CropRatioBottom: 3,
HasCover: true, Brightness: 0,
SortPathMode: 1, Contrast: 0,
ForegroundColor: "000", AutoRotate: false,
BackgroundColor: "FFF", AutoSplitDoublePage: false,
Format: "jpeg", NoBlankImage: true,
profiles: profiles.New(), Manga: false,
HasCover: true,
LimitMb: 0,
StripFirstDirectoryFromToc: false,
SortPathMode: 1,
ForegroundColor: "000",
BackgroundColor: "FFF",
NoResize: false,
Format: "jpeg",
profiles: profiles.New(),
} }
} }
@ -138,7 +145,7 @@ func (o *Options) LoadConfig() error {
// Get current settings for fields that can be saved // Get current settings for fields that can be saved
func (o *Options) ShowConfig() string { func (o *Options) ShowConfig() string {
var profileDesc string var profileDesc, viewDesc string
profile := o.GetProfile() profile := o.GetProfile()
if profile != nil { if profile != nil {
profileDesc = fmt.Sprintf( profileDesc = fmt.Sprintf(
@ -148,6 +155,13 @@ func (o *Options) ShowConfig() string {
profile.Width, profile.Width,
profile.Height, profile.Height,
) )
perfectWidth, perfectHeight := profile.PerfectDim()
viewDesc = fmt.Sprintf(
"%dx%d",
perfectWidth,
perfectHeight,
)
} }
sortpathmode := "" sortpathmode := ""
@ -160,13 +174,6 @@ func (o *Options) ShowConfig() string {
sortpathmode = "path=alphanum, file=alphanum" sortpathmode = "path=alphanum, file=alphanum"
} }
aspectRatio := "auto"
if o.AspectRatio > 0 {
aspectRatio = fmt.Sprintf("1:%.02f", o.AspectRatio)
} else if o.AspectRatio < 0 {
aspectRatio = fmt.Sprintf("1:%0.2f (device)", float64(profile.Height)/float64(profile.Width))
}
var b strings.Builder var b strings.Builder
for _, v := range []struct { for _, v := range []struct {
Key string Key string
@ -174,6 +181,8 @@ func (o *Options) ShowConfig() string {
Condition bool Condition bool
}{ }{
{"Profile", profileDesc, true}, {"Profile", profileDesc, true},
{"ViewRatio", fmt.Sprintf("1:%s", strings.TrimRight(fmt.Sprintf("%f", profiles.PerfectRatio), "0")), true},
{"View", viewDesc, true},
{"Format", o.Format, true}, {"Format", o.Format, true},
{"Quality", o.Quality, o.Format == "jpeg"}, {"Quality", o.Quality, o.Format == "jpeg"},
{"Grayscale", o.Grayscale, true}, {"Grayscale", o.Grayscale, true},
@ -192,8 +201,6 @@ func (o *Options) ShowConfig() string {
{"Foreground Color", fmt.Sprintf("#%s", o.ForegroundColor), true}, {"Foreground Color", fmt.Sprintf("#%s", o.ForegroundColor), true},
{"Background Color", fmt.Sprintf("#%s", o.BackgroundColor), true}, {"Background Color", fmt.Sprintf("#%s", o.BackgroundColor), true},
{"Resize", !o.NoResize, true}, {"Resize", !o.NoResize, true},
{"Aspect Ratio", aspectRatio, true},
{"Portrait Only", o.PortraitOnly, true},
} { } {
if v.Condition { if v.Condition {
b.WriteString(fmt.Sprintf("\n %-26s: %v", v.Key, v.Value)) b.WriteString(fmt.Sprintf("\n %-26s: %v", v.Key, v.Value))

View File

@ -15,6 +15,21 @@ type Profile struct {
Height int Height int
} }
// Recommended ratio of image for perfect rendering Portrait or Landscape.
const PerfectRatio = 1.6
// Compute best dimension based on device size
func (p Profile) PerfectDim() (int, int) {
width, height := float64(p.Width), float64(p.Height)
perfectWidth, perfectHeight := height/PerfectRatio, width*PerfectRatio
if perfectWidth > width {
perfectWidth = width
} else {
perfectHeight = height
}
return int(perfectWidth), int(perfectHeight)
}
type Profiles []Profile type Profiles []Profile
// Initialize list of all supported profiles. // Initialize list of all supported profiles.

View File

@ -6,7 +6,6 @@ package epub
import ( import (
"archive/zip" "archive/zip"
"fmt" "fmt"
"math"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -194,52 +193,6 @@ func (e *ePub) getTree(images []*epubimage.Image, skip_files bool) string {
return c.WriteString("") return c.WriteString("")
} }
func (e *ePub) computeAspectRatio(epubParts []*epubPart) float64 {
var (
bestAspectRatio float64
bestAspectRatioCount int
aspectRatio = map[float64]int{}
)
trunc := func(v float64) float64 {
return float64(math.Round(v*10000)) / 10000
}
for _, p := range epubParts {
aspectRatio[trunc(p.Cover.OriginalAspectRatio)]++
for _, i := range p.Images {
aspectRatio[trunc(i.OriginalAspectRatio)]++
}
}
for k, v := range aspectRatio {
if v > bestAspectRatioCount {
bestAspectRatio, bestAspectRatioCount = k, v
}
}
return bestAspectRatio
}
func (e *ePub) computeViewPort(epubParts []*epubPart) {
if e.Image.View.AspectRatio == -1 {
return //keep device size
}
// readjusting view port
bestAspectRatio := e.Image.View.AspectRatio
if bestAspectRatio == 0 {
bestAspectRatio = e.computeAspectRatio(epubParts)
}
viewWidth, viewHeight := int(float64(e.Image.View.Height)/bestAspectRatio), int(float64(e.Image.View.Width)*bestAspectRatio)
if viewWidth > e.Image.View.Width {
e.Image.View.Height = viewHeight
} else {
e.Image.View.Width = viewWidth
}
}
// create the zip // create the zip
func (e *ePub) Write() error { func (e *ePub) Write() error {
type zipContent struct { type zipContent struct {
@ -278,7 +231,6 @@ func (e *ePub) Write() error {
Quiet: e.Quiet, Quiet: e.Quiet,
}) })
e.computeViewPort(epubParts)
for i, part := range epubParts { for i, part := range epubParts {
ext := filepath.Ext(e.Output) ext := filepath.Ext(e.Output)
suffix := "" suffix := ""
@ -299,12 +251,9 @@ func (e *ePub) Write() error {
if totalParts > 1 { if totalParts > 1 {
title = fmt.Sprintf("%s [%d/%d]", title, i+1, totalParts) title = fmt.Sprintf("%s [%d/%d]", title, i+1, totalParts)
} }
titleAlign := "" titleAlign := "left:0"
if !e.Image.View.PortraitOnly { if e.Image.Manga {
titleAlign = "left:0" titleAlign = "right:0"
if e.Image.Manga {
titleAlign = "right:0"
}
} }
content := []zipContent{ content := []zipContent{
@ -326,7 +275,10 @@ func (e *ePub) Write() error {
{"OEBPS/Text/style.css", e.render(epubtemplates.Style, map[string]any{ {"OEBPS/Text/style.css", e.render(epubtemplates.Style, map[string]any{
"View": e.Image.View, "View": e.Image.View,
})}, })},
{"OEBPS/Text/space_title.xhtml", e.render(epubtemplates.Blank, map[string]any{
"Title": "Blank Page Title",
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.View.Width, e.Image.View.Height),
})},
{"OEBPS/Text/title.xhtml", e.render(epubtemplates.Text, map[string]any{ {"OEBPS/Text/title.xhtml", e.render(epubtemplates.Text, map[string]any{
"Title": title, "Title": title,
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.View.Width, e.Image.View.Height), "ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.View.Width, e.Image.View.Height),
@ -334,14 +286,6 @@ func (e *ePub) Write() error {
"ImageStyle": part.Cover.ImgStyle(e.Image.View.Width, e.Image.View.Height, titleAlign), "ImageStyle": part.Cover.ImgStyle(e.Image.View.Width, e.Image.View.Height, titleAlign),
})}, })},
} }
if !e.Image.View.PortraitOnly {
content = append(content, zipContent{
"OEBPS/Text/space_title.xhtml", e.render(epubtemplates.Blank, map[string]any{
"Title": "Blank Page Title",
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.View.Width, e.Image.View.Height),
}),
})
}
if err = wz.WriteMagic(); err != nil { if err = wz.WriteMagic(); err != nil {
return err return err
@ -375,7 +319,7 @@ func (e *ePub) Write() error {
} }
// Double Page or Last Image that is not a double page // Double Page or Last Image that is not a double page
if !e.Image.View.PortraitOnly && (img.DoublePage || (img.Part == 0 && img == lastImage)) { if img.DoublePage || (img.Part == 0 && img == lastImage) {
if err := e.writeBlank(wz, img); err != nil { if err := e.writeBlank(wz, img); err != nil {
return err return err
} }

View File

@ -10,19 +10,18 @@ import (
) )
type Image struct { type Image struct {
Id int Id int
Part int Part int
Raw image.Image Raw image.Image
Width int Width int
Height int Height int
IsCover bool IsCover bool
IsBlank bool IsBlank bool
DoublePage bool DoublePage bool
Path string Path string
Name string Name string
Position string Position string
Format string Format string
OriginalAspectRatio float64
} }
// key name of the blank plage after the image // key name of the blank plage after the image

View File

@ -85,18 +85,17 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
} }
img := &epubimage.Image{ img := &epubimage.Image{
Id: input.Id, Id: input.Id,
Part: part, Part: part,
Raw: raw, Raw: raw,
Width: dst.Bounds().Dx(), Width: dst.Bounds().Dx(),
Height: dst.Bounds().Dy(), Height: dst.Bounds().Dy(),
IsCover: input.Id == 0 && part == 0, IsCover: input.Id == 0 && part == 0,
IsBlank: dst.Bounds().Dx() == 1 && dst.Bounds().Dy() == 1, IsBlank: dst.Bounds().Dx() == 1 && dst.Bounds().Dy() == 1,
DoublePage: part == 0 && src.Bounds().Dx() > src.Bounds().Dy(), DoublePage: part == 0 && src.Bounds().Dx() > src.Bounds().Dy(),
Path: input.Path, Path: input.Path,
Name: input.Name, Name: input.Name,
Format: e.Image.Format, Format: e.Image.Format,
OriginalAspectRatio: float64(src.Bounds().Dy()) / float64(src.Bounds().Dx()),
} }
if err = imgStorage.Add(img.EPUBImgPath(), dst, e.Image.Quality); err != nil { if err = imgStorage.Add(img.EPUBImgPath(), dst, e.Image.Quality); err != nil {

View File

@ -16,8 +16,6 @@ type Color struct {
type View struct { type View struct {
Width, Height int Width, Height int
AspectRatio float64
PortraitOnly bool
Color Color Color Color
} }

View File

@ -67,12 +67,7 @@ func Content(o *ContentOptions) string {
} else { } else {
spine.CreateAttr("page-progression-direction", "ltr") spine.CreateAttr("page-progression-direction", "ltr")
} }
addToElement(spine, getSpine)
if o.ImageOptions.View.PortraitOnly {
addToElement(spine, getSpinePortrait)
} else {
addToElement(spine, getSpineAuto)
}
guide := pkg.CreateElement("guide") guide := pkg.CreateElement("guide")
addToElement(guide, getGuide) addToElement(guide, getGuide)
@ -87,6 +82,9 @@ func Content(o *ContentOptions) string {
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},
{"meta", tagAttrs{"property": "rendition:layout"}, "pre-paginated"},
{"meta", tagAttrs{"property": "rendition:spread"}, "auto"},
{"meta", tagAttrs{"property": "rendition:orientation"}, "auto"},
{"meta", tagAttrs{"property": "schema:accessMode"}, "visual"}, {"meta", tagAttrs{"property": "schema:accessMode"}, "visual"},
{"meta", tagAttrs{"property": "schema:accessModeSufficient"}, "visual"}, {"meta", tagAttrs{"property": "schema:accessModeSufficient"}, "visual"},
{"meta", tagAttrs{"property": "schema:accessibilityHazard"}, "noFlashingHazard"}, {"meta", tagAttrs{"property": "schema:accessibilityHazard"}, "noFlashingHazard"},
@ -104,20 +102,6 @@ func getMeta(o *ContentOptions) []tag {
{"dc:date", tagAttrs{}, o.UpdatedAt}, {"dc:date", tagAttrs{}, o.UpdatedAt},
} }
if o.ImageOptions.View.PortraitOnly {
metas = append(metas, []tag{
{"meta", tagAttrs{"property": "rendition:layout"}, "pre-paginated"},
{"meta", tagAttrs{"property": "rendition:spread"}, "none"},
{"meta", tagAttrs{"property": "rendition:orientation"}, "portrait"},
}...)
} else {
metas = append(metas, []tag{
{"meta", tagAttrs{"property": "rendition:layout"}, "pre-paginated"},
{"meta", tagAttrs{"property": "rendition:spread"}, "auto"},
{"meta", tagAttrs{"property": "rendition:orientation"}, "auto"},
}...)
}
if o.ImageOptions.Manga { if o.ImageOptions.Manga {
metas = append(metas, tag{"meta", tagAttrs{"name": "primary-writing-mode", "content": "horizontal-rl"}, ""}) metas = append(metas, tag{"meta", tagAttrs{"name": "primary-writing-mode", "content": "horizontal-rl"}, ""})
} else { } else {
@ -158,21 +142,18 @@ func getManifest(o *ContentOptions) []tag {
items := []tag{ items := []tag{
{"item", tagAttrs{"id": "toc", "href": "toc.xhtml", "properties": "nav", "media-type": "application/xhtml+xml"}, ""}, {"item", tagAttrs{"id": "toc", "href": "toc.xhtml", "properties": "nav", "media-type": "application/xhtml+xml"}, ""},
{"item", tagAttrs{"id": "css", "href": "Text/style.css", "media-type": "text/css"}, ""}, {"item", tagAttrs{"id": "css", "href": "Text/style.css", "media-type": "text/css"}, ""},
{"item", tagAttrs{"id": "space_title", "href": "Text/space_title.xhtml", "media-type": "application/xhtml+xml"}, ""},
{"item", tagAttrs{"id": "page_title", "href": "Text/title.xhtml", "media-type": "application/xhtml+xml"}, ""}, {"item", tagAttrs{"id": "page_title", "href": "Text/title.xhtml", "media-type": "application/xhtml+xml"}, ""},
{"item", tagAttrs{"id": "img_title", "href": fmt.Sprintf("Images/title.%s", o.ImageOptions.Format), "media-type": fmt.Sprintf("image/%s", o.ImageOptions.Format)}, ""}, {"item", tagAttrs{"id": "img_title", "href": fmt.Sprintf("Images/title.%s", o.ImageOptions.Format), "media-type": fmt.Sprintf("image/%s", o.ImageOptions.Format)}, ""},
} }
if !o.ImageOptions.View.PortraitOnly {
items = append(items, tag{"item", tagAttrs{"id": "space_title", "href": "Text/space_title.xhtml", "media-type": "application/xhtml+xml"}, ""})
}
if o.ImageOptions.HasCover || o.Current > 1 { if o.ImageOptions.HasCover || o.Current > 1 {
addTag(o.Cover, false) addTag(o.Cover, false)
} }
lastImage := o.Images[len(o.Images)-1] lastImage := o.Images[len(o.Images)-1]
for _, img := range o.Images { for _, img := range o.Images {
addTag(img, !o.ImageOptions.View.PortraitOnly && (img.DoublePage || (img.Part == 0 && img == lastImage))) addTag(img, img.DoublePage || (img.Part == 0 && img == lastImage))
} }
items = append(items, imageTags...) items = append(items, imageTags...)
@ -183,7 +164,7 @@ func getManifest(o *ContentOptions) []tag {
} }
// spine part of the content // spine part of the content
func getSpineAuto(o *ContentOptions) []tag { func getSpine(o *ContentOptions) []tag {
isOnTheRight := !o.ImageOptions.Manga isOnTheRight := !o.ImageOptions.Manga
getSpread := func(isDoublePage bool) string { getSpread := func(isDoublePage bool) string {
isOnTheRight = !isOnTheRight isOnTheRight = !isOnTheRight
@ -233,20 +214,6 @@ func getSpineAuto(o *ContentOptions) []tag {
return spine return spine
} }
func getSpinePortrait(o *ContentOptions) []tag {
spine := []tag{
{"itemref", tagAttrs{"idref": "page_title"}, ""},
}
for _, img := range o.Images {
spine = append(spine, tag{
"itemref",
tagAttrs{"idref": img.PageKey()},
"",
})
}
return spine
}
// guide part of the content // guide part of the content
func getGuide(o *ContentOptions) []tag { func getGuide(o *ContentOptions) []tag {
guide := []tag{} guide := []tag{}

View File

@ -99,6 +99,7 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
fmt.Fprintln(os.Stderr, cmd.Options) fmt.Fprintln(os.Stderr, cmd.Options)
profile := cmd.Options.GetProfile() profile := cmd.Options.GetProfile()
perfectWidth, perfectHeight := profile.PerfectDim()
if err := epub.New(&epuboptions.Options{ if err := epub.New(&epuboptions.Options{
Input: cmd.Options.Input, Input: cmd.Options.Input,
@ -130,10 +131,8 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
Manga: cmd.Options.Manga, Manga: cmd.Options.Manga,
HasCover: cmd.Options.HasCover, HasCover: cmd.Options.HasCover,
View: &epuboptions.View{ View: &epuboptions.View{
Width: profile.Width, Width: perfectWidth,
Height: profile.Height, Height: perfectHeight,
AspectRatio: cmd.Options.AspectRatio,
PortraitOnly: cmd.Options.PortraitOnly,
Color: epuboptions.Color{ Color: epuboptions.Color{
Foreground: cmd.Options.ForegroundColor, Foreground: cmd.Options.ForegroundColor,
Background: cmd.Options.BackgroundColor, Background: cmd.Options.BackgroundColor,