@zinejs/pdf 0.7.4 → 0.9.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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # @zinejs/pdf
2
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.
3
+ PDF content source for [zinejs](https://zinejs.com/docs/), powered by [pdf.js](https://mozilla.github.io/pdf.js/). Renders PDF pages to canvases that [`@zinejs/core`](https://www.npmjs.com/package/@zinejs/core) paints as a flipbook.
4
+
5
+ > **Docs and examples:** [zinejs.com/docs](https://zinejs.com/docs/) · [CDN example](https://zinejs.com/examples/html-cdn)
4
6
 
5
7
  ## Install
6
8
 
@@ -8,107 +10,64 @@ PDF content source for [zinejs](https://github.com/), powered by [pdf.js](https:
8
10
  npm install @zinejs/core @zinejs/pdf
9
11
  ```
10
12
 
11
- `pdfjs-dist` is a normal dependency of `@zinejs/pdf`, so it installs with the package. The plugin still loads it lazily (dynamic `import`) so it never lands in the `@zinejs/core` bundle. If your app already depends on `pdfjs-dist`, the package manager will typically dedupe to one copy — keep major versions compatible so the worker matches the library.
13
+ `pdfjs-dist` installs with this package. It is loaded lazily (dynamic `import`) so it never lands in the `@zinejs/core` bundle. If your app already depends on `pdfjs-dist`, keep major versions compatible so the worker matches the library.
12
14
 
13
15
  ## Usage
14
16
 
15
17
  ```js
16
- import { Zine } from '@zinejs/core';
17
- import { PdfSource } from '@zinejs/pdf';
18
+ import { Zine } from '@zinejs/core'
19
+ import { PdfSource } from '@zinejs/pdf'
18
20
 
19
- const book = new Zine(document.getElementById('book'), {
21
+ new Zine(document.getElementById('book'), {
20
22
  source: new PdfSource('document.pdf'),
21
- });
23
+ })
22
24
  ```
23
25
 
24
- Under a bundler that's all you need — the pdf.js worker is resolved for you.
25
-
26
- ### CDN / no bundler
27
-
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).
26
+ Under a bundler that is all you need: the pdf.js worker is resolved for you.
29
27
 
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):
33
-
34
- ```html
35
- <script type="module">
36
- import * as pdfjsLib from 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.mjs';
37
- globalThis.pdfjsLib = pdfjsLib;
38
- </script>
39
-
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>
43
- const book = new ZineJS.Zine(document.getElementById('book'), {
44
- source: new ZineJS.PdfSource('document.pdf', {
45
- workerSrc: 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.worker.min.mjs',
46
- }),
47
- });
48
- </script>
49
- ```
28
+ `src` can also be raw bytes or a pdf.js document you already opened:
50
29
 
51
- Image-only books can skip pdf.js and use a plain classic script:
52
-
53
- ```html
54
- <script src="https://cdn.jsdelivr.net/npm/@zinejs/core/dist/index.umd.js"></script>
55
- <script>
56
- new ZineJS.Zine(document.getElementById('book'), {
57
- source: new ZineJS.ImageSource(['page-01.png', 'page-02.png']),
58
- });
59
- </script>
30
+ ```js
31
+ new PdfSource(arrayBuffer)
32
+ new PdfSource(await getDocument(url).promise)
60
33
  ```
61
34
 
62
- `PdfSource` prefers `globalThis.pdfjsLib` when present (CDN), otherwise dynamic-imports `pdfjs-dist` (bundlers).
63
-
64
- ## The pdf.js worker (`workerSrc`)
35
+ ## The pdf.js worker
65
36
 
66
- pdf.js parses and rasterizes in a Web Worker, a separate file the host must locate. By default `PdfSource` resolves it to:
37
+ pdf.js parses and rasterizes in a Web Worker. By default `PdfSource` points at the **legacy** worker (because `legacy` defaults to `true`):
67
38
 
68
39
  ```js
69
- new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href
40
+ new URL('pdfjs-dist/legacy/build/pdf.worker.min.mjs', import.meta.url).href
70
41
  ```
71
42
 
72
- 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.
73
-
74
- ### Vite / webpack 5 / esbuild
75
-
76
- Nothing to do — these bundlers statically rewrite the `new URL(..., import.meta.url)` above and emit the worker asset:
43
+ Vite, webpack 5, and esbuild rewrite that literal and emit the asset. Resolution order: `workerSrc` option, then `pdfjsLib.GlobalWorkerOptions.workerSrc`, then the auto-default. Pass `workerSrc` only for CDN / UMD or a file you copied yourself, and keep it on the same build as the library (legacy vs modern).
77
44
 
78
- ```js
79
- new PdfSource('document.pdf');
80
- ```
45
+ If you already set `GlobalWorkerOptions.workerSrc`, leave `workerSrc` off. A pre-created pdf.js document needs no worker from this package.
81
46
 
82
- ### Custom path
47
+ CDN / no bundler: load pdf.js first, expose `globalThis.pdfjsLib`, then the UMD builds (core first). See the [HTML CDN example](https://zinejs.com/examples/html-cdn).
83
48
 
84
- Serving the worker yourself (copied to your public dir, etc.):
49
+ ## Options
85
50
 
86
51
  ```js
87
- new PdfSource('document.pdf', { workerSrc: '/assets/pdf.worker.min.mjs' });
52
+ new PdfSource(src, {
53
+ workerSrc, // override the auto-resolved worker URL
54
+ renderScale: 1, // pages rasterize at renderScale × devicePixelRatio
55
+ preload: 1, // adjacent pages to prefetch (default 1)
56
+ maxCacheBytes, // soft cap on cached page bytes (default ~256 MB)
57
+ progressive: false, // true: low-res first, then crisp
58
+ disableAutoFetch: false, // true: fetch only the ranges visible pages need
59
+ legacy: true, // false: lean modern pdf.js (recent engines only)
60
+ })
88
61
  ```
89
62
 
90
- ### Already configuring pdf.js yourself
91
-
92
- If your app sets `GlobalWorkerOptions.workerSrc`, `PdfSource` leaves it alone — no `workerSrc` needed.
63
+ ### Legacy vs modern (`legacy`)
93
64
 
94
- ## Passing bytes or a pre-created document
65
+ pdf.js ships two builds. The **legacy** build is transpiled with polyfills and is the default, so PDF books stay usable on older engines as well as current ones. The **modern** build is smaller and a touch faster. Pass `legacy: false` when your audience is on recent browsers:
95
66
 
96
67
  ```js
97
- new PdfSource(arrayBuffer); // raw PDF bytes (ArrayBuffer | Uint8Array)
98
- new PdfSource(await getDocument(...).promise); // a pdf.js document you created; no workerSrc needed
68
+ new PdfSource('document.pdf', { legacy: false })
99
69
  ```
100
70
 
101
- ## Options
102
-
103
- ```ts
104
- new PdfSource(src, {
105
- workerSrc, // string — override the auto-resolved worker URL (see above)
106
- renderScale: 1, // pages rasterize at renderScale × devicePixelRatio
107
- preload: 1, // adjacent pages to prefetch around a requested page
108
- maxCacheBytes, // soft cap on cached page bytes (default ~256 MB); LRU-evicts beyond it
109
- progressive: true, // paint a low-res page first, then swap to crisp (faster first paint)
110
- disableAutoFetch: true, // fetch only the byte ranges visible pages need (range-capable servers)
111
- });
112
- ```
71
+ The flag also picks the matching worker. It is ignored when you pass a pre-created document or set `globalThis.pdfjsLib`, since those already chose their build.
113
72
 
114
- `src` is a URL string, `ArrayBuffer`/`Uint8Array` of PDF bytes, or a pre-created pdf.js document.
73
+ Mozilla documents legacy as Chrome 125+, Firefox ESR, Safari 18+, and Chromium Edge. We have seen PDF text rasterize correctly back to Chrome 114. Full notes: [browser support](https://github.com/awecode/zinejs#browser-support).
package/dist/index.js CHANGED
@@ -12,9 +12,9 @@ function n(e) {
12
12
  return "document.pdf";
13
13
  }
14
14
  }
15
- async function r() {
16
- let e = globalThis;
17
- return e.pdfjsLib ? e.pdfjsLib : await import("pdfjs-dist");
15
+ async function r(e) {
16
+ let t = globalThis;
17
+ return t.pdfjsLib ? t.pdfjsLib : e ? await import("pdfjs-dist/legacy/build/pdf.mjs") : await import("pdfjs-dist");
18
18
  }
19
19
  var i = class {
20
20
  pageCount = 0;
@@ -26,56 +26,57 @@ var i = class {
26
26
  #a;
27
27
  #o;
28
28
  #s;
29
- #c = null;
30
- #l = /* @__PURE__ */ new Map();
31
- #u = 0;
32
- #d = null;
33
- #f = /* @__PURE__ */ new Map();
29
+ #c;
30
+ #l = null;
31
+ #u = /* @__PURE__ */ new Map();
32
+ #d = 0;
33
+ #f = null;
34
34
  #p = /* @__PURE__ */ new Map();
35
+ #m = /* @__PURE__ */ new Map();
35
36
  constructor(e, t = {}) {
36
- this.#e = e, this.#t = t.workerSrc, this.#n = t.renderScale ?? 1, this.#r = t.preload ?? 1, this.#a = t.maxCacheBytes ?? 268435456, this.#o = t.progressive ?? !1, this.#s = t.disableAutoFetch ?? !1;
37
+ 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, this.#c = t.legacy ?? !0;
37
38
  }
38
39
  onPageUpdate(e) {
39
- this.#d = e;
40
+ this.#f = e;
40
41
  }
41
42
  onProgress(e) {
42
- this.#c = e;
43
+ this.#l = e;
43
44
  }
44
45
  async open() {
45
46
  if (t(this.#e)) this.#i = this.#e;
46
47
  else {
47
- let e = await r();
48
- e.GlobalWorkerOptions.workerSrc = this.#m(e.GlobalWorkerOptions.workerSrc);
48
+ let e = await r(this.#c);
49
+ e.GlobalWorkerOptions.workerSrc = this.#h(e.GlobalWorkerOptions.workerSrc);
49
50
  let t = typeof this.#e == "string" ? { url: this.#e } : { data: this.#e };
50
51
  this.#s && (t.disableAutoFetch = !0);
51
52
  let n = e.getDocument(t);
52
- this.#c && (n.onProgress = this.#c), this.#i = await n.promise;
53
+ this.#l && (n.onProgress = this.#l), this.#i = await n.promise;
53
54
  }
54
55
  this.pageCount = this.#i.numPages;
55
56
  }
56
- #m(e) {
57
+ #h(e) {
57
58
  if (this.#t) return this.#t;
58
59
  if (e) return e;
59
60
  try {
60
- return (new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href);
61
+ return this.#c ? (new URL('pdfjs-dist/legacy/build/pdf.worker.min.mjs', import.meta.url).href) : (new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href);
61
62
  } catch {
62
63
  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.");
63
64
  }
64
65
  }
65
66
  get(e, t) {
66
- if (t && t.scale > 1) return this.#b(e, t.scale);
67
- let n = this.#_(e);
67
+ if (t && t.scale > 1) return this.#x(e, t.scale, t.maxSize);
68
+ let n = this.#v(e);
68
69
  for (let t = 1; t <= this.#r; t++) this.prefetch([e - t, e + t]);
69
70
  return n;
70
71
  }
71
72
  prefetch(e) {
72
- for (let t of e) t >= 0 && t < this.pageCount && this.#_(t).catch(() => {});
73
+ for (let t of e) t >= 0 && t < this.pageCount && this.#v(t).catch(() => {});
73
74
  }
74
75
  getText(e) {
75
- let t = this.#p.get(e);
76
+ let t = this.#m.get(e);
76
77
  if (t) return t;
77
- let n = this.#g(e).catch(() => "");
78
- return this.#p.set(e, n), n;
78
+ let n = this.#_(e).catch(() => "");
79
+ return this.#m.set(e, n), n;
79
80
  }
80
81
  async getOutline() {
81
82
  let e = this.#i;
@@ -89,12 +90,12 @@ var i = class {
89
90
  if (!t?.length) return [];
90
91
  let n = async (e) => Promise.all(e.map(async (e) => ({
91
92
  title: (e.title ?? "").trim() || "Untitled",
92
- page: await this.#h(e.dest),
93
+ page: await this.#g(e.dest),
93
94
  children: e.items?.length ? await n(e.items) : []
94
95
  })));
95
96
  return n(t);
96
97
  }
97
- async #h(e) {
98
+ async #g(e) {
98
99
  let t = this.#i;
99
100
  if (!t || e == null) return null;
100
101
  try {
@@ -123,7 +124,7 @@ var i = class {
123
124
  }
124
125
  return null;
125
126
  }
126
- async #g(e) {
127
+ async #_(e) {
127
128
  if (this.#i === null) throw Error("PdfSource: use after open() — the document is not loaded.");
128
129
  let t = await this.#i.getPage(e + 1);
129
130
  if (typeof t.getTextContent != "function") return "";
@@ -132,48 +133,53 @@ var i = class {
132
133
  return r;
133
134
  }
134
135
  destroy() {
135
- this.#i?.destroy?.(), this.#l.clear();
136
- for (let t of this.#f.values()) t.then(e).catch(() => {});
137
- this.#f.clear(), this.#p.clear(), this.#u = 0, this.#i = null;
136
+ this.#i?.destroy?.(), this.#u.clear();
137
+ for (let t of this.#p.values()) t.then(e).catch(() => {});
138
+ this.#p.clear(), this.#m.clear(), this.#d = 0, this.#i = null;
138
139
  }
139
- #_(e) {
140
- let t = this.#l.get(e);
141
- if (t) return this.#l.delete(e), this.#l.set(e, t), t.promise;
140
+ #v(e) {
141
+ let t = this.#u.get(e);
142
+ if (t) return this.#u.delete(e), this.#u.set(e, t), t.promise;
142
143
  let n = typeof devicePixelRatio == "number" ? devicePixelRatio : 1, r = this.#n * n, i = this.#o ? r * .35 : r, a = {
143
144
  promise: void 0,
144
145
  bytes: 0
145
146
  };
146
- return a.promise = this.#x(e, i).then((t) => (a.bytes = t.width * t.height * 4, this.#u += a.bytes, this.#y(e), this.#o && this.#v(e, a, r), t), (t) => {
147
- throw this.#l.delete(e), t;
148
- }), this.#l.set(e, a), a.promise;
147
+ return a.promise = this.#S(e, i).then((t) => (a.bytes = t.width * t.height * 4, this.#d += a.bytes, this.#b(e), this.#o && this.#y(e, a, r), t), (t) => {
148
+ throw this.#u.delete(e), t;
149
+ }), this.#u.set(e, a), a.promise;
149
150
  }
150
- #v(e, t, n) {
151
- this.#x(e, n).then((n) => {
152
- this.#l.get(e) === t && (this.#u -= t.bytes, t.promise = Promise.resolve(n), t.bytes = n.width * n.height * 4, this.#u += t.bytes, this.#y(e), this.#d?.(e));
151
+ #y(e, t, n) {
152
+ this.#S(e, n).then((n) => {
153
+ this.#u.get(e) === t && (this.#d -= t.bytes, t.promise = Promise.resolve(n), t.bytes = n.width * n.height * 4, this.#d += t.bytes, this.#b(e), this.#f?.(e));
153
154
  }).catch(() => {});
154
155
  }
155
- #y(e) {
156
- for (let [t, n] of this.#l) {
157
- if (this.#u <= this.#a) break;
158
- t === e || n.bytes === 0 || (this.#l.delete(t), this.#u -= n.bytes);
156
+ #b(e) {
157
+ for (let [t, n] of this.#u) {
158
+ if (this.#d <= this.#a) break;
159
+ t === e || n.bytes === 0 || (this.#u.delete(t), this.#d -= n.bytes);
159
160
  }
160
161
  }
161
- #b(t, n) {
162
- let r = Math.min(n, 6), i = `${t}@${r}`, a = this.#f.get(i);
163
- if (a) return a;
164
- let o = typeof devicePixelRatio == "number" ? devicePixelRatio : 1, s = this.#x(t, this.#n * o * r).catch((e) => {
165
- throw this.#f.delete(i), e;
162
+ #x(t, n, r) {
163
+ let i = Math.min(n, 6), a = `${t}@${i}`, o = this.#p.get(a);
164
+ if (o) return o;
165
+ let s = typeof devicePixelRatio == "number" ? devicePixelRatio : 1, c = this.#S(t, this.#n * s * i, r).catch((e) => {
166
+ throw this.#p.delete(a), e;
166
167
  });
167
- for (let [t, n] of this.#f) t.endsWith(`@${r}`) || (this.#f.delete(t), n.then(e).catch(() => {}));
168
- return this.#f.set(i, s), s;
168
+ for (let [t, n] of this.#p) t.endsWith(`@${i}`) || (this.#p.delete(t), n.then(e).catch(() => {}));
169
+ return this.#p.set(a, c), c;
169
170
  }
170
- async #x(e, t) {
171
+ async #S(e, t, n) {
171
172
  if (this.#i === null) throw Error("PdfSource: use after open() — the document is not loaded.");
172
- let n = await this.#i.getPage(e + 1), r = n.getViewport({ scale: t }), i = document.createElement("canvas");
173
- return i.width = Math.ceil(r.width), i.height = Math.ceil(r.height), await n.render({
174
- canvasContext: i.getContext("2d"),
175
- viewport: r
176
- }).promise, i;
173
+ let r = await this.#i.getPage(e + 1);
174
+ if (n !== void 0 && n > 0) {
175
+ let e = r.getViewport({ scale: 1 }), i = n / Math.max(e.width, e.height);
176
+ t = Math.min(t, i);
177
+ }
178
+ let i = r.getViewport({ scale: t }), a = document.createElement("canvas");
179
+ return a.width = Math.ceil(i.width), a.height = Math.ceil(i.height), await r.render({
180
+ canvasContext: a.getContext("2d"),
181
+ viewport: i
182
+ }).promise, a;
177
183
  }
178
184
  };
179
185
  //#endregion
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#progressive","#disableAutoFetch","#onUpdate","#onProgress","#doc","#resolveWorkerSrc","#renderZoomed","#renderPage","#textCache","#extractText","#destinationPage","#cache","#zoomCache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type {\n DownloadInfo,\n LoadProgress,\n OutlineItem,\n PageContent,\n PageRequest,\n Source,\n} from '@zinejs/core';\n\n/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */\ninterface PdfPageLike {\n getViewport(opts: { scale: number }): { width: number; height: number };\n /** `transform` is an affine matrix applied before painting, used to offset a cropped tile. */\n render(opts: { canvasContext: unknown; viewport: unknown; transform?: number[] }): {\n promise: Promise<void>;\n };\n /** Optional: absent on scanned PDFs with no text layer, and on hand-rolled document stubs. */\n getTextContent?(): Promise<{ items: { str?: string; hasEOL?: boolean }[] }>;\n}\n/** A pdf.js outline entry. `dest` is either a named destination or an explicit array whose first\n * element references the target page. */\ninterface PdfOutlineNode {\n title?: string;\n dest?: string | unknown[] | null;\n items?: PdfOutlineNode[];\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\n /** Optional: absent on documents without a table of contents, and on hand-rolled stubs. */\n getOutline?(): Promise<PdfOutlineNode[] | null>;\n getDestination?(id: string): Promise<unknown[] | null>;\n getPageIndex?(ref: unknown): Promise<number>;\n}\ninterface DocParams {\n url?: string;\n data?: ArrayBuffer | Uint8Array;\n disableAutoFetch?: boolean;\n}\n/** pdf.js's loading task: the document promise, plus an assignable download-progress callback. */\ninterface PdfLoadingTask {\n promise: Promise<PdfDocumentLike>;\n onProgress?: (p: LoadProgress) => void;\n}\ninterface PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: DocParams): PdfLoadingTask;\n}\n\n/** A URL, raw bytes, or a pre-created pdf.js document. */\nexport type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;\n\nexport interface PdfSourceOptions {\n /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */\n workerSrc?: string;\n /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */\n renderScale?: number;\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */\n maxCacheBytes?: number;\n /** Paint a low-res page first, then swap to crisp (faster first paint). Default false. */\n progressive?: boolean;\n /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */\n disableAutoFetch?: boolean;\n}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\n// Linear-resolution fraction for the progressive low-res first pass (~1/9 the pixels).\nconst PROGRESSIVE_LOW_RATIO = 0.35;\n// Ceiling on zoom-tile magnification. Past roughly this point a screen pixel already holds more\n// detail than the reader can resolve, and rasterizing further only costs memory and time.\nconst MAX_ZOOM_RENDER_SCALE = 6;\n\n/** Free a canvas's backing store. Safari in particular holds the memory until the surface is\n * resized away, and a zoomed page is large enough for that to matter. */\nfunction releaseCanvas(content: PageContent): void {\n if (typeof HTMLCanvasElement !== 'undefined' && content instanceof HTMLCanvasElement) {\n content.width = 0;\n content.height = 0;\n }\n}\n\ninterface CacheEntry {\n promise: Promise<PageContent>;\n bytes: number; // 0 until the render resolves and its size is known\n}\n\nfunction isPdfDocument(src: PdfSrc): src is PdfDocumentLike {\n return (\n typeof src === 'object' &&\n src !== null &&\n 'numPages' in src &&\n typeof (src as PdfDocumentLike).getPage === 'function'\n );\n}\n\n/** Last path segment of a URL, for naming a download. Falls back when the URL has no filename\n * (a directory, a data: URI, or something unparseable). */\nfunction filenameFromUrl(url: string): string {\n try {\n const path = new URL(url, 'http://x').pathname;\n const last = path.split('/').filter(Boolean).pop();\n return last && /\\.[a-z0-9]+$/i.test(last) ? decodeURIComponent(last) : 'document.pdf';\n } catch {\n return 'document.pdf';\n }\n}\n\n/**\n * Load pdf.js. CDN / UMD hosts typically expose `globalThis.pdfjsLib` from a\n * `<script>` of pdf.js; bundlers have no global and resolve the dynamic import.\n */\nasync function loadPdfjs(): Promise<PdfjsModule> {\n const g = globalThis as typeof globalThis & { pdfjsLib?: PdfjsModule };\n if (g.pdfjsLib) return g.pdfjsLib;\n return (await import('pdfjs-dist')) as unknown as PdfjsModule;\n}\n\n/**\n * Renders PDF pages to canvases for zinejs. pdf.js is a dependency of this\n * package, loaded lazily so it never lands in the core bundle. Async — pass it\n * to `new Zine`, which calls `open()` (learning the page count) before building\n * spreads.\n */\nexport class PdfSource implements Source {\n pageCount = 0;\n #src: PdfSrc;\n #workerSrc: string | undefined;\n #renderScale: number;\n #preload: number;\n #doc: PdfDocumentLike | null = null;\n #maxCacheBytes: number;\n #progressive: boolean;\n #disableAutoFetch: boolean;\n #onProgress: ((progress: LoadProgress) => void) | null = null;\n #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\n #onUpdate: ((index: number) => void) | null = null;\n /** Zoomed page rasters keyed `index@scale`, apart from #cache: one is worth many normal pages\n * and would evict the whole spread from a byte cap sized for fit-to-screen. */\n #zoomCache = new Map<string, Promise<PageContent>>();\n\n /** Extracted page text, kept apart from #cache: text is scale-independent and negligible next\n * to a raster, so it has no place in the byte budget that evicts bitmaps. */\n #textCache = new Map<number, Promise<string>>();\n\n constructor(src: PdfSrc, options: PdfSourceOptions = {}) {\n this.#src = src;\n this.#workerSrc = options.workerSrc;\n this.#renderScale = options.renderScale ?? 1;\n this.#preload = options.preload ?? 1;\n this.#maxCacheBytes = options.maxCacheBytes ?? DEFAULT_MAX_CACHE_BYTES;\n this.#progressive = options.progressive ?? false;\n this.#disableAutoFetch = options.disableAutoFetch ?? false;\n }\n\n /** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */\n onPageUpdate(handler: (index: number) => void): void {\n this.#onUpdate = handler;\n }\n\n /** Register a handler called as the document downloads, driving a loading indicator. */\n onProgress(handler: (progress: LoadProgress) => void): void {\n this.#onProgress = handler;\n }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = await loadPdfjs();\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params: DocParams =\n typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\n if (this.#disableAutoFetch) params.disableAutoFetch = true;\n const task = pdfjs.getDocument(params);\n if (this.#onProgress) task.onProgress = this.#onProgress;\n this.#doc = await task.promise;\n }\n this.pageCount = this.#doc.numPages;\n }\n\n /** Precedence: an explicit `workerSrc` → an already-configured global → an auto-resolved default. */\n #resolveWorkerSrc(configured: string): string {\n if (this.#workerSrc) return this.#workerSrc;\n if (configured) return configured;\n try {\n // Bundlers (Vite / webpack 5 / esbuild) statically rewrite this and emit the worker,\n // so the common case needs no `workerSrc`. Non-bundler / UMD hosts pass it explicitly.\n return \"@@ZINE_PDF_WORKER_URL@@\";\n } catch {\n throw new Error(\n \"PdfSource: couldn't auto-resolve pdf.js's worker. Pass a `workerSrc` URL \" +\n '(see the @zinejs/pdf README, \"The pdf.js worker\"), set pdfjs ' +\n 'GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.',\n );\n }\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n // A zoomed request wants detail the fit-to-screen raster never had. PDF pages are vector, so\n // re-render at the magnification being viewed. Memoized per zoom level, not in the byte-capped\n // page cache: a zoomed page is many times the size of a normal one and would evict the whole\n // spread, and a reader who zooms out and back in should not pay to render it twice.\n if (opts && opts.scale > 1) return this.#renderZoomed(index, opts.scale);\n\n const decoded = this.#renderPage(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#renderPage(i).catch(() => {}); // prefetch failures surface on a real get()\n }\n }\n }\n\n /**\n * The page's text, for search. Empty for a page with no text layer (a scan, or a pdf.js build\n * without text extraction) — callers treat that as \"nothing to match\" rather than an error.\n */\n getText(index: number): Promise<string> {\n const cached = this.#textCache.get(index);\n if (cached) return cached;\n const pending = this.#extractText(index).catch(() => '');\n this.#textCache.set(index, pending);\n return pending;\n }\n\n /**\n * The document's table of contents, flattened into the shape the outline panel wants.\n *\n * Empty when the PDF has no outline, which is common. Destinations are resolved to page\n * indices here rather than in the UI: doing it once, up front, keeps clicking an entry\n * instant, and an entry whose destination cannot be resolved is kept with a null page so the\n * heading still shows.\n */\n async getOutline(): Promise<OutlineItem[]> {\n const doc = this.#doc;\n if (!doc || typeof doc.getOutline !== 'function') return [];\n let nodes: PdfOutlineNode[] | null;\n try {\n nodes = await doc.getOutline();\n } catch {\n return [];\n }\n if (!nodes?.length) return [];\n const convert = async (list: PdfOutlineNode[]): Promise<OutlineItem[]> =>\n Promise.all(\n list.map(async (node) => ({\n title: (node.title ?? '').trim() || 'Untitled',\n page: await this.#destinationPage(node.dest),\n children: node.items?.length ? await convert(node.items) : [],\n })),\n );\n return convert(nodes);\n }\n\n /** Resolve a pdf.js destination to a zero-based page index, or null if it cannot be. */\n async #destinationPage(dest: string | unknown[] | null | undefined): Promise<number | null> {\n const doc = this.#doc;\n if (!doc || dest == null) return null;\n try {\n // A named destination has to be looked up before it yields the usual explicit array.\n const explicit =\n typeof dest === 'string' ? await doc.getDestination?.(dest) : (dest as unknown[]);\n const ref = explicit?.[0];\n if (ref == null) return null;\n // Some destinations carry a bare page number instead of a reference.\n if (typeof ref === 'number') return ref;\n const index = await doc.getPageIndex?.(ref);\n return typeof index === 'number' ? index : null;\n } catch {\n return null; // a broken destination should not cost us the whole outline\n }\n }\n\n /**\n * The original PDF, for a download control.\n *\n * Null when the source was handed a pre-opened pdf.js document: the bytes belong to whoever\n * created it, and re-fetching them is not this class's call to make.\n */\n async getDownload(): Promise<DownloadInfo | null> {\n const src = this.#src;\n if (typeof src === 'string') {\n return { url: src, filename: filenameFromUrl(src) };\n }\n if (src instanceof ArrayBuffer || ArrayBuffer.isView(src)) {\n const blob = new Blob([src as BlobPart], { type: 'application/pdf' });\n return { url: URL.createObjectURL(blob), filename: 'document.pdf', revoke: true };\n }\n return null;\n }\n\n async #extractText(index: number): Promise<string> {\n if (this.#doc === null) {\n throw new Error('PdfSource: use after open() — the document is not loaded.');\n }\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n if (typeof page.getTextContent !== 'function') return '';\n const content = await page.getTextContent();\n let text = '';\n for (const item of content.items) {\n if (typeof item.str !== 'string') continue; // marked-content entries carry no text\n text += item.str;\n // pdf.js splits a visual line into runs; only hasEOL ends one.\n text += item.hasEOL ? '\\n' : '';\n }\n return text;\n }\n\n destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.clear();\n for (const pending of this.#zoomCache.values()) {\n void pending.then(releaseCanvas).catch(() => {});\n }\n this.#zoomCache.clear();\n this.#textCache.clear();\n this.#cachedBytes = 0;\n this.#doc = null;\n }\n\n #renderPage(index: number): Promise<PageContent> {\n const existing = this.#cache.get(index);\n if (existing) {\n // Mark most-recently-used: re-insert so it sits at the end of the Map's order.\n this.#cache.delete(index);\n this.#cache.set(index, existing);\n return existing.promise;\n }\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const full = this.#renderScale * dpr;\n const firstScale = this.#progressive ? full * PROGRESSIVE_LOW_RATIO : full;\n\n const entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index, firstScale).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n if (this.#progressive) this.#upgrade(index, entry, full);\n return content;\n },\n (error: unknown) => {\n this.#cache.delete(index); // let a later get() retry\n throw error;\n },\n );\n this.#cache.set(index, entry);\n return entry.promise;\n }\n\n /** Progressive: re-render at full resolution, swap it into the cache, and signal an update. */\n #upgrade(index: number, entry: CacheEntry, fullScale: number): void {\n void this.#rasterize(index, fullScale)\n .then((hi) => {\n if (this.#cache.get(index) !== entry) return; // evicted or replaced meanwhile\n this.#cachedBytes -= entry.bytes;\n entry.promise = Promise.resolve(hi);\n entry.bytes = hi.width * hi.height * 4;\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n this.#onUpdate?.(index);\n })\n .catch(() => {}); // keep the low-res if the upgrade fails\n }\n\n /** Drop least-recently-used pages (front of the Map) until under the byte cap. */\n #evictToFit(keepIndex: number): void {\n for (const [index, entry] of this.#cache) {\n if (this.#cachedBytes <= this.#maxCacheBytes) break;\n if (index === keepIndex || entry.bytes === 0) continue; // keep the newest; skip in-flight renders\n this.#cache.delete(index);\n this.#cachedBytes -= entry.bytes;\n }\n }\n\n /**\n * Render a whole page at zoom magnification, memoized by page and zoom level.\n *\n * Whole-page rather than a crop of what is visible: the renderer draws a page as one texture, so\n * a partial raster cannot be handed to it, and a crop would have to be re-rendered on every pan\n * anyway. Held apart from the byte-capped page cache, which is sized for fit-to-screen rasters.\n */\n #renderZoomed(index: number, zoom: number): Promise<PageContent> {\n const scale = Math.min(zoom, MAX_ZOOM_RENDER_SCALE);\n const key = `${index}@${scale}`;\n const cached = this.#zoomCache.get(key);\n if (cached) return cached;\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const promise = this.#rasterize(index, this.#renderScale * dpr * scale).catch(\n (error: unknown) => {\n this.#zoomCache.delete(key); // let a later zoom retry\n throw error;\n },\n );\n // One zoom level at a time, but both pages of the spread at that level.\n for (const [k, pending] of this.#zoomCache) {\n if (!k.endsWith(`@${scale}`)) {\n this.#zoomCache.delete(k);\n void pending.then(releaseCanvas).catch(() => {});\n }\n }\n this.#zoomCache.set(key, promise);\n return promise;\n }\n\n async #rasterize(index: number, scale: number): Promise<PageContent> {\n if (this.#doc === null) throw new Error('PdfSource: use after open() — the document is not loaded.');\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n const viewport = page.getViewport({ scale });\n const canvas = document.createElement('canvas');\n canvas.width = Math.ceil(viewport.width);\n canvas.height = Math.ceil(viewport.height);\n await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;\n return canvas;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#progressive","#disableAutoFetch","#legacy","#onUpdate","#onProgress","#doc","#resolveWorkerSrc","#renderZoomed","#renderPage","#textCache","#extractText","#destinationPage","#cache","#zoomCache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type {\n DownloadInfo,\n LoadProgress,\n OutlineItem,\n PageContent,\n PageRequest,\n Source,\n} from '@zinejs/core';\n\n/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */\ninterface PdfPageLike {\n getViewport(opts: { scale: number }): { width: number; height: number };\n /** `transform` is an affine matrix applied before painting, used to offset a cropped tile. */\n render(opts: { canvasContext: unknown; viewport: unknown; transform?: number[] }): {\n promise: Promise<void>;\n };\n /** Optional: absent on scanned PDFs with no text layer, and on hand-rolled document stubs. */\n getTextContent?(): Promise<{ items: { str?: string; hasEOL?: boolean }[] }>;\n}\n/** A pdf.js outline entry. `dest` is either a named destination or an explicit array whose first\n * element references the target page. */\ninterface PdfOutlineNode {\n title?: string;\n dest?: string | unknown[] | null;\n items?: PdfOutlineNode[];\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\n /** Optional: absent on documents without a table of contents, and on hand-rolled stubs. */\n getOutline?(): Promise<PdfOutlineNode[] | null>;\n getDestination?(id: string): Promise<unknown[] | null>;\n getPageIndex?(ref: unknown): Promise<number>;\n}\ninterface DocParams {\n url?: string;\n data?: ArrayBuffer | Uint8Array;\n disableAutoFetch?: boolean;\n}\n/** pdf.js's loading task: the document promise, plus an assignable download-progress callback. */\ninterface PdfLoadingTask {\n promise: Promise<PdfDocumentLike>;\n onProgress?: (p: LoadProgress) => void;\n}\ninterface PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: DocParams): PdfLoadingTask;\n}\n\n/** A URL, raw bytes, or a pre-created pdf.js document. */\nexport type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;\n\nexport interface PdfSourceOptions {\n /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */\n workerSrc?: string;\n /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */\n renderScale?: number;\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */\n maxCacheBytes?: number;\n /** Paint a low-res page first, then swap to crisp (faster first paint). Default false. */\n progressive?: boolean;\n /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */\n disableAutoFetch?: boolean;\n /**\n * Load pdf.js's legacy build (transpiled with polyfills) instead of the modern one. The legacy\n * build runs on both old and modern browsers; the modern build is smaller and a touch faster but\n * needs a recent engine. Default true, favouring reach. Set false to serve the lean modern build\n * when your audience is on current browsers. Ignored when a pre-created pdf.js document or a\n * global `pdfjsLib` is supplied — those already picked their build.\n */\n legacy?: boolean;\n}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\n// Linear-resolution fraction for the progressive low-res first pass (~1/9 the pixels).\nconst PROGRESSIVE_LOW_RATIO = 0.35;\n// Ceiling on zoom-tile magnification. Past roughly this point a screen pixel already holds more\n// detail than the reader can resolve, and rasterizing further only costs memory and time.\nconst MAX_ZOOM_RENDER_SCALE = 6;\n\n/** Free a canvas's backing store. Safari in particular holds the memory until the surface is\n * resized away, and a zoomed page is large enough for that to matter. */\nfunction releaseCanvas(content: PageContent): void {\n if (typeof HTMLCanvasElement !== 'undefined' && content instanceof HTMLCanvasElement) {\n content.width = 0;\n content.height = 0;\n }\n}\n\ninterface CacheEntry {\n promise: Promise<PageContent>;\n bytes: number; // 0 until the render resolves and its size is known\n}\n\nfunction isPdfDocument(src: PdfSrc): src is PdfDocumentLike {\n return (\n typeof src === 'object' &&\n src !== null &&\n 'numPages' in src &&\n typeof (src as PdfDocumentLike).getPage === 'function'\n );\n}\n\n/** Last path segment of a URL, for naming a download. Falls back when the URL has no filename\n * (a directory, a data: URI, or something unparseable). */\nfunction filenameFromUrl(url: string): string {\n try {\n const path = new URL(url, 'http://x').pathname;\n const last = path.split('/').filter(Boolean).pop();\n return last && /\\.[a-z0-9]+$/i.test(last) ? decodeURIComponent(last) : 'document.pdf';\n } catch {\n return 'document.pdf';\n }\n}\n\n/**\n * Load pdf.js. CDN / UMD hosts typically expose `globalThis.pdfjsLib` from a\n * `<script>` of pdf.js; bundlers have no global and resolve the dynamic import.\n *\n * `legacy` picks the transpiled build, which runs on older engines too. Its entry and worker are a\n * matched pair, so `#resolveWorkerSrc` reads the same flag. Each specifier is a plain string literal\n * so a bundler can see and emit the chosen chunk; the branch means only the selected one is fetched.\n */\nasync function loadPdfjs(legacy: boolean): Promise<PdfjsModule> {\n const g = globalThis as typeof globalThis & { pdfjsLib?: PdfjsModule };\n if (g.pdfjsLib) return g.pdfjsLib;\n const mod = legacy ? await import('pdfjs-dist/legacy/build/pdf.mjs') : await import('pdfjs-dist');\n return mod 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 #legacy: boolean;\n #onProgress: ((progress: LoadProgress) => void) | null = null;\n #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\n #onUpdate: ((index: number) => void) | null = null;\n /** Zoomed page rasters keyed `index@scale`, apart from #cache: one is worth many normal pages\n * and would evict the whole spread from a byte cap sized for fit-to-screen. */\n #zoomCache = new Map<string, Promise<PageContent>>();\n\n /** Extracted page text, kept apart from #cache: text is scale-independent and negligible next\n * to a raster, so it has no place in the byte budget that evicts bitmaps. */\n #textCache = new Map<number, Promise<string>>();\n\n constructor(src: PdfSrc, options: PdfSourceOptions = {}) {\n this.#src = src;\n this.#workerSrc = options.workerSrc;\n this.#renderScale = options.renderScale ?? 1;\n this.#preload = options.preload ?? 1;\n this.#maxCacheBytes = options.maxCacheBytes ?? DEFAULT_MAX_CACHE_BYTES;\n this.#progressive = options.progressive ?? false;\n this.#disableAutoFetch = options.disableAutoFetch ?? false;\n this.#legacy = options.legacy ?? true;\n }\n\n /** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */\n onPageUpdate(handler: (index: number) => void): void {\n this.#onUpdate = handler;\n }\n\n /** Register a handler called as the document downloads, driving a loading indicator. */\n onProgress(handler: (progress: LoadProgress) => void): void {\n this.#onProgress = handler;\n }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = await loadPdfjs(this.#legacy);\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params: DocParams =\n typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\n if (this.#disableAutoFetch) params.disableAutoFetch = true;\n const task = pdfjs.getDocument(params);\n if (this.#onProgress) task.onProgress = this.#onProgress;\n this.#doc = await task.promise;\n }\n this.pageCount = this.#doc.numPages;\n }\n\n /** Precedence: an explicit `workerSrc` → an already-configured global → an auto-resolved default. */\n #resolveWorkerSrc(configured: string): string {\n if (this.#workerSrc) return this.#workerSrc;\n if (configured) return configured;\n try {\n // Bundlers (Vite / webpack 5 / esbuild) statically rewrite these and emit the worker,\n // so the common case needs no `workerSrc`. Non-bundler / UMD hosts pass it explicitly. The\n // worker must match the build loaded in loadPdfjs — a legacy main with a modern worker (or\n // vice versa) mismatches — so the same #legacy flag chooses here too.\n return this.#legacy\n ? \"@@ZINE_PDF_WORKER:pdfjs-dist/legacy/build/pdf.worker.min.mjs@@\"\n : \"@@ZINE_PDF_WORKER:pdfjs-dist/build/pdf.worker.min.mjs@@\";\n } catch {\n throw new Error(\n \"PdfSource: couldn't auto-resolve pdf.js's worker. Pass a `workerSrc` URL \" +\n '(see the @zinejs/pdf README, \"The pdf.js worker\"), set pdfjs ' +\n 'GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.',\n );\n }\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n // A zoomed request wants detail the fit-to-screen raster never had. PDF pages are vector, so\n // re-render at the magnification being viewed. Memoized per zoom level, not in the byte-capped\n // page cache: a zoomed page is many times the size of a normal one and would evict the whole\n // spread, and a reader who zooms out and back in should not pay to render it twice.\n if (opts && opts.scale > 1) return this.#renderZoomed(index, opts.scale, opts.maxSize);\n\n const decoded = this.#renderPage(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#renderPage(i).catch(() => {}); // prefetch failures surface on a real get()\n }\n }\n }\n\n /**\n * The page's text, for search. Empty for a page with no text layer (a scan, or a pdf.js build\n * without text extraction) — callers treat that as \"nothing to match\" rather than an error.\n */\n getText(index: number): Promise<string> {\n const cached = this.#textCache.get(index);\n if (cached) return cached;\n const pending = this.#extractText(index).catch(() => '');\n this.#textCache.set(index, pending);\n return pending;\n }\n\n /**\n * The document's table of contents, flattened into the shape the outline panel wants.\n *\n * Empty when the PDF has no outline, which is common. Destinations are resolved to page\n * indices here rather than in the UI: doing it once, up front, keeps clicking an entry\n * instant, and an entry whose destination cannot be resolved is kept with a null page so the\n * heading still shows.\n */\n async getOutline(): Promise<OutlineItem[]> {\n const doc = this.#doc;\n if (!doc || typeof doc.getOutline !== 'function') return [];\n let nodes: PdfOutlineNode[] | null;\n try {\n nodes = await doc.getOutline();\n } catch {\n return [];\n }\n if (!nodes?.length) return [];\n const convert = async (list: PdfOutlineNode[]): Promise<OutlineItem[]> =>\n Promise.all(\n list.map(async (node) => ({\n title: (node.title ?? '').trim() || 'Untitled',\n page: await this.#destinationPage(node.dest),\n children: node.items?.length ? await convert(node.items) : [],\n })),\n );\n return convert(nodes);\n }\n\n /** Resolve a pdf.js destination to a zero-based page index, or null if it cannot be. */\n async #destinationPage(dest: string | unknown[] | null | undefined): Promise<number | null> {\n const doc = this.#doc;\n if (!doc || dest == null) return null;\n try {\n // A named destination has to be looked up before it yields the usual explicit array.\n const explicit =\n typeof dest === 'string' ? await doc.getDestination?.(dest) : (dest as unknown[]);\n const ref = explicit?.[0];\n if (ref == null) return null;\n // Some destinations carry a bare page number instead of a reference.\n if (typeof ref === 'number') return ref;\n const index = await doc.getPageIndex?.(ref);\n return typeof index === 'number' ? index : null;\n } catch {\n return null; // a broken destination should not cost us the whole outline\n }\n }\n\n /**\n * The original PDF, for a download control.\n *\n * Null when the source was handed a pre-opened pdf.js document: the bytes belong to whoever\n * created it, and re-fetching them is not this class's call to make.\n */\n async getDownload(): Promise<DownloadInfo | null> {\n const src = this.#src;\n if (typeof src === 'string') {\n return { url: src, filename: filenameFromUrl(src) };\n }\n if (src instanceof ArrayBuffer || ArrayBuffer.isView(src)) {\n const blob = new Blob([src as BlobPart], { type: 'application/pdf' });\n return { url: URL.createObjectURL(blob), filename: 'document.pdf', revoke: true };\n }\n return null;\n }\n\n async #extractText(index: number): Promise<string> {\n if (this.#doc === null) {\n throw new Error('PdfSource: use after open() — the document is not loaded.');\n }\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n if (typeof page.getTextContent !== 'function') return '';\n const content = await page.getTextContent();\n let text = '';\n for (const item of content.items) {\n if (typeof item.str !== 'string') continue; // marked-content entries carry no text\n text += item.str;\n // pdf.js splits a visual line into runs; only hasEOL ends one.\n text += item.hasEOL ? '\\n' : '';\n }\n return text;\n }\n\n destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.clear();\n for (const pending of this.#zoomCache.values()) {\n void pending.then(releaseCanvas).catch(() => {});\n }\n this.#zoomCache.clear();\n this.#textCache.clear();\n this.#cachedBytes = 0;\n this.#doc = null;\n }\n\n #renderPage(index: number): Promise<PageContent> {\n const existing = this.#cache.get(index);\n if (existing) {\n // Mark most-recently-used: re-insert so it sits at the end of the Map's order.\n this.#cache.delete(index);\n this.#cache.set(index, existing);\n return existing.promise;\n }\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const full = this.#renderScale * dpr;\n const firstScale = this.#progressive ? full * PROGRESSIVE_LOW_RATIO : full;\n\n const entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index, firstScale).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n if (this.#progressive) this.#upgrade(index, entry, full);\n return content;\n },\n (error: unknown) => {\n this.#cache.delete(index); // let a later get() retry\n throw error;\n },\n );\n this.#cache.set(index, entry);\n return entry.promise;\n }\n\n /** Progressive: re-render at full resolution, swap it into the cache, and signal an update. */\n #upgrade(index: number, entry: CacheEntry, fullScale: number): void {\n void this.#rasterize(index, fullScale)\n .then((hi) => {\n if (this.#cache.get(index) !== entry) return; // evicted or replaced meanwhile\n this.#cachedBytes -= entry.bytes;\n entry.promise = Promise.resolve(hi);\n entry.bytes = hi.width * hi.height * 4;\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n this.#onUpdate?.(index);\n })\n .catch(() => {}); // keep the low-res if the upgrade fails\n }\n\n /** Drop least-recently-used pages (front of the Map) until under the byte cap. */\n #evictToFit(keepIndex: number): void {\n for (const [index, entry] of this.#cache) {\n if (this.#cachedBytes <= this.#maxCacheBytes) break;\n if (index === keepIndex || entry.bytes === 0) continue; // keep the newest; skip in-flight renders\n this.#cache.delete(index);\n this.#cachedBytes -= entry.bytes;\n }\n }\n\n /**\n * Render a whole page at zoom magnification, memoized by page and zoom level.\n *\n * Whole-page rather than a crop of what is visible: the renderer draws a page as one texture, so\n * a partial raster cannot be handed to it, and a crop would have to be re-rendered on every pan\n * anyway. Held apart from the byte-capped page cache, which is sized for fit-to-screen rasters.\n */\n #renderZoomed(index: number, zoom: number, maxSize?: number): Promise<PageContent> {\n const scale = Math.min(zoom, MAX_ZOOM_RENDER_SCALE);\n const key = `${index}@${scale}`;\n const cached = this.#zoomCache.get(key);\n if (cached) return cached;\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const promise = this.#rasterize(index, this.#renderScale * dpr * scale, maxSize).catch(\n (error: unknown) => {\n this.#zoomCache.delete(key); // let a later zoom retry\n throw error;\n },\n );\n // One zoom level at a time, but both pages of the spread at that level.\n for (const [k, pending] of this.#zoomCache) {\n if (!k.endsWith(`@${scale}`)) {\n this.#zoomCache.delete(k);\n void pending.then(releaseCanvas).catch(() => {});\n }\n }\n this.#zoomCache.set(key, promise);\n return promise;\n }\n\n async #rasterize(index: number, scale: number, maxSize?: 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 // Cap the scale so neither axis exceeds the GPU's texture limit: past it the upload paints black\n // (the renderer downscales as a backstop, but rendering to fit avoids the wasted memory spike).\n if (maxSize !== undefined && maxSize > 0) {\n const base = page.getViewport({ scale: 1 });\n const fit = maxSize / Math.max(base.width, base.height);\n scale = Math.min(scale, fit);\n }\n const viewport = page.getViewport({ scale });\n const canvas = document.createElement('canvas');\n canvas.width = Math.ceil(viewport.width);\n canvas.height = Math.ceil(viewport.height);\n await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;\n return canvas;\n }\n}\n"],"mappings":"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"}
package/dist/index.umd.js CHANGED
@@ -1,3 +1,3 @@
1
- (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.ZineJS=e.ZineJS||{}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=256*1024*1024,n=.35,r=6;function i(e){typeof HTMLCanvasElement<`u`&&e instanceof HTMLCanvasElement&&(e.width=0,e.height=0)}function a(e){return typeof e==`object`&&!!e&&`numPages`in e&&typeof e.getPage==`function`}function o(e){try{let t=new URL(e,`http://x`).pathname.split(`/`).filter(Boolean).pop();return t&&/\.[a-z0-9]+$/i.test(t)?decodeURIComponent(t):`document.pdf`}catch{return`document.pdf`}}async function s(){let e=globalThis;return e.pdfjsLib?e.pdfjsLib:await import(`pdfjs-dist`)}e.PdfSource=class{pageCount=0;#e;#t;#n;#r;#i=null;#a;#o;#s;#c=null;#l=new Map;#u=0;#d=null;#f=new Map;#p=new Map;constructor(e,n={}){this.#e=e,this.#t=n.workerSrc,this.#n=n.renderScale??1,this.#r=n.preload??1,this.#a=n.maxCacheBytes??t,this.#o=n.progressive??!1,this.#s=n.disableAutoFetch??!1}onPageUpdate(e){this.#d=e}onProgress(e){this.#c=e}async open(){if(a(this.#e))this.#i=this.#e;else{let e=await s();e.GlobalWorkerOptions.workerSrc=this.#m(e.GlobalWorkerOptions.workerSrc);let t=typeof this.#e==`string`?{url:this.#e}:{data:this.#e};this.#s&&(t.disableAutoFetch=!0);let n=e.getDocument(t);this.#c&&(n.onProgress=this.#c),this.#i=await n.promise}this.pageCount=this.#i.numPages}#m(e){if(this.#t)return this.#t;if(e)return e;try{return(() => { throw new Error('PdfSource: pass workerSrc for UMD/CDN'); })()}catch{throw Error(`PdfSource: couldn't auto-resolve pdf.js's worker. Pass a \`workerSrc\` URL (see the @zinejs/pdf README, "The pdf.js worker"), set pdfjs GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.`)}}get(e,t){if(t&&t.scale>1)return this.#b(e,t.scale);let n=this.#_(e);for(let t=1;t<=this.#r;t++)this.prefetch([e-t,e+t]);return n}prefetch(e){for(let t of e)t>=0&&t<this.pageCount&&this.#_(t).catch(()=>{})}getText(e){let t=this.#p.get(e);if(t)return t;let n=this.#g(e).catch(()=>``);return this.#p.set(e,n),n}async getOutline(){let e=this.#i;if(!e||typeof e.getOutline!=`function`)return[];let t;try{t=await e.getOutline()}catch{return[]}if(!t?.length)return[];let n=async e=>Promise.all(e.map(async e=>({title:(e.title??``).trim()||`Untitled`,page:await this.#h(e.dest),children:e.items?.length?await n(e.items):[]})));return n(t)}async#h(e){let t=this.#i;if(!t||e==null)return null;try{let n=(typeof e==`string`?await t.getDestination?.(e):e)?.[0];if(n==null)return null;if(typeof n==`number`)return n;let r=await t.getPageIndex?.(n);return typeof r==`number`?r:null}catch{return null}}async getDownload(){let e=this.#e;if(typeof e==`string`)return{url:e,filename:o(e)};if(e instanceof ArrayBuffer||ArrayBuffer.isView(e)){let t=new Blob([e],{type:`application/pdf`});return{url:URL.createObjectURL(t),filename:`document.pdf`,revoke:!0}}return null}async#g(e){if(this.#i===null)throw Error(`PdfSource: use after open() — the document is not loaded.`);let t=await this.#i.getPage(e+1);if(typeof t.getTextContent!=`function`)return``;let n=await t.getTextContent(),r=``;for(let e of n.items)typeof e.str==`string`&&(r+=e.str,r+=e.hasEOL?`
2
- `:``);return r}destroy(){this.#i?.destroy?.(),this.#l.clear();for(let e of this.#f.values())e.then(i).catch(()=>{});this.#f.clear(),this.#p.clear(),this.#u=0,this.#i=null}#_(e){let t=this.#l.get(e);if(t)return this.#l.delete(e),this.#l.set(e,t),t.promise;let r=typeof devicePixelRatio==`number`?devicePixelRatio:1,i=this.#n*r,a=this.#o?i*n:i,o={promise:void 0,bytes:0};return o.promise=this.#x(e,a).then(t=>(o.bytes=t.width*t.height*4,this.#u+=o.bytes,this.#y(e),this.#o&&this.#v(e,o,i),t),t=>{throw this.#l.delete(e),t}),this.#l.set(e,o),o.promise}#v(e,t,n){this.#x(e,n).then(n=>{this.#l.get(e)===t&&(this.#u-=t.bytes,t.promise=Promise.resolve(n),t.bytes=n.width*n.height*4,this.#u+=t.bytes,this.#y(e),this.#d?.(e))}).catch(()=>{})}#y(e){for(let[t,n]of this.#l){if(this.#u<=this.#a)break;t===e||n.bytes===0||(this.#l.delete(t),this.#u-=n.bytes)}}#b(e,t){let n=Math.min(t,r),a=`${e}@${n}`,o=this.#f.get(a);if(o)return o;let s=typeof devicePixelRatio==`number`?devicePixelRatio:1,c=this.#x(e,this.#n*s*n).catch(e=>{throw this.#f.delete(a),e});for(let[e,t]of this.#f)e.endsWith(`@${n}`)||(this.#f.delete(e),t.then(i).catch(()=>{}));return this.#f.set(a,c),c}async#x(e,t){if(this.#i===null)throw Error(`PdfSource: use after open() — the document is not loaded.`);let n=await this.#i.getPage(e+1),r=n.getViewport({scale:t}),i=document.createElement(`canvas`);return i.width=Math.ceil(r.width),i.height=Math.ceil(r.height),await n.render({canvasContext:i.getContext(`2d`),viewport:r}).promise,i}}});
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.ZineJS=e.ZineJS||{}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=256*1024*1024,n=.35,r=6;function i(e){typeof HTMLCanvasElement<`u`&&e instanceof HTMLCanvasElement&&(e.width=0,e.height=0)}function a(e){return typeof e==`object`&&!!e&&`numPages`in e&&typeof e.getPage==`function`}function o(e){try{let t=new URL(e,`http://x`).pathname.split(`/`).filter(Boolean).pop();return t&&/\.[a-z0-9]+$/i.test(t)?decodeURIComponent(t):`document.pdf`}catch{return`document.pdf`}}async function s(e){let t=globalThis;return t.pdfjsLib?t.pdfjsLib:e?await import(`pdfjs-dist/legacy/build/pdf.mjs`):await import(`pdfjs-dist`)}e.PdfSource=class{pageCount=0;#e;#t;#n;#r;#i=null;#a;#o;#s;#c;#l=null;#u=new Map;#d=0;#f=null;#p=new Map;#m=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,this.#c=n.legacy??!0}onPageUpdate(e){this.#f=e}onProgress(e){this.#l=e}async open(){if(a(this.#e))this.#i=this.#e;else{let e=await s(this.#c);e.GlobalWorkerOptions.workerSrc=this.#h(e.GlobalWorkerOptions.workerSrc);let t=typeof this.#e==`string`?{url:this.#e}:{data:this.#e};this.#s&&(t.disableAutoFetch=!0);let n=e.getDocument(t);this.#l&&(n.onProgress=this.#l),this.#i=await n.promise}this.pageCount=this.#i.numPages}#h(e){if(this.#t)return this.#t;if(e)return e;try{return this.#c?(() => { throw new Error('PdfSource: pass workerSrc for UMD/CDN'); })():(() => { 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.#x(e,t.scale,t.maxSize);let n=this.#v(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.#v(t).catch(()=>{})}getText(e){let t=this.#m.get(e);if(t)return t;let n=this.#_(e).catch(()=>``);return this.#m.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.#g(e.dest),children:e.items?.length?await n(e.items):[]})));return n(t)}async#g(e){let t=this.#i;if(!t||e==null)return null;try{let n=(typeof e==`string`?await t.getDestination?.(e):e)?.[0];if(n==null)return null;if(typeof n==`number`)return n;let r=await t.getPageIndex?.(n);return typeof r==`number`?r:null}catch{return null}}async getDownload(){let e=this.#e;if(typeof e==`string`)return{url:e,filename:o(e)};if(e instanceof ArrayBuffer||ArrayBuffer.isView(e)){let t=new Blob([e],{type:`application/pdf`});return{url:URL.createObjectURL(t),filename:`document.pdf`,revoke:!0}}return null}async#_(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.#u.clear();for(let e of this.#p.values())e.then(i).catch(()=>{});this.#p.clear(),this.#m.clear(),this.#d=0,this.#i=null}#v(e){let t=this.#u.get(e);if(t)return this.#u.delete(e),this.#u.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.#S(e,a).then(t=>(o.bytes=t.width*t.height*4,this.#d+=o.bytes,this.#b(e),this.#o&&this.#y(e,o,i),t),t=>{throw this.#u.delete(e),t}),this.#u.set(e,o),o.promise}#y(e,t,n){this.#S(e,n).then(n=>{this.#u.get(e)===t&&(this.#d-=t.bytes,t.promise=Promise.resolve(n),t.bytes=n.width*n.height*4,this.#d+=t.bytes,this.#b(e),this.#f?.(e))}).catch(()=>{})}#b(e){for(let[t,n]of this.#u){if(this.#d<=this.#a)break;t===e||n.bytes===0||(this.#u.delete(t),this.#d-=n.bytes)}}#x(e,t,n){let a=Math.min(t,r),o=`${e}@${a}`,s=this.#p.get(o);if(s)return s;let c=typeof devicePixelRatio==`number`?devicePixelRatio:1,l=this.#S(e,this.#n*c*a,n).catch(e=>{throw this.#p.delete(o),e});for(let[e,t]of this.#p)e.endsWith(`@${a}`)||(this.#p.delete(e),t.then(i).catch(()=>{}));return this.#p.set(o,l),l}async#S(e,t,n){if(this.#i===null)throw Error(`PdfSource: use after open() — the document is not loaded.`);let r=await this.#i.getPage(e+1);if(n!==void 0&&n>0){let e=r.getViewport({scale:1}),i=n/Math.max(e.width,e.height);t=Math.min(t,i)}let i=r.getViewport({scale:t}),a=document.createElement(`canvas`);return a.width=Math.ceil(i.width),a.height=Math.ceil(i.height),await r.render({canvasContext:a.getContext(`2d`),viewport:i}).promise,a}}});
3
3
  //# sourceMappingURL=index.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.umd.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#progressive","#disableAutoFetch","#onUpdate","#onProgress","#doc","#resolveWorkerSrc","#renderZoomed","#renderPage","#textCache","#extractText","#destinationPage","#cache","#zoomCache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type {\n DownloadInfo,\n LoadProgress,\n OutlineItem,\n PageContent,\n PageRequest,\n Source,\n} from '@zinejs/core';\n\n/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */\ninterface PdfPageLike {\n getViewport(opts: { scale: number }): { width: number; height: number };\n /** `transform` is an affine matrix applied before painting, used to offset a cropped tile. */\n render(opts: { canvasContext: unknown; viewport: unknown; transform?: number[] }): {\n promise: Promise<void>;\n };\n /** Optional: absent on scanned PDFs with no text layer, and on hand-rolled document stubs. */\n getTextContent?(): Promise<{ items: { str?: string; hasEOL?: boolean }[] }>;\n}\n/** A pdf.js outline entry. `dest` is either a named destination or an explicit array whose first\n * element references the target page. */\ninterface PdfOutlineNode {\n title?: string;\n dest?: string | unknown[] | null;\n items?: PdfOutlineNode[];\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\n /** Optional: absent on documents without a table of contents, and on hand-rolled stubs. */\n getOutline?(): Promise<PdfOutlineNode[] | null>;\n getDestination?(id: string): Promise<unknown[] | null>;\n getPageIndex?(ref: unknown): Promise<number>;\n}\ninterface DocParams {\n url?: string;\n data?: ArrayBuffer | Uint8Array;\n disableAutoFetch?: boolean;\n}\n/** pdf.js's loading task: the document promise, plus an assignable download-progress callback. */\ninterface PdfLoadingTask {\n promise: Promise<PdfDocumentLike>;\n onProgress?: (p: LoadProgress) => void;\n}\ninterface PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: DocParams): PdfLoadingTask;\n}\n\n/** A URL, raw bytes, or a pre-created pdf.js document. */\nexport type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;\n\nexport interface PdfSourceOptions {\n /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */\n workerSrc?: string;\n /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */\n renderScale?: number;\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */\n maxCacheBytes?: number;\n /** Paint a low-res page first, then swap to crisp (faster first paint). Default false. */\n progressive?: boolean;\n /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */\n disableAutoFetch?: boolean;\n}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\n// Linear-resolution fraction for the progressive low-res first pass (~1/9 the pixels).\nconst PROGRESSIVE_LOW_RATIO = 0.35;\n// Ceiling on zoom-tile magnification. Past roughly this point a screen pixel already holds more\n// detail than the reader can resolve, and rasterizing further only costs memory and time.\nconst MAX_ZOOM_RENDER_SCALE = 6;\n\n/** Free a canvas's backing store. Safari in particular holds the memory until the surface is\n * resized away, and a zoomed page is large enough for that to matter. */\nfunction releaseCanvas(content: PageContent): void {\n if (typeof HTMLCanvasElement !== 'undefined' && content instanceof HTMLCanvasElement) {\n content.width = 0;\n content.height = 0;\n }\n}\n\ninterface CacheEntry {\n promise: Promise<PageContent>;\n bytes: number; // 0 until the render resolves and its size is known\n}\n\nfunction isPdfDocument(src: PdfSrc): src is PdfDocumentLike {\n return (\n typeof src === 'object' &&\n src !== null &&\n 'numPages' in src &&\n typeof (src as PdfDocumentLike).getPage === 'function'\n );\n}\n\n/** Last path segment of a URL, for naming a download. Falls back when the URL has no filename\n * (a directory, a data: URI, or something unparseable). */\nfunction filenameFromUrl(url: string): string {\n try {\n const path = new URL(url, 'http://x').pathname;\n const last = path.split('/').filter(Boolean).pop();\n return last && /\\.[a-z0-9]+$/i.test(last) ? decodeURIComponent(last) : 'document.pdf';\n } catch {\n return 'document.pdf';\n }\n}\n\n/**\n * Load pdf.js. CDN / UMD hosts typically expose `globalThis.pdfjsLib` from a\n * `<script>` of pdf.js; bundlers have no global and resolve the dynamic import.\n */\nasync function loadPdfjs(): Promise<PdfjsModule> {\n const g = globalThis as typeof globalThis & { pdfjsLib?: PdfjsModule };\n if (g.pdfjsLib) return g.pdfjsLib;\n return (await import('pdfjs-dist')) as unknown as PdfjsModule;\n}\n\n/**\n * Renders PDF pages to canvases for zinejs. pdf.js is a dependency of this\n * package, loaded lazily so it never lands in the core bundle. Async — pass it\n * to `new Zine`, which calls `open()` (learning the page count) before building\n * spreads.\n */\nexport class PdfSource implements Source {\n pageCount = 0;\n #src: PdfSrc;\n #workerSrc: string | undefined;\n #renderScale: number;\n #preload: number;\n #doc: PdfDocumentLike | null = null;\n #maxCacheBytes: number;\n #progressive: boolean;\n #disableAutoFetch: boolean;\n #onProgress: ((progress: LoadProgress) => void) | null = null;\n #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\n #onUpdate: ((index: number) => void) | null = null;\n /** Zoomed page rasters keyed `index@scale`, apart from #cache: one is worth many normal pages\n * and would evict the whole spread from a byte cap sized for fit-to-screen. */\n #zoomCache = new Map<string, Promise<PageContent>>();\n\n /** Extracted page text, kept apart from #cache: text is scale-independent and negligible next\n * to a raster, so it has no place in the byte budget that evicts bitmaps. */\n #textCache = new Map<number, Promise<string>>();\n\n constructor(src: PdfSrc, options: PdfSourceOptions = {}) {\n this.#src = src;\n this.#workerSrc = options.workerSrc;\n this.#renderScale = options.renderScale ?? 1;\n this.#preload = options.preload ?? 1;\n this.#maxCacheBytes = options.maxCacheBytes ?? DEFAULT_MAX_CACHE_BYTES;\n this.#progressive = options.progressive ?? false;\n this.#disableAutoFetch = options.disableAutoFetch ?? false;\n }\n\n /** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */\n onPageUpdate(handler: (index: number) => void): void {\n this.#onUpdate = handler;\n }\n\n /** Register a handler called as the document downloads, driving a loading indicator. */\n onProgress(handler: (progress: LoadProgress) => void): void {\n this.#onProgress = handler;\n }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = await loadPdfjs();\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params: DocParams =\n typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\n if (this.#disableAutoFetch) params.disableAutoFetch = true;\n const task = pdfjs.getDocument(params);\n if (this.#onProgress) task.onProgress = this.#onProgress;\n this.#doc = await task.promise;\n }\n this.pageCount = this.#doc.numPages;\n }\n\n /** Precedence: an explicit `workerSrc` → an already-configured global → an auto-resolved default. */\n #resolveWorkerSrc(configured: string): string {\n if (this.#workerSrc) return this.#workerSrc;\n if (configured) return configured;\n try {\n // Bundlers (Vite / webpack 5 / esbuild) statically rewrite this and emit the worker,\n // so the common case needs no `workerSrc`. Non-bundler / UMD hosts pass it explicitly.\n return \"@@ZINE_PDF_WORKER_URL@@\";\n } catch {\n throw new Error(\n \"PdfSource: couldn't auto-resolve pdf.js's worker. Pass a `workerSrc` URL \" +\n '(see the @zinejs/pdf README, \"The pdf.js worker\"), set pdfjs ' +\n 'GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.',\n );\n }\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n // A zoomed request wants detail the fit-to-screen raster never had. PDF pages are vector, so\n // re-render at the magnification being viewed. Memoized per zoom level, not in the byte-capped\n // page cache: a zoomed page is many times the size of a normal one and would evict the whole\n // spread, and a reader who zooms out and back in should not pay to render it twice.\n if (opts && opts.scale > 1) return this.#renderZoomed(index, opts.scale);\n\n const decoded = this.#renderPage(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#renderPage(i).catch(() => {}); // prefetch failures surface on a real get()\n }\n }\n }\n\n /**\n * The page's text, for search. Empty for a page with no text layer (a scan, or a pdf.js build\n * without text extraction) — callers treat that as \"nothing to match\" rather than an error.\n */\n getText(index: number): Promise<string> {\n const cached = this.#textCache.get(index);\n if (cached) return cached;\n const pending = this.#extractText(index).catch(() => '');\n this.#textCache.set(index, pending);\n return pending;\n }\n\n /**\n * The document's table of contents, flattened into the shape the outline panel wants.\n *\n * Empty when the PDF has no outline, which is common. Destinations are resolved to page\n * indices here rather than in the UI: doing it once, up front, keeps clicking an entry\n * instant, and an entry whose destination cannot be resolved is kept with a null page so the\n * heading still shows.\n */\n async getOutline(): Promise<OutlineItem[]> {\n const doc = this.#doc;\n if (!doc || typeof doc.getOutline !== 'function') return [];\n let nodes: PdfOutlineNode[] | null;\n try {\n nodes = await doc.getOutline();\n } catch {\n return [];\n }\n if (!nodes?.length) return [];\n const convert = async (list: PdfOutlineNode[]): Promise<OutlineItem[]> =>\n Promise.all(\n list.map(async (node) => ({\n title: (node.title ?? '').trim() || 'Untitled',\n page: await this.#destinationPage(node.dest),\n children: node.items?.length ? await convert(node.items) : [],\n })),\n );\n return convert(nodes);\n }\n\n /** Resolve a pdf.js destination to a zero-based page index, or null if it cannot be. */\n async #destinationPage(dest: string | unknown[] | null | undefined): Promise<number | null> {\n const doc = this.#doc;\n if (!doc || dest == null) return null;\n try {\n // A named destination has to be looked up before it yields the usual explicit array.\n const explicit =\n typeof dest === 'string' ? await doc.getDestination?.(dest) : (dest as unknown[]);\n const ref = explicit?.[0];\n if (ref == null) return null;\n // Some destinations carry a bare page number instead of a reference.\n if (typeof ref === 'number') return ref;\n const index = await doc.getPageIndex?.(ref);\n return typeof index === 'number' ? index : null;\n } catch {\n return null; // a broken destination should not cost us the whole outline\n }\n }\n\n /**\n * The original PDF, for a download control.\n *\n * Null when the source was handed a pre-opened pdf.js document: the bytes belong to whoever\n * created it, and re-fetching them is not this class's call to make.\n */\n async getDownload(): Promise<DownloadInfo | null> {\n const src = this.#src;\n if (typeof src === 'string') {\n return { url: src, filename: filenameFromUrl(src) };\n }\n if (src instanceof ArrayBuffer || ArrayBuffer.isView(src)) {\n const blob = new Blob([src as BlobPart], { type: 'application/pdf' });\n return { url: URL.createObjectURL(blob), filename: 'document.pdf', revoke: true };\n }\n return null;\n }\n\n async #extractText(index: number): Promise<string> {\n if (this.#doc === null) {\n throw new Error('PdfSource: use after open() — the document is not loaded.');\n }\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n if (typeof page.getTextContent !== 'function') return '';\n const content = await page.getTextContent();\n let text = '';\n for (const item of content.items) {\n if (typeof item.str !== 'string') continue; // marked-content entries carry no text\n text += item.str;\n // pdf.js splits a visual line into runs; only hasEOL ends one.\n text += item.hasEOL ? '\\n' : '';\n }\n return text;\n }\n\n destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.clear();\n for (const pending of this.#zoomCache.values()) {\n void pending.then(releaseCanvas).catch(() => {});\n }\n this.#zoomCache.clear();\n this.#textCache.clear();\n this.#cachedBytes = 0;\n this.#doc = null;\n }\n\n #renderPage(index: number): Promise<PageContent> {\n const existing = this.#cache.get(index);\n if (existing) {\n // Mark most-recently-used: re-insert so it sits at the end of the Map's order.\n this.#cache.delete(index);\n this.#cache.set(index, existing);\n return existing.promise;\n }\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const full = this.#renderScale * dpr;\n const firstScale = this.#progressive ? full * PROGRESSIVE_LOW_RATIO : full;\n\n const entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index, firstScale).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n if (this.#progressive) this.#upgrade(index, entry, full);\n return content;\n },\n (error: unknown) => {\n this.#cache.delete(index); // let a later get() retry\n throw error;\n },\n );\n this.#cache.set(index, entry);\n return entry.promise;\n }\n\n /** Progressive: re-render at full resolution, swap it into the cache, and signal an update. */\n #upgrade(index: number, entry: CacheEntry, fullScale: number): void {\n void this.#rasterize(index, fullScale)\n .then((hi) => {\n if (this.#cache.get(index) !== entry) return; // evicted or replaced meanwhile\n this.#cachedBytes -= entry.bytes;\n entry.promise = Promise.resolve(hi);\n entry.bytes = hi.width * hi.height * 4;\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n this.#onUpdate?.(index);\n })\n .catch(() => {}); // keep the low-res if the upgrade fails\n }\n\n /** Drop least-recently-used pages (front of the Map) until under the byte cap. */\n #evictToFit(keepIndex: number): void {\n for (const [index, entry] of this.#cache) {\n if (this.#cachedBytes <= this.#maxCacheBytes) break;\n if (index === keepIndex || entry.bytes === 0) continue; // keep the newest; skip in-flight renders\n this.#cache.delete(index);\n this.#cachedBytes -= entry.bytes;\n }\n }\n\n /**\n * Render a whole page at zoom magnification, memoized by page and zoom level.\n *\n * Whole-page rather than a crop of what is visible: the renderer draws a page as one texture, so\n * a partial raster cannot be handed to it, and a crop would have to be re-rendered on every pan\n * anyway. Held apart from the byte-capped page cache, which is sized for fit-to-screen rasters.\n */\n #renderZoomed(index: number, zoom: number): Promise<PageContent> {\n const scale = Math.min(zoom, MAX_ZOOM_RENDER_SCALE);\n const key = `${index}@${scale}`;\n const cached = this.#zoomCache.get(key);\n if (cached) return cached;\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const promise = this.#rasterize(index, this.#renderScale * dpr * scale).catch(\n (error: unknown) => {\n this.#zoomCache.delete(key); // let a later zoom retry\n throw error;\n },\n );\n // One zoom level at a time, but both pages of the spread at that level.\n for (const [k, pending] of this.#zoomCache) {\n if (!k.endsWith(`@${scale}`)) {\n this.#zoomCache.delete(k);\n void pending.then(releaseCanvas).catch(() => {});\n }\n }\n this.#zoomCache.set(key, promise);\n return promise;\n }\n\n async #rasterize(index: number, scale: number): Promise<PageContent> {\n if (this.#doc === null) throw new Error('PdfSource: use after open() — the document is not loaded.');\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n const viewport = page.getViewport({ scale });\n const canvas = document.createElement('canvas');\n canvas.width = Math.ceil(viewport.width);\n canvas.height = Math.ceil(viewport.height);\n await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;\n return canvas;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.umd.js","names":["#src","#workerSrc","#renderScale","#preload","#maxCacheBytes","#progressive","#disableAutoFetch","#legacy","#onUpdate","#onProgress","#doc","#resolveWorkerSrc","#renderZoomed","#renderPage","#textCache","#extractText","#destinationPage","#cache","#zoomCache","#cachedBytes","#rasterize","#evictToFit","#upgrade"],"sources":["../src/pdfSource.ts"],"sourcesContent":["import type {\n DownloadInfo,\n LoadProgress,\n OutlineItem,\n PageContent,\n PageRequest,\n Source,\n} from '@zinejs/core';\n\n/** Minimal shapes of the pdf.js API we rely on (avoids a hard type dependency). */\ninterface PdfPageLike {\n getViewport(opts: { scale: number }): { width: number; height: number };\n /** `transform` is an affine matrix applied before painting, used to offset a cropped tile. */\n render(opts: { canvasContext: unknown; viewport: unknown; transform?: number[] }): {\n promise: Promise<void>;\n };\n /** Optional: absent on scanned PDFs with no text layer, and on hand-rolled document stubs. */\n getTextContent?(): Promise<{ items: { str?: string; hasEOL?: boolean }[] }>;\n}\n/** A pdf.js outline entry. `dest` is either a named destination or an explicit array whose first\n * element references the target page. */\ninterface PdfOutlineNode {\n title?: string;\n dest?: string | unknown[] | null;\n items?: PdfOutlineNode[];\n}\ninterface PdfDocumentLike {\n numPages: number;\n getPage(pageNumber: number): Promise<PdfPageLike>;\n destroy?(): void;\n /** Optional: absent on documents without a table of contents, and on hand-rolled stubs. */\n getOutline?(): Promise<PdfOutlineNode[] | null>;\n getDestination?(id: string): Promise<unknown[] | null>;\n getPageIndex?(ref: unknown): Promise<number>;\n}\ninterface DocParams {\n url?: string;\n data?: ArrayBuffer | Uint8Array;\n disableAutoFetch?: boolean;\n}\n/** pdf.js's loading task: the document promise, plus an assignable download-progress callback. */\ninterface PdfLoadingTask {\n promise: Promise<PdfDocumentLike>;\n onProgress?: (p: LoadProgress) => void;\n}\ninterface PdfjsModule {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(src: DocParams): PdfLoadingTask;\n}\n\n/** A URL, raw bytes, or a pre-created pdf.js document. */\nexport type PdfSrc = string | ArrayBuffer | Uint8Array | PdfDocumentLike;\n\nexport interface PdfSourceOptions {\n /** URL to pdf.js's worker. Optional: auto-resolved under bundlers; pass it for CDN / UMD / custom paths. */\n workerSrc?: string;\n /** Base render scale; each page is rasterized at `renderScale × devicePixelRatio`. Default 1. */\n renderScale?: number;\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** Soft cap on cached page bytes; least-recently-used pages evict beyond it. Default ~256 MB. */\n maxCacheBytes?: number;\n /** Paint a low-res page first, then swap to crisp (faster first paint). Default false. */\n progressive?: boolean;\n /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */\n disableAutoFetch?: boolean;\n /**\n * Load pdf.js's legacy build (transpiled with polyfills) instead of the modern one. The legacy\n * build runs on both old and modern browsers; the modern build is smaller and a touch faster but\n * needs a recent engine. Default true, favouring reach. Set false to serve the lean modern build\n * when your audience is on current browsers. Ignored when a pre-created pdf.js document or a\n * global `pdfjsLib` is supplied — those already picked their build.\n */\n legacy?: boolean;\n}\n\nconst DEFAULT_MAX_CACHE_BYTES = 256 * 1024 * 1024;\n// Linear-resolution fraction for the progressive low-res first pass (~1/9 the pixels).\nconst PROGRESSIVE_LOW_RATIO = 0.35;\n// Ceiling on zoom-tile magnification. Past roughly this point a screen pixel already holds more\n// detail than the reader can resolve, and rasterizing further only costs memory and time.\nconst MAX_ZOOM_RENDER_SCALE = 6;\n\n/** Free a canvas's backing store. Safari in particular holds the memory until the surface is\n * resized away, and a zoomed page is large enough for that to matter. */\nfunction releaseCanvas(content: PageContent): void {\n if (typeof HTMLCanvasElement !== 'undefined' && content instanceof HTMLCanvasElement) {\n content.width = 0;\n content.height = 0;\n }\n}\n\ninterface CacheEntry {\n promise: Promise<PageContent>;\n bytes: number; // 0 until the render resolves and its size is known\n}\n\nfunction isPdfDocument(src: PdfSrc): src is PdfDocumentLike {\n return (\n typeof src === 'object' &&\n src !== null &&\n 'numPages' in src &&\n typeof (src as PdfDocumentLike).getPage === 'function'\n );\n}\n\n/** Last path segment of a URL, for naming a download. Falls back when the URL has no filename\n * (a directory, a data: URI, or something unparseable). */\nfunction filenameFromUrl(url: string): string {\n try {\n const path = new URL(url, 'http://x').pathname;\n const last = path.split('/').filter(Boolean).pop();\n return last && /\\.[a-z0-9]+$/i.test(last) ? decodeURIComponent(last) : 'document.pdf';\n } catch {\n return 'document.pdf';\n }\n}\n\n/**\n * Load pdf.js. CDN / UMD hosts typically expose `globalThis.pdfjsLib` from a\n * `<script>` of pdf.js; bundlers have no global and resolve the dynamic import.\n *\n * `legacy` picks the transpiled build, which runs on older engines too. Its entry and worker are a\n * matched pair, so `#resolveWorkerSrc` reads the same flag. Each specifier is a plain string literal\n * so a bundler can see and emit the chosen chunk; the branch means only the selected one is fetched.\n */\nasync function loadPdfjs(legacy: boolean): Promise<PdfjsModule> {\n const g = globalThis as typeof globalThis & { pdfjsLib?: PdfjsModule };\n if (g.pdfjsLib) return g.pdfjsLib;\n const mod = legacy ? await import('pdfjs-dist/legacy/build/pdf.mjs') : await import('pdfjs-dist');\n return mod 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 #legacy: boolean;\n #onProgress: ((progress: LoadProgress) => void) | null = null;\n #cache = new Map<number, CacheEntry>();\n #cachedBytes = 0;\n #onUpdate: ((index: number) => void) | null = null;\n /** Zoomed page rasters keyed `index@scale`, apart from #cache: one is worth many normal pages\n * and would evict the whole spread from a byte cap sized for fit-to-screen. */\n #zoomCache = new Map<string, Promise<PageContent>>();\n\n /** Extracted page text, kept apart from #cache: text is scale-independent and negligible next\n * to a raster, so it has no place in the byte budget that evicts bitmaps. */\n #textCache = new Map<number, Promise<string>>();\n\n constructor(src: PdfSrc, options: PdfSourceOptions = {}) {\n this.#src = src;\n this.#workerSrc = options.workerSrc;\n this.#renderScale = options.renderScale ?? 1;\n this.#preload = options.preload ?? 1;\n this.#maxCacheBytes = options.maxCacheBytes ?? DEFAULT_MAX_CACHE_BYTES;\n this.#progressive = options.progressive ?? false;\n this.#disableAutoFetch = options.disableAutoFetch ?? false;\n this.#legacy = options.legacy ?? true;\n }\n\n /** Register a handler called when a page upgrades from its progressive low-res pass to crisp. */\n onPageUpdate(handler: (index: number) => void): void {\n this.#onUpdate = handler;\n }\n\n /** Register a handler called as the document downloads, driving a loading indicator. */\n onProgress(handler: (progress: LoadProgress) => void): void {\n this.#onProgress = handler;\n }\n\n async open(): Promise<void> {\n if (isPdfDocument(this.#src)) {\n this.#doc = this.#src;\n } else {\n const pdfjs = await loadPdfjs(this.#legacy);\n pdfjs.GlobalWorkerOptions.workerSrc = this.#resolveWorkerSrc(pdfjs.GlobalWorkerOptions.workerSrc);\n const params: DocParams =\n typeof this.#src === 'string' ? { url: this.#src } : { data: this.#src };\n if (this.#disableAutoFetch) params.disableAutoFetch = true;\n const task = pdfjs.getDocument(params);\n if (this.#onProgress) task.onProgress = this.#onProgress;\n this.#doc = await task.promise;\n }\n this.pageCount = this.#doc.numPages;\n }\n\n /** Precedence: an explicit `workerSrc` → an already-configured global → an auto-resolved default. */\n #resolveWorkerSrc(configured: string): string {\n if (this.#workerSrc) return this.#workerSrc;\n if (configured) return configured;\n try {\n // Bundlers (Vite / webpack 5 / esbuild) statically rewrite these and emit the worker,\n // so the common case needs no `workerSrc`. Non-bundler / UMD hosts pass it explicitly. The\n // worker must match the build loaded in loadPdfjs — a legacy main with a modern worker (or\n // vice versa) mismatches — so the same #legacy flag chooses here too.\n return this.#legacy\n ? \"@@ZINE_PDF_WORKER:pdfjs-dist/legacy/build/pdf.worker.min.mjs@@\"\n : \"@@ZINE_PDF_WORKER:pdfjs-dist/build/pdf.worker.min.mjs@@\";\n } catch {\n throw new Error(\n \"PdfSource: couldn't auto-resolve pdf.js's worker. Pass a `workerSrc` URL \" +\n '(see the @zinejs/pdf README, \"The pdf.js worker\"), set pdfjs ' +\n 'GlobalWorkerOptions.workerSrc yourself, or pass a pre-created pdf.js document.',\n );\n }\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n // A zoomed request wants detail the fit-to-screen raster never had. PDF pages are vector, so\n // re-render at the magnification being viewed. Memoized per zoom level, not in the byte-capped\n // page cache: a zoomed page is many times the size of a normal one and would evict the whole\n // spread, and a reader who zooms out and back in should not pay to render it twice.\n if (opts && opts.scale > 1) return this.#renderZoomed(index, opts.scale, opts.maxSize);\n\n const decoded = this.#renderPage(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#renderPage(i).catch(() => {}); // prefetch failures surface on a real get()\n }\n }\n }\n\n /**\n * The page's text, for search. Empty for a page with no text layer (a scan, or a pdf.js build\n * without text extraction) — callers treat that as \"nothing to match\" rather than an error.\n */\n getText(index: number): Promise<string> {\n const cached = this.#textCache.get(index);\n if (cached) return cached;\n const pending = this.#extractText(index).catch(() => '');\n this.#textCache.set(index, pending);\n return pending;\n }\n\n /**\n * The document's table of contents, flattened into the shape the outline panel wants.\n *\n * Empty when the PDF has no outline, which is common. Destinations are resolved to page\n * indices here rather than in the UI: doing it once, up front, keeps clicking an entry\n * instant, and an entry whose destination cannot be resolved is kept with a null page so the\n * heading still shows.\n */\n async getOutline(): Promise<OutlineItem[]> {\n const doc = this.#doc;\n if (!doc || typeof doc.getOutline !== 'function') return [];\n let nodes: PdfOutlineNode[] | null;\n try {\n nodes = await doc.getOutline();\n } catch {\n return [];\n }\n if (!nodes?.length) return [];\n const convert = async (list: PdfOutlineNode[]): Promise<OutlineItem[]> =>\n Promise.all(\n list.map(async (node) => ({\n title: (node.title ?? '').trim() || 'Untitled',\n page: await this.#destinationPage(node.dest),\n children: node.items?.length ? await convert(node.items) : [],\n })),\n );\n return convert(nodes);\n }\n\n /** Resolve a pdf.js destination to a zero-based page index, or null if it cannot be. */\n async #destinationPage(dest: string | unknown[] | null | undefined): Promise<number | null> {\n const doc = this.#doc;\n if (!doc || dest == null) return null;\n try {\n // A named destination has to be looked up before it yields the usual explicit array.\n const explicit =\n typeof dest === 'string' ? await doc.getDestination?.(dest) : (dest as unknown[]);\n const ref = explicit?.[0];\n if (ref == null) return null;\n // Some destinations carry a bare page number instead of a reference.\n if (typeof ref === 'number') return ref;\n const index = await doc.getPageIndex?.(ref);\n return typeof index === 'number' ? index : null;\n } catch {\n return null; // a broken destination should not cost us the whole outline\n }\n }\n\n /**\n * The original PDF, for a download control.\n *\n * Null when the source was handed a pre-opened pdf.js document: the bytes belong to whoever\n * created it, and re-fetching them is not this class's call to make.\n */\n async getDownload(): Promise<DownloadInfo | null> {\n const src = this.#src;\n if (typeof src === 'string') {\n return { url: src, filename: filenameFromUrl(src) };\n }\n if (src instanceof ArrayBuffer || ArrayBuffer.isView(src)) {\n const blob = new Blob([src as BlobPart], { type: 'application/pdf' });\n return { url: URL.createObjectURL(blob), filename: 'document.pdf', revoke: true };\n }\n return null;\n }\n\n async #extractText(index: number): Promise<string> {\n if (this.#doc === null) {\n throw new Error('PdfSource: use after open() — the document is not loaded.');\n }\n const page = await this.#doc.getPage(index + 1); // pdf.js pages are 1-indexed\n if (typeof page.getTextContent !== 'function') return '';\n const content = await page.getTextContent();\n let text = '';\n for (const item of content.items) {\n if (typeof item.str !== 'string') continue; // marked-content entries carry no text\n text += item.str;\n // pdf.js splits a visual line into runs; only hasEOL ends one.\n text += item.hasEOL ? '\\n' : '';\n }\n return text;\n }\n\n destroy(): void {\n this.#doc?.destroy?.();\n this.#cache.clear();\n for (const pending of this.#zoomCache.values()) {\n void pending.then(releaseCanvas).catch(() => {});\n }\n this.#zoomCache.clear();\n this.#textCache.clear();\n this.#cachedBytes = 0;\n this.#doc = null;\n }\n\n #renderPage(index: number): Promise<PageContent> {\n const existing = this.#cache.get(index);\n if (existing) {\n // Mark most-recently-used: re-insert so it sits at the end of the Map's order.\n this.#cache.delete(index);\n this.#cache.set(index, existing);\n return existing.promise;\n }\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const full = this.#renderScale * dpr;\n const firstScale = this.#progressive ? full * PROGRESSIVE_LOW_RATIO : full;\n\n const entry: CacheEntry = { promise: undefined as unknown as Promise<PageContent>, bytes: 0 };\n entry.promise = this.#rasterize(index, firstScale).then(\n (content) => {\n entry.bytes = content.width * content.height * 4; // RGBA\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n if (this.#progressive) this.#upgrade(index, entry, full);\n return content;\n },\n (error: unknown) => {\n this.#cache.delete(index); // let a later get() retry\n throw error;\n },\n );\n this.#cache.set(index, entry);\n return entry.promise;\n }\n\n /** Progressive: re-render at full resolution, swap it into the cache, and signal an update. */\n #upgrade(index: number, entry: CacheEntry, fullScale: number): void {\n void this.#rasterize(index, fullScale)\n .then((hi) => {\n if (this.#cache.get(index) !== entry) return; // evicted or replaced meanwhile\n this.#cachedBytes -= entry.bytes;\n entry.promise = Promise.resolve(hi);\n entry.bytes = hi.width * hi.height * 4;\n this.#cachedBytes += entry.bytes;\n this.#evictToFit(index);\n this.#onUpdate?.(index);\n })\n .catch(() => {}); // keep the low-res if the upgrade fails\n }\n\n /** Drop least-recently-used pages (front of the Map) until under the byte cap. */\n #evictToFit(keepIndex: number): void {\n for (const [index, entry] of this.#cache) {\n if (this.#cachedBytes <= this.#maxCacheBytes) break;\n if (index === keepIndex || entry.bytes === 0) continue; // keep the newest; skip in-flight renders\n this.#cache.delete(index);\n this.#cachedBytes -= entry.bytes;\n }\n }\n\n /**\n * Render a whole page at zoom magnification, memoized by page and zoom level.\n *\n * Whole-page rather than a crop of what is visible: the renderer draws a page as one texture, so\n * a partial raster cannot be handed to it, and a crop would have to be re-rendered on every pan\n * anyway. Held apart from the byte-capped page cache, which is sized for fit-to-screen rasters.\n */\n #renderZoomed(index: number, zoom: number, maxSize?: number): Promise<PageContent> {\n const scale = Math.min(zoom, MAX_ZOOM_RENDER_SCALE);\n const key = `${index}@${scale}`;\n const cached = this.#zoomCache.get(key);\n if (cached) return cached;\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const promise = this.#rasterize(index, this.#renderScale * dpr * scale, maxSize).catch(\n (error: unknown) => {\n this.#zoomCache.delete(key); // let a later zoom retry\n throw error;\n },\n );\n // One zoom level at a time, but both pages of the spread at that level.\n for (const [k, pending] of this.#zoomCache) {\n if (!k.endsWith(`@${scale}`)) {\n this.#zoomCache.delete(k);\n void pending.then(releaseCanvas).catch(() => {});\n }\n }\n this.#zoomCache.set(key, promise);\n return promise;\n }\n\n async #rasterize(index: number, scale: number, maxSize?: 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 // Cap the scale so neither axis exceeds the GPU's texture limit: past it the upload paints black\n // (the renderer downscales as a backstop, but rendering to fit avoids the wasted memory spike).\n if (maxSize !== undefined && maxSize > 0) {\n const base = page.getViewport({ scale: 1 });\n const fit = maxSize / Math.max(base.width, base.height);\n scale = Math.min(scale, fit);\n }\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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"}
@@ -54,6 +54,14 @@ export interface PdfSourceOptions {
54
54
  progressive?: boolean;
55
55
  /** Ask pdf.js to fetch only the byte ranges visible pages need (range-capable servers). Default false. */
56
56
  disableAutoFetch?: boolean;
57
+ /**
58
+ * Load pdf.js's legacy build (transpiled with polyfills) instead of the modern one. The legacy
59
+ * build runs on both old and modern browsers; the modern build is smaller and a touch faster but
60
+ * needs a recent engine. Default true, favouring reach. Set false to serve the lean modern build
61
+ * when your audience is on current browsers. Ignored when a pre-created pdf.js document or a
62
+ * global `pdfjsLib` is supplied — those already picked their build.
63
+ */
64
+ legacy?: boolean;
57
65
  }
58
66
  /**
59
67
  * Renders PDF pages to canvases for zinejs. pdf.js is a dependency of this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zinejs/pdf",
3
- "version": "0.7.4",
3
+ "version": "0.9.0",
4
4
  "description": "PDF content source for zinejs, powered by pdf.js.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -18,7 +18,7 @@
18
18
  "url": "git+https://github.com/awecode/zinejs.git",
19
19
  "directory": "packages/pdf"
20
20
  },
21
- "homepage": "https://github.com/awecode/zinejs/tree/main/packages/pdf#readme",
21
+ "homepage": "https://zinejs.com",
22
22
  "bugs": "https://github.com/awecode/zinejs/issues",
23
23
  "files": [
24
24
  "dist"
@@ -40,10 +40,10 @@
40
40
  "pdfjs-dist": ">=4"
41
41
  },
42
42
  "peerDependencies": {
43
- "@zinejs/core": "^0.7.4"
43
+ "@zinejs/core": "^0.9.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@zinejs/core": "^0.7.4"
46
+ "@zinejs/core": "^0.9.0"
47
47
  },
48
48
  "size-limit": [
49
49
  {