Compare commits

...

6 Commits

Author SHA1 Message Date
abf3b8facf
add shortcut 2023-05-01 19:27:51 +02:00
728129aba2
reorg params and display relevant params only 2023-05-01 19:08:26 +02:00
a816350c97
add png output images format 2023-05-01 18:59:05 +02:00
58cd5f5399
add nofilter option 2023-05-01 17:25:19 +02:00
ee00ed2615
option to keep skip resize filter 2023-05-01 17:09:47 +02:00
13103b0eba
do not enlarge smaller images
all width/height are computed based on viewport dimension.
images will enlarge if smaller, and resize to fit if larger.
2023-05-01 14:11:58 +02:00
12 changed files with 175 additions and 117 deletions

View File

@ -110,7 +110,6 @@ func (c *Converter) InitParse() {
c.AddIntParam(&c.Options.Brightness, "brightness", c.Options.Brightness, "Brightness readjustement: between -100 and 100, > 0 lighter, < 0 darker")
c.AddIntParam(&c.Options.Contrast, "contrast", c.Options.Contrast, "Contrast readjustement: between -100 and 100, > 0 more contrast, < 0 less contrast")
c.AddBoolParam(&c.Options.AutoRotate, "autorotate", c.Options.AutoRotate, "Auto Rotate page when width > height")
c.AddBoolParam(&c.Options.Auto, "auto", false, "Activate all automatic options")
c.AddBoolParam(&c.Options.AutoSplitDoublePage, "autosplitdoublepage", c.Options.AutoSplitDoublePage, "Auto Split double page when width > height")
c.AddBoolParam(&c.Options.NoBlankImage, "noblankimage", c.Options.NoBlankImage, "Remove blank image")
c.AddBoolParam(&c.Options.Manga, "manga", c.Options.Manga, "Manga mode (right to left)")
@ -120,12 +119,22 @@ func (c *Converter) InitParse() {
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.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.AddSection("Default config")
c.AddBoolParam(&c.Options.Show, "show", false, "Show your default parameters")
c.AddBoolParam(&c.Options.Save, "save", false, "Save your parameters as default")
c.AddBoolParam(&c.Options.Reset, "reset", false, "Reset your parameters to default")
c.AddSection("Shortcut")
c.AddBoolParam(&c.Options.Auto, "auto", false, "Activate all automatic options")
c.AddBoolParam(&c.Options.NoFilter, "nofilter", false, "Deactivate all filters")
c.AddBoolParam(&c.Options.MaxQuality, "maxquality", false, "Max quality: color png + noresize")
c.AddBoolParam(&c.Options.BestQuality, "bestquality", false, "Max quality: color jpg q100 + noresize")
c.AddBoolParam(&c.Options.GreatQuality, "greatquality", false, "Max quality: grayscale jpg q90 + noresize")
c.AddBoolParam(&c.Options.GoodQuality, "goodquality", false, "Max quality: grayscale jpg q90")
c.AddSection("Other")
c.AddIntParam(&c.Options.Workers, "workers", runtime.NumCPU(), "Number of workers")
c.AddBoolParam(&c.Options.Dry, "dry", false, "Dry run to show all options")
@ -211,6 +220,36 @@ func (c *Converter) Parse() {
c.Options.AutoRotate = true
c.Options.AutoSplitDoublePage = true
}
if c.Options.MaxQuality {
c.Options.Format = "png"
c.Options.Grayscale = false
c.Options.NoResize = true
} else if c.Options.BestQuality {
c.Options.Format = "jpeg"
c.Options.Quality = 100
c.Options.Grayscale = false
c.Options.NoResize = true
} else if c.Options.GreatQuality {
c.Options.Format = "jpeg"
c.Options.Quality = 90
c.Options.Grayscale = true
c.Options.NoResize = true
} else if c.Options.GoodQuality {
c.Options.Format = "jpeg"
c.Options.Quality = 90
c.Options.Grayscale = true
c.Options.NoResize = false
}
if c.Options.NoFilter {
c.Options.Crop = false
c.Options.Brightness = 0
c.Options.Contrast = 0
c.Options.AutoRotate = false
c.Options.NoBlankImage = false
c.Options.NoResize = true
}
}
// Check parameters
@ -307,6 +346,11 @@ func (c *Converter) Validate() error {
return errors.New("background color must have color format in hexa: [0-9A-F]{3}")
}
// Format
if !(c.Options.Format == "jpeg" || c.Options.Format == "png") {
return errors.New("format should be jpeg or png")
}
return nil
}

View File

@ -31,7 +31,6 @@ type Options struct {
CropRatioBottom int `yaml:"crop_ratio_bottom"`
Brightness int `yaml:"brightness"`
Contrast int `yaml:"contrast"`
Auto bool `yaml:"-"`
AutoRotate bool `yaml:"auto_rotate"`
AutoSplitDoublePage bool `yaml:"auto_split_double_page"`
NoBlankImage bool `yaml:"no_blank_image"`
@ -42,12 +41,22 @@ type Options struct {
SortPathMode int `yaml:"sort_path_mode"`
ForegroundColor string `yaml:"foreground_color"`
BackgroundColor string `yaml:"background_color"`
NoResize bool `yaml:"noresize"`
Format string `yaml:"format"`
// Default Config
Show bool `yaml:"-"`
Save bool `yaml:"-"`
Reset bool `yaml:"-"`
// Shortcut
Auto bool `yaml:"-"`
NoFilter bool `yaml:"-"`
MaxQuality bool `yaml:"-"`
BestQuality bool `yaml:"-"`
GreatQuality bool `yaml:"-"`
GoodQuality bool `yaml:"-"`
// Other
Workers int `yaml:"-"`
Dry bool `yaml:"-"`
@ -83,6 +92,8 @@ func New() *Options {
SortPathMode: 1,
ForegroundColor: "000",
BackgroundColor: "FFF",
NoResize: false,
Format: "jpeg",
profiles: profiles.New(),
}
}
@ -152,10 +163,6 @@ func (o *Options) ShowConfig() string {
perfectHeight,
)
}
limitmb := "nolimit"
if o.LimitMb > 0 {
limitmb = fmt.Sprintf("%d Mb", o.LimitMb)
}
sortpathmode := ""
switch o.SortPathMode {
@ -169,30 +176,35 @@ func (o *Options) ShowConfig() string {
var b strings.Builder
for _, v := range []struct {
K string
V any
Key string
Value any
Condition bool
}{
{"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)},
{"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},
{"Crop", o.Crop, true},
{"CropRatio", fmt.Sprintf("%d Left - %d Up - %d Right - %d Bottom", o.CropRatioLeft, o.CropRatioUp, o.CropRatioRight, o.CropRatioBottom), o.Crop},
{"Brightness", o.Brightness, o.Brightness != 0},
{"Contrast", o.Contrast, o.Contrast != 0},
{"AutoRotate", o.AutoRotate, true},
{"AutoSplitDoublePage", o.AutoSplitDoublePage, true},
{"NoBlankImage", o.NoBlankImage, true},
{"Manga", o.Manga, true},
{"HasCover", o.HasCover, true},
{"LimitMb", fmt.Sprintf("%d Mb", o.LimitMb), o.LimitMb != 0},
{"StripFirstDirectoryFromToc", o.StripFirstDirectoryFromToc, true},
{"SortPathMode", sortpathmode, true},
{"Foreground Color", fmt.Sprintf("#%s", o.ForegroundColor), true},
{"Background Color", fmt.Sprintf("#%s", o.BackgroundColor), true},
{"Resize", !o.NoResize, true},
} {
b.WriteString(fmt.Sprintf("\n %-26s: %v", v.K, v.V))
if v.Condition {
b.WriteString(fmt.Sprintf("\n %-26s: %v", v.Key, v.Value))
}
}
return b.String()
}

View File

@ -282,7 +282,7 @@ func (e *ePub) Write() error {
{"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),
"ImagePath": "Images/title.jpg",
"ImagePath": fmt.Sprintf("Images/title.%s", e.Image.Format),
"ImageStyle": part.Cover.ImgStyle(e.Image.View.Width, e.Image.View.Height, titleAlign),
})},
}

View File

@ -6,6 +6,7 @@ package epubimage
import (
"fmt"
"image"
"strings"
)
type Image struct {
@ -20,6 +21,7 @@ type Image struct {
Path string
Name string
Position string
Format string
}
// key name of the blank plage after the image
@ -59,7 +61,7 @@ func (i *Image) ImgKey() string {
// image path
func (i *Image) ImgPath() string {
return fmt.Sprintf("Images/%s.jpg", i.ImgKey())
return fmt.Sprintf("Images/%s.%s", i.ImgKey(), i.Format)
}
// image path into the EPUB
@ -72,24 +74,48 @@ 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, align string) string {
marginW, marginH := float64(viewWidth-i.Width)/2, float64(viewHeight-i.Height)/2
relWidth, relHeight := i.RelSize(viewWidth, viewHeight)
marginW, marginH := float64(viewWidth-relWidth)/2, float64(viewHeight-relHeight)/2
style := []string{}
style = append(style, fmt.Sprintf("width:%dpx", relWidth))
style = append(style, fmt.Sprintf("height:%dpx", relHeight))
style = append(style, fmt.Sprintf("top:%.2f%%", marginH*100/float64(viewHeight)))
if align == "" {
switch i.Position {
case "rendition:page-spread-left":
align = "right:0"
style = append(style, "right:0")
case "rendition:page-spread-right":
align = "left:0"
style = append(style, "left:0")
default:
align = fmt.Sprintf("left:%.2f%%", marginW*100/float64(viewWidth))
style = append(style, fmt.Sprintf("left:%.2f%%", marginW*100/float64(viewWidth)))
}
} else {
style = append(style, align)
}
return fmt.Sprintf(
"width:%dpx; height:%dpx; top:%.2f%%; %s;",
i.Width,
i.Height,
marginH*100/float64(viewHeight),
align,
)
return strings.Join(style, "; ")
}
func (i *Image) RelSize(viewWidth, viewHeight int) (relWidth, relHeight int) {
w, h := viewWidth, viewHeight
srcw, srch := i.Width, i.Height
if w <= 0 || h <= 0 || srcw <= 0 || srch <= 0 {
return
}
wratio := float64(srcw) / float64(w)
hratio := float64(srch) / float64(h)
if wratio > hratio {
relWidth = w
relHeight = int(float64(srch)/wratio + 0.5)
} else {
relHeight = h
relWidth = int(float64(srcw)/hratio + 0.5)
}
return
}

View File

@ -1,48 +0,0 @@
package epubimagefilters
import (
"image"
"image/draw"
"github.com/disintegration/gift"
)
// Resize image by keeping aspect ratio.
// This will reduce or enlarge image to fit into the viewWidth and viewHeight.
func Resize(viewWidth, viewHeight int, resampling gift.Resampling) gift.Filter {
return &resizeFilter{
viewWidth, viewHeight, resampling,
}
}
type resizeFilter struct {
viewWidth, viewHeight int
resampling gift.Resampling
}
func (p *resizeFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
w, h := p.viewWidth, p.viewHeight
srcw, srch := srcBounds.Dx(), srcBounds.Dy()
if w <= 0 || h <= 0 || srcw <= 0 || srch <= 0 {
return image.Rect(0, 0, 0, 0)
}
wratio := float64(srcw) / float64(w)
hratio := float64(srch) / float64(h)
var dstw, dsth int
if wratio > hratio {
dstw = w
dsth = int(float64(srch)/wratio + 0.5)
} else {
dsth = h
dstw = int(float64(srcw)/hratio + 0.5)
}
return image.Rect(0, 0, dstw, dsth)
}
func (p *resizeFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
gift.Resize(dst.Bounds().Dx(), dst.Bounds().Dy(), p.resampling).Draw(dst, src, options)
}

View File

@ -41,6 +41,7 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
Id: img.Id,
Path: img.Path,
Name: img.Name,
Format: e.Image.Format,
})
}
@ -59,13 +60,17 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
})
wg := &sync.WaitGroup{}
imgStorage, err := epubzip.NewEPUBZipStorageImageWriter(e.ImgStorage())
imgStorage, err := epubzip.NewEPUBZipStorageImageWriter(e.ImgStorage(), e.Image.Format)
if err != nil {
bar.Close()
return nil, err
}
for i := 0; i < e.WorkersRatio(50); i++ {
wr := 50
if e.Image.Format == "png" {
wr = 100
}
for i := 0; i < e.WorkersRatio(wr); i++ {
wg.Add(1)
go func() {
defer wg.Done()
@ -90,6 +95,7 @@ func (e *EPUBImageProcessor) Load() (images []*epubimage.Image, err error) {
DoublePage: part == 0 && src.Bounds().Dx() > src.Bounds().Dy(),
Path: input.Path,
Name: input.Name,
Format: e.Image.Format,
}
if err = imgStorage.Add(img.EPUBImgPath(), dst, e.Image.Quality); err != nil {
@ -201,10 +207,12 @@ func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.
splitFilter = append(splitFilter, f)
}
filters = append(filters,
epubimagefilters.Resize(e.Image.View.Width, e.Image.View.Height, gift.LanczosResampling),
epubimagefilters.Pixel(),
)
if e.Image.Resize {
f := gift.ResizeToFit(e.Image.View.Width, e.Image.View.Height, gift.LanczosResampling)
filters = append(filters, f)
}
filters = append(filters, epubimagefilters.Pixel())
// convert
{
@ -232,10 +240,10 @@ func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.
// convert double page
for _, b := range []bool{e.Image.Manga, !e.Image.Manga} {
g := gift.New(splitFilter...)
g.Add(
epubimagefilters.CropSplitDoublePage(b),
epubimagefilters.Resize(e.Image.View.Width, e.Image.View.Height, gift.LanczosResampling),
)
g.Add(epubimagefilters.CropSplitDoublePage(b))
if e.Image.Resize {
g.Add(gift.ResizeToFit(e.Image.View.Width, e.Image.View.Height, gift.LanczosResampling))
}
dst := e.createImage(src, g.Bounds(src.Bounds()))
g.Draw(dst, src)
images = append(images, dst)
@ -251,5 +259,10 @@ func (e *EPUBImageProcessor) CoverTitleData(src image.Image, title string) (*epu
dst := e.createImage(src, g.Bounds(src.Bounds()))
g.Draw(dst, src)
return epubzip.CompressImage("OEBPS/Images/title.jpg", dst, e.Image.Quality)
return epubzip.CompressImage(
fmt.Sprintf("OEBPS/Images/title.%s", e.Image.Format),
e.Image.Format,
dst,
e.Image.Quality,
)
}

View File

@ -31,6 +31,8 @@ type Image struct {
HasCover bool
View *View
GrayScale bool
Resize bool
Format string
}
type Options struct {

View File

@ -127,7 +127,7 @@ func getManifest(o *ContentOptions) []tag {
var imageTags, pageTags, spaceTags []tag
addTag := func(img *epubimage.Image, withSpace bool) {
imageTags = append(imageTags,
tag{"item", tagAttrs{"id": img.ImgKey(), "href": img.ImgPath(), "media-type": "image/jpeg"}, ""},
tag{"item", tagAttrs{"id": img.ImgKey(), "href": img.ImgPath(), "media-type": fmt.Sprintf("image/%s", o.ImageOptions.Format)}, ""},
)
pageTags = append(pageTags,
tag{"item", tagAttrs{"id": img.PageKey(), "href": img.PagePath(), "media-type": "application/xhtml+xml"}, ""},
@ -144,7 +144,7 @@ func getManifest(o *ContentOptions) []tag {
{"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": "Images/title.jpg", "media-type": "image/jpeg"}, ""},
{"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.HasCover || o.Current > 1 {

View File

@ -8,8 +8,6 @@
<meta name="viewport" content="{{ .ViewPort }}"/>
</head>
<body>
<div>
<img src="../{{ .ImagePath }}" alt="{{ .Title }}" style="{{ .ImageStyle }}"/>
</div>
</body>
</html>

View File

@ -4,9 +4,11 @@ import (
"archive/zip"
"bytes"
"compress/flate"
"fmt"
"hash/crc32"
"image"
"image/jpeg"
"image/png"
"time"
)
@ -16,13 +18,20 @@ type ZipImage struct {
}
// create gzip encoded jpeg
func CompressImage(filename string, img image.Image, quality int) (*ZipImage, error) {
func CompressImage(filename string, format string, img image.Image, quality int) (*ZipImage, error) {
var (
data, cdata bytes.Buffer
err error
)
switch format {
case "png":
err = png.Encode(&data, img)
case "jpeg":
err = jpeg.Encode(&data, img, &jpeg.Options{Quality: quality})
default:
err = fmt.Errorf("unknown format %q", format)
}
if err != nil {
return nil, err
}

View File

@ -10,17 +10,17 @@ import (
type EPUBZipStorageImageWriter struct {
fh *os.File
fz *zip.Writer
format string
mut *sync.Mutex
}
func NewEPUBZipStorageImageWriter(filename string) (*EPUBZipStorageImageWriter, error) {
func NewEPUBZipStorageImageWriter(filename string, format string) (*EPUBZipStorageImageWriter, error) {
fh, err := os.Create(filename)
if err != nil {
return nil, err
}
fz := zip.NewWriter(fh)
return &EPUBZipStorageImageWriter{fh, fz, &sync.Mutex{}}, nil
return &EPUBZipStorageImageWriter{fh, fz, format, &sync.Mutex{}}, nil
}
func (e *EPUBZipStorageImageWriter) Close() error {
@ -32,7 +32,7 @@ func (e *EPUBZipStorageImageWriter) Close() error {
}
func (e *EPUBZipStorageImageWriter) Add(filename string, img image.Image, quality int) error {
zipImage, err := CompressImage(filename, img, quality)
zipImage, err := CompressImage(filename, e.format, img, quality)
if err != nil {
return err
}

View File

@ -138,6 +138,8 @@ $ go install github.com/celogeek/go-comic-converter/v%d@%s
Background: cmd.Options.BackgroundColor,
},
},
Resize: !cmd.Options.NoResize,
Format: cmd.Options.Format,
},
}).Write(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)