diff --git a/internal/epub/epub.go b/internal/epub/epub.go index b49edcf..7cff3e5 100644 --- a/internal/epub/epub.go +++ b/internal/epub/epub.go @@ -14,6 +14,7 @@ import ( epubimageprocessing "github.com/celogeek/go-comic-converter/v2/internal/epub/image_processing" epubprogress "github.com/celogeek/go-comic-converter/v2/internal/epub/progress" epubtemplates "github.com/celogeek/go-comic-converter/v2/internal/epub/templates" + epubtree "github.com/celogeek/go-comic-converter/v2/internal/epub/tree" epubzip "github.com/celogeek/go-comic-converter/v2/internal/epub/zip" "github.com/gofrs/uuid" ) @@ -192,6 +193,23 @@ func (e *ePub) getParts() ([]*epubPart, error) { return parts, nil } +func (e *ePub) getTree(images []*epubimage.Image, skip_files bool) string { + t := epubtree.New() + 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 @@ -247,8 +265,19 @@ func (e *ePub) Write() error { content := []zipContent{ {"META-INF/container.xml", epubtemplates.Container}, {"META-INF/com.apple.ibooks.display-options.xml", epubtemplates.AppleBooks}, - {"OEBPS/content.opf", e.getContent(title, part, i+1, totalParts).String()}, - {"OEBPS/toc.xhtml", e.getToc(title, part.Images)}, + {"OEBPS/content.opf", epubtemplates.Content(&epubtemplates.ContentOptions{ + Title: title, + UID: e.UID, + Author: e.Author, + Publisher: e.Publisher, + UpdatedAt: e.UpdatedAt, + ImageOptions: e.Image, + Cover: part.Cover, + Images: part.Images, + Current: i + 1, + Total: totalParts, + })}, + {"OEBPS/toc.xhtml", epubtemplates.Toc(title, e.StripFirstDirectoryFromToc, part.Images)}, {"OEBPS/Text/style.css", e.render(epubtemplates.Style, map[string]any{ "PageWidth": e.Image.ViewWidth, "PageHeight": e.Image.ViewHeight, diff --git a/internal/epub/epub_content.go b/internal/epub/epub_content.go deleted file mode 100644 index 9cbc44c..0000000 --- a/internal/epub/epub_content.go +++ /dev/null @@ -1,202 +0,0 @@ -package epub - -import ( - "fmt" - - "github.com/beevik/etree" - epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image" -) - -type Content struct { - doc *etree.Document -} - -func (c *Content) String() string { - c.doc.Indent(2) - r, _ := c.doc.WriteToString() - return r -} - -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": "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.Image.ViewWidth, e.Image.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.Image.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 *epubimage.Image) Tag { - return Tag{"item", TagAttrs{"id": img.Key("img"), "href": img.ImgPath(), "media-type": "image/jpeg"}, ""} - } - hTag := func(img *epubimage.Image) Tag { - return Tag{"item", TagAttrs{"id": img.Key("page"), "href": img.TextPath(), "media-type": "application/xhtml+xml"}, ""} - } - sTag := func(img *epubimage.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"}, ""}, - {"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"}, ""}, - } - - if e.Image.HasCover || currentPart > 1 { - 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 { - isOnTheRight := !e.Image.Manga - getSpread := func(doublePageNoBlank bool) string { - isOnTheRight = !isOnTheRight - if doublePageNoBlank { - // Center the double page then start back to comic mode (mange/normal) - isOnTheRight = !e.Image.Manga - return "rendition:page-spread-center" - } - if isOnTheRight { - return "rendition:page-spread-right" - } else { - return "rendition:page-spread-left" - } - } - - spine := []Tag{ - {"itemref", TagAttrs{"idref": "page_title", "properties": getSpread(true)}, ""}, - } - for _, img := range part.Images { - spine = append(spine, Tag{ - "itemref", - TagAttrs{"idref": img.Key("page"), "properties": getSpread(img.DoublePage && e.Image.NoBlankPage)}, - "", - }) - if img.DoublePage && isOnTheRight && !e.Image.NoBlankPage { - spine = append(spine, Tag{ - "itemref", - TagAttrs{"idref": img.SpaceKey("page"), "properties": getSpread(false)}, - "", - }) - } - } - if e.Image.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/#") - - 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.Image.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, - } -} diff --git a/internal/epub/epub_tree.go b/internal/epub/epub_tree.go deleted file mode 100644 index 015ef78..0000000 --- a/internal/epub/epub_tree.go +++ /dev/null @@ -1,25 +0,0 @@ -package epub - -import ( - "path/filepath" - - epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image" - epubtree "github.com/celogeek/go-comic-converter/v2/internal/epub/tree" -) - -func (e *ePub) getTree(images []*epubimage.Image, skip_files bool) string { - t := epubtree.New() - 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("") -} diff --git a/internal/epub/templates/epub_templates_content.go b/internal/epub/templates/epub_templates_content.go new file mode 100644 index 0000000..d1b513c --- /dev/null +++ b/internal/epub/templates/epub_templates_content.go @@ -0,0 +1,206 @@ +package epubtemplates + +import ( + "fmt" + + "github.com/beevik/etree" + epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image" +) + +type ContentOptions struct { + Title string + UID string + Author string + Publisher string + UpdatedAt string + ImageOptions *epubimage.Options + Cover *epubimage.Image + Images []*epubimage.Image + Current int + Total int +} + +type tagAttrs map[string]string + +type tag struct { + name string + attrs tagAttrs + value string +} + +func Content(o *ContentOptions) string { + 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/#") + + addToElement := func(elm *etree.Element, meth func(o *ContentOptions) []tag) { + for _, p := range meth(o) { + 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, getMeta) + + manifest := pkg.CreateElement("manifest") + addToElement(manifest, getManifest) + + spine := pkg.CreateElement("spine") + if o.ImageOptions.Manga { + spine.CreateAttr("page-progression-direction", "rtl") + } else { + spine.CreateAttr("page-progression-direction", "ltr") + } + addToElement(spine, getSpine) + + guide := pkg.CreateElement("guide") + addToElement(guide, getGuide) + + doc.Indent(2) + r, _ := doc.WriteToString() + + return r +} + +func getMeta(o *ContentOptions) []tag { + metas := []tag{ + {"meta", tagAttrs{"property": "dcterms:modified"}, o.UpdatedAt}, + {"meta", tagAttrs{"property": "rendition:layout"}, "pre-paginated"}, + {"meta", tagAttrs{"property": "rendition:spread"}, "auto"}, + {"meta", tagAttrs{"property": "rendition:orientation"}, "auto"}, + {"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", o.ImageOptions.ViewWidth, o.ImageOptions.ViewHeight)}, ""}, + {"dc:title", tagAttrs{}, o.Title}, + {"dc:identifier", tagAttrs{"id": "ean"}, fmt.Sprintf("urn:uuid:%s", o.UID)}, + {"dc:language", tagAttrs{}, "en"}, + {"dc:creator", tagAttrs{}, o.Author}, + {"dc:publisher", tagAttrs{}, o.Publisher}, + {"dc:contributor", tagAttrs{}, "Go Comic Convertor"}, + {"dc:date", tagAttrs{}, o.UpdatedAt}, + } + + if o.ImageOptions.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 o.Cover != nil { + metas = append(metas, tag{"meta", tagAttrs{"name": "cover", "content": o.Cover.Key("img")}, ""}) + } + + if o.Total > 1 { + metas = append( + metas, + tag{"meta", tagAttrs{"name": "calibre:series", "content": o.Title}, ""}, + tag{"meta", tagAttrs{"name": "calibre:series_index", "content": fmt.Sprint(o.Current)}, ""}, + ) + } + + return metas +} + +func getManifest(o *ContentOptions) []tag { + itag := func(img *epubimage.Image) tag { + return tag{"item", tagAttrs{"id": img.Key("img"), "href": img.ImgPath(), "media-type": "image/jpeg"}, ""} + } + htag := func(img *epubimage.Image) tag { + return tag{"item", tagAttrs{"id": img.Key("page"), "href": img.TextPath(), "media-type": "application/xhtml+xml"}, ""} + } + stag := func(img *epubimage.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"}, ""}, + {"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"}, ""}, + } + + if o.ImageOptions.HasCover || o.Current > 1 { + items = append(items, itag(o.Cover), htag(o.Cover)) + } + + for _, img := range o.Images { + if img.Part == 1 { + items = append(items, stag(img)) + } + items = append(items, itag(img), htag(img)) + } + items = append(items, stag(o.Images[len(o.Images)-1])) + + return items +} + +func getSpine(o *ContentOptions) []tag { + isOnTheRight := !o.ImageOptions.Manga + getSpread := func(doublePageNoBlank bool) string { + isOnTheRight = !isOnTheRight + if doublePageNoBlank { + // Center the double page then start back to comic mode (mange/normal) + isOnTheRight = !o.ImageOptions.Manga + return "rendition:page-spread-center" + } + if isOnTheRight { + return "rendition:page-spread-right" + } else { + return "rendition:page-spread-left" + } + } + + spine := []tag{ + {"itemref", tagAttrs{"idref": "page_title", "properties": getSpread(true)}, ""}, + } + for _, img := range o.Images { + spine = append(spine, tag{ + "itemref", + tagAttrs{"idref": img.Key("page"), "properties": getSpread(img.DoublePage && o.ImageOptions.NoBlankPage)}, + "", + }) + if img.DoublePage && isOnTheRight && !o.ImageOptions.NoBlankPage { + spine = append(spine, tag{ + "itemref", + tagAttrs{"idref": img.SpaceKey("page"), "properties": getSpread(false)}, + "", + }) + } + } + if o.ImageOptions.Manga == isOnTheRight { + spine = append(spine, tag{ + "itemref", + tagAttrs{"idref": o.Images[len(o.Images)-1].SpaceKey("page"), "properties": getSpread(false)}, + "", + }) + } + + return spine +} + +func getGuide(o *ContentOptions) []tag { + guide := []tag{} + if o.Cover != nil { + guide = append(guide, tag{"reference", tagAttrs{"type": "cover", "title": "cover", "href": o.Cover.TextPath()}, ""}) + } + guide = append(guide, tag{"reference", tagAttrs{"type": "text", "title": "content", "href": o.Images[0].TextPath()}, ""}) + return guide +} diff --git a/internal/epub/epub_toc.go b/internal/epub/templates/epub_toc.go similarity index 90% rename from internal/epub/epub_toc.go rename to internal/epub/templates/epub_toc.go index bdf8607..7fc2842 100644 --- a/internal/epub/epub_toc.go +++ b/internal/epub/templates/epub_toc.go @@ -1,4 +1,4 @@ -package epub +package epubtemplates import ( "path/filepath" @@ -8,7 +8,7 @@ import ( epubimage "github.com/celogeek/go-comic-converter/v2/internal/epub/image" ) -func (e *ePub) getToc(title string, images []*epubimage.Image) string { +func Toc(title string, stripFirstDirectoryFromToc bool, images []*epubimage.Image) string { doc := etree.NewDocument() doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`) doc.CreateDirective("DOCTYPE html") @@ -42,7 +42,7 @@ func (e *ePub) getToc(title string, images []*epubimage.Image) string { } } - if len(ol.ChildElements()) == 1 && e.StripFirstDirectoryFromToc { + if len(ol.ChildElements()) == 1 && stripFirstDirectoryFromToc { ol = ol.FindElement("/li/ol") }