feat(backend): cover extraction for cbz and epub (opf cover-image/meta)

This commit is contained in:
2026-09-05 00:23:02 +08:00
parent 589cb210bb
commit b02ca67d16
2 changed files with 200 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
package bookfile
import (
"archive/zip"
"encoding/xml"
"errors"
"io"
"net/url"
"path"
"strings"
)
func CBZCover(f io.ReaderAt, size int64) ([]byte, string, error) {
idx, err := PageIndex(f, size)
if err != nil {
return nil, "", err
}
if len(idx) == 0 {
return nil, "", errors.New("no page images")
}
img, err := ReadEntry(f, size, idx[0])
return img, strings.ToLower(path.Ext(idx[0])), err
}
func readZipEntry(zr *zip.Reader, name string) ([]byte, error) {
for _, zf := range zr.File {
if zf.Name == name {
rc, err := zf.Open()
if err != nil {
return nil, err
}
defer rc.Close()
return io.ReadAll(io.LimitReader(rc, 8<<20))
}
}
return nil, errors.New("entry not found: " + name)
}
type opfDoc struct {
XMLName xml.Name `xml:"package"`
Metadata struct {
Meta []struct {
Name string `xml:"name,attr"`
Content string `xml:"content,attr"`
} `xml:"meta"`
} `xml:"metadata"`
Manifest struct {
Items []struct {
ID string `xml:"id,attr"`
Href string `xml:"href,attr"`
Properties string `xml:"properties,attr"`
MediaType string `xml:"media-type,attr"`
} `xml:"item"`
} `xml:"manifest"`
}
func EPUBCover(f io.ReaderAt, size int64) ([]byte, string, error) {
zr, err := zip.NewReader(f, size)
if err != nil {
return nil, "", ErrNotZip
}
cont, err := readZipEntry(zr, "META-INF/container.xml")
if err != nil {
return nil, "", err
}
var root struct {
XMLName xml.Name `xml:"container"`
RootFiles []struct {
FullPath string `xml:"full-path,attr"`
} `xml:"rootfiles>rootfile"`
}
if err := xml.Unmarshal(cont, &root); err != nil || len(root.RootFiles) == 0 {
return nil, "", errors.New("bad container.xml")
}
opfPath := root.RootFiles[0].FullPath
opfData, err := readZipEntry(zr, opfPath)
if err != nil {
return nil, "", err
}
var doc opfDoc
if err := xml.Unmarshal(opfData, &doc); err != nil {
return nil, "", errors.New("bad opf")
}
metaCoverID := ""
for _, m := range doc.Metadata.Meta {
if m.Name == "cover" {
metaCoverID = m.Content
}
}
href := ""
for _, it := range doc.Manifest.Items {
if strings.Contains(it.Properties, "cover-image") {
href = it.Href
break
}
if metaCoverID != "" && it.ID == metaCoverID {
href = it.Href
break
}
}
if href == "" {
for _, it := range doc.Manifest.Items {
if strings.HasPrefix(it.MediaType, "image/") && strings.Contains(strings.ToLower(it.ID), "cover") {
href = it.Href
break
}
}
}
if href == "" {
return nil, "", errors.New("no cover found")
}
unescaped, e := url.PathUnescape(href)
if e == nil {
href = unescaped
}
name := path.Join(path.Dir(opfPath), href)
if name == "." {
name = href
}
img, err := readZipEntry(zr, name)
if err != nil {
return nil, "", err
}
return img, strings.ToLower(path.Ext(name)), nil
}