Compare commits

...

7 Commits

Author SHA1 Message Date
003a91a07e
Merge pull request 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 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

@ -84,6 +84,12 @@ func (c *Converter) AddIntParam(p *int, name string, value int, usage string) {
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
func (c *Converter) AddBoolParam(p *bool, name string, value bool, usage string) {
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.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.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.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")
}
// Aspect Ratio
if c.Options.AspectRatio < 0 && c.Options.AspectRatio != -1 {
return errors.New("aspect ratio should be: -1, 0, > 0")
}
return nil
}

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

@ -15,21 +15,6 @@ type Profile struct {
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
// Initialize list of all supported profiles.

@ -6,6 +6,7 @@ package epub
import (
"archive/zip"
"fmt"
"math"
"os"
"path/filepath"
"regexp"
@ -193,6 +194,52 @@ func (e *ePub) getTree(images []*epubimage.Image, skip_files bool) string {
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
func (e *ePub) Write() error {
type zipContent struct {
@ -231,6 +278,7 @@ func (e *ePub) Write() error {
Quiet: e.Quiet,
})
e.computeViewPort(epubParts)
for i, part := range epubParts {
ext := filepath.Ext(e.Output)
suffix := ""
@ -251,9 +299,12 @@ func (e *ePub) Write() error {
if totalParts > 1 {
title = fmt.Sprintf("%s [%d/%d]", title, i+1, totalParts)
}
titleAlign := "left:0"
if e.Image.Manga {
titleAlign = "right:0"
titleAlign := ""
if !e.Image.View.PortraitOnly {
titleAlign = "left:0"
if e.Image.Manga {
titleAlign = "right:0"
}
}
content := []zipContent{
@ -275,10 +326,7 @@ func (e *ePub) Write() error {
{"OEBPS/Text/style.css", e.render(epubtemplates.Style, map[string]any{
"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{
"Title": title,
"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),
})},
}
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 {
return err
@ -319,7 +375,7 @@ func (e *ePub) Write() error {
}
// 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 {
return err
}

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

@ -85,17 +85,18 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
}
img := &epubimage.Image{
Id: input.Id,
Part: part,
Raw: raw,
Width: dst.Bounds().Dx(),
Height: dst.Bounds().Dy(),
IsCover: input.Id == 0 && part == 0,
IsBlank: dst.Bounds().Dx() == 1 && dst.Bounds().Dy() == 1,
DoublePage: part == 0 && src.Bounds().Dx() > src.Bounds().Dy(),
Path: input.Path,
Name: input.Name,
Format: e.Image.Format,
Id: input.Id,
Part: part,
Raw: raw,
Width: dst.Bounds().Dx(),
Height: dst.Bounds().Dy(),
IsCover: input.Id == 0 && part == 0,
IsBlank: dst.Bounds().Dx() == 1 && dst.Bounds().Dy() == 1,
DoublePage: part == 0 && src.Bounds().Dx() > src.Bounds().Dy(),
Path: input.Path,
Name: input.Name,
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 {

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

@ -67,7 +67,12 @@ func Content(o *ContentOptions) string {
} else {
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")
addToElement(guide, getGuide)
@ -82,9 +87,6 @@ func Content(o *ContentOptions) string {
func getMeta(o *ContentOptions) []tag {
metas := []tag{
{"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:accessModeSufficient"}, "visual"},
{"meta", tagAttrs{"property": "schema:accessibilityHazard"}, "noFlashingHazard"},
@ -102,6 +104,20 @@ func getMeta(o *ContentOptions) []tag {
{"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 {
metas = append(metas, tag{"meta", tagAttrs{"name": "primary-writing-mode", "content": "horizontal-rl"}, ""})
} else {
@ -142,18 +158,21 @@ func getManifest(o *ContentOptions) []tag {
items := []tag{
{"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": "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": "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 {
addTag(o.Cover, false)
}
lastImage := o.Images[len(o.Images)-1]
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...)
@ -164,7 +183,7 @@ func getManifest(o *ContentOptions) []tag {
}
// spine part of the content
func getSpine(o *ContentOptions) []tag {
func getSpineAuto(o *ContentOptions) []tag {
isOnTheRight := !o.ImageOptions.Manga
getSpread := func(isDoublePage bool) string {
isOnTheRight = !isOnTheRight
@ -214,6 +233,20 @@ func getSpine(o *ContentOptions) []tag {
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
func getGuide(o *ContentOptions) []tag {
guide := []tag{}

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