diff --git a/internal/epub/content.go b/internal/epub/content.go
new file mode 100644
index 0000000..5310c7b
--- /dev/null
+++ b/internal/epub/content.go
@@ -0,0 +1,197 @@
+package epub
+
+import (
+ "fmt"
+
+ "github.com/beevik/etree"
+)
+
+type Content struct {
+ doc *etree.Document
+}
+
+type TagAttrs map[string]string
+
+type Tag struct {
+ name string
+ attrs TagAttrs
+ value string
+}
+
+func (e *ePub) getMeta(title string, part *epubPart, currentPart, totalPart int) []Tag {
+ metas := []Tag{
+ {"meta", TagAttrs{"property": "dcterms:modified"}, e.UpdatedAt},
+ {"meta", TagAttrs{"property": "rendition:layout"}, "pre-paginated"},
+ {"meta", TagAttrs{"property": "rendition:spread"}, "auto"},
+ {"meta", TagAttrs{"property": "rendition:orientation"}, "auto"},
+ {"meta", TagAttrs{"property": "ibooks:specified-fonts"}, "true"},
+ {"meta", TagAttrs{"property": "schema:accessMode"}, "visual"},
+ {"meta", TagAttrs{"property": "schema:accessModeSufficient"}, "visual"},
+ {"meta", TagAttrs{"property": "schema:accessibilityHazard"}, "noFlashingHazard"},
+ {"meta", TagAttrs{"property": "schema:accessibilityHazard"}, "noMotionSimulationHazard"},
+ {"meta", TagAttrs{"property": "schema:accessibilityHazard"}, "noSoundHazard"},
+ {"meta", TagAttrs{"name": "book-type", "content": "comic"}, ""},
+ {"opf:meta", TagAttrs{"name": "fixed-layout", "content": "true"}, ""},
+ {"opf:meta", TagAttrs{"name": "original-resolution", "content": fmt.Sprintf("%dx%d", e.ViewWidth, e.ViewHeight)}, ""},
+ {"dc:title", TagAttrs{}, title},
+ {"dc:identifier", TagAttrs{"id": "ean"}, fmt.Sprintf("urn:uuid:%s", e.UID)},
+ {"dc:language", TagAttrs{}, "en"},
+ {"dc:creator", TagAttrs{}, e.Author},
+ {"dc:publisher", TagAttrs{}, e.Publisher},
+ {"dc:contributor", TagAttrs{}, "Go Comic Convertor"},
+ {"dc:date", TagAttrs{}, e.UpdatedAt},
+ }
+
+ if e.Manga {
+ metas = append(metas, Tag{"meta", TagAttrs{"name": "primary-writing-mode", "content": "horizontal-rl"}, ""})
+ } else {
+ metas = append(metas, Tag{"meta", TagAttrs{"name": "primary-writing-mode", "content": "horizontal-lr"}, ""})
+ }
+
+ if part.Cover != nil {
+ metas = append(metas, Tag{"meta", TagAttrs{"name": "cover", "content": part.Cover.Key("img")}, ""})
+ }
+
+ if totalPart > 1 {
+ metas = append(
+ metas,
+ Tag{"meta", TagAttrs{"name": "calibre:series", "content": e.Title}, ""},
+ Tag{"meta", TagAttrs{"name": "calibre:series_index", "content": fmt.Sprint(currentPart)}, ""},
+ )
+ }
+
+ return metas
+}
+
+func (e *ePub) getManifest(title string, part *epubPart, currentPart, totalPart int) []Tag {
+ iTag := func(img *Image) Tag {
+ return Tag{"item", TagAttrs{"id": img.Key("img"), "href": img.ImgPath(), "media-type": "image/jpeg"}, ""}
+ }
+ hTag := func(img *Image) Tag {
+ return Tag{"item", TagAttrs{"id": img.Key("page"), "href": img.TextPath(), "media-type": "application/xhtml+xml"}, ""}
+ }
+ sTag := func(img *Image) Tag {
+ return Tag{"item", TagAttrs{"id": img.SpaceKey("page"), "href": img.SpacePath(), "media-type": "application/xhtml+xml"}, ""}
+ }
+ items := []Tag{
+ {"item", TagAttrs{"id": "toc", "href": "toc.xhtml", "properties": "nav", "media-type": "application/xhtml+xml"}, ""},
+ {"item", TagAttrs{"id": "css", "href": "Text/style.css", "media-type": "text/css"}, ""},
+ }
+
+ if part.Cover != nil {
+ items = append(items, iTag(part.Cover), hTag(part.Cover))
+ }
+
+ for _, img := range part.Images {
+ if img.Part == 1 {
+ items = append(items, sTag(img))
+ }
+ items = append(items, iTag(img), hTag(img))
+ }
+ items = append(items, sTag(part.Images[len(part.Images)-1]))
+
+ return items
+}
+
+func (e *ePub) getSpine(title string, part *epubPart, currentPart, totalPart int) []Tag {
+ spine := []Tag{}
+ isOnTheRight := !e.Manga
+ getSpread := func(doublePageNoBlank bool) string {
+ isOnTheRight = !isOnTheRight
+ if doublePageNoBlank {
+ // Center the double page then start back to comic mode (mange/normal)
+ isOnTheRight = !e.Manga
+ return "rendition:page-spread-center"
+ }
+ if isOnTheRight {
+ return "rendition:page-spread-right"
+ } else {
+ return "rendition:page-spread-left"
+ }
+ }
+ for _, img := range part.Images {
+ spine = append(spine, Tag{
+ "itemref",
+ TagAttrs{"idref": img.Key("page"), "properties": getSpread(img.DoublePage && e.NoBlankPage)},
+ "",
+ })
+ if img.DoublePage && isOnTheRight && !e.NoBlankPage {
+ spine = append(spine, Tag{
+ "itemref",
+ TagAttrs{"idref": img.SpaceKey("page"), "properties": getSpread(false)},
+ "",
+ })
+ }
+ }
+ if e.Manga == isOnTheRight {
+ spine = append(spine, Tag{
+ "itemref",
+ TagAttrs{"idref": part.Images[len(part.Images)-1].SpaceKey("page"), "properties": getSpread(false)},
+ "",
+ })
+ }
+
+ return spine
+}
+
+func (e *ePub) getGuide(title string, part *epubPart, currentPart, totalPart int) []Tag {
+ guide := []Tag{}
+ if part.Cover != nil {
+ guide = append(guide, Tag{"reference", TagAttrs{"type": "cover", "title": "cover", "href": part.Cover.TextPath()}, ""})
+ }
+ guide = append(guide, Tag{"reference", TagAttrs{"type": "text", "title": "content", "href": part.Images[0].TextPath()}, ""})
+ return guide
+}
+
+func (e *ePub) getContent(title string, part *epubPart, currentPart, totalPart int) *Content {
+ doc := etree.NewDocument()
+ doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
+
+ pkg := doc.CreateElement("package")
+ pkg.CreateAttr("xmlns", "http://www.idpf.org/2007/opf")
+ pkg.CreateAttr("unique-identifier", "ean")
+ pkg.CreateAttr("version", "3.0")
+ pkg.CreateAttr("prefix", "rendition: http://www.idpf.org/vocab/rendition/# ibooks: http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/")
+
+ addToElement := func(elm *etree.Element, meth func(title string, part *epubPart, currentPart, totalPart int) []Tag) {
+ for _, p := range meth(title, part, currentPart, totalPart) {
+ meta := elm.CreateElement(p.name)
+ for k, v := range p.attrs {
+ meta.CreateAttr(k, v)
+ }
+ meta.SortAttrs()
+ if p.value != "" {
+ meta.CreateText(p.value)
+ }
+ }
+ }
+
+ metadata := pkg.CreateElement("metadata")
+ metadata.CreateAttr("xmlns:dc", "http://purl.org/dc/elements/1.1/")
+ metadata.CreateAttr("xmlns:opf", "http://www.idpf.org/2007/opf")
+ addToElement(metadata, e.getMeta)
+
+ manifest := pkg.CreateElement("manifest")
+ addToElement(manifest, e.getManifest)
+
+ spine := pkg.CreateElement("spine")
+ if e.Manga {
+ spine.CreateAttr("page-progression-direction", "rtl")
+ } else {
+ spine.CreateAttr("page-progression-direction", "ltr")
+ }
+ addToElement(spine, e.getSpine)
+
+ guide := pkg.CreateElement("guide")
+ addToElement(guide, e.getGuide)
+
+ return &Content{
+ doc,
+ }
+}
+
+func (c *Content) String() string {
+ c.doc.Indent(2)
+ r, _ := c.doc.WriteToString()
+ return r
+}
diff --git a/internal/epub/core.go b/internal/epub/core.go
index d8e9eae..21dd5a4 100644
--- a/internal/epub/core.go
+++ b/internal/epub/core.go
@@ -1,7 +1,6 @@
package epub
import (
- "encoding/xml"
"fmt"
"os"
"path/filepath"
@@ -134,7 +133,6 @@ func (e *ePub) getParts() ([]*epubPart, error) {
currentSize := baseSize
currentImages := make([]*Image, 0)
part := 1
- imgIsOnRightSide := false
for _, img := range images {
imgSize := img.Data.CompressedSize() + xhtmlSize
@@ -144,14 +142,11 @@ func (e *ePub) getParts() ([]*epubPart, error) {
Images: currentImages,
})
part += 1
- imgIsOnRightSide = false
currentSize = baseSize
currentImages = make([]*Image, 0)
}
currentSize += imgSize
- img.NeedSpace = img.Part == 1 && imgIsOnRightSide
currentImages = append(currentImages, img)
- imgIsOnRightSide = !imgIsOnRightSide
}
if len(currentImages) > 0 {
parts = append(parts, &epubPart{
@@ -163,59 +158,6 @@ func (e *ePub) getParts() ([]*epubPart, error) {
return parts, nil
}
-func (e *ePub) getToc(images []*Image) *TocChildren {
- paths := map[string]*TocPart{
- ".": {},
- }
- for _, img := range images {
- currentPath := "."
- for _, path := range strings.Split(img.Path, string(filepath.Separator)) {
- parentPath := currentPath
- currentPath = filepath.Join(currentPath, path)
- if _, ok := paths[currentPath]; ok {
- continue
- }
- part := &TocPart{
- Title: TocTitle{
- Value: path,
- Link: fmt.Sprintf("Text/%d_p%d.xhtml", img.Id, img.Part),
- },
- }
- paths[currentPath] = part
- if paths[parentPath].Children == nil {
- paths[parentPath].Children = &TocChildren{}
- }
- paths[parentPath].Children.Tags = append(paths[parentPath].Children.Tags, part)
- }
- }
-
- children := paths["."].Children
-
- if children != nil && e.StripFirstDirectoryFromToc && len(children.Tags) == 1 {
- children = children.Tags[0].Children
- }
-
- return children
-
-}
-
-func (e *ePub) getTree(images []*Image, skip_files bool) string {
- t := NewTree()
- for _, img := range images {
- if skip_files {
- t.Add(img.Path)
- } else {
- t.Add(filepath.Join(img.Path, img.Name))
- }
- }
- c := t.Root()
- if skip_files && e.StripFirstDirectoryFromToc && len(c.Children) == 1 {
- c = c.Children[0]
- }
-
- return c.toString("")
-}
-
func (e *ePub) Write() error {
type zipContent struct {
Name string
@@ -263,33 +205,11 @@ func (e *ePub) Write() error {
title = fmt.Sprintf("%s [%d/%d]", title, i+1, totalParts)
}
- tocChildren := e.getToc(part.Images)
- toc := []byte{}
- if tocChildren != nil {
- toc, err = xml.MarshalIndent(tocChildren.Tags, " ", " ")
- if err != nil {
- return err
- }
- }
-
content := []zipContent{
{"META-INF/container.xml", containerTmpl},
- {"OEBPS/content.opf", e.render(contentTmpl, map[string]any{
- "Info": e,
- "Cover": part.Cover,
- "Images": part.Images,
- "Title": title,
- "Part": i + 1,
- "Total": totalParts,
- })},
- {"OEBPS/toc.ncx", e.render(tocTmpl, map[string]any{
- "Info": e,
- "Title": title,
- })},
- {"OEBPS/nav.xhtml", e.render(navTmpl, map[string]any{
- "Title": title,
- "TOC": string(toc),
- })},
+ {"META-INF/com.apple.ibooks.display-options.xml", appleBooksTmpl},
+ {"OEBPS/content.opf", e.getContent(title, part, i+1, totalParts).String()},
+ {"OEBPS/toc.xhtml", e.getToc(title, part.Images)},
{"OEBPS/Text/style.css", styleTmpl},
{"OEBPS/Text/part.xhtml", e.render(partTmpl, map[string]any{
"Info": e,
@@ -310,7 +230,17 @@ func (e *ePub) Write() error {
// Cover exist or part > 1
// If no cover, part 2 and more will include the image as a cover
if e.HasCover || i > 0 {
- wz.WriteImage(part.Cover.Data)
+ if err := wz.WriteFile(fmt.Sprintf("OEBPS/%s", part.Cover.TextPath()), e.render(textTmpl, map[string]any{
+ "Info": e,
+ "Image": part.Cover,
+ "Manga": e.Manga,
+ "Top": fmt.Sprintf("%d", (e.ViewHeight-part.Cover.Height)/2),
+ })); err != nil {
+ return err
+ }
+ if err := wz.WriteImage(part.Cover.Data); err != nil {
+ return err
+ }
}
for i, img := range part.Images {
diff --git a/internal/epub/image_processing.go b/internal/epub/image_processing.go
index f1e345a..f9f6018 100644
--- a/internal/epub/image_processing.go
+++ b/internal/epub/image_processing.go
@@ -26,15 +26,35 @@ import (
)
type Image struct {
- Id int
- Part int
- Data *ImageData
- Width int
- Height int
- IsCover bool
- NeedSpace bool
- Path string
- Name string
+ Id int
+ Part int
+ Data *ImageData
+ Width int
+ Height int
+ IsCover bool
+ DoublePage bool
+ Path string
+ Name string
+}
+
+func (i *Image) Key(prefix string) string {
+ return fmt.Sprintf("%s_%d_p%d", prefix, i.Id, i.Part)
+}
+
+func (i *Image) SpaceKey(prefix string) string {
+ return fmt.Sprintf("%s_%d_sp", prefix, i.Id)
+}
+
+func (i *Image) TextPath() string {
+ return fmt.Sprintf("Text/%d_p%d.xhtml", i.Id, i.Part)
+}
+
+func (i *Image) ImgPath() string {
+ return fmt.Sprintf("Images/%d_p%d.jpg", i.Id, i.Part)
+}
+
+func (i *Image) SpacePath() string {
+ return fmt.Sprintf("Text/%d_sp.xhtml", i.Id)
}
type imageTask struct {
@@ -130,15 +150,15 @@ func (e *ePub) LoadImages() ([]*Image, error) {
for img := range imageInput {
img.Reader.Close()
images = append(images, &Image{
- Id: img.Id,
- Part: 0,
- Data: nil,
- Width: 0,
- Height: 0,
- IsCover: false,
- NeedSpace: false, // NeedSpace reajust during parts computation
- Path: img.Path,
- Name: img.Name,
+ Id: img.Id,
+ Part: 0,
+ Data: nil,
+ Width: 0,
+ Height: 0,
+ IsCover: false,
+ DoublePage: false,
+ Path: img.Path,
+ Name: img.Name,
})
}
@@ -180,15 +200,17 @@ func (e *ePub) LoadImages() ([]*Image, error) {
g.Draw(dst, src)
imageOutput <- &Image{
- Id: img.Id,
- Part: 0,
- Data: newImageData(img.Id, 0, dst, e.ImageOptions.Quality),
- Width: dst.Bounds().Dx(),
- Height: dst.Bounds().Dy(),
- IsCover: img.Id == 0,
- NeedSpace: false,
- Path: img.Path,
- Name: img.Name,
+ Id: img.Id,
+ Part: 0,
+ Data: newImageData(img.Id, 0, dst, e.ImageOptions.Quality),
+ Width: dst.Bounds().Dx(),
+ Height: dst.Bounds().Dy(),
+ IsCover: img.Id == 0,
+ DoublePage: src.Bounds().Dx() > src.Bounds().Dy() &&
+ src.Bounds().Dx() > e.ImageOptions.ViewHeight &&
+ src.Bounds().Dy() > e.ImageOptions.ViewWidth,
+ Path: img.Path,
+ Name: img.Name,
}
// Auto split double page
@@ -205,15 +227,15 @@ func (e *ePub) LoadImages() ([]*Image, error) {
dst := image.NewGray(g.Bounds(src.Bounds()))
g.Draw(dst, src)
imageOutput <- &Image{
- Id: img.Id,
- Part: part,
- Data: newImageData(img.Id, part, dst, e.ImageOptions.Quality),
- Width: dst.Bounds().Dx(),
- Height: dst.Bounds().Dy(),
- IsCover: false,
- NeedSpace: false, // NeedSpace reajust during parts computation
- Path: img.Path,
- Name: img.Name,
+ Id: img.Id,
+ Part: part,
+ Data: newImageData(img.Id, part, dst, e.ImageOptions.Quality),
+ Width: dst.Bounds().Dx(),
+ Height: dst.Bounds().Dy(),
+ IsCover: false,
+ DoublePage: false,
+ Path: img.Path,
+ Name: img.Name,
}
}
}
diff --git a/internal/epub/templates.go b/internal/epub/templates.go
index f0dea83..f885161 100644
--- a/internal/epub/templates.go
+++ b/internal/epub/templates.go
@@ -5,14 +5,8 @@ import _ "embed"
//go:embed "templates/container.xml.tmpl"
var containerTmpl string
-//go:embed "templates/content.opf.tmpl"
-var contentTmpl string
-
-//go:embed "templates/toc.ncx.tmpl"
-var tocTmpl string
-
-//go:embed "templates/nav.xhtml.tmpl"
-var navTmpl string
+//go:embed "templates/applebooks.xml.tmpl"
+var appleBooksTmpl string
//go:embed "templates/style.css.tmpl"
var styleTmpl string
diff --git a/internal/epub/templates/applebooks.xml.tmpl b/internal/epub/templates/applebooks.xml.tmpl
new file mode 100644
index 0000000..aad0f8c
--- /dev/null
+++ b/internal/epub/templates/applebooks.xml.tmpl
@@ -0,0 +1,6 @@
+
+