feat(backend): cover extraction for cbz and epub (opf cover-image/meta)
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user