mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
208 lines
6.9 KiB
JavaScript
208 lines
6.9 KiB
JavaScript
const _defaultPdfjsPath = (path) =>
|
|
new URL(`./vendor/pdfjs/${path}`, import.meta.url).toString();
|
|
|
|
export const config = {
|
|
pdfjsPath: _defaultPdfjsPath,
|
|
};
|
|
|
|
import "./vendor/pdfjs/pdf.mjs";
|
|
const pdfjsLib = globalThis.pdfjsLib;
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = _defaultPdfjsPath("pdf.worker.mjs");
|
|
|
|
const fetchText = async (url) => await (await fetch(url)).text();
|
|
|
|
const textLayerBuilderCSS = await fetchText(
|
|
_defaultPdfjsPath("text_layer_builder.css"),
|
|
);
|
|
|
|
const annotationLayerBuilderCSS = await fetchText(
|
|
_defaultPdfjsPath("annotation_layer_builder.css"),
|
|
);
|
|
|
|
const render = async (page, doc, zoom) => {
|
|
const scale = zoom * devicePixelRatio;
|
|
doc.documentElement.style.transform = `scale(${1 / devicePixelRatio})`;
|
|
doc.documentElement.style.transformOrigin = "top left";
|
|
doc.documentElement.style.setProperty("--scale-factor", scale);
|
|
const viewport = page.getViewport({ scale });
|
|
|
|
// the canvas must be in the `PDFDocument`'s `ownerDocument`
|
|
// (`globalThis.document` by default); that's where the fonts are loaded
|
|
const canvas = document.createElement("canvas");
|
|
canvas.height = viewport.height;
|
|
canvas.width = viewport.width;
|
|
const canvasContext = canvas.getContext("2d");
|
|
await page.render({ canvasContext, viewport }).promise;
|
|
doc.querySelector("#canvas").replaceChildren(doc.adoptNode(canvas));
|
|
|
|
const container = doc.querySelector(".textLayer");
|
|
const textLayer = new pdfjsLib.TextLayer({
|
|
textContentSource: await page.streamTextContent(),
|
|
container,
|
|
viewport,
|
|
});
|
|
await textLayer.render();
|
|
|
|
// hide "offscreen" canvases appended to docuemnt when rendering text layer
|
|
// https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/pdf_viewer.css#L51-L58
|
|
for (const canvas of document.querySelectorAll(".hiddenCanvasElement"))
|
|
Object.assign(canvas.style, {
|
|
position: "absolute",
|
|
top: "0",
|
|
left: "0",
|
|
width: "0",
|
|
height: "0",
|
|
display: "none",
|
|
});
|
|
|
|
// fix text selection
|
|
// https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/text_layer_builder.js#L105-L107
|
|
const endOfContent = document.createElement("div");
|
|
endOfContent.className = "endOfContent";
|
|
container.append(endOfContent);
|
|
// TODO: this only works in Firefox; see https://github.com/mozilla/pdf.js/pull/17923
|
|
container.onpointerdown = () => container.classList.add("selecting");
|
|
container.onpointerup = () => container.classList.remove("selecting");
|
|
|
|
const div = doc.querySelector(".annotationLayer");
|
|
const linkService = {
|
|
goToDestination: () => {},
|
|
getDestinationHash: (dest) => JSON.stringify(dest),
|
|
addLinkAttributes: (link, url) => (link.href = url),
|
|
};
|
|
await new pdfjsLib.AnnotationLayer({
|
|
page,
|
|
viewport,
|
|
div,
|
|
linkService,
|
|
}).render({ annotations: await page.getAnnotations() });
|
|
};
|
|
|
|
const renderPage = async (page, getImageBlob) => {
|
|
const viewport = page.getViewport({ scale: 1 });
|
|
if (getImageBlob) {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.height = viewport.height;
|
|
canvas.width = viewport.width;
|
|
const canvasContext = canvas.getContext("2d");
|
|
await page.render({ canvasContext, viewport }).promise;
|
|
return new Promise((resolve) => canvas.toBlob(resolve));
|
|
}
|
|
const src = URL.createObjectURL(
|
|
new Blob(
|
|
[
|
|
`
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=${viewport.width}, height=${viewport.height}">
|
|
<style>
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
/*
|
|
https://github.com/mozilla/pdf.js/commit/bd05b255fabfc313b194bfe9a17ccded4d90fb5a
|
|
*/
|
|
:root {
|
|
--user-unit: 1;
|
|
--total-scale-factor: calc(var(--scale-factor) * var(--user-unit));
|
|
--scale-round-x: 1px;
|
|
--scale-round-y: 1px;
|
|
}
|
|
${textLayerBuilderCSS}
|
|
${annotationLayerBuilderCSS}
|
|
</style>
|
|
<div id="canvas"></div>
|
|
<div class="textLayer"></div>
|
|
<div class="annotationLayer"></div>
|
|
`,
|
|
],
|
|
{ type: "text/html" },
|
|
),
|
|
);
|
|
const onZoom = ({ doc, scale }) => render(page, doc, scale);
|
|
return { src, onZoom };
|
|
};
|
|
|
|
const makeTOCItem = (item) => ({
|
|
label: item.title,
|
|
href: JSON.stringify(item.dest),
|
|
subitems: item.items.length ? item.items.map(makeTOCItem) : null,
|
|
});
|
|
|
|
export const makePDF = async (file, options = {}) => {
|
|
const cMapUrl = options.cMapUrl ?? config.pdfjsPath("cmaps/");
|
|
const standardFontDataUrl = options.standardFontDataUrl ?? config.pdfjsPath("standard_fonts/");
|
|
if (options.workerSrc) {
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = options.workerSrc;
|
|
}
|
|
const transport = new pdfjsLib.PDFDataRangeTransport(file.size, []);
|
|
transport.requestDataRange = (begin, end) => {
|
|
file
|
|
.slice(begin, end)
|
|
.arrayBuffer()
|
|
.then((chunk) => {
|
|
transport.onDataRange(begin, chunk);
|
|
});
|
|
};
|
|
const pdf = await pdfjsLib.getDocument({
|
|
range: transport,
|
|
cMapUrl,
|
|
standardFontDataUrl,
|
|
isEvalSupported: false,
|
|
}).promise;
|
|
|
|
const book = { rendition: { layout: "pre-paginated" } };
|
|
|
|
const { metadata, info } = (await pdf.getMetadata()) ?? {};
|
|
// TODO: for better results, parse `metadata.getRaw()`
|
|
book.metadata = {
|
|
title: metadata?.get("dc:title") ?? info?.Title,
|
|
author: metadata?.get("dc:creator") ?? info?.Author,
|
|
contributor: metadata?.get("dc:contributor"),
|
|
description: metadata?.get("dc:description") ?? info?.Subject,
|
|
language: metadata?.get("dc:language"),
|
|
publisher: metadata?.get("dc:publisher"),
|
|
subject: metadata?.get("dc:subject"),
|
|
identifier: metadata?.get("dc:identifier"),
|
|
source: metadata?.get("dc:source"),
|
|
rights: metadata?.get("dc:rights"),
|
|
};
|
|
|
|
const outline = await pdf.getOutline();
|
|
book.toc = outline?.map(makeTOCItem);
|
|
|
|
const cache = new Map();
|
|
book.sections = Array.from({ length: pdf.numPages }).map((_, i) => ({
|
|
id: i,
|
|
load: async () => {
|
|
const cached = cache.get(i);
|
|
if (cached) return cached;
|
|
const url = await renderPage(await pdf.getPage(i + 1));
|
|
cache.set(i, url);
|
|
return url;
|
|
},
|
|
size: 1000,
|
|
}));
|
|
book.isExternal = (uri) => /^\w+:/i.test(uri);
|
|
book.resolveHref = async (href) => {
|
|
const parsed = JSON.parse(href);
|
|
const dest =
|
|
typeof parsed === "string" ? await pdf.getDestination(parsed) : parsed;
|
|
const index = await pdf.getPageIndex(dest[0]);
|
|
return { index };
|
|
};
|
|
book.splitTOCHref = async (href) => {
|
|
const parsed = JSON.parse(href);
|
|
const dest =
|
|
typeof parsed === "string" ? await pdf.getDestination(parsed) : parsed;
|
|
const index = await pdf.getPageIndex(dest[0]);
|
|
return [index, null];
|
|
};
|
|
book.getTOCFragment = (doc) => doc.documentElement;
|
|
book.getCover = async () => renderPage(await pdf.getPage(1), true);
|
|
book.destroy = () => pdf.destroy();
|
|
return book;
|
|
};
|