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/README.md +97 -195
- package/dist/index.d.mts +123 -118
- package/dist/index.mjs +429 -223
- package/dist/pdfjs.mjs +6 -6
- package/package.json +14 -31
- package/dist/index.cjs +0 -449
- package/dist/index.d.cts +0 -180
- 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
|
-
* Creates a `PDFDocumentProxy` from binary PDF data
|
|
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,177 +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
|
+
];
|
|
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
|
-
*
|
|
210
|
-
*
|
|
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
|
-
|
|
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 (
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
|
244
|
-
|
|
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
|
|
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
|
|
253
|
-
const canvasFactory =
|
|
254
|
-
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);
|
|
255
392
|
try {
|
|
393
|
+
const { canvas } = canvasAndContext;
|
|
256
394
|
await page.render({
|
|
257
|
-
canvas
|
|
258
|
-
canvasContext: drawingContext.context,
|
|
395
|
+
canvas,
|
|
259
396
|
viewport
|
|
260
397
|
}).promise;
|
|
261
|
-
const {
|
|
262
|
-
|
|
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(
|
|
265
|
-
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);
|
|
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(
|
|
271
|
-
|
|
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(
|
|
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
|
-
}
|
|
455
|
+
}
|
|
456
|
+
return rgba;
|
|
276
457
|
}
|
|
277
|
-
async function
|
|
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
|
|
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
|
-
|
|
541
|
+
return area / viewArea;
|
|
286
542
|
}
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
593
|
+
return Number.isNaN(parsedDate) ? null : new Date(parsedDate);
|
|
341
594
|
}
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|