unpdf 1.9.0 → 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/README.md +97 -280
- package/dist/index.d.mts +123 -130
- package/dist/index.mjs +439 -246
- package/dist/pdfjs.mjs +6 -6
- package/package.json +14 -30
- package/dist/index.cjs +0 -462
- package/dist/index.d.cts +0 -192
- package/dist/pdfjs.d.ts +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
37
|
-
|
|
40
|
+
// src/utils.ts
|
|
41
|
+
let pdfjsLoader;
|
|
42
|
+
let resolvedModule;
|
|
38
43
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
|
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
|
-
|
|
72
|
-
standardFontDataUrl:
|
|
73
|
-
cMapUrl:
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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,189 +116,437 @@ async function interopDefault(m) {
|
|
|
121
116
|
const resolved = await m;
|
|
122
117
|
return resolved.default || resolved;
|
|
123
118
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
192
|
-
*
|
|
193
|
-
*
|
|
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
|
|
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
|
-
|
|
207
|
-
|
|
151
|
+
// src/_internal/operator-list.ts
|
|
152
|
+
const IDENTITY_MATRIX = [
|
|
153
|
+
1,
|
|
154
|
+
0,
|
|
155
|
+
0,
|
|
156
|
+
1,
|
|
157
|
+
0,
|
|
158
|
+
0
|
|
159
|
+
];
|
|
208
160
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
|
|
213
|
-
*
|
|
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
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Maps the unit square, which PDF images are drawn into, through a transform
|
|
218
|
+
* and returns the axis-aligned box around the result.
|
|
219
|
+
*/
|
|
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.
|
|
214
338
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* })
|
|
220
|
-
* .png()
|
|
221
|
-
* .toFile(`${imageIndex}.png`)
|
|
222
|
-
* }
|
|
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.
|
|
223
343
|
*/
|
|
224
|
-
async function extractImages
|
|
344
|
+
async function extractImages(data, pageNumber, options = {}) {
|
|
225
345
|
return await withDocument(data, async (pdf) => {
|
|
226
346
|
if (pageNumber < 1 || pageNumber > pdf.numPages) throw new Error(`Invalid page number. Must be between 1 and ${pdf.numPages}.`);
|
|
227
347
|
const page = await pdf.getPage(pageNumber);
|
|
228
|
-
const operatorList = await page.getOperatorList();
|
|
229
|
-
const { OPS } = await getResolvedPDFJS();
|
|
348
|
+
const [operatorList, optionalContentConfig] = await Promise.all([page.getOperatorList(), pdf.getOptionalContentConfig()]);
|
|
349
|
+
const { ImageKind, OPS } = await getResolvedPDFJS();
|
|
230
350
|
const images = [];
|
|
231
|
-
for (
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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;
|
|
244
366
|
images.push({
|
|
245
|
-
data,
|
|
246
|
-
width,
|
|
247
|
-
height,
|
|
248
|
-
|
|
249
|
-
|
|
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)
|
|
250
372
|
});
|
|
251
373
|
}
|
|
252
374
|
return images;
|
|
375
|
+
}, {
|
|
376
|
+
isOffscreenCanvasSupported: false,
|
|
377
|
+
isImageDecoderSupported: false
|
|
253
378
|
});
|
|
254
379
|
}
|
|
255
|
-
async function renderPageAsImage
|
|
256
|
-
|
|
380
|
+
async function renderPageAsImage(data, pageNumber, options = {}) {
|
|
381
|
+
await prepareCanvasEnvironment(options.canvasImport);
|
|
257
382
|
return await withDocument(data, async (pdf) => {
|
|
258
383
|
if (pageNumber < 1 || pageNumber > pdf.numPages) throw new Error(`Invalid page number. Must be between 1 and ${pdf.numPages}.`);
|
|
259
384
|
const page = await pdf.getPage(pageNumber);
|
|
260
385
|
const defaultViewport = page.getViewport({ scale: 1 });
|
|
261
|
-
let scale = options.scale
|
|
262
|
-
if (options.width) scale = options.width / defaultViewport.width;
|
|
263
|
-
else if (options.height) scale = options.height / defaultViewport.height;
|
|
264
|
-
const viewport = page.getViewport({ scale
|
|
265
|
-
const canvasFactory =
|
|
266
|
-
const
|
|
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);
|
|
267
392
|
try {
|
|
393
|
+
const { canvas } = canvasAndContext;
|
|
268
394
|
await page.render({
|
|
269
|
-
canvas
|
|
270
|
-
canvasContext: drawingContext.context,
|
|
395
|
+
canvas,
|
|
271
396
|
viewport
|
|
272
397
|
}).promise;
|
|
273
|
-
const {
|
|
274
|
-
|
|
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
|
+
}
|
|
275
405
|
if ("encode" in canvas) {
|
|
276
|
-
const buffer = await canvas.encode(
|
|
277
|
-
return
|
|
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);
|
|
278
408
|
}
|
|
279
409
|
const blob = await new Promise((resolve) => {
|
|
280
|
-
canvas.toBlob(resolve);
|
|
410
|
+
canvas.toBlob(resolve, mimeType, quality);
|
|
281
411
|
});
|
|
282
|
-
if (!blob) throw new Error(
|
|
283
|
-
|
|
412
|
+
if (!blob) throw new Error(`Failed to encode canvas as ${mimeType}.`);
|
|
413
|
+
assertEncodedType(blob.type, mimeType);
|
|
414
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
284
415
|
} finally {
|
|
285
|
-
canvasFactory.destroy(
|
|
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;
|
|
286
454
|
}
|
|
287
|
-
}
|
|
455
|
+
}
|
|
456
|
+
return rgba;
|
|
288
457
|
}
|
|
289
|
-
async function
|
|
290
|
-
if (isBrowser) return DOMCanvasFactory;
|
|
458
|
+
async function prepareCanvasEnvironment(canvasImport) {
|
|
291
459
|
if (isNode) {
|
|
292
460
|
if (!canvasImport) throw new Error("Parameter \"canvasImport\" is required in Node.js environment.");
|
|
293
461
|
await resolveCanvasModule(canvasImport);
|
|
294
462
|
injectCanvasConstructors();
|
|
295
|
-
return
|
|
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);
|
|
296
540
|
}
|
|
297
|
-
|
|
541
|
+
return area / viewArea;
|
|
298
542
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
async function extractLinks$1(data) {
|
|
543
|
+
// src/links.ts
|
|
544
|
+
async function extractLinks(data) {
|
|
302
545
|
return await withDocument(data, async (pdf) => {
|
|
303
|
-
const
|
|
546
|
+
const links = await Promise.all(Array.from({ length: pdf.numPages }, (_, i) => getPageLinks(pdf, i + 1)));
|
|
304
547
|
return {
|
|
305
548
|
totalPages: pdf.numPages,
|
|
306
|
-
links
|
|
549
|
+
links
|
|
307
550
|
};
|
|
308
551
|
});
|
|
309
552
|
}
|
|
@@ -313,8 +556,7 @@ async function getPageLinks(document, pageNumber) {
|
|
|
313
556
|
for (const annotation of annotations) if (annotation.subtype === "Link" && annotation.url) links.push(annotation.url);
|
|
314
557
|
return links;
|
|
315
558
|
}
|
|
316
|
-
|
|
317
|
-
//#region src/meta.ts
|
|
559
|
+
// src/meta.ts
|
|
318
560
|
const XMP_DATE_PROPERTIES = [
|
|
319
561
|
"xmp:createdate",
|
|
320
562
|
"xmp:modifydate",
|
|
@@ -323,37 +565,35 @@ const XMP_DATE_PROPERTIES = [
|
|
|
323
565
|
"xap:modifydate",
|
|
324
566
|
"xap:metadatadate"
|
|
325
567
|
];
|
|
326
|
-
async function getMeta
|
|
568
|
+
async function getMeta(data, options = {}) {
|
|
327
569
|
return await withDocument(data, async (pdf) => {
|
|
328
570
|
const meta = await pdf.getMetadata();
|
|
329
|
-
const info = meta
|
|
571
|
+
const info = { ...meta.info };
|
|
572
|
+
let metadata = meta.metadata;
|
|
330
573
|
if (options.parseDates) {
|
|
331
574
|
const { PDFDateString } = await getResolvedPDFJS();
|
|
332
|
-
if (info
|
|
333
|
-
if (info
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
} });
|
|
575
|
+
if (info.CreationDate) info.CreationDate = PDFDateString.toDateObject(info.CreationDate);
|
|
576
|
+
if (info.ModDate) info.ModDate = PDFDateString.toDateObject(info.ModDate);
|
|
577
|
+
if (metadata) {
|
|
578
|
+
const Metadata = metadata.constructor;
|
|
579
|
+
metadata = new Metadata({
|
|
580
|
+
parsedData: new Map(Array.from(metadata, ([name, value]) => [name, XMP_DATE_PROPERTIES.includes(name) && value ? parseISODateString(value) : value])),
|
|
581
|
+
rawData: metadata.getRaw()
|
|
582
|
+
});
|
|
583
|
+
}
|
|
342
584
|
}
|
|
343
585
|
return {
|
|
344
586
|
info,
|
|
345
|
-
metadata:
|
|
587
|
+
metadata: metadata ?? null
|
|
346
588
|
};
|
|
347
589
|
});
|
|
348
590
|
}
|
|
349
591
|
function parseISODateString(isoDateString) {
|
|
350
|
-
if (!isoDateString) return;
|
|
351
592
|
const parsedDate = Date.parse(isoDateString);
|
|
352
|
-
|
|
593
|
+
return Number.isNaN(parsedDate) ? null : new Date(parsedDate);
|
|
353
594
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
async function extractTextItems$1(data) {
|
|
595
|
+
// src/text.ts
|
|
596
|
+
async function extractTextItems(data) {
|
|
357
597
|
return await withDocument(data, async (pdf) => {
|
|
358
598
|
const items = await Promise.all(Array.from({ length: pdf.numPages }, (_, i) => getPageTextItems(pdf, i + 1)));
|
|
359
599
|
return {
|
|
@@ -380,7 +620,7 @@ async function getPageTextItems(document, pageNumber) {
|
|
|
380
620
|
};
|
|
381
621
|
});
|
|
382
622
|
}
|
|
383
|
-
async function extractText
|
|
623
|
+
async function extractText(data, options = {}) {
|
|
384
624
|
const { mergePages = false } = options;
|
|
385
625
|
return await withDocument(data, async (pdf) => {
|
|
386
626
|
const texts = await Promise.all(Array.from({ length: pdf.numPages }, (_, i) => getPageText(pdf, i + 1)));
|
|
@@ -400,51 +640,4 @@ async function getPageText(document, pageNumber) {
|
|
|
400
640
|
function normalizeMergedText(texts) {
|
|
401
641
|
return texts.join("\n").replace(/[^\S\n]+/g, " ").replace(/ ?\n ?/g, "\n").replace(/\n{3,}/g, "\n\n");
|
|
402
642
|
}
|
|
403
|
-
|
|
404
|
-
//#region src/config.ts
|
|
405
|
-
/**
|
|
406
|
-
* By default, unpdf will use the latest version of PDF.js compiled for
|
|
407
|
-
* serverless environments. If you want to use a different version, you can
|
|
408
|
-
* provide a custom resolver function.
|
|
409
|
-
*
|
|
410
|
-
* @example
|
|
411
|
-
* // Use the official PDF.js build (make sure to install it first)
|
|
412
|
-
* import { definePDFJSModule } from 'unpdf'
|
|
413
|
-
*
|
|
414
|
-
* await definePDFJSModule(() => import('pdfjs-dist'))
|
|
415
|
-
*/
|
|
416
|
-
async function definePDFJSModule(pdfjs) {
|
|
417
|
-
await resolvePDFJSImport(pdfjs, { reload: true });
|
|
418
|
-
}
|
|
419
|
-
/** @deprecated Use `definePDFJSModule` instead. Will be removed in v2. */
|
|
420
|
-
async function configureUnPDF(options) {
|
|
421
|
-
await resolvePDFJSImport(options.pdfjs, { reload: true });
|
|
422
|
-
}
|
|
423
|
-
//#endregion
|
|
424
|
-
//#region src/index.ts
|
|
425
|
-
const getMeta = async (...args) => {
|
|
426
|
-
await resolvePDFJSImport();
|
|
427
|
-
return await getMeta$1(...args);
|
|
428
|
-
};
|
|
429
|
-
const extractText = async (...args) => {
|
|
430
|
-
await resolvePDFJSImport();
|
|
431
|
-
return await extractText$1(...args);
|
|
432
|
-
};
|
|
433
|
-
const extractTextItems = async (...args) => {
|
|
434
|
-
await resolvePDFJSImport();
|
|
435
|
-
return await extractTextItems$1(...args);
|
|
436
|
-
};
|
|
437
|
-
const extractImages = async (...args) => {
|
|
438
|
-
await resolvePDFJSImport();
|
|
439
|
-
return await extractImages$1(...args);
|
|
440
|
-
};
|
|
441
|
-
const renderPageAsImage = async (...args) => {
|
|
442
|
-
await resolvePDFJSImport();
|
|
443
|
-
return await renderPageAsImage$1(...args);
|
|
444
|
-
};
|
|
445
|
-
const extractLinks = async (...args) => {
|
|
446
|
-
await resolvePDFJSImport();
|
|
447
|
-
return await extractLinks$1(...args);
|
|
448
|
-
};
|
|
449
|
-
//#endregion
|
|
450
|
-
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 };
|