@lexmata/micropdf 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +985 -0
  3. package/binding.gyp +73 -0
  4. package/dist/annot.d.ts +458 -0
  5. package/dist/annot.d.ts.map +1 -0
  6. package/dist/annot.js +697 -0
  7. package/dist/annot.js.map +1 -0
  8. package/dist/archive.d.ts +128 -0
  9. package/dist/archive.d.ts.map +1 -0
  10. package/dist/archive.js +268 -0
  11. package/dist/archive.js.map +1 -0
  12. package/dist/buffer.d.ts +572 -0
  13. package/dist/buffer.d.ts.map +1 -0
  14. package/dist/buffer.js +971 -0
  15. package/dist/buffer.js.map +1 -0
  16. package/dist/colorspace.d.ts +287 -0
  17. package/dist/colorspace.d.ts.map +1 -0
  18. package/dist/colorspace.js +542 -0
  19. package/dist/colorspace.js.map +1 -0
  20. package/dist/context.d.ts +184 -0
  21. package/dist/context.d.ts.map +1 -0
  22. package/dist/context.js +320 -0
  23. package/dist/context.js.map +1 -0
  24. package/dist/cookie.d.ts +164 -0
  25. package/dist/cookie.d.ts.map +1 -0
  26. package/dist/cookie.js +306 -0
  27. package/dist/cookie.js.map +1 -0
  28. package/dist/device.d.ts +169 -0
  29. package/dist/device.d.ts.map +1 -0
  30. package/dist/device.js +350 -0
  31. package/dist/device.js.map +1 -0
  32. package/dist/display-list.d.ts +202 -0
  33. package/dist/display-list.d.ts.map +1 -0
  34. package/dist/display-list.js +410 -0
  35. package/dist/display-list.js.map +1 -0
  36. package/dist/document.d.ts +637 -0
  37. package/dist/document.d.ts.map +1 -0
  38. package/dist/document.js +902 -0
  39. package/dist/document.js.map +1 -0
  40. package/dist/easy.d.ts +423 -0
  41. package/dist/easy.d.ts.map +1 -0
  42. package/dist/easy.js +644 -0
  43. package/dist/easy.js.map +1 -0
  44. package/dist/enhanced.d.ts +226 -0
  45. package/dist/enhanced.d.ts.map +1 -0
  46. package/dist/enhanced.js +368 -0
  47. package/dist/enhanced.js.map +1 -0
  48. package/dist/filter.d.ts +51 -0
  49. package/dist/filter.d.ts.map +1 -0
  50. package/dist/filter.js +381 -0
  51. package/dist/filter.js.map +1 -0
  52. package/dist/font.d.ts +222 -0
  53. package/dist/font.d.ts.map +1 -0
  54. package/dist/font.js +381 -0
  55. package/dist/font.js.map +1 -0
  56. package/dist/form.d.ts +214 -0
  57. package/dist/form.d.ts.map +1 -0
  58. package/dist/form.js +497 -0
  59. package/dist/form.js.map +1 -0
  60. package/dist/geometry.d.ts +469 -0
  61. package/dist/geometry.d.ts.map +1 -0
  62. package/dist/geometry.js +780 -0
  63. package/dist/geometry.js.map +1 -0
  64. package/dist/image.d.ts +172 -0
  65. package/dist/image.d.ts.map +1 -0
  66. package/dist/image.js +348 -0
  67. package/dist/image.js.map +1 -0
  68. package/dist/index.d.ts +171 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +339 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/link.d.ts +168 -0
  73. package/dist/link.d.ts.map +1 -0
  74. package/dist/link.js +343 -0
  75. package/dist/link.js.map +1 -0
  76. package/dist/micropdf.d.ts +40 -0
  77. package/dist/micropdf.d.ts.map +1 -0
  78. package/dist/micropdf.js +45 -0
  79. package/dist/micropdf.js.map +1 -0
  80. package/dist/nanopdf.d.ts +40 -0
  81. package/dist/nanopdf.d.ts.map +1 -0
  82. package/dist/nanopdf.js +45 -0
  83. package/dist/nanopdf.js.map +1 -0
  84. package/dist/native.d.ts +242 -0
  85. package/dist/native.d.ts.map +1 -0
  86. package/dist/native.js +509 -0
  87. package/dist/native.js.map +1 -0
  88. package/dist/output.d.ts +166 -0
  89. package/dist/output.d.ts.map +1 -0
  90. package/dist/output.js +365 -0
  91. package/dist/output.js.map +1 -0
  92. package/dist/path.d.ts +420 -0
  93. package/dist/path.d.ts.map +1 -0
  94. package/dist/path.js +687 -0
  95. package/dist/path.js.map +1 -0
  96. package/dist/pdf/object.d.ts +489 -0
  97. package/dist/pdf/object.d.ts.map +1 -0
  98. package/dist/pdf/object.js +1045 -0
  99. package/dist/pdf/object.js.map +1 -0
  100. package/dist/pixmap.d.ts +315 -0
  101. package/dist/pixmap.d.ts.map +1 -0
  102. package/dist/pixmap.js +590 -0
  103. package/dist/pixmap.js.map +1 -0
  104. package/dist/profiler.d.ts +159 -0
  105. package/dist/profiler.d.ts.map +1 -0
  106. package/dist/profiler.js +380 -0
  107. package/dist/profiler.js.map +1 -0
  108. package/dist/render-options.d.ts +227 -0
  109. package/dist/render-options.d.ts.map +1 -0
  110. package/dist/render-options.js +130 -0
  111. package/dist/render-options.js.map +1 -0
  112. package/dist/resource-tracking.d.ts +332 -0
  113. package/dist/resource-tracking.d.ts.map +1 -0
  114. package/dist/resource-tracking.js +653 -0
  115. package/dist/resource-tracking.js.map +1 -0
  116. package/dist/simple.d.ts +276 -0
  117. package/dist/simple.d.ts.map +1 -0
  118. package/dist/simple.js +343 -0
  119. package/dist/simple.js.map +1 -0
  120. package/dist/stext.d.ts +290 -0
  121. package/dist/stext.d.ts.map +1 -0
  122. package/dist/stext.js +312 -0
  123. package/dist/stext.js.map +1 -0
  124. package/dist/stream.d.ts +174 -0
  125. package/dist/stream.d.ts.map +1 -0
  126. package/dist/stream.js +476 -0
  127. package/dist/stream.js.map +1 -0
  128. package/dist/text.d.ts +337 -0
  129. package/dist/text.d.ts.map +1 -0
  130. package/dist/text.js +454 -0
  131. package/dist/text.js.map +1 -0
  132. package/dist/typed-arrays.d.ts +127 -0
  133. package/dist/typed-arrays.d.ts.map +1 -0
  134. package/dist/typed-arrays.js +410 -0
  135. package/dist/typed-arrays.js.map +1 -0
  136. package/dist/types.d.ts +358 -0
  137. package/dist/types.d.ts.map +1 -0
  138. package/dist/types.js +216 -0
  139. package/dist/types.js.map +1 -0
  140. package/native/annot.cc +557 -0
  141. package/native/buffer.cc +204 -0
  142. package/native/colorspace.cc +166 -0
  143. package/native/context.cc +84 -0
  144. package/native/cookie.cc +179 -0
  145. package/native/device.cc +179 -0
  146. package/native/display_list.cc +179 -0
  147. package/native/document.cc +268 -0
  148. package/native/enhanced.cc +70 -0
  149. package/native/font.cc +282 -0
  150. package/native/form.cc +523 -0
  151. package/native/geometry.cc +255 -0
  152. package/native/image.cc +216 -0
  153. package/native/include/micropdf/enhanced.h +38 -0
  154. package/native/include/micropdf/types.h +36 -0
  155. package/native/include/micropdf.h +106 -0
  156. package/native/include/mupdf-ffi.h +39 -0
  157. package/native/include/mupdf.h +11 -0
  158. package/native/include/mupdf_minimal.h +381 -0
  159. package/native/lib/linux-x64/libmicropdf.a +0 -0
  160. package/native/link.cc +234 -0
  161. package/native/micropdf.cc +71 -0
  162. package/native/output.cc +229 -0
  163. package/native/page.cc +572 -0
  164. package/native/path.cc +259 -0
  165. package/native/pixmap.cc +240 -0
  166. package/native/stext.cc +610 -0
  167. package/native/stream.cc +239 -0
  168. package/package.json +120 -0
  169. package/scripts/build-from-rust.js +97 -0
  170. package/scripts/install.js +184 -0
@@ -0,0 +1,902 @@
1
+ /**
2
+ * Document - PDF document handling
3
+ *
4
+ * This module provides the primary API for working with PDF documents. It handles
5
+ * document lifecycle, page access, metadata, security, and rendering operations.
6
+ *
7
+ * This implementation mirrors the Rust `fitz::document::Document` for 100% API compatibility.
8
+ *
9
+ * @module document
10
+ * @example
11
+ * ```typescript
12
+ * import { Document } from 'micropdf';
13
+ *
14
+ * // Open a PDF from a file
15
+ * const doc = Document.open('document.pdf');
16
+ *
17
+ * // Check if password is required
18
+ * if (doc.needsPassword()) {
19
+ * doc.authenticate('password');
20
+ * }
21
+ *
22
+ * // Get page count
23
+ * console.log(`Pages: ${doc.pageCount}`);
24
+ *
25
+ * // Load and render a page
26
+ * const page = doc.loadPage(0);
27
+ * const pixmap = page.toPixmap(Matrix.identity());
28
+ *
29
+ * // Extract text
30
+ * const text = page.extractText();
31
+ *
32
+ * // Clean up
33
+ * page.drop();
34
+ * doc.close();
35
+ * ```
36
+ */
37
+ import { Buffer } from './buffer.js';
38
+ import { Colorspace } from './colorspace.js';
39
+ import { Rect, Matrix, Quad } from './geometry.js';
40
+ import { native } from './native.js';
41
+ import { Pixmap } from './pixmap.js';
42
+ import { mergeRenderOptions, validateRenderOptions, dpiToScale } from './render-options.js';
43
+ import { MicroPDFError, LinkDestType } from './types.js';
44
+ /**
45
+ * An item in the document outline (table of contents / bookmarks).
46
+ *
47
+ * Outline items form a tree structure representing the document's navigation hierarchy.
48
+ * Each item can link to a page number, URI, or have child items.
49
+ *
50
+ * @class OutlineItem
51
+ * @example
52
+ * ```typescript
53
+ * const outline = doc.getOutline();
54
+ * for (const item of outline) {
55
+ * console.log(`${item.title} -> Page ${item.page}`);
56
+ * for (const child of item.children) {
57
+ * console.log(` ${child.title} -> Page ${child.page}`);
58
+ * }
59
+ * }
60
+ * ```
61
+ */
62
+ export class OutlineItem {
63
+ /**
64
+ * The title/label of this outline item.
65
+ * @readonly
66
+ * @type {string}
67
+ */
68
+ title;
69
+ /**
70
+ * The destination page number (0-indexed), if this item links to a page.
71
+ * Undefined if this item links to a URI instead.
72
+ * @readonly
73
+ * @type {number | undefined}
74
+ */
75
+ page;
76
+ /**
77
+ * The destination URI, if this item links to an external resource.
78
+ * Undefined if this item links to a page instead.
79
+ * @readonly
80
+ * @type {string | undefined}
81
+ */
82
+ uri;
83
+ /**
84
+ * Child outline items nested under this item.
85
+ * @readonly
86
+ * @type {OutlineItem[]}
87
+ */
88
+ children;
89
+ /**
90
+ * Creates a new outline item.
91
+ *
92
+ * @param {string} title - The title of the outline item
93
+ * @param {number} [page] - The destination page number (0-indexed)
94
+ * @param {string} [uri] - The destination URI
95
+ */
96
+ constructor(title, page, uri) {
97
+ this.title = title;
98
+ this.page = page;
99
+ this.uri = uri;
100
+ this.children = [];
101
+ }
102
+ }
103
+ /**
104
+ * A page in a PDF document.
105
+ *
106
+ * Represents a single page within a PDF document with methods for rendering,
107
+ * text extraction, search, and link retrieval. Pages are loaded from a Document
108
+ * using `Document.loadPage()`.
109
+ *
110
+ * **Important**: Pages must be explicitly freed using `drop()` when no longer needed
111
+ * to prevent memory leaks.
112
+ *
113
+ * @class Page
114
+ * @example
115
+ * ```typescript
116
+ * const doc = Document.open('document.pdf');
117
+ * const page = doc.loadPage(0); // Load first page
118
+ *
119
+ * try {
120
+ * // Get page dimensions
121
+ * console.log(`Size: ${page.bounds.width} x ${page.bounds.height}`);
122
+ * console.log(`Rotation: ${page.rotation}°`);
123
+ *
124
+ * // Extract text
125
+ * const text = page.extractText();
126
+ * console.log(text);
127
+ *
128
+ * // Search for text
129
+ * const hits = page.searchText('hello');
130
+ * console.log(`Found ${hits.length} occurrences`);
131
+ *
132
+ * // Render to pixmap
133
+ * const matrix = Matrix.scale(2, 2); // 2x zoom
134
+ * const pixmap = page.toPixmap(matrix);
135
+ * } finally {
136
+ * page.drop(); // Always clean up!
137
+ * }
138
+ *
139
+ * doc.close();
140
+ * ```
141
+ */
142
+ export class Page {
143
+ /** @internal - Native context handle */
144
+ _ctx;
145
+ /** @internal - Native page handle */
146
+ _page;
147
+ _pageNumber;
148
+ _bounds;
149
+ _mediaBox;
150
+ _rotation;
151
+ /** @internal */
152
+ constructor(_document, pageNumber, bounds, mediaBox, rotation, ctx, page) {
153
+ this._ctx = ctx;
154
+ this._page = page;
155
+ this._pageNumber = pageNumber;
156
+ this._bounds = bounds;
157
+ this._mediaBox = mediaBox;
158
+ this._rotation = rotation;
159
+ }
160
+ /**
161
+ * Get the page number (0-based)
162
+ */
163
+ get pageNumber() {
164
+ return this._pageNumber;
165
+ }
166
+ /**
167
+ * Get the page bounds
168
+ */
169
+ get bounds() {
170
+ return this._bounds;
171
+ }
172
+ /**
173
+ * Get the media box
174
+ */
175
+ get mediaBox() {
176
+ return this._mediaBox;
177
+ }
178
+ /**
179
+ * Get the crop box
180
+ */
181
+ get cropBox() {
182
+ // Default to media box if no crop box specified
183
+ return this._mediaBox;
184
+ }
185
+ /**
186
+ * Get the page rotation (0, 90, 180, or 270)
187
+ */
188
+ get rotation() {
189
+ return this._rotation;
190
+ }
191
+ /**
192
+ * Render the page to a pixmap using FFI
193
+ * @throws Error when native bindings are not available
194
+ */
195
+ toPixmap(matrix = Matrix.IDENTITY, colorspace = Colorspace.deviceRGB(), alpha = true) {
196
+ if (!this._ctx || !this._page) {
197
+ throw new Error('Page rendering requires native FFI bindings (fz_run_page, fz_new_bbox_device)');
198
+ }
199
+ const m = Matrix.from(matrix);
200
+ const nativeMatrix = {
201
+ a: m.a,
202
+ b: m.b,
203
+ c: m.c,
204
+ d: m.d,
205
+ e: m.e,
206
+ f: m.f
207
+ };
208
+ const nativeColorspace = {
209
+ name: colorspace.name,
210
+ n: colorspace.n,
211
+ type: colorspace.type.toString()
212
+ };
213
+ const nativePixmap = native.renderPage(this._ctx, this._page, nativeMatrix, nativeColorspace, alpha);
214
+ // Convert native pixmap to TypeScript Pixmap
215
+ return Pixmap.create(colorspace, nativePixmap.width, nativePixmap.height, alpha);
216
+ }
217
+ /**
218
+ * Render the page to PNG using FFI
219
+ * @throws Error when native bindings are not available
220
+ */
221
+ toPNG(dpi = 72) {
222
+ if (!this._ctx || !this._page) {
223
+ throw new Error('PNG encoding requires native FFI bindings (fz_save_pixmap_as_png)');
224
+ }
225
+ const nativeColorspace = {
226
+ name: 'DeviceRGB',
227
+ n: 3,
228
+ type: 'RGB'
229
+ };
230
+ const pngBuffer = native.renderPageToPNG(this._ctx, this._page, dpi, nativeColorspace);
231
+ return new Uint8Array(pngBuffer);
232
+ }
233
+ /**
234
+ * Render page with advanced options
235
+ *
236
+ * Provides fine-grained control over rendering quality, colorspace,
237
+ * anti-aliasing, and other rendering parameters.
238
+ *
239
+ * @param options - Rendering options
240
+ * @returns Pixmap containing the rendered page
241
+ * @throws Error if native bindings are not available
242
+ * @throws Error if options are invalid
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * // High-quality print rendering
247
+ * const pixmap = page.renderWithOptions({
248
+ * dpi: 300,
249
+ * colorspace: Colorspace.deviceRGB(),
250
+ * alpha: true,
251
+ * antiAlias: AntiAliasLevel.High
252
+ * });
253
+ *
254
+ * // Fast preview rendering
255
+ * const preview = page.renderWithOptions({
256
+ * dpi: 72,
257
+ * antiAlias: AntiAliasLevel.Low
258
+ * });
259
+ * ```
260
+ */
261
+ renderWithOptions(options = {}) {
262
+ if (!this._ctx || !this._page) {
263
+ throw new Error('Rendering requires native FFI bindings');
264
+ }
265
+ // Validate options
266
+ validateRenderOptions(options);
267
+ // Merge with defaults
268
+ const opts = mergeRenderOptions(options);
269
+ // Calculate transform matrix from DPI or use provided transform
270
+ const matrix = opts.transform
271
+ ? opts.transform
272
+ : Matrix.scale(dpiToScale(opts.dpi), dpiToScale(opts.dpi));
273
+ // Use existing toPixmap method with derived parameters
274
+ // TODO: In future, pass antiAlias level, renderAnnotations, renderFormFields through FFI
275
+ return this.toPixmap(matrix, opts.colorspace, opts.alpha);
276
+ }
277
+ /**
278
+ * Render page with progress tracking
279
+ *
280
+ * Extended rendering with progress callbacks and timeout support.
281
+ * Useful for long-running renders of complex pages.
282
+ *
283
+ * @param options - Extended rendering options with callbacks
284
+ * @returns Promise resolving to the rendered pixmap
285
+ * @throws Error if rendering fails or times out
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * const pixmap = await page.renderWithProgress({
290
+ * dpi: 600,
291
+ * onProgress: (current, total) => {
292
+ * console.log(`Rendering: ${Math.round(current/total * 100)}%`);
293
+ * return true; // Continue rendering
294
+ * },
295
+ * onError: (error) => {
296
+ * console.error('Render error:', error);
297
+ * },
298
+ * timeout: 30000 // 30 second timeout
299
+ * });
300
+ * ```
301
+ */
302
+ async renderWithProgress(options = {}) {
303
+ return new Promise((resolve, reject) => {
304
+ // const startTime = Date.now(); // TODO: Use for progress tracking
305
+ let timeoutId;
306
+ // Set up timeout if specified
307
+ if (options.timeout) {
308
+ timeoutId = setTimeout(() => {
309
+ const error = 'Rendering timeout exceeded';
310
+ if (options.onError) {
311
+ options.onError(error);
312
+ }
313
+ reject(new Error(error));
314
+ }, options.timeout);
315
+ }
316
+ try {
317
+ // For now, perform synchronous render
318
+ // TODO: Implement async rendering with FFI progress callbacks
319
+ const pixmap = this.renderWithOptions(options);
320
+ // Simulate progress callback
321
+ if (options.onProgress) {
322
+ options.onProgress(100, 100);
323
+ }
324
+ if (timeoutId) {
325
+ clearTimeout(timeoutId);
326
+ }
327
+ resolve(pixmap);
328
+ }
329
+ catch (error) {
330
+ if (timeoutId) {
331
+ clearTimeout(timeoutId);
332
+ }
333
+ const errorMsg = error instanceof Error ? error.message : String(error);
334
+ if (options.onError) {
335
+ options.onError(errorMsg);
336
+ }
337
+ reject(error);
338
+ }
339
+ });
340
+ }
341
+ /**
342
+ * Extract text from the page using FFI
343
+ * @throws Error when native bindings are not available
344
+ */
345
+ getText() {
346
+ if (!this._ctx || !this._page) {
347
+ throw new Error('Text extraction requires native FFI bindings (fz_new_stext_page_from_page)');
348
+ }
349
+ return native.extractText(this._ctx, this._page);
350
+ }
351
+ /**
352
+ * Get text blocks from the page using FFI
353
+ * @throws Error when native bindings are not available
354
+ */
355
+ getTextBlocks() {
356
+ if (!this._ctx || !this._page) {
357
+ throw new Error('Text block extraction requires native FFI bindings (fz_new_stext_page_from_page)');
358
+ }
359
+ const blocks = native.extractTextBlocks(this._ctx, this._page);
360
+ return blocks.map((block) => ({
361
+ text: block.text,
362
+ bbox: Rect.from(block.bbox),
363
+ lines: [] // Lines will be populated when FFI provides detailed text structure
364
+ }));
365
+ }
366
+ /**
367
+ * Get links from the page using FFI
368
+ * @throws Error when native bindings are not available
369
+ */
370
+ getLinks() {
371
+ if (!this._ctx || !this._page) {
372
+ throw new Error('Link extraction requires native FFI bindings (fz_load_links, pdf_annot_type)');
373
+ }
374
+ const links = native.getPageLinks(this._ctx, this._page);
375
+ return links.map((link) => {
376
+ const bounds = Rect.from(link.rect);
377
+ if (link.uri) {
378
+ return { bounds, dest: LinkDestType.URI, uri: link.uri };
379
+ }
380
+ else if (link.page !== undefined) {
381
+ return { bounds, dest: LinkDestType.Goto, page: link.page };
382
+ }
383
+ else {
384
+ return { bounds, dest: LinkDestType.None };
385
+ }
386
+ });
387
+ }
388
+ /**
389
+ * Search for text on the page using FFI
390
+ * @throws Error when native bindings are not available
391
+ */
392
+ search(needle) {
393
+ if (!this._ctx || !this._page) {
394
+ throw new Error('Text search requires native FFI bindings (fz_search_stext_page)');
395
+ }
396
+ const rects = native.searchText(this._ctx, this._page, needle, false);
397
+ // Convert Rects to Quads (each rect becomes a quad)
398
+ return rects.map((rect) => {
399
+ const r = Rect.from(rect);
400
+ return new Quad({ x: r.x0, y: r.y0 }, { x: r.x1, y: r.y0 }, { x: r.x0, y: r.y1 }, { x: r.x1, y: r.y1 });
401
+ });
402
+ }
403
+ /**
404
+ * Search for text on the page (alias for search)
405
+ */
406
+ searchText(needle) {
407
+ return this.search(needle);
408
+ }
409
+ /**
410
+ * Extract text from the page (alias for getText for MuPDF API compatibility)
411
+ */
412
+ extractText() {
413
+ return this.getText();
414
+ }
415
+ /**
416
+ * Extract text blocks from the page (alias for getTextBlocks for MuPDF API compatibility)
417
+ */
418
+ extractTextBlocks() {
419
+ return this.getTextBlocks();
420
+ }
421
+ /**
422
+ * Drop/release the page resources
423
+ */
424
+ drop() {
425
+ // Clean up native resources if available
426
+ if (this._ctx && this._page) {
427
+ // Native cleanup would go here
428
+ delete this._page;
429
+ delete this._ctx;
430
+ }
431
+ }
432
+ }
433
+ /**
434
+ * A PDF or other supported document format.
435
+ *
436
+ * The Document class is the main entry point for working with PDF files. It provides
437
+ * methods for opening documents from files or memory, accessing pages, checking security,
438
+ * reading metadata, and performing document-level operations.
439
+ *
440
+ * **Supported formats**: PDF, XPS, CBZ, and other formats supported by MuPDF.
441
+ *
442
+ * **Resource Management**: Documents must be explicitly closed using `close()` when
443
+ * done to free native resources and prevent memory leaks.
444
+ *
445
+ * @class Document
446
+ * @example
447
+ * ```typescript
448
+ * // Open from file
449
+ * const doc = Document.open('document.pdf');
450
+ *
451
+ * // Open from buffer
452
+ * const buffer = fs.readFileSync('document.pdf');
453
+ * const doc2 = Document.openFromBuffer(buffer);
454
+ *
455
+ * // Check basic info
456
+ * console.log(`Pages: ${doc.pageCount}`);
457
+ * console.log(`Title: ${doc.getMetadata('Title')}`);
458
+ * console.log(`Author: ${doc.getMetadata('Author')}`);
459
+ *
460
+ * // Handle password-protected PDFs
461
+ * if (doc.needsPassword()) {
462
+ * const success = doc.authenticate('password123');
463
+ * if (!success) {
464
+ * throw new Error('Invalid password');
465
+ * }
466
+ * }
467
+ *
468
+ * // Check permissions
469
+ * if (!doc.hasPermission(4)) { // FZ_PERMISSION_PRINT
470
+ * console.warn('Printing is not allowed');
471
+ * }
472
+ *
473
+ * // Work with pages
474
+ * for (let i = 0; i < doc.pageCount; i++) {
475
+ * const page = doc.loadPage(i);
476
+ * const text = page.extractText();
477
+ * console.log(`Page ${i + 1}: ${text.substring(0, 100)}...`);
478
+ * page.drop();
479
+ * }
480
+ *
481
+ * // Save modified document
482
+ * doc.save('output.pdf');
483
+ *
484
+ * // Always clean up
485
+ * doc.close();
486
+ * ```
487
+ *
488
+ * @example
489
+ * ```typescript
490
+ * // Using try-finally for proper cleanup
491
+ * const doc = Document.open('document.pdf');
492
+ * try {
493
+ * // Work with document
494
+ * const page = doc.loadPage(0);
495
+ * try {
496
+ * const text = page.extractText();
497
+ * console.log(text);
498
+ * } finally {
499
+ * page.drop();
500
+ * }
501
+ * } finally {
502
+ * doc.close();
503
+ * }
504
+ * ```
505
+ */
506
+ export class Document {
507
+ _ctx;
508
+ _doc;
509
+ _pages;
510
+ _format;
511
+ _metadata;
512
+ _outline;
513
+ _needsPassword;
514
+ _isAuthenticated;
515
+ constructor(_buffer, pages, format, needsPassword, isAuthenticated) {
516
+ this._pages = pages;
517
+ this._format = format;
518
+ this._needsPassword = needsPassword;
519
+ this._isAuthenticated = isAuthenticated;
520
+ this._metadata = new Map();
521
+ this._outline = [];
522
+ }
523
+ /**
524
+ * Open a document from a file path
525
+ * Uses native FFI bindings when available for full rendering support
526
+ */
527
+ static open(path, password) {
528
+ // Try native bindings first for full rendering support
529
+ try {
530
+ const ctx = native.createContext();
531
+ const nativeDoc = native.openDocumentFromPath(ctx, path);
532
+ const pageCount = native.countPages(ctx, nativeDoc);
533
+ // Parse PDF header for version info
534
+ const fs = require('node:fs');
535
+ const data = fs.readFileSync(path);
536
+ const header = new TextDecoder().decode(data.slice(0, Math.min(1024, data.length)));
537
+ const versionMatch = header.match(/%PDF-(\d+\.\d+)/);
538
+ const version = versionMatch ? versionMatch[1] : '1.4';
539
+ const format = `PDF ${version}`;
540
+ const doc = new Document(Buffer.fromBuffer(data), [], format, false, true);
541
+ doc._ctx = ctx;
542
+ doc._doc = nativeDoc;
543
+ // Load pages with native bounds
544
+ const pages = [];
545
+ for (let i = 0; i < pageCount; i++) {
546
+ const nativePage = native.loadPage(ctx, nativeDoc, i);
547
+ const bounds = native.boundPage(ctx, nativePage);
548
+ const rect = new Rect(bounds.x0, bounds.y0, bounds.x1, bounds.y1);
549
+ // Pass native handles through constructor
550
+ const page = new Page(doc, i, rect, rect, 0, ctx, nativePage);
551
+ pages.push(page);
552
+ }
553
+ doc._pages = pages;
554
+ // Authenticate if password provided
555
+ if (password !== undefined) {
556
+ doc.authenticate(password);
557
+ }
558
+ return doc;
559
+ }
560
+ catch {
561
+ // Fall back to pure TypeScript implementation
562
+ const fs = require('node:fs');
563
+ const data = fs.readFileSync(path);
564
+ return Document.fromBuffer(Buffer.fromBuffer(data), password);
565
+ }
566
+ }
567
+ /**
568
+ * Open a document from a buffer
569
+ * Uses native FFI bindings when available for full rendering support
570
+ */
571
+ static fromBuffer(buffer, password) {
572
+ // Parse PDF header to get version
573
+ const data = buffer.toUint8Array();
574
+ const header = new TextDecoder().decode(data.slice(0, Math.min(1024, data.length)));
575
+ // Check if it's a PDF
576
+ if (!header.startsWith('%PDF-')) {
577
+ throw MicroPDFError.argument('Not a PDF document');
578
+ }
579
+ // Extract version
580
+ const versionMatch = header.match(/%PDF-(\d+\.\d+)/);
581
+ const version = versionMatch ? versionMatch[1] : '1.4';
582
+ const format = `PDF ${version}`;
583
+ // Try native bindings first for full rendering support
584
+ try {
585
+ const ctx = native.createContext();
586
+ const nativeDoc = native.openDocument(ctx, globalThis.Buffer.from(data));
587
+ const pageCount = native.countPages(ctx, nativeDoc);
588
+ const doc = new Document(buffer, [], format, false, true);
589
+ doc._ctx = ctx;
590
+ doc._doc = nativeDoc;
591
+ // Load pages with native bounds
592
+ const pages = [];
593
+ for (let i = 0; i < pageCount; i++) {
594
+ const nativePage = native.loadPage(ctx, nativeDoc, i);
595
+ const bounds = native.boundPage(ctx, nativePage);
596
+ const rect = new Rect(bounds.x0, bounds.y0, bounds.x1, bounds.y1);
597
+ // Pass native handles through constructor
598
+ const page = new Page(doc, i, rect, rect, 0, ctx, nativePage);
599
+ pages.push(page);
600
+ }
601
+ doc._pages = pages;
602
+ // Authenticate if password provided
603
+ if (password !== undefined) {
604
+ doc.authenticate(password);
605
+ }
606
+ return doc;
607
+ }
608
+ catch {
609
+ // Fall back to pure TypeScript implementation
610
+ }
611
+ // Parse pages (simplified - just count /Type /Page objects)
612
+ const content = new TextDecoder().decode(data);
613
+ const pageMatches = content.match(/\/Type\s*\/Page\b/g);
614
+ const pageCount = pageMatches ? pageMatches.length : 0;
615
+ // Create placeholder pages
616
+ const pages = [];
617
+ const defaultBounds = new Rect(0, 0, 612, 792); // US Letter size
618
+ // Parse MediaBox if present
619
+ const mediaBoxMatch = content.match(/\/MediaBox\s*\[\s*([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\s*]/);
620
+ let mediaBox = defaultBounds;
621
+ if (mediaBoxMatch) {
622
+ mediaBox = new Rect(Number.parseFloat(mediaBoxMatch[1] ?? '0'), Number.parseFloat(mediaBoxMatch[2] ?? '0'), Number.parseFloat(mediaBoxMatch[3] ?? '612'), Number.parseFloat(mediaBoxMatch[4] ?? '792'));
623
+ }
624
+ const doc = new Document(buffer, [], format, false, true);
625
+ for (let i = 0; i < pageCount; i++) {
626
+ pages.push(new Page(doc, i, mediaBox, mediaBox, 0));
627
+ }
628
+ // Update pages array
629
+ doc._pages = pages;
630
+ // Try to authenticate if password provided
631
+ if (password !== undefined) {
632
+ doc.authenticate(password);
633
+ }
634
+ return doc;
635
+ }
636
+ /**
637
+ * Open a document from a Uint8Array
638
+ */
639
+ static fromUint8Array(data, password) {
640
+ return Document.fromBuffer(Buffer.fromUint8Array(data), password);
641
+ }
642
+ /**
643
+ * Get the page count
644
+ */
645
+ get pageCount() {
646
+ return this._pages.length;
647
+ }
648
+ /**
649
+ * Get the document format (e.g., "PDF 1.4")
650
+ */
651
+ get format() {
652
+ return this._format;
653
+ }
654
+ /**
655
+ * Check if the document needs a password
656
+ */
657
+ get needsPassword() {
658
+ return this._needsPassword;
659
+ }
660
+ /**
661
+ * Check if the document is authenticated
662
+ */
663
+ get isAuthenticated() {
664
+ return this._isAuthenticated;
665
+ }
666
+ /**
667
+ * Check if the document is a PDF
668
+ */
669
+ get isPDF() {
670
+ return this._format.startsWith('PDF');
671
+ }
672
+ /**
673
+ * Check if the document is reflowable (e.g., EPUB)
674
+ */
675
+ get isReflowable() {
676
+ return false; // PDFs are not reflowable
677
+ }
678
+ /**
679
+ * Authenticate with a password using FFI
680
+ * @returns true if authentication successful
681
+ * @throws Error when native bindings are not available
682
+ */
683
+ authenticate(password) {
684
+ if (!this._ctx || !this._doc) {
685
+ throw new Error('Authentication requires native FFI bindings (pdf_authenticate_password)');
686
+ }
687
+ const result = native.authenticatePassword(this._ctx, this._doc, password);
688
+ if (result) {
689
+ this._isAuthenticated = true;
690
+ }
691
+ return result;
692
+ }
693
+ /**
694
+ * Check if the document has a specific permission using FFI
695
+ * @param permission The permission to check (print, edit, copy, annotate)
696
+ * @throws Error when native bindings are not available
697
+ */
698
+ hasPermission(permission) {
699
+ if (!this._ctx || !this._doc) {
700
+ throw new Error('Permission check requires native FFI bindings (pdf_has_permission)');
701
+ }
702
+ // Map permission string to permission bits
703
+ const permissionMap = {
704
+ print: 4,
705
+ edit: 8,
706
+ copy: 16,
707
+ annotate: 32
708
+ };
709
+ const permissionBit = permissionMap[permission] ?? 0;
710
+ return native.hasPermission(this._ctx, this._doc, permissionBit);
711
+ }
712
+ /**
713
+ * Get page label for a given page number
714
+ * @param pageNum Page number (0-based)
715
+ * @returns Page label (e.g., "i", "ii", "1", "2", "A-1")
716
+ */
717
+ getPageLabel(pageNum) {
718
+ // Default to simple page numbering if no label scheme defined
719
+ return String(pageNum + 1);
720
+ }
721
+ /**
722
+ * Get page number from a page label
723
+ * @param label Page label to look up
724
+ * @returns Page number (0-based) or -1 if not found
725
+ */
726
+ getPageFromLabel(label) {
727
+ // Simple implementation: try to parse as number
728
+ const num = Number.parseInt(label, 10);
729
+ if (!isNaN(num) && num > 0 && num <= this.pageCount) {
730
+ return num - 1;
731
+ }
732
+ return -1;
733
+ }
734
+ /**
735
+ * Check if the document is valid (not corrupted)
736
+ */
737
+ isValid() {
738
+ return this._pages.length > 0;
739
+ }
740
+ /**
741
+ * Resolve a named destination to a page location using FFI
742
+ * @param name Named destination (e.g., "section1", "chapter2")
743
+ * @returns Page number (0-based) or undefined if not found
744
+ * @throws Error when native bindings are not available
745
+ */
746
+ resolveNamedDest(name) {
747
+ if (!this._ctx || !this._doc) {
748
+ throw new Error('Named destination lookup requires native FFI bindings (pdf_lookup_dest)');
749
+ }
750
+ const result = native.resolveLink(this._ctx, this._doc, name);
751
+ return result !== null ? result : undefined;
752
+ }
753
+ /**
754
+ * Count chapters in the document (for structured documents)
755
+ */
756
+ countChapters() {
757
+ // For PDFs without chapter structure, treat as single chapter
758
+ return 1;
759
+ }
760
+ /**
761
+ * Count pages in a specific chapter
762
+ * @param chapterIndex Chapter index (0-based)
763
+ */
764
+ countChapterPages(chapterIndex) {
765
+ if (chapterIndex === 0) {
766
+ return this.pageCount;
767
+ }
768
+ return 0;
769
+ }
770
+ /**
771
+ * Get page number from a chapter and page location
772
+ * @param chapter Chapter index (0-based)
773
+ * @param page Page within chapter (0-based)
774
+ */
775
+ pageNumberFromLocation(chapter, page) {
776
+ // Simple implementation: just return page for single chapter
777
+ if (chapter === 0) {
778
+ return page;
779
+ }
780
+ return -1;
781
+ }
782
+ /**
783
+ * Layout the document with specific width and height (for reflowable documents)
784
+ * @param width Target width
785
+ * @param height Target height
786
+ * @param em Font size in points
787
+ */
788
+ layout(_width, _height, _em = 12) {
789
+ // No-op for non-reflowable PDFs
790
+ // This is used for EPUB and other reflowable formats
791
+ }
792
+ /**
793
+ * Clone the document (create a copy)
794
+ * Note: This creates a shallow copy sharing the same data
795
+ */
796
+ clone() {
797
+ const cloned = Object.create(Document.prototype);
798
+ cloned._pages = this._pages;
799
+ cloned._format = this._format;
800
+ cloned._needsPassword = this._needsPassword;
801
+ cloned._isAuthenticated = this._isAuthenticated;
802
+ cloned._metadata = new Map(this._metadata);
803
+ cloned._outline = [...this._outline];
804
+ return cloned;
805
+ }
806
+ /**
807
+ * Get a page by index
808
+ */
809
+ getPage(index) {
810
+ if (index < 0 || index >= this._pages.length) {
811
+ throw MicroPDFError.argument(`Page index ${index} out of bounds (0..${this._pages.length})`);
812
+ }
813
+ return this._pages[index];
814
+ }
815
+ /**
816
+ * Load a page by index (alias for getPage for MuPDF API compatibility)
817
+ */
818
+ loadPage(index) {
819
+ return this.getPage(index);
820
+ }
821
+ /**
822
+ * Check if the document needs a password (method form for API compatibility)
823
+ */
824
+ needsPasswordCheck() {
825
+ return this._needsPassword;
826
+ }
827
+ /**
828
+ * Close the document and release resources
829
+ */
830
+ close() {
831
+ // Clean up native resources if available
832
+ if (this._ctx && this._doc) {
833
+ // Native cleanup would go here
834
+ delete this._doc;
835
+ delete this._ctx;
836
+ }
837
+ this._pages = [];
838
+ }
839
+ /**
840
+ * Iterate over all pages
841
+ */
842
+ *pages() {
843
+ for (const page of this._pages) {
844
+ yield page;
845
+ }
846
+ }
847
+ /**
848
+ * Get the document outline (table of contents)
849
+ */
850
+ getOutline() {
851
+ return this._outline;
852
+ }
853
+ /**
854
+ * Get metadata value
855
+ */
856
+ getMetadata(key) {
857
+ return this._metadata.get(key);
858
+ }
859
+ /**
860
+ * Set metadata value
861
+ */
862
+ setMetadata(key, value) {
863
+ this._metadata.set(key, value);
864
+ }
865
+ /**
866
+ * Get the title
867
+ */
868
+ get title() {
869
+ return this._metadata.get('info:Title');
870
+ }
871
+ /**
872
+ * Get the author
873
+ */
874
+ get author() {
875
+ return this._metadata.get('info:Author');
876
+ }
877
+ /**
878
+ * Get the subject
879
+ */
880
+ get subject() {
881
+ return this._metadata.get('info:Subject');
882
+ }
883
+ /**
884
+ * Get the keywords
885
+ */
886
+ get keywords() {
887
+ return this._metadata.get('info:Keywords');
888
+ }
889
+ /**
890
+ * Get the creator application
891
+ */
892
+ get creator() {
893
+ return this._metadata.get('info:Creator');
894
+ }
895
+ /**
896
+ * Get the producer application
897
+ */
898
+ get producer() {
899
+ return this._metadata.get('info:Producer');
900
+ }
901
+ }
902
+ //# sourceMappingURL=document.js.map