@embedpdf/engines 1.0.2 → 1.0.4

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.
Files changed (49) hide show
  1. package/dist/converters.cjs +149 -115
  2. package/dist/converters.cjs.map +1 -1
  3. package/dist/converters.d.ts +2 -1
  4. package/dist/converters.js +143 -86
  5. package/dist/converters.js.map +1 -1
  6. package/dist/index.cjs +5362 -5994
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.ts +1479 -6
  9. package/dist/index.js +5399 -356
  10. package/dist/index.js.map +1 -1
  11. package/dist/pdfium-direct-engine.cjs +4037 -0
  12. package/dist/pdfium-direct-engine.cjs.map +1 -0
  13. package/dist/{pdfium.d.cts → pdfium-direct-engine.d.ts} +4 -96
  14. package/dist/pdfium-direct-engine.js +4035 -0
  15. package/dist/pdfium-direct-engine.js.map +1 -0
  16. package/dist/pdfium-worker-engine.cjs +800 -0
  17. package/dist/pdfium-worker-engine.cjs.map +1 -0
  18. package/dist/{worker.d.cts → pdfium-worker-engine.d.ts} +36 -4
  19. package/dist/pdfium-worker-engine.js +798 -0
  20. package/dist/pdfium-worker-engine.js.map +1 -0
  21. package/dist/pdfium.cjs +4243 -5663
  22. package/dist/pdfium.cjs.map +1 -1
  23. package/dist/pdfium.d.ts +131 -3
  24. package/dist/pdfium.js +4288 -21
  25. package/dist/pdfium.js.map +1 -1
  26. package/dist/preact.cjs +39 -0
  27. package/dist/preact.cjs.map +1 -0
  28. package/dist/preact.d.ts +13 -0
  29. package/dist/preact.js +37 -0
  30. package/dist/preact.js.map +1 -0
  31. package/dist/react.cjs +39 -0
  32. package/dist/react.cjs.map +1 -0
  33. package/dist/react.d.ts +13 -0
  34. package/dist/react.js +37 -0
  35. package/dist/react.js.map +1 -0
  36. package/dist/worker.cjs +771 -1104
  37. package/dist/worker.cjs.map +1 -1
  38. package/dist/worker.d.ts +30 -4
  39. package/dist/worker.js +786 -11
  40. package/dist/worker.js.map +1 -1
  41. package/package.json +42 -8
  42. package/dist/chunk-NDTYBBMQ.js +0 -4615
  43. package/dist/chunk-NDTYBBMQ.js.map +0 -1
  44. package/dist/chunk-YZLT3A2D.js +0 -1101
  45. package/dist/chunk-YZLT3A2D.js.map +0 -1
  46. package/dist/converters.d.cts +0 -69
  47. package/dist/index.d.cts +0 -32
  48. package/dist/runner-BvRtPCKL.d.cts +0 -131
  49. package/dist/runner-BvRtPCKL.d.ts +0 -131
@@ -0,0 +1,4037 @@
1
+ 'use strict';
2
+
3
+ var models = require('@embedpdf/models');
4
+ var pdfium = require('@embedpdf/pdfium');
5
+
6
+ /**
7
+ * Read string from WASM heap
8
+ * @param wasmModule - pdfium wasm module instance
9
+ * @param readChars - function to read chars
10
+ * @param parseChars - function to parse chars
11
+ * @param defaultLength - default length of chars that needs to read
12
+ * @returns string from the heap
13
+ *
14
+ * @public
15
+ */
16
+ function readString(wasmModule, readChars, parseChars, defaultLength = 100) {
17
+ let buffer = wasmModule.wasmExports.malloc(defaultLength);
18
+ for (let i = 0; i < defaultLength; i++) {
19
+ wasmModule.HEAP8[buffer + i] = 0;
20
+ }
21
+ const actualLength = readChars(buffer, defaultLength);
22
+ let str;
23
+ if (actualLength > defaultLength) {
24
+ wasmModule.wasmExports.free(buffer);
25
+ buffer = wasmModule.wasmExports.malloc(actualLength);
26
+ for (let i = 0; i < actualLength; i++) {
27
+ wasmModule.HEAP8[buffer + i] = 0;
28
+ }
29
+ readChars(buffer, actualLength);
30
+ str = parseChars(buffer);
31
+ }
32
+ else {
33
+ str = parseChars(buffer);
34
+ }
35
+ wasmModule.wasmExports.free(buffer);
36
+ return str;
37
+ }
38
+ /**
39
+ * Read arraybyffer from WASM heap
40
+ * @param wasmModule - pdfium wasm module instance
41
+ * @param readChars - function to read chars
42
+ * @returns arraybuffer from the heap
43
+ *
44
+ * @public
45
+ */
46
+ function readArrayBuffer(wasmModule, readChars) {
47
+ const bufferSize = readChars(0, 0);
48
+ const bufferPtr = wasmModule.wasmExports.malloc(bufferSize);
49
+ readChars(bufferPtr, bufferSize);
50
+ const arrayBuffer = new ArrayBuffer(bufferSize);
51
+ const view = new DataView(arrayBuffer);
52
+ for (let i = 0; i < bufferSize; i++) {
53
+ view.setInt8(i, wasmModule.getValue(bufferPtr + i, 'i8'));
54
+ }
55
+ wasmModule.wasmExports.free(bufferPtr);
56
+ return arrayBuffer;
57
+ }
58
+
59
+ class PdfCache {
60
+ constructor(pdfium) {
61
+ this.pdfium = pdfium;
62
+ this.docs = new Map();
63
+ }
64
+ /** Open (or re-use) a document */
65
+ setDocument(id, filePtr, docPtr) {
66
+ let ctx = this.docs.get(id);
67
+ if (!ctx) {
68
+ ctx = new DocumentContext(filePtr, docPtr, this.pdfium);
69
+ this.docs.set(id, ctx);
70
+ }
71
+ }
72
+ /** Retrieve the DocumentContext for a given PdfDocumentObject */
73
+ getContext(docId) {
74
+ return this.docs.get(docId);
75
+ }
76
+ /** Close & fully release a document and all its pages */
77
+ closeDocument(docId) {
78
+ const ctx = this.docs.get(docId);
79
+ if (!ctx)
80
+ return false;
81
+ ctx.dispose(); // tears down pages first, then FPDF_CloseDocument, free()
82
+ this.docs.delete(docId);
83
+ return true;
84
+ }
85
+ }
86
+ class DocumentContext {
87
+ constructor(filePtr, docPtr, pdfium) {
88
+ this.filePtr = filePtr;
89
+ this.docPtr = docPtr;
90
+ this.pageCache = new PageCache(pdfium, docPtr);
91
+ }
92
+ /** Main accessor for pages */
93
+ acquirePage(pageIdx) {
94
+ return this.pageCache.acquire(pageIdx);
95
+ }
96
+ /** Tear down all pages + this document */
97
+ dispose() {
98
+ // 1️⃣ release all pages (with their TTL or immediate)
99
+ this.pageCache.forceReleaseAll();
100
+ // 2️⃣ close the PDFium document
101
+ this.pageCache.pdf.FPDF_CloseDocument(this.docPtr);
102
+ // 3️⃣ free the file handle
103
+ this.pageCache.pdf.pdfium.wasmExports.free(this.filePtr);
104
+ }
105
+ }
106
+ class PageCache {
107
+ constructor(pdf, docPtr) {
108
+ this.pdf = pdf;
109
+ this.docPtr = docPtr;
110
+ this.cache = new Map();
111
+ }
112
+ acquire(pageIdx) {
113
+ let ctx = this.cache.get(pageIdx);
114
+ if (!ctx) {
115
+ const pagePtr = this.pdf.FPDF_LoadPage(this.docPtr, pageIdx);
116
+ ctx = new PageContext(this.pdf, this.docPtr, pageIdx, pagePtr, () => {
117
+ this.cache.delete(pageIdx);
118
+ });
119
+ this.cache.set(pageIdx, ctx);
120
+ }
121
+ ctx.clearExpiryTimer(); // cancel any pending teardown
122
+ ctx.bumpRefCount(); // bump ref‐count
123
+ return ctx;
124
+ }
125
+ forceReleaseAll() {
126
+ for (const ctx of this.cache.values()) {
127
+ ctx.disposeImmediate();
128
+ }
129
+ this.cache.clear();
130
+ }
131
+ }
132
+ const PAGE_TTL = 5000; // 5 seconds
133
+ class PageContext {
134
+ constructor(pdf, docPtr, pageIdx, pagePtr, onFinalDispose) {
135
+ this.pdf = pdf;
136
+ this.docPtr = docPtr;
137
+ this.pageIdx = pageIdx;
138
+ this.pagePtr = pagePtr;
139
+ this.onFinalDispose = onFinalDispose;
140
+ this.refCount = 0;
141
+ this.disposed = false;
142
+ }
143
+ /** Called by PageCache.acquire() */
144
+ bumpRefCount() {
145
+ if (this.disposed)
146
+ throw new Error('Context already disposed');
147
+ this.refCount++;
148
+ }
149
+ /** Called by PageCache.acquire() */
150
+ clearExpiryTimer() {
151
+ if (this.expiryTimer) {
152
+ clearTimeout(this.expiryTimer);
153
+ this.expiryTimer = undefined;
154
+ }
155
+ }
156
+ /** Called by PageCache.release() internally */
157
+ release() {
158
+ if (this.disposed)
159
+ return;
160
+ this.refCount--;
161
+ if (this.refCount === 0) {
162
+ // schedule the one-and-only timer for the page
163
+ this.expiryTimer = setTimeout(() => this.disposeImmediate(), PAGE_TTL);
164
+ }
165
+ }
166
+ /** Tear down _all_ sub-pointers & the page. */
167
+ disposeImmediate() {
168
+ if (this.disposed)
169
+ return;
170
+ this.disposed = true;
171
+ // 2️⃣ close text-page if opened
172
+ if (this.textPagePtr !== undefined) {
173
+ this.pdf.FPDFText_ClosePage(this.textPagePtr);
174
+ }
175
+ // 3️⃣ close form-fill if opened
176
+ if (this.formHandle !== undefined) {
177
+ this.pdf.FORM_OnBeforeClosePage(this.pagePtr, this.formHandle);
178
+ this.pdf.PDFiumExt_ExitFormFillEnvironment(this.formHandle);
179
+ }
180
+ if (this.formInfoPtr !== undefined) {
181
+ this.pdf.PDFiumExt_CloseFormFillInfo(this.formInfoPtr);
182
+ }
183
+ // 4️⃣ finally close the page itself
184
+ this.pdf.FPDF_ClosePage(this.pagePtr);
185
+ // 5️⃣ remove from the cache
186
+ this.onFinalDispose();
187
+ }
188
+ // ── public helpers ──
189
+ /** Always safe: opens (once) and returns the text-page ptr. */
190
+ getTextPage() {
191
+ this.ensureAlive();
192
+ if (this.textPagePtr === undefined) {
193
+ this.textPagePtr = this.pdf.FPDFText_LoadPage(this.pagePtr);
194
+ }
195
+ return this.textPagePtr;
196
+ }
197
+ /** Always safe: opens (once) and returns the form-fill handle. */
198
+ getFormHandle() {
199
+ this.ensureAlive();
200
+ if (this.formHandle === undefined) {
201
+ this.formInfoPtr = this.pdf.PDFiumExt_OpenFormFillInfo();
202
+ this.formHandle = this.pdf.PDFiumExt_InitFormFillEnvironment(this.docPtr, this.formInfoPtr);
203
+ this.pdf.FORM_OnAfterLoadPage(this.pagePtr, this.formHandle);
204
+ }
205
+ return this.formHandle;
206
+ }
207
+ /**
208
+ * Safely execute `fn` with an annotation pointer.
209
+ * Pointer is ALWAYS closed afterwards.
210
+ */
211
+ withAnnotation(annotIdx, fn) {
212
+ this.ensureAlive();
213
+ const annotPtr = this.pdf.FPDFPage_GetAnnot(this.pagePtr, annotIdx);
214
+ try {
215
+ return fn(annotPtr);
216
+ }
217
+ finally {
218
+ this.pdf.FPDFPage_CloseAnnot(annotPtr);
219
+ }
220
+ }
221
+ ensureAlive() {
222
+ if (this.disposed)
223
+ throw new Error('PageContext already disposed');
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Format of bitmap
229
+ */
230
+ var BitmapFormat;
231
+ (function (BitmapFormat) {
232
+ BitmapFormat[BitmapFormat["Bitmap_Gray"] = 1] = "Bitmap_Gray";
233
+ BitmapFormat[BitmapFormat["Bitmap_BGR"] = 2] = "Bitmap_BGR";
234
+ BitmapFormat[BitmapFormat["Bitmap_BGRx"] = 3] = "Bitmap_BGRx";
235
+ BitmapFormat[BitmapFormat["Bitmap_BGRA"] = 4] = "Bitmap_BGRA";
236
+ })(BitmapFormat || (BitmapFormat = {}));
237
+ /**
238
+ * Pdf rendering flag
239
+ */
240
+ var RenderFlag;
241
+ (function (RenderFlag) {
242
+ RenderFlag[RenderFlag["ANNOT"] = 1] = "ANNOT";
243
+ RenderFlag[RenderFlag["LCD_TEXT"] = 2] = "LCD_TEXT";
244
+ RenderFlag[RenderFlag["NO_NATIVETEXT"] = 4] = "NO_NATIVETEXT";
245
+ RenderFlag[RenderFlag["GRAYSCALE"] = 8] = "GRAYSCALE";
246
+ RenderFlag[RenderFlag["DEBUG_INFO"] = 128] = "DEBUG_INFO";
247
+ RenderFlag[RenderFlag["NO_CATCH"] = 256] = "NO_CATCH";
248
+ RenderFlag[RenderFlag["RENDER_LIMITEDIMAGECACHE"] = 512] = "RENDER_LIMITEDIMAGECACHE";
249
+ RenderFlag[RenderFlag["RENDER_FORCEHALFTONE"] = 1024] = "RENDER_FORCEHALFTONE";
250
+ RenderFlag[RenderFlag["PRINTING"] = 2048] = "PRINTING";
251
+ RenderFlag[RenderFlag["REVERSE_BYTE_ORDER"] = 16] = "REVERSE_BYTE_ORDER";
252
+ })(RenderFlag || (RenderFlag = {}));
253
+ const LOG_SOURCE = 'PDFiumEngine';
254
+ const LOG_CATEGORY = 'Engine';
255
+ /**
256
+ * Error code of pdfium library
257
+ */
258
+ var PdfiumErrorCode;
259
+ (function (PdfiumErrorCode) {
260
+ PdfiumErrorCode[PdfiumErrorCode["Success"] = 0] = "Success";
261
+ PdfiumErrorCode[PdfiumErrorCode["Unknown"] = 1] = "Unknown";
262
+ PdfiumErrorCode[PdfiumErrorCode["File"] = 2] = "File";
263
+ PdfiumErrorCode[PdfiumErrorCode["Format"] = 3] = "Format";
264
+ PdfiumErrorCode[PdfiumErrorCode["Password"] = 4] = "Password";
265
+ PdfiumErrorCode[PdfiumErrorCode["Security"] = 5] = "Security";
266
+ PdfiumErrorCode[PdfiumErrorCode["Page"] = 6] = "Page";
267
+ PdfiumErrorCode[PdfiumErrorCode["XFALoad"] = 7] = "XFALoad";
268
+ PdfiumErrorCode[PdfiumErrorCode["XFALayout"] = 8] = "XFALayout";
269
+ })(PdfiumErrorCode || (PdfiumErrorCode = {}));
270
+ const browserImageDataToBlobConverter = (pdfImageData) => {
271
+ // Check if we're in a browser environment
272
+ if (typeof OffscreenCanvas === 'undefined') {
273
+ throw new Error('OffscreenCanvas is not available in this environment. ' +
274
+ 'This converter is intended for browser use only. ' +
275
+ 'Please use createNodeImageDataToBlobConverter() or createNodeCanvasImageDataToBlobConverter() for Node.js.');
276
+ }
277
+ const imageData = new ImageData(pdfImageData.data, pdfImageData.width, pdfImageData.height);
278
+ const off = new OffscreenCanvas(imageData.width, imageData.height);
279
+ off.getContext('2d').putImageData(imageData, 0, 0);
280
+ return off.convertToBlob({ type: 'image/webp' });
281
+ };
282
+ /**
283
+ * Pdf engine that based on pdfium wasm
284
+ */
285
+ class PdfiumEngine {
286
+ /**
287
+ * Create an instance of PdfiumEngine
288
+ * @param wasmModule - pdfium wasm module
289
+ * @param logger - logger instance
290
+ * @param imageDataToBlobConverter - function to convert ImageData to Blob
291
+ */
292
+ constructor(pdfiumModule, logger = new models.NoopLogger(), imageDataConverter = browserImageDataToBlobConverter) {
293
+ this.pdfiumModule = pdfiumModule;
294
+ this.logger = logger;
295
+ this.imageDataConverter = imageDataConverter;
296
+ this.cache = new PdfCache(this.pdfiumModule);
297
+ }
298
+ /**
299
+ * {@inheritDoc @embedpdf/models!PdfEngine.initialize}
300
+ *
301
+ * @public
302
+ */
303
+ initialize() {
304
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'initialize');
305
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `Initialize`, 'Begin', 'General');
306
+ this.pdfiumModule.PDFiumExt_Init();
307
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `Initialize`, 'End', 'General');
308
+ return models.PdfTaskHelper.resolve(true);
309
+ }
310
+ /**
311
+ * {@inheritDoc @embedpdf/models!PdfEngine.destroy}
312
+ *
313
+ * @public
314
+ */
315
+ destroy() {
316
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'destroy');
317
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `Destroy`, 'Begin', 'General');
318
+ this.pdfiumModule.FPDF_DestroyLibrary();
319
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `Destroy`, 'End', 'General');
320
+ return models.PdfTaskHelper.resolve(true);
321
+ }
322
+ /**
323
+ * {@inheritDoc @embedpdf/models!PdfEngine.openDocumentUrl}
324
+ *
325
+ * @public
326
+ */
327
+ openDocumentUrl(file, options) {
328
+ const mode = options?.mode ?? 'auto';
329
+ const password = options?.password ?? '';
330
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'openDocumentUrl called', file.url, mode);
331
+ // We'll create a task to wrap asynchronous steps
332
+ const task = models.PdfTaskHelper.create();
333
+ // Start an async procedure
334
+ (async () => {
335
+ try {
336
+ // Decide on approach
337
+ if (mode === 'full-fetch') {
338
+ const fetchFullTask = await this.fetchFullAndOpen(file, password);
339
+ fetchFullTask.wait((doc) => task.resolve(doc), (err) => task.reject(err.reason));
340
+ }
341
+ else if (mode === 'range-request') {
342
+ const openDocumentWithRangeRequestTask = await this.openDocumentWithRangeRequest(file, password);
343
+ openDocumentWithRangeRequestTask.wait((doc) => task.resolve(doc), (err) => task.reject(err.reason));
344
+ }
345
+ else {
346
+ // mode: 'auto'
347
+ const { supportsRanges, fileLength, content } = await this.checkRangeSupport(file.url);
348
+ if (supportsRanges) {
349
+ const openDocumentWithRangeRequestTask = await this.openDocumentWithRangeRequest(file, password, fileLength);
350
+ openDocumentWithRangeRequestTask.wait((doc) => task.resolve(doc), (err) => task.reject(err.reason));
351
+ }
352
+ else if (content) {
353
+ // If we already have the content from the range check, use it
354
+ const pdfFile = { id: file.id, content };
355
+ this.openDocumentFromBuffer(pdfFile, password).wait((doc) => task.resolve(doc), (err) => task.reject(err.reason));
356
+ }
357
+ else {
358
+ const fetchFullTask = await this.fetchFullAndOpen(file, password);
359
+ fetchFullTask.wait((doc) => task.resolve(doc), (err) => task.reject(err.reason));
360
+ }
361
+ }
362
+ }
363
+ catch (err) {
364
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, 'openDocumentUrl error', err);
365
+ task.reject({
366
+ code: models.PdfErrorCode.Unknown,
367
+ message: String(err),
368
+ });
369
+ }
370
+ })();
371
+ return task;
372
+ }
373
+ /**
374
+ * Check if the server supports range requests:
375
+ * Sends a HEAD request and sees if 'Accept-Ranges: bytes'.
376
+ */
377
+ async checkRangeSupport(url) {
378
+ try {
379
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'checkRangeSupport', url);
380
+ // First try HEAD request
381
+ const headResponse = await fetch(url, { method: 'HEAD' });
382
+ const fileLength = headResponse.headers.get('Content-Length');
383
+ const acceptRanges = headResponse.headers.get('Accept-Ranges');
384
+ // If server explicitly supports ranges, we're done
385
+ if (acceptRanges === 'bytes') {
386
+ return {
387
+ supportsRanges: true,
388
+ fileLength: parseInt(fileLength ?? '0'),
389
+ content: null,
390
+ };
391
+ }
392
+ // Test actual range request support
393
+ const testResponse = await fetch(url, {
394
+ headers: { Range: 'bytes=0-1' },
395
+ });
396
+ // If we get 200 instead of 206, server doesn't support ranges
397
+ // Return the full content since we'll need it anyway
398
+ if (testResponse.status === 200) {
399
+ const content = await testResponse.arrayBuffer();
400
+ return {
401
+ supportsRanges: false,
402
+ fileLength: parseInt(fileLength ?? '0'),
403
+ content: content,
404
+ };
405
+ }
406
+ // 206 Partial Content indicates range support
407
+ return {
408
+ supportsRanges: testResponse.status === 206,
409
+ fileLength: parseInt(fileLength ?? '0'),
410
+ content: null,
411
+ };
412
+ }
413
+ catch (e) {
414
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, 'checkRangeSupport failed', e);
415
+ throw new Error('Failed to check range support: ' + e);
416
+ }
417
+ }
418
+ /**
419
+ * Fully fetch the file (using fetch) into an ArrayBuffer,
420
+ * then call openDocumentFromBuffer.
421
+ */
422
+ async fetchFullAndOpen(file, password) {
423
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'fetchFullAndOpen', file.url);
424
+ // 1. fetch entire PDF as array buffer
425
+ const response = await fetch(file.url);
426
+ if (!response.ok) {
427
+ throw new Error(`Could not fetch PDF: ${response.statusText}`);
428
+ }
429
+ const arrayBuf = await response.arrayBuffer();
430
+ // 2. create a PdfFile object
431
+ const pdfFile = {
432
+ id: file.id,
433
+ content: arrayBuf,
434
+ };
435
+ // 3. call openDocumentFromBuffer (the method you already have)
436
+ // that returns a PdfTask, but let's wrap it in a Promise
437
+ return this.openDocumentFromBuffer(pdfFile, password);
438
+ }
439
+ /**
440
+ * Use your synchronous partial-loading approach:
441
+ * - In your snippet, it's done via `openDocumentFromLoader`.
442
+ * - We'll do a synchronous XHR read callback that pulls
443
+ * the desired byte ranges.
444
+ */
445
+ async openDocumentWithRangeRequest(file, password, knownFileLength) {
446
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'openDocumentWithRangeRequest', file.url);
447
+ // We first do a HEAD or a partial fetch to get the fileLength:
448
+ const fileLength = knownFileLength ?? (await this.retrieveFileLength(file.url)).fileLength;
449
+ // 2. define the callback function used by openDocumentFromLoader
450
+ const callback = (offset, length) => {
451
+ // Perform synchronous XHR:
452
+ const xhr = new XMLHttpRequest();
453
+ xhr.open('GET', file.url, false); // note: block in the Worker
454
+ xhr.overrideMimeType('text/plain; charset=x-user-defined');
455
+ xhr.setRequestHeader('Range', `bytes=${offset}-${offset + length - 1}`);
456
+ xhr.send(null);
457
+ if (xhr.status === 206 || xhr.status === 200) {
458
+ return this.convertResponseToUint8Array(xhr.responseText);
459
+ }
460
+ throw new Error(`Range request failed with status ${xhr.status}`);
461
+ };
462
+ // 3. call `openDocumentFromLoader`
463
+ return this.openDocumentFromLoader({
464
+ id: file.id,
465
+ fileLength,
466
+ callback,
467
+ }, password);
468
+ }
469
+ /**
470
+ * Helper to do a HEAD request or partial GET to find file length.
471
+ */
472
+ async retrieveFileLength(url) {
473
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'retrieveFileLength', url);
474
+ // We'll do a HEAD request to get Content-Length
475
+ const resp = await fetch(url, { method: 'HEAD' });
476
+ if (!resp.ok) {
477
+ throw new Error(`Failed HEAD request for file length: ${resp.statusText}`);
478
+ }
479
+ const lenStr = resp.headers.get('Content-Length') || '0';
480
+ const fileLength = parseInt(lenStr, 10) || 0;
481
+ if (!fileLength) {
482
+ throw new Error(`Content-Length not found or zero.`);
483
+ }
484
+ return { fileLength };
485
+ }
486
+ /**
487
+ * Convert response text (x-user-defined) to a Uint8Array
488
+ * for partial data.
489
+ */
490
+ convertResponseToUint8Array(text) {
491
+ const array = new Uint8Array(text.length);
492
+ for (let i = 0; i < text.length; i++) {
493
+ // & 0xff ensures we only get the lower 8 bits
494
+ array[i] = text.charCodeAt(i) & 0xff;
495
+ }
496
+ return array;
497
+ }
498
+ /**
499
+ * {@inheritDoc @embedpdf/models!PdfEngine.openDocument}
500
+ *
501
+ * @public
502
+ */
503
+ openDocumentFromBuffer(file, password = '') {
504
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'openDocumentFromBuffer', file, password);
505
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `OpenDocumentFromBuffer`, 'Begin', file.id);
506
+ const array = new Uint8Array(file.content);
507
+ const length = array.length;
508
+ const filePtr = this.malloc(length);
509
+ this.pdfiumModule.pdfium.HEAPU8.set(array, filePtr);
510
+ const docPtr = this.pdfiumModule.FPDF_LoadMemDocument(filePtr, length, password);
511
+ if (!docPtr) {
512
+ const lastError = this.pdfiumModule.FPDF_GetLastError();
513
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, `FPDF_LoadMemDocument failed with ${lastError}`);
514
+ this.free(filePtr);
515
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `OpenDocumentFromBuffer`, 'End', file.id);
516
+ return models.PdfTaskHelper.reject({
517
+ code: lastError,
518
+ message: `FPDF_LoadMemDocument failed`,
519
+ });
520
+ }
521
+ const pageCount = this.pdfiumModule.FPDF_GetPageCount(docPtr);
522
+ const pages = [];
523
+ const sizePtr = this.malloc(8);
524
+ for (let index = 0; index < pageCount; index++) {
525
+ const result = this.pdfiumModule.FPDF_GetPageSizeByIndexF(docPtr, index, sizePtr);
526
+ if (!result) {
527
+ const lastError = this.pdfiumModule.FPDF_GetLastError();
528
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, `FPDF_GetPageSizeByIndexF failed with ${lastError}`);
529
+ this.free(sizePtr);
530
+ this.pdfiumModule.FPDF_CloseDocument(docPtr);
531
+ this.free(filePtr);
532
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `OpenDocumentFromBuffer`, 'End', file.id);
533
+ return models.PdfTaskHelper.reject({
534
+ code: lastError,
535
+ message: `FPDF_GetPageSizeByIndexF failed`,
536
+ });
537
+ }
538
+ const page = {
539
+ index,
540
+ size: {
541
+ width: this.pdfiumModule.pdfium.getValue(sizePtr, 'float'),
542
+ height: this.pdfiumModule.pdfium.getValue(sizePtr + 4, 'float'),
543
+ },
544
+ };
545
+ pages.push(page);
546
+ }
547
+ this.free(sizePtr);
548
+ const pdfDoc = {
549
+ id: file.id,
550
+ pageCount,
551
+ pages,
552
+ };
553
+ this.cache.setDocument(file.id, filePtr, docPtr);
554
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `OpenDocumentFromBuffer`, 'End', file.id);
555
+ return models.PdfTaskHelper.resolve(pdfDoc);
556
+ }
557
+ /**
558
+ * {@inheritDoc @embedpdf/models!PdfEngine.openDocumentFromLoader}
559
+ *
560
+ * @public
561
+ */
562
+ openDocumentFromLoader(fileLoader, password = '') {
563
+ const { fileLength, callback, ...file } = fileLoader;
564
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'openDocumentFromLoader', file, password);
565
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `OpenDocumentFromLoader`, 'Begin', file.id);
566
+ const readBlock = (_pThis, // Pointer to the FPDF_FILEACCESS structure
567
+ offset, // Pointer to a buffer to receive the data
568
+ pBuf, // Offset position from the beginning of the file
569
+ length) => {
570
+ try {
571
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'readBlock', offset, length, pBuf);
572
+ if (offset < 0 || offset >= fileLength) {
573
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, 'Offset out of bounds:', offset);
574
+ return 0;
575
+ }
576
+ // Get data chunk using the callback
577
+ const data = callback(offset, length);
578
+ // Copy the data to PDFium's buffer
579
+ const dest = new Uint8Array(this.pdfiumModule.pdfium.HEAPU8.buffer, pBuf, data.length);
580
+ dest.set(data);
581
+ return data.length;
582
+ }
583
+ catch (error) {
584
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, 'ReadBlock error:', error);
585
+ return 0;
586
+ }
587
+ };
588
+ const callbackPtr = this.pdfiumModule.pdfium.addFunction(readBlock, 'iiiii');
589
+ // Create FPDF_FILEACCESS struct
590
+ const structSize = 12;
591
+ const fileAccessPtr = this.malloc(structSize);
592
+ // Set up struct fields
593
+ this.pdfiumModule.pdfium.setValue(fileAccessPtr, fileLength, 'i32');
594
+ this.pdfiumModule.pdfium.setValue(fileAccessPtr + 4, callbackPtr, 'i32');
595
+ this.pdfiumModule.pdfium.setValue(fileAccessPtr + 8, 0, 'i32');
596
+ // Load document
597
+ const docPtr = this.pdfiumModule.FPDF_LoadCustomDocument(fileAccessPtr, password);
598
+ if (!docPtr) {
599
+ const lastError = this.pdfiumModule.FPDF_GetLastError();
600
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, `FPDF_LoadCustomDocument failed with ${lastError}`);
601
+ this.free(fileAccessPtr);
602
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `OpenDocumentFromLoader`, 'End', file.id);
603
+ return models.PdfTaskHelper.reject({
604
+ code: lastError,
605
+ message: `FPDF_LoadCustomDocument failed`,
606
+ });
607
+ }
608
+ const pageCount = this.pdfiumModule.FPDF_GetPageCount(docPtr);
609
+ const pages = [];
610
+ const sizePtr = this.malloc(8);
611
+ for (let index = 0; index < pageCount; index++) {
612
+ const result = this.pdfiumModule.FPDF_GetPageSizeByIndexF(docPtr, index, sizePtr);
613
+ if (!result) {
614
+ const lastError = this.pdfiumModule.FPDF_GetLastError();
615
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, `FPDF_GetPageSizeByIndexF failed with ${lastError}`);
616
+ this.free(sizePtr);
617
+ this.pdfiumModule.FPDF_CloseDocument(docPtr);
618
+ this.free(fileAccessPtr);
619
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `OpenDocumentFromLoader`, 'End', file.id);
620
+ return models.PdfTaskHelper.reject({
621
+ code: lastError,
622
+ message: `FPDF_GetPageSizeByIndexF failed`,
623
+ });
624
+ }
625
+ const page = {
626
+ index,
627
+ size: {
628
+ width: this.pdfiumModule.pdfium.getValue(sizePtr, 'float'),
629
+ height: this.pdfiumModule.pdfium.getValue(sizePtr + 4, 'float'),
630
+ },
631
+ };
632
+ pages.push(page);
633
+ }
634
+ this.free(sizePtr);
635
+ const pdfDoc = {
636
+ id: file.id,
637
+ pageCount,
638
+ pages,
639
+ };
640
+ this.cache.setDocument(file.id, fileAccessPtr, docPtr);
641
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `OpenDocumentFromLoader`, 'End', file.id);
642
+ return models.PdfTaskHelper.resolve(pdfDoc);
643
+ }
644
+ /**
645
+ * {@inheritDoc @embedpdf/models!PdfEngine.getMetadata}
646
+ *
647
+ * @public
648
+ */
649
+ getMetadata(doc) {
650
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'getMetadata', doc);
651
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetMetadata`, 'Begin', doc.id);
652
+ const ctx = this.cache.getContext(doc.id);
653
+ if (!ctx) {
654
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetMetadata`, 'End', doc.id);
655
+ return models.PdfTaskHelper.reject({
656
+ code: models.PdfErrorCode.DocNotOpen,
657
+ message: 'document does not open',
658
+ });
659
+ }
660
+ const metadata = {
661
+ title: this.readMetaText(ctx.docPtr, 'Title'),
662
+ author: this.readMetaText(ctx.docPtr, 'Author'),
663
+ subject: this.readMetaText(ctx.docPtr, 'Subject'),
664
+ keywords: this.readMetaText(ctx.docPtr, 'Keywords'),
665
+ producer: this.readMetaText(ctx.docPtr, 'Producer'),
666
+ creator: this.readMetaText(ctx.docPtr, 'Creator'),
667
+ creationDate: this.readMetaText(ctx.docPtr, 'CreationDate'),
668
+ modificationDate: this.readMetaText(ctx.docPtr, 'ModDate'),
669
+ };
670
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetMetadata`, 'End', doc.id);
671
+ return models.PdfTaskHelper.resolve(metadata);
672
+ }
673
+ /**
674
+ * {@inheritDoc @embedpdf/models!PdfEngine.getDocPermissions}
675
+ *
676
+ * @public
677
+ */
678
+ getDocPermissions(doc) {
679
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'getDocPermissions', doc);
680
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `getDocPermissions`, 'Begin', doc.id);
681
+ const ctx = this.cache.getContext(doc.id);
682
+ if (!ctx) {
683
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `getDocPermissions`, 'End', doc.id);
684
+ return models.PdfTaskHelper.reject({
685
+ code: models.PdfErrorCode.DocNotOpen,
686
+ message: 'document does not open',
687
+ });
688
+ }
689
+ const permissions = this.pdfiumModule.FPDF_GetDocPermissions(ctx.docPtr);
690
+ return models.PdfTaskHelper.resolve(permissions);
691
+ }
692
+ /**
693
+ * {@inheritDoc @embedpdf/models!PdfEngine.getDocUserPermissions}
694
+ *
695
+ * @public
696
+ */
697
+ getDocUserPermissions(doc) {
698
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'getDocUserPermissions', doc);
699
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `getDocUserPermissions`, 'Begin', doc.id);
700
+ const ctx = this.cache.getContext(doc.id);
701
+ if (!ctx) {
702
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `getDocUserPermissions`, 'End', doc.id);
703
+ return models.PdfTaskHelper.reject({
704
+ code: models.PdfErrorCode.DocNotOpen,
705
+ message: 'document does not open',
706
+ });
707
+ }
708
+ const permissions = this.pdfiumModule.FPDF_GetDocUserPermissions(ctx.docPtr);
709
+ return models.PdfTaskHelper.resolve(permissions);
710
+ }
711
+ /**
712
+ * {@inheritDoc @embedpdf/models!PdfEngine.getSignatures}
713
+ *
714
+ * @public
715
+ */
716
+ getSignatures(doc) {
717
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'getSignatures', doc);
718
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetSignatures`, 'Begin', doc.id);
719
+ const ctx = this.cache.getContext(doc.id);
720
+ if (!ctx) {
721
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetSignatures`, 'End', doc.id);
722
+ return models.PdfTaskHelper.reject({
723
+ code: models.PdfErrorCode.DocNotOpen,
724
+ message: 'document does not open',
725
+ });
726
+ }
727
+ const signatures = [];
728
+ const count = this.pdfiumModule.FPDF_GetSignatureCount(ctx.docPtr);
729
+ for (let i = 0; i < count; i++) {
730
+ const signatureObjPtr = this.pdfiumModule.FPDF_GetSignatureObject(ctx.docPtr, i);
731
+ const contents = readArrayBuffer(this.pdfiumModule.pdfium, (buffer, bufferSize) => {
732
+ return this.pdfiumModule.FPDFSignatureObj_GetContents(signatureObjPtr, buffer, bufferSize);
733
+ });
734
+ const byteRange = readArrayBuffer(this.pdfiumModule.pdfium, (buffer, bufferSize) => {
735
+ return (this.pdfiumModule.FPDFSignatureObj_GetByteRange(signatureObjPtr, buffer, bufferSize) * 4);
736
+ });
737
+ const subFilter = readArrayBuffer(this.pdfiumModule.pdfium, (buffer, bufferSize) => {
738
+ return this.pdfiumModule.FPDFSignatureObj_GetSubFilter(signatureObjPtr, buffer, bufferSize);
739
+ });
740
+ const reason = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
741
+ return this.pdfiumModule.FPDFSignatureObj_GetReason(signatureObjPtr, buffer, bufferLength);
742
+ }, this.pdfiumModule.pdfium.UTF16ToString);
743
+ const time = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
744
+ return this.pdfiumModule.FPDFSignatureObj_GetTime(signatureObjPtr, buffer, bufferLength);
745
+ }, this.pdfiumModule.pdfium.UTF8ToString);
746
+ const docMDP = this.pdfiumModule.FPDFSignatureObj_GetDocMDPPermission(signatureObjPtr);
747
+ signatures.push({
748
+ contents,
749
+ byteRange,
750
+ subFilter,
751
+ reason,
752
+ time,
753
+ docMDP,
754
+ });
755
+ }
756
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetSignatures`, 'End', doc.id);
757
+ return models.PdfTaskHelper.resolve(signatures);
758
+ }
759
+ /**
760
+ * {@inheritDoc @embedpdf/models!PdfEngine.getBookmarks}
761
+ *
762
+ * @public
763
+ */
764
+ getBookmarks(doc) {
765
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'getBookmarks', doc);
766
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetBookmarks`, 'Begin', doc.id);
767
+ const ctx = this.cache.getContext(doc.id);
768
+ if (!ctx) {
769
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `getBookmarks`, 'End', doc.id);
770
+ return models.PdfTaskHelper.reject({
771
+ code: models.PdfErrorCode.DocNotOpen,
772
+ message: 'document does not open',
773
+ });
774
+ }
775
+ const bookmarks = this.readPdfBookmarks(ctx.docPtr, 0);
776
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetBookmarks`, 'End', doc.id);
777
+ return models.PdfTaskHelper.resolve({
778
+ bookmarks,
779
+ });
780
+ }
781
+ /**
782
+ * {@inheritDoc @embedpdf/models!PdfEngine.renderPage}
783
+ *
784
+ * @public
785
+ */
786
+ renderPage(doc, page, scaleFactor = 1, rotation = models.Rotation.Degree0, dpr = 1, options = { withAnnotations: false }) {
787
+ const task = new models.Task();
788
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'renderPage', doc, page, scaleFactor, rotation, dpr, options);
789
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RenderPage`, 'Begin', `${doc.id}-${page.index}`);
790
+ const ctx = this.cache.getContext(doc.id);
791
+ if (!ctx) {
792
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RenderPage`, 'End', `${doc.id}-${page.index}`);
793
+ return models.PdfTaskHelper.reject({
794
+ code: models.PdfErrorCode.DocNotOpen,
795
+ message: 'document does not open',
796
+ });
797
+ }
798
+ const imageData = this.renderPageRectToImageData(ctx, page, {
799
+ origin: { x: 0, y: 0 },
800
+ size: page.size,
801
+ }, scaleFactor, rotation, dpr, options);
802
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RenderPage`, 'End', `${doc.id}-${page.index}`);
803
+ this.imageDataConverter(imageData).then((blob) => task.resolve(blob));
804
+ return task;
805
+ }
806
+ /**
807
+ * {@inheritDoc @embedpdf/models!PdfEngine.renderPageRect}
808
+ *
809
+ * @public
810
+ */
811
+ renderPageRect(doc, page, scaleFactor, rotation, dpr, rect, options) {
812
+ const task = new models.Task();
813
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'renderPageRect', doc, page, scaleFactor, rotation, dpr, rect, options);
814
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RenderPageRect`, 'Begin', `${doc.id}-${page.index}`);
815
+ const ctx = this.cache.getContext(doc.id);
816
+ if (!ctx) {
817
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RenderPageRect`, 'End', `${doc.id}-${page.index}`);
818
+ return models.PdfTaskHelper.reject({
819
+ code: models.PdfErrorCode.DocNotOpen,
820
+ message: 'document does not open',
821
+ });
822
+ }
823
+ const imageData = this.renderPageRectToImageData(ctx, page, rect, scaleFactor, rotation, dpr, options);
824
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RenderPageRect`, 'End', `${doc.id}-${page.index}`);
825
+ this.imageDataConverter(imageData).then((blob) => task.resolve(blob));
826
+ return task;
827
+ }
828
+ /**
829
+ * {@inheritDoc @embedpdf/models!PdfEngine.getAllAnnotations}
830
+ *
831
+ * @public
832
+ */
833
+ getAllAnnotations(doc) {
834
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'getAllAnnotations', doc);
835
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetAllAnnotations`, 'Begin', doc.id);
836
+ const ctx = this.cache.getContext(doc.id);
837
+ if (!ctx) {
838
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetAllAnnotations`, 'End', doc.id);
839
+ return models.PdfTaskHelper.reject({
840
+ code: models.PdfErrorCode.DocNotOpen,
841
+ message: 'document does not open',
842
+ });
843
+ }
844
+ const annotations = this.readAllAnnotations(doc, ctx);
845
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetAllAnnotations`, 'End', doc.id);
846
+ return models.PdfTaskHelper.resolve(annotations);
847
+ }
848
+ readAllAnnotations(doc, ctx) {
849
+ const annotationsByPage = {};
850
+ for (let i = 0; i < doc.pageCount; i++) {
851
+ const pageAnnotations = this.readPageAnnotations(ctx, doc.pages[i]);
852
+ annotationsByPage[i] = pageAnnotations;
853
+ }
854
+ return annotationsByPage;
855
+ }
856
+ /**
857
+ * {@inheritDoc @embedpdf/models!PdfEngine.getPageAnnotations}
858
+ *
859
+ * @public
860
+ */
861
+ getPageAnnotations(doc, page) {
862
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'getPageAnnotations', doc, page);
863
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetPageAnnotations`, 'Begin', `${doc.id}-${page.index}`);
864
+ const ctx = this.cache.getContext(doc.id);
865
+ if (!ctx) {
866
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetPageAnnotations`, 'End', `${doc.id}-${page.index}`);
867
+ return models.PdfTaskHelper.reject({
868
+ code: models.PdfErrorCode.DocNotOpen,
869
+ message: 'document does not open',
870
+ });
871
+ }
872
+ const annotations = this.readPageAnnotations(ctx, page);
873
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetPageAnnotations`, 'End', `${doc.id}-${page.index}`);
874
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, `GetPageAnnotations`, `${doc.id}-${page.index}`, annotations);
875
+ return models.PdfTaskHelper.resolve(annotations);
876
+ }
877
+ /**
878
+ * {@inheritDoc @embedpdf/models!PdfEngine.createPageAnnotation}
879
+ *
880
+ * @public
881
+ */
882
+ createPageAnnotation(doc, page, annotation) {
883
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'createPageAnnotation', doc, page, annotation);
884
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `CreatePageAnnotation`, 'Begin', `${doc.id}-${page.index}`);
885
+ const ctx = this.cache.getContext(doc.id);
886
+ if (!ctx) {
887
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `CreatePageAnnotation`, 'End', `${doc.id}-${page.index}`);
888
+ return models.PdfTaskHelper.reject({
889
+ code: models.PdfErrorCode.DocNotOpen,
890
+ message: 'document does not open',
891
+ });
892
+ }
893
+ const pageCtx = ctx.acquirePage(page.index);
894
+ const annotationPtr = this.pdfiumModule.FPDFPage_CreateAnnot(pageCtx.pagePtr, annotation.type);
895
+ if (!annotationPtr) {
896
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `CreatePageAnnotation`, 'End', `${doc.id}-${page.index}`);
897
+ pageCtx.release();
898
+ return models.PdfTaskHelper.reject({
899
+ code: models.PdfErrorCode.CantCreateAnnot,
900
+ message: 'can not create annotation with specified type',
901
+ });
902
+ }
903
+ if (!this.setPageAnnoRect(page, pageCtx.pagePtr, annotationPtr, annotation.rect)) {
904
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
905
+ pageCtx.release();
906
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `CreatePageAnnotation`, 'End', `${doc.id}-${page.index}`);
907
+ return models.PdfTaskHelper.reject({
908
+ code: models.PdfErrorCode.CantSetAnnotRect,
909
+ message: 'can not set the rect of the annotation',
910
+ });
911
+ }
912
+ let isSucceed = false;
913
+ switch (annotation.type) {
914
+ case models.PdfAnnotationSubtype.INK:
915
+ isSucceed = this.addInkStroke(page, pageCtx.pagePtr, annotationPtr, annotation.inkList);
916
+ break;
917
+ case models.PdfAnnotationSubtype.STAMP:
918
+ isSucceed = this.addStampContent(ctx.docPtr, page, pageCtx.pagePtr, annotationPtr, annotation.rect, annotation.contents);
919
+ break;
920
+ }
921
+ if (!isSucceed) {
922
+ this.pdfiumModule.FPDFPage_RemoveAnnot(pageCtx.pagePtr, annotationPtr);
923
+ pageCtx.release();
924
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `CreatePageAnnotation`, 'End', `${doc.id}-${page.index}`);
925
+ return models.PdfTaskHelper.reject({
926
+ code: models.PdfErrorCode.CantSetAnnotContent,
927
+ message: 'can not add content of the annotation',
928
+ });
929
+ }
930
+ this.pdfiumModule.FPDFPage_GenerateContent(pageCtx.pagePtr);
931
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
932
+ pageCtx.release();
933
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `CreatePageAnnotation`, 'End', `${doc.id}-${page.index}`);
934
+ return models.PdfTaskHelper.resolve(true);
935
+ }
936
+ /**
937
+ * {@inheritDoc @embedpdf/models!PdfEngine.transformPageAnnotation}
938
+ *
939
+ * @public
940
+ */
941
+ transformPageAnnotation(doc, page, annotation, transformation) {
942
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'transformPageAnnotation', doc, page, annotation, transformation);
943
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `TransformPageAnnotation`, 'Begin', `${doc.id}-${page.index}`);
944
+ const ctx = this.cache.getContext(doc.id);
945
+ if (!ctx) {
946
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `TransformPageAnnotation`, 'End', `${doc.id}-${page.index}`);
947
+ return models.PdfTaskHelper.reject({
948
+ code: models.PdfErrorCode.DocNotOpen,
949
+ message: 'document does not open',
950
+ });
951
+ }
952
+ const pageCtx = ctx.acquirePage(page.index);
953
+ const annotationPtr = this.pdfiumModule.FPDFPage_GetAnnot(pageCtx.pagePtr, annotation.id);
954
+ const rect = {
955
+ origin: {
956
+ x: annotation.rect.origin.x + transformation.offset.x,
957
+ y: annotation.rect.origin.y + transformation.offset.y,
958
+ },
959
+ size: {
960
+ width: annotation.rect.size.width * transformation.scale.width,
961
+ height: annotation.rect.size.height * transformation.scale.height,
962
+ },
963
+ };
964
+ if (!this.setPageAnnoRect(page, pageCtx.pagePtr, annotationPtr, rect)) {
965
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
966
+ pageCtx.release();
967
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `TransformPageAnnotation`, 'End', `${doc.id}-${page.index}`);
968
+ return models.PdfTaskHelper.reject({
969
+ code: models.PdfErrorCode.CantSetAnnotRect,
970
+ message: 'can not set the rect of the annotation',
971
+ });
972
+ }
973
+ switch (annotation.type) {
974
+ case models.PdfAnnotationSubtype.INK:
975
+ {
976
+ if (!this.pdfiumModule.FPDFAnnot_RemoveInkList(annotationPtr)) {
977
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
978
+ pageCtx.release();
979
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `TransformPageAnnotation`, 'End', `${doc.id}-${page.index}`);
980
+ return models.PdfTaskHelper.reject({
981
+ code: models.PdfErrorCode.CantRemoveInkList,
982
+ message: 'can not set the rect of the annotation',
983
+ });
984
+ }
985
+ const inkList = annotation.inkList.map((inkStroke) => {
986
+ return {
987
+ points: inkStroke.points.map((point) => {
988
+ return {
989
+ x: rect.origin.x +
990
+ (point.x - annotation.rect.origin.x) * transformation.scale.width,
991
+ y: rect.origin.y +
992
+ (point.y - annotation.rect.origin.y) * transformation.scale.height,
993
+ };
994
+ }),
995
+ };
996
+ });
997
+ if (!this.addInkStroke(page, pageCtx.pagePtr, annotationPtr, inkList)) {
998
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
999
+ pageCtx.release();
1000
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `TransformPageAnnotation`, 'End', `${doc.id}-${page.index}`);
1001
+ return models.PdfTaskHelper.reject({
1002
+ code: models.PdfErrorCode.CantAddInkStoke,
1003
+ message: 'can not add stroke to the ink list of annotation',
1004
+ });
1005
+ }
1006
+ }
1007
+ break;
1008
+ }
1009
+ this.pdfiumModule.FPDFPage_GenerateContent(pageCtx.pagePtr);
1010
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
1011
+ pageCtx.release();
1012
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `TransformPageAnnotation`, 'End', `${doc.id}-${page.index}`);
1013
+ return models.PdfTaskHelper.resolve(true);
1014
+ }
1015
+ /**
1016
+ * {@inheritDoc @embedpdf/models!PdfEngine.removePageAnnotation}
1017
+ *
1018
+ * @public
1019
+ */
1020
+ removePageAnnotation(doc, page, annotation) {
1021
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'removePageAnnotation', doc, page, annotation);
1022
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RemovePageAnnotation`, 'Begin', `${doc.id}-${page.index}`);
1023
+ const ctx = this.cache.getContext(doc.id);
1024
+ if (!ctx) {
1025
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RemovePageAnnotation`, 'End', `${doc.id}-${page.index}`);
1026
+ return models.PdfTaskHelper.reject({
1027
+ code: models.PdfErrorCode.DocNotOpen,
1028
+ message: 'document does not open',
1029
+ });
1030
+ }
1031
+ const pageCtx = ctx.acquirePage(page.index);
1032
+ let result = false;
1033
+ result = this.pdfiumModule.FPDFPage_RemoveAnnot(pageCtx.pagePtr, annotation.id);
1034
+ if (!result) {
1035
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, `FPDFPage_RemoveAnnot Failed`, `${doc.id}-${page.index}`);
1036
+ }
1037
+ else {
1038
+ result = this.pdfiumModule.FPDFPage_GenerateContent(pageCtx.pagePtr);
1039
+ if (!result) {
1040
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, `FPDFPage_GenerateContent Failed`, `${doc.id}-${page.index}`);
1041
+ }
1042
+ }
1043
+ pageCtx.release();
1044
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RemovePageAnnotation`, 'End', `${doc.id}-${page.index}`);
1045
+ return models.PdfTaskHelper.resolve(result);
1046
+ }
1047
+ /**
1048
+ * {@inheritDoc @embedpdf/models!PdfEngine.getPageTextRects}
1049
+ *
1050
+ * @public
1051
+ */
1052
+ getPageTextRects(doc, page, scaleFactor, rotation) {
1053
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'getPageTextRects', doc, page, scaleFactor, rotation);
1054
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetPageTextRects`, 'Begin', `${doc.id}-${page.index}`);
1055
+ const ctx = this.cache.getContext(doc.id);
1056
+ if (!ctx) {
1057
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetPageTextRects`, 'End', `${doc.id}-${page.index}`);
1058
+ return models.PdfTaskHelper.reject({
1059
+ code: models.PdfErrorCode.DocNotOpen,
1060
+ message: 'document does not open',
1061
+ });
1062
+ }
1063
+ const pageCtx = ctx.acquirePage(page.index);
1064
+ const textPagePtr = this.pdfiumModule.FPDFText_LoadPage(pageCtx.pagePtr);
1065
+ const textRects = this.readPageTextRects(page, pageCtx.docPtr, pageCtx.pagePtr, textPagePtr);
1066
+ this.pdfiumModule.FPDFText_ClosePage(textPagePtr);
1067
+ pageCtx.release();
1068
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetPageTextRects`, 'End', `${doc.id}-${page.index}`);
1069
+ return models.PdfTaskHelper.resolve(textRects);
1070
+ }
1071
+ /**
1072
+ * {@inheritDoc @embedpdf/models!PdfEngine.renderThumbnail}
1073
+ *
1074
+ * @public
1075
+ */
1076
+ renderThumbnail(doc, page, scaleFactor, rotation, dpr) {
1077
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'renderThumbnail', doc, page, scaleFactor, rotation, dpr);
1078
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RenderThumbnail`, 'Begin', `${doc.id}-${page.index}`);
1079
+ const ctx = this.cache.getContext(doc.id);
1080
+ if (!ctx) {
1081
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RenderThumbnail`, 'End', `${doc.id}-${page.index}`);
1082
+ return models.PdfTaskHelper.reject({
1083
+ code: models.PdfErrorCode.DocNotOpen,
1084
+ message: 'document does not open',
1085
+ });
1086
+ }
1087
+ scaleFactor = Math.max(scaleFactor, 0.5);
1088
+ const result = this.renderPage(doc, page, scaleFactor, rotation, dpr, {
1089
+ withAnnotations: true,
1090
+ });
1091
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `RenderThumbnail`, 'End', `${doc.id}-${page.index}`);
1092
+ return result;
1093
+ }
1094
+ /**
1095
+ * {@inheritDoc @embedpdf/models!PdfEngine.getAttachments}
1096
+ *
1097
+ * @public
1098
+ */
1099
+ getAttachments(doc) {
1100
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'getAttachments', doc);
1101
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetAttachments`, 'Begin', doc.id);
1102
+ const ctx = this.cache.getContext(doc.id);
1103
+ if (!ctx) {
1104
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetAttachments`, 'End', doc.id);
1105
+ return models.PdfTaskHelper.reject({
1106
+ code: models.PdfErrorCode.DocNotOpen,
1107
+ message: 'document does not open',
1108
+ });
1109
+ }
1110
+ const attachments = [];
1111
+ const count = this.pdfiumModule.FPDFDoc_GetAttachmentCount(ctx.docPtr);
1112
+ for (let i = 0; i < count; i++) {
1113
+ const attachment = this.readPdfAttachment(ctx.docPtr, i);
1114
+ attachments.push(attachment);
1115
+ }
1116
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `GetAttachments`, 'End', doc.id);
1117
+ return models.PdfTaskHelper.resolve(attachments);
1118
+ }
1119
+ /**
1120
+ * {@inheritDoc @embedpdf/models!PdfEngine.readAttachmentContent}
1121
+ *
1122
+ * @public
1123
+ */
1124
+ readAttachmentContent(doc, attachment) {
1125
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'readAttachmentContent', doc, attachment);
1126
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ReadAttachmentContent`, 'Begin', doc.id);
1127
+ const ctx = this.cache.getContext(doc.id);
1128
+ if (!ctx) {
1129
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ReadAttachmentContent`, 'End', doc.id);
1130
+ return models.PdfTaskHelper.reject({
1131
+ code: models.PdfErrorCode.DocNotOpen,
1132
+ message: 'document does not open',
1133
+ });
1134
+ }
1135
+ const attachmentPtr = this.pdfiumModule.FPDFDoc_GetAttachment(ctx.docPtr, attachment.index);
1136
+ const sizePtr = this.malloc(8);
1137
+ if (!this.pdfiumModule.FPDFAttachment_GetFile(attachmentPtr, 0, 0, sizePtr)) {
1138
+ this.free(sizePtr);
1139
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ReadAttachmentContent`, 'End', doc.id);
1140
+ return models.PdfTaskHelper.reject({
1141
+ code: models.PdfErrorCode.CantReadAttachmentSize,
1142
+ message: 'can not read attachment size',
1143
+ });
1144
+ }
1145
+ const size = this.pdfiumModule.pdfium.getValue(sizePtr, 'i64');
1146
+ const contentPtr = this.malloc(size);
1147
+ if (!this.pdfiumModule.FPDFAttachment_GetFile(attachmentPtr, contentPtr, size, sizePtr)) {
1148
+ this.free(sizePtr);
1149
+ this.free(contentPtr);
1150
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ReadAttachmentContent`, 'End', doc.id);
1151
+ return models.PdfTaskHelper.reject({
1152
+ code: models.PdfErrorCode.CantReadAttachmentContent,
1153
+ message: 'can not read attachment content',
1154
+ });
1155
+ }
1156
+ const buffer = new ArrayBuffer(size);
1157
+ const view = new DataView(buffer);
1158
+ for (let i = 0; i < size; i++) {
1159
+ view.setInt8(i, this.pdfiumModule.pdfium.getValue(contentPtr + i, 'i8'));
1160
+ }
1161
+ this.free(sizePtr);
1162
+ this.free(contentPtr);
1163
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ReadAttachmentContent`, 'End', doc.id);
1164
+ return models.PdfTaskHelper.resolve(buffer);
1165
+ }
1166
+ /**
1167
+ * {@inheritDoc @embedpdf/models!PdfEngine.setFormFieldValue}
1168
+ *
1169
+ * @public
1170
+ */
1171
+ setFormFieldValue(doc, page, annotation, value) {
1172
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'SetFormFieldValue', doc, annotation, value);
1173
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SetFormFieldValue`, 'Begin', `${doc.id}-${annotation.id}`);
1174
+ const ctx = this.cache.getContext(doc.id);
1175
+ if (!ctx) {
1176
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'SetFormFieldValue', 'document is not opened');
1177
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SetFormFieldValue`, 'End', `${doc.id}-${annotation.id}`);
1178
+ return models.PdfTaskHelper.reject({
1179
+ code: models.PdfErrorCode.DocNotOpen,
1180
+ message: 'document does not open',
1181
+ });
1182
+ }
1183
+ const formFillInfoPtr = this.pdfiumModule.PDFiumExt_OpenFormFillInfo();
1184
+ const formHandle = this.pdfiumModule.PDFiumExt_InitFormFillEnvironment(ctx.docPtr, formFillInfoPtr);
1185
+ const pageCtx = ctx.acquirePage(page.index);
1186
+ this.pdfiumModule.FORM_OnAfterLoadPage(pageCtx.pagePtr, formHandle);
1187
+ const annotationPtr = this.pdfiumModule.FPDFPage_GetAnnot(pageCtx.pagePtr, annotation.id);
1188
+ if (!this.pdfiumModule.FORM_SetFocusedAnnot(formHandle, annotationPtr)) {
1189
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'SetFormFieldValue', 'failed to set focused annotation');
1190
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SetFormFieldValue`, 'End', `${doc.id}-${annotation.id}`);
1191
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
1192
+ this.pdfiumModule.FORM_OnBeforeClosePage(pageCtx.pagePtr, formHandle);
1193
+ pageCtx.release();
1194
+ this.pdfiumModule.PDFiumExt_ExitFormFillEnvironment(formHandle);
1195
+ this.pdfiumModule.PDFiumExt_CloseFormFillInfo(formFillInfoPtr);
1196
+ return models.PdfTaskHelper.reject({
1197
+ code: models.PdfErrorCode.CantFocusAnnot,
1198
+ message: 'failed to set focused annotation',
1199
+ });
1200
+ }
1201
+ switch (value.kind) {
1202
+ case 'text':
1203
+ {
1204
+ if (!this.pdfiumModule.FORM_SelectAllText(formHandle, pageCtx.pagePtr)) {
1205
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'SetFormFieldValue', 'failed to select all text');
1206
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SetFormFieldValue`, 'End', `${doc.id}-${annotation.id}`);
1207
+ this.pdfiumModule.FORM_ForceToKillFocus(formHandle);
1208
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
1209
+ this.pdfiumModule.FORM_OnBeforeClosePage(pageCtx.pagePtr, formHandle);
1210
+ pageCtx.release();
1211
+ this.pdfiumModule.PDFiumExt_ExitFormFillEnvironment(formHandle);
1212
+ this.pdfiumModule.PDFiumExt_CloseFormFillInfo(formFillInfoPtr);
1213
+ return models.PdfTaskHelper.reject({
1214
+ code: models.PdfErrorCode.CantSelectText,
1215
+ message: 'failed to select all text',
1216
+ });
1217
+ }
1218
+ const length = 2 * (value.text.length + 1);
1219
+ const textPtr = this.malloc(length);
1220
+ this.pdfiumModule.pdfium.stringToUTF16(value.text, textPtr, length);
1221
+ this.pdfiumModule.FORM_ReplaceSelection(formHandle, pageCtx.pagePtr, textPtr);
1222
+ this.free(textPtr);
1223
+ }
1224
+ break;
1225
+ case 'selection':
1226
+ {
1227
+ if (!this.pdfiumModule.FORM_SetIndexSelected(formHandle, pageCtx.pagePtr, value.index, value.isSelected)) {
1228
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'SetFormFieldValue', 'failed to set index selected');
1229
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SetFormFieldValue`, 'End', `${doc.id}-${annotation.id}`);
1230
+ this.pdfiumModule.FORM_ForceToKillFocus(formHandle);
1231
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
1232
+ this.pdfiumModule.FORM_OnBeforeClosePage(pageCtx.pagePtr, formHandle);
1233
+ pageCtx.release();
1234
+ this.pdfiumModule.PDFiumExt_ExitFormFillEnvironment(formHandle);
1235
+ this.pdfiumModule.PDFiumExt_CloseFormFillInfo(formFillInfoPtr);
1236
+ return models.PdfTaskHelper.reject({
1237
+ code: models.PdfErrorCode.CantSelectOption,
1238
+ message: 'failed to set index selected',
1239
+ });
1240
+ }
1241
+ }
1242
+ break;
1243
+ case 'checked':
1244
+ {
1245
+ const kReturn = 0x0d;
1246
+ if (!this.pdfiumModule.FORM_OnChar(formHandle, pageCtx.pagePtr, kReturn, 0)) {
1247
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'SetFormFieldValue', 'failed to set field checked');
1248
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SetFormFieldValue`, 'End', `${doc.id}-${annotation.id}`);
1249
+ this.pdfiumModule.FORM_ForceToKillFocus(formHandle);
1250
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
1251
+ this.pdfiumModule.FORM_OnBeforeClosePage(pageCtx.pagePtr, formHandle);
1252
+ pageCtx.release();
1253
+ this.pdfiumModule.PDFiumExt_ExitFormFillEnvironment(formHandle);
1254
+ this.pdfiumModule.PDFiumExt_CloseFormFillInfo(formFillInfoPtr);
1255
+ return models.PdfTaskHelper.reject({
1256
+ code: models.PdfErrorCode.CantCheckField,
1257
+ message: 'failed to set field checked',
1258
+ });
1259
+ }
1260
+ }
1261
+ break;
1262
+ }
1263
+ this.pdfiumModule.FORM_ForceToKillFocus(formHandle);
1264
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
1265
+ this.pdfiumModule.FORM_OnBeforeClosePage(pageCtx.pagePtr, formHandle);
1266
+ pageCtx.release();
1267
+ this.pdfiumModule.PDFiumExt_ExitFormFillEnvironment(formHandle);
1268
+ this.pdfiumModule.PDFiumExt_CloseFormFillInfo(formFillInfoPtr);
1269
+ return models.PdfTaskHelper.resolve(true);
1270
+ }
1271
+ /**
1272
+ * {@inheritDoc @embedpdf/models!PdfEngine.flattenPage}
1273
+ *
1274
+ * @public
1275
+ */
1276
+ flattenPage(doc, page, flag) {
1277
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'flattenPage', doc, page, flag);
1278
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `flattenPage`, 'Begin', doc.id);
1279
+ const ctx = this.cache.getContext(doc.id);
1280
+ if (!ctx) {
1281
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `flattenPage`, 'End', doc.id);
1282
+ return models.PdfTaskHelper.reject({
1283
+ code: models.PdfErrorCode.DocNotOpen,
1284
+ message: 'document does not open',
1285
+ });
1286
+ }
1287
+ const pageCtx = ctx.acquirePage(page.index);
1288
+ const result = this.pdfiumModule.FPDFPage_Flatten(pageCtx.pagePtr, flag);
1289
+ pageCtx.release();
1290
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `flattenPage`, 'End', doc.id);
1291
+ return models.PdfTaskHelper.resolve(result);
1292
+ }
1293
+ /**
1294
+ * {@inheritDoc @embedpdf/models!PdfEngine.extractPages}
1295
+ *
1296
+ * @public
1297
+ */
1298
+ extractPages(doc, pageIndexes) {
1299
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'extractPages', doc, pageIndexes);
1300
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ExtractPages`, 'Begin', doc.id);
1301
+ const ctx = this.cache.getContext(doc.id);
1302
+ if (!ctx) {
1303
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ExtractPages`, 'End', doc.id);
1304
+ return models.PdfTaskHelper.reject({
1305
+ code: models.PdfErrorCode.DocNotOpen,
1306
+ message: 'document does not open',
1307
+ });
1308
+ }
1309
+ const newDocPtr = this.pdfiumModule.FPDF_CreateNewDocument();
1310
+ if (!newDocPtr) {
1311
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ExtractPages`, 'End', doc.id);
1312
+ return models.PdfTaskHelper.reject({
1313
+ code: models.PdfErrorCode.CantCreateNewDoc,
1314
+ message: 'can not create new document',
1315
+ });
1316
+ }
1317
+ const pageIndexesPtr = this.malloc(pageIndexes.length * 4);
1318
+ for (let i = 0; i < pageIndexes.length; i++) {
1319
+ this.pdfiumModule.pdfium.setValue(pageIndexesPtr + i * 4, pageIndexes[i], 'i32');
1320
+ }
1321
+ if (!this.pdfiumModule.FPDF_ImportPagesByIndex(newDocPtr, ctx.docPtr, pageIndexesPtr, pageIndexes.length, 0)) {
1322
+ this.pdfiumModule.FPDF_CloseDocument(newDocPtr);
1323
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ExtractPages`, 'End', doc.id);
1324
+ return models.PdfTaskHelper.reject({
1325
+ code: models.PdfErrorCode.CantImportPages,
1326
+ message: 'can not import pages to new document',
1327
+ });
1328
+ }
1329
+ const buffer = this.saveDocument(newDocPtr);
1330
+ this.pdfiumModule.FPDF_CloseDocument(newDocPtr);
1331
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ExtractPages`, 'End', doc.id);
1332
+ return models.PdfTaskHelper.resolve(buffer);
1333
+ }
1334
+ /**
1335
+ * {@inheritDoc @embedpdf/models!PdfEngine.extractText}
1336
+ *
1337
+ * @public
1338
+ */
1339
+ extractText(doc, pageIndexes) {
1340
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'extractText', doc, pageIndexes);
1341
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ExtractText`, 'Begin', doc.id);
1342
+ const ctx = this.cache.getContext(doc.id);
1343
+ if (!ctx) {
1344
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ExtractText`, 'End', doc.id);
1345
+ return models.PdfTaskHelper.reject({
1346
+ code: models.PdfErrorCode.DocNotOpen,
1347
+ message: 'document does not open',
1348
+ });
1349
+ }
1350
+ const strings = [];
1351
+ for (let i = 0; i < pageIndexes.length; i++) {
1352
+ const pageCtx = ctx.acquirePage(pageIndexes[i]);
1353
+ const textPagePtr = this.pdfiumModule.FPDFText_LoadPage(pageCtx.pagePtr);
1354
+ const charCount = this.pdfiumModule.FPDFText_CountChars(textPagePtr);
1355
+ const bufferPtr = this.malloc((charCount + 1) * 2);
1356
+ this.pdfiumModule.FPDFText_GetText(textPagePtr, 0, charCount, bufferPtr);
1357
+ const text = this.pdfiumModule.pdfium.UTF16ToString(bufferPtr);
1358
+ this.free(bufferPtr);
1359
+ strings.push(text);
1360
+ this.pdfiumModule.FPDFText_ClosePage(textPagePtr);
1361
+ pageCtx.release();
1362
+ }
1363
+ const text = strings.join('\n\n');
1364
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `ExtractText`, 'End', doc.id);
1365
+ return models.PdfTaskHelper.resolve(text);
1366
+ }
1367
+ /**
1368
+ * {@inheritDoc @embedpdf/models!PdfEngine.merge}
1369
+ *
1370
+ * @public
1371
+ */
1372
+ merge(files) {
1373
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'merge', files);
1374
+ const fileIds = files.map((file) => file.id).join('.');
1375
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `Merge`, 'Begin', fileIds);
1376
+ const newDocPtr = this.pdfiumModule.FPDF_CreateNewDocument();
1377
+ if (!newDocPtr) {
1378
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `Merge`, 'End', fileIds);
1379
+ return models.PdfTaskHelper.reject({
1380
+ code: models.PdfErrorCode.CantCreateNewDoc,
1381
+ message: 'can not create new document',
1382
+ });
1383
+ }
1384
+ const ptrs = [];
1385
+ for (const file of files.reverse()) {
1386
+ const array = new Uint8Array(file.content);
1387
+ const length = array.length;
1388
+ const filePtr = this.malloc(length);
1389
+ this.pdfiumModule.pdfium.HEAPU8.set(array, filePtr);
1390
+ const docPtr = this.pdfiumModule.FPDF_LoadMemDocument(filePtr, length, '');
1391
+ if (!docPtr) {
1392
+ const lastError = this.pdfiumModule.FPDF_GetLastError();
1393
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, `FPDF_LoadMemDocument failed with ${lastError}`);
1394
+ this.free(filePtr);
1395
+ for (const ptr of ptrs) {
1396
+ this.pdfiumModule.FPDF_CloseDocument(ptr.docPtr);
1397
+ this.free(ptr.filePtr);
1398
+ }
1399
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `Merge`, 'End', fileIds);
1400
+ return models.PdfTaskHelper.reject({
1401
+ code: lastError,
1402
+ message: `FPDF_LoadMemDocument failed`,
1403
+ });
1404
+ }
1405
+ ptrs.push({ filePtr, docPtr });
1406
+ if (!this.pdfiumModule.FPDF_ImportPages(newDocPtr, docPtr, '', 0)) {
1407
+ this.pdfiumModule.FPDF_CloseDocument(newDocPtr);
1408
+ for (const ptr of ptrs) {
1409
+ this.pdfiumModule.FPDF_CloseDocument(ptr.docPtr);
1410
+ this.free(ptr.filePtr);
1411
+ }
1412
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `Merge`, 'End', fileIds);
1413
+ return models.PdfTaskHelper.reject({
1414
+ code: models.PdfErrorCode.CantImportPages,
1415
+ message: 'can not import pages to new document',
1416
+ });
1417
+ }
1418
+ }
1419
+ const buffer = this.saveDocument(newDocPtr);
1420
+ this.pdfiumModule.FPDF_CloseDocument(newDocPtr);
1421
+ for (const ptr of ptrs) {
1422
+ this.pdfiumModule.FPDF_CloseDocument(ptr.docPtr);
1423
+ this.free(ptr.filePtr);
1424
+ }
1425
+ const file = {
1426
+ id: `${Math.random()}`,
1427
+ content: buffer,
1428
+ };
1429
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `Merge`, 'End', fileIds);
1430
+ return models.PdfTaskHelper.resolve(file);
1431
+ }
1432
+ /**
1433
+ * Merges specific pages from multiple PDF documents in a custom order
1434
+ *
1435
+ * @param mergeConfigs Array of configurations specifying which pages to merge from which documents
1436
+ * @returns A PdfTask that resolves with the merged PDF file
1437
+ * @public
1438
+ */
1439
+ mergePages(mergeConfigs) {
1440
+ const configIds = mergeConfigs
1441
+ .map((config) => `${config.docId}:${config.pageIndices.join(',')}`)
1442
+ .join('|');
1443
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'mergePages', mergeConfigs);
1444
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `MergePages`, 'Begin', configIds);
1445
+ // Create a new document to import pages into
1446
+ const newDocPtr = this.pdfiumModule.FPDF_CreateNewDocument();
1447
+ if (!newDocPtr) {
1448
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `MergePages`, 'End', configIds);
1449
+ return models.PdfTaskHelper.reject({
1450
+ code: models.PdfErrorCode.CantCreateNewDoc,
1451
+ message: 'Cannot create new document',
1452
+ });
1453
+ }
1454
+ try {
1455
+ // Process each merge configuration in reverse order (since we're inserting at position 0)
1456
+ // This ensures the final document has pages in the order specified by the user
1457
+ for (const config of [...mergeConfigs].reverse()) {
1458
+ // Check if the document is open
1459
+ const ctx = this.cache.getContext(config.docId);
1460
+ if (!ctx) {
1461
+ this.logger.warn(LOG_SOURCE, LOG_CATEGORY, `Document ${config.docId} is not open, skipping`);
1462
+ continue;
1463
+ }
1464
+ // Get the page count for this document
1465
+ const pageCount = this.pdfiumModule.FPDF_GetPageCount(ctx.docPtr);
1466
+ // Filter out invalid page indices
1467
+ const validPageIndices = config.pageIndices.filter((index) => index >= 0 && index < pageCount);
1468
+ if (validPageIndices.length === 0) {
1469
+ continue; // No valid pages to import
1470
+ }
1471
+ // Convert 0-based indices to 1-based for PDFium and join with commas
1472
+ const pageString = validPageIndices.map((index) => index + 1).join(',');
1473
+ try {
1474
+ // Import all specified pages at once from this document
1475
+ if (!this.pdfiumModule.FPDF_ImportPages(newDocPtr, ctx.docPtr, pageString, 0)) {
1476
+ throw new Error(`Failed to import pages ${pageString} from document ${config.docId}`);
1477
+ }
1478
+ }
1479
+ finally {
1480
+ }
1481
+ }
1482
+ // Save the new document to buffer
1483
+ const buffer = this.saveDocument(newDocPtr);
1484
+ const file = {
1485
+ id: `${Math.random()}`,
1486
+ content: buffer,
1487
+ };
1488
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `MergePages`, 'End', configIds);
1489
+ return models.PdfTaskHelper.resolve(file);
1490
+ }
1491
+ catch (error) {
1492
+ this.logger.error(LOG_SOURCE, LOG_CATEGORY, 'mergePages failed', error);
1493
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `MergePages`, 'End', configIds);
1494
+ return models.PdfTaskHelper.reject({
1495
+ code: models.PdfErrorCode.CantImportPages,
1496
+ message: error instanceof Error ? error.message : 'Failed to merge pages',
1497
+ });
1498
+ }
1499
+ finally {
1500
+ // Clean up the new document
1501
+ if (newDocPtr) {
1502
+ this.pdfiumModule.FPDF_CloseDocument(newDocPtr);
1503
+ }
1504
+ }
1505
+ }
1506
+ /**
1507
+ * {@inheritDoc @embedpdf/models!PdfEngine.saveAsCopy}
1508
+ *
1509
+ * @public
1510
+ */
1511
+ saveAsCopy(doc) {
1512
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'saveAsCopy', doc);
1513
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SaveAsCopy`, 'Begin', doc.id);
1514
+ const ctx = this.cache.getContext(doc.id);
1515
+ if (!ctx) {
1516
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SaveAsCopy`, 'End', doc.id);
1517
+ return models.PdfTaskHelper.reject({
1518
+ code: models.PdfErrorCode.DocNotOpen,
1519
+ message: 'document does not open',
1520
+ });
1521
+ }
1522
+ const buffer = this.saveDocument(ctx.docPtr);
1523
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SaveAsCopy`, 'End', doc.id);
1524
+ return models.PdfTaskHelper.resolve(buffer);
1525
+ }
1526
+ /**
1527
+ * {@inheritDoc @embedpdf/models!PdfEngine.closeDocument}
1528
+ *
1529
+ * @public
1530
+ */
1531
+ closeDocument(doc) {
1532
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'closeDocument', doc);
1533
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `CloseDocument`, 'Begin', doc.id);
1534
+ const ctx = this.cache.getContext(doc.id);
1535
+ if (!ctx) {
1536
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `CloseDocument`, 'End', doc.id);
1537
+ return models.PdfTaskHelper.reject({
1538
+ code: models.PdfErrorCode.DocNotOpen,
1539
+ message: 'document does not open',
1540
+ });
1541
+ }
1542
+ ctx.dispose();
1543
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `CloseDocument`, 'End', doc.id);
1544
+ return models.PdfTaskHelper.resolve(true);
1545
+ }
1546
+ /**
1547
+ * Memory allocation
1548
+ * @param size - size of memory space
1549
+ * @returns pointer to memory space
1550
+ *
1551
+ * @public
1552
+ */
1553
+ malloc(size) {
1554
+ const ptr = this.pdfiumModule.pdfium.wasmExports.malloc(size);
1555
+ for (let i = 0; i < size; i++) {
1556
+ this.pdfiumModule.pdfium.HEAP8[ptr + i] = 0;
1557
+ }
1558
+ return ptr;
1559
+ }
1560
+ /**
1561
+ * Free memory space
1562
+ * @param ptr pointer to memory space
1563
+ *
1564
+ * @public
1565
+ */
1566
+ free(ptr) {
1567
+ this.pdfiumModule.pdfium.wasmExports.free(ptr);
1568
+ }
1569
+ /**
1570
+ * Set the rect of specified annotation
1571
+ * @param page - page info that the annotation is belonged to
1572
+ * @param pagePtr - pointer of page object
1573
+ * @param annotationPtr - pointer to annotation object
1574
+ * @param inkList - ink lists that added to the annotation
1575
+ * @returns whether the ink lists is setted
1576
+ *
1577
+ * @private
1578
+ */
1579
+ addInkStroke(page, pagePtr, annotationPtr, inkList) {
1580
+ for (const inkStroke of inkList) {
1581
+ const inkPointsCount = inkStroke.points.length;
1582
+ const inkPointsPtr = this.malloc(inkPointsCount * 8);
1583
+ for (let i = 0; i < inkPointsCount; i++) {
1584
+ const point = inkStroke.points[i];
1585
+ const { x, y } = this.convertDevicePointToPagePoint(page, point);
1586
+ this.pdfiumModule.pdfium.setValue(inkPointsPtr + i * 8, x, 'float');
1587
+ this.pdfiumModule.pdfium.setValue(inkPointsPtr + i * 8 + 4, y, 'float');
1588
+ }
1589
+ if (this.pdfiumModule.FPDFAnnot_AddInkStroke(annotationPtr, inkPointsPtr, inkPointsCount) === -1) {
1590
+ this.free(inkPointsPtr);
1591
+ return false;
1592
+ }
1593
+ this.free(inkPointsPtr);
1594
+ }
1595
+ return true;
1596
+ }
1597
+ /**
1598
+ * Add contents to stamp annotation
1599
+ * @param docPtr - pointer to pdf document object
1600
+ * @param page - page info
1601
+ * @param pagePtr - pointer to page object
1602
+ * @param annotationPtr - pointer to stamp annotation
1603
+ * @param rect - rect of stamp annotation
1604
+ * @param contents - contents of stamp annotation
1605
+ * @returns whether contents is added to annotation
1606
+ *
1607
+ * @private
1608
+ */
1609
+ addStampContent(docPtr, page, pagePtr, annotationPtr, rect, contents) {
1610
+ for (const content of contents) {
1611
+ switch (content.type) {
1612
+ case models.PdfPageObjectType.IMAGE:
1613
+ return this.addImageObject(docPtr, page, pagePtr, annotationPtr, rect.origin, content.imageData);
1614
+ }
1615
+ }
1616
+ return false;
1617
+ }
1618
+ /**
1619
+ * Add image object to annotation
1620
+ * @param docPtr - pointer to pdf document object
1621
+ * @param page - page info
1622
+ * @param pagePtr - pointer to page object
1623
+ * @param annotationPtr - pointer to stamp annotation
1624
+ * @param position - position of image
1625
+ * @param imageData - data of image
1626
+ * @returns whether image is added to annotation
1627
+ *
1628
+ * @private
1629
+ */
1630
+ addImageObject(docPtr, page, pagePtr, annotationPtr, position, imageData) {
1631
+ const bytesPerPixel = 4;
1632
+ const pixelCount = imageData.width * imageData.height;
1633
+ const bitmapBufferPtr = this.malloc(bytesPerPixel * pixelCount);
1634
+ if (!bitmapBufferPtr) {
1635
+ return false;
1636
+ }
1637
+ for (let i = 0; i < pixelCount; i++) {
1638
+ const red = imageData.data[i * bytesPerPixel];
1639
+ const green = imageData.data[i * bytesPerPixel + 1];
1640
+ const blue = imageData.data[i * bytesPerPixel + 2];
1641
+ const alpha = imageData.data[i * bytesPerPixel + 3];
1642
+ this.pdfiumModule.pdfium.setValue(bitmapBufferPtr + i * bytesPerPixel, blue, 'i8');
1643
+ this.pdfiumModule.pdfium.setValue(bitmapBufferPtr + i * bytesPerPixel + 1, green, 'i8');
1644
+ this.pdfiumModule.pdfium.setValue(bitmapBufferPtr + i * bytesPerPixel + 2, red, 'i8');
1645
+ this.pdfiumModule.pdfium.setValue(bitmapBufferPtr + i * bytesPerPixel + 3, alpha, 'i8');
1646
+ }
1647
+ const format = BitmapFormat.Bitmap_BGRA;
1648
+ const bitmapPtr = this.pdfiumModule.FPDFBitmap_CreateEx(imageData.width, imageData.height, format, bitmapBufferPtr, 0);
1649
+ if (!bitmapPtr) {
1650
+ this.free(bitmapBufferPtr);
1651
+ return false;
1652
+ }
1653
+ const imageObjectPtr = this.pdfiumModule.FPDFPageObj_NewImageObj(docPtr);
1654
+ if (!imageObjectPtr) {
1655
+ this.pdfiumModule.FPDFBitmap_Destroy(bitmapPtr);
1656
+ this.free(bitmapBufferPtr);
1657
+ return false;
1658
+ }
1659
+ if (!this.pdfiumModule.FPDFImageObj_SetBitmap(pagePtr, 0, imageObjectPtr, bitmapPtr)) {
1660
+ this.pdfiumModule.FPDFBitmap_Destroy(bitmapPtr);
1661
+ this.pdfiumModule.FPDFPageObj_Destroy(imageObjectPtr);
1662
+ this.free(bitmapBufferPtr);
1663
+ return false;
1664
+ }
1665
+ const matrixPtr = this.malloc(6 * 4);
1666
+ this.pdfiumModule.pdfium.setValue(matrixPtr, imageData.width, 'float');
1667
+ this.pdfiumModule.pdfium.setValue(matrixPtr + 4, 0, 'float');
1668
+ this.pdfiumModule.pdfium.setValue(matrixPtr + 8, 0, 'float');
1669
+ this.pdfiumModule.pdfium.setValue(matrixPtr + 12, imageData.height, 'float');
1670
+ this.pdfiumModule.pdfium.setValue(matrixPtr + 16, 0, 'float');
1671
+ this.pdfiumModule.pdfium.setValue(matrixPtr + 20, 0, 'float');
1672
+ if (!this.pdfiumModule.FPDFPageObj_SetMatrix(imageObjectPtr, matrixPtr)) {
1673
+ this.free(matrixPtr);
1674
+ this.pdfiumModule.FPDFBitmap_Destroy(bitmapPtr);
1675
+ this.pdfiumModule.FPDFPageObj_Destroy(imageObjectPtr);
1676
+ this.free(bitmapBufferPtr);
1677
+ return false;
1678
+ }
1679
+ this.free(matrixPtr);
1680
+ this.pdfiumModule.FPDFPageObj_Transform(imageObjectPtr, 1, 0, 0, 1, position.x, position.y);
1681
+ if (!this.pdfiumModule.FPDFAnnot_AppendObject(annotationPtr, imageObjectPtr)) {
1682
+ this.pdfiumModule.FPDFBitmap_Destroy(bitmapPtr);
1683
+ this.pdfiumModule.FPDFPageObj_Destroy(imageObjectPtr);
1684
+ this.free(bitmapBufferPtr);
1685
+ return false;
1686
+ }
1687
+ this.pdfiumModule.FPDFPage_GenerateContent(pagePtr);
1688
+ this.pdfiumModule.FPDFBitmap_Destroy(bitmapPtr);
1689
+ this.free(bitmapBufferPtr);
1690
+ return true;
1691
+ }
1692
+ /**
1693
+ * Save document to array buffer
1694
+ * @param docPtr - pointer to pdf document
1695
+ * @returns array buffer contains the pdf content
1696
+ *
1697
+ * @private
1698
+ */
1699
+ saveDocument(docPtr) {
1700
+ const writerPtr = this.pdfiumModule.PDFiumExt_OpenFileWriter();
1701
+ this.pdfiumModule.PDFiumExt_SaveAsCopy(docPtr, writerPtr);
1702
+ const size = this.pdfiumModule.PDFiumExt_GetFileWriterSize(writerPtr);
1703
+ const dataPtr = this.malloc(size);
1704
+ this.pdfiumModule.PDFiumExt_GetFileWriterData(writerPtr, dataPtr, size);
1705
+ const buffer = new ArrayBuffer(size);
1706
+ const view = new DataView(buffer);
1707
+ for (let i = 0; i < size; i++) {
1708
+ view.setInt8(i, this.pdfiumModule.pdfium.getValue(dataPtr + i, 'i8'));
1709
+ }
1710
+ this.free(dataPtr);
1711
+ this.pdfiumModule.PDFiumExt_CloseFileWriter(writerPtr);
1712
+ return buffer;
1713
+ }
1714
+ /**
1715
+ * Read metadata from pdf document
1716
+ * @param docPtr - pointer to pdf document
1717
+ * @param key - key of metadata field
1718
+ * @returns metadata value
1719
+ *
1720
+ * @private
1721
+ */
1722
+ readMetaText(docPtr, key) {
1723
+ return readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
1724
+ return this.pdfiumModule.FPDF_GetMetaText(docPtr, key, buffer, bufferLength);
1725
+ }, this.pdfiumModule.pdfium.UTF16ToString);
1726
+ }
1727
+ /**
1728
+ * Read bookmarks in the pdf document
1729
+ * @param docPtr - pointer to pdf document
1730
+ * @param rootBookmarkPtr - pointer to root bookmark
1731
+ * @returns bookmarks in the pdf document
1732
+ *
1733
+ * @private
1734
+ */
1735
+ readPdfBookmarks(docPtr, rootBookmarkPtr = 0) {
1736
+ let bookmarkPtr = this.pdfiumModule.FPDFBookmark_GetFirstChild(docPtr, rootBookmarkPtr);
1737
+ const bookmarks = [];
1738
+ while (bookmarkPtr) {
1739
+ const bookmark = this.readPdfBookmark(docPtr, bookmarkPtr);
1740
+ bookmarks.push(bookmark);
1741
+ const nextBookmarkPtr = this.pdfiumModule.FPDFBookmark_GetNextSibling(docPtr, bookmarkPtr);
1742
+ bookmarkPtr = nextBookmarkPtr;
1743
+ }
1744
+ return bookmarks;
1745
+ }
1746
+ /**
1747
+ * Read bookmark in the pdf document
1748
+ * @param docPtr - pointer to pdf document
1749
+ * @param bookmarkPtr - pointer to bookmark object
1750
+ * @returns pdf bookmark object
1751
+ *
1752
+ * @private
1753
+ */
1754
+ readPdfBookmark(docPtr, bookmarkPtr) {
1755
+ const title = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
1756
+ return this.pdfiumModule.FPDFBookmark_GetTitle(bookmarkPtr, buffer, bufferLength);
1757
+ }, this.pdfiumModule.pdfium.UTF16ToString);
1758
+ const bookmarks = this.readPdfBookmarks(docPtr, bookmarkPtr);
1759
+ const target = this.readPdfBookmarkTarget(docPtr, () => {
1760
+ return this.pdfiumModule.FPDFBookmark_GetAction(bookmarkPtr);
1761
+ }, () => {
1762
+ return this.pdfiumModule.FPDFBookmark_GetDest(docPtr, bookmarkPtr);
1763
+ });
1764
+ return {
1765
+ title,
1766
+ target,
1767
+ children: bookmarks,
1768
+ };
1769
+ }
1770
+ /**
1771
+ * Read text rects in pdf page
1772
+ * @param page - pdf page info
1773
+ * @param docPtr - pointer to pdf document
1774
+ * @param pagePtr - pointer to pdf page
1775
+ * @param textPagePtr - pointer to pdf text page
1776
+ * @returns text rects in the pdf page
1777
+ *
1778
+ * @public
1779
+ */
1780
+ readPageTextRects(page, docPtr, pagePtr, textPagePtr) {
1781
+ const rectsCount = this.pdfiumModule.FPDFText_CountRects(textPagePtr, 0, -1);
1782
+ const textRects = [];
1783
+ for (let i = 0; i < rectsCount; i++) {
1784
+ const topPtr = this.malloc(8);
1785
+ const leftPtr = this.malloc(8);
1786
+ const rightPtr = this.malloc(8);
1787
+ const bottomPtr = this.malloc(8);
1788
+ const isSucceed = this.pdfiumModule.FPDFText_GetRect(textPagePtr, i, leftPtr, topPtr, rightPtr, bottomPtr);
1789
+ if (!isSucceed) {
1790
+ this.free(leftPtr);
1791
+ this.free(topPtr);
1792
+ this.free(rightPtr);
1793
+ this.free(bottomPtr);
1794
+ continue;
1795
+ }
1796
+ const left = this.pdfiumModule.pdfium.getValue(leftPtr, 'double');
1797
+ const top = this.pdfiumModule.pdfium.getValue(topPtr, 'double');
1798
+ const right = this.pdfiumModule.pdfium.getValue(rightPtr, 'double');
1799
+ const bottom = this.pdfiumModule.pdfium.getValue(bottomPtr, 'double');
1800
+ this.free(leftPtr);
1801
+ this.free(topPtr);
1802
+ this.free(rightPtr);
1803
+ this.free(bottomPtr);
1804
+ const deviceXPtr = this.malloc(4);
1805
+ const deviceYPtr = this.malloc(4);
1806
+ this.pdfiumModule.FPDF_PageToDevice(pagePtr, 0, 0, page.size.width, page.size.height, 0, left, top, deviceXPtr, deviceYPtr);
1807
+ const x = this.pdfiumModule.pdfium.getValue(deviceXPtr, 'i32');
1808
+ const y = this.pdfiumModule.pdfium.getValue(deviceYPtr, 'i32');
1809
+ this.free(deviceXPtr);
1810
+ this.free(deviceYPtr);
1811
+ const rect = {
1812
+ origin: {
1813
+ x,
1814
+ y,
1815
+ },
1816
+ size: {
1817
+ width: Math.ceil(Math.abs(right - left)),
1818
+ height: Math.ceil(Math.abs(top - bottom)),
1819
+ },
1820
+ };
1821
+ const utf16Length = this.pdfiumModule.FPDFText_GetBoundedText(textPagePtr, left, top, right, bottom, 0, 0);
1822
+ const bytesCount = (utf16Length + 1) * 2; // include NIL
1823
+ const textBuffer = this.malloc(bytesCount);
1824
+ this.pdfiumModule.FPDFText_GetBoundedText(textPagePtr, left, top, right, bottom, textBuffer, utf16Length);
1825
+ const content = this.pdfiumModule.pdfium.UTF16ToString(textBuffer);
1826
+ this.free(textBuffer);
1827
+ const charIndex = this.pdfiumModule.FPDFText_GetCharIndexAtPos(textPagePtr, left, top, 2, 2);
1828
+ let fontFamily = '';
1829
+ let fontSize = rect.size.height;
1830
+ if (charIndex >= 0) {
1831
+ fontSize = this.pdfiumModule.FPDFText_GetFontSize(textPagePtr, charIndex);
1832
+ const fontNameLength = this.pdfiumModule.FPDFText_GetFontInfo(textPagePtr, charIndex, 0, 0, 0);
1833
+ const bytesCount = fontNameLength + 1; // include NIL
1834
+ const textBufferPtr = this.malloc(bytesCount);
1835
+ const flagsPtr = this.malloc(4);
1836
+ this.pdfiumModule.FPDFText_GetFontInfo(textPagePtr, charIndex, textBufferPtr, bytesCount, flagsPtr);
1837
+ fontFamily = this.pdfiumModule.pdfium.UTF8ToString(textBufferPtr);
1838
+ this.free(textBufferPtr);
1839
+ this.free(flagsPtr);
1840
+ }
1841
+ const textRect = {
1842
+ content,
1843
+ rect,
1844
+ font: {
1845
+ family: fontFamily,
1846
+ size: fontSize,
1847
+ },
1848
+ };
1849
+ textRects.push(textRect);
1850
+ }
1851
+ return textRects;
1852
+ }
1853
+ /**
1854
+ * Return geometric + logical text layout for one page
1855
+ * (glyph-only implementation, no FPDFText_GetRect).
1856
+ *
1857
+ * @public
1858
+ */
1859
+ getPageGeometry(doc, page) {
1860
+ const label = 'getPageGeometry';
1861
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, label, 'Begin', doc.id);
1862
+ /* ── guards ───────────────────────────────────────────── */
1863
+ const ctx = this.cache.getContext(doc.id);
1864
+ if (!ctx) {
1865
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, label, 'End', doc.id);
1866
+ return models.PdfTaskHelper.reject({
1867
+ code: models.PdfErrorCode.DocNotOpen,
1868
+ message: 'document does not open',
1869
+ });
1870
+ }
1871
+ /* ── native handles ──────────────────────────────────── */
1872
+ const pageCtx = ctx.acquirePage(page.index);
1873
+ const textPagePtr = pageCtx.getTextPage();
1874
+ /* ── 1. read ALL glyphs in logical order ─────────────── */
1875
+ const glyphCount = this.pdfiumModule.FPDFText_CountChars(textPagePtr);
1876
+ const glyphs = [];
1877
+ for (let i = 0; i < glyphCount; i++) {
1878
+ const g = this.readGlyphInfo(page, pageCtx.pagePtr, textPagePtr, i);
1879
+ glyphs.push(g);
1880
+ }
1881
+ /* ── 2. build visual runs from glyph stream ───────────── */
1882
+ const runs = this.buildRunsFromGlyphs(glyphs, textPagePtr);
1883
+ /* ── 3. cleanup & resolve task ───────────────────────── */
1884
+ pageCtx.release();
1885
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, label, 'End', doc.id);
1886
+ return models.PdfTaskHelper.resolve({ runs });
1887
+ }
1888
+ /**
1889
+ * Group consecutive glyphs that belong to the same CPDF_TextObject
1890
+ * using FPDFText_GetTextObject(), and calculate rotation from glyph positions.
1891
+ */
1892
+ buildRunsFromGlyphs(glyphs, textPagePtr) {
1893
+ const runs = [];
1894
+ let current = null;
1895
+ let curObjPtr = null;
1896
+ /** ── main loop ──────────────────────────────────────────── */
1897
+ for (let i = 0; i < glyphs.length; i++) {
1898
+ const g = glyphs[i];
1899
+ /* 1 — find the CPDF_TextObject this glyph belongs to */
1900
+ const objPtr = this.pdfiumModule.FPDFText_GetTextObject(textPagePtr, i);
1901
+ if (g.isEmpty) {
1902
+ continue;
1903
+ }
1904
+ /* 2 — start a new run when the text object changes */
1905
+ if (objPtr !== curObjPtr) {
1906
+ curObjPtr = objPtr;
1907
+ current = {
1908
+ rect: {
1909
+ x: g.origin.x,
1910
+ y: g.origin.y,
1911
+ width: g.size.width,
1912
+ height: g.size.height,
1913
+ },
1914
+ charStart: i,
1915
+ glyphs: [],
1916
+ };
1917
+ runs.push(current);
1918
+ }
1919
+ /* 3 — append the slim glyph record */
1920
+ current.glyphs.push({
1921
+ x: g.origin.x,
1922
+ y: g.origin.y,
1923
+ width: g.size.width,
1924
+ height: g.size.height,
1925
+ flags: g.isSpace ? 1 : 0,
1926
+ });
1927
+ /* 4 — expand the run's bounding rect */
1928
+ const right = g.origin.x + g.size.width;
1929
+ const bottom = g.origin.y + g.size.height;
1930
+ current.rect.width =
1931
+ Math.max(current.rect.x + current.rect.width, right) - current.rect.x;
1932
+ current.rect.y = Math.min(current.rect.y, g.origin.y);
1933
+ current.rect.height =
1934
+ Math.max(current.rect.y + current.rect.height, bottom) - current.rect.y;
1935
+ }
1936
+ return runs;
1937
+ }
1938
+ /**
1939
+ * Extract glyph geometry + metadata for `charIndex`
1940
+ *
1941
+ * Returns device–space coordinates:
1942
+ * x,y → **top-left** corner (integer-pixels)
1943
+ * w,h → width / height (integer-pixels, ≥ 1)
1944
+ *
1945
+ * And two flags:
1946
+ * isSpace → true if the glyph's Unicode code-point is U+0020
1947
+ */
1948
+ readGlyphInfo(page, pagePtr, textPagePtr, charIndex) {
1949
+ // ── native stack temp pointers ──────────────────────────────
1950
+ const dx1Ptr = this.malloc(4);
1951
+ const dy1Ptr = this.malloc(4);
1952
+ const dx2Ptr = this.malloc(4);
1953
+ const dy2Ptr = this.malloc(4);
1954
+ const rectPtr = this.malloc(16); // 4 floats = 16 bytes
1955
+ let x = 0, y = 0, width = 0, height = 0, isSpace = false;
1956
+ // ── 1) raw glyph bbox in page-user-space
1957
+ if (this.pdfiumModule.FPDFText_GetLooseCharBox(textPagePtr, charIndex, rectPtr)) {
1958
+ const left = this.pdfiumModule.pdfium.getValue(rectPtr, 'float');
1959
+ const top = this.pdfiumModule.pdfium.getValue(rectPtr + 4, 'float');
1960
+ const right = this.pdfiumModule.pdfium.getValue(rectPtr + 8, 'float');
1961
+ const bottom = this.pdfiumModule.pdfium.getValue(rectPtr + 12, 'float');
1962
+ if (left === right || top === bottom) {
1963
+ return {
1964
+ origin: { x: 0, y: 0 },
1965
+ size: { width: 0, height: 0 },
1966
+ isEmpty: true,
1967
+ };
1968
+ }
1969
+ // ── 2) map 2 opposite corners to device-space
1970
+ this.pdfiumModule.FPDF_PageToDevice(pagePtr, 0, 0, page.size.width, page.size.height,
1971
+ /*rotate=*/ 0, left, top, dx1Ptr, dy1Ptr);
1972
+ this.pdfiumModule.FPDF_PageToDevice(pagePtr, 0, 0, page.size.width, page.size.height,
1973
+ /*rotate=*/ 0, right, bottom, dx2Ptr, dy2Ptr);
1974
+ const x1 = this.pdfiumModule.pdfium.getValue(dx1Ptr, 'i32');
1975
+ const y1 = this.pdfiumModule.pdfium.getValue(dy1Ptr, 'i32');
1976
+ const x2 = this.pdfiumModule.pdfium.getValue(dx2Ptr, 'i32');
1977
+ const y2 = this.pdfiumModule.pdfium.getValue(dy2Ptr, 'i32');
1978
+ x = Math.min(x1, x2);
1979
+ y = Math.min(y1, y2);
1980
+ width = Math.max(1, Math.abs(x2 - x1));
1981
+ height = Math.max(1, Math.abs(y2 - y1));
1982
+ // ── 3) extra flags ───────────────────────────────────────
1983
+ const uc = this.pdfiumModule.FPDFText_GetUnicode(textPagePtr, charIndex);
1984
+ isSpace = uc === 32;
1985
+ }
1986
+ // ── free tmps ───────────────────────────────────────────────
1987
+ [rectPtr, dx1Ptr, dy1Ptr, dx2Ptr, dy2Ptr].forEach((p) => this.free(p));
1988
+ return {
1989
+ origin: { x, y },
1990
+ size: { width, height },
1991
+ ...(isSpace && { isSpace }),
1992
+ };
1993
+ }
1994
+ /**
1995
+ * Geometry-only text extraction
1996
+ * ------------------------------------------
1997
+ * Returns every glyph on the requested page
1998
+ * in the logical order delivered by PDFium.
1999
+ *
2000
+ * The promise resolves to an array of objects:
2001
+ * {
2002
+ * idx: number; // glyph index on the page (0…n-1)
2003
+ * origin: { x: number; y: number };
2004
+ * size: { width: number; height: number };
2005
+ * angle: number; // degrees, counter-clock-wise
2006
+ * isSpace: boolean; // true → U+0020
2007
+ * }
2008
+ *
2009
+ * No Unicode is included; front-end decides whether to hydrate it.
2010
+ */
2011
+ getPageGlyphs(doc, page) {
2012
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'getPageGlyphs', doc, page);
2013
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, 'getPageGlyphs', 'Begin', doc.id);
2014
+ // ── 1) safety: document handle must be alive ───────────────
2015
+ const ctx = this.cache.getContext(doc.id);
2016
+ if (!ctx) {
2017
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, 'getPageGlyphs', 'End', doc.id);
2018
+ return models.PdfTaskHelper.reject({
2019
+ code: models.PdfErrorCode.DocNotOpen,
2020
+ message: 'document does not open',
2021
+ });
2022
+ }
2023
+ // ── 2) load page + text page handles ───────────────────────
2024
+ const pageCtx = ctx.acquirePage(page.index);
2025
+ const textPagePtr = pageCtx.getTextPage();
2026
+ // ── 3) iterate all glyphs in logical order ─────────────────
2027
+ const total = this.pdfiumModule.FPDFText_CountChars(textPagePtr);
2028
+ const glyphs = new Array(total);
2029
+ for (let i = 0; i < total; i++) {
2030
+ const g = this.readGlyphInfo(page, pageCtx.pagePtr, textPagePtr, i);
2031
+ if (g.isEmpty) {
2032
+ continue;
2033
+ }
2034
+ glyphs[i] = { ...g };
2035
+ }
2036
+ // ── 4) clean-up native handles ─────────────────────────────
2037
+ pageCtx.release();
2038
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, 'getPageGlyphs', 'End', doc.id);
2039
+ return models.PdfTaskHelper.resolve(glyphs);
2040
+ }
2041
+ readCharBox(page, pagePtr, textPagePtr, charIndex) {
2042
+ const topPtr = this.malloc(8);
2043
+ const leftPtr = this.malloc(8);
2044
+ const bottomPtr = this.malloc(8);
2045
+ const rightPtr = this.malloc(8);
2046
+ let x = 0;
2047
+ let y = 0;
2048
+ let width = 0;
2049
+ let height = 0;
2050
+ if (this.pdfiumModule.FPDFText_GetCharBox(textPagePtr, charIndex, leftPtr, rightPtr, bottomPtr, topPtr)) {
2051
+ const top = this.pdfiumModule.pdfium.getValue(topPtr, 'double');
2052
+ const left = this.pdfiumModule.pdfium.getValue(leftPtr, 'double');
2053
+ const bottom = this.pdfiumModule.pdfium.getValue(bottomPtr, 'double');
2054
+ const right = this.pdfiumModule.pdfium.getValue(rightPtr, 'double');
2055
+ const deviceXPtr = this.malloc(4);
2056
+ const deviceYPtr = this.malloc(4);
2057
+ this.pdfiumModule.FPDF_PageToDevice(pagePtr, 0, 0, page.size.width, page.size.height, 0, left, top, deviceXPtr, deviceYPtr);
2058
+ x = this.pdfiumModule.pdfium.getValue(deviceXPtr, 'i32');
2059
+ y = this.pdfiumModule.pdfium.getValue(deviceYPtr, 'i32');
2060
+ this.free(deviceXPtr);
2061
+ this.free(deviceYPtr);
2062
+ width = Math.ceil(Math.abs(right - left));
2063
+ height = Math.ceil(Math.abs(top - bottom));
2064
+ }
2065
+ this.free(topPtr);
2066
+ this.free(leftPtr);
2067
+ this.free(bottomPtr);
2068
+ this.free(rightPtr);
2069
+ return {
2070
+ origin: {
2071
+ x,
2072
+ y,
2073
+ },
2074
+ size: {
2075
+ width,
2076
+ height,
2077
+ },
2078
+ };
2079
+ }
2080
+ /**
2081
+ * Read page annotations
2082
+ * @param page - page info
2083
+ * @param docPtr - pointer to pdf document
2084
+ * @param pagePtr - pointer to pdf page
2085
+ * @param textPagePtr - pointe to pdf text page
2086
+ * @param scaleFactor - scale factor
2087
+ * @param rotation - rotation angle
2088
+ * @returns annotations on the pdf page
2089
+ *
2090
+ * @private
2091
+ */
2092
+ readPageAnnotations(ctx, page) {
2093
+ const pageCtx = ctx.acquirePage(page.index);
2094
+ const annotationCount = this.pdfiumModule.FPDFPage_GetAnnotCount(pageCtx.pagePtr);
2095
+ const annotations = [];
2096
+ for (let i = 0; i < annotationCount; i++) {
2097
+ pageCtx.withAnnotation(i, (annotPtr) => {
2098
+ const annotation = this.readPageAnnotation(page, pageCtx, annotPtr, i);
2099
+ if (annotation) {
2100
+ annotations.push(annotation);
2101
+ }
2102
+ });
2103
+ }
2104
+ return annotations;
2105
+ }
2106
+ /**
2107
+ * Read pdf annotation from pdf document
2108
+ * @param page - pdf page infor
2109
+ * @param docPtr - pointer to pdf document object
2110
+ * @param pagePtr - pointer to pdf page object
2111
+ * @param textPagePtr - pointer to pdf text page object
2112
+ * @param formHandle - form handle
2113
+ * @param index - index of annotation in the pdf page
2114
+ * @param scaleFactor - factor of scalling
2115
+ * @param rotation - rotation angle
2116
+ * @returns pdf annotation
2117
+ *
2118
+ * @private
2119
+ */
2120
+ readPageAnnotation(page, pageCtx, annotationPtr, index) {
2121
+ const subType = this.pdfiumModule.FPDFAnnot_GetSubtype(annotationPtr);
2122
+ let annotation;
2123
+ switch (subType) {
2124
+ case models.PdfAnnotationSubtype.TEXT:
2125
+ {
2126
+ annotation = this.readPdfTextAnno(page, pageCtx.pagePtr, annotationPtr, index);
2127
+ }
2128
+ break;
2129
+ case models.PdfAnnotationSubtype.FREETEXT:
2130
+ {
2131
+ annotation = this.readPdfFreeTextAnno(page, pageCtx.pagePtr, annotationPtr, index);
2132
+ }
2133
+ break;
2134
+ case models.PdfAnnotationSubtype.LINK:
2135
+ {
2136
+ annotation = this.readPdfLinkAnno(page, pageCtx.docPtr, pageCtx.pagePtr, pageCtx.getTextPage(), annotationPtr, index);
2137
+ }
2138
+ break;
2139
+ case models.PdfAnnotationSubtype.WIDGET:
2140
+ {
2141
+ annotation = this.readPdfWidgetAnno(page, pageCtx.pagePtr, annotationPtr, pageCtx.getFormHandle(), index);
2142
+ }
2143
+ break;
2144
+ case models.PdfAnnotationSubtype.FILEATTACHMENT:
2145
+ {
2146
+ annotation = this.readPdfFileAttachmentAnno(page, pageCtx.pagePtr, annotationPtr, index);
2147
+ }
2148
+ break;
2149
+ case models.PdfAnnotationSubtype.INK:
2150
+ {
2151
+ annotation = this.readPdfInkAnno(page, pageCtx.pagePtr, annotationPtr, index);
2152
+ }
2153
+ break;
2154
+ case models.PdfAnnotationSubtype.POLYGON:
2155
+ {
2156
+ annotation = this.readPdfPolygonAnno(page, pageCtx.pagePtr, annotationPtr, index);
2157
+ }
2158
+ break;
2159
+ case models.PdfAnnotationSubtype.POLYLINE:
2160
+ {
2161
+ annotation = this.readPdfPolylineAnno(page, pageCtx.pagePtr, annotationPtr, index);
2162
+ }
2163
+ break;
2164
+ case models.PdfAnnotationSubtype.LINE:
2165
+ {
2166
+ annotation = this.readPdfLineAnno(page, pageCtx.pagePtr, annotationPtr, index);
2167
+ }
2168
+ break;
2169
+ case models.PdfAnnotationSubtype.HIGHLIGHT:
2170
+ annotation = this.readPdfHighlightAnno(page, pageCtx.pagePtr, annotationPtr, index);
2171
+ break;
2172
+ case models.PdfAnnotationSubtype.STAMP:
2173
+ {
2174
+ annotation = this.readPdfStampAnno(pageCtx.docPtr, page, pageCtx.pagePtr, annotationPtr, index);
2175
+ }
2176
+ break;
2177
+ case models.PdfAnnotationSubtype.SQUARE:
2178
+ {
2179
+ annotation = this.readPdfSquareAnno(page, pageCtx.pagePtr, annotationPtr, index);
2180
+ }
2181
+ break;
2182
+ case models.PdfAnnotationSubtype.CIRCLE:
2183
+ {
2184
+ annotation = this.readPdfCircleAnno(page, pageCtx.pagePtr, annotationPtr, index);
2185
+ }
2186
+ break;
2187
+ case models.PdfAnnotationSubtype.UNDERLINE:
2188
+ {
2189
+ annotation = this.readPdfUnderlineAnno(page, pageCtx.pagePtr, annotationPtr, index);
2190
+ }
2191
+ break;
2192
+ case models.PdfAnnotationSubtype.SQUIGGLY:
2193
+ {
2194
+ annotation = this.readPdfSquigglyAnno(page, pageCtx.pagePtr, annotationPtr, index);
2195
+ }
2196
+ break;
2197
+ case models.PdfAnnotationSubtype.STRIKEOUT:
2198
+ {
2199
+ annotation = this.readPdfStrikeOutAnno(page, pageCtx.pagePtr, annotationPtr, index);
2200
+ }
2201
+ break;
2202
+ case models.PdfAnnotationSubtype.CARET:
2203
+ {
2204
+ annotation = this.readPdfCaretAnno(page, pageCtx.pagePtr, annotationPtr, index);
2205
+ }
2206
+ break;
2207
+ case models.PdfAnnotationSubtype.POPUP:
2208
+ break;
2209
+ default:
2210
+ {
2211
+ annotation = this.readPdfAnno(page, pageCtx.pagePtr, subType, annotationPtr, index);
2212
+ }
2213
+ break;
2214
+ }
2215
+ return annotation;
2216
+ }
2217
+ /**
2218
+ * Return the colour stored directly in the annotation dictionary's `/C` entry.
2219
+ *
2220
+ * Most PDFs created by Acrobat, Microsoft Office, LaTeX, etc. include this entry.
2221
+ * When the key is absent (common in macOS Preview, Chrome, Drawboard) the call
2222
+ * fails and the function returns `undefined`.
2223
+ *
2224
+ * @param annotationPtr - pointer to an `FPDF_ANNOTATION`
2225
+ * @returns An RGBA tuple (0-255 channels) or `undefined` if no `/C` entry exists
2226
+ *
2227
+ * @private
2228
+ */
2229
+ readAnnotationColor(annotationPtr) {
2230
+ const rPtr = this.malloc(4);
2231
+ const gPtr = this.malloc(4);
2232
+ const bPtr = this.malloc(4);
2233
+ const aPtr = this.malloc(4);
2234
+ // colourType 0 = "colour" (stroke/fill); other types are interior/border
2235
+ const ok = this.pdfiumModule.FPDFAnnot_GetColor(annotationPtr,
2236
+ /* colorType = */ 0, rPtr, gPtr, bPtr, aPtr);
2237
+ let colour;
2238
+ if (ok) {
2239
+ colour = {
2240
+ red: this.pdfiumModule.pdfium.getValue(rPtr, 'i32') & 0xff,
2241
+ green: this.pdfiumModule.pdfium.getValue(gPtr, 'i32') & 0xff,
2242
+ blue: this.pdfiumModule.pdfium.getValue(bPtr, 'i32') & 0xff,
2243
+ alpha: this.pdfiumModule.pdfium.getValue(aPtr, 'i32') & 0xff, // 0 = transparent, 255 = opaque
2244
+ };
2245
+ }
2246
+ this.free(rPtr);
2247
+ this.free(gPtr);
2248
+ this.free(bPtr);
2249
+ this.free(aPtr);
2250
+ return colour;
2251
+ }
2252
+ /* --------------------------------------------------------------------------- */
2253
+ /**
2254
+ * Extract the fill (or, if absent, the stroke) colour from a **path object**
2255
+ * inside an appearance stream.
2256
+ *
2257
+ * Works for simple highlights produced by Chrome, Preview, etc. that paint a
2258
+ * single filled rectangle with the desired tint.
2259
+ *
2260
+ * @param pathPtr - pointer to a `FPDF_PAGEOBJECT` of type **PATH**
2261
+ * @returns RGBA tuple or `undefined` when no colour is set on the path
2262
+ *
2263
+ * @private
2264
+ */
2265
+ getColorFromPath(pathPtr) {
2266
+ const r = this.malloc(4), g = this.malloc(4), b = this.malloc(4), a = this.malloc(4);
2267
+ const fillOk = this.pdfiumModule.FPDFPageObj_GetFillColor(pathPtr, r, g, b, a);
2268
+ const strokeOk = !fillOk && // try stroke only if fill failed
2269
+ this.pdfiumModule.FPDFPageObj_GetStrokeColor(pathPtr, r, g, b, a);
2270
+ const ok = fillOk || strokeOk;
2271
+ let c;
2272
+ if (ok) {
2273
+ c = {
2274
+ red: this.pdfiumModule.pdfium.getValue(r, 'i32') & 0xff,
2275
+ green: this.pdfiumModule.pdfium.getValue(g, 'i32') & 0xff,
2276
+ blue: this.pdfiumModule.pdfium.getValue(b, 'i32') & 0xff,
2277
+ alpha: this.pdfiumModule.pdfium.getValue(a, 'i32') & 0xff,
2278
+ };
2279
+ }
2280
+ this.free(r);
2281
+ this.free(g);
2282
+ this.free(b);
2283
+ this.free(a);
2284
+ return c;
2285
+ }
2286
+ /* --------------------------------------------------------------------------- */
2287
+ /**
2288
+ * Recursively walk a page-object tree (PATHs and nested FORM XObjects) until
2289
+ * a colour can be extracted.
2290
+ *
2291
+ * Acrobat often wraps its highlight rectangle in a Form XObject referenced by
2292
+ * the "Do" operator, so this function drills down unlimited depth.
2293
+ *
2294
+ * @param objPtr - pointer to a `FPDF_PAGEOBJECT`
2295
+ * @returns First RGBA tint found, or `undefined` if none of the descendants
2296
+ * carry an explicit fill/stroke colour
2297
+ *
2298
+ * @private
2299
+ */
2300
+ walkPageObjTree(objPtr) {
2301
+ const type = this.pdfiumModule.FPDFPageObj_GetType(objPtr);
2302
+ if (type === models.PdfPageObjectType.PATH)
2303
+ return this.getColorFromPath(objPtr);
2304
+ if (type !== models.PdfPageObjectType.FORM)
2305
+ return undefined;
2306
+ const cnt = this.pdfiumModule.FPDFFormObj_CountObjects(objPtr);
2307
+ for (let i = 0; i < cnt; i++) {
2308
+ const child = this.pdfiumModule.FPDFFormObj_GetObject(objPtr, i);
2309
+ if (!child)
2310
+ continue;
2311
+ const c = this.walkPageObjTree(child);
2312
+ if (c)
2313
+ return c;
2314
+ }
2315
+ return undefined;
2316
+ }
2317
+ /* --------------------------------------------------------------------------- */
2318
+ /**
2319
+ * Iterate over every top-level object in the annotation's **appearance stream**
2320
+ * and invoke {@link walkPageObjTree} to locate a usable tint.
2321
+ *
2322
+ * Catches:
2323
+ * • Simple filled path (Preview, Chrome)
2324
+ * • Form XObject containing the path (Acrobat)
2325
+ *
2326
+ * @param annotPtr - pointer to an `FPDF_ANNOTATION`
2327
+ * @returns RGBA tuple or `undefined` when no colour can be resolved from AP
2328
+ *
2329
+ * @private
2330
+ */
2331
+ colorFromAppearance(annotPtr) {
2332
+ const n = this.pdfiumModule.FPDFAnnot_GetObjectCount(annotPtr);
2333
+ for (let i = 0; i < n; i++) {
2334
+ const obj = this.pdfiumModule.FPDFAnnot_GetObject(annotPtr, i);
2335
+ if (!obj)
2336
+ continue;
2337
+ const c = this.walkPageObjTree(obj);
2338
+ if (c)
2339
+ return c;
2340
+ }
2341
+ return undefined;
2342
+ }
2343
+ /* --------------------------------------------------------------------------- */
2344
+ /**
2345
+ * Resolve the visible fill colour for **Highlight / Underline / StrikeOut /
2346
+ * Squiggly** markup annotations.
2347
+ *
2348
+ * Resolution order (first non-`undefined` wins):
2349
+ * 1. `/C` dictionary entry – fast, present in Acrobat / Office PDFs
2350
+ * 2. Appearance-stream objects – drills into paths & nested forms
2351
+ * 3. Hard-coded fallback (Acrobat-style opaque yellow)
2352
+ *
2353
+ * @param annotationPtr - pointer to an `FPDF_ANNOTATION`
2354
+ * @param fallback - colour to use when the PDF stores no tint at all
2355
+ * @returns Guaranteed RGBA tuple (never `undefined`)
2356
+ *
2357
+ * @private
2358
+ */
2359
+ resolveAnnotationColor(annotationPtr, fallback = { red: 255, green: 245, blue: 155, alpha: 255 }) {
2360
+ return (this.readAnnotationColor(annotationPtr) ?? // 1 – /C entry
2361
+ this.colorFromAppearance(annotationPtr) ?? // 2 – AP stream walk
2362
+ fallback // 3 – default
2363
+ );
2364
+ }
2365
+ /**
2366
+ * Read `/QuadPoints` from any annotation and convert each quadrilateral to
2367
+ * device-space coordinates.
2368
+ *
2369
+ * The four points are returned in natural reading order:
2370
+ * `p1 → p2` (top edge) and `p4 → p3` (bottom edge).
2371
+ * This preserves the true shape for rotated / skewed text, whereas callers
2372
+ * that only need axis-aligned boxes can collapse each quad themselves.
2373
+ *
2374
+ * @param page - logical page info object (`PdfPageObject`)
2375
+ * @param annotationPtr - pointer to the annotation whose quads are needed
2376
+ * @returns Array of `Quad` objects (`[]` if the annotation has no quads)
2377
+ *
2378
+ * @private
2379
+ */
2380
+ readAnnotationQuads(page, annotationPtr) {
2381
+ const quadCount = this.pdfiumModule.FPDFAnnot_CountAttachmentPoints(annotationPtr);
2382
+ if (quadCount === 0)
2383
+ return [];
2384
+ const FS_QUADPOINTSF_SIZE = 8 * 4; // eight floats, 32 bytes
2385
+ const quads = [];
2386
+ for (let qi = 0; qi < quadCount; qi++) {
2387
+ const quadPtr = this.malloc(FS_QUADPOINTSF_SIZE);
2388
+ const ok = this.pdfiumModule.FPDFAnnot_GetAttachmentPoints(annotationPtr, qi, quadPtr);
2389
+ if (ok) {
2390
+ // read the eight floats
2391
+ const xs = [];
2392
+ const ys = [];
2393
+ for (let i = 0; i < 4; i++) {
2394
+ const base = quadPtr + i * 8; // 8 bytes per point (x+y)
2395
+ xs.push(this.pdfiumModule.pdfium.getValue(base, 'float'));
2396
+ ys.push(this.pdfiumModule.pdfium.getValue(base + 4, 'float'));
2397
+ }
2398
+ // convert to device-space
2399
+ const p1 = this.convertPagePointToDevicePoint(page, { x: xs[0], y: ys[0] });
2400
+ const p2 = this.convertPagePointToDevicePoint(page, { x: xs[1], y: ys[1] });
2401
+ const p3 = this.convertPagePointToDevicePoint(page, { x: xs[2], y: ys[2] });
2402
+ const p4 = this.convertPagePointToDevicePoint(page, { x: xs[3], y: ys[3] });
2403
+ quads.push({ p1, p2, p3, p4 });
2404
+ }
2405
+ this.free(quadPtr);
2406
+ }
2407
+ return quads;
2408
+ }
2409
+ /**
2410
+ * Read pdf text annotation
2411
+ * @param page - pdf page infor
2412
+ * @param pagePtr - pointer to pdf page object
2413
+ * @param annotationPtr - pointer to pdf annotation
2414
+ * @param index - index of annotation in the pdf page
2415
+ * @returns pdf text annotation
2416
+ *
2417
+ * @private
2418
+ */
2419
+ readPdfTextAnno(page, pagePtr, annotationPtr, index) {
2420
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2421
+ const annoRect = this.readPageAnnoRect(annotationPtr);
2422
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, annoRect);
2423
+ const author = this.getAnnotString(annotationPtr, 'T');
2424
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2425
+ const modified = this.toIsoDate(modifiedRaw);
2426
+ const contents = this.getAnnotString(annotationPtr, 'Contents') || '';
2427
+ const state = this.getAnnotString(annotationPtr, 'State');
2428
+ const stateModel = this.getAnnotString(annotationPtr, 'StateModel');
2429
+ const color = this.resolveAnnotationColor(annotationPtr);
2430
+ const inReplyToId = this.getInReplyToId(pagePtr, annotationPtr);
2431
+ const popup = !inReplyToId
2432
+ ? this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index)
2433
+ : undefined;
2434
+ return {
2435
+ status: models.PdfAnnotationObjectStatus.Committed,
2436
+ pageIndex: page.index,
2437
+ id: index,
2438
+ type: models.PdfAnnotationSubtype.TEXT,
2439
+ contents,
2440
+ color,
2441
+ rect,
2442
+ popup,
2443
+ appearances,
2444
+ inReplyToId,
2445
+ author,
2446
+ modified,
2447
+ state,
2448
+ stateModel,
2449
+ };
2450
+ }
2451
+ /**
2452
+ * Read pdf freetext annotation
2453
+ * @param page - pdf page infor
2454
+ * @param pagePtr - pointer to pdf page object
2455
+ * @param annotationPtr - pointer to pdf annotation
2456
+ * @param index - index of annotation in the pdf page
2457
+ * @returns pdf freetext annotation
2458
+ *
2459
+ * @private
2460
+ */
2461
+ readPdfFreeTextAnno(page, pagePtr, annotationPtr, index) {
2462
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2463
+ const annoRect = this.readPageAnnoRect(annotationPtr);
2464
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, annoRect);
2465
+ const contents = this.getAnnotString(annotationPtr, 'Contents') || '';
2466
+ const author = this.getAnnotString(annotationPtr, 'T');
2467
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2468
+ const modified = this.toIsoDate(modifiedRaw);
2469
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2470
+ return {
2471
+ status: models.PdfAnnotationObjectStatus.Committed,
2472
+ pageIndex: page.index,
2473
+ id: index,
2474
+ type: models.PdfAnnotationSubtype.FREETEXT,
2475
+ contents,
2476
+ author,
2477
+ modified,
2478
+ rect,
2479
+ popup,
2480
+ appearances,
2481
+ };
2482
+ }
2483
+ /**
2484
+ * Read pdf link annotation from pdf document
2485
+ * @param page - pdf page infor
2486
+ * @param docPtr - pointer to pdf document object
2487
+ * @param pagePtr - pointer to pdf page object
2488
+ * @param textPagePtr - pointer to pdf text page object
2489
+ * @param annotationPtr - pointer to pdf annotation
2490
+ * @param index - index of annotation in the pdf page
2491
+ * @returns pdf link annotation
2492
+ *
2493
+ * @private
2494
+ */
2495
+ readPdfLinkAnno(page, docPtr, pagePtr, textPagePtr, annotationPtr, index) {
2496
+ const linkPtr = this.pdfiumModule.FPDFAnnot_GetLink(annotationPtr);
2497
+ if (!linkPtr) {
2498
+ return;
2499
+ }
2500
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2501
+ const annoRect = this.readPageAnnoRect(annotationPtr);
2502
+ const { left, top, right, bottom } = annoRect;
2503
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, annoRect);
2504
+ const author = this.getAnnotString(annotationPtr, 'T');
2505
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2506
+ const modified = this.toIsoDate(modifiedRaw);
2507
+ const utf16Length = this.pdfiumModule.FPDFText_GetBoundedText(textPagePtr, left, top, right, bottom, 0, 0);
2508
+ const bytesCount = (utf16Length + 1) * 2; // include NIL
2509
+ const textBufferPtr = this.malloc(bytesCount);
2510
+ this.pdfiumModule.FPDFText_GetBoundedText(textPagePtr, left, top, right, bottom, textBufferPtr, utf16Length);
2511
+ const text = this.pdfiumModule.pdfium.UTF16ToString(textBufferPtr);
2512
+ this.free(textBufferPtr);
2513
+ const target = this.readPdfLinkAnnoTarget(docPtr, () => {
2514
+ return this.pdfiumModule.FPDFLink_GetAction(linkPtr);
2515
+ }, () => {
2516
+ return this.pdfiumModule.FPDFLink_GetDest(docPtr, linkPtr);
2517
+ });
2518
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2519
+ return {
2520
+ status: models.PdfAnnotationObjectStatus.Committed,
2521
+ pageIndex: page.index,
2522
+ id: index,
2523
+ type: models.PdfAnnotationSubtype.LINK,
2524
+ text,
2525
+ target,
2526
+ rect,
2527
+ popup,
2528
+ appearances,
2529
+ author,
2530
+ modified,
2531
+ };
2532
+ }
2533
+ /**
2534
+ * Read pdf widget annotation
2535
+ * @param page - pdf page infor
2536
+ * @param pagePtr - pointer to pdf page object
2537
+ * @param annotationPtr - pointer to pdf annotation
2538
+ * @param formHandle - form handle
2539
+ * @param index - index of annotation in the pdf page
2540
+ * @returns pdf widget annotation
2541
+ *
2542
+ * @private
2543
+ */
2544
+ readPdfWidgetAnno(page, pagePtr, annotationPtr, formHandle, index) {
2545
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2546
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2547
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2548
+ const author = this.getAnnotString(annotationPtr, 'T');
2549
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2550
+ const modified = this.toIsoDate(modifiedRaw);
2551
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2552
+ const field = this.readPdfWidgetAnnoField(formHandle, annotationPtr);
2553
+ return {
2554
+ status: models.PdfAnnotationObjectStatus.Committed,
2555
+ pageIndex: page.index,
2556
+ id: index,
2557
+ type: models.PdfAnnotationSubtype.WIDGET,
2558
+ rect,
2559
+ field,
2560
+ popup,
2561
+ appearances,
2562
+ author,
2563
+ modified,
2564
+ };
2565
+ }
2566
+ /**
2567
+ * Read pdf file attachment annotation
2568
+ * @param page - pdf page infor
2569
+ * @param pagePtr - pointer to pdf page object
2570
+ * @param annotationPtr - pointer to pdf annotation
2571
+ * @param index - index of annotation in the pdf page
2572
+ * @returns pdf file attachment annotation
2573
+ *
2574
+ * @private
2575
+ */
2576
+ readPdfFileAttachmentAnno(page, pagePtr, annotationPtr, index) {
2577
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2578
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2579
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2580
+ const author = this.getAnnotString(annotationPtr, 'T');
2581
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2582
+ const modified = this.toIsoDate(modifiedRaw);
2583
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2584
+ return {
2585
+ status: models.PdfAnnotationObjectStatus.Committed,
2586
+ pageIndex: page.index,
2587
+ id: index,
2588
+ type: models.PdfAnnotationSubtype.FILEATTACHMENT,
2589
+ rect,
2590
+ popup,
2591
+ appearances,
2592
+ author,
2593
+ modified,
2594
+ };
2595
+ }
2596
+ /**
2597
+ * Read pdf ink annotation
2598
+ * @param page - pdf page infor
2599
+ * @param pagePtr - pointer to pdf page object
2600
+ * @param annotationPtr - pointer to pdf annotation
2601
+ * @param index - index of annotation in the pdf page
2602
+ * @returns pdf ink annotation
2603
+ *
2604
+ * @private
2605
+ */
2606
+ readPdfInkAnno(page, pagePtr, annotationPtr, index) {
2607
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2608
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2609
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2610
+ const author = this.getAnnotString(annotationPtr, 'T');
2611
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2612
+ const modified = this.toIsoDate(modifiedRaw);
2613
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2614
+ const inkList = [];
2615
+ const count = this.pdfiumModule.FPDFAnnot_GetInkListCount(annotationPtr);
2616
+ for (let i = 0; i < count; i++) {
2617
+ const points = [];
2618
+ const pointsCount = this.pdfiumModule.FPDFAnnot_GetInkListPath(annotationPtr, i, 0, 0);
2619
+ if (pointsCount > 0) {
2620
+ const pointMemorySize = 8;
2621
+ const pointsPtr = this.malloc(pointsCount * pointMemorySize);
2622
+ this.pdfiumModule.FPDFAnnot_GetInkListPath(annotationPtr, i, pointsPtr, pointsCount);
2623
+ for (let j = 0; j < pointsCount; j++) {
2624
+ const pointX = this.pdfiumModule.pdfium.getValue(pointsPtr + j * 8, 'float');
2625
+ const pointY = this.pdfiumModule.pdfium.getValue(pointsPtr + j * 8 + 4, 'float');
2626
+ const { x, y } = this.convertPagePointToDevicePoint(page, {
2627
+ x: pointX,
2628
+ y: pointY,
2629
+ });
2630
+ points.push({ x, y });
2631
+ }
2632
+ this.free(pointsPtr);
2633
+ }
2634
+ inkList.push({ points });
2635
+ }
2636
+ return {
2637
+ status: models.PdfAnnotationObjectStatus.Committed,
2638
+ pageIndex: page.index,
2639
+ id: index,
2640
+ type: models.PdfAnnotationSubtype.INK,
2641
+ rect,
2642
+ popup,
2643
+ inkList,
2644
+ appearances,
2645
+ author,
2646
+ modified,
2647
+ };
2648
+ }
2649
+ /**
2650
+ * Read pdf polygon annotation
2651
+ * @param page - pdf page infor
2652
+ * @param pagePtr - pointer to pdf page object
2653
+ * @param annotationPtr - pointer to pdf annotation
2654
+ * @param index - index of annotation in the pdf page
2655
+ * @returns pdf polygon annotation
2656
+ *
2657
+ * @private
2658
+ */
2659
+ readPdfPolygonAnno(page, pagePtr, annotationPtr, index) {
2660
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2661
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2662
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2663
+ const author = this.getAnnotString(annotationPtr, 'T');
2664
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2665
+ const modified = this.toIsoDate(modifiedRaw);
2666
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2667
+ const vertices = this.readPdfAnnoVertices(page, pagePtr, annotationPtr);
2668
+ return {
2669
+ status: models.PdfAnnotationObjectStatus.Committed,
2670
+ pageIndex: page.index,
2671
+ id: index,
2672
+ type: models.PdfAnnotationSubtype.POLYGON,
2673
+ rect,
2674
+ popup,
2675
+ vertices,
2676
+ appearances,
2677
+ author,
2678
+ modified,
2679
+ };
2680
+ }
2681
+ /**
2682
+ * Read pdf polyline annotation
2683
+ * @param page - pdf page infor
2684
+ * @param pagePtr - pointer to pdf page object
2685
+ * @param annotationPtr - pointer to pdf annotation
2686
+ * @param index - index of annotation in the pdf page
2687
+ * @returns pdf polyline annotation
2688
+ *
2689
+ * @private
2690
+ */
2691
+ readPdfPolylineAnno(page, pagePtr, annotationPtr, index) {
2692
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2693
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2694
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2695
+ const author = this.getAnnotString(annotationPtr, 'T');
2696
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2697
+ const modified = this.toIsoDate(modifiedRaw);
2698
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2699
+ const vertices = this.readPdfAnnoVertices(page, pagePtr, annotationPtr);
2700
+ return {
2701
+ status: models.PdfAnnotationObjectStatus.Committed,
2702
+ pageIndex: page.index,
2703
+ id: index,
2704
+ type: models.PdfAnnotationSubtype.POLYLINE,
2705
+ rect,
2706
+ popup,
2707
+ vertices,
2708
+ appearances,
2709
+ author,
2710
+ modified,
2711
+ };
2712
+ }
2713
+ /**
2714
+ * Read pdf line annotation
2715
+ * @param page - pdf page infor
2716
+ * @param pagePtr - pointer to pdf page object
2717
+ * @param annotationPtr - pointer to pdf annotation
2718
+ * @param index - index of annotation in the pdf page
2719
+ * @returns pdf line annotation
2720
+ *
2721
+ * @private
2722
+ */
2723
+ readPdfLineAnno(page, pagePtr, annotationPtr, index) {
2724
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2725
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2726
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2727
+ const author = this.getAnnotString(annotationPtr, 'T');
2728
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2729
+ const modified = this.toIsoDate(modifiedRaw);
2730
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2731
+ const startPointPtr = this.malloc(8);
2732
+ const endPointPtr = this.malloc(8);
2733
+ this.pdfiumModule.FPDFAnnot_GetLine(annotationPtr, startPointPtr, endPointPtr);
2734
+ const startPointX = this.pdfiumModule.pdfium.getValue(startPointPtr, 'float');
2735
+ const startPointY = this.pdfiumModule.pdfium.getValue(startPointPtr + 4, 'float');
2736
+ const startPoint = this.convertPagePointToDevicePoint(page, {
2737
+ x: startPointX,
2738
+ y: startPointY,
2739
+ });
2740
+ const endPointX = this.pdfiumModule.pdfium.getValue(endPointPtr, 'float');
2741
+ const endPointY = this.pdfiumModule.pdfium.getValue(endPointPtr + 4, 'float');
2742
+ const endPoint = this.convertPagePointToDevicePoint(page, {
2743
+ x: endPointX,
2744
+ y: endPointY,
2745
+ });
2746
+ this.free(startPointPtr);
2747
+ this.free(endPointPtr);
2748
+ return {
2749
+ status: models.PdfAnnotationObjectStatus.Committed,
2750
+ pageIndex: page.index,
2751
+ id: index,
2752
+ type: models.PdfAnnotationSubtype.LINE,
2753
+ rect,
2754
+ popup,
2755
+ startPoint,
2756
+ endPoint,
2757
+ appearances,
2758
+ author,
2759
+ modified,
2760
+ };
2761
+ }
2762
+ /**
2763
+ * Read pdf highlight annotation
2764
+ * @param page - pdf page infor
2765
+ * @param pagePtr - pointer to pdf page object
2766
+ * @param annotationPtr - pointer to pdf annotation
2767
+ * @param index - index of annotation in the pdf page
2768
+ * @returns pdf highlight annotation
2769
+ *
2770
+ * @private
2771
+ */
2772
+ readPdfHighlightAnno(page, pagePtr, annotationPtr, index) {
2773
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2774
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2775
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2776
+ const quads = this.readAnnotationQuads(page, annotationPtr);
2777
+ const color = this.resolveAnnotationColor(annotationPtr);
2778
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2779
+ const author = this.getAnnotString(annotationPtr, 'T');
2780
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2781
+ const modified = this.toIsoDate(modifiedRaw);
2782
+ return {
2783
+ status: models.PdfAnnotationObjectStatus.Committed,
2784
+ pageIndex: page.index,
2785
+ id: index,
2786
+ type: models.PdfAnnotationSubtype.HIGHLIGHT,
2787
+ rect,
2788
+ popup,
2789
+ appearances,
2790
+ segmentRects: quads.map(models.quadToRect),
2791
+ color,
2792
+ author,
2793
+ modified,
2794
+ };
2795
+ }
2796
+ /**
2797
+ * Read pdf underline annotation
2798
+ * @param page - pdf page infor
2799
+ * @param pagePtr - pointer to pdf page object
2800
+ * @param annotationPtr - pointer to pdf annotation
2801
+ * @param index - index of annotation in the pdf page
2802
+ * @returns pdf underline annotation
2803
+ *
2804
+ * @private
2805
+ */
2806
+ readPdfUnderlineAnno(page, pagePtr, annotationPtr, index) {
2807
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2808
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2809
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2810
+ const author = this.getAnnotString(annotationPtr, 'T');
2811
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2812
+ const modified = this.toIsoDate(modifiedRaw);
2813
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2814
+ return {
2815
+ status: models.PdfAnnotationObjectStatus.Committed,
2816
+ pageIndex: page.index,
2817
+ id: index,
2818
+ type: models.PdfAnnotationSubtype.UNDERLINE,
2819
+ rect,
2820
+ popup,
2821
+ appearances,
2822
+ author,
2823
+ modified,
2824
+ };
2825
+ }
2826
+ /**
2827
+ * Read strikeout annotation
2828
+ * @param page - pdf page infor
2829
+ * @param pagePtr - pointer to pdf page object
2830
+ * @param annotationPtr - pointer to pdf annotation
2831
+ * @param index - index of annotation in the pdf page
2832
+ * @returns pdf strikeout annotation
2833
+ *
2834
+ * @private
2835
+ */
2836
+ readPdfStrikeOutAnno(page, pagePtr, annotationPtr, index) {
2837
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2838
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2839
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2840
+ const author = this.getAnnotString(annotationPtr, 'T');
2841
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2842
+ const modified = this.toIsoDate(modifiedRaw);
2843
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2844
+ return {
2845
+ status: models.PdfAnnotationObjectStatus.Committed,
2846
+ pageIndex: page.index,
2847
+ id: index,
2848
+ type: models.PdfAnnotationSubtype.STRIKEOUT,
2849
+ rect,
2850
+ popup,
2851
+ appearances,
2852
+ author,
2853
+ modified,
2854
+ };
2855
+ }
2856
+ /**
2857
+ * Read pdf squiggly annotation
2858
+ * @param page - pdf page infor
2859
+ * @param pagePtr - pointer to pdf page object
2860
+ * @param annotationPtr - pointer to pdf annotation
2861
+ * @param index - index of annotation in the pdf page
2862
+ * @returns pdf squiggly annotation
2863
+ *
2864
+ * @private
2865
+ */
2866
+ readPdfSquigglyAnno(page, pagePtr, annotationPtr, index) {
2867
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2868
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2869
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2870
+ const author = this.getAnnotString(annotationPtr, 'T');
2871
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2872
+ const modified = this.toIsoDate(modifiedRaw);
2873
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2874
+ return {
2875
+ status: models.PdfAnnotationObjectStatus.Committed,
2876
+ pageIndex: page.index,
2877
+ id: index,
2878
+ type: models.PdfAnnotationSubtype.SQUIGGLY,
2879
+ rect,
2880
+ popup,
2881
+ appearances,
2882
+ author,
2883
+ modified,
2884
+ };
2885
+ }
2886
+ /**
2887
+ * Read pdf caret annotation
2888
+ * @param page - pdf page infor
2889
+ * @param pagePtr - pointer to pdf page object
2890
+ * @param annotationPtr - pointer to pdf annotation
2891
+ * @param index - index of annotation in the pdf page
2892
+ * @returns pdf caret annotation
2893
+ *
2894
+ * @private
2895
+ */
2896
+ readPdfCaretAnno(page, pagePtr, annotationPtr, index) {
2897
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2898
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2899
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2900
+ const author = this.getAnnotString(annotationPtr, 'T');
2901
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2902
+ const modified = this.toIsoDate(modifiedRaw);
2903
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2904
+ return {
2905
+ status: models.PdfAnnotationObjectStatus.Committed,
2906
+ pageIndex: page.index,
2907
+ id: index,
2908
+ type: models.PdfAnnotationSubtype.CARET,
2909
+ rect,
2910
+ popup,
2911
+ appearances,
2912
+ author,
2913
+ modified,
2914
+ };
2915
+ }
2916
+ /**
2917
+ * Read pdf stamp annotation
2918
+ * @param docPtr - pointer to pdf document object
2919
+ * @param page - pdf page infor
2920
+ * @param pagePtr - pointer to pdf page object
2921
+ * @param annotationPtr - pointer to pdf annotation
2922
+ * @param index - index of annotation in the pdf page
2923
+ * @returns pdf stamp annotation
2924
+ *
2925
+ * @private
2926
+ */
2927
+ readPdfStampAnno(docPtr, page, pagePtr, annotationPtr, index) {
2928
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2929
+ const pageRect = this.readPageAnnoRect(annotationPtr);
2930
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2931
+ const author = this.getAnnotString(annotationPtr, 'T');
2932
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2933
+ const modified = this.toIsoDate(modifiedRaw);
2934
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2935
+ const contents = [];
2936
+ const objectCount = this.pdfiumModule.FPDFAnnot_GetObjectCount(annotationPtr);
2937
+ for (let i = 0; i < objectCount; i++) {
2938
+ const annotationObjectPtr = this.pdfiumModule.FPDFAnnot_GetObject(annotationPtr, i);
2939
+ const pageObj = this.readPdfPageObject(annotationObjectPtr);
2940
+ if (pageObj) {
2941
+ contents.push(pageObj);
2942
+ }
2943
+ }
2944
+ return {
2945
+ status: models.PdfAnnotationObjectStatus.Committed,
2946
+ pageIndex: page.index,
2947
+ id: index,
2948
+ type: models.PdfAnnotationSubtype.STAMP,
2949
+ rect,
2950
+ popup,
2951
+ contents,
2952
+ appearances,
2953
+ author,
2954
+ modified,
2955
+ };
2956
+ }
2957
+ /**
2958
+ * Read pdf object in pdf page
2959
+ * @param pageObjectPtr - pointer to pdf object in page
2960
+ * @returns pdf object in page
2961
+ *
2962
+ * @private
2963
+ */
2964
+ readPdfPageObject(pageObjectPtr) {
2965
+ const type = this.pdfiumModule.FPDFPageObj_GetType(pageObjectPtr);
2966
+ switch (type) {
2967
+ case models.PdfPageObjectType.PATH:
2968
+ return this.readPathObject(pageObjectPtr);
2969
+ case models.PdfPageObjectType.IMAGE:
2970
+ return this.readImageObject(pageObjectPtr);
2971
+ case models.PdfPageObjectType.FORM:
2972
+ return this.readFormObject(pageObjectPtr);
2973
+ }
2974
+ }
2975
+ /**
2976
+ * Read pdf path object
2977
+ * @param pathObjectPtr - pointer to pdf path object in page
2978
+ * @returns pdf path object
2979
+ *
2980
+ * @private
2981
+ */
2982
+ readPathObject(pathObjectPtr) {
2983
+ const segmentCount = this.pdfiumModule.FPDFPath_CountSegments(pathObjectPtr);
2984
+ const leftPtr = this.malloc(4);
2985
+ const bottomPtr = this.malloc(4);
2986
+ const rightPtr = this.malloc(4);
2987
+ const topPtr = this.malloc(4);
2988
+ this.pdfiumModule.FPDFPageObj_GetBounds(pathObjectPtr, leftPtr, bottomPtr, rightPtr, topPtr);
2989
+ const left = this.pdfiumModule.pdfium.getValue(leftPtr, 'float');
2990
+ const bottom = this.pdfiumModule.pdfium.getValue(bottomPtr, 'float');
2991
+ const right = this.pdfiumModule.pdfium.getValue(rightPtr, 'float');
2992
+ const top = this.pdfiumModule.pdfium.getValue(topPtr, 'float');
2993
+ const bounds = { left, bottom, right, top };
2994
+ this.free(leftPtr);
2995
+ this.free(bottomPtr);
2996
+ this.free(rightPtr);
2997
+ this.free(topPtr);
2998
+ const segments = [];
2999
+ for (let i = 0; i < segmentCount; i++) {
3000
+ const segment = this.readPdfSegment(pathObjectPtr, i);
3001
+ segments.push(segment);
3002
+ }
3003
+ const matrix = this.readPdfPageObjectTransformMatrix(pathObjectPtr);
3004
+ return {
3005
+ type: models.PdfPageObjectType.PATH,
3006
+ bounds,
3007
+ segments,
3008
+ matrix,
3009
+ };
3010
+ }
3011
+ /**
3012
+ * Read segment of pdf path object
3013
+ * @param annotationObjectPtr - pointer to pdf path object
3014
+ * @param segmentIndex - index of segment
3015
+ * @returns pdf segment in pdf path
3016
+ *
3017
+ * @private
3018
+ */
3019
+ readPdfSegment(annotationObjectPtr, segmentIndex) {
3020
+ const segmentPtr = this.pdfiumModule.FPDFPath_GetPathSegment(annotationObjectPtr, segmentIndex);
3021
+ const segmentType = this.pdfiumModule.FPDFPathSegment_GetType(segmentPtr);
3022
+ const isClosed = this.pdfiumModule.FPDFPathSegment_GetClose(segmentPtr);
3023
+ const pointXPtr = this.malloc(4);
3024
+ const pointYPtr = this.malloc(4);
3025
+ this.pdfiumModule.FPDFPathSegment_GetPoint(segmentPtr, pointXPtr, pointYPtr);
3026
+ const pointX = this.pdfiumModule.pdfium.getValue(pointXPtr, 'float');
3027
+ const pointY = this.pdfiumModule.pdfium.getValue(pointYPtr, 'float');
3028
+ this.free(pointXPtr);
3029
+ this.free(pointYPtr);
3030
+ return {
3031
+ type: segmentType,
3032
+ point: { x: pointX, y: pointY },
3033
+ isClosed,
3034
+ };
3035
+ }
3036
+ /**
3037
+ * Read pdf image object from pdf document
3038
+ * @param pageObjectPtr - pointer to pdf image object in page
3039
+ * @returns pdf image object
3040
+ *
3041
+ * @private
3042
+ */
3043
+ readImageObject(imageObjectPtr) {
3044
+ const bitmapPtr = this.pdfiumModule.FPDFImageObj_GetBitmap(imageObjectPtr);
3045
+ const bitmapBufferPtr = this.pdfiumModule.FPDFBitmap_GetBuffer(bitmapPtr);
3046
+ const bitmapWidth = this.pdfiumModule.FPDFBitmap_GetWidth(bitmapPtr);
3047
+ const bitmapHeight = this.pdfiumModule.FPDFBitmap_GetHeight(bitmapPtr);
3048
+ const format = this.pdfiumModule.FPDFBitmap_GetFormat(bitmapPtr);
3049
+ const pixelCount = bitmapWidth * bitmapHeight;
3050
+ const bytesPerPixel = 4;
3051
+ const array = new Uint8ClampedArray(pixelCount * bytesPerPixel);
3052
+ for (let i = 0; i < pixelCount; i++) {
3053
+ switch (format) {
3054
+ case BitmapFormat.Bitmap_BGR:
3055
+ {
3056
+ const blue = this.pdfiumModule.pdfium.getValue(bitmapBufferPtr + i * 3, 'i8');
3057
+ const green = this.pdfiumModule.pdfium.getValue(bitmapBufferPtr + i * 3 + 1, 'i8');
3058
+ const red = this.pdfiumModule.pdfium.getValue(bitmapBufferPtr + i * 3 + 2, 'i8');
3059
+ array[i * bytesPerPixel] = red;
3060
+ array[i * bytesPerPixel + 1] = green;
3061
+ array[i * bytesPerPixel + 2] = blue;
3062
+ array[i * bytesPerPixel + 3] = 100;
3063
+ }
3064
+ break;
3065
+ }
3066
+ }
3067
+ const imageData = new ImageData(array, bitmapWidth, bitmapHeight);
3068
+ const matrix = this.readPdfPageObjectTransformMatrix(imageObjectPtr);
3069
+ return {
3070
+ type: models.PdfPageObjectType.IMAGE,
3071
+ imageData,
3072
+ matrix,
3073
+ };
3074
+ }
3075
+ /**
3076
+ * Read form object from pdf document
3077
+ * @param formObjectPtr - pointer to pdf form object in page
3078
+ * @returns pdf form object
3079
+ *
3080
+ * @private
3081
+ */
3082
+ readFormObject(formObjectPtr) {
3083
+ const objectCount = this.pdfiumModule.FPDFFormObj_CountObjects(formObjectPtr);
3084
+ const objects = [];
3085
+ for (let i = 0; i < objectCount; i++) {
3086
+ const pageObjectPtr = this.pdfiumModule.FPDFFormObj_GetObject(formObjectPtr, i);
3087
+ const pageObj = this.readPdfPageObject(pageObjectPtr);
3088
+ if (pageObj) {
3089
+ objects.push(pageObj);
3090
+ }
3091
+ }
3092
+ const matrix = this.readPdfPageObjectTransformMatrix(formObjectPtr);
3093
+ return {
3094
+ type: models.PdfPageObjectType.FORM,
3095
+ objects,
3096
+ matrix,
3097
+ };
3098
+ }
3099
+ /**
3100
+ * Read pdf object in pdf page
3101
+ * @param pageObjectPtr - pointer to pdf object in page
3102
+ * @returns pdf object in page
3103
+ *
3104
+ * @private
3105
+ */
3106
+ readPdfPageObjectTransformMatrix(pageObjectPtr) {
3107
+ const matrixPtr = this.malloc(4 * 6);
3108
+ if (this.pdfiumModule.FPDFPageObj_GetMatrix(pageObjectPtr, matrixPtr)) {
3109
+ const a = this.pdfiumModule.pdfium.getValue(matrixPtr, 'float');
3110
+ const b = this.pdfiumModule.pdfium.getValue(matrixPtr + 4, 'float');
3111
+ const c = this.pdfiumModule.pdfium.getValue(matrixPtr + 8, 'float');
3112
+ const d = this.pdfiumModule.pdfium.getValue(matrixPtr + 12, 'float');
3113
+ const e = this.pdfiumModule.pdfium.getValue(matrixPtr + 16, 'float');
3114
+ const f = this.pdfiumModule.pdfium.getValue(matrixPtr + 20, 'float');
3115
+ this.free(matrixPtr);
3116
+ return { a, b, c, d, e, f };
3117
+ }
3118
+ this.free(matrixPtr);
3119
+ return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
3120
+ }
3121
+ /**
3122
+ * Read circle annotation
3123
+ * @param page - pdf page infor
3124
+ * @param pagePtr - pointer to pdf page object
3125
+ * @param annotationPtr - pointer to pdf annotation
3126
+ * @param index - index of annotation in the pdf page
3127
+ * @returns pdf circle annotation
3128
+ *
3129
+ * @private
3130
+ */
3131
+ readPdfCircleAnno(page, pagePtr, annotationPtr, index) {
3132
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
3133
+ const pageRect = this.readPageAnnoRect(annotationPtr);
3134
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
3135
+ const author = this.getAnnotString(annotationPtr, 'T');
3136
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
3137
+ const modified = this.toIsoDate(modifiedRaw);
3138
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3139
+ return {
3140
+ status: models.PdfAnnotationObjectStatus.Committed,
3141
+ pageIndex: page.index,
3142
+ id: index,
3143
+ type: models.PdfAnnotationSubtype.CIRCLE,
3144
+ rect,
3145
+ popup,
3146
+ appearances,
3147
+ author,
3148
+ modified,
3149
+ };
3150
+ }
3151
+ /**
3152
+ * Read square annotation
3153
+ * @param page - pdf page infor
3154
+ * @param pagePtr - pointer to pdf page object
3155
+ * @param annotationPtr - pointer to pdf annotation
3156
+ * @param index - index of annotation in the pdf page
3157
+ * @returns pdf square annotation
3158
+ *
3159
+ * @private
3160
+ */
3161
+ readPdfSquareAnno(page, pagePtr, annotationPtr, index) {
3162
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
3163
+ const pageRect = this.readPageAnnoRect(annotationPtr);
3164
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
3165
+ const author = this.getAnnotString(annotationPtr, 'T');
3166
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
3167
+ const modified = this.toIsoDate(modifiedRaw);
3168
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3169
+ return {
3170
+ status: models.PdfAnnotationObjectStatus.Committed,
3171
+ pageIndex: page.index,
3172
+ id: index,
3173
+ type: models.PdfAnnotationSubtype.SQUARE,
3174
+ rect,
3175
+ popup,
3176
+ appearances,
3177
+ author,
3178
+ modified,
3179
+ };
3180
+ }
3181
+ /**
3182
+ * Read basic info of unsupported pdf annotation
3183
+ * @param page - pdf page infor
3184
+ * @param pagePtr - pointer to pdf page object
3185
+ * @param type - type of annotation
3186
+ * @param annotationPtr - pointer to pdf annotation
3187
+ * @param index - index of annotation in the pdf page
3188
+ * @returns pdf annotation
3189
+ *
3190
+ * @private
3191
+ */
3192
+ readPdfAnno(page, pagePtr, type, annotationPtr, index) {
3193
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
3194
+ const pageRect = this.readPageAnnoRect(annotationPtr);
3195
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
3196
+ const author = this.getAnnotString(annotationPtr, 'T');
3197
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
3198
+ const modified = this.toIsoDate(modifiedRaw);
3199
+ const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3200
+ return {
3201
+ status: models.PdfAnnotationObjectStatus.Committed,
3202
+ pageIndex: page.index,
3203
+ id: index,
3204
+ type,
3205
+ rect,
3206
+ popup,
3207
+ appearances,
3208
+ author,
3209
+ modified,
3210
+ };
3211
+ }
3212
+ /**
3213
+ * Resolve `/IRT` → parent-annotation index on the same page.
3214
+ *
3215
+ * @param pagePtr - pointer to FPDF_PAGE
3216
+ * @param annotationPtr - pointer to FPDF_ANNOTATION
3217
+ * @returns index (`0…count-1`) or `undefined` when the annotation is *not* a reply
3218
+ *
3219
+ * @private
3220
+ */
3221
+ getInReplyToId(pagePtr, annotationPtr) {
3222
+ const parentPtr = this.pdfiumModule.FPDFAnnot_GetLinkedAnnot(annotationPtr, 'IRT');
3223
+ if (!parentPtr)
3224
+ return;
3225
+ // PDFium ≥ 5100 – returns −1 when annot not found on page
3226
+ const idx = this.pdfiumModule.FPDFPage_GetAnnotIndex(pagePtr, parentPtr);
3227
+ return idx >= 0 ? idx : undefined;
3228
+ }
3229
+ /**
3230
+ * Parse a PDF date string **D:YYYYMMDDHHmmSSOHH'mm'** to ISO-8601.
3231
+ *
3232
+ * Returns `undefined` if the input is malformed.
3233
+ *
3234
+ * @private
3235
+ */
3236
+ toIsoDate(pdfDate) {
3237
+ if (!pdfDate?.startsWith('D:'))
3238
+ return;
3239
+ // Minimal parse – ignore timezone for brevity
3240
+ const y = pdfDate.substring(2, 6);
3241
+ const m = pdfDate.substring(6, 8) || '01';
3242
+ const d = pdfDate.substring(8, 10) || '01';
3243
+ const H = pdfDate.substring(10, 12) || '00';
3244
+ const M = pdfDate.substring(12, 14) || '00';
3245
+ const S = pdfDate.substring(14, 16) || '00';
3246
+ return `${y}-${m}-${d}T${H}:${M}:${S}`;
3247
+ }
3248
+ /**
3249
+ * Fetch a string value (`/T`, `/M`, `/State`, …) from an annotation.
3250
+ *
3251
+ * @returns decoded UTF-8 string or `undefined` when the key is absent
3252
+ *
3253
+ * @private
3254
+ */
3255
+ getAnnotString(annotationPtr, key) {
3256
+ const len = this.pdfiumModule.FPDFAnnot_GetStringValue(annotationPtr, key, 0, 0);
3257
+ if (len === 0)
3258
+ return;
3259
+ const bytes = (len + 1) * 2;
3260
+ const ptr = this.malloc(bytes);
3261
+ this.pdfiumModule.FPDFAnnot_GetStringValue(annotationPtr, key, ptr, bytes);
3262
+ const value = this.pdfiumModule.pdfium.UTF16ToString(ptr);
3263
+ this.free(ptr);
3264
+ return value || undefined;
3265
+ }
3266
+ /**
3267
+ * Read linked popup of pdf annotation
3268
+ * @param page - pdf page infor
3269
+ * @param pagePtr - pointer to pdf page object
3270
+ * @param annotationPtr - pointer to pdf annotation
3271
+ * @param index - index of annotation in the pdf page
3272
+ * @returns pdf popup linked to annotation
3273
+ *
3274
+ * @private
3275
+ */
3276
+ readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index) {
3277
+ const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
3278
+ const popupAnnotationPtr = this.pdfiumModule.FPDFAnnot_GetLinkedAnnot(annotationPtr, 'Popup');
3279
+ if (!popupAnnotationPtr) {
3280
+ return;
3281
+ }
3282
+ const pageRect = this.readPageAnnoRect(popupAnnotationPtr);
3283
+ const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
3284
+ const author = this.getAnnotString(annotationPtr, 'T');
3285
+ const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
3286
+ const modified = this.toIsoDate(modifiedRaw);
3287
+ const contents = this.getAnnotString(annotationPtr, 'Contents') || '';
3288
+ const open = this.getAnnotString(annotationPtr, 'Open') || 'false';
3289
+ this.pdfiumModule.FPDFPage_CloseAnnot(popupAnnotationPtr);
3290
+ return {
3291
+ status: models.PdfAnnotationObjectStatus.Committed,
3292
+ pageIndex: page.index,
3293
+ id: index,
3294
+ type: models.PdfAnnotationSubtype.POPUP,
3295
+ rect,
3296
+ contents,
3297
+ open: open === 'true',
3298
+ appearances,
3299
+ author,
3300
+ modified,
3301
+ };
3302
+ }
3303
+ /**
3304
+ * Read vertices of pdf annotation
3305
+ * @param page - pdf page infor
3306
+ * @param pagePtr - pointer to pdf page object
3307
+ * @param annotationPtr - pointer to pdf annotation
3308
+ * @returns vertices of pdf annotation
3309
+ *
3310
+ * @private
3311
+ */
3312
+ readPdfAnnoVertices(page, pagePtr, annotationPtr) {
3313
+ const vertices = [];
3314
+ const count = this.pdfiumModule.FPDFAnnot_GetVertices(annotationPtr, 0, 0);
3315
+ const pointMemorySize = 8;
3316
+ const pointsPtr = this.malloc(count * pointMemorySize);
3317
+ this.pdfiumModule.FPDFAnnot_GetVertices(annotationPtr, pointsPtr, count);
3318
+ for (let i = 0; i < count; i++) {
3319
+ const pointX = this.pdfiumModule.pdfium.getValue(pointsPtr + i * pointMemorySize, 'float');
3320
+ const pointY = this.pdfiumModule.pdfium.getValue(pointsPtr + i * pointMemorySize + 4, 'float');
3321
+ const { x, y } = this.convertPagePointToDevicePoint(page, {
3322
+ x: pointX,
3323
+ y: pointY,
3324
+ });
3325
+ vertices.push({
3326
+ x,
3327
+ y,
3328
+ });
3329
+ }
3330
+ this.free(pointsPtr);
3331
+ return vertices;
3332
+ }
3333
+ /**
3334
+ * Read the target of pdf bookmark
3335
+ * @param docPtr - pointer to pdf document object
3336
+ * @param getActionPtr - callback function to retrive the pointer of action
3337
+ * @param getDestinationPtr - callback function to retrive the pointer of destination
3338
+ * @returns target of pdf bookmark
3339
+ *
3340
+ * @private
3341
+ */
3342
+ readPdfBookmarkTarget(docPtr, getActionPtr, getDestinationPtr) {
3343
+ const actionPtr = getActionPtr();
3344
+ if (actionPtr) {
3345
+ const action = this.readPdfAction(docPtr, actionPtr);
3346
+ return {
3347
+ type: 'action',
3348
+ action,
3349
+ };
3350
+ }
3351
+ else {
3352
+ const destinationPtr = getDestinationPtr();
3353
+ if (destinationPtr) {
3354
+ const destination = this.readPdfDestination(docPtr, destinationPtr);
3355
+ return {
3356
+ type: 'destination',
3357
+ destination,
3358
+ };
3359
+ }
3360
+ }
3361
+ }
3362
+ /**
3363
+ * Read field of pdf widget annotation
3364
+ * @param formHandle - form handle
3365
+ * @param annotationPtr - pointer to pdf annotation
3366
+ * @returns field of pdf widget annotation
3367
+ *
3368
+ * @private
3369
+ */
3370
+ readPdfWidgetAnnoField(formHandle, annotationPtr) {
3371
+ const flag = this.pdfiumModule.FPDFAnnot_GetFormFieldFlags(formHandle, annotationPtr);
3372
+ const type = this.pdfiumModule.FPDFAnnot_GetFormFieldType(formHandle, annotationPtr);
3373
+ const name = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
3374
+ return this.pdfiumModule.FPDFAnnot_GetFormFieldName(formHandle, annotationPtr, buffer, bufferLength);
3375
+ }, this.pdfiumModule.pdfium.UTF16ToString);
3376
+ const alternateName = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
3377
+ return this.pdfiumModule.FPDFAnnot_GetFormFieldAlternateName(formHandle, annotationPtr, buffer, bufferLength);
3378
+ }, this.pdfiumModule.pdfium.UTF16ToString);
3379
+ const value = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
3380
+ return this.pdfiumModule.FPDFAnnot_GetFormFieldValue(formHandle, annotationPtr, buffer, bufferLength);
3381
+ }, this.pdfiumModule.pdfium.UTF16ToString);
3382
+ const options = [];
3383
+ if (type === models.PDF_FORM_FIELD_TYPE.COMBOBOX || type === models.PDF_FORM_FIELD_TYPE.LISTBOX) {
3384
+ const count = this.pdfiumModule.FPDFAnnot_GetOptionCount(formHandle, annotationPtr);
3385
+ for (let i = 0; i < count; i++) {
3386
+ const label = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
3387
+ return this.pdfiumModule.FPDFAnnot_GetOptionLabel(formHandle, annotationPtr, i, buffer, bufferLength);
3388
+ }, this.pdfiumModule.pdfium.UTF16ToString);
3389
+ const isSelected = this.pdfiumModule.FPDFAnnot_IsOptionSelected(formHandle, annotationPtr, i);
3390
+ options.push({
3391
+ label,
3392
+ isSelected,
3393
+ });
3394
+ }
3395
+ }
3396
+ let isChecked = false;
3397
+ if (type === models.PDF_FORM_FIELD_TYPE.CHECKBOX || type === models.PDF_FORM_FIELD_TYPE.RADIOBUTTON) {
3398
+ isChecked = this.pdfiumModule.FPDFAnnot_IsChecked(formHandle, annotationPtr);
3399
+ }
3400
+ return {
3401
+ flag,
3402
+ type,
3403
+ name,
3404
+ alternateName,
3405
+ value,
3406
+ isChecked,
3407
+ options,
3408
+ };
3409
+ }
3410
+ /**
3411
+ * render rectangle of pdf page to image
3412
+ * @param docPtr - pointer to pdf document object
3413
+ * @param page - pdf page infor
3414
+ * @param rect - rectangle info
3415
+ * @param scaleFactor - factor of scalling
3416
+ * @param rotation - rotation angle
3417
+ * @param options - render options
3418
+ * @returns image data
3419
+ *
3420
+ * @private
3421
+ */
3422
+ renderPageRectToImageData(ctx, page, rect, scaleFactor, rotation, dpr, options) {
3423
+ const format = BitmapFormat.Bitmap_BGRA;
3424
+ const bytesPerPixel = 4;
3425
+ // Round the transformed dimensions to whole pixels
3426
+ const rectSize = models.toIntRect(models.transformRect(page.size, rect, rotation, scaleFactor * dpr));
3427
+ const pageSize = models.toIntSize(models.transformSize(page.size, rotation, scaleFactor * dpr));
3428
+ const bitmapHeapLength = rectSize.size.width * rectSize.size.height * bytesPerPixel;
3429
+ const bitmapHeapPtr = this.malloc(bitmapHeapLength);
3430
+ const bitmapPtr = this.pdfiumModule.FPDFBitmap_CreateEx(rectSize.size.width, rectSize.size.height, format, bitmapHeapPtr, rectSize.size.width * bytesPerPixel);
3431
+ this.pdfiumModule.FPDFBitmap_FillRect(bitmapPtr, 0, 0, rectSize.size.width, rectSize.size.height, 0xffffffff);
3432
+ let flags = RenderFlag.REVERSE_BYTE_ORDER;
3433
+ if (options?.withAnnotations) {
3434
+ flags = flags | RenderFlag.ANNOT;
3435
+ }
3436
+ const pageCtx = ctx.acquirePage(page.index);
3437
+ this.pdfiumModule.FPDF_RenderPageBitmap(bitmapPtr, pageCtx.pagePtr, -rectSize.origin.x, -rectSize.origin.y, pageSize.width, pageSize.height, rotation, flags);
3438
+ this.pdfiumModule.FPDFBitmap_Destroy(bitmapPtr);
3439
+ pageCtx.release();
3440
+ const data = this.pdfiumModule.pdfium.HEAPU8.subarray(bitmapHeapPtr, bitmapHeapPtr + bitmapHeapLength);
3441
+ const imageData = {
3442
+ data: new Uint8ClampedArray(data),
3443
+ width: rectSize.size.width,
3444
+ height: rectSize.size.height,
3445
+ };
3446
+ this.free(bitmapHeapPtr);
3447
+ return imageData;
3448
+ }
3449
+ /**
3450
+ * Read the target of pdf link annotation
3451
+ * @param docPtr - pointer to pdf document object
3452
+ * @param getActionPtr - callback function to retrive the pointer of action
3453
+ * @param getDestinationPtr - callback function to retrive the pointer of destination
3454
+ * @returns target of link
3455
+ *
3456
+ * @private
3457
+ */
3458
+ readPdfLinkAnnoTarget(docPtr, getActionPtr, getDestinationPtr) {
3459
+ const destinationPtr = getDestinationPtr();
3460
+ if (destinationPtr) {
3461
+ const destination = this.readPdfDestination(docPtr, destinationPtr);
3462
+ return {
3463
+ type: 'destination',
3464
+ destination,
3465
+ };
3466
+ }
3467
+ else {
3468
+ const actionPtr = getActionPtr();
3469
+ if (actionPtr) {
3470
+ const action = this.readPdfAction(docPtr, actionPtr);
3471
+ return {
3472
+ type: 'action',
3473
+ action,
3474
+ };
3475
+ }
3476
+ }
3477
+ }
3478
+ /**
3479
+ * Read pdf action from pdf document
3480
+ * @param docPtr - pointer to pdf document object
3481
+ * @param actionPtr - pointer to pdf action object
3482
+ * @returns pdf action object
3483
+ *
3484
+ * @private
3485
+ */
3486
+ readPdfAction(docPtr, actionPtr) {
3487
+ const actionType = this.pdfiumModule.FPDFAction_GetType(actionPtr);
3488
+ let action;
3489
+ switch (actionType) {
3490
+ case models.PdfActionType.Unsupported:
3491
+ action = {
3492
+ type: models.PdfActionType.Unsupported,
3493
+ };
3494
+ break;
3495
+ case models.PdfActionType.Goto:
3496
+ {
3497
+ const destinationPtr = this.pdfiumModule.FPDFAction_GetDest(docPtr, actionPtr);
3498
+ if (destinationPtr) {
3499
+ const destination = this.readPdfDestination(docPtr, destinationPtr);
3500
+ action = {
3501
+ type: models.PdfActionType.Goto,
3502
+ destination,
3503
+ };
3504
+ }
3505
+ else {
3506
+ action = {
3507
+ type: models.PdfActionType.Unsupported,
3508
+ };
3509
+ }
3510
+ }
3511
+ break;
3512
+ case models.PdfActionType.RemoteGoto:
3513
+ {
3514
+ // In case of remote goto action,
3515
+ // the application should first use FPDFAction_GetFilePath
3516
+ // to get file path, then load that particular document,
3517
+ // and use its document handle to call this
3518
+ action = {
3519
+ type: models.PdfActionType.Unsupported,
3520
+ };
3521
+ }
3522
+ break;
3523
+ case models.PdfActionType.URI:
3524
+ {
3525
+ const uri = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
3526
+ return this.pdfiumModule.FPDFAction_GetURIPath(docPtr, actionPtr, buffer, bufferLength);
3527
+ }, this.pdfiumModule.pdfium.UTF8ToString);
3528
+ action = {
3529
+ type: models.PdfActionType.URI,
3530
+ uri,
3531
+ };
3532
+ }
3533
+ break;
3534
+ case models.PdfActionType.LaunchAppOrOpenFile:
3535
+ {
3536
+ const path = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
3537
+ return this.pdfiumModule.FPDFAction_GetFilePath(actionPtr, buffer, bufferLength);
3538
+ }, this.pdfiumModule.pdfium.UTF8ToString);
3539
+ action = {
3540
+ type: models.PdfActionType.LaunchAppOrOpenFile,
3541
+ path,
3542
+ };
3543
+ }
3544
+ break;
3545
+ }
3546
+ return action;
3547
+ }
3548
+ /**
3549
+ * Read pdf destination object
3550
+ * @param docPtr - pointer to pdf document object
3551
+ * @param destinationPtr - pointer to pdf destination
3552
+ * @returns pdf destination object
3553
+ *
3554
+ * @private
3555
+ */
3556
+ readPdfDestination(docPtr, destinationPtr) {
3557
+ const pageIndex = this.pdfiumModule.FPDFDest_GetDestPageIndex(docPtr, destinationPtr);
3558
+ // Every params is a float value
3559
+ const maxParmamsCount = 4;
3560
+ const paramsCountPtr = this.malloc(maxParmamsCount);
3561
+ const paramsPtr = this.malloc(maxParmamsCount * 4);
3562
+ const zoomMode = this.pdfiumModule.FPDFDest_GetView(destinationPtr, paramsCountPtr, paramsPtr);
3563
+ const paramsCount = this.pdfiumModule.pdfium.getValue(paramsCountPtr, 'i32');
3564
+ const view = [];
3565
+ for (let i = 0; i < paramsCount; i++) {
3566
+ const paramPtr = paramsPtr + i * 4;
3567
+ view.push(this.pdfiumModule.pdfium.getValue(paramPtr, 'float'));
3568
+ }
3569
+ this.free(paramsCountPtr);
3570
+ this.free(paramsPtr);
3571
+ if (zoomMode === models.PdfZoomMode.XYZ) {
3572
+ const hasXPtr = this.malloc(1);
3573
+ const hasYPtr = this.malloc(1);
3574
+ const hasZPtr = this.malloc(1);
3575
+ const xPtr = this.malloc(4);
3576
+ const yPtr = this.malloc(4);
3577
+ const zPtr = this.malloc(4);
3578
+ const isSucceed = this.pdfiumModule.FPDFDest_GetLocationInPage(destinationPtr, hasXPtr, hasYPtr, hasZPtr, xPtr, yPtr, zPtr);
3579
+ if (isSucceed) {
3580
+ const hasX = this.pdfiumModule.pdfium.getValue(hasXPtr, 'i8');
3581
+ const hasY = this.pdfiumModule.pdfium.getValue(hasYPtr, 'i8');
3582
+ const hasZ = this.pdfiumModule.pdfium.getValue(hasZPtr, 'i8');
3583
+ const x = hasX ? this.pdfiumModule.pdfium.getValue(xPtr, 'float') : 0;
3584
+ const y = hasY ? this.pdfiumModule.pdfium.getValue(yPtr, 'float') : 0;
3585
+ const zoom = hasZ ? this.pdfiumModule.pdfium.getValue(zPtr, 'float') : 0;
3586
+ this.free(hasXPtr);
3587
+ this.free(hasYPtr);
3588
+ this.free(hasZPtr);
3589
+ this.free(xPtr);
3590
+ this.free(yPtr);
3591
+ this.free(zPtr);
3592
+ return {
3593
+ pageIndex,
3594
+ zoom: {
3595
+ mode: zoomMode,
3596
+ params: {
3597
+ x,
3598
+ y,
3599
+ zoom,
3600
+ },
3601
+ },
3602
+ view,
3603
+ };
3604
+ }
3605
+ this.free(hasXPtr);
3606
+ this.free(hasYPtr);
3607
+ this.free(hasZPtr);
3608
+ this.free(xPtr);
3609
+ this.free(yPtr);
3610
+ this.free(zPtr);
3611
+ return {
3612
+ pageIndex,
3613
+ zoom: {
3614
+ mode: zoomMode,
3615
+ params: {
3616
+ x: 0,
3617
+ y: 0,
3618
+ zoom: 0,
3619
+ },
3620
+ },
3621
+ view,
3622
+ };
3623
+ }
3624
+ return {
3625
+ pageIndex,
3626
+ zoom: {
3627
+ mode: zoomMode,
3628
+ },
3629
+ view,
3630
+ };
3631
+ }
3632
+ /**
3633
+ * Read attachmet from pdf document
3634
+ * @param docPtr - pointer to pdf document object
3635
+ * @param index - index of attachment
3636
+ * @returns attachment content
3637
+ *
3638
+ * @private
3639
+ */
3640
+ readPdfAttachment(docPtr, index) {
3641
+ const attachmentPtr = this.pdfiumModule.FPDFDoc_GetAttachment(docPtr, index);
3642
+ const name = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
3643
+ return this.pdfiumModule.FPDFAttachment_GetName(attachmentPtr, buffer, bufferLength);
3644
+ }, this.pdfiumModule.pdfium.UTF16ToString);
3645
+ const creationDate = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
3646
+ return this.pdfiumModule.FPDFAttachment_GetStringValue(attachmentPtr, 'CreationDate', buffer, bufferLength);
3647
+ }, this.pdfiumModule.pdfium.UTF16ToString);
3648
+ const checksum = readString(this.pdfiumModule.pdfium, (buffer, bufferLength) => {
3649
+ return this.pdfiumModule.FPDFAttachment_GetStringValue(attachmentPtr, 'Checksum', buffer, bufferLength);
3650
+ }, this.pdfiumModule.pdfium.UTF16ToString);
3651
+ return {
3652
+ index,
3653
+ name,
3654
+ creationDate,
3655
+ checksum,
3656
+ };
3657
+ }
3658
+ /**
3659
+ * Convert coordinate of point from device coordinate to page coordinate
3660
+ * @param page - pdf page infor
3661
+ * @param position - position of point
3662
+ * @returns converted position
3663
+ *
3664
+ * @private
3665
+ */
3666
+ convertDevicePointToPagePoint(page, position) {
3667
+ const x = position.x;
3668
+ const y = page.size.height - position.y;
3669
+ return { x, y };
3670
+ }
3671
+ /**
3672
+ * Convert coordinate of point from page coordinate to device coordinate
3673
+ * @param page - pdf page infor
3674
+ * @param position - position of point
3675
+ * @returns converted position
3676
+ *
3677
+ * @private
3678
+ */
3679
+ convertPagePointToDevicePoint(page, position) {
3680
+ const x = position.x;
3681
+ const y = page.size.height - position.y;
3682
+ return { x, y };
3683
+ }
3684
+ /**
3685
+ * Convert coordinate of rectangle from page coordinate to device coordinate
3686
+ * @param page - pdf page infor
3687
+ * @param pagePtr - pointer to pdf page object
3688
+ * @param pageRect - rectangle that needs to be converted
3689
+ * @returns converted rectangle
3690
+ *
3691
+ * @private
3692
+ */
3693
+ convertPageRectToDeviceRect(page, pagePtr, pageRect) {
3694
+ const { x, y } = this.convertPagePointToDevicePoint(page, {
3695
+ x: pageRect.left,
3696
+ y: pageRect.top,
3697
+ });
3698
+ const rect = {
3699
+ origin: {
3700
+ x,
3701
+ y,
3702
+ },
3703
+ size: {
3704
+ width: Math.abs(pageRect.right - pageRect.left),
3705
+ height: Math.abs(pageRect.top - pageRect.bottom),
3706
+ },
3707
+ };
3708
+ return rect;
3709
+ }
3710
+ /**
3711
+ * Read the appearance stream of annotation
3712
+ * @param annotationPtr - pointer to pdf annotation
3713
+ * @param mode - appearance mode
3714
+ * @returns appearance stream
3715
+ *
3716
+ * @private
3717
+ */
3718
+ readPageAnnoAppearanceStreams(annotationPtr) {
3719
+ return {
3720
+ normal: this.readPageAnnoAppearanceStream(annotationPtr, models.AppearanceMode.Normal),
3721
+ rollover: this.readPageAnnoAppearanceStream(annotationPtr, models.AppearanceMode.Rollover),
3722
+ down: this.readPageAnnoAppearanceStream(annotationPtr, models.AppearanceMode.Down),
3723
+ };
3724
+ }
3725
+ /**
3726
+ * Read the appearance stream of annotation
3727
+ * @param annotationPtr - pointer to pdf annotation
3728
+ * @param mode - appearance mode
3729
+ * @returns appearance stream
3730
+ *
3731
+ * @private
3732
+ */
3733
+ readPageAnnoAppearanceStream(annotationPtr, mode = models.AppearanceMode.Normal) {
3734
+ const utf16Length = this.pdfiumModule.FPDFAnnot_GetAP(annotationPtr, mode, 0, 0);
3735
+ const bytesCount = (utf16Length + 1) * 2; // include NIL
3736
+ const bufferPtr = this.malloc(bytesCount);
3737
+ this.pdfiumModule.FPDFAnnot_GetAP(annotationPtr, mode, bufferPtr, bytesCount);
3738
+ const ap = this.pdfiumModule.pdfium.UTF16ToString(bufferPtr);
3739
+ this.free(bufferPtr);
3740
+ return ap;
3741
+ }
3742
+ /**
3743
+ * Set the rect of specified annotation
3744
+ * @param page - page info that the annotation is belonged to
3745
+ * @param pagePtr - pointer of page object
3746
+ * @param annotationPtr - pointer to annotation object
3747
+ * @param rect - target rectangle
3748
+ * @returns whether the rect is setted
3749
+ *
3750
+ * @private
3751
+ */
3752
+ setPageAnnoRect(page, pagePtr, annotationPtr, rect) {
3753
+ const pageXPtr = this.malloc(8);
3754
+ const pageYPtr = this.malloc(8);
3755
+ if (!this.pdfiumModule.FPDF_DeviceToPage(pagePtr, 0, 0, page.size.width, page.size.height, 0, rect.origin.x, rect.origin.y, pageXPtr, pageYPtr)) {
3756
+ this.free(pageXPtr);
3757
+ this.free(pageYPtr);
3758
+ return false;
3759
+ }
3760
+ const pageX = this.pdfiumModule.pdfium.getValue(pageXPtr, 'double');
3761
+ const pageY = this.pdfiumModule.pdfium.getValue(pageYPtr, 'double');
3762
+ this.free(pageXPtr);
3763
+ this.free(pageYPtr);
3764
+ const pageRectPtr = this.malloc(4 * 4);
3765
+ this.pdfiumModule.pdfium.setValue(pageRectPtr, pageX, 'float');
3766
+ this.pdfiumModule.pdfium.setValue(pageRectPtr + 4, pageY, 'float');
3767
+ this.pdfiumModule.pdfium.setValue(pageRectPtr + 8, pageX + rect.size.width, 'float');
3768
+ this.pdfiumModule.pdfium.setValue(pageRectPtr + 12, pageY - rect.size.height, 'float');
3769
+ if (!this.pdfiumModule.FPDFAnnot_SetRect(annotationPtr, pageRectPtr)) {
3770
+ this.free(pageRectPtr);
3771
+ return false;
3772
+ }
3773
+ this.free(pageRectPtr);
3774
+ return true;
3775
+ }
3776
+ /**
3777
+ * Read the rectangle of annotation
3778
+ * @param annotationPtr - pointer to pdf annotation
3779
+ * @returns rectangle of annotation
3780
+ *
3781
+ * @private
3782
+ */
3783
+ readPageAnnoRect(annotationPtr) {
3784
+ const pageRectPtr = this.malloc(4 * 4);
3785
+ const pageRect = {
3786
+ left: 0,
3787
+ top: 0,
3788
+ right: 0,
3789
+ bottom: 0,
3790
+ };
3791
+ if (this.pdfiumModule.FPDFAnnot_GetRect(annotationPtr, pageRectPtr)) {
3792
+ pageRect.left = this.pdfiumModule.pdfium.getValue(pageRectPtr, 'float');
3793
+ pageRect.top = this.pdfiumModule.pdfium.getValue(pageRectPtr + 4, 'float');
3794
+ pageRect.right = this.pdfiumModule.pdfium.getValue(pageRectPtr + 8, 'float');
3795
+ pageRect.bottom = this.pdfiumModule.pdfium.getValue(pageRectPtr + 12, 'float');
3796
+ }
3797
+ this.free(pageRectPtr);
3798
+ return pageRect;
3799
+ }
3800
+ /**
3801
+ * Get highlight rects for a specific character range (for search highlighting)
3802
+ * @param page - pdf page info
3803
+ * @param pagePtr - pointer to pdf page
3804
+ * @param textPagePtr - pointer to pdf text page
3805
+ * @param startIndex - starting character index
3806
+ * @param charCount - number of characters in the range
3807
+ * @returns array of rectangles for highlighting the specified character range
3808
+ *
3809
+ * @private
3810
+ */
3811
+ getHighlightRects(page, pagePtr, textPagePtr, startIndex, charCount) {
3812
+ const rectsCount = this.pdfiumModule.FPDFText_CountRects(textPagePtr, startIndex, charCount);
3813
+ const highlightRects = [];
3814
+ for (let i = 0; i < rectsCount; i++) {
3815
+ const topPtr = this.malloc(8);
3816
+ const leftPtr = this.malloc(8);
3817
+ const rightPtr = this.malloc(8);
3818
+ const bottomPtr = this.malloc(8);
3819
+ const isSucceed = this.pdfiumModule.FPDFText_GetRect(textPagePtr, i, leftPtr, topPtr, rightPtr, bottomPtr);
3820
+ if (!isSucceed) {
3821
+ this.free(leftPtr);
3822
+ this.free(topPtr);
3823
+ this.free(rightPtr);
3824
+ this.free(bottomPtr);
3825
+ continue;
3826
+ }
3827
+ const left = this.pdfiumModule.pdfium.getValue(leftPtr, 'double');
3828
+ const top = this.pdfiumModule.pdfium.getValue(topPtr, 'double');
3829
+ const right = this.pdfiumModule.pdfium.getValue(rightPtr, 'double');
3830
+ const bottom = this.pdfiumModule.pdfium.getValue(bottomPtr, 'double');
3831
+ this.free(leftPtr);
3832
+ this.free(topPtr);
3833
+ this.free(rightPtr);
3834
+ this.free(bottomPtr);
3835
+ const deviceXPtr = this.malloc(4);
3836
+ const deviceYPtr = this.malloc(4);
3837
+ this.pdfiumModule.FPDF_PageToDevice(pagePtr, 0, 0, page.size.width, page.size.height, 0, left, top, deviceXPtr, deviceYPtr);
3838
+ const x = this.pdfiumModule.pdfium.getValue(deviceXPtr, 'i32');
3839
+ const y = this.pdfiumModule.pdfium.getValue(deviceYPtr, 'i32');
3840
+ this.free(deviceXPtr);
3841
+ this.free(deviceYPtr);
3842
+ // Convert the bottom-right coordinates to width/height
3843
+ const width = Math.ceil(Math.abs(right - left));
3844
+ const height = Math.ceil(Math.abs(top - bottom));
3845
+ highlightRects.push({
3846
+ origin: { x, y },
3847
+ size: { width, height },
3848
+ });
3849
+ }
3850
+ return highlightRects;
3851
+ }
3852
+ /**
3853
+ * Search for a keyword across all pages in the document
3854
+ * Returns all search results throughout the entire document
3855
+ *
3856
+ * @param doc - Pdf document object
3857
+ * @param keyword - Search keyword
3858
+ * @param flags - Match flags for search
3859
+ * @returns Promise of all search results in the document
3860
+ *
3861
+ * @public
3862
+ */
3863
+ searchAllPages(doc, keyword, flags = []) {
3864
+ this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'searchAllPages', doc, keyword, flags);
3865
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SearchAllPages`, 'Begin', doc.id);
3866
+ const ctx = this.cache.getContext(doc.id);
3867
+ if (!ctx) {
3868
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SearchAllPages`, 'End', doc.id);
3869
+ return models.PdfTaskHelper.resolve({ results: [], total: 0 });
3870
+ }
3871
+ const length = 2 * (keyword.length + 1);
3872
+ const keywordPtr = this.malloc(length);
3873
+ this.pdfiumModule.pdfium.stringToUTF16(keyword, keywordPtr, length);
3874
+ const flag = flags.reduce((flag, currFlag) => {
3875
+ return flag | currFlag;
3876
+ }, models.MatchFlag.None);
3877
+ const results = [];
3878
+ // Search through all pages
3879
+ const searchAllPagesTask = models.PdfTaskHelper.create();
3880
+ // Execute search in a separate function to avoid issues with resolve parameter
3881
+ const executeSearch = async () => {
3882
+ for (let pageIndex = 0; pageIndex < doc.pageCount; pageIndex++) {
3883
+ // Get all results for the current page efficiently (load page only once)
3884
+ const pageResults = this.searchAllInPage(ctx, doc.pages[pageIndex], keywordPtr, flag);
3885
+ results.push(...pageResults);
3886
+ }
3887
+ this.free(keywordPtr);
3888
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SearchAllPages`, 'End', doc.id);
3889
+ searchAllPagesTask.resolve({
3890
+ results,
3891
+ total: results.length,
3892
+ });
3893
+ };
3894
+ // Start the search process
3895
+ executeSearch().catch((error) => {
3896
+ this.free(keywordPtr);
3897
+ this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `SearchAllPages`, 'End', doc.id);
3898
+ searchAllPagesTask.reject({
3899
+ code: models.PdfErrorCode.Unknown,
3900
+ message: `Error searching document: ${error}`,
3901
+ });
3902
+ });
3903
+ return searchAllPagesTask;
3904
+ }
3905
+ /**
3906
+ * Extract word-aligned context for a search hit.
3907
+ *
3908
+ * @param fullText full UTF-16 page text (fetch this once per page!)
3909
+ * @param start index of 1st char that matched
3910
+ * @param count number of chars in the match
3911
+ * @param windowChars minimum context chars to keep left & right
3912
+ */
3913
+ buildContext(fullText, start, count, windowChars = 30) {
3914
+ const WORD_BREAK = /[\s\u00A0.,;:!?()\[\]{}<>/\\\-"'`"”\u2013\u2014]/;
3915
+ // Find the start of a word moving left
3916
+ const findWordStart = (index) => {
3917
+ while (index > 0 && !WORD_BREAK.test(fullText[index - 1]))
3918
+ index--;
3919
+ return index;
3920
+ };
3921
+ // Find the end of a word moving right
3922
+ const findWordEnd = (index) => {
3923
+ while (index < fullText.length && !WORD_BREAK.test(fullText[index]))
3924
+ index++;
3925
+ return index;
3926
+ };
3927
+ // Move left to build context
3928
+ let left = start;
3929
+ while (left > 0 && WORD_BREAK.test(fullText[left - 1]))
3930
+ left--; // Skip blanks
3931
+ let collected = 0;
3932
+ while (left > 0 && collected < windowChars) {
3933
+ left--;
3934
+ if (!WORD_BREAK.test(fullText[left]))
3935
+ collected++;
3936
+ }
3937
+ left = findWordStart(left);
3938
+ // Move right to build context
3939
+ let right = start + count;
3940
+ while (right < fullText.length && WORD_BREAK.test(fullText[right]))
3941
+ right++; // Skip blanks
3942
+ collected = 0;
3943
+ while (right < fullText.length && collected < windowChars) {
3944
+ if (!WORD_BREAK.test(fullText[right]))
3945
+ collected++;
3946
+ right++;
3947
+ }
3948
+ right = findWordEnd(right);
3949
+ // Compose the context
3950
+ const before = fullText.slice(left, start).replace(/\s+/g, ' ').trimStart();
3951
+ const match = fullText.slice(start, start + count);
3952
+ const after = fullText
3953
+ .slice(start + count, right)
3954
+ .replace(/\s+/g, ' ')
3955
+ .trimEnd();
3956
+ return {
3957
+ before: this.tidy(before),
3958
+ match: this.tidy(match),
3959
+ after: this.tidy(after),
3960
+ truncatedLeft: left > 0,
3961
+ truncatedRight: right < fullText.length,
3962
+ };
3963
+ }
3964
+ /**
3965
+ * Tidy the text to remove any non-printable characters and whitespace
3966
+ * @param s - text to tidy
3967
+ * @returns tidied text
3968
+ *
3969
+ * @private
3970
+ */
3971
+ tidy(s) {
3972
+ return (s
3973
+ /* 1️⃣ join words split by hyphen + U+FFFE + whitespace */
3974
+ .replace(/-\uFFFE\s*/g, '')
3975
+ /* 2️⃣ drop any remaining U+FFFE, soft-hyphen, zero-width chars */
3976
+ .replace(/[\uFFFE\u00AD\u200B\u2060\uFEFF]/g, '')
3977
+ /* 3️⃣ collapse whitespace so we stay on one line */
3978
+ .replace(/\s+/g, ' '));
3979
+ }
3980
+ /**
3981
+ * Search for all occurrences of a keyword on a single page
3982
+ * This method efficiently loads the page only once and finds all matches
3983
+ *
3984
+ * @param docPtr - pointer to pdf document
3985
+ * @param page - pdf page object
3986
+ * @param pageIndex - index of the page
3987
+ * @param keywordPtr - pointer to the search keyword
3988
+ * @param flag - search flags
3989
+ * @returns array of search results on this page
3990
+ *
3991
+ * @private
3992
+ */
3993
+ searchAllInPage(ctx, page, keywordPtr, flag) {
3994
+ const pageIndex = page.index;
3995
+ // Load the page and text page only once
3996
+ const pageCtx = ctx.acquirePage(pageIndex);
3997
+ const textPagePtr = pageCtx.getTextPage();
3998
+ // Load the full text of the page once
3999
+ const total = this.pdfiumModule.FPDFText_CountChars(textPagePtr);
4000
+ const bufPtr = this.malloc(2 * (total + 1));
4001
+ this.pdfiumModule.FPDFText_GetText(textPagePtr, 0, total, bufPtr);
4002
+ const fullText = this.pdfiumModule.pdfium.UTF16ToString(bufPtr);
4003
+ this.free(bufPtr);
4004
+ const pageResults = [];
4005
+ // Initialize search handle once for the page
4006
+ const searchHandle = this.pdfiumModule.FPDFText_FindStart(textPagePtr, keywordPtr, flag, 0);
4007
+ // Call FindNext repeatedly to get all matches on the page
4008
+ while (this.pdfiumModule.FPDFText_FindNext(searchHandle)) {
4009
+ const charIndex = this.pdfiumModule.FPDFText_GetSchResultIndex(searchHandle);
4010
+ const charCount = this.pdfiumModule.FPDFText_GetSchCount(searchHandle);
4011
+ const rects = this.getHighlightRects(page, pageCtx.pagePtr, textPagePtr, charIndex, charCount);
4012
+ const context = this.buildContext(fullText, charIndex, charCount);
4013
+ pageResults.push({
4014
+ pageIndex,
4015
+ charIndex,
4016
+ charCount,
4017
+ rects,
4018
+ context,
4019
+ });
4020
+ }
4021
+ // Close the search handle only once after finding all results
4022
+ this.pdfiumModule.FPDFText_FindClose(searchHandle);
4023
+ // Close the text page and page only once
4024
+ pageCtx.release();
4025
+ return pageResults;
4026
+ }
4027
+ }
4028
+
4029
+ async function createPdfiumEngine(wasmUrl) {
4030
+ const response = await fetch(wasmUrl);
4031
+ const wasmBinary = await response.arrayBuffer();
4032
+ const wasmModule = await pdfium.init({ wasmBinary });
4033
+ return new PdfiumEngine(wasmModule);
4034
+ }
4035
+
4036
+ exports.createPdfiumEngine = createPdfiumEngine;
4037
+ //# sourceMappingURL=pdfium-direct-engine.cjs.map