Skip to content

Commit 66e8108

Browse files
committed
修复 EPUB 2 封面解析
1 parent 4c946fd commit 66e8108

2 files changed

Lines changed: 192 additions & 9 deletions

File tree

internal/archive/epub_reader.go

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type opfPackage struct {
7676
Metadata opfMetadata `xml:"metadata"`
7777
Manifest opfManifest `xml:"manifest"`
7878
Spine opfSpine `xml:"spine"`
79+
Guide opfGuide `xml:"guide"`
7980
}
8081

8182
type opfMetadata struct {
@@ -90,6 +91,12 @@ type opfMetadata struct {
9091
Value string `xml:",chardata"`
9192
Scheme string `xml:"scheme,attr"`
9293
} `xml:"identifier"`
94+
Meta []opfMeta `xml:"meta"`
95+
}
96+
97+
type opfMeta struct {
98+
Name string `xml:"name,attr"`
99+
Content string `xml:"content,attr"`
93100
}
94101

95102
type opfManifest struct {
@@ -113,6 +120,15 @@ type opfItemRef struct {
113120
Linear string `xml:"linear,attr"`
114121
}
115122

123+
type opfGuide struct {
124+
References []opfGuideReference `xml:"reference"`
125+
}
126+
127+
type opfGuideReference struct {
128+
Type string `xml:"type,attr"`
129+
Href string `xml:"href,attr"`
130+
}
131+
116132
type ncxDocument struct {
117133
NavMap ncxNavMap `xml:"navMap"`
118134
}
@@ -198,16 +214,8 @@ func (r *epubReader) parseEpub() error {
198214
// Track all resources
199215
href, _ := resolveEpubHref(opfDir, item.Href)
200216
r.resources[href] = true
201-
202-
// Find cover image
203-
if item.Props == "cover-image" ||
204-
strings.Contains(strings.ToLower(item.ID), "cover") &&
205-
strings.HasPrefix(item.MediaType, "image/") {
206-
if r.coverPath == "" {
207-
r.coverPath = href
208-
}
209-
}
210217
}
218+
r.coverPath = r.findPackageCover(pkg, opfDir, manifestMap)
211219

212220
// Step 3: Build spine order. It remains the content index and fallback order,
213221
// but EPUB nav/NCX is preferred for the visible chapter list.
@@ -403,6 +411,112 @@ func resolveEpubHref(baseDir, href string) (string, string) {
403411
return filePart, fragment
404412
}
405413

414+
func (r *epubReader) findPackageCover(pkg opfPackage, opfDir string, manifestMap map[string]opfItem) string {
415+
for _, item := range pkg.Manifest.Items {
416+
if hasSpaceSeparatedToken(item.Props, "cover-image") && strings.HasPrefix(item.MediaType, "image/") {
417+
href, _ := resolveEpubHref(opfDir, item.Href)
418+
return href
419+
}
420+
}
421+
422+
for _, meta := range pkg.Metadata.Meta {
423+
if !strings.EqualFold(strings.TrimSpace(meta.Name), "cover") {
424+
continue
425+
}
426+
id := strings.TrimPrefix(strings.TrimSpace(meta.Content), "#")
427+
if item, ok := manifestMap[id]; ok {
428+
if coverPath := r.coverPathFromManifestItem(item, opfDir); coverPath != "" {
429+
return coverPath
430+
}
431+
}
432+
}
433+
434+
for _, ref := range pkg.Guide.References {
435+
if !strings.EqualFold(strings.TrimSpace(ref.Type), "cover") {
436+
continue
437+
}
438+
guidePath, _ := resolveEpubHref(opfDir, ref.Href)
439+
if guidePath == "" {
440+
continue
441+
}
442+
for _, item := range pkg.Manifest.Items {
443+
itemPath, _ := resolveEpubHref(opfDir, item.Href)
444+
if itemPath == guidePath && strings.HasPrefix(item.MediaType, "image/") {
445+
return guidePath
446+
}
447+
}
448+
if coverPath := r.coverPathFromDocument(guidePath); coverPath != "" {
449+
return coverPath
450+
}
451+
}
452+
453+
for _, item := range pkg.Manifest.Items {
454+
if strings.Contains(strings.ToLower(item.ID), "cover") && strings.HasPrefix(item.MediaType, "image/") {
455+
href, _ := resolveEpubHref(opfDir, item.Href)
456+
return href
457+
}
458+
}
459+
return ""
460+
}
461+
462+
func (r *epubReader) coverPathFromManifestItem(item opfItem, opfDir string) string {
463+
itemPath, _ := resolveEpubHref(opfDir, item.Href)
464+
if itemPath == "" {
465+
return ""
466+
}
467+
if strings.HasPrefix(item.MediaType, "image/") {
468+
return itemPath
469+
}
470+
if strings.HasPrefix(item.MediaType, "application/xhtml") || strings.HasPrefix(item.MediaType, "text/html") {
471+
return r.coverPathFromDocument(itemPath)
472+
}
473+
return ""
474+
}
475+
476+
func (r *epubReader) coverPathFromDocument(documentPath string) string {
477+
data, err := r.readZipFile(documentPath)
478+
if err != nil {
479+
return ""
480+
}
481+
482+
tokenizer := html.NewTokenizer(strings.NewReader(string(data)))
483+
for {
484+
tokenType := tokenizer.Next()
485+
if tokenType == html.ErrorToken {
486+
return ""
487+
}
488+
if tokenType != html.StartTagToken && tokenType != html.SelfClosingTagToken {
489+
continue
490+
}
491+
token := tokenizer.Token()
492+
if !strings.EqualFold(token.Data, "img") && !strings.EqualFold(token.Data, "image") {
493+
continue
494+
}
495+
for _, attr := range token.Attr {
496+
key := strings.ToLower(attr.Key)
497+
if key != "src" && key != "href" && key != "xlink:href" &&
498+
!(strings.EqualFold(attr.Namespace, "xlink") && key == "href") {
499+
continue
500+
}
501+
imageHref := strings.TrimSpace(attr.Val)
502+
if imageHref == "" || strings.HasPrefix(imageHref, "data:") || strings.HasPrefix(imageHref, "//") {
503+
continue
504+
}
505+
if parsed, err := url.Parse(imageHref); err == nil && parsed.IsAbs() {
506+
continue
507+
}
508+
baseDir := path.Dir(documentPath)
509+
if baseDir == "." {
510+
baseDir = ""
511+
}
512+
imagePath, _ := resolveEpubHref(baseDir, imageHref)
513+
if imagePath != "" && r.epubFileExists(imagePath) {
514+
return imagePath
515+
}
516+
}
517+
}
518+
}
519+
406520
func chapterParts(chapterRefs, spineRefs []epubChapterRef, chapterIndex int) []epubChapterPart {
407521
ref := chapterRefs[chapterIndex]
408522
direct := func(endFragment string) []epubChapterPart {

internal/archive/epub_reader_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,75 @@ func TestEpubReaderSpineFallbackKeepsDocumentsSeparate(t *testing.T) {
207207
}
208208
}
209209

210+
func TestEpubReaderExtractsEPUB2MetadataCover(t *testing.T) {
211+
cover := "jpeg-cover-data"
212+
fp := writeTestEpub(t, "epub2-meta-cover.epub", map[string]string{
213+
"mimetype": "application/epub+zip",
214+
"META-INF/container.xml": testContainerXML,
215+
"OEBPS/content.opf": `<?xml version="1.0" encoding="UTF-8"?>
216+
<package xmlns="http://www.idpf.org/2007/opf" version="2.0">
217+
<metadata><meta name="cover" content="mid4221"/></metadata>
218+
<manifest>
219+
<item id="mid4221" href="Images/mid4221.jpg" media-type="image/jpeg"/>
220+
<item id="chapter" href="Text/chapter.xhtml" media-type="application/xhtml+xml"/>
221+
</manifest>
222+
<spine><itemref idref="chapter"/></spine>
223+
</package>`,
224+
"OEBPS/Images/mid4221.jpg": cover,
225+
"OEBPS/Text/chapter.xhtml": testXHTML("Chapter", "body"),
226+
})
227+
228+
reader, err := NewReader(fp)
229+
if err != nil {
230+
t.Fatalf("NewReader() error = %v", err)
231+
}
232+
defer reader.Close()
233+
234+
got, err := GetEpubCoverImage(reader)
235+
if err != nil {
236+
t.Fatalf("GetEpubCoverImage() error = %v", err)
237+
}
238+
if string(got) != cover {
239+
t.Fatalf("GetEpubCoverImage() = %q, want %q", got, cover)
240+
}
241+
}
242+
243+
func TestEpubReaderExtractsEPUB2GuideSVGCover(t *testing.T) {
244+
cover := "guide-cover-data"
245+
fp := writeTestEpub(t, "epub2-guide-cover.epub", map[string]string{
246+
"mimetype": "application/epub+zip",
247+
"META-INF/container.xml": testContainerXML,
248+
"OEBPS/content.opf": `<?xml version="1.0" encoding="UTF-8"?>
249+
<package xmlns="http://www.idpf.org/2007/opf" version="2.0">
250+
<metadata><title>Guide cover</title></metadata>
251+
<manifest>
252+
<item id="image42" href="Images/image42.jpg" media-type="image/jpeg"/>
253+
<item id="coverpage" href="Text/cover.xhtml" media-type="application/xhtml+xml"/>
254+
<item id="chapter" href="Text/chapter.xhtml" media-type="application/xhtml+xml"/>
255+
</manifest>
256+
<spine><itemref idref="coverpage"/><itemref idref="chapter"/></spine>
257+
<guide><reference type="cover" href="Text/cover.xhtml"/></guide>
258+
</package>`,
259+
"OEBPS/Images/image42.jpg": cover,
260+
"OEBPS/Text/cover.xhtml": `<html xmlns="http://www.w3.org/1999/xhtml"><body><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><image xlink:href="../Images/image42.jpg"/></svg></body></html>`,
261+
"OEBPS/Text/chapter.xhtml": testXHTML("Chapter", "body"),
262+
})
263+
264+
reader, err := NewReader(fp)
265+
if err != nil {
266+
t.Fatalf("NewReader() error = %v", err)
267+
}
268+
defer reader.Close()
269+
270+
got, err := GetEpubCoverImage(reader)
271+
if err != nil {
272+
t.Fatalf("GetEpubCoverImage() error = %v", err)
273+
}
274+
if string(got) != cover {
275+
t.Fatalf("GetEpubCoverImage() = %q, want %q", got, cover)
276+
}
277+
}
278+
210279
const testContainerXML = `<?xml version="1.0" encoding="UTF-8"?><container><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>`
211280

212281
func testOPF(chapters []string, withNCX bool) string {

0 commit comments

Comments
 (0)