@zinejs/pdf 0.4.2 → 0.6.0
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/README.md +7 -16
- package/dist/index.js +108 -20
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +2 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/pdfSource.d.ts +43 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -27,28 +27,19 @@ Under a bundler that's all you need — the pdf.js worker is resolved for you.
|
|
|
27
27
|
|
|
28
28
|
Both packages ship **UMD** builds (`dist/index.umd.js`) that share the `ZineJS` global — load **core first**, then pdf (the pdf build uses `extend: true` so it merges into `ZineJS` instead of replacing it).
|
|
29
29
|
|
|
30
|
-
Modern `pdfjs-dist` is ESM-only, so
|
|
30
|
+
Modern `pdfjs-dist` is ESM-only, so load it in a module script first, expose
|
|
31
|
+
`globalThis.pdfjsLib`, then use classic deferred `<script>` tags for the UMD
|
|
32
|
+
builds (and pass an explicit `workerSrc` — no bundler rewrites the worker URL):
|
|
31
33
|
|
|
32
34
|
```html
|
|
33
35
|
<script type="module">
|
|
34
36
|
import * as pdfjsLib from 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.mjs';
|
|
35
37
|
globalThis.pdfjsLib = pdfjsLib;
|
|
38
|
+
</script>
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
s.onload = resolve;
|
|
41
|
-
s.onerror = reject;
|
|
42
|
-
document.head.append(s);
|
|
43
|
-
});
|
|
44
|
-
await new Promise((resolve, reject) => {
|
|
45
|
-
const s = document.createElement('script');
|
|
46
|
-
s.src = 'https://cdn.jsdelivr.net/npm/@zinejs/pdf/dist/index.umd.js';
|
|
47
|
-
s.onload = resolve;
|
|
48
|
-
s.onerror = reject;
|
|
49
|
-
document.head.append(s);
|
|
50
|
-
});
|
|
51
|
-
|
|
40
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@zinejs/core/dist/index.umd.js"></script>
|
|
41
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@zinejs/pdf/dist/index.umd.js"></script>
|
|
42
|
+
<script defer>
|
|
52
43
|
const book = new ZineJS.Zine(document.getElementById('book'), {
|
|
53
44
|
source: new ZineJS.PdfSource('document.pdf', {
|
|
54
45
|
workerSrc: 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.worker.min.mjs',
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
function e(e) {
|
|
2
2
|
return typeof e == "object" && !!e && "numPages" in e && typeof e.getPage == "function";
|
|
3
3
|
}
|
|
4
|
-
|
|
4
|
+
function t(e) {
|
|
5
|
+
try {
|
|
6
|
+
let t = new URL(e, "http://x").pathname.split("/").filter(Boolean).pop();
|
|
7
|
+
return t && /\.[a-z0-9]+$/i.test(t) ? decodeURIComponent(t) : "document.pdf";
|
|
8
|
+
} catch {
|
|
9
|
+
return "document.pdf";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function n() {
|
|
5
13
|
let e = globalThis;
|
|
6
14
|
return e.pdfjsLib ? e.pdfjsLib : await import("pdfjs-dist");
|
|
7
15
|
}
|
|
8
|
-
var
|
|
16
|
+
var r = class {
|
|
9
17
|
pageCount = 0;
|
|
10
18
|
#e;
|
|
11
19
|
#t;
|
|
@@ -18,6 +26,7 @@ var n = class {
|
|
|
18
26
|
#c = /* @__PURE__ */ new Map();
|
|
19
27
|
#l = 0;
|
|
20
28
|
#u = null;
|
|
29
|
+
#d = /* @__PURE__ */ new Map();
|
|
21
30
|
constructor(e, t = {}) {
|
|
22
31
|
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;
|
|
23
32
|
}
|
|
@@ -27,14 +36,14 @@ var n = class {
|
|
|
27
36
|
async open() {
|
|
28
37
|
if (e(this.#e)) this.#i = this.#e;
|
|
29
38
|
else {
|
|
30
|
-
let e = await
|
|
31
|
-
e.GlobalWorkerOptions.workerSrc = this.#
|
|
32
|
-
let
|
|
33
|
-
this.#s && (
|
|
39
|
+
let e = await n();
|
|
40
|
+
e.GlobalWorkerOptions.workerSrc = this.#f(e.GlobalWorkerOptions.workerSrc);
|
|
41
|
+
let t = typeof this.#e == "string" ? { url: this.#e } : { data: this.#e };
|
|
42
|
+
this.#s && (t.disableAutoFetch = !0), this.#i = await e.getDocument(t).promise;
|
|
34
43
|
}
|
|
35
44
|
this.pageCount = this.#i.numPages;
|
|
36
45
|
}
|
|
37
|
-
#
|
|
46
|
+
#f(e) {
|
|
38
47
|
if (this.#t) return this.#t;
|
|
39
48
|
if (e) return e;
|
|
40
49
|
try {
|
|
@@ -43,40 +52,119 @@ var n = class {
|
|
|
43
52
|
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.");
|
|
44
53
|
}
|
|
45
54
|
}
|
|
46
|
-
get(e) {
|
|
47
|
-
|
|
55
|
+
get(e, t) {
|
|
56
|
+
if (t && t.scale > 1) return this.#v(e, t);
|
|
57
|
+
let n = this.#h(e);
|
|
48
58
|
for (let t = 1; t <= this.#r; t++) this.prefetch([e - t, e + t]);
|
|
49
|
-
return
|
|
59
|
+
return n;
|
|
50
60
|
}
|
|
51
61
|
prefetch(e) {
|
|
52
|
-
for (let t of e) t >= 0 && t < this.pageCount && this.#
|
|
62
|
+
for (let t of e) t >= 0 && t < this.pageCount && this.#h(t).catch(() => {});
|
|
63
|
+
}
|
|
64
|
+
getText(e) {
|
|
65
|
+
let t = this.#d.get(e);
|
|
66
|
+
if (t) return t;
|
|
67
|
+
let n = this.#m(e).catch(() => "");
|
|
68
|
+
return this.#d.set(e, n), n;
|
|
69
|
+
}
|
|
70
|
+
async getOutline() {
|
|
71
|
+
let e = this.#i;
|
|
72
|
+
if (!e || typeof e.getOutline != "function") return [];
|
|
73
|
+
let t;
|
|
74
|
+
try {
|
|
75
|
+
t = await e.getOutline();
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
if (!t?.length) return [];
|
|
80
|
+
let n = async (e) => Promise.all(e.map(async (e) => ({
|
|
81
|
+
title: (e.title ?? "").trim() || "Untitled",
|
|
82
|
+
page: await this.#p(e.dest),
|
|
83
|
+
children: e.items?.length ? await n(e.items) : []
|
|
84
|
+
})));
|
|
85
|
+
return n(t);
|
|
86
|
+
}
|
|
87
|
+
async #p(e) {
|
|
88
|
+
let t = this.#i;
|
|
89
|
+
if (!t || e == null) return null;
|
|
90
|
+
try {
|
|
91
|
+
let n = (typeof e == "string" ? await t.getDestination?.(e) : e)?.[0];
|
|
92
|
+
if (n == null) return null;
|
|
93
|
+
if (typeof n == "number") return n;
|
|
94
|
+
let r = await t.getPageIndex?.(n);
|
|
95
|
+
return typeof r == "number" ? r : null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async getDownload() {
|
|
101
|
+
let e = this.#e;
|
|
102
|
+
if (typeof e == "string") return {
|
|
103
|
+
url: e,
|
|
104
|
+
filename: t(e)
|
|
105
|
+
};
|
|
106
|
+
if (e instanceof ArrayBuffer || ArrayBuffer.isView(e)) {
|
|
107
|
+
let t = new Blob([e], { type: "application/pdf" });
|
|
108
|
+
return {
|
|
109
|
+
url: URL.createObjectURL(t),
|
|
110
|
+
filename: "document.pdf",
|
|
111
|
+
revoke: !0
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
async #m(e) {
|
|
117
|
+
if (this.#i === null) throw Error("PdfSource: use after open() — the document is not loaded.");
|
|
118
|
+
let t = await this.#i.getPage(e + 1);
|
|
119
|
+
if (typeof t.getTextContent != "function") return "";
|
|
120
|
+
let n = await t.getTextContent(), r = "";
|
|
121
|
+
for (let e of n.items) typeof e.str == "string" && (r += e.str, r += e.hasEOL ? "\n" : "");
|
|
122
|
+
return r;
|
|
53
123
|
}
|
|
54
124
|
destroy() {
|
|
55
|
-
this.#i?.destroy?.(), this.#c.clear(), this.#l = 0, this.#i = null;
|
|
125
|
+
this.#i?.destroy?.(), this.#c.clear(), this.#d.clear(), this.#l = 0, this.#i = null;
|
|
56
126
|
}
|
|
57
|
-
#
|
|
127
|
+
#h(e) {
|
|
58
128
|
let t = this.#c.get(e);
|
|
59
129
|
if (t) return this.#c.delete(e), this.#c.set(e, t), t.promise;
|
|
60
130
|
let n = typeof devicePixelRatio == "number" ? devicePixelRatio : 1, r = this.#n * n, i = this.#o ? r * .35 : r, a = {
|
|
61
131
|
promise: void 0,
|
|
62
132
|
bytes: 0
|
|
63
133
|
};
|
|
64
|
-
return a.promise = this.#
|
|
134
|
+
return a.promise = this.#y(e, i).then((t) => (a.bytes = t.width * t.height * 4, this.#l += a.bytes, this.#_(e), this.#o && this.#g(e, a, r), t), (t) => {
|
|
65
135
|
throw this.#c.delete(e), t;
|
|
66
136
|
}), this.#c.set(e, a), a.promise;
|
|
67
137
|
}
|
|
68
|
-
#
|
|
69
|
-
this.#
|
|
70
|
-
this.#c.get(e) === t && (this.#l -= t.bytes, t.promise = Promise.resolve(n), t.bytes = n.width * n.height * 4, this.#l += t.bytes, this.#
|
|
138
|
+
#g(e, t, n) {
|
|
139
|
+
this.#y(e, n).then((n) => {
|
|
140
|
+
this.#c.get(e) === t && (this.#l -= t.bytes, t.promise = Promise.resolve(n), t.bytes = n.width * n.height * 4, this.#l += t.bytes, this.#_(e), this.#u?.(e));
|
|
71
141
|
}).catch(() => {});
|
|
72
142
|
}
|
|
73
|
-
#
|
|
143
|
+
#_(e) {
|
|
74
144
|
for (let [t, n] of this.#c) {
|
|
75
145
|
if (this.#l <= this.#a) break;
|
|
76
146
|
t === e || n.bytes === 0 || (this.#c.delete(t), this.#l -= n.bytes);
|
|
77
147
|
}
|
|
78
148
|
}
|
|
79
|
-
async #
|
|
149
|
+
async #v(e, t) {
|
|
150
|
+
if (this.#i === null) throw Error("PdfSource: use after open() — the document is not loaded.");
|
|
151
|
+
let n = await this.#i.getPage(e + 1), r = typeof devicePixelRatio == "number" ? devicePixelRatio : 1, i = this.#n * r * Math.min(t.scale, 6), a = n.getViewport({ scale: i }), o = t.region, s = document.createElement("canvas");
|
|
152
|
+
s.width = Math.max(1, Math.ceil(a.width * o.width)), s.height = Math.max(1, Math.ceil(a.height * o.height));
|
|
153
|
+
let c = [
|
|
154
|
+
1,
|
|
155
|
+
0,
|
|
156
|
+
0,
|
|
157
|
+
1,
|
|
158
|
+
-a.width * o.x,
|
|
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) {
|
|
80
168
|
if (this.#i === null) throw Error("PdfSource: use after open() — the document is not loaded.");
|
|
81
169
|
let n = await this.#i.getPage(e + 1), r = n.getViewport({ scale: t }), i = document.createElement("canvas");
|
|
82
170
|
return i.width = Math.ceil(r.width), i.height = Math.ceil(r.height), await n.render({
|
|
@@ -86,6 +174,6 @@ var n = class {
|
|
|
86
174
|
}
|
|
87
175
|
};
|
|
88
176
|
//#endregion
|
|
89
|
-
export {
|
|
177
|
+
export { r as PdfSource };
|
|
90
178
|
|
|
91
179
|
//# 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","#renderPage","#cache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type { PageContent, 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 render(opts: { canvasContext: unknown; viewport: unknown }): { promise: Promise<void> };\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\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\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/**\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\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): Promise<PageContent> {\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 destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.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 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.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"}
|
package/dist/index.umd.js
CHANGED
|
@@ -1,2 +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;function
|
|
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 a(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 o(){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=new Map;#l=0;#u=null;#d=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.#u=e}async open(){if(i(this.#e))this.#i=this.#e;else{let e=await o();e.GlobalWorkerOptions.workerSrc=this.#f(e.GlobalWorkerOptions.workerSrc);let t=typeof this.#e==`string`?{url:this.#e}:{data:this.#e};this.#s&&(t.disableAutoFetch=!0),this.#i=await e.getDocument(t).promise}this.pageCount=this.#i.numPages}#f(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.#v(e,t);let n=this.#h(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.#h(t).catch(()=>{})}getText(e){let t=this.#d.get(e);if(t)return t;let n=this.#m(e).catch(()=>``);return this.#d.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.#p(e.dest),children:e.items?.length?await n(e.items):[]})));return n(t)}async#p(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:a(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#m(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.#c.clear(),this.#d.clear(),this.#l=0,this.#i=null}#h(e){let t=this.#c.get(e);if(t)return this.#c.delete(e),this.#c.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.#y(e,a).then(t=>(o.bytes=t.width*t.height*4,this.#l+=o.bytes,this.#_(e),this.#o&&this.#g(e,o,i),t),t=>{throw this.#c.delete(e),t}),this.#c.set(e,o),o.promise}#g(e,t,n){this.#y(e,n).then(n=>{this.#c.get(e)===t&&(this.#l-=t.bytes,t.promise=Promise.resolve(n),t.bytes=n.width*n.height*4,this.#l+=t.bytes,this.#_(e),this.#u?.(e))}).catch(()=>{})}#_(e){for(let[t,n]of this.#c){if(this.#l<=this.#a)break;t===e||n.bytes===0||(this.#c.delete(t),this.#l-=n.bytes)}}async#v(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),i=typeof devicePixelRatio==`number`?devicePixelRatio:1,a=this.#n*i*Math.min(t.scale,r),o=n.getViewport({scale:a}),s=t.region,c=document.createElement(`canvas`);c.width=Math.max(1,Math.ceil(o.width*s.width)),c.height=Math.max(1,Math.ceil(o.height*s.height));let l=[1,0,0,1,-o.width*s.x,-o.height*s.y];return await n.render({canvasContext:c.getContext(`2d`),viewport:o,transform:l}).promise,c}async#y(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}}});
|
|
2
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","#renderPage","#cache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type { PageContent, 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 render(opts: { canvasContext: unknown; viewport: unknown }): { promise: Promise<void> };\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\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\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/**\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\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): Promise<PageContent> {\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 destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.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 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":"wRAwCA,IAAM,EAA0B,IAAM,KAAO,KAEvC,EAAwB,IAO9B,SAAS,EAAc,EAAqC,CAC1D,OACE,OAAO,GAAQ,YACf,GACA,aAAc,GACd,OAAQ,EAAwB,SAAY,UAEhD,CAMA,eAAe,GAAkC,CAC/C,IAAM,EAAI,WAEV,OADI,EAAE,SAAiB,EAAE,SACjB,MAAM,OAAO,aACvB,kBAQyC,CACvC,UAAY,EACZ,GACA,GACA,GACA,GACA,GAA+B,KAC/B,GACA,GACA,GACA,GAAS,IAAI,IACb,GAAe,EACf,GAA8C,KAE9C,YAAY,EAAa,EAA4B,CAAC,EAAG,CACvD,KAAKA,GAAO,EACZ,KAAKC,GAAa,EAAQ,UAC1B,KAAKC,GAAe,EAAQ,aAAe,EAC3C,KAAKC,GAAW,EAAQ,SAAW,EACnC,KAAKC,GAAiB,EAAQ,eAAiB,EAC/C,KAAKC,GAAe,EAAQ,aAAe,GAC3C,KAAKC,GAAoB,EAAQ,kBAAoB,EACvD,CAGA,aAAa,EAAwC,CACnD,KAAKC,GAAY,CACnB,CAEA,MAAM,MAAsB,CAC1B,GAAI,EAAc,KAAKP,EAAI,EACzB,KAAKQ,GAAO,KAAKR,OACZ,CACL,IAAM,EAAQ,MAAM,EAAU,EAC9B,EAAM,oBAAoB,UAAY,KAAKS,GAAkB,EAAM,oBAAoB,SAAS,EAChG,IAAM,EACJ,OAAO,KAAKT,IAAS,SAAW,CAAE,IAAK,KAAKA,EAAK,EAAI,CAAE,KAAM,KAAKA,EAAK,EACrE,KAAKM,KAAmB,EAAO,iBAAmB,IACtD,KAAKE,GAAO,MAAM,EAAM,YAAY,CAAM,CAAC,CAAC,OAC9C,CACA,KAAK,UAAY,KAAKA,GAAK,QAC7B,CAGA,GAAkB,EAA4B,CAC5C,GAAI,KAAKP,GAAY,OAAO,KAAKA,GACjC,GAAI,EAAY,OAAO,EACvB,GAAI,CAGF,MAAO,yBACT,MAAQ,CACN,MAAU,MACR,wNAGF,CACF,CACF,CAEA,IAAI,EAAqC,CACvC,IAAM,EAAU,KAAKS,GAAY,CAAK,EACtC,IAAK,IAAI,EAAI,EAAG,GAAK,KAAKP,GAAU,IAClC,KAAK,SAAS,CAAC,EAAQ,EAAG,EAAQ,CAAC,CAAC,EAEtC,OAAO,CACT,CAEA,SAAS,EAAyB,CAChC,IAAK,IAAM,KAAK,EACV,GAAK,GAAK,EAAI,KAAK,WACrB,KAAUO,GAAY,CAAC,CAAC,CAAC,UAAY,CAAC,CAAC,CAG7C,CAEA,SAAgB,CACd,KAAKF,IAAM,UAAU,EACrB,KAAKG,GAAO,MAAM,EAClB,KAAKC,GAAe,EACpB,KAAKJ,GAAO,IACd,CAEA,GAAY,EAAqC,CAC/C,IAAM,EAAW,KAAKG,GAAO,IAAI,CAAK,EACtC,GAAI,EAIF,OAFA,KAAKA,GAAO,OAAO,CAAK,EACxB,KAAKA,GAAO,IAAI,EAAO,CAAQ,EACxB,EAAS,QAGlB,IAAM,EAAM,OAAO,kBAAqB,SAAW,iBAAmB,EAChE,EAAO,KAAKT,GAAe,EAC3B,EAAa,KAAKG,GAAe,EAAO,EAAwB,EAEhE,EAAoB,CAAE,QAAS,IAAA,GAA8C,MAAO,CAAE,EAe5F,MAdA,GAAM,QAAU,KAAKQ,GAAW,EAAO,CAAU,CAAC,CAAC,KAChD,IACC,EAAM,MAAQ,EAAQ,MAAQ,EAAQ,OAAS,EAC/C,KAAKD,IAAgB,EAAM,MAC3B,KAAKE,GAAY,CAAK,EAClB,KAAKT,IAAc,KAAKU,GAAS,EAAO,EAAO,CAAI,EAChD,GAER,GAAmB,CAElB,MADA,KAAKJ,GAAO,OAAO,CAAK,EAClB,CACR,CACF,EACA,KAAKA,GAAO,IAAI,EAAO,CAAK,EACrB,EAAM,OACf,CAGA,GAAS,EAAe,EAAmB,EAAyB,CAClE,KAAUE,GAAW,EAAO,CAAS,CAAC,CACnC,KAAM,GAAO,CACR,KAAKF,GAAO,IAAI,CAAK,IAAM,IAC/B,KAAKC,IAAgB,EAAM,MAC3B,EAAM,QAAU,QAAQ,QAAQ,CAAE,EAClC,EAAM,MAAQ,EAAG,MAAQ,EAAG,OAAS,EACrC,KAAKA,IAAgB,EAAM,MAC3B,KAAKE,GAAY,CAAK,EACtB,KAAKP,KAAY,CAAK,EACxB,CAAC,CAAC,CACD,UAAY,CAAC,CAAC,CACnB,CAGA,GAAY,EAAyB,CACnC,IAAK,GAAM,CAAC,EAAO,KAAU,KAAKI,GAAQ,CACxC,GAAI,KAAKC,IAAgB,KAAKR,GAAgB,MAC1C,IAAU,GAAa,EAAM,QAAU,IAC3C,KAAKO,GAAO,OAAO,CAAK,EACxB,KAAKC,IAAgB,EAAM,MAC7B,CACF,CAEA,KAAMC,GAAW,EAAe,EAAqC,CACnE,GAAI,KAAKL,KAAS,KAAM,MAAU,MAAM,2DAA2D,EACnG,IAAM,EAAO,MAAM,KAAKA,GAAK,QAAQ,EAAQ,CAAC,EACxC,EAAW,EAAK,YAAY,CAAE,OAAM,CAAC,EACrC,EAAS,SAAS,cAAc,QAAQ,EAI9C,MAHA,GAAO,MAAQ,KAAK,KAAK,EAAS,KAAK,EACvC,EAAO,OAAS,KAAK,KAAK,EAAS,MAAM,EACzC,MAAM,EAAK,OAAO,CAAE,cAAe,EAAO,WAAW,IAAI,EAAG,UAAS,CAAC,CAAC,CAAC,QACjE,CACT,CACF"}
|
|
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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"}
|
package/dist/pdfSource.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PageContent, Source } from '@zinejs/core';
|
|
1
|
+
import type { DownloadInfo, 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: {
|
|
@@ -7,17 +7,37 @@ interface PdfPageLike {
|
|
|
7
7
|
width: number;
|
|
8
8
|
height: number;
|
|
9
9
|
};
|
|
10
|
+
/** `transform` is an affine matrix applied before painting, used to offset a cropped tile. */
|
|
10
11
|
render(opts: {
|
|
11
12
|
canvasContext: unknown;
|
|
12
13
|
viewport: unknown;
|
|
14
|
+
transform?: number[];
|
|
13
15
|
}): {
|
|
14
16
|
promise: Promise<void>;
|
|
15
17
|
};
|
|
18
|
+
/** Optional: absent on scanned PDFs with no text layer, and on hand-rolled document stubs. */
|
|
19
|
+
getTextContent?(): Promise<{
|
|
20
|
+
items: {
|
|
21
|
+
str?: string;
|
|
22
|
+
hasEOL?: boolean;
|
|
23
|
+
}[];
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
/** A pdf.js outline entry. `dest` is either a named destination or an explicit array whose first
|
|
27
|
+
* element references the target page. */
|
|
28
|
+
interface PdfOutlineNode {
|
|
29
|
+
title?: string;
|
|
30
|
+
dest?: string | unknown[] | null;
|
|
31
|
+
items?: PdfOutlineNode[];
|
|
16
32
|
}
|
|
17
33
|
interface PdfDocumentLike {
|
|
18
34
|
numPages: number;
|
|
19
35
|
getPage(pageNumber: number): Promise<PdfPageLike>;
|
|
20
36
|
destroy?(): void;
|
|
37
|
+
/** Optional: absent on documents without a table of contents, and on hand-rolled stubs. */
|
|
38
|
+
getOutline?(): Promise<PdfOutlineNode[] | null>;
|
|
39
|
+
getDestination?(id: string): Promise<unknown[] | null>;
|
|
40
|
+
getPageIndex?(ref: unknown): Promise<number>;
|
|
21
41
|
}
|
|
22
42
|
/** A URL, raw bytes, or a pre-created pdf.js document. */
|
|
23
43
|
export type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;
|
|
@@ -48,8 +68,29 @@ export declare class PdfSource implements Source {
|
|
|
48
68
|
/** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */
|
|
49
69
|
onPageUpdate(handler: (index: number) => void): void;
|
|
50
70
|
open(): Promise<void>;
|
|
51
|
-
get(index: number): Promise<PageContent>;
|
|
71
|
+
get(index: number, opts?: PageRequest): Promise<PageContent>;
|
|
52
72
|
prefetch(indices: number[]): void;
|
|
73
|
+
/**
|
|
74
|
+
* The page's text, for search. Empty for a page with no text layer (a scan, or a pdf.js build
|
|
75
|
+
* without text extraction) — callers treat that as "nothing to match" rather than an error.
|
|
76
|
+
*/
|
|
77
|
+
getText(index: number): Promise<string>;
|
|
78
|
+
/**
|
|
79
|
+
* The document's table of contents, flattened into the shape the outline panel wants.
|
|
80
|
+
*
|
|
81
|
+
* Empty when the PDF has no outline, which is common. Destinations are resolved to page
|
|
82
|
+
* indices here rather than in the UI: doing it once, up front, keeps clicking an entry
|
|
83
|
+
* instant, and an entry whose destination cannot be resolved is kept with a null page so the
|
|
84
|
+
* heading still shows.
|
|
85
|
+
*/
|
|
86
|
+
getOutline(): Promise<OutlineItem[]>;
|
|
87
|
+
/**
|
|
88
|
+
* The original PDF, for a download control.
|
|
89
|
+
*
|
|
90
|
+
* Null when the source was handed a pre-opened pdf.js document: the bytes belong to whoever
|
|
91
|
+
* created it, and re-fetching them is not this class's call to make.
|
|
92
|
+
*/
|
|
93
|
+
getDownload(): Promise<DownloadInfo | null>;
|
|
53
94
|
destroy(): void;
|
|
54
95
|
}
|
|
55
96
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zinejs/pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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.
|
|
43
|
+
"@zinejs/core": "^0.6.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@zinejs/core": "^0.
|
|
46
|
+
"@zinejs/core": "^0.6.0"
|
|
47
47
|
},
|
|
48
48
|
"size-limit": [
|
|
49
49
|
{
|