@zinejs/pdf 0.7.0 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +60 -57
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +2 -2
- package/dist/index.umd.js.map +1 -1
- package/dist/pdfSource.d.ts +3 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
function e(e) {
|
|
2
|
-
|
|
2
|
+
typeof HTMLCanvasElement < "u" && e instanceof HTMLCanvasElement && (e.width = 0, e.height = 0);
|
|
3
3
|
}
|
|
4
4
|
function t(e) {
|
|
5
|
+
return typeof e == "object" && !!e && "numPages" in e && typeof e.getPage == "function";
|
|
6
|
+
}
|
|
7
|
+
function n(e) {
|
|
5
8
|
try {
|
|
6
9
|
let t = new URL(e, "http://x").pathname.split("/").filter(Boolean).pop();
|
|
7
10
|
return t && /\.[a-z0-9]+$/i.test(t) ? decodeURIComponent(t) : "document.pdf";
|
|
@@ -9,11 +12,11 @@ function t(e) {
|
|
|
9
12
|
return "document.pdf";
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
|
-
async function
|
|
15
|
+
async function r() {
|
|
13
16
|
let e = globalThis;
|
|
14
17
|
return e.pdfjsLib ? e.pdfjsLib : await import("pdfjs-dist");
|
|
15
18
|
}
|
|
16
|
-
var
|
|
19
|
+
var i = class {
|
|
17
20
|
pageCount = 0;
|
|
18
21
|
#e;
|
|
19
22
|
#t;
|
|
@@ -23,27 +26,34 @@ var r = class {
|
|
|
23
26
|
#a;
|
|
24
27
|
#o;
|
|
25
28
|
#s;
|
|
26
|
-
#c =
|
|
27
|
-
#l =
|
|
28
|
-
#u =
|
|
29
|
-
#d =
|
|
29
|
+
#c = null;
|
|
30
|
+
#l = /* @__PURE__ */ new Map();
|
|
31
|
+
#u = 0;
|
|
32
|
+
#d = null;
|
|
33
|
+
#f = /* @__PURE__ */ new Map();
|
|
34
|
+
#p = /* @__PURE__ */ new Map();
|
|
30
35
|
constructor(e, t = {}) {
|
|
31
36
|
this.#e = e, this.#t = t.workerSrc, this.#n = t.renderScale ?? 1, this.#r = t.preload ?? 1, this.#a = t.maxCacheBytes ?? 268435456, this.#o = t.progressive ?? !1, this.#s = t.disableAutoFetch ?? !1;
|
|
32
37
|
}
|
|
33
38
|
onPageUpdate(e) {
|
|
34
|
-
this.#
|
|
39
|
+
this.#d = e;
|
|
40
|
+
}
|
|
41
|
+
onProgress(e) {
|
|
42
|
+
this.#c = e;
|
|
35
43
|
}
|
|
36
44
|
async open() {
|
|
37
|
-
if (
|
|
45
|
+
if (t(this.#e)) this.#i = this.#e;
|
|
38
46
|
else {
|
|
39
|
-
let e = await
|
|
40
|
-
e.GlobalWorkerOptions.workerSrc = this.#
|
|
47
|
+
let e = await r();
|
|
48
|
+
e.GlobalWorkerOptions.workerSrc = this.#m(e.GlobalWorkerOptions.workerSrc);
|
|
41
49
|
let t = typeof this.#e == "string" ? { url: this.#e } : { data: this.#e };
|
|
42
|
-
this.#s && (t.disableAutoFetch = !0)
|
|
50
|
+
this.#s && (t.disableAutoFetch = !0);
|
|
51
|
+
let n = e.getDocument(t);
|
|
52
|
+
this.#c && (n.onProgress = this.#c), this.#i = await n.promise;
|
|
43
53
|
}
|
|
44
54
|
this.pageCount = this.#i.numPages;
|
|
45
55
|
}
|
|
46
|
-
#
|
|
56
|
+
#m(e) {
|
|
47
57
|
if (this.#t) return this.#t;
|
|
48
58
|
if (e) return e;
|
|
49
59
|
try {
|
|
@@ -53,19 +63,19 @@ var r = class {
|
|
|
53
63
|
}
|
|
54
64
|
}
|
|
55
65
|
get(e, t) {
|
|
56
|
-
if (t && t.scale > 1) return this.#
|
|
57
|
-
let n = this.#
|
|
66
|
+
if (t && t.scale > 1) return this.#b(e, t.scale);
|
|
67
|
+
let n = this.#_(e);
|
|
58
68
|
for (let t = 1; t <= this.#r; t++) this.prefetch([e - t, e + t]);
|
|
59
69
|
return n;
|
|
60
70
|
}
|
|
61
71
|
prefetch(e) {
|
|
62
|
-
for (let t of e) t >= 0 && t < this.pageCount && this.#
|
|
72
|
+
for (let t of e) t >= 0 && t < this.pageCount && this.#_(t).catch(() => {});
|
|
63
73
|
}
|
|
64
74
|
getText(e) {
|
|
65
|
-
let t = this.#
|
|
75
|
+
let t = this.#p.get(e);
|
|
66
76
|
if (t) return t;
|
|
67
|
-
let n = this.#
|
|
68
|
-
return this.#
|
|
77
|
+
let n = this.#g(e).catch(() => "");
|
|
78
|
+
return this.#p.set(e, n), n;
|
|
69
79
|
}
|
|
70
80
|
async getOutline() {
|
|
71
81
|
let e = this.#i;
|
|
@@ -79,12 +89,12 @@ var r = class {
|
|
|
79
89
|
if (!t?.length) return [];
|
|
80
90
|
let n = async (e) => Promise.all(e.map(async (e) => ({
|
|
81
91
|
title: (e.title ?? "").trim() || "Untitled",
|
|
82
|
-
page: await this.#
|
|
92
|
+
page: await this.#h(e.dest),
|
|
83
93
|
children: e.items?.length ? await n(e.items) : []
|
|
84
94
|
})));
|
|
85
95
|
return n(t);
|
|
86
96
|
}
|
|
87
|
-
async #
|
|
97
|
+
async #h(e) {
|
|
88
98
|
let t = this.#i;
|
|
89
99
|
if (!t || e == null) return null;
|
|
90
100
|
try {
|
|
@@ -101,7 +111,7 @@ var r = class {
|
|
|
101
111
|
let e = this.#e;
|
|
102
112
|
if (typeof e == "string") return {
|
|
103
113
|
url: e,
|
|
104
|
-
filename:
|
|
114
|
+
filename: n(e)
|
|
105
115
|
};
|
|
106
116
|
if (e instanceof ArrayBuffer || ArrayBuffer.isView(e)) {
|
|
107
117
|
let t = new Blob([e], { type: "application/pdf" });
|
|
@@ -113,7 +123,7 @@ var r = class {
|
|
|
113
123
|
}
|
|
114
124
|
return null;
|
|
115
125
|
}
|
|
116
|
-
async #
|
|
126
|
+
async #g(e) {
|
|
117
127
|
if (this.#i === null) throw Error("PdfSource: use after open() — the document is not loaded.");
|
|
118
128
|
let t = await this.#i.getPage(e + 1);
|
|
119
129
|
if (typeof t.getTextContent != "function") return "";
|
|
@@ -122,49 +132,42 @@ var r = class {
|
|
|
122
132
|
return r;
|
|
123
133
|
}
|
|
124
134
|
destroy() {
|
|
125
|
-
this.#i?.destroy?.(), this.#
|
|
135
|
+
this.#i?.destroy?.(), this.#l.clear();
|
|
136
|
+
for (let t of this.#f.values()) t.then(e).catch(() => {});
|
|
137
|
+
this.#f.clear(), this.#p.clear(), this.#u = 0, this.#i = null;
|
|
126
138
|
}
|
|
127
|
-
#
|
|
128
|
-
let t = this.#
|
|
129
|
-
if (t) return this.#
|
|
139
|
+
#_(e) {
|
|
140
|
+
let t = this.#l.get(e);
|
|
141
|
+
if (t) return this.#l.delete(e), this.#l.set(e, t), t.promise;
|
|
130
142
|
let n = typeof devicePixelRatio == "number" ? devicePixelRatio : 1, r = this.#n * n, i = this.#o ? r * .35 : r, a = {
|
|
131
143
|
promise: void 0,
|
|
132
144
|
bytes: 0
|
|
133
145
|
};
|
|
134
|
-
return a.promise = this.#
|
|
135
|
-
throw this.#
|
|
136
|
-
}), this.#
|
|
146
|
+
return a.promise = this.#x(e, i).then((t) => (a.bytes = t.width * t.height * 4, this.#u += a.bytes, this.#y(e), this.#o && this.#v(e, a, r), t), (t) => {
|
|
147
|
+
throw this.#l.delete(e), t;
|
|
148
|
+
}), this.#l.set(e, a), a.promise;
|
|
137
149
|
}
|
|
138
|
-
#
|
|
139
|
-
this.#
|
|
140
|
-
this.#
|
|
150
|
+
#v(e, t, n) {
|
|
151
|
+
this.#x(e, n).then((n) => {
|
|
152
|
+
this.#l.get(e) === t && (this.#u -= t.bytes, t.promise = Promise.resolve(n), t.bytes = n.width * n.height * 4, this.#u += t.bytes, this.#y(e), this.#d?.(e));
|
|
141
153
|
}).catch(() => {});
|
|
142
154
|
}
|
|
143
|
-
#
|
|
144
|
-
for (let [t, n] of this.#
|
|
145
|
-
if (this.#
|
|
146
|
-
t === e || n.bytes === 0 || (this.#
|
|
155
|
+
#y(e) {
|
|
156
|
+
for (let [t, n] of this.#l) {
|
|
157
|
+
if (this.#u <= this.#a) break;
|
|
158
|
+
t === e || n.bytes === 0 || (this.#l.delete(t), this.#u -= n.bytes);
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
-a.height * o.y
|
|
160
|
-
];
|
|
161
|
-
return await n.render({
|
|
162
|
-
canvasContext: s.getContext("2d"),
|
|
163
|
-
viewport: a,
|
|
164
|
-
transform: c
|
|
165
|
-
}).promise, s;
|
|
166
|
-
}
|
|
167
|
-
async #y(e, t) {
|
|
161
|
+
#b(t, n) {
|
|
162
|
+
let r = Math.min(n, 6), i = `${t}@${r}`, a = this.#f.get(i);
|
|
163
|
+
if (a) return a;
|
|
164
|
+
let o = typeof devicePixelRatio == "number" ? devicePixelRatio : 1, s = this.#x(t, this.#n * o * r).catch((e) => {
|
|
165
|
+
throw this.#f.delete(i), e;
|
|
166
|
+
});
|
|
167
|
+
for (let [t, n] of this.#f) t.endsWith(`@${r}`) || (this.#f.delete(t), n.then(e).catch(() => {}));
|
|
168
|
+
return this.#f.set(i, s), s;
|
|
169
|
+
}
|
|
170
|
+
async #x(e, t) {
|
|
168
171
|
if (this.#i === null) throw Error("PdfSource: use after open() — the document is not loaded.");
|
|
169
172
|
let n = await this.#i.getPage(e + 1), r = n.getViewport({ scale: t }), i = document.createElement("canvas");
|
|
170
173
|
return i.width = Math.ceil(r.width), i.height = Math.ceil(r.height), await n.render({
|
|
@@ -174,6 +177,6 @@ var r = class {
|
|
|
174
177
|
}
|
|
175
178
|
};
|
|
176
179
|
//#endregion
|
|
177
|
-
export {
|
|
180
|
+
export { i as PdfSource };
|
|
178
181
|
|
|
179
182
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#progressive","#disableAutoFetch","#onUpdate","#doc","#resolveWorkerSrc","#renderTile","#renderPage","#textCache","#extractText","#destinationPage","#cache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type { DownloadInfo, OutlineItem, PageContent, PageRequest, Source } from '@zinejs/core';\n\n/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */\ninterface PdfPageLike {\n getViewport(opts: { scale: number }): { width: number; height: number };\n /** `transform` is an affine matrix applied before painting, used to offset a cropped tile. */\n render(opts: { canvasContext: unknown; viewport: unknown; transform?: number[] }): {\n promise: Promise<void>;\n };\n /** Optional: absent on scanned PDFs with no text layer, and on hand-rolled document stubs. */\n getTextContent?(): Promise<{ items: { str?: string; hasEOL?: boolean }[] }>;\n}\n/** A pdf.js outline entry. `dest` is either a named destination or an explicit array whose first\n * element references the target page. */\ninterface PdfOutlineNode {\n title?: string;\n dest?: string | unknown[] | null;\n items?: PdfOutlineNode[];\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\n /** Optional: absent on documents without a table of contents, and on hand-rolled stubs. */\n getOutline?(): Promise<PdfOutlineNode[] | null>;\n getDestination?(id: string): Promise<unknown[] | null>;\n getPageIndex?(ref: unknown): Promise<number>;\n}\ninterface DocParams {\n url?: string;\n data?: ArrayBuffer | Uint8Array;\n disableAutoFetch?: boolean;\n}\ninterface PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: DocParams): { promise: Promise<PdfDocumentLike> };\n}\n\n/** A URL, raw bytes, or a pre-created pdf.js document. */\nexport type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;\n\nexport interface PdfSourceOptions {\n /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */\n workerSrc?: string;\n /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */\n renderScale?: number;\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */\n maxCacheBytes?: number;\n /** Paint a low-res page first, then swap to crisp (faster first paint). Default false. */\n progressive?: boolean;\n /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */\n disableAutoFetch?: boolean;\n}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\n// Linear-resolution fraction for the progressive low-res first pass (~1/9 the pixels).\nconst PROGRESSIVE_LOW_RATIO = 0.35;\n// Ceiling on zoom-tile magnification. Past roughly this point a screen pixel already holds more\n// detail than the reader can resolve, and rasterizing further only costs memory and time.\nconst MAX_ZOOM_RENDER_SCALE = 6;\n\ninterface CacheEntry {\n promise: Promise<PageContent>;\n bytes: number; // 0 until the render resolves and its size is known\n}\n\nfunction isPdfDocument(src: PdfSrc): src is PdfDocumentLike {\n return (\n typeof src === 'object' &&\n src !== null &&\n 'numPages' in src &&\n typeof (src as PdfDocumentLike).getPage === 'function'\n );\n}\n\n/** Last path segment of a URL, for naming a download. Falls back when the URL has no filename\n * (a directory, a data: URI, or something unparseable). */\nfunction filenameFromUrl(url: string): string {\n try {\n const path = new URL(url, 'http://x').pathname;\n const last = path.split('/').filter(Boolean).pop();\n return last && /\\.[a-z0-9]+$/i.test(last) ? decodeURIComponent(last) : 'document.pdf';\n } catch {\n return 'document.pdf';\n }\n}\n\n/**\n * Load pdf.js. CDN / UMD hosts typically expose `globalThis.pdfjsLib` from a\n * `<script>` of pdf.js; bundlers have no global and resolve the dynamic import.\n */\nasync function loadPdfjs(): Promise<PdfjsModule> {\n const g = globalThis as typeof globalThis & { pdfjsLib?: PdfjsModule };\n if (g.pdfjsLib) return g.pdfjsLib;\n return (await import('pdfjs-dist')) as unknown as PdfjsModule;\n}\n\n/**\n * Renders PDF pages to canvases for zinejs. pdf.js is a dependency of this\n * package, loaded lazily so it never lands in the core bundle. Async — pass it\n * to `new Zine`, which calls `open()` (learning the page count) before building\n * spreads.\n */\nexport class PdfSource implements Source {\n pageCount = 0;\n #src: PdfSrc;\n #workerSrc: string | undefined;\n #renderScale: number;\n #preload: number;\n #doc: PdfDocumentLike | null = null;\n #maxCacheBytes: number;\n #progressive: boolean;\n #disableAutoFetch: boolean;\n #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\n #onUpdate: ((index: number) => void) | null = null;\n /** Extracted page text, kept apart from #cache: text is scale-independent and negligible next\n * to a raster, so it has no place in the byte budget that evicts bitmaps. */\n #textCache = new Map<number, Promise<string>>();\n\n constructor(src: PdfSrc, options: PdfSourceOptions = {}) {\n this.#src = src;\n this.#workerSrc = options.workerSrc;\n this.#renderScale = options.renderScale ?? 1;\n this.#preload = options.preload ?? 1;\n this.#maxCacheBytes = options.maxCacheBytes ?? DEFAULT_MAX_CACHE_BYTES;\n this.#progressive = options.progressive ?? false;\n this.#disableAutoFetch = options.disableAutoFetch ?? false;\n }\n\n /** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */\n onPageUpdate(handler: (index: number) => void): void {\n this.#onUpdate = handler;\n }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = await loadPdfjs();\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params: DocParams =\n typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\n if (this.#disableAutoFetch) params.disableAutoFetch = true;\n this.#doc = await pdfjs.getDocument(params).promise;\n }\n this.pageCount = this.#doc.numPages;\n }\n\n /** Precedence: an explicit `workerSrc` → an already-configured global → an auto-resolved default. */\n #resolveWorkerSrc(configured: string): string {\n if (this.#workerSrc) return this.#workerSrc;\n if (configured) return configured;\n try {\n // Bundlers (Vite / webpack 5 / esbuild) statically rewrite this and emit the worker,\n // so the common case needs no `workerSrc`. Non-bundler / UMD hosts pass it explicitly.\n return \"@@ZINE_PDF_WORKER_URL@@\";\n } catch {\n throw new Error(\n \"PdfSource: couldn't auto-resolve pdf.js's worker. Pass a `workerSrc` URL \" +\n '(see the @zinejs/pdf README, \"The pdf.js worker\"), set pdfjs ' +\n 'GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.',\n );\n }\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n // A zoomed request wants detail the fit-to-screen raster never had. PDF pages are vector, so\n // re-render the visible part at the magnification being viewed. Deliberately uncached: the\n // region changes as the reader pans, and these rasters are far too large to keep around.\n if (opts && opts.scale > 1) return this.#renderTile(index, opts);\n\n const decoded = this.#renderPage(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#renderPage(i).catch(() => {}); // prefetch failures surface on a real get()\n }\n }\n }\n\n /**\n * The page's text, for search. Empty for a page with no text layer (a scan, or a pdf.js build\n * without text extraction) — callers treat that as \"nothing to match\" rather than an error.\n */\n getText(index: number): Promise<string> {\n const cached = this.#textCache.get(index);\n if (cached) return cached;\n const pending = this.#extractText(index).catch(() => '');\n this.#textCache.set(index, pending);\n return pending;\n }\n\n /**\n * The document's table of contents, flattened into the shape the outline panel wants.\n *\n * Empty when the PDF has no outline, which is common. Destinations are resolved to page\n * indices here rather than in the UI: doing it once, up front, keeps clicking an entry\n * instant, and an entry whose destination cannot be resolved is kept with a null page so the\n * heading still shows.\n */\n async getOutline(): Promise<OutlineItem[]> {\n const doc = this.#doc;\n if (!doc || typeof doc.getOutline !== 'function') return [];\n let nodes: PdfOutlineNode[] | null;\n try {\n nodes = await doc.getOutline();\n } catch {\n return [];\n }\n if (!nodes?.length) return [];\n const convert = async (list: PdfOutlineNode[]): Promise<OutlineItem[]> =>\n Promise.all(\n list.map(async (node) => ({\n title: (node.title ?? '').trim() || 'Untitled',\n page: await this.#destinationPage(node.dest),\n children: node.items?.length ? await convert(node.items) : [],\n })),\n );\n return convert(nodes);\n }\n\n /** Resolve a pdf.js destination to a zero-based page index, or null if it cannot be. */\n async #destinationPage(dest: string | unknown[] | null | undefined): Promise<number | null> {\n const doc = this.#doc;\n if (!doc || dest == null) return null;\n try {\n // A named destination has to be looked up before it yields the usual explicit array.\n const explicit =\n typeof dest === 'string' ? await doc.getDestination?.(dest) : (dest as unknown[]);\n const ref = explicit?.[0];\n if (ref == null) return null;\n // Some destinations carry a bare page number instead of a reference.\n if (typeof ref === 'number') return ref;\n const index = await doc.getPageIndex?.(ref);\n return typeof index === 'number' ? index : null;\n } catch {\n return null; // a broken destination should not cost us the whole outline\n }\n }\n\n /**\n * The original PDF, for a download control.\n *\n * Null when the source was handed a pre-opened pdf.js document: the bytes belong to whoever\n * created it, and re-fetching them is not this class's call to make.\n */\n async getDownload(): Promise<DownloadInfo | null> {\n const src = this.#src;\n if (typeof src === 'string') {\n return { url: src, filename: filenameFromUrl(src) };\n }\n if (src instanceof ArrayBuffer || ArrayBuffer.isView(src)) {\n const blob = new Blob([src as BlobPart], { type: 'application/pdf' });\n return { url: URL.createObjectURL(blob), filename: 'document.pdf', revoke: true };\n }\n return null;\n }\n\n async #extractText(index: number): Promise<string> {\n if (this.#doc === null) {\n throw new Error('PdfSource: use after open() — the document is not loaded.');\n }\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n if (typeof page.getTextContent !== 'function') return '';\n const content = await page.getTextContent();\n let text = '';\n for (const item of content.items) {\n if (typeof item.str !== 'string') continue; // marked-content entries carry no text\n text += item.str;\n // pdf.js splits a visual line into runs; only hasEOL ends one.\n text += item.hasEOL ? '\\n' : '';\n }\n return text;\n }\n\n destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.clear();\n this.#textCache.clear();\n this.#cachedBytes = 0;\n this.#doc = null;\n }\n\n #renderPage(index: number): Promise<PageContent> {\n const existing = this.#cache.get(index);\n if (existing) {\n // Mark most-recently-used: re-insert so it sits at the end of the Map's order.\n this.#cache.delete(index);\n this.#cache.set(index, existing);\n return existing.promise;\n }\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const full = this.#renderScale * dpr;\n const firstScale = this.#progressive ? full * PROGRESSIVE_LOW_RATIO : full;\n\n const entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index, firstScale).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n if (this.#progressive) this.#upgrade(index, entry, full);\n return content;\n },\n (error: unknown) => {\n this.#cache.delete(index); // let a later get() retry\n throw error;\n },\n );\n this.#cache.set(index, entry);\n return entry.promise;\n }\n\n /** Progressive: re-render at full resolution, swap it into the cache, and signal an update. */\n #upgrade(index: number, entry: CacheEntry, fullScale: number): void {\n void this.#rasterize(index, fullScale)\n .then((hi) => {\n if (this.#cache.get(index) !== entry) return; // evicted or replaced meanwhile\n this.#cachedBytes -= entry.bytes;\n entry.promise = Promise.resolve(hi);\n entry.bytes = hi.width * hi.height * 4;\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n this.#onUpdate?.(index);\n })\n .catch(() => {}); // keep the low-res if the upgrade fails\n }\n\n /** Drop least-recently-used pages (front of the Map) until under the byte cap. */\n #evictToFit(keepIndex: number): void {\n for (const [index, entry] of this.#cache) {\n if (this.#cachedBytes <= this.#maxCacheBytes) break;\n if (index === keepIndex || entry.bytes === 0) continue; // keep the newest; skip in-flight renders\n this.#cache.delete(index);\n this.#cachedBytes -= entry.bytes;\n }\n }\n\n /**\n * Render just the visible part of a page at zoom magnification.\n *\n * The tile covers `region` of the page and is sized to the pixels actually on screen, so its\n * cost stays flat however far the reader zooms in — a full-page re-render would grow with the\n * square of the scale and dwarf the whole page cache.\n */\n async #renderTile(index: number, req: PageRequest): Promise<PageContent> {\n if (this.#doc === null) throw new Error('PdfSource: use after open() — the document is not loaded.');\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const base = this.#renderScale * dpr;\n // The tile is only ever as many pixels as the region occupies on screen, so clamping the\n // scale bounds its size no matter how deep the zoom goes.\n const scale = base * Math.min(req.scale, MAX_ZOOM_RENDER_SCALE);\n const full = page.getViewport({ scale });\n\n const r = req.region;\n const canvas = document.createElement('canvas');\n canvas.width = Math.max(1, Math.ceil(full.width * r.width));\n canvas.height = Math.max(1, Math.ceil(full.height * r.height));\n // Shift the page under the canvas so the region lands at the origin; pdf.js has no crop of\n // its own, and rendering full-size into a small canvas would just clip the top-left corner.\n const transform = [1, 0, 0, 1, -full.width * r.x, -full.height * r.y];\n await page.render({ canvasContext: canvas.getContext('2d'), viewport: full, transform }).promise;\n return canvas;\n }\n\n async #rasterize(index: number, scale: number): Promise<PageContent> {\n if (this.#doc === null) throw new Error('PdfSource: use after open() — the document is not loaded.');\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n const viewport = page.getViewport({ scale });\n const canvas = document.createElement('canvas');\n canvas.width = Math.ceil(viewport.width);\n canvas.height = Math.ceil(viewport.height);\n await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;\n return canvas;\n }\n}\n"],"mappings":"AAoEA,SAAS,EAAc,GAAqC;CAC1D,OACE,OAAO,KAAQ,cACf,KACA,cAAc,KACd,OAAQ,EAAwB,WAAY;AAEhD;AAIA,SAAS,EAAgB,GAAqB;CAC5C,IAAI;EAEF,IAAM,IADO,IAAI,IAAI,GAAK,UAAU,CAAC,CAAC,SACpB,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,IAAI;EACjD,OAAO,KAAQ,gBAAgB,KAAK,CAAI,IAAI,mBAAmB,CAAI,IAAI;CACzE,QAAQ;EACN,OAAO;CACT;AACF;AAMA,eAAe,IAAkC;CAC/C,IAAM,IAAI;CAEV,OADI,EAAE,WAAiB,EAAE,WACjB,MAAM,OAAO;AACvB;AAQA,IAAa,IAAb,MAAyC;CACvC,YAAY;CACZ;CACA;CACA;CACA;CACA,KAA+B;CAC/B;CACA;CACA;CACA,qBAAS,IAAI,IAAwB;CACrC,KAAe;CACf,KAA8C;CAG9C,qBAAa,IAAI,IAA6B;CAE9C,YAAY,GAAa,IAA4B,CAAC,GAAG;EAOvD,AANA,KAAKA,KAAO,GACZ,KAAKC,KAAa,EAAQ,WAC1B,KAAKC,KAAe,EAAQ,eAAe,GAC3C,KAAKC,KAAW,EAAQ,WAAW,GACnC,KAAKC,KAAiB,EAAQ,iBAAiB,WAC/C,KAAKC,KAAe,EAAQ,eAAe,IAC3C,KAAKC,KAAoB,EAAQ,oBAAoB;CACvD;CAGA,aAAa,GAAwC;EACnD,KAAKC,KAAY;CACnB;CAEA,MAAM,OAAsB;EAC1B,IAAI,EAAc,KAAKP,EAAI,GACzB,KAAKQ,KAAO,KAAKR;OACZ;GACL,IAAM,IAAQ,MAAM,EAAU;GAC9B,EAAM,oBAAoB,YAAY,KAAKS,GAAkB,EAAM,oBAAoB,SAAS;GAChG,IAAM,IACJ,OAAO,KAAKT,MAAS,WAAW,EAAE,KAAK,KAAKA,GAAK,IAAI,EAAE,MAAM,KAAKA,GAAK;GAEzE,AADI,KAAKM,OAAmB,EAAO,mBAAmB,KACtD,KAAKE,KAAO,MAAM,EAAM,YAAY,CAAM,CAAC,CAAC;EAC9C;EACA,KAAK,YAAY,KAAKA,GAAK;CAC7B;CAGA,GAAkB,GAA4B;EAC5C,IAAI,KAAKP,IAAY,OAAO,KAAKA;EACjC,IAAI,GAAY,OAAO;EACvB,IAAI;GAGF,OAAO;EACT,QAAQ;GACN,MAAU,MACR,wNAGF;EACF;CACF;CAEA,IAAI,GAAe,GAA0C;EAI3D,IAAI,KAAQ,EAAK,QAAQ,GAAG,OAAO,KAAKS,GAAY,GAAO,CAAI;EAE/D,IAAM,IAAU,KAAKC,GAAY,CAAK;EACtC,KAAK,IAAI,IAAI,GAAG,KAAK,KAAKR,IAAU,KAClC,KAAK,SAAS,CAAC,IAAQ,GAAG,IAAQ,CAAC,CAAC;EAEtC,OAAO;CACT;CAEA,SAAS,GAAyB;EAChC,KAAK,IAAM,KAAK,GACd,AAAI,KAAK,KAAK,IAAI,KAAK,aACrB,KAAUQ,GAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;CAG7C;CAMA,QAAQ,GAAgC;EACtC,IAAM,IAAS,KAAKC,GAAW,IAAI,CAAK;EACxC,IAAI,GAAQ,OAAO;EACnB,IAAM,IAAU,KAAKC,GAAa,CAAK,CAAC,CAAC,YAAY,EAAE;EAEvD,OADA,KAAKD,GAAW,IAAI,GAAO,CAAO,GAC3B;CACT;CAUA,MAAM,aAAqC;EACzC,IAAM,IAAM,KAAKJ;EACjB,IAAI,CAAC,KAAO,OAAO,EAAI,cAAe,YAAY,OAAO,CAAC;EAC1D,IAAI;EACJ,IAAI;GACF,IAAQ,MAAM,EAAI,WAAW;EAC/B,QAAQ;GACN,OAAO,CAAC;EACV;EACA,IAAI,CAAC,GAAO,QAAQ,OAAO,CAAC;EAC5B,IAAM,IAAU,OAAO,MACrB,QAAQ,IACN,EAAK,IAAI,OAAO,OAAU;GACxB,QAAQ,EAAK,SAAS,GAAA,CAAI,KAAK,KAAK;GACpC,MAAM,MAAM,KAAKM,GAAiB,EAAK,IAAI;GAC3C,UAAU,EAAK,OAAO,SAAS,MAAM,EAAQ,EAAK,KAAK,IAAI,CAAC;EAC9D,EAAE,CACJ;EACF,OAAO,EAAQ,CAAK;CACtB;CAGA,MAAMA,GAAiB,GAAqE;EAC1F,IAAM,IAAM,KAAKN;EACjB,IAAI,CAAC,KAAO,KAAQ,MAAM,OAAO;EACjC,IAAI;GAIF,IAAM,KADJ,OAAO,KAAS,WAAW,MAAM,EAAI,iBAAiB,CAAI,IAAK,EAAA,GAC1C;GACvB,IAAI,KAAO,MAAM,OAAO;GAExB,IAAI,OAAO,KAAQ,UAAU,OAAO;GACpC,IAAM,IAAQ,MAAM,EAAI,eAAe,CAAG;GAC1C,OAAO,OAAO,KAAU,WAAW,IAAQ;EAC7C,QAAQ;GACN,OAAO;EACT;CACF;CAQA,MAAM,cAA4C;EAChD,IAAM,IAAM,KAAKR;EACjB,IAAI,OAAO,KAAQ,UACjB,OAAO;GAAE,KAAK;GAAK,UAAU,EAAgB,CAAG;EAAE;EAEpD,IAAI,aAAe,eAAe,YAAY,OAAO,CAAG,GAAG;GACzD,IAAM,IAAO,IAAI,KAAK,CAAC,CAAe,GAAG,EAAE,MAAM,kBAAkB,CAAC;GACpE,OAAO;IAAE,KAAK,IAAI,gBAAgB,CAAI;IAAG,UAAU;IAAgB,QAAQ;GAAK;EAClF;EACA,OAAO;CACT;CAEA,MAAMa,GAAa,GAAgC;EACjD,IAAI,KAAKL,OAAS,MAChB,MAAU,MAAM,2DAA2D;EAE7E,IAAM,IAAO,MAAM,KAAKA,GAAK,QAAQ,IAAQ,CAAC;EAC9C,IAAI,OAAO,EAAK,kBAAmB,YAAY,OAAO;EACtD,IAAM,IAAU,MAAM,EAAK,eAAe,GACtC,IAAO;EACX,KAAK,IAAM,KAAQ,EAAQ,OACrB,OAAO,EAAK,OAAQ,aACxB,KAAQ,EAAK,KAEb,KAAQ,EAAK,SAAS,OAAO;EAE/B,OAAO;CACT;CAEA,UAAgB;EAKd,AAJA,KAAKA,IAAM,UAAU,GACrB,KAAKO,GAAO,MAAM,GAClB,KAAKH,GAAW,MAAM,GACtB,KAAKI,KAAe,GACpB,KAAKR,KAAO;CACd;CAEA,GAAY,GAAqC;EAC/C,IAAM,IAAW,KAAKO,GAAO,IAAI,CAAK;EACtC,IAAI,GAIF,OAFA,KAAKA,GAAO,OAAO,CAAK,GACxB,KAAKA,GAAO,IAAI,GAAO,CAAQ,GACxB,EAAS;EAGlB,IAAM,IAAM,OAAO,oBAAqB,WAAW,mBAAmB,GAChE,IAAO,KAAKb,KAAe,GAC3B,IAAa,KAAKG,KAAe,IAAO,MAAwB,GAEhE,IAAoB;GAAE,SAAS,KAAA;GAA8C,OAAO;EAAE;EAe5F,OAdA,EAAM,UAAU,KAAKY,GAAW,GAAO,CAAU,CAAC,CAAC,MAChD,OACC,EAAM,QAAQ,EAAQ,QAAQ,EAAQ,SAAS,GAC/C,KAAKD,MAAgB,EAAM,OAC3B,KAAKE,GAAY,CAAK,GAClB,KAAKb,MAAc,KAAKc,GAAS,GAAO,GAAO,CAAI,GAChD,KAER,MAAmB;GAElB,MADA,KAAKJ,GAAO,OAAO,CAAK,GAClB;EACR,CACF,GACA,KAAKA,GAAO,IAAI,GAAO,CAAK,GACrB,EAAM;CACf;CAGA,GAAS,GAAe,GAAmB,GAAyB;EAClE,KAAUE,GAAW,GAAO,CAAS,CAAC,CACnC,MAAM,MAAO;GACR,KAAKF,GAAO,IAAI,CAAK,MAAM,MAC/B,KAAKC,MAAgB,EAAM,OAC3B,EAAM,UAAU,QAAQ,QAAQ,CAAE,GAClC,EAAM,QAAQ,EAAG,QAAQ,EAAG,SAAS,GACrC,KAAKA,MAAgB,EAAM,OAC3B,KAAKE,GAAY,CAAK,GACtB,KAAKX,KAAY,CAAK;EACxB,CAAC,CAAC,CACD,YAAY,CAAC,CAAC;CACnB;CAGA,GAAY,GAAyB;EACnC,KAAK,IAAM,CAAC,GAAO,MAAU,KAAKQ,IAAQ;GACxC,IAAI,KAAKC,MAAgB,KAAKZ,IAAgB;GAC1C,MAAU,KAAa,EAAM,UAAU,MAC3C,KAAKW,GAAO,OAAO,CAAK,GACxB,KAAKC,MAAgB,EAAM;EAC7B;CACF;CASA,MAAMN,GAAY,GAAe,GAAwC;EACvE,IAAI,KAAKF,OAAS,MAAM,MAAU,MAAM,2DAA2D;EACnG,IAAM,IAAO,MAAM,KAAKA,GAAK,QAAQ,IAAQ,CAAC,GAExC,IAAM,OAAO,oBAAqB,WAAW,mBAAmB,GAIhE,IAHO,KAAKN,KAAe,IAGZ,KAAK,IAAI,EAAI,OAAO,CAAqB,GACxD,IAAO,EAAK,YAAY,EAAE,SAAM,CAAC,GAEjC,IAAI,EAAI,QACR,IAAS,SAAS,cAAc,QAAQ;EAE9C,AADA,EAAO,QAAQ,KAAK,IAAI,GAAG,KAAK,KAAK,EAAK,QAAQ,EAAE,KAAK,CAAC,GAC1D,EAAO,SAAS,KAAK,IAAI,GAAG,KAAK,KAAK,EAAK,SAAS,EAAE,MAAM,CAAC;EAG7D,IAAM,IAAY;GAAC;GAAG;GAAG;GAAG;GAAG,CAAC,EAAK,QAAQ,EAAE;GAAG,CAAC,EAAK,SAAS,EAAE;EAAC;EAEpE,OADA,MAAM,EAAK,OAAO;GAAE,eAAe,EAAO,WAAW,IAAI;GAAG,UAAU;GAAM;EAAU,CAAC,CAAC,CAAC,SAClF;CACT;CAEA,MAAMe,GAAW,GAAe,GAAqC;EACnE,IAAI,KAAKT,OAAS,MAAM,MAAU,MAAM,2DAA2D;EACnG,IAAM,IAAO,MAAM,KAAKA,GAAK,QAAQ,IAAQ,CAAC,GACxC,IAAW,EAAK,YAAY,EAAE,SAAM,CAAC,GACrC,IAAS,SAAS,cAAc,QAAQ;EAI9C,OAHA,EAAO,QAAQ,KAAK,KAAK,EAAS,KAAK,GACvC,EAAO,SAAS,KAAK,KAAK,EAAS,MAAM,GACzC,MAAM,EAAK,OAAO;GAAE,eAAe,EAAO,WAAW,IAAI;GAAG;EAAS,CAAC,CAAC,CAAC,SACjE;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#progressive","#disableAutoFetch","#onUpdate","#onProgress","#doc","#resolveWorkerSrc","#renderZoomed","#renderPage","#textCache","#extractText","#destinationPage","#cache","#zoomCache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type {\n DownloadInfo,\n LoadProgress,\n OutlineItem,\n PageContent,\n PageRequest,\n Source,\n} from '@zinejs/core';\n\n/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */\ninterface PdfPageLike {\n getViewport(opts: { scale: number }): { width: number; height: number };\n /** `transform` is an affine matrix applied before painting, used to offset a cropped tile. */\n render(opts: { canvasContext: unknown; viewport: unknown; transform?: number[] }): {\n promise: Promise<void>;\n };\n /** Optional: absent on scanned PDFs with no text layer, and on hand-rolled document stubs. */\n getTextContent?(): Promise<{ items: { str?: string; hasEOL?: boolean }[] }>;\n}\n/** A pdf.js outline entry. `dest` is either a named destination or an explicit array whose first\n * element references the target page. */\ninterface PdfOutlineNode {\n title?: string;\n dest?: string | unknown[] | null;\n items?: PdfOutlineNode[];\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\n /** Optional: absent on documents without a table of contents, and on hand-rolled stubs. */\n getOutline?(): Promise<PdfOutlineNode[] | null>;\n getDestination?(id: string): Promise<unknown[] | null>;\n getPageIndex?(ref: unknown): Promise<number>;\n}\ninterface DocParams {\n url?: string;\n data?: ArrayBuffer | Uint8Array;\n disableAutoFetch?: boolean;\n}\n/** pdf.js's loading task: the document promise, plus an assignable download-progress callback. */\ninterface PdfLoadingTask {\n promise: Promise<PdfDocumentLike>;\n onProgress?: (p: LoadProgress) => void;\n}\ninterface PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: DocParams): PdfLoadingTask;\n}\n\n/** A URL, raw bytes, or a pre-created pdf.js document. */\nexport type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;\n\nexport interface PdfSourceOptions {\n /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */\n workerSrc?: string;\n /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */\n renderScale?: number;\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */\n maxCacheBytes?: number;\n /** Paint a low-res page first, then swap to crisp (faster first paint). Default false. */\n progressive?: boolean;\n /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */\n disableAutoFetch?: boolean;\n}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\n// Linear-resolution fraction for the progressive low-res first pass (~1/9 the pixels).\nconst PROGRESSIVE_LOW_RATIO = 0.35;\n// Ceiling on zoom-tile magnification. Past roughly this point a screen pixel already holds more\n// detail than the reader can resolve, and rasterizing further only costs memory and time.\nconst MAX_ZOOM_RENDER_SCALE = 6;\n\n/** Free a canvas's backing store. Safari in particular holds the memory until the surface is\n * resized away, and a zoomed page is large enough for that to matter. */\nfunction releaseCanvas(content: PageContent): void {\n if (typeof HTMLCanvasElement !== 'undefined' && content instanceof HTMLCanvasElement) {\n content.width = 0;\n content.height = 0;\n }\n}\n\ninterface CacheEntry {\n promise: Promise<PageContent>;\n bytes: number; // 0 until the render resolves and its size is known\n}\n\nfunction isPdfDocument(src: PdfSrc): src is PdfDocumentLike {\n return (\n typeof src === 'object' &&\n src !== null &&\n 'numPages' in src &&\n typeof (src as PdfDocumentLike).getPage === 'function'\n );\n}\n\n/** Last path segment of a URL, for naming a download. Falls back when the URL has no filename\n * (a directory, a data: URI, or something unparseable). */\nfunction filenameFromUrl(url: string): string {\n try {\n const path = new URL(url, 'http://x').pathname;\n const last = path.split('/').filter(Boolean).pop();\n return last && /\\.[a-z0-9]+$/i.test(last) ? decodeURIComponent(last) : 'document.pdf';\n } catch {\n return 'document.pdf';\n }\n}\n\n/**\n * Load pdf.js. CDN / UMD hosts typically expose `globalThis.pdfjsLib` from a\n * `<script>` of pdf.js; bundlers have no global and resolve the dynamic import.\n */\nasync function loadPdfjs(): Promise<PdfjsModule> {\n const g = globalThis as typeof globalThis & { pdfjsLib?: PdfjsModule };\n if (g.pdfjsLib) return g.pdfjsLib;\n return (await import('pdfjs-dist')) as unknown as PdfjsModule;\n}\n\n/**\n * Renders PDF pages to canvases for zinejs. pdf.js is a dependency of this\n * package, loaded lazily so it never lands in the core bundle. Async — pass it\n * to `new Zine`, which calls `open()` (learning the page count) before building\n * spreads.\n */\nexport class PdfSource implements Source {\n pageCount = 0;\n #src: PdfSrc;\n #workerSrc: string | undefined;\n #renderScale: number;\n #preload: number;\n #doc: PdfDocumentLike | null = null;\n #maxCacheBytes: number;\n #progressive: boolean;\n #disableAutoFetch: boolean;\n #onProgress: ((progress: LoadProgress) => void) | null = null;\n #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\n #onUpdate: ((index: number) => void) | null = null;\n /** Zoomed page rasters keyed `index@scale`, apart from #cache: one is worth many normal pages\n * and would evict the whole spread from a byte cap sized for fit-to-screen. */\n #zoomCache = new Map<string, Promise<PageContent>>();\n\n /** Extracted page text, kept apart from #cache: text is scale-independent and negligible next\n * to a raster, so it has no place in the byte budget that evicts bitmaps. */\n #textCache = new Map<number, Promise<string>>();\n\n constructor(src: PdfSrc, options: PdfSourceOptions = {}) {\n this.#src = src;\n this.#workerSrc = options.workerSrc;\n this.#renderScale = options.renderScale ?? 1;\n this.#preload = options.preload ?? 1;\n this.#maxCacheBytes = options.maxCacheBytes ?? DEFAULT_MAX_CACHE_BYTES;\n this.#progressive = options.progressive ?? false;\n this.#disableAutoFetch = options.disableAutoFetch ?? false;\n }\n\n /** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */\n onPageUpdate(handler: (index: number) => void): void {\n this.#onUpdate = handler;\n }\n\n /** Register a handler called as the document downloads, driving a loading indicator. */\n onProgress(handler: (progress: LoadProgress) => void): void {\n this.#onProgress = handler;\n }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = await loadPdfjs();\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params: DocParams =\n typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\n if (this.#disableAutoFetch) params.disableAutoFetch = true;\n const task = pdfjs.getDocument(params);\n if (this.#onProgress) task.onProgress = this.#onProgress;\n this.#doc = await task.promise;\n }\n this.pageCount = this.#doc.numPages;\n }\n\n /** Precedence: an explicit `workerSrc` → an already-configured global → an auto-resolved default. */\n #resolveWorkerSrc(configured: string): string {\n if (this.#workerSrc) return this.#workerSrc;\n if (configured) return configured;\n try {\n // Bundlers (Vite / webpack 5 / esbuild) statically rewrite this and emit the worker,\n // so the common case needs no `workerSrc`. Non-bundler / UMD hosts pass it explicitly.\n return \"@@ZINE_PDF_WORKER_URL@@\";\n } catch {\n throw new Error(\n \"PdfSource: couldn't auto-resolve pdf.js's worker. Pass a `workerSrc` URL \" +\n '(see the @zinejs/pdf README, \"The pdf.js worker\"), set pdfjs ' +\n 'GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.',\n );\n }\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n // A zoomed request wants detail the fit-to-screen raster never had. PDF pages are vector, so\n // re-render at the magnification being viewed. Memoized per zoom level, not in the byte-capped\n // page cache: a zoomed page is many times the size of a normal one and would evict the whole\n // spread, and a reader who zooms out and back in should not pay to render it twice.\n if (opts && opts.scale > 1) return this.#renderZoomed(index, opts.scale);\n\n const decoded = this.#renderPage(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#renderPage(i).catch(() => {}); // prefetch failures surface on a real get()\n }\n }\n }\n\n /**\n * The page's text, for search. Empty for a page with no text layer (a scan, or a pdf.js build\n * without text extraction) — callers treat that as \"nothing to match\" rather than an error.\n */\n getText(index: number): Promise<string> {\n const cached = this.#textCache.get(index);\n if (cached) return cached;\n const pending = this.#extractText(index).catch(() => '');\n this.#textCache.set(index, pending);\n return pending;\n }\n\n /**\n * The document's table of contents, flattened into the shape the outline panel wants.\n *\n * Empty when the PDF has no outline, which is common. Destinations are resolved to page\n * indices here rather than in the UI: doing it once, up front, keeps clicking an entry\n * instant, and an entry whose destination cannot be resolved is kept with a null page so the\n * heading still shows.\n */\n async getOutline(): Promise<OutlineItem[]> {\n const doc = this.#doc;\n if (!doc || typeof doc.getOutline !== 'function') return [];\n let nodes: PdfOutlineNode[] | null;\n try {\n nodes = await doc.getOutline();\n } catch {\n return [];\n }\n if (!nodes?.length) return [];\n const convert = async (list: PdfOutlineNode[]): Promise<OutlineItem[]> =>\n Promise.all(\n list.map(async (node) => ({\n title: (node.title ?? '').trim() || 'Untitled',\n page: await this.#destinationPage(node.dest),\n children: node.items?.length ? await convert(node.items) : [],\n })),\n );\n return convert(nodes);\n }\n\n /** Resolve a pdf.js destination to a zero-based page index, or null if it cannot be. */\n async #destinationPage(dest: string | unknown[] | null | undefined): Promise<number | null> {\n const doc = this.#doc;\n if (!doc || dest == null) return null;\n try {\n // A named destination has to be looked up before it yields the usual explicit array.\n const explicit =\n typeof dest === 'string' ? await doc.getDestination?.(dest) : (dest as unknown[]);\n const ref = explicit?.[0];\n if (ref == null) return null;\n // Some destinations carry a bare page number instead of a reference.\n if (typeof ref === 'number') return ref;\n const index = await doc.getPageIndex?.(ref);\n return typeof index === 'number' ? index : null;\n } catch {\n return null; // a broken destination should not cost us the whole outline\n }\n }\n\n /**\n * The original PDF, for a download control.\n *\n * Null when the source was handed a pre-opened pdf.js document: the bytes belong to whoever\n * created it, and re-fetching them is not this class's call to make.\n */\n async getDownload(): Promise<DownloadInfo | null> {\n const src = this.#src;\n if (typeof src === 'string') {\n return { url: src, filename: filenameFromUrl(src) };\n }\n if (src instanceof ArrayBuffer || ArrayBuffer.isView(src)) {\n const blob = new Blob([src as BlobPart], { type: 'application/pdf' });\n return { url: URL.createObjectURL(blob), filename: 'document.pdf', revoke: true };\n }\n return null;\n }\n\n async #extractText(index: number): Promise<string> {\n if (this.#doc === null) {\n throw new Error('PdfSource: use after open() — the document is not loaded.');\n }\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n if (typeof page.getTextContent !== 'function') return '';\n const content = await page.getTextContent();\n let text = '';\n for (const item of content.items) {\n if (typeof item.str !== 'string') continue; // marked-content entries carry no text\n text += item.str;\n // pdf.js splits a visual line into runs; only hasEOL ends one.\n text += item.hasEOL ? '\\n' : '';\n }\n return text;\n }\n\n destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.clear();\n for (const pending of this.#zoomCache.values()) {\n void pending.then(releaseCanvas).catch(() => {});\n }\n this.#zoomCache.clear();\n this.#textCache.clear();\n this.#cachedBytes = 0;\n this.#doc = null;\n }\n\n #renderPage(index: number): Promise<PageContent> {\n const existing = this.#cache.get(index);\n if (existing) {\n // Mark most-recently-used: re-insert so it sits at the end of the Map's order.\n this.#cache.delete(index);\n this.#cache.set(index, existing);\n return existing.promise;\n }\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const full = this.#renderScale * dpr;\n const firstScale = this.#progressive ? full * PROGRESSIVE_LOW_RATIO : full;\n\n const entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index, firstScale).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n if (this.#progressive) this.#upgrade(index, entry, full);\n return content;\n },\n (error: unknown) => {\n this.#cache.delete(index); // let a later get() retry\n throw error;\n },\n );\n this.#cache.set(index, entry);\n return entry.promise;\n }\n\n /** Progressive: re-render at full resolution, swap it into the cache, and signal an update. */\n #upgrade(index: number, entry: CacheEntry, fullScale: number): void {\n void this.#rasterize(index, fullScale)\n .then((hi) => {\n if (this.#cache.get(index) !== entry) return; // evicted or replaced meanwhile\n this.#cachedBytes -= entry.bytes;\n entry.promise = Promise.resolve(hi);\n entry.bytes = hi.width * hi.height * 4;\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n this.#onUpdate?.(index);\n })\n .catch(() => {}); // keep the low-res if the upgrade fails\n }\n\n /** Drop least-recently-used pages (front of the Map) until under the byte cap. */\n #evictToFit(keepIndex: number): void {\n for (const [index, entry] of this.#cache) {\n if (this.#cachedBytes <= this.#maxCacheBytes) break;\n if (index === keepIndex || entry.bytes === 0) continue; // keep the newest; skip in-flight renders\n this.#cache.delete(index);\n this.#cachedBytes -= entry.bytes;\n }\n }\n\n /**\n * Render a whole page at zoom magnification, memoized by page and zoom level.\n *\n * Whole-page rather than a crop of what is visible: the renderer draws a page as one texture, so\n * a partial raster cannot be handed to it, and a crop would have to be re-rendered on every pan\n * anyway. Held apart from the byte-capped page cache, which is sized for fit-to-screen rasters.\n */\n #renderZoomed(index: number, zoom: number): Promise<PageContent> {\n const scale = Math.min(zoom, MAX_ZOOM_RENDER_SCALE);\n const key = `${index}@${scale}`;\n const cached = this.#zoomCache.get(key);\n if (cached) return cached;\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const promise = this.#rasterize(index, this.#renderScale * dpr * scale).catch(\n (error: unknown) => {\n this.#zoomCache.delete(key); // let a later zoom retry\n throw error;\n },\n );\n // One zoom level at a time, but both pages of the spread at that level.\n for (const [k, pending] of this.#zoomCache) {\n if (!k.endsWith(`@${scale}`)) {\n this.#zoomCache.delete(k);\n void pending.then(releaseCanvas).catch(() => {});\n }\n }\n this.#zoomCache.set(key, promise);\n return promise;\n }\n\n async #rasterize(index: number, scale: number): Promise<PageContent> {\n if (this.#doc === null) throw new Error('PdfSource: use after open() — the document is not loaded.');\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n const viewport = page.getViewport({ scale });\n const canvas = document.createElement('canvas');\n canvas.width = Math.ceil(viewport.width);\n canvas.height = Math.ceil(viewport.height);\n await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;\n return canvas;\n }\n}\n"],"mappings":"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"}
|
package/dist/index.umd.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.ZineJS=e.ZineJS||{}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=256*1024*1024,n=.35,r=6;function i(e){return typeof e==`object`&&!!e&&`numPages`in e&&typeof e.getPage==`function`}function
|
|
2
|
-
`:``);return r}destroy(){this.#i?.destroy?.(),this.#
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.ZineJS=e.ZineJS||{}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=256*1024*1024,n=.35,r=6;function i(e){typeof HTMLCanvasElement<`u`&&e instanceof HTMLCanvasElement&&(e.width=0,e.height=0)}function a(e){return typeof e==`object`&&!!e&&`numPages`in e&&typeof e.getPage==`function`}function o(e){try{let t=new URL(e,`http://x`).pathname.split(`/`).filter(Boolean).pop();return t&&/\.[a-z0-9]+$/i.test(t)?decodeURIComponent(t):`document.pdf`}catch{return`document.pdf`}}async function s(){let e=globalThis;return e.pdfjsLib?e.pdfjsLib:await import(`pdfjs-dist`)}e.PdfSource=class{pageCount=0;#e;#t;#n;#r;#i=null;#a;#o;#s;#c=null;#l=new Map;#u=0;#d=null;#f=new Map;#p=new Map;constructor(e,n={}){this.#e=e,this.#t=n.workerSrc,this.#n=n.renderScale??1,this.#r=n.preload??1,this.#a=n.maxCacheBytes??t,this.#o=n.progressive??!1,this.#s=n.disableAutoFetch??!1}onPageUpdate(e){this.#d=e}onProgress(e){this.#c=e}async open(){if(a(this.#e))this.#i=this.#e;else{let e=await s();e.GlobalWorkerOptions.workerSrc=this.#m(e.GlobalWorkerOptions.workerSrc);let t=typeof this.#e==`string`?{url:this.#e}:{data:this.#e};this.#s&&(t.disableAutoFetch=!0);let n=e.getDocument(t);this.#c&&(n.onProgress=this.#c),this.#i=await n.promise}this.pageCount=this.#i.numPages}#m(e){if(this.#t)return this.#t;if(e)return e;try{return(() => { throw new Error('PdfSource: pass workerSrc for UMD/CDN'); })()}catch{throw Error(`PdfSource: couldn't auto-resolve pdf.js's worker. Pass a \`workerSrc\` URL (see the @zinejs/pdf README, "The pdf.js worker"), set pdfjs GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.`)}}get(e,t){if(t&&t.scale>1)return this.#b(e,t.scale);let n=this.#_(e);for(let t=1;t<=this.#r;t++)this.prefetch([e-t,e+t]);return n}prefetch(e){for(let t of e)t>=0&&t<this.pageCount&&this.#_(t).catch(()=>{})}getText(e){let t=this.#p.get(e);if(t)return t;let n=this.#g(e).catch(()=>``);return this.#p.set(e,n),n}async getOutline(){let e=this.#i;if(!e||typeof e.getOutline!=`function`)return[];let t;try{t=await e.getOutline()}catch{return[]}if(!t?.length)return[];let n=async e=>Promise.all(e.map(async e=>({title:(e.title??``).trim()||`Untitled`,page:await this.#h(e.dest),children:e.items?.length?await n(e.items):[]})));return n(t)}async#h(e){let t=this.#i;if(!t||e==null)return null;try{let n=(typeof e==`string`?await t.getDestination?.(e):e)?.[0];if(n==null)return null;if(typeof n==`number`)return n;let r=await t.getPageIndex?.(n);return typeof r==`number`?r:null}catch{return null}}async getDownload(){let e=this.#e;if(typeof e==`string`)return{url:e,filename:o(e)};if(e instanceof ArrayBuffer||ArrayBuffer.isView(e)){let t=new Blob([e],{type:`application/pdf`});return{url:URL.createObjectURL(t),filename:`document.pdf`,revoke:!0}}return null}async#g(e){if(this.#i===null)throw Error(`PdfSource: use after open() — the document is not loaded.`);let t=await this.#i.getPage(e+1);if(typeof t.getTextContent!=`function`)return``;let n=await t.getTextContent(),r=``;for(let e of n.items)typeof e.str==`string`&&(r+=e.str,r+=e.hasEOL?`
|
|
2
|
+
`:``);return r}destroy(){this.#i?.destroy?.(),this.#l.clear();for(let e of this.#f.values())e.then(i).catch(()=>{});this.#f.clear(),this.#p.clear(),this.#u=0,this.#i=null}#_(e){let t=this.#l.get(e);if(t)return this.#l.delete(e),this.#l.set(e,t),t.promise;let r=typeof devicePixelRatio==`number`?devicePixelRatio:1,i=this.#n*r,a=this.#o?i*n:i,o={promise:void 0,bytes:0};return o.promise=this.#x(e,a).then(t=>(o.bytes=t.width*t.height*4,this.#u+=o.bytes,this.#y(e),this.#o&&this.#v(e,o,i),t),t=>{throw this.#l.delete(e),t}),this.#l.set(e,o),o.promise}#v(e,t,n){this.#x(e,n).then(n=>{this.#l.get(e)===t&&(this.#u-=t.bytes,t.promise=Promise.resolve(n),t.bytes=n.width*n.height*4,this.#u+=t.bytes,this.#y(e),this.#d?.(e))}).catch(()=>{})}#y(e){for(let[t,n]of this.#l){if(this.#u<=this.#a)break;t===e||n.bytes===0||(this.#l.delete(t),this.#u-=n.bytes)}}#b(e,t){let n=Math.min(t,r),a=`${e}@${n}`,o=this.#f.get(a);if(o)return o;let s=typeof devicePixelRatio==`number`?devicePixelRatio:1,c=this.#x(e,this.#n*s*n).catch(e=>{throw this.#f.delete(a),e});for(let[e,t]of this.#f)e.endsWith(`@${n}`)||(this.#f.delete(e),t.then(i).catch(()=>{}));return this.#f.set(a,c),c}async#x(e,t){if(this.#i===null)throw Error(`PdfSource: use after open() — the document is not loaded.`);let n=await this.#i.getPage(e+1),r=n.getViewport({scale:t}),i=document.createElement(`canvas`);return i.width=Math.ceil(r.width),i.height=Math.ceil(r.height),await n.render({canvasContext:i.getContext(`2d`),viewport:r}).promise,i}}});
|
|
3
3
|
//# sourceMappingURL=index.umd.js.map
|
package/dist/index.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.umd.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#progressive","#disableAutoFetch","#onUpdate","#doc","#resolveWorkerSrc","#renderTile","#renderPage","#textCache","#extractText","#destinationPage","#cache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type { DownloadInfo, OutlineItem, PageContent, PageRequest, Source } from '@zinejs/core';\n\n/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */\ninterface PdfPageLike {\n getViewport(opts: { scale: number }): { width: number; height: number };\n /** `transform` is an affine matrix applied before painting, used to offset a cropped tile. */\n render(opts: { canvasContext: unknown; viewport: unknown; transform?: number[] }): {\n promise: Promise<void>;\n };\n /** Optional: absent on scanned PDFs with no text layer, and on hand-rolled document stubs. */\n getTextContent?(): Promise<{ items: { str?: string; hasEOL?: boolean }[] }>;\n}\n/** A pdf.js outline entry. `dest` is either a named destination or an explicit array whose first\n * element references the target page. */\ninterface PdfOutlineNode {\n title?: string;\n dest?: string | unknown[] | null;\n items?: PdfOutlineNode[];\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\n /** Optional: absent on documents without a table of contents, and on hand-rolled stubs. */\n getOutline?(): Promise<PdfOutlineNode[] | null>;\n getDestination?(id: string): Promise<unknown[] | null>;\n getPageIndex?(ref: unknown): Promise<number>;\n}\ninterface DocParams {\n url?: string;\n data?: ArrayBuffer | Uint8Array;\n disableAutoFetch?: boolean;\n}\ninterface PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: DocParams): { promise: Promise<PdfDocumentLike> };\n}\n\n/** A URL, raw bytes, or a pre-created pdf.js document. */\nexport type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;\n\nexport interface PdfSourceOptions {\n /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */\n workerSrc?: string;\n /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */\n renderScale?: number;\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */\n maxCacheBytes?: number;\n /** Paint a low-res page first, then swap to crisp (faster first paint). Default false. */\n progressive?: boolean;\n /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */\n disableAutoFetch?: boolean;\n}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\n// Linear-resolution fraction for the progressive low-res first pass (~1/9 the pixels).\nconst PROGRESSIVE_LOW_RATIO = 0.35;\n// Ceiling on zoom-tile magnification. Past roughly this point a screen pixel already holds more\n// detail than the reader can resolve, and rasterizing further only costs memory and time.\nconst MAX_ZOOM_RENDER_SCALE = 6;\n\ninterface CacheEntry {\n promise: Promise<PageContent>;\n bytes: number; // 0 until the render resolves and its size is known\n}\n\nfunction isPdfDocument(src: PdfSrc): src is PdfDocumentLike {\n return (\n typeof src === 'object' &&\n src !== null &&\n 'numPages' in src &&\n typeof (src as PdfDocumentLike).getPage === 'function'\n );\n}\n\n/** Last path segment of a URL, for naming a download. Falls back when the URL has no filename\n * (a directory, a data: URI, or something unparseable). */\nfunction filenameFromUrl(url: string): string {\n try {\n const path = new URL(url, 'http://x').pathname;\n const last = path.split('/').filter(Boolean).pop();\n return last && /\\.[a-z0-9]+$/i.test(last) ? decodeURIComponent(last) : 'document.pdf';\n } catch {\n return 'document.pdf';\n }\n}\n\n/**\n * Load pdf.js. CDN / UMD hosts typically expose `globalThis.pdfjsLib` from a\n * `<script>` of pdf.js; bundlers have no global and resolve the dynamic import.\n */\nasync function loadPdfjs(): Promise<PdfjsModule> {\n const g = globalThis as typeof globalThis & { pdfjsLib?: PdfjsModule };\n if (g.pdfjsLib) return g.pdfjsLib;\n return (await import('pdfjs-dist')) as unknown as PdfjsModule;\n}\n\n/**\n * Renders PDF pages to canvases for zinejs. pdf.js is a dependency of this\n * package, loaded lazily so it never lands in the core bundle. Async — pass it\n * to `new Zine`, which calls `open()` (learning the page count) before building\n * spreads.\n */\nexport class PdfSource implements Source {\n pageCount = 0;\n #src: PdfSrc;\n #workerSrc: string | undefined;\n #renderScale: number;\n #preload: number;\n #doc: PdfDocumentLike | null = null;\n #maxCacheBytes: number;\n #progressive: boolean;\n #disableAutoFetch: boolean;\n #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\n #onUpdate: ((index: number) => void) | null = null;\n /** Extracted page text, kept apart from #cache: text is scale-independent and negligible next\n * to a raster, so it has no place in the byte budget that evicts bitmaps. */\n #textCache = new Map<number, Promise<string>>();\n\n constructor(src: PdfSrc, options: PdfSourceOptions = {}) {\n this.#src = src;\n this.#workerSrc = options.workerSrc;\n this.#renderScale = options.renderScale ?? 1;\n this.#preload = options.preload ?? 1;\n this.#maxCacheBytes = options.maxCacheBytes ?? DEFAULT_MAX_CACHE_BYTES;\n this.#progressive = options.progressive ?? false;\n this.#disableAutoFetch = options.disableAutoFetch ?? false;\n }\n\n /** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */\n onPageUpdate(handler: (index: number) => void): void {\n this.#onUpdate = handler;\n }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = await loadPdfjs();\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params: DocParams =\n typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\n if (this.#disableAutoFetch) params.disableAutoFetch = true;\n this.#doc = await pdfjs.getDocument(params).promise;\n }\n this.pageCount = this.#doc.numPages;\n }\n\n /** Precedence: an explicit `workerSrc` → an already-configured global → an auto-resolved default. */\n #resolveWorkerSrc(configured: string): string {\n if (this.#workerSrc) return this.#workerSrc;\n if (configured) return configured;\n try {\n // Bundlers (Vite / webpack 5 / esbuild) statically rewrite this and emit the worker,\n // so the common case needs no `workerSrc`. Non-bundler / UMD hosts pass it explicitly.\n return \"@@ZINE_PDF_WORKER_URL@@\";\n } catch {\n throw new Error(\n \"PdfSource: couldn't auto-resolve pdf.js's worker. Pass a `workerSrc` URL \" +\n '(see the @zinejs/pdf README, \"The pdf.js worker\"), set pdfjs ' +\n 'GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.',\n );\n }\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n // A zoomed request wants detail the fit-to-screen raster never had. PDF pages are vector, so\n // re-render the visible part at the magnification being viewed. Deliberately uncached: the\n // region changes as the reader pans, and these rasters are far too large to keep around.\n if (opts && opts.scale > 1) return this.#renderTile(index, opts);\n\n const decoded = this.#renderPage(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#renderPage(i).catch(() => {}); // prefetch failures surface on a real get()\n }\n }\n }\n\n /**\n * The page's text, for search. Empty for a page with no text layer (a scan, or a pdf.js build\n * without text extraction) — callers treat that as \"nothing to match\" rather than an error.\n */\n getText(index: number): Promise<string> {\n const cached = this.#textCache.get(index);\n if (cached) return cached;\n const pending = this.#extractText(index).catch(() => '');\n this.#textCache.set(index, pending);\n return pending;\n }\n\n /**\n * The document's table of contents, flattened into the shape the outline panel wants.\n *\n * Empty when the PDF has no outline, which is common. Destinations are resolved to page\n * indices here rather than in the UI: doing it once, up front, keeps clicking an entry\n * instant, and an entry whose destination cannot be resolved is kept with a null page so the\n * heading still shows.\n */\n async getOutline(): Promise<OutlineItem[]> {\n const doc = this.#doc;\n if (!doc || typeof doc.getOutline !== 'function') return [];\n let nodes: PdfOutlineNode[] | null;\n try {\n nodes = await doc.getOutline();\n } catch {\n return [];\n }\n if (!nodes?.length) return [];\n const convert = async (list: PdfOutlineNode[]): Promise<OutlineItem[]> =>\n Promise.all(\n list.map(async (node) => ({\n title: (node.title ?? '').trim() || 'Untitled',\n page: await this.#destinationPage(node.dest),\n children: node.items?.length ? await convert(node.items) : [],\n })),\n );\n return convert(nodes);\n }\n\n /** Resolve a pdf.js destination to a zero-based page index, or null if it cannot be. */\n async #destinationPage(dest: string | unknown[] | null | undefined): Promise<number | null> {\n const doc = this.#doc;\n if (!doc || dest == null) return null;\n try {\n // A named destination has to be looked up before it yields the usual explicit array.\n const explicit =\n typeof dest === 'string' ? await doc.getDestination?.(dest) : (dest as unknown[]);\n const ref = explicit?.[0];\n if (ref == null) return null;\n // Some destinations carry a bare page number instead of a reference.\n if (typeof ref === 'number') return ref;\n const index = await doc.getPageIndex?.(ref);\n return typeof index === 'number' ? index : null;\n } catch {\n return null; // a broken destination should not cost us the whole outline\n }\n }\n\n /**\n * The original PDF, for a download control.\n *\n * Null when the source was handed a pre-opened pdf.js document: the bytes belong to whoever\n * created it, and re-fetching them is not this class's call to make.\n */\n async getDownload(): Promise<DownloadInfo | null> {\n const src = this.#src;\n if (typeof src === 'string') {\n return { url: src, filename: filenameFromUrl(src) };\n }\n if (src instanceof ArrayBuffer || ArrayBuffer.isView(src)) {\n const blob = new Blob([src as BlobPart], { type: 'application/pdf' });\n return { url: URL.createObjectURL(blob), filename: 'document.pdf', revoke: true };\n }\n return null;\n }\n\n async #extractText(index: number): Promise<string> {\n if (this.#doc === null) {\n throw new Error('PdfSource: use after open() — the document is not loaded.');\n }\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n if (typeof page.getTextContent !== 'function') return '';\n const content = await page.getTextContent();\n let text = '';\n for (const item of content.items) {\n if (typeof item.str !== 'string') continue; // marked-content entries carry no text\n text += item.str;\n // pdf.js splits a visual line into runs; only hasEOL ends one.\n text += item.hasEOL ? '\\n' : '';\n }\n return text;\n }\n\n destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.clear();\n this.#textCache.clear();\n this.#cachedBytes = 0;\n this.#doc = null;\n }\n\n #renderPage(index: number): Promise<PageContent> {\n const existing = this.#cache.get(index);\n if (existing) {\n // Mark most-recently-used: re-insert so it sits at the end of the Map's order.\n this.#cache.delete(index);\n this.#cache.set(index, existing);\n return existing.promise;\n }\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const full = this.#renderScale * dpr;\n const firstScale = this.#progressive ? full * PROGRESSIVE_LOW_RATIO : full;\n\n const entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index, firstScale).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n if (this.#progressive) this.#upgrade(index, entry, full);\n return content;\n },\n (error: unknown) => {\n this.#cache.delete(index); // let a later get() retry\n throw error;\n },\n );\n this.#cache.set(index, entry);\n return entry.promise;\n }\n\n /** Progressive: re-render at full resolution, swap it into the cache, and signal an update. */\n #upgrade(index: number, entry: CacheEntry, fullScale: number): void {\n void this.#rasterize(index, fullScale)\n .then((hi) => {\n if (this.#cache.get(index) !== entry) return; // evicted or replaced meanwhile\n this.#cachedBytes -= entry.bytes;\n entry.promise = Promise.resolve(hi);\n entry.bytes = hi.width * hi.height * 4;\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n this.#onUpdate?.(index);\n })\n .catch(() => {}); // keep the low-res if the upgrade fails\n }\n\n /** Drop least-recently-used pages (front of the Map) until under the byte cap. */\n #evictToFit(keepIndex: number): void {\n for (const [index, entry] of this.#cache) {\n if (this.#cachedBytes <= this.#maxCacheBytes) break;\n if (index === keepIndex || entry.bytes === 0) continue; // keep the newest; skip in-flight renders\n this.#cache.delete(index);\n this.#cachedBytes -= entry.bytes;\n }\n }\n\n /**\n * Render just the visible part of a page at zoom magnification.\n *\n * The tile covers `region` of the page and is sized to the pixels actually on screen, so its\n * cost stays flat however far the reader zooms in — a full-page re-render would grow with the\n * square of the scale and dwarf the whole page cache.\n */\n async #renderTile(index: number, req: PageRequest): Promise<PageContent> {\n if (this.#doc === null) throw new Error('PdfSource: use after open() — the document is not loaded.');\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const base = this.#renderScale * dpr;\n // The tile is only ever as many pixels as the region occupies on screen, so clamping the\n // scale bounds its size no matter how deep the zoom goes.\n const scale = base * Math.min(req.scale, MAX_ZOOM_RENDER_SCALE);\n const full = page.getViewport({ scale });\n\n const r = req.region;\n const canvas = document.createElement('canvas');\n canvas.width = Math.max(1, Math.ceil(full.width * r.width));\n canvas.height = Math.max(1, Math.ceil(full.height * r.height));\n // Shift the page under the canvas so the region lands at the origin; pdf.js has no crop of\n // its own, and rendering full-size into a small canvas would just clip the top-left corner.\n const transform = [1, 0, 0, 1, -full.width * r.x, -full.height * r.y];\n await page.render({ canvasContext: canvas.getContext('2d'), viewport: full, transform }).promise;\n return canvas;\n }\n\n async #rasterize(index: number, scale: number): Promise<PageContent> {\n if (this.#doc === null) throw new Error('PdfSource: use after open() — the document is not loaded.');\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n const viewport = page.getViewport({ scale });\n const canvas = document.createElement('canvas');\n canvas.width = Math.ceil(viewport.width);\n canvas.height = Math.ceil(viewport.height);\n await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;\n return canvas;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.umd.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#progressive","#disableAutoFetch","#onUpdate","#onProgress","#doc","#resolveWorkerSrc","#renderZoomed","#renderPage","#textCache","#extractText","#destinationPage","#cache","#zoomCache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type {\n DownloadInfo,\n LoadProgress,\n OutlineItem,\n PageContent,\n PageRequest,\n Source,\n} from '@zinejs/core';\n\n/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */\ninterface PdfPageLike {\n getViewport(opts: { scale: number }): { width: number; height: number };\n /** `transform` is an affine matrix applied before painting, used to offset a cropped tile. */\n render(opts: { canvasContext: unknown; viewport: unknown; transform?: number[] }): {\n promise: Promise<void>;\n };\n /** Optional: absent on scanned PDFs with no text layer, and on hand-rolled document stubs. */\n getTextContent?(): Promise<{ items: { str?: string; hasEOL?: boolean }[] }>;\n}\n/** A pdf.js outline entry. `dest` is either a named destination or an explicit array whose first\n * element references the target page. */\ninterface PdfOutlineNode {\n title?: string;\n dest?: string | unknown[] | null;\n items?: PdfOutlineNode[];\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\n /** Optional: absent on documents without a table of contents, and on hand-rolled stubs. */\n getOutline?(): Promise<PdfOutlineNode[] | null>;\n getDestination?(id: string): Promise<unknown[] | null>;\n getPageIndex?(ref: unknown): Promise<number>;\n}\ninterface DocParams {\n url?: string;\n data?: ArrayBuffer | Uint8Array;\n disableAutoFetch?: boolean;\n}\n/** pdf.js's loading task: the document promise, plus an assignable download-progress callback. */\ninterface PdfLoadingTask {\n promise: Promise<PdfDocumentLike>;\n onProgress?: (p: LoadProgress) => void;\n}\ninterface PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: DocParams): PdfLoadingTask;\n}\n\n/** A URL, raw bytes, or a pre-created pdf.js document. */\nexport type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;\n\nexport interface PdfSourceOptions {\n /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */\n workerSrc?: string;\n /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */\n renderScale?: number;\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */\n maxCacheBytes?: number;\n /** Paint a low-res page first, then swap to crisp (faster first paint). Default false. */\n progressive?: boolean;\n /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */\n disableAutoFetch?: boolean;\n}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\n// Linear-resolution fraction for the progressive low-res first pass (~1/9 the pixels).\nconst PROGRESSIVE_LOW_RATIO = 0.35;\n// Ceiling on zoom-tile magnification. Past roughly this point a screen pixel already holds more\n// detail than the reader can resolve, and rasterizing further only costs memory and time.\nconst MAX_ZOOM_RENDER_SCALE = 6;\n\n/** Free a canvas's backing store. Safari in particular holds the memory until the surface is\n * resized away, and a zoomed page is large enough for that to matter. */\nfunction releaseCanvas(content: PageContent): void {\n if (typeof HTMLCanvasElement !== 'undefined' && content instanceof HTMLCanvasElement) {\n content.width = 0;\n content.height = 0;\n }\n}\n\ninterface CacheEntry {\n promise: Promise<PageContent>;\n bytes: number; // 0 until the render resolves and its size is known\n}\n\nfunction isPdfDocument(src: PdfSrc): src is PdfDocumentLike {\n return (\n typeof src === 'object' &&\n src !== null &&\n 'numPages' in src &&\n typeof (src as PdfDocumentLike).getPage === 'function'\n );\n}\n\n/** Last path segment of a URL, for naming a download. Falls back when the URL has no filename\n * (a directory, a data: URI, or something unparseable). */\nfunction filenameFromUrl(url: string): string {\n try {\n const path = new URL(url, 'http://x').pathname;\n const last = path.split('/').filter(Boolean).pop();\n return last && /\\.[a-z0-9]+$/i.test(last) ? decodeURIComponent(last) : 'document.pdf';\n } catch {\n return 'document.pdf';\n }\n}\n\n/**\n * Load pdf.js. CDN / UMD hosts typically expose `globalThis.pdfjsLib` from a\n * `<script>` of pdf.js; bundlers have no global and resolve the dynamic import.\n */\nasync function loadPdfjs(): Promise<PdfjsModule> {\n const g = globalThis as typeof globalThis & { pdfjsLib?: PdfjsModule };\n if (g.pdfjsLib) return g.pdfjsLib;\n return (await import('pdfjs-dist')) as unknown as PdfjsModule;\n}\n\n/**\n * Renders PDF pages to canvases for zinejs. pdf.js is a dependency of this\n * package, loaded lazily so it never lands in the core bundle. Async — pass it\n * to `new Zine`, which calls `open()` (learning the page count) before building\n * spreads.\n */\nexport class PdfSource implements Source {\n pageCount = 0;\n #src: PdfSrc;\n #workerSrc: string | undefined;\n #renderScale: number;\n #preload: number;\n #doc: PdfDocumentLike | null = null;\n #maxCacheBytes: number;\n #progressive: boolean;\n #disableAutoFetch: boolean;\n #onProgress: ((progress: LoadProgress) => void) | null = null;\n #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\n #onUpdate: ((index: number) => void) | null = null;\n /** Zoomed page rasters keyed `index@scale`, apart from #cache: one is worth many normal pages\n * and would evict the whole spread from a byte cap sized for fit-to-screen. */\n #zoomCache = new Map<string, Promise<PageContent>>();\n\n /** Extracted page text, kept apart from #cache: text is scale-independent and negligible next\n * to a raster, so it has no place in the byte budget that evicts bitmaps. */\n #textCache = new Map<number, Promise<string>>();\n\n constructor(src: PdfSrc, options: PdfSourceOptions = {}) {\n this.#src = src;\n this.#workerSrc = options.workerSrc;\n this.#renderScale = options.renderScale ?? 1;\n this.#preload = options.preload ?? 1;\n this.#maxCacheBytes = options.maxCacheBytes ?? DEFAULT_MAX_CACHE_BYTES;\n this.#progressive = options.progressive ?? false;\n this.#disableAutoFetch = options.disableAutoFetch ?? false;\n }\n\n /** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */\n onPageUpdate(handler: (index: number) => void): void {\n this.#onUpdate = handler;\n }\n\n /** Register a handler called as the document downloads, driving a loading indicator. */\n onProgress(handler: (progress: LoadProgress) => void): void {\n this.#onProgress = handler;\n }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = await loadPdfjs();\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params: DocParams =\n typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\n if (this.#disableAutoFetch) params.disableAutoFetch = true;\n const task = pdfjs.getDocument(params);\n if (this.#onProgress) task.onProgress = this.#onProgress;\n this.#doc = await task.promise;\n }\n this.pageCount = this.#doc.numPages;\n }\n\n /** Precedence: an explicit `workerSrc` → an already-configured global → an auto-resolved default. */\n #resolveWorkerSrc(configured: string): string {\n if (this.#workerSrc) return this.#workerSrc;\n if (configured) return configured;\n try {\n // Bundlers (Vite / webpack 5 / esbuild) statically rewrite this and emit the worker,\n // so the common case needs no `workerSrc`. Non-bundler / UMD hosts pass it explicitly.\n return \"@@ZINE_PDF_WORKER_URL@@\";\n } catch {\n throw new Error(\n \"PdfSource: couldn't auto-resolve pdf.js's worker. Pass a `workerSrc` URL \" +\n '(see the @zinejs/pdf README, \"The pdf.js worker\"), set pdfjs ' +\n 'GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.',\n );\n }\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n // A zoomed request wants detail the fit-to-screen raster never had. PDF pages are vector, so\n // re-render at the magnification being viewed. Memoized per zoom level, not in the byte-capped\n // page cache: a zoomed page is many times the size of a normal one and would evict the whole\n // spread, and a reader who zooms out and back in should not pay to render it twice.\n if (opts && opts.scale > 1) return this.#renderZoomed(index, opts.scale);\n\n const decoded = this.#renderPage(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#renderPage(i).catch(() => {}); // prefetch failures surface on a real get()\n }\n }\n }\n\n /**\n * The page's text, for search. Empty for a page with no text layer (a scan, or a pdf.js build\n * without text extraction) — callers treat that as \"nothing to match\" rather than an error.\n */\n getText(index: number): Promise<string> {\n const cached = this.#textCache.get(index);\n if (cached) return cached;\n const pending = this.#extractText(index).catch(() => '');\n this.#textCache.set(index, pending);\n return pending;\n }\n\n /**\n * The document's table of contents, flattened into the shape the outline panel wants.\n *\n * Empty when the PDF has no outline, which is common. Destinations are resolved to page\n * indices here rather than in the UI: doing it once, up front, keeps clicking an entry\n * instant, and an entry whose destination cannot be resolved is kept with a null page so the\n * heading still shows.\n */\n async getOutline(): Promise<OutlineItem[]> {\n const doc = this.#doc;\n if (!doc || typeof doc.getOutline !== 'function') return [];\n let nodes: PdfOutlineNode[] | null;\n try {\n nodes = await doc.getOutline();\n } catch {\n return [];\n }\n if (!nodes?.length) return [];\n const convert = async (list: PdfOutlineNode[]): Promise<OutlineItem[]> =>\n Promise.all(\n list.map(async (node) => ({\n title: (node.title ?? '').trim() || 'Untitled',\n page: await this.#destinationPage(node.dest),\n children: node.items?.length ? await convert(node.items) : [],\n })),\n );\n return convert(nodes);\n }\n\n /** Resolve a pdf.js destination to a zero-based page index, or null if it cannot be. */\n async #destinationPage(dest: string | unknown[] | null | undefined): Promise<number | null> {\n const doc = this.#doc;\n if (!doc || dest == null) return null;\n try {\n // A named destination has to be looked up before it yields the usual explicit array.\n const explicit =\n typeof dest === 'string' ? await doc.getDestination?.(dest) : (dest as unknown[]);\n const ref = explicit?.[0];\n if (ref == null) return null;\n // Some destinations carry a bare page number instead of a reference.\n if (typeof ref === 'number') return ref;\n const index = await doc.getPageIndex?.(ref);\n return typeof index === 'number' ? index : null;\n } catch {\n return null; // a broken destination should not cost us the whole outline\n }\n }\n\n /**\n * The original PDF, for a download control.\n *\n * Null when the source was handed a pre-opened pdf.js document: the bytes belong to whoever\n * created it, and re-fetching them is not this class's call to make.\n */\n async getDownload(): Promise<DownloadInfo | null> {\n const src = this.#src;\n if (typeof src === 'string') {\n return { url: src, filename: filenameFromUrl(src) };\n }\n if (src instanceof ArrayBuffer || ArrayBuffer.isView(src)) {\n const blob = new Blob([src as BlobPart], { type: 'application/pdf' });\n return { url: URL.createObjectURL(blob), filename: 'document.pdf', revoke: true };\n }\n return null;\n }\n\n async #extractText(index: number): Promise<string> {\n if (this.#doc === null) {\n throw new Error('PdfSource: use after open() — the document is not loaded.');\n }\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n if (typeof page.getTextContent !== 'function') return '';\n const content = await page.getTextContent();\n let text = '';\n for (const item of content.items) {\n if (typeof item.str !== 'string') continue; // marked-content entries carry no text\n text += item.str;\n // pdf.js splits a visual line into runs; only hasEOL ends one.\n text += item.hasEOL ? '\\n' : '';\n }\n return text;\n }\n\n destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.clear();\n for (const pending of this.#zoomCache.values()) {\n void pending.then(releaseCanvas).catch(() => {});\n }\n this.#zoomCache.clear();\n this.#textCache.clear();\n this.#cachedBytes = 0;\n this.#doc = null;\n }\n\n #renderPage(index: number): Promise<PageContent> {\n const existing = this.#cache.get(index);\n if (existing) {\n // Mark most-recently-used: re-insert so it sits at the end of the Map's order.\n this.#cache.delete(index);\n this.#cache.set(index, existing);\n return existing.promise;\n }\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const full = this.#renderScale * dpr;\n const firstScale = this.#progressive ? full * PROGRESSIVE_LOW_RATIO : full;\n\n const entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index, firstScale).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n if (this.#progressive) this.#upgrade(index, entry, full);\n return content;\n },\n (error: unknown) => {\n this.#cache.delete(index); // let a later get() retry\n throw error;\n },\n );\n this.#cache.set(index, entry);\n return entry.promise;\n }\n\n /** Progressive: re-render at full resolution, swap it into the cache, and signal an update. */\n #upgrade(index: number, entry: CacheEntry, fullScale: number): void {\n void this.#rasterize(index, fullScale)\n .then((hi) => {\n if (this.#cache.get(index) !== entry) return; // evicted or replaced meanwhile\n this.#cachedBytes -= entry.bytes;\n entry.promise = Promise.resolve(hi);\n entry.bytes = hi.width * hi.height * 4;\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n this.#onUpdate?.(index);\n })\n .catch(() => {}); // keep the low-res if the upgrade fails\n }\n\n /** Drop least-recently-used pages (front of the Map) until under the byte cap. */\n #evictToFit(keepIndex: number): void {\n for (const [index, entry] of this.#cache) {\n if (this.#cachedBytes <= this.#maxCacheBytes) break;\n if (index === keepIndex || entry.bytes === 0) continue; // keep the newest; skip in-flight renders\n this.#cache.delete(index);\n this.#cachedBytes -= entry.bytes;\n }\n }\n\n /**\n * Render a whole page at zoom magnification, memoized by page and zoom level.\n *\n * Whole-page rather than a crop of what is visible: the renderer draws a page as one texture, so\n * a partial raster cannot be handed to it, and a crop would have to be re-rendered on every pan\n * anyway. Held apart from the byte-capped page cache, which is sized for fit-to-screen rasters.\n */\n #renderZoomed(index: number, zoom: number): Promise<PageContent> {\n const scale = Math.min(zoom, MAX_ZOOM_RENDER_SCALE);\n const key = `${index}@${scale}`;\n const cached = this.#zoomCache.get(key);\n if (cached) return cached;\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const promise = this.#rasterize(index, this.#renderScale * dpr * scale).catch(\n (error: unknown) => {\n this.#zoomCache.delete(key); // let a later zoom retry\n throw error;\n },\n );\n // One zoom level at a time, but both pages of the spread at that level.\n for (const [k, pending] of this.#zoomCache) {\n if (!k.endsWith(`@${scale}`)) {\n this.#zoomCache.delete(k);\n void pending.then(releaseCanvas).catch(() => {});\n }\n }\n this.#zoomCache.set(key, promise);\n return promise;\n }\n\n async #rasterize(index: number, scale: number): Promise<PageContent> {\n if (this.#doc === null) throw new Error('PdfSource: use after open() — the document is not loaded.');\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n const viewport = page.getViewport({ scale });\n const canvas = document.createElement('canvas');\n canvas.width = Math.ceil(viewport.width);\n canvas.height = Math.ceil(viewport.height);\n await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;\n return canvas;\n }\n}\n"],"mappings":"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"}
|
package/dist/pdfSource.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DownloadInfo, OutlineItem, PageContent, PageRequest, Source } from '@zinejs/core';
|
|
1
|
+
import type { DownloadInfo, LoadProgress, OutlineItem, PageContent, PageRequest, Source } from '@zinejs/core';
|
|
2
2
|
/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */
|
|
3
3
|
interface PdfPageLike {
|
|
4
4
|
getViewport(opts: {
|
|
@@ -67,6 +67,8 @@ export declare class PdfSource implements Source {
|
|
|
67
67
|
constructor(src: PdfSrc, options?: PdfSourceOptions);
|
|
68
68
|
/** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */
|
|
69
69
|
onPageUpdate(handler: (index: number) => void): void;
|
|
70
|
+
/** Register a handler called as the document downloads, driving a loading indicator. */
|
|
71
|
+
onProgress(handler: (progress: LoadProgress) => void): void;
|
|
70
72
|
open(): Promise<void>;
|
|
71
73
|
get(index: number, opts?: PageRequest): Promise<PageContent>;
|
|
72
74
|
prefetch(indices: number[]): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zinejs/pdf",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "PDF content source for zinejs, powered by pdf.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"pdfjs-dist": ">=4"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@zinejs/core": "^0.7.
|
|
43
|
+
"@zinejs/core": "^0.7.4"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@zinejs/core": "^0.7.
|
|
46
|
+
"@zinejs/core": "^0.7.4"
|
|
47
47
|
},
|
|
48
48
|
"size-limit": [
|
|
49
49
|
{
|