Compare commits

..

7 Commits

Author SHA1 Message Date
8b04cbc38f
support grayscale or color mode
grayscale is on by default
as the conversion is mainly
useful for eInk device.
2023-04-30 18:20:54 +02:00
1c1c1fd38c
rename high resolution profile 2023-04-30 18:12:29 +02:00
94983b0c61
add customization for body color 2023-04-30 16:49:55 +02:00
bcad0389fa
improve alignment on spread images for landscape 2023-04-30 16:25:02 +02:00
5b0051720f
simplify options display 2023-04-30 14:10:27 +02:00
5ec963a11e
change perfect ratio to 1.6 2023-04-30 13:51:06 +02:00
edfa349b01
add high resolution profile 2023-04-30 13:50:36 +02:00
10 changed files with 142 additions and 84 deletions

View File

@ -14,6 +14,7 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strings"
"time"
@ -100,6 +101,7 @@ func (c *Converter) InitParse() {
c.AddSection("Config")
c.AddStringParam(&c.Options.Profile, "profile", c.Options.Profile, fmt.Sprintf("Profile to use: \n%s", c.Options.AvailableProfiles()))
c.AddIntParam(&c.Options.Quality, "quality", c.Options.Quality, "Quality of the image")
c.AddBoolParam(&c.Options.Grayscale, "grayscale", c.Options.Grayscale, "Grayscale image. Ideal for eInk devices.")
c.AddBoolParam(&c.Options.Crop, "crop", c.Options.Crop, "Crop images")
c.AddIntParam(&c.Options.CropRatioLeft, "crop-ratio-left", c.Options.CropRatioLeft, "Crop ratio left: ratio of pixels allow to be non blank while cutting on the left.")
c.AddIntParam(&c.Options.CropRatioUp, "crop-ratio-up", c.Options.CropRatioUp, "Crop ratio up: ratio of pixels allow to be non blank while cutting on the top.")
@ -116,6 +118,8 @@ func (c *Converter) InitParse() {
c.AddIntParam(&c.Options.LimitMb, "limitmb", c.Options.LimitMb, "Limit size of the EPUB: Default nolimit (0), Minimum 20")
c.AddBoolParam(&c.Options.StripFirstDirectoryFromToc, "strip", c.Options.StripFirstDirectoryFromToc, "Strip first directory from the TOC if only 1")
c.AddIntParam(&c.Options.SortPathMode, "sort", c.Options.SortPathMode, "Sort path mode\n0 = alpha for path and file\n1 = alphanum for path and alpha for file\n2 = alphanum for path and file")
c.AddStringParam(&c.Options.ForegroundColor, "foreground-color", c.Options.ForegroundColor, "Foreground color in hexa format RGB. Black=000, White=FFF")
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.AddSection("Default config")
c.AddBoolParam(&c.Options.Show, "show", false, "Show your default parameters")
@ -293,6 +297,16 @@ func (c *Converter) Validate() error {
return errors.New("sort should be 0, 1 or 2")
}
// Color
colorRegex := regexp.MustCompile("^[0-9A-F]{3}$")
if !colorRegex.MatchString(c.Options.ForegroundColor) {
return errors.New("foreground color must have color format in hexa: [0-9A-F]{3}")
}
if !colorRegex.MatchString(c.Options.BackgroundColor) {
return errors.New("background color must have color format in hexa: [0-9A-F]{3}")
}
return nil
}

View File

@ -23,6 +23,7 @@ type Options struct {
// 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"`
@ -39,6 +40,8 @@ type Options struct {
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"`
// Default Config
Show bool `yaml:"-"`
@ -62,6 +65,7 @@ func New() *Options {
return &Options{
Profile: "",
Quality: 85,
Grayscale: true,
Crop: true,
CropRatioLeft: 1,
CropRatioUp: 1,
@ -77,32 +81,34 @@ func New() *Options {
LimitMb: 0,
StripFirstDirectoryFromToc: false,
SortPathMode: 1,
ForegroundColor: "000",
BackgroundColor: "FFF",
profiles: profiles.New(),
}
}
func (o *Options) Header() string {
return `Go Comic Converter
Options:`
return "Go Comic Converter\n\nOptions:"
}
func (o *Options) String() string {
return fmt.Sprintf(`%s
Input : %s
Output : %s
Author : %s
Title : %s
Workers : %d%s
`,
o.Header(),
o.Input,
o.Output,
o.Author,
o.Title,
o.Workers,
o.ShowConfig(),
)
var b strings.Builder
b.WriteString(o.Header())
for _, v := range []struct {
K string
V any
}{
{"Input", o.Input},
{"Output", o.Output},
{"Author", o.Author},
{"Title", o.Title},
{"Workers", o.Workers},
} {
b.WriteString(fmt.Sprintf("\n %-26s: %v", v.K, v.V))
}
b.WriteString(o.ShowConfig())
b.WriteRune('\n')
return b.String()
}
// Config file: ~/.go-comic-converter.yaml
@ -161,40 +167,34 @@ func (o *Options) ShowConfig() string {
sortpathmode = "path=alphanum, file=alphanum"
}
return fmt.Sprintf(`
Profile : %s
ViewRatio : 1:%s
View : %s
Quality : %d
Crop : %v
CropRatio : %d Left - %d Up - %d Right - %d Bottom
Brightness : %d
Contrast : %d
AutoRotate : %v
AutoSplitDoublePage : %v
NoBlankImage : %v
Manga : %v
HasCover : %v
LimitMb : %s
StripFirstDirectoryFromToc: %v
SortPathMode : %s`,
profileDesc,
strings.TrimRight(fmt.Sprintf("%f", profiles.PerfectRatio), "0"),
viewDesc,
o.Quality,
o.Crop,
o.CropRatioLeft, o.CropRatioUp, o.CropRatioRight, o.CropRatioBottom,
o.Brightness,
o.Contrast,
o.AutoRotate,
o.AutoSplitDoublePage,
o.NoBlankImage,
o.Manga,
o.HasCover,
limitmb,
o.StripFirstDirectoryFromToc,
sortpathmode,
)
var b strings.Builder
for _, v := range []struct {
K string
V any
}{
{"Profile", profileDesc},
{"ViewRatio", fmt.Sprintf("1:%s", strings.TrimRight(fmt.Sprintf("%f", profiles.PerfectRatio), "0"))},
{"View", viewDesc},
{"Quality", o.Quality},
{"Grayscale", o.Grayscale},
{"Crop", o.Crop},
{"CropRatio", fmt.Sprintf("%d Left - %d Up - %d Right - %d Bottom", o.CropRatioLeft, o.CropRatioUp, o.CropRatioRight, o.CropRatioBottom)},
{"Brightness", o.Brightness},
{"Contrast", o.Contrast},
{"AutoRotate", o.AutoRotate},
{"AutoSplitDoublePage", o.AutoSplitDoublePage},
{"NoBlankImage", o.NoBlankImage},
{"Manga", o.Manga},
{"HasCover", o.HasCover},
{"LimitMb", limitmb},
{"StripFirstDirectoryFromToc", o.StripFirstDirectoryFromToc},
{"SortPathMode", sortpathmode},
{"Foreground Color", fmt.Sprintf("#%s", o.ForegroundColor)},
{"Background Color", fmt.Sprintf("#%s", o.BackgroundColor)},
} {
b.WriteString(fmt.Sprintf("\n %-26s: %v", v.K, v.V))
}
return b.String()
}
// reset all settings to default value

View File

@ -16,7 +16,7 @@ type Profile struct {
}
// Recommended ratio of image for perfect rendering Portrait or Landscape.
const PerfectRatio = 1.5
const PerfectRatio = 1.6
// Compute best dimension based on device size
func (p Profile) PerfectDim() (int, int) {
@ -60,6 +60,8 @@ func New() Profiles {
{"KoF", "Kobo Forma", 1440, 1920},
{"KoS", "Kobo Sage", 1440, 1920},
{"KoE", "Kobo Elipsa", 1404, 1872},
// High Resolution for Tablette
{"HR", "High Resolution", 2400, 3840},
}
}

View File

@ -77,7 +77,7 @@ func (e *ePub) writeImage(wz *epubzip.EPUBZip, img *epubimage.Image, zipImg *zip
"Title": fmt.Sprintf("Image %d Part %d", img.Id, img.Part),
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.View.Width, e.Image.View.Height),
"ImagePath": img.ImgPath(),
"ImageStyle": img.ImgStyle(e.Image.View.Width, e.Image.View.Height, e.Image.Manga),
"ImageStyle": img.ImgStyle(e.Image.View.Width, e.Image.View.Height, ""),
})),
)
if err == nil {
@ -251,6 +251,10 @@ 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"
}
content := []zipContent{
{"META-INF/container.xml", epubtemplates.Container},
@ -269,8 +273,7 @@ func (e *ePub) Write() error {
})},
{"OEBPS/toc.xhtml", epubtemplates.Toc(title, e.StripFirstDirectoryFromToc, part.Images)},
{"OEBPS/Text/style.css", e.render(epubtemplates.Style, map[string]any{
"PageWidth": e.Image.View.Width,
"PageHeight": e.Image.View.Height,
"View": e.Image.View,
})},
{"OEBPS/Text/space_title.xhtml", e.render(epubtemplates.Blank, map[string]any{
"Title": "Blank Page Title",
@ -280,7 +283,7 @@ func (e *ePub) Write() error {
"Title": title,
"ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.View.Width, e.Image.View.Height),
"ImagePath": "Images/title.jpg",
"ImageStyle": part.Cover.ImgStyle(e.Image.View.Width, e.Image.View.Height, e.Image.Manga),
"ImageStyle": part.Cover.ImgStyle(e.Image.View.Width, e.Image.View.Height, titleAlign),
})},
}

View File

@ -19,6 +19,7 @@ type Image struct {
DoublePage bool
Path string
Name string
Position string
}
// key name of the blank plage after the image
@ -70,24 +71,17 @@ func (i *Image) EPUBImgPath() string {
//
// 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, align string) string {
marginW, marginH := float64(viewWidth-i.Width)/2, float64(viewHeight-i.Height)/2
left, top := marginW*100/float64(viewWidth), marginH*100/float64(viewHeight)
var align string
switch i.Part {
case 0:
align = fmt.Sprintf("left:%.2f%%", left)
case 1:
if manga {
align = "left:0"
} else {
if align == "" {
switch i.Position {
case "rendition:page-spread-left":
align = "right:0"
}
case 2:
if manga {
align = "right:0"
} else {
case "rendition:page-spread-right":
align = "left:0"
default:
align = fmt.Sprintf("left:%.2f%%", marginW*100/float64(viewWidth))
}
}
@ -95,7 +89,7 @@ func (i *Image) ImgStyle(viewWidth, viewHeight int, manga bool) string {
"width:%dpx; height:%dpx; top:%.2f%%; %s;",
i.Width,
i.Height,
top,
marginH*100/float64(viewHeight),
align,
)
}

View File

@ -6,6 +6,7 @@ package epubimageprocessor
import (
"fmt"
"image"
"image/draw"
"os"
"sync"
@ -126,6 +127,37 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
return images, nil
}
func (e *EPUBImageProcessor) createImage(src image.Image, r image.Rectangle) draw.Image {
if e.Options.Image.GrayScale {
return image.NewGray(r)
}
switch t := src.(type) {
case *image.Gray:
return image.NewGray(r)
case *image.Gray16:
return image.NewGray16(r)
case *image.RGBA:
return image.NewRGBA(r)
case *image.RGBA64:
return image.NewRGBA64(r)
case *image.NRGBA:
return image.NewNRGBA(r)
case *image.NRGBA64:
return image.NewNRGBA64(r)
case *image.Alpha:
return image.NewAlpha(r)
case *image.Alpha16:
return image.NewAlpha16(r)
case *image.CMYK:
return image.NewCMYK(r)
case *image.Paletted:
return image.NewPaletted(r, t.Palette)
default:
return image.NewNRGBA64(r)
}
}
// transform image into 1 or 3 images
// only doublepage with autosplit has 3 versions
func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.Image {
@ -177,7 +209,7 @@ func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.
// convert
{
g := gift.New(filters...)
dst := image.NewGray(g.Bounds(src.Bounds()))
dst := e.createImage(src, g.Bounds(src.Bounds()))
g.Draw(dst, src)
images = append(images, dst)
}
@ -204,7 +236,7 @@ func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.
epubimagefilters.CropSplitDoublePage(b),
epubimagefilters.Resize(e.Image.View.Width, e.Image.View.Height, gift.LanczosResampling),
)
dst := image.NewGray(g.Bounds(src.Bounds()))
dst := e.createImage(src, g.Bounds(src.Bounds()))
g.Draw(dst, src)
images = append(images, dst)
}
@ -213,11 +245,11 @@ func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.
}
// create a title page with the cover
func (e *EPUBImageProcessor) CoverTitleData(img image.Image, title string) (*epubzip.ZipImage, error) {
func (e *EPUBImageProcessor) CoverTitleData(src image.Image, title string) (*epubzip.ZipImage, error) {
// Create a blur version of the cover
g := gift.New(epubimagefilters.CoverTitle(title))
dst := image.NewGray(g.Bounds(img.Bounds()))
g.Draw(dst, img)
dst := e.createImage(src, g.Bounds(src.Bounds()))
g.Draw(dst, src)
return epubzip.CompressImage("OEBPS/Images/title.jpg", dst, e.Image.Quality)
}

View File

@ -10,8 +10,13 @@ type Crop struct {
Left, Up, Right, Bottom int
}
type Color struct {
Foreground, Background string
}
type View struct {
Width, Height int
Color Color
}
type Image struct {
@ -25,6 +30,7 @@ type Image struct {
Manga bool
HasCover bool
View *View
GrayScale bool
}
type Options struct {

View File

@ -195,9 +195,11 @@ func getSpine(o *ContentOptions) []tag {
"",
})
}
// register position for style adjustment
img.Position = getSpread(img.DoublePage)
spine = append(spine, tag{
"itemref",
tagAttrs{"idref": img.PageKey(), "properties": getSpread(img.DoublePage)},
tagAttrs{"idref": img.PageKey(), "properties": img.Position},
"",
})
}

View File

@ -1,12 +1,12 @@
body {
color: #000;
background: #FFF;
color: #{{ .View.Color.Foreground }};
background: #{{ .View.Color.Background }};
top: 0;
left: 0;
margin: 0;
padding: 0;
width: {{ .PageWidth }}px;
height: {{ .PageHeight }}px;
width: {{ .View.Width }}px;
height: {{ .View.Height }}px;
text-align: center;
}

View File

@ -114,6 +114,8 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
DryVerbose: cmd.Options.DryVerbose,
Quiet: cmd.Options.Quiet,
Image: &epuboptions.Image{
Quality: cmd.Options.Quality,
GrayScale: cmd.Options.Grayscale,
Crop: &epuboptions.Crop{
Enabled: cmd.Options.Crop,
Left: cmd.Options.CropRatioLeft,
@ -121,7 +123,6 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
Right: cmd.Options.CropRatioRight,
Bottom: cmd.Options.CropRatioBottom,
},
Quality: cmd.Options.Quality,
Brightness: cmd.Options.Brightness,
Contrast: cmd.Options.Contrast,
AutoRotate: cmd.Options.AutoRotate,
@ -132,6 +133,10 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
View: &epuboptions.View{
Width: perfectWidth,
Height: perfectHeight,
Color: epuboptions.Color{
Foreground: cmd.Options.ForegroundColor,
Background: cmd.Options.BackgroundColor,
},
},
},
}).Write(); err != nil {