mirror of
https://github.com/readest/readest.git
synced 2026-10-19 02:06:57 +00:00
fix(import): pick the declared cover when <meta name="cover"> holds an href (#6635)
* fix(import): pick the declared cover when <meta name="cover"> holds an href Some EPUB2 producers (FB2 converters among them) write the cover image's href into `<meta name="cover" content>` instead of its manifest id. Both parsers only looked the value up as an id, missed, and fell through to their heuristics: the Rust import parser took the "first" image from a HashMap (an arbitrary one, so a back-matter illustration became the cover), and foliate-js took the guide's XHTML cover page and returned it as the cover blob. - Resolve the meta content as an href when no manifest id matches, in the Rust parser and in foliate-js (readest/foliate-js#123). - Keep the Rust parser's cover manifest in document order so every heuristic picks the same item as foliate-js's `manifest.find`, and the same EPUB always gets the same cover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(import): only accept an image for a <meta name="cover"> href match The href fallback could match the XHTML cover page (`content="cover.xhtml"`) and return its bytes as the cover. Restrict it to image items so such books fall through to the image heuristic as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(readme): drop the status column from the features table Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
12ed02db5e
commit
12bd7f34c3
4 changed files with 226 additions and 72 deletions
50
README.md
50
README.md
|
|
@ -44,37 +44,35 @@
|
|||
|
||||
## Features
|
||||
|
||||
<div align="left">✅ Implemented</div>
|
||||
|
||||
| **Feature** | **Description** | **Status** |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ---------- |
|
||||
| **Multi-Format Support** | Support EPUB, PDF, MOBI, KF8 (AZW3), FB2, CBZ, TXT, MD (Markdown) | ✅ |
|
||||
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
|
||||
| **Full-Text Search** | Search inside a book or across the current library shelf to find relevant sections. | ✅ |
|
||||
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience and use instant mode for quicker interactions. | ✅ |
|
||||
| **Dictionary/Wikipedia Lookup** | Look up words with built-in sources or import local packs, including Yomitan ZIP/RDICT, from Settings → Custom Dictionaries. | ✅ |
|
||||
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
|
||||
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
|
||||
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
|
||||
| **OPDS/Calibre Integration** | Integrate OPDS/Calibre to access online libraries and catalogs. | ✅ |
|
||||
| **Web Page Clipping** | Open websites, sign in, and clip pages with **From Web Browser**. **From Web Novel** can reuse your browser session to import selected chapters and their images. | ✅ |
|
||||
| **Translate with DeepL and Yandex** | From a single sentence to the entire book—translate instantly. | ✅ |
|
||||
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | ✅ |
|
||||
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ |
|
||||
| [**Read-Along Narration**][link-readalong] | Play embedded EPUB 3 Media Overlays with timed highlighting, or pair a reflowable EPUB locally with DRM-free MP3, M4A, or M4B narration. [Storyteller][link-storyteller] remains an option for generating phrase-aligned EPUBs. | ✅ |
|
||||
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
|
||||
| [**Sync with Koreader**][link-kosync-wiki] | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | ✅ |
|
||||
| **Accessibility** | Provides full keyboard navigation and support for screen readers such as VoiceOver, TalkBack, NVDA, and Orca. | ✅ |
|
||||
| **Visual & Focus Aids** | Reading ruler, paragraph-by-paragraph reading mode, and speed reading features. | ✅ |
|
||||
| **Feature** | **Description** |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Multi-Format Support** | Support EPUB, PDF, MOBI, KF8 (AZW3), FB2, CBZ, TXT, HTML, MD (Markdown) |
|
||||
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. |
|
||||
| **Full-Text Search** | Search inside a book or across the current library shelf to find relevant sections. |
|
||||
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience and use instant mode for quicker interactions. |
|
||||
| **Dictionary/Wikipedia Lookup** | Look up words with built-in sources or import local packs, including Yomitan ZIP/RDICT, from Settings → Custom Dictionaries. |
|
||||
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. |
|
||||
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. |
|
||||
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. |
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. |
|
||||
| **OPDS/Calibre Integration** | Integrate OPDS/Calibre to access online libraries and catalogs. |
|
||||
| **Web Page Clipping** | Open websites, sign in, and clip pages with **From Web Browser**. **From Web Novel** can reuse your browser session to import selected chapters and their images. |
|
||||
| **Translate with DeepL and Yandex** | From a single sentence to the entire book—translate instantly. |
|
||||
| **Audiobook Support** | Extend functionality to play and manage audiobooks. |
|
||||
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. |
|
||||
| [**Read-Along Narration**][link-readalong] | Play embedded EPUB 3 Media Overlays with timed highlighting, or pair a reflowable EPUB locally with DRM-free MP3, M4A, or M4B narration. [Storyteller][link-storyteller] remains an option for generating phrase-aligned EPUBs. |
|
||||
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. |
|
||||
| [**Sync with Koreader**][link-kosync-wiki] | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. |
|
||||
| **Accessibility** | Provides full keyboard navigation and support for screen readers such as VoiceOver, TalkBack, NVDA, and Orca. |
|
||||
| **Visual & Focus Aids** | Reading ruler, paragraph-by-paragraph reading mode, and speed reading features. |
|
||||
|
||||
## Planned Features
|
||||
|
||||
<div align="left">🛠 Building</div>
|
||||
<div align="left">🔄 Planned</div>
|
||||
<div align="right">🛠 Building | 🔄 Planned</div>
|
||||
|
||||
| **Feature** | **Description** | **Priority** |
|
||||
| **Feature** | **Description** | |
|
||||
| ------------------------------- | -------------------------------------------------------------------------- | ------------ |
|
||||
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
|
||||
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🛠 |
|
||||
|
|
|
|||
|
|
@ -565,9 +565,11 @@ struct ManifestItem {
|
|||
/// `parse_opf_cover_inputs` and consumed by `resolve_cover_path`.
|
||||
#[derive(Debug, Default)]
|
||||
struct OpfCoverInputs {
|
||||
/// id → manifest item. Needed for the `<meta name="cover" content="id">`
|
||||
/// legacy shorthand and for the `properties="cover-image"` lookup.
|
||||
manifest: std::collections::HashMap<String, ManifestItem>,
|
||||
/// (id, manifest item) in document order, so every cover heuristic picks
|
||||
/// the same item foliate-js's `manifest.find` would. Needed for the
|
||||
/// `<meta name="cover" content="id">` legacy shorthand and for the
|
||||
/// `properties="cover-image"` lookup.
|
||||
manifest: Vec<(String, ManifestItem)>,
|
||||
/// Value of the legacy `<meta name="cover" content="...">` element, if
|
||||
/// present. EPUB2 publishers used this to point at the cover manifest
|
||||
/// item by id.
|
||||
|
|
@ -591,8 +593,7 @@ fn parse_opf_cover_inputs(bytes: &[u8]) -> Result<OpfCoverInputs, String> {
|
|||
let mut in_manifest = false;
|
||||
|
||||
let process_manifest_item =
|
||||
|attrs: &[(Vec<u8>, Vec<u8>)],
|
||||
manifest: &mut std::collections::HashMap<String, ManifestItem>| {
|
||||
|attrs: &[(Vec<u8>, Vec<u8>)], manifest: &mut Vec<(String, ManifestItem)>| {
|
||||
let mut id = String::new();
|
||||
let mut item = ManifestItem::default();
|
||||
for (k, v) in attrs {
|
||||
|
|
@ -605,7 +606,7 @@ fn parse_opf_cover_inputs(bytes: &[u8]) -> Result<OpfCoverInputs, String> {
|
|||
}
|
||||
}
|
||||
if !id.is_empty() {
|
||||
manifest.insert(id, item);
|
||||
manifest.push((id, item));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -671,13 +672,22 @@ fn parse_opf_cover_inputs(bytes: &[u8]) -> Result<OpfCoverInputs, String> {
|
|||
// ---------------------------------------------------------------------------
|
||||
// Cover resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
/// Manifest item by id. On a (malformed) duplicate id the last one wins, as
|
||||
/// in foliate-js's `manifestById` map.
|
||||
fn manifest_item<'a>(manifest: &'a [(String, ManifestItem)], id: &str) -> Option<&'a ManifestItem> {
|
||||
manifest
|
||||
.iter()
|
||||
.rfind(|(i, _)| i == id)
|
||||
.map(|(_, item)| item)
|
||||
}
|
||||
|
||||
fn resolve_cover_path(
|
||||
manifest: &std::collections::HashMap<String, ManifestItem>,
|
||||
manifest: &[(String, ManifestItem)],
|
||||
cover_id: &Option<String>,
|
||||
opf_path: &str,
|
||||
) -> Option<String> {
|
||||
// 1) properties="cover-image" (EPUB3)
|
||||
for item in manifest.values() {
|
||||
for (_, item) in manifest {
|
||||
if item
|
||||
.properties
|
||||
.split_ascii_whitespace()
|
||||
|
|
@ -686,11 +696,18 @@ fn resolve_cover_path(
|
|||
return Some(resolve_relative(opf_path, &item.href));
|
||||
}
|
||||
}
|
||||
// 2) <meta name="cover" content="<id>"/> -> manifest[id] (EPUB2)
|
||||
// 2) <meta name="cover" content="<id>"/> -> manifest[id] (EPUB2). Some
|
||||
// producers put the image's href in `content` instead of its id.
|
||||
if let Some(id) = cover_id {
|
||||
if let Some(item) = manifest.get(id) {
|
||||
if let Some(item) = manifest_item(manifest, id) {
|
||||
return Some(resolve_relative(opf_path, &item.href));
|
||||
}
|
||||
let path = resolve_relative(opf_path, id);
|
||||
if manifest.iter().any(|(_, item)| {
|
||||
item.media_type.starts_with("image/") && resolve_relative(opf_path, &item.href) == path
|
||||
}) {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
// 3) Heuristic: image item whose id/href contains "cover".
|
||||
//
|
||||
|
|
@ -704,12 +721,9 @@ fn resolve_cover_path(
|
|||
// pass 2 (fallback): if pass 1 found nothing (e.g. the EPUB only ships
|
||||
// SVG covers), allow SVG so we don't lose covers on
|
||||
// odd-but-valid EPUBs. `nav` is still excluded.
|
||||
fn pick(
|
||||
manifest: &std::collections::HashMap<String, ManifestItem>,
|
||||
allow_svg: bool,
|
||||
) -> Option<&ManifestItem> {
|
||||
fn pick(manifest: &[(String, ManifestItem)], allow_svg: bool) -> Option<&ManifestItem> {
|
||||
let mut best: Option<&ManifestItem> = None;
|
||||
for item in manifest.values() {
|
||||
for (_, item) in manifest {
|
||||
if !item.media_type.starts_with("image/") {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -874,7 +888,6 @@ mod tests {
|
|||
use crate::parser_common::COVER_MAX_LONG_EDGE;
|
||||
use image::GenericImageView;
|
||||
use md5::{Digest, Md5};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
|
|
@ -901,11 +914,11 @@ mod tests {
|
|||
let inputs = parse_opf_cover_inputs(xml).expect("opf parses");
|
||||
assert_eq!(inputs.cover_id.as_deref(), Some("cover-img"));
|
||||
assert_eq!(inputs.manifest.len(), 3);
|
||||
let cover = inputs.manifest.get("cover-img").expect("cover entry");
|
||||
let cover = manifest_item(&inputs.manifest, "cover-img").expect("cover entry");
|
||||
assert_eq!(cover.href, "images/cover.jpg");
|
||||
assert_eq!(cover.media_type, "image/jpeg");
|
||||
assert!(cover.properties.is_empty());
|
||||
let nav = inputs.manifest.get("nav").expect("nav entry");
|
||||
let nav = manifest_item(&inputs.manifest, "nav").expect("nav entry");
|
||||
assert_eq!(nav.properties, "nav");
|
||||
}
|
||||
|
||||
|
|
@ -954,7 +967,7 @@ mod tests {
|
|||
let inputs = parse_opf_cover_inputs(xml).expect("opf parses");
|
||||
assert_eq!(inputs.cover_id.as_deref(), Some("cover-image"));
|
||||
assert_eq!(inputs.manifest.len(), 2);
|
||||
let cover = inputs.manifest.get("cover-image").expect("cover entry");
|
||||
let cover = manifest_item(&inputs.manifest, "cover-image").expect("cover entry");
|
||||
assert_eq!(cover.href, "images/cover.jpg");
|
||||
assert_eq!(cover.media_type, "image/jpeg");
|
||||
assert_eq!(cover.properties, "cover-image");
|
||||
|
|
@ -1001,73 +1014,139 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn cover_resolution_prefers_epub3_properties() {
|
||||
let mut manifest = HashMap::new();
|
||||
manifest.insert(
|
||||
let mut manifest = Vec::new();
|
||||
manifest.push((
|
||||
"img1".into(),
|
||||
ManifestItem {
|
||||
href: "img/foo.jpg".into(),
|
||||
media_type: "image/jpeg".into(),
|
||||
properties: "cover-image".into(),
|
||||
},
|
||||
);
|
||||
manifest.insert(
|
||||
));
|
||||
manifest.push((
|
||||
"img2".into(),
|
||||
ManifestItem {
|
||||
href: "img/bar.jpg".into(),
|
||||
media_type: "image/jpeg".into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
);
|
||||
));
|
||||
let p = resolve_cover_path(&manifest, &None, "OEBPS/content.opf").unwrap();
|
||||
assert_eq!(p, "OEBPS/img/foo.jpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cover_resolution_falls_back_to_meta_cover() {
|
||||
let mut manifest = HashMap::new();
|
||||
manifest.insert(
|
||||
let mut manifest = Vec::new();
|
||||
manifest.push((
|
||||
"cov".into(),
|
||||
ManifestItem {
|
||||
href: "images/c.png".into(),
|
||||
media_type: "image/png".into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
);
|
||||
manifest.insert(
|
||||
));
|
||||
manifest.push((
|
||||
"other".into(),
|
||||
ManifestItem {
|
||||
href: "images/o.png".into(),
|
||||
media_type: "image/png".into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
);
|
||||
));
|
||||
let p = resolve_cover_path(&manifest, &Some("cov".into()), "content.opf").unwrap();
|
||||
assert_eq!(p, "images/c.png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cover_resolution_accepts_meta_cover_href() {
|
||||
// FB2 converters write the image href, not its manifest id, into
|
||||
// `<meta name="cover" content>`.
|
||||
let mut manifest = Vec::new();
|
||||
for (id, href) in [
|
||||
("pic_1.jpg", "images/pic_1.jpg"),
|
||||
("back", "images/backcover.jpg"),
|
||||
] {
|
||||
manifest.push((
|
||||
id.into(),
|
||||
ManifestItem {
|
||||
href: href.into(),
|
||||
media_type: "image/jpeg".into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
));
|
||||
}
|
||||
let p = resolve_cover_path(
|
||||
&manifest,
|
||||
&Some("images/pic_1.jpg".into()),
|
||||
"OEBPS/Content.opf",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(p, "OEBPS/images/pic_1.jpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cover_resolution_ignores_meta_cover_href_to_non_image() {
|
||||
// An href naming the XHTML cover page must not win over the image.
|
||||
let mut manifest = Vec::new();
|
||||
for (id, href, media_type) in [
|
||||
("cover", "cover.xhtml", "application/xhtml+xml"),
|
||||
("img", "images/cover.jpg", "image/jpeg"),
|
||||
] {
|
||||
manifest.push((
|
||||
id.into(),
|
||||
ManifestItem {
|
||||
href: href.into(),
|
||||
media_type: media_type.into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
));
|
||||
}
|
||||
let p = resolve_cover_path(&manifest, &Some("cover.xhtml".into()), "OEBPS/content.opf")
|
||||
.unwrap();
|
||||
assert_eq!(p, "OEBPS/images/cover.jpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cover_heuristic_falls_back_to_first_image_in_manifest_order() {
|
||||
let mut manifest = Vec::new();
|
||||
for n in 1..=20 {
|
||||
manifest.push((
|
||||
format!("pic_{n}"),
|
||||
ManifestItem {
|
||||
href: format!("images/pic_{n}.jpg"),
|
||||
media_type: "image/jpeg".into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
));
|
||||
}
|
||||
let p = resolve_cover_path(&manifest, &None, "OEBPS/content.opf").unwrap();
|
||||
assert_eq!(p, "OEBPS/images/pic_1.jpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cover_heuristic_skips_svg_when_raster_available() {
|
||||
// Without `properties=cover-image` and without `<meta name=cover>`, an
|
||||
// SVG sitting next to a JPEG must NOT be picked: SVGs in EPUBs are
|
||||
// typically the cover *page* (a wrapper xhtml/svg), not the actual
|
||||
// cover image.
|
||||
let mut manifest = HashMap::new();
|
||||
manifest.insert(
|
||||
let mut manifest = Vec::new();
|
||||
manifest.push((
|
||||
"cov-svg".into(),
|
||||
ManifestItem {
|
||||
href: "images/cover.svg".into(),
|
||||
media_type: "image/svg+xml".into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
);
|
||||
manifest.insert(
|
||||
));
|
||||
manifest.push((
|
||||
"cov-jpg".into(),
|
||||
ManifestItem {
|
||||
href: "images/cover.jpg".into(),
|
||||
media_type: "image/jpeg".into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
);
|
||||
));
|
||||
let p = resolve_cover_path(&manifest, &None, "OEBPS/content.opf").unwrap();
|
||||
assert_eq!(p, "OEBPS/images/cover.jpg");
|
||||
}
|
||||
|
|
@ -1076,23 +1155,23 @@ mod tests {
|
|||
fn cover_heuristic_falls_back_to_svg_when_only_svg_present() {
|
||||
// Edge-case EPUBs that ship only an SVG cover must still resolve a
|
||||
// cover path — pass-2 of the heuristic re-runs with SVG allowed.
|
||||
let mut manifest = HashMap::new();
|
||||
manifest.insert(
|
||||
let mut manifest = Vec::new();
|
||||
manifest.push((
|
||||
"cov-svg".into(),
|
||||
ManifestItem {
|
||||
href: "images/cover.svg".into(),
|
||||
media_type: "image/svg+xml".into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
);
|
||||
manifest.insert(
|
||||
));
|
||||
manifest.push((
|
||||
"ch1".into(),
|
||||
ManifestItem {
|
||||
href: "text/ch1.xhtml".into(),
|
||||
media_type: "application/xhtml+xml".into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
);
|
||||
));
|
||||
let p = resolve_cover_path(&manifest, &None, "OEBPS/content.opf").unwrap();
|
||||
assert_eq!(p, "OEBPS/images/cover.svg");
|
||||
}
|
||||
|
|
@ -1102,23 +1181,23 @@ mod tests {
|
|||
// Defensive: even though `nav` belongs on xhtml per spec, properties
|
||||
// is a token list and we never want to pick a nav-tagged item as a
|
||||
// cover. The non-nav image must win.
|
||||
let mut manifest = HashMap::new();
|
||||
manifest.insert(
|
||||
let mut manifest = Vec::new();
|
||||
manifest.push((
|
||||
"weird-nav".into(),
|
||||
ManifestItem {
|
||||
href: "images/cover.jpg".into(),
|
||||
media_type: "image/jpeg".into(),
|
||||
properties: "nav".into(),
|
||||
},
|
||||
);
|
||||
manifest.insert(
|
||||
));
|
||||
manifest.push((
|
||||
"real".into(),
|
||||
ManifestItem {
|
||||
href: "images/other.jpg".into(),
|
||||
media_type: "image/jpeg".into(),
|
||||
properties: String::new(),
|
||||
},
|
||||
);
|
||||
));
|
||||
let p = resolve_cover_path(&manifest, &None, "OEBPS/content.opf").unwrap();
|
||||
assert_eq!(p, "OEBPS/images/other.jpg");
|
||||
}
|
||||
|
|
@ -1376,7 +1455,7 @@ mod tests {
|
|||
);
|
||||
let inputs = parse_opf_cover_inputs(&bytes).expect("opf parses through BOM");
|
||||
assert_eq!(inputs.cover_id.as_deref(), Some("cv"));
|
||||
assert!(inputs.manifest.contains_key("cv"));
|
||||
assert!(manifest_item(&inputs.manifest, "cv").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { DocumentLoader } from '@/libs/document';
|
||||
|
||||
const imageBytes = (tag: number) => new Uint8Array([0xff, 0xd8, 0xff, 0xe0, tag]);
|
||||
|
||||
/**
|
||||
* Some EPUB2 producers (FB2 converters among them) write the cover image's
|
||||
* href into `<meta name="cover" content>` instead of its manifest id, and
|
||||
* point the guide's `cover` reference at the XHTML cover page.
|
||||
*/
|
||||
const createMetaCoverHrefEpub = async () => {
|
||||
const { ZipWriter, BlobWriter, TextReader, Uint8ArrayReader } = await import('@zip.js/zip.js');
|
||||
const writer = new ZipWriter(new BlobWriter('application/epub+zip'));
|
||||
|
||||
await writer.add('mimetype', new TextReader('application/epub+zip'), { level: 0 });
|
||||
await writer.add(
|
||||
'META-INF/container.xml',
|
||||
new TextReader(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||
<rootfiles>
|
||||
<rootfile full-path="OEBPS/Content.opf" media-type="application/oebps-package+xml"/>
|
||||
</rootfiles>
|
||||
</container>`),
|
||||
);
|
||||
await writer.add(
|
||||
'OEBPS/Content.opf',
|
||||
new TextReader(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<package version="2.0" unique-identifier="BookID" xmlns="http://www.idpf.org/2007/opf">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<dc:identifier id="BookID">meta-cover-href</dc:identifier>
|
||||
<dc:title>test</dc:title>
|
||||
<dc:language>en</dc:language>
|
||||
<meta name="cover" content="images/pic_1.jpg" />
|
||||
</metadata>
|
||||
<manifest>
|
||||
<item id="pic_1.jpg" href="images/pic_1.jpg" media-type="image/jpeg" />
|
||||
<item id="pic_2.jpg" href="images/pic_2.jpg" media-type="image/jpeg" />
|
||||
<item id="pic_3.jpg" href="images/pic_3.jpg" media-type="image/jpeg" />
|
||||
<item id="cover" href="cover.xhtml" media-type="application/xhtml+xml" />
|
||||
</manifest>
|
||||
<spine>
|
||||
<itemref idref="cover" />
|
||||
</spine>
|
||||
<guide>
|
||||
<reference type="cover" title="cover" href="cover.xhtml" />
|
||||
</guide>
|
||||
</package>`),
|
||||
);
|
||||
await writer.add(
|
||||
'OEBPS/cover.xhtml',
|
||||
new TextReader(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>Cover</title></head>
|
||||
<body><img alt="Cover" src="images/pic_1.jpg" /></body>
|
||||
</html>`),
|
||||
);
|
||||
for (const n of [1, 2, 3]) {
|
||||
await writer.add(`OEBPS/images/pic_${n}.jpg`, new Uint8ArrayReader(imageBytes(n)));
|
||||
}
|
||||
|
||||
const blob = await writer.close();
|
||||
return new File([await blob.arrayBuffer()], 'meta-cover-href.epub', {
|
||||
type: 'application/epub+zip',
|
||||
});
|
||||
};
|
||||
|
||||
describe('EPUB2 <meta name="cover"> holding an href', () => {
|
||||
it('resolves the cover image by href when no manifest id matches', async () => {
|
||||
const { book } = await new DocumentLoader(await createMetaCoverHrefEpub()).open();
|
||||
const cover = await book.getCover();
|
||||
|
||||
expect(cover).not.toBeNull();
|
||||
expect(cover!.type).toBe('image/jpeg');
|
||||
expect(new Uint8Array(await cover!.arrayBuffer())).toEqual(imageBytes(1));
|
||||
});
|
||||
});
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 3e8d6fdf1e8c05ebedebffe1a8666bb4439cce3a
|
||||
Subproject commit a1913acfcd479af6eda12e8557a76bcb0e35d062
|
||||
Loading…
Add table
Add a link
Reference in a new issue