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" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"regexp"
"runtime" "runtime"
"strings" "strings"
"time" "time"
@ -100,6 +101,7 @@ func (c *Converter) InitParse() {
c.AddSection("Config") c.AddSection("Config")
c.AddStringParam(&c.Options.Profile, "profile", c.Options.Profile, fmt.Sprintf("Profile to use: \n%s", c.Options.AvailableProfiles())) 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.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.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.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.") 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.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.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.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.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")
@ -293,6 +297,16 @@ func (c *Converter) Validate() error {
return errors.New("sort should be 0, 1 or 2") 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 return nil
} }

View File

@ -23,6 +23,7 @@ type Options struct {
// Config // Config
Profile string `yaml:"profile"` Profile string `yaml:"profile"`
Quality int `yaml:"quality"` Quality int `yaml:"quality"`
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"`
@ -39,6 +40,8 @@ type Options struct {
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"`
BackgroundColor string `yaml:"background_color"`
// Default Config // Default Config
Show bool `yaml:"-"` Show bool `yaml:"-"`
@ -62,6 +65,7 @@ func New() *Options {
return &Options{ return &Options{
Profile: "", Profile: "",
Quality: 85, Quality: 85,
Grayscale: true,
Crop: true, Crop: true,
CropRatioLeft: 1, CropRatioLeft: 1,
CropRatioUp: 1, CropRatioUp: 1,
@ -77,32 +81,34 @@ func New() *Options {
LimitMb: 0, LimitMb: 0,
StripFirstDirectoryFromToc: false, StripFirstDirectoryFromToc: false,
SortPathMode: 1, SortPathMode: 1,
ForegroundColor: "000",
BackgroundColor: "FFF",
profiles: profiles.New(), profiles: profiles.New(),
} }
} }
func (o *Options) Header() string { func (o *Options) Header() string {
return `Go Comic Converter return "Go Comic Converter\n\nOptions:"
Options:`
} }
func (o *Options) String() string { func (o *Options) String() string {
return fmt.Sprintf(`%s var b strings.Builder
Input : %s b.WriteString(o.Header())
Output : %s for _, v := range []struct {
Author : %s K string
Title : %s V any
Workers : %d%s }{
`, {"Input", o.Input},
o.Header(), {"Output", o.Output},
o.Input, {"Author", o.Author},
o.Output, {"Title", o.Title},
o.Author, {"Workers", o.Workers},
o.Title, } {
o.Workers, b.WriteString(fmt.Sprintf("\n %-26s: %v", v.K, v.V))
o.ShowConfig(), }
) b.WriteString(o.ShowConfig())
b.WriteRune('\n')
return b.String()
} }
// Config file: ~/.go-comic-converter.yaml // Config file: ~/.go-comic-converter.yaml
@ -161,40 +167,34 @@ func (o *Options) ShowConfig() string {
sortpathmode = "path=alphanum, file=alphanum" sortpathmode = "path=alphanum, file=alphanum"
} }
return fmt.Sprintf(` var b strings.Builder
Profile : %s for _, v := range []struct {
ViewRatio : 1:%s K string
View : %s V any
Quality : %d }{
Crop : %v {"Profile", profileDesc},
CropRatio : %d Left - %d Up - %d Right - %d Bottom {"ViewRatio", fmt.Sprintf("1:%s", strings.TrimRight(fmt.Sprintf("%f", profiles.PerfectRatio), "0"))},
Brightness : %d {"View", viewDesc},
Contrast : %d {"Quality", o.Quality},
AutoRotate : %v {"Grayscale", o.Grayscale},
AutoSplitDoublePage : %v {"Crop", o.Crop},
NoBlankImage : %v {"CropRatio", fmt.Sprintf("%d Left - %d Up - %d Right - %d Bottom", o.CropRatioLeft, o.CropRatioUp, o.CropRatioRight, o.CropRatioBottom)},
Manga : %v {"Brightness", o.Brightness},
HasCover : %v {"Contrast", o.Contrast},
LimitMb : %s {"AutoRotate", o.AutoRotate},
StripFirstDirectoryFromToc: %v {"AutoSplitDoublePage", o.AutoSplitDoublePage},
SortPathMode : %s`, {"NoBlankImage", o.NoBlankImage},
profileDesc, {"Manga", o.Manga},
strings.TrimRight(fmt.Sprintf("%f", profiles.PerfectRatio), "0"), {"HasCover", o.HasCover},
viewDesc, {"LimitMb", limitmb},
o.Quality, {"StripFirstDirectoryFromToc", o.StripFirstDirectoryFromToc},
o.Crop, {"SortPathMode", sortpathmode},
o.CropRatioLeft, o.CropRatioUp, o.CropRatioRight, o.CropRatioBottom, {"Foreground Color", fmt.Sprintf("#%s", o.ForegroundColor)},
o.Brightness, {"Background Color", fmt.Sprintf("#%s", o.BackgroundColor)},
o.Contrast, } {
o.AutoRotate, b.WriteString(fmt.Sprintf("\n %-26s: %v", v.K, v.V))
o.AutoSplitDoublePage, }
o.NoBlankImage, return b.String()
o.Manga,
o.HasCover,
limitmb,
o.StripFirstDirectoryFromToc,
sortpathmode,
)
} }
// reset all settings to default value // 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. // 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 // Compute best dimension based on device size
func (p Profile) PerfectDim() (int, int) { func (p Profile) PerfectDim() (int, int) {
@ -60,6 +60,8 @@ func New() Profiles {
{"KoF", "Kobo Forma", 1440, 1920}, {"KoF", "Kobo Forma", 1440, 1920},
{"KoS", "Kobo Sage", 1440, 1920}, {"KoS", "Kobo Sage", 1440, 1920},
{"KoE", "Kobo Elipsa", 1404, 1872}, {"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), "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), "ViewPort": fmt.Sprintf("width=%d,height=%d", e.Image.View.Width, e.Image.View.Height),
"ImagePath": img.ImgPath(), "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 { if err == nil {
@ -251,6 +251,10 @@ 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"
if e.Image.Manga {
titleAlign = "right:0"
}
content := []zipContent{ content := []zipContent{
{"META-INF/container.xml", epubtemplates.Container}, {"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/toc.xhtml", epubtemplates.Toc(title, e.StripFirstDirectoryFromToc, part.Images)},
{"OEBPS/Text/style.css", e.render(epubtemplates.Style, map[string]any{ {"OEBPS/Text/style.css", e.render(epubtemplates.Style, map[string]any{
"PageWidth": e.Image.View.Width, "View": e.Image.View,
"PageHeight": e.Image.View.Height,
})}, })},
{"OEBPS/Text/space_title.xhtml", e.render(epubtemplates.Blank, map[string]any{ {"OEBPS/Text/space_title.xhtml", e.render(epubtemplates.Blank, map[string]any{
"Title": "Blank Page Title", "Title": "Blank Page Title",
@ -280,7 +283,7 @@ func (e *ePub) Write() error {
"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),
"ImagePath": "Images/title.jpg", "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 DoublePage bool
Path string Path string
Name string Name string
Position string
} }
// key name of the blank plage after the image // key name of the blank plage after the image
@ -70,24 +71,17 @@ func (i *Image) EPUBImgPath() string {
// //
// center by default. // center by default.
// align to left or right if it's part of the splitted double page. // 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 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 if align == "" {
switch i.Part { switch i.Position {
case 0: case "rendition:page-spread-left":
align = fmt.Sprintf("left:%.2f%%", left)
case 1:
if manga {
align = "left:0"
} else {
align = "right:0" align = "right:0"
} case "rendition:page-spread-right":
case 2:
if manga {
align = "right:0"
} else {
align = "left:0" 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;", "width:%dpx; height:%dpx; top:%.2f%%; %s;",
i.Width, i.Width,
i.Height, i.Height,
top, marginH*100/float64(viewHeight),
align, align,
) )
} }

View File

@ -6,6 +6,7 @@ package epubimageprocessor
import ( import (
"fmt" "fmt"
"image" "image"
"image/draw"
"os" "os"
"sync" "sync"
@ -126,6 +127,37 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
return images, nil 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 // transform image into 1 or 3 images
// only doublepage with autosplit has 3 versions // only doublepage with autosplit has 3 versions
func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.Image { 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 // convert
{ {
g := gift.New(filters...) g := gift.New(filters...)
dst := image.NewGray(g.Bounds(src.Bounds())) dst := e.createImage(src, g.Bounds(src.Bounds()))
g.Draw(dst, src) g.Draw(dst, src)
images = append(images, dst) images = append(images, dst)
} }
@ -204,7 +236,7 @@ func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.
epubimagefilters.CropSplitDoublePage(b), epubimagefilters.CropSplitDoublePage(b),
epubimagefilters.Resize(e.Image.View.Width, e.Image.View.Height, gift.LanczosResampling), 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) g.Draw(dst, src)
images = append(images, dst) 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 // 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 // Create a blur version of the cover
g := gift.New(epubimagefilters.CoverTitle(title)) g := gift.New(epubimagefilters.CoverTitle(title))
dst := image.NewGray(g.Bounds(img.Bounds())) dst := e.createImage(src, g.Bounds(src.Bounds()))
g.Draw(dst, img) g.Draw(dst, src)
return epubzip.CompressImage("OEBPS/Images/title.jpg", dst, e.Image.Quality) 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 Left, Up, Right, Bottom int
} }
type Color struct {
Foreground, Background string
}
type View struct { type View struct {
Width, Height int Width, Height int
Color Color
} }
type Image struct { type Image struct {
@ -25,6 +30,7 @@ type Image struct {
Manga bool Manga bool
HasCover bool HasCover bool
View *View View *View
GrayScale bool
} }
type Options struct { 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{ spine = append(spine, tag{
"itemref", "itemref",
tagAttrs{"idref": img.PageKey(), "properties": getSpread(img.DoublePage)}, tagAttrs{"idref": img.PageKey(), "properties": img.Position},
"", "",
}) })
} }

View File

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

View File

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