@@ -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
8182type 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
95102type 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+
116132type 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+
406520func chapterParts (chapterRefs , spineRefs []epubChapterRef , chapterIndex int ) []epubChapterPart {
407521 ref := chapterRefs [chapterIndex ]
408522 direct := func (endFragment string ) []epubChapterPart {
0 commit comments