unpdf 1.9.1 → 2.0.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/dist/index.mjs CHANGED
@@ -1,4 +1,8 @@
1
- //#region src/_internal/dommatrix-polyfill.ts
1
+ // src/_internal/dommatrix-polyfill.ts
2
+ /**
3
+ * Stubs `DOMMatrix`, which PDF.js reads while its modules evaluate, so it must
4
+ * run before any PDF.js module is imported.
5
+ */
2
6
  function polyfillDOMMatrix() {
3
7
  if (typeof globalThis.DOMMatrix !== "undefined") return;
4
8
  globalThis.DOMMatrix = class DOMMatrix {
@@ -33,44 +37,32 @@ function polyfillDOMMatrix() {
33
37
  }
34
38
  };
35
39
  }
36
- //#endregion
37
- //#region src/_internal/env.ts
40
+ // src/utils.ts
41
+ let pdfjsLoader;
42
+ let resolvedModule;
38
43
  /**
39
- * Stubs browser globals that PDF.js expects at the module level
40
- * in non-browser environments.
41
- *
42
- * @remarks
43
- * Must be called before importing any PDF.js module, since PDF.js
44
- * accesses these globals at parse time (top-level constants).
44
+ * Whether PDF.js takes its Node.js code paths, by PDF.js' own rule: a
45
+ * `process` object outside of NW.js and Electron renderers. It decides between
46
+ * PDF.js' Node.js and DOM factories for canvases, fonts, and CMaps.
45
47
  */
46
- function stubBrowserGlobals() {
47
- polyfillDOMMatrix();
48
- }
49
- //#endregion
50
- //#region src/utils.ts
51
- let resolvedModule;
52
- const isNode = globalThis.process?.release?.name === "node";
53
- const isBrowser = typeof window !== "undefined";
48
+ const isNode = isNodeProcess(globalThis.process);
54
49
  /**
55
- * Creates a `PDFDocumentProxy` from binary PDF data.
56
- *
57
- * Applies the following defaults:
58
- * - `useSystemFonts: true`
59
- *
60
- * In Node.js environments, additionally applies:
61
- * - `disableFontFace: true`
62
- * - `standardFontDataUrl` resolved from the local `pdfjs-dist` package
63
- * - `cMapUrl` and `cMapPacked` resolved from the local `pdfjs-dist` package
50
+ * Creates a `PDFDocumentProxy` from binary PDF data, with `useSystemFonts:
51
+ * true`. In Node.js with `pdfjs-dist` installed, it uses that package's
52
+ * standard fonts instead of system fonts, and its packed CMaps.
64
53
  */
65
54
  async function getDocumentProxy(data, options = {}) {
66
55
  const { getDocument } = await getResolvedPDFJS();
67
56
  let nodeDefaults = {};
68
57
  if (isNode) try {
69
- const base = import.meta.resolve("pdfjs-dist/package.json");
58
+ const nodeProcess = globalThis.process;
59
+ const { dirname, join } = nodeProcess.getBuiltinModule("node:path");
60
+ const { fileURLToPath } = nodeProcess.getBuiltinModule("node:url");
61
+ const pdfjsDir = dirname(fileURLToPath(import.meta.resolve("pdfjs-dist/package.json")));
70
62
  nodeDefaults = {
71
- disableFontFace: true,
72
- standardFontDataUrl: new URL("./standard_fonts/", base).href,
73
- cMapUrl: new URL("./cmaps/", base).href,
63
+ useSystemFonts: false,
64
+ standardFontDataUrl: `${join(pdfjsDir, "standard_fonts")}/`,
65
+ cMapUrl: `${join(pdfjsDir, "cmaps")}/`,
74
66
  cMapPacked: true
75
67
  };
76
68
  } catch {}
@@ -95,24 +87,27 @@ async function withDocument(data, operation, options) {
95
87
  if (pdf !== data) await pdf.loadingTask.destroy();
96
88
  }
97
89
  }
98
- async function getResolvedPDFJS() {
99
- if (!resolvedModule) await resolvePDFJSImport();
100
- return resolvedModule;
90
+ /**
91
+ * Sets the PDF.js build every other method uses. The build loads on first use,
92
+ * so call this before any other method. Without it, the serverless build is
93
+ * used.
94
+ *
95
+ * @example
96
+ * // In Node.js, the legacy build of `pdfjs-dist` (install it first)
97
+ * import { definePDFJSModule } from 'unpdf'
98
+ *
99
+ * definePDFJSModule(() => import('pdfjs-dist/legacy/build/pdf.mjs'))
100
+ */
101
+ function definePDFJSModule(pdfjs) {
102
+ pdfjsLoader = pdfjs;
103
+ resolvedModule = void 0;
101
104
  }
102
- async function resolvePDFJSImport(pdfjsResolver, { reload = false } = {}) {
103
- if (resolvedModule && !reload) return;
104
- stubBrowserGlobals();
105
- if (pdfjsResolver) try {
106
- resolvedModule = await interopDefault(pdfjsResolver());
107
- return;
108
- } catch (error) {
109
- throw new Error(`PDF.js could not be resolved: ${error}`);
110
- }
111
- try {
112
- resolvedModule = await import("unpdf/pdfjs");
113
- } catch (error) {
114
- throw new Error(`Serverless PDF.js bundle could not be resolved: ${error}`);
115
- }
105
+ function getResolvedPDFJS() {
106
+ resolvedModule ??= loadPDFJS(pdfjsLoader).catch((error) => {
107
+ resolvedModule = void 0;
108
+ throw error;
109
+ });
110
+ return resolvedModule;
116
111
  }
117
112
  function isPDFDocumentProxy(data) {
118
113
  return typeof data === "object" && data !== null && "_pdfInfo" in data;
@@ -121,177 +116,437 @@ async function interopDefault(m) {
121
116
  const resolved = await m;
122
117
  return resolved.default || resolved;
123
118
  }
124
- //#endregion
125
- //#region src/_internal/canvas.ts
126
- let resolvedCanvasModule;
127
- /**
128
- * Derived from the PDF.js project by the Mozilla Foundation.
129
- * @see https://github.com/mozilla/pdf.js/blob/b8de9a372f9bbf7e33adb362eeae5ef1919dba73/src/display/canvas_factory.js#L18
130
- * @license Apache-2.0
131
- */
132
- var BaseCanvasFactory = class {
133
- #enableHWA = false;
134
- constructor({ enableHWA = false } = {}) {
135
- this.#enableHWA = enableHWA;
136
- }
137
- create(width, height) {
138
- const canvas = this._createCanvas(width, height);
139
- return {
140
- canvas,
141
- context: canvas.getContext("2d", { willReadFrequently: !this.#enableHWA })
142
- };
143
- }
144
- reset({ canvas }, width, height) {
145
- if (!canvas) throw new Error("Canvas is not specified");
146
- canvas.width = width;
147
- canvas.height = height;
148
- }
149
- destroy(context) {
150
- if (!context.canvas) throw new Error("Canvas is not specified");
151
- context.canvas.width = 0;
152
- context.canvas.height = 0;
153
- context.canvas = void 0;
154
- context.context = void 0;
155
- }
156
- _createCanvas(width, height) {
157
- throw new Error("Not implemented");
158
- }
159
- };
160
- /**
161
- * Derived from the PDF.js project by the Mozilla Foundation.
162
- * @see https://github.com/mozilla/pdf.js/blob/b8de9a372f9bbf7e33adb362eeae5ef1919dba73/src/display/canvas_factory.js#L18
163
- * @license Apache-2.0
164
- */
165
- var DOMCanvasFactory = class extends BaseCanvasFactory {
166
- constructor({ ownerDocument = globalThis.document, enableHWA = false } = {}) {
167
- super({ enableHWA });
168
- this._document = ownerDocument;
169
- }
170
- _createCanvas(width, height) {
171
- const canvas = this._document.createElement("canvas");
172
- canvas.width = width;
173
- canvas.height = height;
174
- return canvas;
175
- }
176
- };
177
- var NodeCanvasFactory = class extends BaseCanvasFactory {
178
- constructor({ enableHWA = false } = {}) {
179
- super({ enableHWA });
180
- }
181
- _createCanvas(width, height) {
182
- if (!resolvedCanvasModule) throw new Error("@napi-rs/canvas module is not resolved");
183
- return resolvedCanvasModule.createCanvas(width, height);
119
+ async function loadPDFJS(loader) {
120
+ polyfillDOMMatrix();
121
+ try {
122
+ return await interopDefault(loader ? loader() : import("unpdf/pdfjs"));
123
+ } catch (error) {
124
+ throw new Error(`PDF.js could not be resolved: ${error}`, { cause: error });
184
125
  }
185
- };
126
+ }
127
+ function isNodeProcess(nodeProcess) {
128
+ return typeof nodeProcess === "object" && `${nodeProcess}` === "[object process]" && !nodeProcess.versions.nw && !(nodeProcess.versions.electron && nodeProcess.type && nodeProcess.type !== "browser");
129
+ }
130
+ // src/_internal/canvas.ts
131
+ let resolvedCanvasModule;
186
132
  async function resolveCanvasModule(canvasImport) {
187
133
  resolvedCanvasModule ??= await interopDefault(canvasImport());
188
134
  globalThis[Symbol.for("unpdf.canvasModule")] = resolvedCanvasModule;
189
135
  }
190
136
  /**
191
- * Injects `DOMMatrix`, `ImageData` and `Path2D` constructors from the
192
- * `@napi-rs/canvas` package into the global scope. This is necessary for
193
- * Node.js environments that do not support these constructors natively.
137
+ * Injects the `DOMMatrix` and `Path2D` constructors of `@napi-rs/canvas` into
138
+ * the global scope, which the PDF.js display layer instantiates while
139
+ * rendering. Only the legacy `pdfjs-dist` build polyfills them itself.
194
140
  *
195
141
  * @remarks
196
- * `DOMMatrix` is always set because {@link stubBrowserGlobals} may have
142
+ * `DOMMatrix` is always set because {@link polyfillDOMMatrix} may have
197
143
  * installed a minimal stub to allow PDF.js to import – the real
198
144
  * `@napi-rs/canvas` implementation must take precedence for rendering.
199
145
  */
200
146
  function injectCanvasConstructors() {
201
147
  if (!resolvedCanvasModule) return;
202
148
  globalThis.DOMMatrix = resolvedCanvasModule.DOMMatrix;
203
- if (typeof globalThis.ImageData === "undefined") globalThis.ImageData = resolvedCanvasModule.ImageData;
204
149
  if (typeof globalThis.Path2D === "undefined") globalThis.Path2D = resolvedCanvasModule.Path2D;
205
150
  }
206
- //#endregion
207
- //#region src/image.ts
151
+ // src/_internal/operator-list.ts
152
+ const IDENTITY_MATRIX = [
153
+ 1,
154
+ 0,
155
+ 0,
156
+ 1,
157
+ 0,
158
+ 0
159
+ ];
160
+ /**
161
+ * Walks an operator list in painting order, with the graphics state each
162
+ * operation draws with and whether it is visible. Skips the content of soft
163
+ * masks, which only modulates transparency.
164
+ */
165
+ function* walkOperatorList(operatorList, ops, optionalContentConfig) {
166
+ const stateStack = [];
167
+ let state = {
168
+ transform: IDENTITY_MATRIX,
169
+ textRenderingMode: 0
170
+ };
171
+ const softMaskGroupFlags = [];
172
+ const contentVisibilityFlags = [];
173
+ for (let i = 0; i < operatorList.fnArray.length; i++) {
174
+ const fn = operatorList.fnArray[i];
175
+ const args = operatorList.argsArray[i];
176
+ if (fn === ops.save) stateStack.push(state);
177
+ else if (fn === ops.restore || fn === ops.paintFormXObjectEnd || fn === ops.endAnnotation) state = stateStack.pop() ?? state;
178
+ else if (fn === ops.transform) state = {
179
+ ...state,
180
+ transform: multiplyMatrices(state.transform, args)
181
+ };
182
+ else if (fn === ops.paintFormXObjectBegin) {
183
+ stateStack.push(state);
184
+ const [formMatrix] = args;
185
+ if (formMatrix) state = {
186
+ ...state,
187
+ transform: multiplyMatrices(state.transform, formMatrix)
188
+ };
189
+ } else if (fn === ops.beginAnnotation) {
190
+ stateStack.push(state);
191
+ const [, , annotationTransform, appearanceMatrix] = args;
192
+ state = {
193
+ transform: multiplyMatrices(Array.from(annotationTransform), appearanceMatrix),
194
+ textRenderingMode: 0
195
+ };
196
+ } else if (fn === ops.setTextRenderingMode) state = {
197
+ ...state,
198
+ textRenderingMode: args[0]
199
+ };
200
+ else if (fn === ops.beginGroup) softMaskGroupFlags.push(Boolean(args[0]?.smask));
201
+ else if (fn === ops.endGroup) softMaskGroupFlags.pop();
202
+ else if (fn === ops.beginMarkedContent) contentVisibilityFlags.push(true);
203
+ else if (fn === ops.beginMarkedContentProps) {
204
+ const [tag, properties] = args;
205
+ contentVisibilityFlags.push(tag !== "OC" || optionalContentConfig.isVisible(properties));
206
+ } else if (fn === ops.endMarkedContent) contentVisibilityFlags.pop();
207
+ if (softMaskGroupFlags.includes(true)) continue;
208
+ yield {
209
+ fn,
210
+ args,
211
+ ...state,
212
+ isVisible: !contentVisibilityFlags.includes(false)
213
+ };
214
+ }
215
+ }
208
216
  /**
209
- * Extracts images from a specific page of a PDF document, including necessary metadata,
210
- * such as width, height, and calculated color channels.
217
+ * Maps the unit square, which PDF images are drawn into, through a transform
218
+ * and returns the axis-aligned box around the result.
211
219
  */
212
- async function extractImages$1(data, pageNumber) {
220
+ function getUnitSquareBounds(transform) {
221
+ const [a, b, c, d, e, f] = transform;
222
+ const xs = [
223
+ e,
224
+ a + e,
225
+ c + e,
226
+ a + c + e
227
+ ];
228
+ const ys = [
229
+ f,
230
+ b + f,
231
+ d + f,
232
+ b + d + f
233
+ ];
234
+ const minX = Math.min(...xs);
235
+ const minY = Math.min(...ys);
236
+ return {
237
+ x: minX,
238
+ y: minY,
239
+ width: Math.max(...xs) - minX,
240
+ height: Math.max(...ys) - minY
241
+ };
242
+ }
243
+ /** Applies `inner` first, then `outer`, like `cm` concatenating onto the current matrix. */
244
+ function multiplyMatrices(outer, inner) {
245
+ const [a, b, c, d, e, f] = outer;
246
+ const [a2, b2, c2, d2, e2, f2] = Array.from(inner);
247
+ return [
248
+ a * a2 + c * b2,
249
+ b * a2 + d * b2,
250
+ a * c2 + c * d2,
251
+ b * c2 + d * d2,
252
+ a * e2 + c * f2 + e,
253
+ b * e2 + d * f2 + f
254
+ ];
255
+ }
256
+ // src/_internal/png.ts
257
+ const PNG_SIGNATURE = [
258
+ 137,
259
+ 80,
260
+ 78,
261
+ 71,
262
+ 13,
263
+ 10,
264
+ 26,
265
+ 10
266
+ ];
267
+ let crcTable;
268
+ /**
269
+ * Encodes RGBA pixels as an unfiltered 8-bit RGBA PNG, compressed through
270
+ * `CompressionStream`, which Node.js, Cloudflare Workers, and browsers all provide.
271
+ */
272
+ async function encodePNG(rgba, width, height) {
273
+ const rowLength = width * 4;
274
+ const scanlines = new Uint8Array((rowLength + 1) * height);
275
+ for (let y = 0; y < height; y++) scanlines.set(rgba.subarray(y * rowLength, (y + 1) * rowLength), y * (rowLength + 1) + 1);
276
+ const header = /* @__PURE__ */ new Uint8Array(13);
277
+ const headerView = new DataView(header.buffer);
278
+ headerView.setUint32(0, width);
279
+ headerView.setUint32(4, height);
280
+ header[8] = 8;
281
+ header[9] = 6;
282
+ const chunks = [
283
+ createChunk("IHDR", header),
284
+ createChunk("IDAT", await deflate(scanlines)),
285
+ createChunk("IEND", /* @__PURE__ */ new Uint8Array(0))
286
+ ];
287
+ const png = new Uint8Array(PNG_SIGNATURE.length + chunks.reduce((sum, chunk) => sum + chunk.length, 0));
288
+ png.set(PNG_SIGNATURE);
289
+ let offset = PNG_SIGNATURE.length;
290
+ for (const chunk of chunks) {
291
+ png.set(chunk, offset);
292
+ offset += chunk.length;
293
+ }
294
+ return png;
295
+ }
296
+ async function deflate(data) {
297
+ const stream = new CompressionStream("deflate");
298
+ const writer = stream.writable.getWriter();
299
+ const [compressed] = await Promise.all([
300
+ new Response(stream.readable).arrayBuffer(),
301
+ writer.write(data),
302
+ writer.close()
303
+ ]);
304
+ return new Uint8Array(compressed);
305
+ }
306
+ /** Lays out a chunk as length, type, data, and the CRC of type and data. */
307
+ function createChunk(type, data) {
308
+ const chunk = new Uint8Array(12 + data.length);
309
+ const view = new DataView(chunk.buffer);
310
+ view.setUint32(0, data.length);
311
+ for (let i = 0; i < 4; i++) chunk[4 + i] = type.charCodeAt(i);
312
+ chunk.set(data, 8);
313
+ view.setUint32(8 + data.length, crc32(chunk.subarray(4, 8 + data.length)));
314
+ return chunk;
315
+ }
316
+ function crc32(bytes) {
317
+ crcTable ??= createCRCTable();
318
+ let crc = 4294967295;
319
+ for (const byte of bytes) crc = crcTable[(crc ^ byte) & 255] ^ crc >>> 8;
320
+ return (crc ^ 4294967295) >>> 0;
321
+ }
322
+ function createCRCTable() {
323
+ const table = /* @__PURE__ */ new Uint32Array(256);
324
+ for (let n = 0; n < 256; n++) {
325
+ let crc = n;
326
+ for (let k = 0; k < 8; k++) crc = crc & 1 ? 3988292384 ^ crc >>> 1 : crc >>> 1;
327
+ table[n] = crc;
328
+ }
329
+ return table;
330
+ }
331
+ // src/image.ts
332
+ /**
333
+ * Extracts the images painted on a page – image XObjects, inline images, and
334
+ * image masks – as RGBA pixels. Image masks come out opaque black where they
335
+ * paint and transparent elsewhere, since their color is the fill color at the
336
+ * time of painting. A 1×1 image mask that paints its pixel, which PDF.js
337
+ * turns into a solid fill, is skipped.
338
+ *
339
+ * @remarks
340
+ * A caller-supplied document proxy created with `isOffscreenCanvasSupported`
341
+ * or `isImageDecoderSupported` (both on by default outside of Node.js) hands
342
+ * some images out as bitmaps instead of pixels, which are skipped.
343
+ */
344
+ async function extractImages(data, pageNumber, options = {}) {
213
345
  return await withDocument(data, async (pdf) => {
214
346
  if (pageNumber < 1 || pageNumber > pdf.numPages) throw new Error(`Invalid page number. Must be between 1 and ${pdf.numPages}.`);
215
347
  const page = await pdf.getPage(pageNumber);
216
- const operatorList = await page.getOperatorList();
217
- const { OPS } = await getResolvedPDFJS();
348
+ const [operatorList, optionalContentConfig] = await Promise.all([page.getOperatorList(), pdf.getOptionalContentConfig()]);
349
+ const { ImageKind, OPS } = await getResolvedPDFJS();
218
350
  const images = [];
219
- for (let i = 0; i < operatorList.fnArray.length; i++) {
220
- if (operatorList.fnArray[i] !== OPS.paintImageXObject) continue;
221
- const imageKey = operatorList.argsArray[i][0];
222
- const image = await new Promise((resolve) => (imageKey.startsWith("g_") ? page.commonObjs : page.objs).get(imageKey, resolve));
223
- if (!image || !image.data || !image.width || !image.height) continue;
224
- const { width, height, data } = image;
225
- const calculatedChannels = data.length / (width * height);
226
- if (![
227
- 1,
228
- 3,
229
- 4
230
- ].includes(calculatedChannels)) continue;
231
- const channels = calculatedChannels;
351
+ for (const { fn, args, transform, isVisible } of walkOperatorList(operatorList, OPS, optionalContentConfig)) {
352
+ if (!isVisible) continue;
353
+ let key = null;
354
+ let image;
355
+ if (fn === OPS.paintImageXObject) {
356
+ key = args[0];
357
+ image = await getPageObject(page, key);
358
+ } else if (fn === OPS.paintInlineImageXObject) image = args[0];
359
+ else if (fn === OPS.paintImageMaskXObject) {
360
+ key = args[0].data;
361
+ image = await getPageObject(page, key);
362
+ } else continue;
363
+ if (!image) continue;
364
+ const pixels = toRGBA(image, fn === OPS.paintImageMaskXObject, ImageKind);
365
+ if (!pixels) continue;
232
366
  images.push({
233
- data,
234
- width,
235
- height,
236
- channels,
237
- key: imageKey
367
+ data: options.format === "png" ? await encodePNG(pixels, image.width, image.height) : pixels,
368
+ width: image.width,
369
+ height: image.height,
370
+ key,
371
+ bounds: getUnitSquareBounds(transform)
238
372
  });
239
373
  }
240
374
  return images;
375
+ }, {
376
+ isOffscreenCanvasSupported: false,
377
+ isImageDecoderSupported: false
241
378
  });
242
379
  }
243
- async function renderPageAsImage$1(data, pageNumber, options = {}) {
244
- const CanvasFactory = await createIsomorphicCanvasFactory(options.canvasImport);
380
+ async function renderPageAsImage(data, pageNumber, options = {}) {
381
+ await prepareCanvasEnvironment(options.canvasImport);
245
382
  return await withDocument(data, async (pdf) => {
246
383
  if (pageNumber < 1 || pageNumber > pdf.numPages) throw new Error(`Invalid page number. Must be between 1 and ${pdf.numPages}.`);
247
384
  const page = await pdf.getPage(pageNumber);
248
385
  const defaultViewport = page.getViewport({ scale: 1 });
249
- let scale = options.scale || 1;
250
- if (options.width) scale = options.width / defaultViewport.width;
251
- else if (options.height) scale = options.height / defaultViewport.height;
252
- const viewport = page.getViewport({ scale: Math.max(0, scale) });
253
- const canvasFactory = new CanvasFactory();
254
- const drawingContext = canvasFactory.create(viewport.width, viewport.height);
386
+ let scale = options.scale ?? 1;
387
+ if (options.width !== void 0) scale = options.width / defaultViewport.width;
388
+ else if (options.height !== void 0) scale = options.height / defaultViewport.height;
389
+ const viewport = page.getViewport({ scale });
390
+ const canvasFactory = pdf.canvasFactory;
391
+ const canvasAndContext = canvasFactory.create(viewport.width, viewport.height);
255
392
  try {
393
+ const { canvas } = canvasAndContext;
256
394
  await page.render({
257
- canvas: drawingContext.canvas,
258
- canvasContext: drawingContext.context,
395
+ canvas,
259
396
  viewport
260
397
  }).promise;
261
- const { canvas } = drawingContext;
262
- if (options.toDataURL) return canvas.toDataURL();
398
+ const { format = "png", quality } = options;
399
+ const mimeType = `image/${format}`;
400
+ if (options.toDataURL) {
401
+ const dataURL = canvas.toDataURL(mimeType, quality);
402
+ assertEncodedType(dataURL.slice(5, dataURL.indexOf(";")), mimeType);
403
+ return dataURL;
404
+ }
263
405
  if ("encode" in canvas) {
264
- const buffer = await canvas.encode("png");
265
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
406
+ const buffer = format === "png" ? await canvas.encode(format) : await canvas.encode(format, quality === void 0 ? void 0 : quality * 100);
407
+ return new Uint8Array(buffer);
266
408
  }
267
409
  const blob = await new Promise((resolve) => {
268
- canvas.toBlob(resolve);
410
+ canvas.toBlob(resolve, mimeType, quality);
269
411
  });
270
- if (!blob) throw new Error("Failed to encode canvas to a PNG blob.");
271
- return await blob.arrayBuffer();
412
+ if (!blob) throw new Error(`Failed to encode canvas as ${mimeType}.`);
413
+ assertEncodedType(blob.type, mimeType);
414
+ return new Uint8Array(await blob.arrayBuffer());
272
415
  } finally {
273
- canvasFactory.destroy(drawingContext);
416
+ canvasFactory.destroy(canvasAndContext);
417
+ }
418
+ });
419
+ }
420
+ function getPageObject(page, objectId) {
421
+ const objects = objectId.startsWith("g_") ? page.commonObjs : page.objs;
422
+ return new Promise((resolve) => objects.get(objectId, resolve));
423
+ }
424
+ /**
425
+ * Converts PDF.js image data into a fresh RGBA buffer, so the result never
426
+ * aliases a buffer PDF.js keeps cached.
427
+ */
428
+ function toRGBA(image, isMask, imageKind) {
429
+ const { data, width, height, kind } = image;
430
+ if (!data) return;
431
+ const rgba = new Uint8Array(width * height * 4);
432
+ if (kind === imageKind.RGBA_32BPP) {
433
+ rgba.set(data.subarray(0, rgba.length));
434
+ return rgba;
435
+ }
436
+ if (kind === imageKind.RGB_24BPP) {
437
+ for (let source = 0, target = 0; target < rgba.length; source += 3, target += 4) {
438
+ rgba[target] = data[source];
439
+ rgba[target + 1] = data[source + 1];
440
+ rgba[target + 2] = data[source + 2];
441
+ rgba[target + 3] = 255;
442
+ }
443
+ return rgba;
444
+ }
445
+ if (!isMask && kind !== imageKind.GRAYSCALE_1BPP) return;
446
+ const bytesPerRow = Math.ceil(width / 8);
447
+ for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
448
+ const bit = data[y * bytesPerRow + (x >> 3)] >> 7 - (x & 7) & 1;
449
+ const target = (y * width + x) * 4;
450
+ if (isMask) rgba[target + 3] = bit === 0 ? 255 : 0;
451
+ else {
452
+ rgba.fill(bit === 0 ? 0 : 255, target, target + 3);
453
+ rgba[target + 3] = 255;
274
454
  }
275
- }, { CanvasFactory });
455
+ }
456
+ return rgba;
276
457
  }
277
- async function createIsomorphicCanvasFactory(canvasImport) {
278
- if (isBrowser) return DOMCanvasFactory;
458
+ async function prepareCanvasEnvironment(canvasImport) {
279
459
  if (isNode) {
280
460
  if (!canvasImport) throw new Error("Parameter \"canvasImport\" is required in Node.js environment.");
281
461
  await resolveCanvasModule(canvasImport);
282
462
  injectCanvasConstructors();
283
- return NodeCanvasFactory;
463
+ return;
464
+ }
465
+ if (typeof document === "undefined") throw new Error("Unsupported environment for canvas creation.");
466
+ }
467
+ function assertEncodedType(encodedType, mimeType) {
468
+ if (encodedType !== mimeType) throw new Error(`This environment can't encode ${mimeType}, only ${encodedType}.`);
469
+ }
470
+ // src/inspect.ts
471
+ const WHITESPACE_PATTERN = /\s/g;
472
+ /**
473
+ * Measures the text and images of every page, such as to tell scanned pages
474
+ * from born-digital ones. Where that line lies is left to the caller.
475
+ */
476
+ async function inspectPages(data) {
477
+ return await withDocument(data, async (pdf) => {
478
+ const { OPS } = await getResolvedPDFJS();
479
+ const optionalContentConfig = await pdf.getOptionalContentConfig();
480
+ const shouldReleasePages = !isPDFDocumentProxy(data);
481
+ const pages = [];
482
+ for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) pages.push(await inspectPage(pdf, pageNumber, OPS, optionalContentConfig, shouldReleasePages));
483
+ return {
484
+ totalPages: pdf.numPages,
485
+ pages
486
+ };
487
+ });
488
+ }
489
+ async function inspectPage(document, pageNumber, ops, optionalContentConfig, shouldReleasePage) {
490
+ const page = await document.getPage(pageNumber);
491
+ const [textContent, operatorList] = await Promise.all([page.getTextContent(), page.getOperatorList()]);
492
+ const textLength = textContent.items.reduce((sum, item) => sum + ("str" in item ? countCharacters(item.str) : 0), 0);
493
+ let invisibleTextLength = 0;
494
+ const imageBounds = [];
495
+ for (const { fn, args, transform, textRenderingMode, isVisible } of walkOperatorList(operatorList, ops, optionalContentConfig)) if (fn === ops.showText && (!isVisible || textRenderingMode === 3 || textRenderingMode === 7)) {
496
+ for (const glyph of args[0]) if (typeof glyph === "object" && glyph?.unicode) invisibleTextLength += countCharacters(glyph.unicode);
497
+ } else if (isVisible && (fn === ops.paintImageXObject || fn === ops.paintInlineImageXObject || fn === ops.paintImageMaskXObject)) imageBounds.push(getUnitSquareBounds(transform));
498
+ if (shouldReleasePage) page.cleanup();
499
+ return {
500
+ textLength,
501
+ invisibleTextLength,
502
+ imageCount: imageBounds.length,
503
+ imageCoverage: getCoverage(imageBounds, page.view)
504
+ };
505
+ }
506
+ function countCharacters(text) {
507
+ return Array.from(text.replace(WHITESPACE_PATTERN, "")).length;
508
+ }
509
+ /**
510
+ * Computes the fraction of a view box the union of boxes covers, exactly: cuts
511
+ * the view box into vertical slabs at every box edge and merges the vertical
512
+ * extents of the boxes spanning each slab.
513
+ */
514
+ function getCoverage(boxes, view) {
515
+ const [viewX0, viewY0, viewX1, viewY1] = view;
516
+ const viewArea = (viewX1 - viewX0) * (viewY1 - viewY0);
517
+ if (viewArea <= 0) return 0;
518
+ const clippedBoxes = boxes.map((box) => ({
519
+ x0: Math.max(box.x, viewX0),
520
+ x1: Math.min(box.x + box.width, viewX1),
521
+ y0: Math.max(box.y, viewY0),
522
+ y1: Math.min(box.y + box.height, viewY1)
523
+ })).filter((box) => box.x0 < box.x1 && box.y0 < box.y1);
524
+ const edges = [...new Set(clippedBoxes.flatMap((box) => [box.x0, box.x1]))].sort((a, b) => a - b);
525
+ let area = 0;
526
+ for (let i = 0; i < edges.length - 1; i++) {
527
+ const left = edges[i];
528
+ const right = edges[i + 1];
529
+ const spans = clippedBoxes.filter((box) => box.x0 <= left && box.x1 >= right).sort((a, b) => a.y0 - b.y0);
530
+ let coveredHeight = 0;
531
+ let coveredTop = -Infinity;
532
+ for (const { y0, y1 } of spans) {
533
+ const start = Math.max(y0, coveredTop);
534
+ if (y1 > start) {
535
+ coveredHeight += y1 - start;
536
+ coveredTop = y1;
537
+ }
538
+ }
539
+ area += coveredHeight * (right - left);
284
540
  }
285
- throw new Error("Unsupported environment for canvas creation.");
541
+ return area / viewArea;
286
542
  }
287
- //#endregion
288
- //#region src/links.ts
289
- async function extractLinks$1(data) {
543
+ // src/links.ts
544
+ async function extractLinks(data) {
290
545
  return await withDocument(data, async (pdf) => {
291
- const pageLinks = await Promise.all(Array.from({ length: pdf.numPages }, (_, i) => getPageLinks(pdf, i + 1)));
546
+ const links = await Promise.all(Array.from({ length: pdf.numPages }, (_, i) => getPageLinks(pdf, i + 1)));
292
547
  return {
293
548
  totalPages: pdf.numPages,
294
- links: pageLinks.flat()
549
+ links
295
550
  };
296
551
  });
297
552
  }
@@ -301,8 +556,7 @@ async function getPageLinks(document, pageNumber) {
301
556
  for (const annotation of annotations) if (annotation.subtype === "Link" && annotation.url) links.push(annotation.url);
302
557
  return links;
303
558
  }
304
- //#endregion
305
- //#region src/meta.ts
559
+ // src/meta.ts
306
560
  const XMP_DATE_PROPERTIES = [
307
561
  "xmp:createdate",
308
562
  "xmp:modifydate",
@@ -311,7 +565,7 @@ const XMP_DATE_PROPERTIES = [
311
565
  "xap:modifydate",
312
566
  "xap:metadatadate"
313
567
  ];
314
- async function getMeta$1(data, options = {}) {
568
+ async function getMeta(data, options = {}) {
315
569
  return await withDocument(data, async (pdf) => {
316
570
  const meta = await pdf.getMetadata();
317
571
  const info = { ...meta.info };
@@ -330,18 +584,16 @@ async function getMeta$1(data, options = {}) {
330
584
  }
331
585
  return {
332
586
  info,
333
- metadata: metadata || {}
587
+ metadata: metadata ?? null
334
588
  };
335
589
  });
336
590
  }
337
591
  function parseISODateString(isoDateString) {
338
- if (!isoDateString) return;
339
592
  const parsedDate = Date.parse(isoDateString);
340
- if (!Number.isNaN(parsedDate)) return new Date(parsedDate);
593
+ return Number.isNaN(parsedDate) ? null : new Date(parsedDate);
341
594
  }
342
- //#endregion
343
- //#region src/text.ts
344
- async function extractTextItems$1(data) {
595
+ // src/text.ts
596
+ async function extractTextItems(data) {
345
597
  return await withDocument(data, async (pdf) => {
346
598
  const items = await Promise.all(Array.from({ length: pdf.numPages }, (_, i) => getPageTextItems(pdf, i + 1)));
347
599
  return {
@@ -368,7 +620,7 @@ async function getPageTextItems(document, pageNumber) {
368
620
  };
369
621
  });
370
622
  }
371
- async function extractText$1(data, options = {}) {
623
+ async function extractText(data, options = {}) {
372
624
  const { mergePages = false } = options;
373
625
  return await withDocument(data, async (pdf) => {
374
626
  const texts = await Promise.all(Array.from({ length: pdf.numPages }, (_, i) => getPageText(pdf, i + 1)));
@@ -388,50 +640,4 @@ async function getPageText(document, pageNumber) {
388
640
  function normalizeMergedText(texts) {
389
641
  return texts.join("\n").replace(/[^\S\n]+/g, " ").replace(/ ?\n ?/g, "\n").replace(/\n{3,}/g, "\n\n");
390
642
  }
391
- //#endregion
392
- //#region src/config.ts
393
- /**
394
- * Sets the PDF.js build every other method uses – call it before any of them.
395
- * Without it, the serverless build is used.
396
- *
397
- * @example
398
- * // In Node.js, the legacy build of `pdfjs-dist` (install it first)
399
- * import { definePDFJSModule } from 'unpdf'
400
- *
401
- * await definePDFJSModule(() => import('pdfjs-dist/legacy/build/pdf.mjs'))
402
- */
403
- async function definePDFJSModule(pdfjs) {
404
- await resolvePDFJSImport(pdfjs, { reload: true });
405
- }
406
- /** @deprecated Use `definePDFJSModule` instead. Will be removed in v2. */
407
- async function configureUnPDF(options) {
408
- await resolvePDFJSImport(options.pdfjs, { reload: true });
409
- }
410
- //#endregion
411
- //#region src/index.ts
412
- const getMeta = async (...args) => {
413
- await resolvePDFJSImport();
414
- return await getMeta$1(...args);
415
- };
416
- const extractText = async (...args) => {
417
- await resolvePDFJSImport();
418
- return await extractText$1(...args);
419
- };
420
- const extractTextItems = async (...args) => {
421
- await resolvePDFJSImport();
422
- return await extractTextItems$1(...args);
423
- };
424
- const extractImages = async (...args) => {
425
- await resolvePDFJSImport();
426
- return await extractImages$1(...args);
427
- };
428
- const renderPageAsImage = async (...args) => {
429
- await resolvePDFJSImport();
430
- return await renderPageAsImage$1(...args);
431
- };
432
- const extractLinks = async (...args) => {
433
- await resolvePDFJSImport();
434
- return await extractLinks$1(...args);
435
- };
436
- //#endregion
437
- export { configureUnPDF, createIsomorphicCanvasFactory, definePDFJSModule, extractImages, extractLinks, extractText, extractTextItems, getDocumentProxy, getMeta, getResolvedPDFJS, renderPageAsImage, resolvePDFJSImport };
643
+ export { definePDFJSModule, extractImages, extractLinks, extractText, extractTextItems, getDocumentProxy, getMeta, getResolvedPDFJS, inspectPages, renderPageAsImage };