@zinejs/pdf 0.1.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 ADDED
@@ -0,0 +1,84 @@
1
+ # @zinejs/pdf
2
+
3
+ PDF content source for [zinejs](https://github.com/), powered by [pdf.js](https://mozilla.github.io/pdf.js/). Renders PDF pages to canvases that the zinejs core paints as a flipbook.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @zinejs/core @zinejs/pdf pdfjs-dist
9
+ ```
10
+
11
+ `pdfjs-dist` is a **peer dependency** — you install it, and the plugin loads it lazily so it never lands in the core bundle.
12
+
13
+ ## Usage
14
+
15
+ ```js
16
+ import { Zine } from '@zinejs/core';
17
+ import { PdfSource } from '@zinejs/pdf';
18
+
19
+ const book = new Zine(document.getElementById('book'), {
20
+ source: new PdfSource('document.pdf'),
21
+ });
22
+ ```
23
+
24
+ Under a bundler that's all you need — the pdf.js worker is resolved for you.
25
+
26
+ ## The pdf.js worker (`workerSrc`)
27
+
28
+ pdf.js parses and rasterizes in a Web Worker, a separate file the host must locate. By default `PdfSource` resolves it to:
29
+
30
+ ```js
31
+ new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href
32
+ ```
33
+
34
+ Resolution precedence: an explicit `workerSrc` option → an already-set `pdfjsLib.GlobalWorkerOptions.workerSrc` → the auto-default above. You only pass `workerSrc` when the default can't apply.
35
+
36
+ ### Vite / webpack 5 / esbuild
37
+
38
+ Nothing to do — these bundlers statically rewrite the `new URL(..., import.meta.url)` above and emit the worker asset:
39
+
40
+ ```js
41
+ new PdfSource('document.pdf');
42
+ ```
43
+
44
+ ### CDN / `<script>` (UMD)
45
+
46
+ No bundler to rewrite the URL, so point at a hosted worker matching your pdf.js version:
47
+
48
+ ```js
49
+ new ZineJS.PdfSource('document.pdf', {
50
+ workerSrc: 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.worker.min.mjs',
51
+ });
52
+ ```
53
+
54
+ ### Custom path
55
+
56
+ Serving the worker yourself (copied to your public dir, etc.):
57
+
58
+ ```js
59
+ new PdfSource('document.pdf', { workerSrc: '/assets/pdf.worker.min.mjs' });
60
+ ```
61
+
62
+ ### Already configuring pdf.js yourself
63
+
64
+ If your app sets `GlobalWorkerOptions.workerSrc`, `PdfSource` leaves it alone — no `workerSrc` needed.
65
+
66
+ ## Passing bytes or a pre-created document
67
+
68
+ ```js
69
+ new PdfSource(arrayBuffer); // raw PDF bytes (ArrayBuffer | Uint8Array)
70
+ new PdfSource(await getDocument(...).promise); // a pdf.js document you created; no workerSrc needed
71
+ ```
72
+
73
+ ## Options
74
+
75
+ ```ts
76
+ new PdfSource(src, {
77
+ workerSrc, // string — override the auto-resolved worker URL (see above)
78
+ renderScale: 1, // pages rasterize at renderScale × devicePixelRatio
79
+ preload: 1, // adjacent pages to prefetch around a requested page
80
+ maxCacheBytes, // soft cap on cached page bytes (default ~256 MB); LRU-evicts beyond it
81
+ });
82
+ ```
83
+
84
+ `src` is a URL string, `ArrayBuffer`/`Uint8Array` of PDF bytes, or a pre-created pdf.js document.
@@ -0,0 +1,2 @@
1
+ export { PdfSource } from './pdfSource';
2
+ export type { PdfSrc, PdfSourceOptions } from './pdfSource';
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ function e(e) {
2
+ return typeof e == "object" && !!e && "numPages" in e && typeof e.getPage == "function";
3
+ }
4
+ var t = class {
5
+ pageCount = 0;
6
+ #e;
7
+ #t;
8
+ #n;
9
+ #r;
10
+ #i = null;
11
+ #a;
12
+ #o = /* @__PURE__ */ new Map();
13
+ #s = 0;
14
+ constructor(e, t = {}) {
15
+ this.#e = e, this.#t = t.workerSrc, this.#n = t.renderScale ?? 1, this.#r = t.preload ?? 1, this.#a = t.maxCacheBytes ?? 268435456;
16
+ }
17
+ async open() {
18
+ if (e(this.#e)) this.#i = this.#e;
19
+ else {
20
+ let e = await import("pdfjs-dist");
21
+ e.GlobalWorkerOptions.workerSrc = this.#c(e.GlobalWorkerOptions.workerSrc);
22
+ let t = typeof this.#e == "string" ? { url: this.#e } : { data: this.#e };
23
+ this.#i = await e.getDocument(t).promise;
24
+ }
25
+ this.pageCount = this.#i.numPages;
26
+ }
27
+ #c(e) {
28
+ if (this.#t) return this.#t;
29
+ if (e) return e;
30
+ try {
31
+ return (new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href);
32
+ } catch {
33
+ 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.");
34
+ }
35
+ }
36
+ get(e) {
37
+ let t = this.#l(e);
38
+ for (let t = 1; t <= this.#r; t++) this.prefetch([e - t, e + t]);
39
+ return t;
40
+ }
41
+ prefetch(e) {
42
+ for (let t of e) t >= 0 && t < this.pageCount && this.#l(t).catch(() => {});
43
+ }
44
+ destroy() {
45
+ this.#i?.destroy?.(), this.#o.clear(), this.#s = 0, this.#i = null;
46
+ }
47
+ #l(e) {
48
+ let t = this.#o.get(e);
49
+ if (t) return this.#o.delete(e), this.#o.set(e, t), t.promise;
50
+ let n = {
51
+ promise: void 0,
52
+ bytes: 0
53
+ };
54
+ return n.promise = this.#d(e).then((t) => (n.bytes = t.width * t.height * 4, this.#s += n.bytes, this.#u(e), t), (t) => {
55
+ throw this.#o.delete(e), t;
56
+ }), this.#o.set(e, n), n.promise;
57
+ }
58
+ #u(e) {
59
+ for (let [t, n] of this.#o) {
60
+ if (this.#s <= this.#a) break;
61
+ t === e || n.bytes === 0 || (this.#o.delete(t), this.#s -= n.bytes);
62
+ }
63
+ }
64
+ async #d(e) {
65
+ if (this.#i === null) throw Error("PdfSource: use after open() — the document is not loaded.");
66
+ let t = await this.#i.getPage(e + 1), n = typeof devicePixelRatio == "number" ? devicePixelRatio : 1, r = t.getViewport({ scale: this.#n * n }), i = document.createElement("canvas");
67
+ return i.width = Math.ceil(r.width), i.height = Math.ceil(r.height), await t.render({
68
+ canvasContext: i.getContext("2d"),
69
+ viewport: r
70
+ }).promise, i;
71
+ }
72
+ };
73
+ //#endregion
74
+ export { t as PdfSource };
75
+
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#doc","#resolveWorkerSrc","#renderPage","#cache","#cachedBytes","#rasterize","#evictToFit"],"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 PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: { url: string } | { data: ArrayBuffer | Uint8Array }): { 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}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\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 * Renders PDF pages to canvases for zinejs. pdf.js is a peer dependency, loaded\n * lazily so it never lands in the core bundle. Async — pass it to `new Zine`,\n * which calls `open()` (learning the page count) before building 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 #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\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 }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = (await import('pdfjs-dist')) as unknown as PdfjsModule;\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params = typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\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 entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\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 /** 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): 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 dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const viewport = page.getViewport({ scale: this.#renderScale * dpr });\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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"}
@@ -0,0 +1,48 @@
1
+ import type { PageContent, Source } from '@zinejs/core';
2
+ /** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */
3
+ interface PdfPageLike {
4
+ getViewport(opts: {
5
+ scale: number;
6
+ }): {
7
+ width: number;
8
+ height: number;
9
+ };
10
+ render(opts: {
11
+ canvasContext: unknown;
12
+ viewport: unknown;
13
+ }): {
14
+ promise: Promise<void>;
15
+ };
16
+ }
17
+ interface PdfDocumentLike {
18
+ numPages: number;
19
+ getPage(pageNumber: number): Promise<PdfPageLike>;
20
+ destroy?(): void;
21
+ }
22
+ /** A URL, raw bytes, or a pre-created pdf.js document. */
23
+ export type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;
24
+ export interface PdfSourceOptions {
25
+ /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */
26
+ workerSrc?: string;
27
+ /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */
28
+ renderScale?: number;
29
+ /** How many adjacent pages to prefetch around a requested page. Default 1. */
30
+ preload?: number;
31
+ /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */
32
+ maxCacheBytes?: number;
33
+ }
34
+ /**
35
+ * Renders PDF pages to canvases for zinejs. pdf.js is a peer dependency, loaded
36
+ * lazily so it never lands in the core bundle. Async — pass it to `new Zine`,
37
+ * which calls `open()` (learning the page count) before building spreads.
38
+ */
39
+ export declare class PdfSource implements Source {
40
+ #private;
41
+ pageCount: number;
42
+ constructor(src: PdfSrc, options?: PdfSourceOptions);
43
+ open(): Promise<void>;
44
+ get(index: number): Promise<PageContent>;
45
+ prefetch(indices: number[]): void;
46
+ destroy(): void;
47
+ }
48
+ export {};
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@zinejs/pdf",
3
+ "version": "0.1.0",
4
+ "description": "PDF content source for zinejs, powered by pdf.js.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "license": "PolyForm-Noncommercial-1.0.0",
8
+ "keywords": [
9
+ "flipbook",
10
+ "pdf",
11
+ "pdf.js",
12
+ "page-flip",
13
+ "viewer",
14
+ "zinejs"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/awecode/zinejs.git",
19
+ "directory": "packages/pdf"
20
+ },
21
+ "homepage": "https://github.com/awecode/zinejs/tree/main/packages/pdf#readme",
22
+ "bugs": "https://github.com/awecode/zinejs/issues",
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "types": "./dist/index.d.ts",
27
+ "module": "./dist/index.js",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "peerDependencies": {
38
+ "pdfjs-dist": ">=4",
39
+ "@zinejs/core": "^0.1.0"
40
+ },
41
+ "devDependencies": {
42
+ "@zinejs/core": "^0.1.0"
43
+ },
44
+ "size-limit": [
45
+ {
46
+ "name": "pdf plugin (ESM, gzip, excl. pdf.js)",
47
+ "path": "dist/index.js",
48
+ "limit": "8 kB",
49
+ "gzip": true
50
+ }
51
+ ],
52
+ "scripts": {
53
+ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.build.json",
54
+ "typecheck": "tsc --noEmit -p tsconfig.json",
55
+ "test": "pnpm typecheck && vitest run",
56
+ "size": "pnpm build && size-limit"
57
+ }
58
+ }