Compare commits

..

7 Commits

Author SHA1 Message Date
003a91a07e
Merge pull request #14 from celogeek/portrait-only-mode
Portrait only mode
2023-05-04 22:20:22 +02:00
5b301526e1
mode portrait only 2023-05-04 22:18:36 +02:00
bd8506d367
add option to portrait only mode 2023-05-04 19:36:26 +02:00
d7e311488f
Merge pull request #13 from celogeek/compute-aspect-ratio
Compute aspect ratio
2023-05-04 19:28:47 +02:00
4d9681b457
higher precision 2023-05-04 19:26:55 +02:00
bb15fa5538
add option to choose aspect ratio 2023-05-04 17:22:19 +02:00
88928168b4
compute original aspect ratio 2023-05-04 16:56:30 +02:00
9 changed files with 196 additions and 111 deletions

View File

@ -84,6 +84,12 @@ 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)
@ -121,6 +127,8 @@ 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")
@ -351,6 +359,11 @@ 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

@ -43,6 +43,8 @@ type Options struct {
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:"-"`
@ -72,7 +74,6 @@ type Options struct {
// Initialize default options. // Initialize default options.
func New() *Options { func New() *Options {
return &Options{ return &Options{
Profile: "",
Quality: 85, Quality: 85,
Grayscale: true, Grayscale: true,
Crop: true, Crop: true,
@ -80,19 +81,11 @@ func New() *Options {
CropRatioUp: 1, CropRatioUp: 1,
CropRatioRight: 1, CropRatioRight: 1,
CropRatioBottom: 3, CropRatioBottom: 3,
Brightness: 0,
Contrast: 0,
AutoRotate: false,
AutoSplitDoublePage: false,
NoBlankImage: true, NoBlankImage: true,
Manga: false,
HasCover: true, HasCover: true,
LimitMb: 0,
StripFirstDirectoryFromToc: false,
SortPathMode: 1, SortPathMode: 1,
ForegroundColor: "000", ForegroundColor: "000",
BackgroundColor: "FFF", BackgroundColor: "FFF",
NoResize: false,
Format: "jpeg", Format: "jpeg",
profiles: profiles.New(), profiles: profiles.New(),
} }
@ -145,7 +138,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, viewDesc string var profileDesc string
profile := o.GetProfile() profile := o.GetProfile()
if profile != nil { if profile != nil {
profileDesc = fmt.Sprintf( profileDesc = fmt.Sprintf(
@ -155,13 +148,6 @@ 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 := ""
@ -174,6 +160,13 @@ 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
@ -181,8 +174,6 @@ 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},
@ -201,6 +192,8 @@ 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,21 +15,6 @@ 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,6 +6,7 @@ package epub
import ( import (
"archive/zip" "archive/zip"
"fmt" "fmt"
"math"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -193,6 +194,52 @@ 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 {
@ -231,6 +278,7 @@ 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 := ""
@ -251,10 +299,13 @@ 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 := "left:0" titleAlign := ""
if !e.Image.View.PortraitOnly {
titleAlign = "left:0"
if e.Image.Manga { if e.Image.Manga {
titleAlign = "right:0" titleAlign = "right:0"
} }
}
content := []zipContent{ content := []zipContent{
{"META-INF/container.xml", epubtemplates.Container}, {"META-INF/container.xml", epubtemplates.Container},
@ -275,10 +326,7 @@ 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),
@ -286,6 +334,14 @@ 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
@ -319,7 +375,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 img.DoublePage || (img.Part == 0 && img == lastImage) { if !e.Image.View.PortraitOnly && (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

@ -22,6 +22,7 @@ type Image struct {
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

@ -96,6 +96,7 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
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,6 +16,8 @@ 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,7 +67,12 @@ 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)
@ -82,9 +87,6 @@ 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"},
@ -102,6 +104,20 @@ 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 {
@ -142,18 +158,21 @@ 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, img.DoublePage || (img.Part == 0 && img == lastImage)) addTag(img, !o.ImageOptions.View.PortraitOnly && (img.DoublePage || (img.Part == 0 && img == lastImage)))
} }
items = append(items, imageTags...) items = append(items, imageTags...)
@ -164,7 +183,7 @@ func getManifest(o *ContentOptions) []tag {
} }
// spine part of the content // spine part of the content
func getSpine(o *ContentOptions) []tag { func getSpineAuto(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
@ -214,6 +233,20 @@ func getSpine(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,7 +99,6 @@ $ 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,
@ -131,8 +130,10 @@ $ 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: perfectWidth, Width: profile.Width,
Height: perfectHeight, Height: profile.Height,
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,