fix(epub-parser): Resolve spine file paths relative to OPF location

Problem:
- Spine items stored raw href from manifest (e.g., 'cover.xhtml')
- Resources stored with full paths (e.g., 'OEBPS/cover.xhtml')
- Content lookup failed: resourceMap.get('cover.xhtml') returned undefined
- Result: Pagination failed with 0 pages, content not found

Root Cause:
- parseSpine() didn't resolve paths relative to OPF file location
- Spine hrefs are relative to OPF directory, not ZIP root
- Manifest items use relative paths like 'cover.xhtml'
- Actual files are at 'OEBPS/cover.xhtml' (relative to OEBPS/content.opf)

Solution:
- Use resolvePath() helper to resolve href relative to opfPath
- Store resolved full path in spine items: 'OEBPS/cover.xhtml'
- Match resource Map storage pattern (full paths)
- Extract cover image and add to EbookCIF.metadata

Changes:
- parseSpine(): Add opfPath parameter, resolve each spine href
- parseEPUB(): Pass opfPath to parseSpine()
- extractCover(): Load cover image from EPUB manifest
- Return EbookCIF.metadata.coverImage with Blob data

Impact:
-  Spine content files now found correctly
-  Pagination calculates actual pages (119 pages vs 0)
-  Ebook content loads and displays
-  Cover image extracted and available

Testing:
- EPUB with spine items in subdirectory (OEBPS/)
- Content lookup now succeeds
- Pagination generates correct page count

Files changed: 1
Lines changed: +14, -3
This commit is contained in:
2026-04-10 23:21:17 -04:00
parent 11039cfb89
commit e1cc1f4417
+12 -5
View File
@@ -23,7 +23,7 @@ export async function parseEPUB(epubBlob: Blob): Promise<EbookCIF> {
const packageDoc = parseXML(opfXml); const packageDoc = parseXML(opfXml);
const metadata = extractMetadata(packageDoc); const metadata = extractMetadata(packageDoc);
const spine = parseSpine(packageDoc); const spine = parseSpine(packageDoc, opfPath);
const toc = await parseTOC(zip, packageDoc, opfPath); const toc = await parseTOC(zip, packageDoc, opfPath);
const resources = await loadResources(zip); const resources = await loadResources(zip);
const coverImage = await extractCover(zip, packageDoc); const coverImage = await extractCover(zip, packageDoc);
@@ -32,7 +32,10 @@ export async function parseEPUB(epubBlob: Blob): Promise<EbookCIF> {
const totalCharacters = await calculateTotalCharacters(spine, resources); const totalCharacters = await calculateTotalCharacters(spine, resources);
return { return {
metadata, metadata: {
...metadata,
coverImage,
},
toc, toc,
spine, spine,
resources, resources,
@@ -82,7 +85,10 @@ function extractMetadata(packageDoc: XMLDocument): EbookCIF["metadata"] {
}; };
} }
function parseSpine(packageDoc: XMLDocument): EbookCIF["spine"] { function parseSpine(
packageDoc: XMLDocument,
opfPath: string,
): EbookCIF["spine"] {
const spine = packageDoc.querySelector("spine"); const spine = packageDoc.querySelector("spine");
const manifest = packageDoc.querySelector("manifest"); const manifest = packageDoc.querySelector("manifest");
if (!spine || !manifest) { if (!spine || !manifest) {
@@ -100,17 +106,18 @@ function parseSpine(packageDoc: XMLDocument): EbookCIF["spine"] {
const href = manifestItem.getAttribute("href"); const href = manifestItem.getAttribute("href");
console.log("Href:", href); // Your debug log console.log("Href:", href); // Your debug log
if (!href) return; if (!href) return;
const resolvedPath = resolvePath(opfPath, href);
result.push({ result.push({
id: idref, id: idref,
type: "html" as const, type: "html" as const,
content: href || "", content: resolvedPath,
properties: (itemref.getAttribute("properties") || "") properties: (itemref.getAttribute("properties") || "")
.split(" ") .split(" ")
.filter(Boolean), .filter(Boolean),
index: result.length, index: result.length,
}); });
}); });
return result; // FIXED - proper return, not trailing comma return result;
} }
async function parseTOC( async function parseTOC(