From a7a29d6326ef9da16dfae3d6afb838d5a8552f7b Mon Sep 17 00:00:00 2001 From: celogeek <65178+celogeek@users.noreply.github.com> Date: Sun, 16 Apr 2023 11:45:00 +0200 Subject: [PATCH] rebuild toc and content handle landscape mode better handling double page with epub 3.3 center spread support improve support for apple book --- internal/epub/content.go | 197 ++++++++++++++++++++ internal/epub/core.go | 98 ++-------- internal/epub/image_processing.go | 94 ++++++---- internal/epub/templates.go | 10 +- internal/epub/templates/applebooks.xml.tmpl | 6 + internal/epub/templates/blank.xhtml.tmpl | 1 - internal/epub/templates/container.xml.tmpl | 9 +- internal/epub/templates/content.opf.tmpl | 57 ------ internal/epub/templates/nav.xhtml.tmpl | 16 -- internal/epub/templates/part.xhtml.tmpl | 4 +- internal/epub/templates/style.css.tmpl | 18 +- internal/epub/templates/toc.ncx.tmpl | 14 -- internal/epub/toc.go | 60 ++++-- internal/epub/tree.go | 17 ++ 14 files changed, 361 insertions(+), 240 deletions(-) create mode 100644 internal/epub/content.go create mode 100644 internal/epub/templates/applebooks.xml.tmpl delete mode 100644 internal/epub/templates/content.opf.tmpl delete mode 100644 internal/epub/templates/nav.xhtml.tmpl delete mode 100644 internal/epub/templates/toc.ncx.tmpl 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 @@ + + + + + + \ No newline at end of file diff --git a/internal/epub/templates/blank.xhtml.tmpl b/internal/epub/templates/blank.xhtml.tmpl index 0252f8b..4a81627 100644 --- a/internal/epub/templates/blank.xhtml.tmpl +++ b/internal/epub/templates/blank.xhtml.tmpl @@ -7,6 +7,5 @@ -

{{ if .Info.Manga }}←{{ else }}→{{ end }}

\ No newline at end of file diff --git a/internal/epub/templates/container.xml.tmpl b/internal/epub/templates/container.xml.tmpl index e1d3db9..c0eba46 100644 --- a/internal/epub/templates/container.xml.tmpl +++ b/internal/epub/templates/container.xml.tmpl @@ -1,6 +1,7 @@ - - - - + + + + \ No newline at end of file diff --git a/internal/epub/templates/content.opf.tmpl b/internal/epub/templates/content.opf.tmpl deleted file mode 100644 index 730190c..0000000 --- a/internal/epub/templates/content.opf.tmpl +++ /dev/null @@ -1,57 +0,0 @@ - - -{{ $info := .Info }} - - {{ .Title }} - en-US - urn:uuid:{{ $info.UID }} - {{ $info.Publisher }} - {{ $info.Publisher }} - {{ $info.UpdatedAt }} - {{ $info.Author }} - {{ $info.UpdatedAt }} - - - - pre-paginated - portrait - -{{ if eq $info.AddPanelView true }} - -{{ end }} -{{ if gt .Total 1 }} - - -{{ end }} - - - - - -{{ if eq $info.AddPanelView true }} - -{{ end }} - -{{ range .Images }} -{{ if eq .IsCover false }} - -{{ end }} -{{ end }} - -{{ range .Images }} - -{{ if eq .NeedSpace true }} - -{{ end }} -{{ end }} - - - -{{ range .Images }} -{{ if eq .NeedSpace true }} - -{{ end }} - -{{ end }} - - diff --git a/internal/epub/templates/nav.xhtml.tmpl b/internal/epub/templates/nav.xhtml.tmpl deleted file mode 100644 index 115bd66..0000000 --- a/internal/epub/templates/nav.xhtml.tmpl +++ /dev/null @@ -1,16 +0,0 @@ - - - - - {{ .Title }} - - - - - \ No newline at end of file diff --git a/internal/epub/templates/part.xhtml.tmpl b/internal/epub/templates/part.xhtml.tmpl index 15dfed0..c152015 100644 --- a/internal/epub/templates/part.xhtml.tmpl +++ b/internal/epub/templates/part.xhtml.tmpl @@ -7,9 +7,9 @@ -

{{ .Info.Title }}

+

{{ .Info.Title }}

{{ if gt .Total 1 }} -

Part {{ .Part }} / {{ .Total }}

+

Part {{ .Part }} / {{ .Total }}

{{ end }} \ No newline at end of file diff --git a/internal/epub/templates/style.css.tmpl b/internal/epub/templates/style.css.tmpl index 2bbe2ef..da8ed34 100644 --- a/internal/epub/templates/style.css.tmpl +++ b/internal/epub/templates/style.css.tmpl @@ -6,10 +6,8 @@ html { } body { - font-size: 16px; + font-size: 1em; text-align: center; - width: 100%; - height: 100%; } body, @@ -64,10 +62,22 @@ h3, h4, h5, h6 { - font-size: 150%; + -webkit-hyphens:none; font-weight: normal; } +h1 { + font-size: 200%; +} + +h2 { + font-size: 150%; +} + +h3 { + font-size: 120%; +} + sup { vertical-align: text-top; } diff --git a/internal/epub/templates/toc.ncx.tmpl b/internal/epub/templates/toc.ncx.tmpl deleted file mode 100644 index 42fec3f..0000000 --- a/internal/epub/templates/toc.ncx.tmpl +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - -{{ .Title }} - -{{ .Title }} - - \ No newline at end of file diff --git a/internal/epub/toc.go b/internal/epub/toc.go index 7bda2ef..d354457 100644 --- a/internal/epub/toc.go +++ b/internal/epub/toc.go @@ -1,22 +1,54 @@ package epub import ( - "encoding/xml" + "path/filepath" + "strings" + + "github.com/beevik/etree" ) -type TocTitle struct { - XMLName xml.Name `xml:"a"` - Value string `xml:",innerxml"` - Link string `xml:"href,attr"` -} +func (e *ePub) getToc(title string, images []*Image) string { + doc := etree.NewDocument() + doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`) + doc.CreateDirective("DOCTYPE html") -type TocChildren struct { - XMLName xml.Name `xml:"ol"` - Tags []*TocPart -} + html := doc.CreateElement("html") + html.CreateAttr("xmlns", "http://www.w3.org/1999/xhtml") + html.CreateAttr("xmlns:epub", "http://www.idpf.org/2007/ops") -type TocPart struct { - XMLName xml.Name `xml:"li"` - Title TocTitle - Children *TocChildren `xml:",omitempty"` + html.CreateElement("head").CreateElement("title").CreateText(title) + body := html.CreateElement("body") + nav := body.CreateElement("nav") + nav.CreateAttr("epub:type", "toc") + nav.CreateAttr("id", "toc") + nav.CreateElement("h2").CreateText(title) + + ol := etree.NewElement("ol") + paths := map[string]*etree.Element{".": ol} + 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 + } + t := paths[parentPath].CreateElement("li") + link := t.CreateElement("a") + link.CreateAttr("href", img.TextPath()) + link.CreateText(path) + paths[currentPath] = t + } + } + + if len(ol.ChildElements()) == 1 && e.StripFirstDirectoryFromToc { + ol = ol.ChildElements()[0] + } + if len(ol.ChildElements()) > 0 { + nav.AddChild(ol) + } + + doc.Indent(2) + r, _ := doc.WriteToString() + return r } diff --git a/internal/epub/tree.go b/internal/epub/tree.go index 7543985..cac65f0 100644 --- a/internal/epub/tree.go +++ b/internal/epub/tree.go @@ -51,3 +51,20 @@ func (n *Node) toString(indent string) string { } return r.String() } + +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("") +}