diff --git a/internal/epub/epub.go b/internal/epub/epub.go index 63b1097..ce13a96 100644 --- a/internal/epub/epub.go +++ b/internal/epub/epub.go @@ -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] diff --git a/internal/epub/imagefilters/epub_image_filters_cover_title.go b/internal/epub/imagefilters/epub_image_filters_cover_title.go index b1abd74..f69d9dd 100644 --- a/internal/epub/imagefilters/epub_image_filters_cover_title.go +++ b/internal/epub/imagefilters/epub_image_filters_cover_title.go @@ -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)) } diff --git a/internal/epub/imageprocessor/epub_image_processor.go b/internal/epub/imageprocessor/epub_image_processor.go index e8d3bd3..a895aa7 100644 --- a/internal/epub/imageprocessor/epub_image_processor.go +++ b/internal/epub/imageprocessor/epub_image_processor.go @@ -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, diff --git a/internal/epub/templates/epub_templates_content.go b/internal/epub/templates/epub_templates_content.go index 8a1d938..b851b62 100644 --- a/internal/epub/templates/epub_templates_content.go +++ b/internal/epub/templates/epub_templates_content.go @@ -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 }