cover improvement

include part in cover if totalParts > 1
cleaner cover in grayscale
This commit is contained in:
Celogeek 2023-05-05 11:39:44 +02:00
parent 4a1ae516ea
commit 0de6cc9acd
Signed by: celogeek
SSH Key Fingerprint: SHA256:njNJLzoLQdbV9PC6ehcruRb0QnEgxABoCYZ+0+aUIYc
4 changed files with 146 additions and 53 deletions

View File

@ -99,6 +99,49 @@ func (e *ePub) writeBlank(wz *epubzip.EPUBZip, img *epubimage.Image) error {
)
}
// write title image
func (e *ePub) writeCoverImage(wz *epubzip.EPUBZip, img *epubimage.Image, part, totalParts int) error {
title := "Cover"
text := ""
if totalParts > 1 {
text = fmt.Sprintf("%d / %d", part, totalParts)
title = fmt.Sprintf("%s %s", title, text)
}
if err := wz.WriteContent(
"OEBPS/Text/cover.xhtml",
[]byte(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": fmt.Sprintf("Images/cover.%s", e.Image.Format),
"ImageStyle": img.ImgStyle(e.Image.View.Width, e.Image.View.Height, ""),
})),
); err != nil {
return err
}
coverTitle, err := e.imageProcessor.CoverTitleData(&epubimageprocessor.CoverTitleDataOptions{
Src: img.Raw,
Name: "cover",
Text: text,
Align: "bottom",
PctWidth: 50,
PctMargin: 50,
MaxFontSize: 96,
BorderSize: 8,
})
if err != nil {
return err
}
if err := wz.WriteRaw(coverTitle); err != nil {
return err
}
return nil
}
// write title image
func (e *ePub) writeTitleImage(wz *epubzip.EPUBZip, img *epubimage.Image, title string) error {
titleAlign := ""
@ -134,7 +177,16 @@ func (e *ePub) writeTitleImage(wz *epubzip.EPUBZip, img *epubimage.Image, title
return err
}
coverTitle, err := e.imageProcessor.CoverTitleData(img.Raw, title)
coverTitle, err := e.imageProcessor.CoverTitleData(&epubimageprocessor.CoverTitleDataOptions{
Src: img.Raw,
Name: "title",
Text: title,
Align: "center",
PctWidth: 100,
PctMargin: 100,
MaxFontSize: 64,
BorderSize: 4,
})
if err != nil {
return err
}
@ -184,11 +236,8 @@ func (e *ePub) getParts() (parts []*epubPart, imgStorage *epubzip.EPUBZipStorage
// compute size of the EPUB part and try to be as close as possible of the target
maxSize := uint64(e.LimitMb * 1024 * 1024)
xhtmlSize := uint64(1024)
// descriptor files + title
baseSize := uint64(16*1024) + imgStorage.Size(cover.EPUBImgPath())
if e.Image.HasCover {
baseSize += imgStorage.Size(cover.EPUBImgPath())
}
// descriptor files + title + cover
baseSize := uint64(16*1024) + imgStorage.Size(cover.EPUBImgPath())*2
currentSize := baseSize
currentImages := make([]*epubimage.Image, 0)
@ -203,9 +252,6 @@ func (e *ePub) getParts() (parts []*epubPart, imgStorage *epubzip.EPUBZipStorage
})
part += 1
currentSize = baseSize
if !e.Image.HasCover {
currentSize += imgStorage.Size(cover.EPUBImgPath())
}
currentImages = make([]*epubimage.Image, 0)
}
currentSize += imgSize
@ -377,16 +423,12 @@ func (e *ePub) Write() error {
}
}
if err = e.writeTitleImage(wz, part.Cover, title); err != nil {
if err = e.writeCoverImage(wz, part.Cover, i+1, totalParts); err != nil {
return err
}
// Cover exist or part > 1
// If no cover, part 2 and more will include the image as a cover
if e.Image.HasCover || i > 0 {
if err := e.writeImage(wz, part.Cover, imgStorage.Get(part.Cover.EPUBImgPath())); err != nil {
return err
}
if err = e.writeTitleImage(wz, part.Cover, title); err != nil {
return err
}
lastImage := part.Images[len(part.Images)-1]

View File

@ -12,12 +12,17 @@ import (
)
// Create a title with the cover image
func CoverTitle(title string) gift.Filter {
return &coverTitle{title}
func CoverTitle(title string, align string, pctWidth int, pctMargin int, maxFontSize int, borderSize int) gift.Filter {
return &coverTitle{title, align, pctWidth, pctMargin, maxFontSize, borderSize}
}
type coverTitle struct {
title string
title string
align string
pctWidth int
pctMargin int
maxFontSize int
borderSize int
}
// size is the same as source
@ -28,28 +33,36 @@ func (p *coverTitle) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangl
// blur the src image, and create a box with the title in the middle
func (p *coverTitle) Draw(dst draw.Image, src image.Image, options *gift.Options) {
draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
if p.title == "" {
return
}
srcWidth, srcHeight := src.Bounds().Dx(), src.Bounds().Dy()
// Calculate size of title
f, _ := truetype.Parse(gomonobold.TTF)
borderSize := 4
var fontSize, textWidth, textHeight int
for fontSize = 64; fontSize >= 12; fontSize -= 1 {
for fontSize = p.maxFontSize; fontSize >= 12; fontSize -= 1 {
face := truetype.NewFace(f, &truetype.Options{Size: float64(fontSize), DPI: 72})
textWidth = font.MeasureString(face, p.title).Ceil()
textHeight = face.Metrics().Ascent.Ceil() + face.Metrics().Descent.Ceil()
if textWidth+2*borderSize < srcWidth && 3*textHeight+2*borderSize < srcHeight {
if textWidth+2*p.borderSize < srcWidth*p.pctWidth/100 && 3*textHeight+2*p.borderSize < srcHeight {
break
}
}
// Draw rectangle in the middle of the image
textPosStart := srcHeight/2 - textHeight/2
textPosEnd := srcHeight/2 + textHeight/2
marginSize := fontSize
borderArea := image.Rect(0, textPosStart-borderSize-marginSize, srcWidth, textPosEnd+borderSize+marginSize)
textArea := image.Rect(borderSize, textPosStart-marginSize, srcWidth-borderSize, textPosEnd+marginSize)
marginSize := fontSize * p.pctMargin / 100
var textPosStart, textPosEnd int
if p.align == "bottom" {
textPosStart = srcHeight - textHeight - p.borderSize - marginSize
textPosEnd = srcHeight - p.borderSize - marginSize
} else {
textPosStart = srcHeight/2 - textHeight/2
textPosEnd = srcHeight/2 + textHeight/2
}
borderArea := image.Rect((srcWidth-(srcWidth*p.pctWidth/100))/2, textPosStart-p.borderSize-marginSize, (srcWidth+(srcWidth*p.pctWidth/100))/2, textPosEnd+p.borderSize+marginSize)
textArea := image.Rect(borderArea.Bounds().Min.X+p.borderSize, textPosStart-marginSize, borderArea.Bounds().Max.X-p.borderSize, textPosEnd+marginSize)
draw.Draw(
dst,
@ -76,9 +89,10 @@ func (p *coverTitle) Draw(dst draw.Image, src image.Image, options *gift.Options
c.SetDst(dst)
c.SetSrc(image.Black)
textLeft := srcWidth/2 - textWidth/2
if textLeft < borderSize {
textLeft = borderSize
textLeft := textArea.Min.X + textArea.Dx()/2 - textWidth/2
if textLeft < textArea.Min.X {
textLeft = textArea.Min.X
}
c.DrawString(p.title, freetype.Pt(textLeft, srcHeight/2+textHeight/4))
textTop := textArea.Min.Y + textArea.Dy()/2 + textHeight/4
c.DrawString(p.title, freetype.Pt(textLeft, textTop))
}

View File

@ -6,6 +6,7 @@ package epubimageprocessor
import (
"fmt"
"image"
"image/color"
"image/draw"
"os"
"sync"
@ -168,7 +169,7 @@ func (e *EPUBImageProcessor) createImage(src image.Image, r image.Rectangle) dra
// 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 {
var filters, splitFilter []gift.Filter
var filters, splitFilters []gift.Filter
var images []image.Image
// Lookup for margin if crop is enable or if we want to remove blank image
@ -188,7 +189,7 @@ func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.
// crop is enable or if blank image with noblankimage options
if e.Image.Crop.Enabled || (e.Image.NoBlankImage && isBlank) {
filters = append(filters, f)
splitFilter = append(splitFilter, f)
splitFilters = append(splitFilters, f)
}
}
@ -199,13 +200,13 @@ func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.
if e.Image.Contrast != 0 {
f := gift.Contrast(float32(e.Image.Contrast))
filters = append(filters, f)
splitFilter = append(splitFilter, f)
splitFilters = append(splitFilters, f)
}
if e.Image.Brightness != 0 {
f := gift.Brightness(float32(e.Image.Brightness))
filters = append(filters, f)
splitFilter = append(splitFilter, f)
splitFilters = append(splitFilters, f)
}
if e.Image.Resize {
@ -213,6 +214,11 @@ func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.
filters = append(filters, f)
}
if e.Image.GrayScale {
filters = append(filters, gift.Grayscale())
splitFilters = append(splitFilters, gift.Grayscale())
}
filters = append(filters, epubimagefilters.Pixel())
// convert
@ -240,7 +246,7 @@ 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 := gift.New(splitFilters...)
g.Add(epubimagefilters.CropSplitDoublePage(b))
if e.Image.Resize {
g.Add(gift.ResizeToFit(e.Image.View.Width, e.Image.View.Height, gift.LanczosResampling))
@ -253,15 +259,52 @@ func (e *EPUBImageProcessor) transformImage(src image.Image, srcId int) []image.
return images
}
type CoverTitleDataOptions struct {
Src image.Image
Name string
Text string
Align string
PctWidth int
PctMargin int
MaxFontSize int
BorderSize int
}
func (e *EPUBImageProcessor) Cover16LevelOfGray(bounds image.Rectangle) draw.Image {
return image.NewPaletted(bounds, color.Palette{
color.Gray{0x00},
color.Gray{0x11},
color.Gray{0x22},
color.Gray{0x33},
color.Gray{0x44},
color.Gray{0x55},
color.Gray{0x66},
color.Gray{0x77},
color.Gray{0x88},
color.Gray{0x99},
color.Gray{0xAA},
color.Gray{0xBB},
color.Gray{0xCC},
color.Gray{0xDD},
color.Gray{0xEE},
color.Gray{0xFF},
})
}
// create a title page with the cover
func (e *EPUBImageProcessor) CoverTitleData(src image.Image, title string) (*epubzip.ZipImage, error) {
func (e *EPUBImageProcessor) CoverTitleData(o *CoverTitleDataOptions) (*epubzip.ZipImage, error) {
// Create a blur version of the cover
g := gift.New(epubimagefilters.CoverTitle(title))
dst := e.createImage(src, g.Bounds(src.Bounds()))
g.Draw(dst, src)
g := gift.New(epubimagefilters.CoverTitle(o.Text, o.Align, o.PctWidth, o.PctMargin, o.MaxFontSize, o.BorderSize))
var dst draw.Image
if o.Name == "cover" && e.Image.GrayScale {
dst = e.Cover16LevelOfGray(o.Src.Bounds())
} else {
dst = e.createImage(o.Src, g.Bounds(o.Src.Bounds()))
}
g.Draw(dst, o.Src)
return epubzip.CompressImage(
fmt.Sprintf("OEBPS/Images/title.%s", e.Image.Format),
fmt.Sprintf("OEBPS/Images/%s.%s", o.Name, e.Image.Format),
e.Image.Format,
dst,
e.Image.Quality,

View File

@ -124,9 +124,7 @@ func getMeta(o *ContentOptions) []tag {
metas = append(metas, tag{"meta", tagAttrs{"name": "primary-writing-mode", "content": "horizontal-lr"}, ""})
}
if o.Cover != nil {
metas = append(metas, tag{"meta", tagAttrs{"name": "cover", "content": o.Cover.ImgKey()}, ""})
}
metas = append(metas, tag{"meta", tagAttrs{"name": "cover", "content": "img_cover"}, ""})
if o.Total > 1 {
metas = append(
@ -160,16 +158,14 @@ func getManifest(o *ContentOptions) []tag {
{"item", tagAttrs{"id": "css", "href": "Text/style.css", "media-type": "text/css"}, ""},
{"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": "page_cover", "href": "Text/cover.xhtml", "media-type": "application/xhtml+xml"}, ""},
{"item", tagAttrs{"id": "img_cover", "href": fmt.Sprintf("Images/cover.%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, !o.ImageOptions.View.PortraitOnly && (img.DoublePage || (img.Part == 0 && img == lastImage)))
@ -249,10 +245,8 @@ func getSpinePortrait(o *ContentOptions) []tag {
// guide part of the content
func getGuide(o *ContentOptions) []tag {
guide := []tag{}
if o.Cover != nil {
guide = append(guide, tag{"reference", tagAttrs{"type": "cover", "title": "cover", "href": o.Cover.PagePath()}, ""})
return []tag{
{"reference", tagAttrs{"type": "cover", "title": "cover", "href": "Text/cover.xhtml"}, ""},
{"reference", tagAttrs{"type": "text", "title": "content", "href": o.Images[0].PagePath()}, ""},
}
guide = append(guide, tag{"reference", tagAttrs{"type": "text", "title": "content", "href": o.Images[0].PagePath()}, ""})
return guide
}