pdf-oxide 0.3.24

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 (62) hide show
  1. package/README.md +218 -0
  2. package/binding.gyp +35 -0
  3. package/package.json +78 -0
  4. package/src/builders/annotation-builder.ts +367 -0
  5. package/src/builders/conversion-options-builder.ts +257 -0
  6. package/src/builders/index.ts +12 -0
  7. package/src/builders/metadata-builder.ts +317 -0
  8. package/src/builders/pdf-builder.ts +386 -0
  9. package/src/builders/search-options-builder.ts +151 -0
  10. package/src/document-editor-manager.ts +318 -0
  11. package/src/errors.ts +1629 -0
  12. package/src/form-field-manager.ts +666 -0
  13. package/src/hybrid-ml-manager.ts +283 -0
  14. package/src/index.ts +453 -0
  15. package/src/managers/accessibility-manager.ts +338 -0
  16. package/src/managers/annotation-manager.ts +439 -0
  17. package/src/managers/barcode-manager.ts +235 -0
  18. package/src/managers/batch-manager.ts +533 -0
  19. package/src/managers/cache-manager.ts +486 -0
  20. package/src/managers/compliance-manager.ts +375 -0
  21. package/src/managers/content-manager.ts +339 -0
  22. package/src/managers/document-utility-manager.ts +922 -0
  23. package/src/managers/dom-pdf-creator.ts +365 -0
  24. package/src/managers/editing-manager.ts +514 -0
  25. package/src/managers/enterprise-manager.ts +478 -0
  26. package/src/managers/extended-managers.ts +437 -0
  27. package/src/managers/extraction-manager.ts +583 -0
  28. package/src/managers/final-utilities.ts +429 -0
  29. package/src/managers/hybrid-ml-advanced.ts +479 -0
  30. package/src/managers/index.ts +239 -0
  31. package/src/managers/layer-manager.ts +500 -0
  32. package/src/managers/metadata-manager.ts +303 -0
  33. package/src/managers/ocr-manager.ts +756 -0
  34. package/src/managers/optimization-manager.ts +262 -0
  35. package/src/managers/outline-manager.ts +196 -0
  36. package/src/managers/page-manager.ts +289 -0
  37. package/src/managers/pattern-detection.ts +440 -0
  38. package/src/managers/rendering-manager.ts +863 -0
  39. package/src/managers/search-manager.ts +385 -0
  40. package/src/managers/security-manager.ts +345 -0
  41. package/src/managers/signature-manager.ts +1664 -0
  42. package/src/managers/streams.ts +618 -0
  43. package/src/managers/xfa-manager.ts +500 -0
  44. package/src/pdf-creator-manager.ts +494 -0
  45. package/src/properties.ts +522 -0
  46. package/src/result-accessors-manager.ts +867 -0
  47. package/src/tests/advanced-features.test.ts +414 -0
  48. package/src/tests/advanced.test.ts +266 -0
  49. package/src/tests/extended-managers.test.ts +316 -0
  50. package/src/tests/final-utilities.test.ts +455 -0
  51. package/src/tests/foundation.test.ts +315 -0
  52. package/src/tests/high-demand.test.ts +257 -0
  53. package/src/tests/specialized.test.ts +97 -0
  54. package/src/thumbnail-manager.ts +272 -0
  55. package/src/types/common.ts +142 -0
  56. package/src/types/document-types.ts +457 -0
  57. package/src/types/index.ts +6 -0
  58. package/src/types/manager-types.ts +284 -0
  59. package/src/types/native-bindings.ts +517 -0
  60. package/src/workers/index.ts +7 -0
  61. package/src/workers/pool.ts +274 -0
  62. package/src/workers/worker.ts +131 -0
@@ -0,0 +1,863 @@
1
+ /**
2
+ * Options for rendering pages to images
3
+ *
4
+ * Provides configurable settings for PDF page rendering including DPI,
5
+ * output format (PNG/JPEG), quality, and maximum dimensions.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const options = new RenderOptions({
10
+ * dpi: 300,
11
+ * format: 'png'
12
+ * });
13
+ * ```
14
+ */
15
+ export interface RenderOptionsConfig {
16
+ dpi?: number;
17
+ format?: 'png' | 'jpeg';
18
+ quality?: number;
19
+ maxWidth?: number | null;
20
+ maxHeight?: number | null;
21
+ }
22
+
23
+ export class RenderOptions {
24
+ dpi: number;
25
+ format: 'png' | 'jpeg';
26
+ quality: number;
27
+ maxWidth: number | null;
28
+ maxHeight: number | null;
29
+
30
+ /**
31
+ * Creates render options with defaults
32
+ * @param config - Configuration options
33
+ */
34
+ constructor(config: RenderOptionsConfig = {}) {
35
+ this.dpi = config.dpi ?? 150;
36
+ this.format = config.format ?? 'png';
37
+ this.quality = config.quality ?? 95;
38
+ this.maxWidth = config.maxWidth ?? null;
39
+ this.maxHeight = config.maxHeight ?? null;
40
+
41
+ this._validate();
42
+ }
43
+
44
+ /**
45
+ * Validates rendering options
46
+ * @private
47
+ */
48
+ private _validate(): void {
49
+ if (typeof this.dpi !== 'number' || this.dpi < 1 || this.dpi > 600) {
50
+ throw new Error('DPI must be between 1 and 600');
51
+ }
52
+
53
+ if (!['png', 'jpeg'].includes(this.format)) {
54
+ throw new Error("Format must be 'png' or 'jpeg'");
55
+ }
56
+
57
+ if (typeof this.quality !== 'number' || this.quality < 1 || this.quality > 100) {
58
+ throw new Error('Quality must be between 1 and 100');
59
+ }
60
+
61
+ if (this.maxWidth !== null && (typeof this.maxWidth !== 'number' || this.maxWidth < 1)) {
62
+ throw new Error('maxWidth must be a positive number');
63
+ }
64
+
65
+ if (this.maxHeight !== null && (typeof this.maxHeight !== 'number' || this.maxHeight < 1)) {
66
+ throw new Error('maxHeight must be a positive number');
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Merges options with defaults, handling null/undefined gracefully
72
+ * @param options - Options to merge
73
+ * @returns Merged options
74
+ * @static
75
+ */
76
+ static merge(options: RenderOptions | RenderOptionsConfig | null = null): RenderOptions {
77
+ if (options === null || options === undefined) {
78
+ return new RenderOptions();
79
+ }
80
+
81
+ if (options instanceof RenderOptions) {
82
+ return options;
83
+ }
84
+
85
+ // Handle plain object
86
+ return new RenderOptions(options);
87
+ }
88
+
89
+ /**
90
+ * Creates preset options for a quality level
91
+ * @param quality - Quality level: 'draft', 'normal', 'high'
92
+ * @returns Preset options
93
+ * @static
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const highQuality = RenderOptions.fromQuality('high');
98
+ * ```
99
+ */
100
+ static fromQuality(quality: 'draft' | 'normal' | 'high'): RenderOptions {
101
+ const presets: Record<string, RenderOptionsConfig> = {
102
+ draft: { dpi: 72, format: 'jpeg', quality: 70 },
103
+ normal: { dpi: 150, format: 'jpeg', quality: 85 },
104
+ high: { dpi: 300, format: 'png', quality: 95 },
105
+ };
106
+
107
+ if (!presets[quality]) {
108
+ throw new Error(`Invalid quality: ${quality}. Must be one of: ${Object.keys(presets).join(', ')}`);
109
+ }
110
+
111
+ return new RenderOptions(presets[quality]);
112
+ }
113
+
114
+ /**
115
+ * Converts to plain object for serialization
116
+ * @returns Plain object representation
117
+ */
118
+ toJSON(): Record<string, any> {
119
+ return {
120
+ dpi: this.dpi,
121
+ format: this.format,
122
+ quality: this.quality,
123
+ maxWidth: this.maxWidth,
124
+ maxHeight: this.maxHeight,
125
+ };
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Page dimensions information
131
+ */
132
+ export interface PageDimensions {
133
+ width: number;
134
+ height: number;
135
+ unit: string;
136
+ widthPts?: number;
137
+ heightPts?: number;
138
+ rotation?: number;
139
+ }
140
+
141
+ /**
142
+ * Page box information
143
+ */
144
+ export interface PageBox {
145
+ x: number;
146
+ y: number;
147
+ width: number;
148
+ height: number;
149
+ }
150
+
151
+ /**
152
+ * Rendering statistics
153
+ */
154
+ export interface RenderingStatistics {
155
+ totalFonts: number;
156
+ totalImages: number;
157
+ avgPageSize: number;
158
+ colorSpaceCount: number;
159
+ pageCount: number;
160
+ maxResolution: number;
161
+ }
162
+
163
+ /**
164
+ * Page resources
165
+ */
166
+ export interface PageResources {
167
+ fonts: any[];
168
+ images: any[];
169
+ colorSpaces: string[];
170
+ patterns: any[];
171
+ }
172
+
173
+ /**
174
+ * Manager for PDF rendering options and capabilities
175
+ *
176
+ * Provides methods to manage PDF rendering settings, page dimensions,
177
+ * color spaces, and rendering-related properties.
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * import { RenderingManager } from 'pdf_oxide';
182
+ *
183
+ * const doc = PdfDocument.open('document.pdf');
184
+ * const renderingManager = new RenderingManager(doc);
185
+ *
186
+ * // Get page dimensions
187
+ * const dimensions = renderingManager.getPageDimensions(0);
188
+ * console.log(`Page size: ${dimensions.width}x${dimensions.height} ${dimensions.unit}`);
189
+ *
190
+ * // Render page to PNG
191
+ * const path = await renderingManager.renderPageToFile(0, 'page.png');
192
+ * ```
193
+ */
194
+ export class RenderingManager {
195
+ private _document: any;
196
+ private _dimensionCache: Map<number, PageDimensions>;
197
+ private _resourceCache: Map<string, any>;
198
+ private _statisticsCache: RenderingStatistics | null;
199
+
200
+ /**
201
+ * Creates a new RenderingManager for the given document
202
+ * @param document - The PDF document
203
+ * @throws Error if document is null or undefined
204
+ */
205
+ constructor(document: any) {
206
+ if (!document) {
207
+ throw new Error('Document is required');
208
+ }
209
+ this._document = document;
210
+ // Performance optimization: cache rendering data
211
+ this._dimensionCache = new Map();
212
+ this._resourceCache = new Map();
213
+ this._statisticsCache = null;
214
+ }
215
+
216
+ /**
217
+ * Clears the rendering cache
218
+ * Useful when document content might have changed
219
+ */
220
+ clearCache(): void {
221
+ this._dimensionCache.clear();
222
+ this._resourceCache.clear();
223
+ this._statisticsCache = null;
224
+ }
225
+
226
+ /**
227
+ * Gets maximum resolution supported
228
+ * @returns Maximum DPI
229
+ */
230
+ getMaxResolution(): number {
231
+ return 300; // Standard high-quality PDF rendering DPI
232
+ }
233
+
234
+ /**
235
+ * Gets supported color spaces
236
+ * @returns Array of color space names
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const colorSpaces = manager.getSupportedColorSpaces();
241
+ * // ['RGB', 'CMYK', 'Grayscale', 'Lab']
242
+ * ```
243
+ */
244
+ getSupportedColorSpaces(): string[] {
245
+ return ['RGB', 'CMYK', 'Grayscale', 'Lab', 'Indexed'];
246
+ }
247
+
248
+ /**
249
+ * Gets dimensions of a page
250
+ * @param pageIndex - Zero-based page index
251
+ * @returns Page dimensions { width, height, unit }
252
+ * @throws Error if page index is invalid
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const dims = manager.getPageDimensions(0);
257
+ * console.log(`${dims.width}${dims.unit} x ${dims.height}${dims.unit}`);
258
+ * ```
259
+ */
260
+ getPageDimensions(pageIndex: number): PageDimensions {
261
+ if (typeof pageIndex !== 'number' || pageIndex < 0) {
262
+ throw new Error('Page index must be a non-negative number');
263
+ }
264
+
265
+ if (pageIndex >= this._document.pageCount) {
266
+ throw new Error(`Page index ${pageIndex} out of range`);
267
+ }
268
+
269
+ // Performance optimization: cache dimensions
270
+ if (this._dimensionCache.has(pageIndex)) {
271
+ return this._dimensionCache.get(pageIndex)!;
272
+ }
273
+
274
+ try {
275
+ // Try native method first (returns dimensions in points)
276
+ if (typeof this._document.getPageDimensions === 'function') {
277
+ const nativeDims = this._document.getPageDimensions(pageIndex);
278
+ // Convert from points (72 pts/inch) to inches
279
+ const dimensions: PageDimensions = {
280
+ width: nativeDims.width / 72,
281
+ height: nativeDims.height / 72,
282
+ unit: 'in',
283
+ widthPts: nativeDims.width,
284
+ heightPts: nativeDims.height,
285
+ };
286
+ this._dimensionCache.set(pageIndex, dimensions);
287
+ return dimensions;
288
+ }
289
+
290
+ // Fallback: standard letter dimensions
291
+ const dimensions: PageDimensions = {
292
+ width: 8.5,
293
+ height: 11,
294
+ unit: 'in',
295
+ widthPts: 612,
296
+ heightPts: 792,
297
+ };
298
+
299
+ this._dimensionCache.set(pageIndex, dimensions);
300
+ return dimensions;
301
+ } catch (error) {
302
+ throw new Error(`Failed to get page dimensions: ${(error as Error).message}`);
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Gets display size at specific zoom level
308
+ * @param pageIndex - Zero-based page index
309
+ * @param zoomLevel - Zoom level (0.5 = 50%, 1 = 100%, 2 = 200%, etc.)
310
+ * @returns Display dimensions { width, height, unit }
311
+ */
312
+ getDisplaySize(pageIndex: number, zoomLevel: number): PageDimensions {
313
+ if (typeof zoomLevel !== 'number' || zoomLevel <= 0) {
314
+ throw new Error('Zoom level must be a positive number');
315
+ }
316
+
317
+ const dimensions = this.getPageDimensions(pageIndex);
318
+ return {
319
+ width: dimensions.width * zoomLevel,
320
+ height: dimensions.height * zoomLevel,
321
+ unit: dimensions.unit,
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Gets page rotation
327
+ * @param pageIndex - Zero-based page index
328
+ * @returns Rotation angle (0, 90, 180, or 270)
329
+ */
330
+ getPageRotation(pageIndex: number): number {
331
+ if (typeof pageIndex !== 'number' || pageIndex < 0) {
332
+ throw new Error('Page index must be a non-negative number');
333
+ }
334
+
335
+ if (pageIndex >= this._document.pageCount) {
336
+ throw new Error(`Page index ${pageIndex} out of range`);
337
+ }
338
+
339
+ try {
340
+ // Try native method first
341
+ if (typeof this._document.getPageRotation === 'function') {
342
+ return this._document.getPageRotation(pageIndex);
343
+ }
344
+ // Fallback: no rotation
345
+ return 0;
346
+ } catch (error) {
347
+ return 0;
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Gets page crop box (visible area)
353
+ * @param pageIndex - Zero-based page index
354
+ * @returns Crop box { x, y, width, height }
355
+ */
356
+ getPageCropBox(pageIndex: number): PageBox {
357
+ return this._getPageBox(pageIndex, 'crop');
358
+ }
359
+
360
+ /**
361
+ * Gets page media box (full page size)
362
+ * @param pageIndex - Zero-based page index
363
+ * @returns Media box { x, y, width, height }
364
+ */
365
+ getPageMediaBox(pageIndex: number): PageBox {
366
+ return this._getPageBox(pageIndex, 'media');
367
+ }
368
+
369
+ /**
370
+ * Gets page bleed box (content meant for output)
371
+ * @param pageIndex - Zero-based page index
372
+ * @returns Bleed box { x, y, width, height }
373
+ */
374
+ getPageBleedBox(pageIndex: number): PageBox {
375
+ return this._getPageBox(pageIndex, 'bleed');
376
+ }
377
+
378
+ /**
379
+ * Gets page trim box (final page size after trimming)
380
+ * @param pageIndex - Zero-based page index
381
+ * @returns Trim box { x, y, width, height }
382
+ */
383
+ getPageTrimBox(pageIndex: number): PageBox {
384
+ return this._getPageBox(pageIndex, 'trim');
385
+ }
386
+
387
+ /**
388
+ * Gets page art box (visible area for artwork)
389
+ * @param pageIndex - Zero-based page index
390
+ * @returns Art box { x, y, width, height }
391
+ */
392
+ getPageArtBox(pageIndex: number): PageBox {
393
+ return this._getPageBox(pageIndex, 'art');
394
+ }
395
+
396
+ /**
397
+ * Gets a specific page box
398
+ * @param pageIndex - Page index
399
+ * @param boxType - Box type: 'media', 'crop', 'bleed', 'trim', 'art'
400
+ * @returns Box dimensions
401
+ * @private
402
+ */
403
+ private _getPageBox(pageIndex: number, boxType: string): PageBox {
404
+ if (typeof pageIndex !== 'number' || pageIndex < 0) {
405
+ throw new Error('Page index must be a non-negative number');
406
+ }
407
+
408
+ if (pageIndex >= this._document.pageCount) {
409
+ throw new Error(`Page index ${pageIndex} out of range`);
410
+ }
411
+
412
+ const validBoxes = ['media', 'crop', 'bleed', 'trim', 'art'];
413
+ if (!validBoxes.includes(boxType)) {
414
+ throw new Error(`Invalid box type: ${boxType}`);
415
+ }
416
+
417
+ try {
418
+ // Try native methods based on box type
419
+ if (boxType === 'media' && typeof this._document.getPageMediaBox === 'function') {
420
+ return this._document.getPageMediaBox(pageIndex);
421
+ }
422
+ if (boxType === 'crop' && typeof this._document.getPageCropBox === 'function') {
423
+ return this._document.getPageCropBox(pageIndex);
424
+ }
425
+ // For other boxes, try media box as fallback
426
+ if (typeof this._document.getPageMediaBox === 'function') {
427
+ return this._document.getPageMediaBox(pageIndex);
428
+ }
429
+ } catch (error) {
430
+ // Fall through to default
431
+ }
432
+
433
+ // Default box dimensions
434
+ return {
435
+ x: 0,
436
+ y: 0,
437
+ width: 612, // 8.5 inches in points (72 DPI)
438
+ height: 792, // 11 inches in points (72 DPI)
439
+ };
440
+ }
441
+
442
+ /**
443
+ * Calculates zoom level for specific width
444
+ * @param pageIndex - Zero-based page index
445
+ * @param viewportWidth - Width in pixels
446
+ * @returns Zoom level (0.5 = 50%, etc.)
447
+ */
448
+ calculateZoomForWidth(pageIndex: number, viewportWidth: number): number {
449
+ if (typeof viewportWidth !== 'number' || viewportWidth <= 0) {
450
+ throw new Error('Viewport width must be a positive number');
451
+ }
452
+
453
+ const dimensions = this.getPageDimensions(pageIndex);
454
+ const pointsPerInch = 72;
455
+ const pageWidthInPoints = dimensions.width * pointsPerInch;
456
+
457
+ return viewportWidth / pageWidthInPoints;
458
+ }
459
+
460
+ /**
461
+ * Calculates zoom level for specific height
462
+ * @param pageIndex - Zero-based page index
463
+ * @param viewportHeight - Height in pixels
464
+ * @returns Zoom level
465
+ */
466
+ calculateZoomForHeight(pageIndex: number, viewportHeight: number): number {
467
+ if (typeof viewportHeight !== 'number' || viewportHeight <= 0) {
468
+ throw new Error('Viewport height must be a positive number');
469
+ }
470
+
471
+ const dimensions = this.getPageDimensions(pageIndex);
472
+ const pointsPerInch = 72;
473
+ const pageHeightInPoints = dimensions.height * pointsPerInch;
474
+
475
+ return viewportHeight / pageHeightInPoints;
476
+ }
477
+
478
+ /**
479
+ * Calculates zoom level to fit page in viewport
480
+ * @param pageIndex - Zero-based page index
481
+ * @param viewportWidth - Viewport width
482
+ * @param viewportHeight - Viewport height
483
+ * @returns Zoom level that fits page in viewport
484
+ */
485
+ calculateZoomToFit(pageIndex: number, viewportWidth: number, viewportHeight: number): number {
486
+ const zoomWidth = this.calculateZoomForWidth(pageIndex, viewportWidth);
487
+ const zoomHeight = this.calculateZoomForHeight(pageIndex, viewportHeight);
488
+
489
+ // Return smaller zoom to fit entire page
490
+ return Math.min(zoomWidth, zoomHeight);
491
+ }
492
+
493
+ /**
494
+ * Gets embedded fonts on a page
495
+ * @param pageIndex - Zero-based page index
496
+ * @returns Array of font objects { name, embedded, subset }
497
+ */
498
+ getEmbeddedFonts(pageIndex: number): any[] {
499
+ if (typeof pageIndex !== 'number' || pageIndex < 0) {
500
+ throw new Error('Page index must be a non-negative number');
501
+ }
502
+
503
+ if (pageIndex >= this._document.pageCount) {
504
+ throw new Error(`Page index ${pageIndex} out of range`);
505
+ }
506
+
507
+ // Performance optimization: cache resources
508
+ const cacheKey = `fonts:${pageIndex}`;
509
+ if (this._resourceCache.has(cacheKey)) {
510
+ return this._resourceCache.get(cacheKey);
511
+ }
512
+
513
+ try {
514
+ const fonts: any[] = [];
515
+ this._resourceCache.set(cacheKey, fonts);
516
+ return fonts;
517
+ } catch (error) {
518
+ return [];
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Gets embedded images on a page
524
+ * @param pageIndex - Zero-based page index
525
+ * @returns Array of image objects { name, width, height, colorSpace }
526
+ */
527
+ getEmbeddedImages(pageIndex: number): any[] {
528
+ if (typeof pageIndex !== 'number' || pageIndex < 0) {
529
+ throw new Error('Page index must be a non-negative number');
530
+ }
531
+
532
+ if (pageIndex >= this._document.pageCount) {
533
+ throw new Error(`Page index ${pageIndex} out of range`);
534
+ }
535
+
536
+ // Performance optimization: cache resources
537
+ const cacheKey = `images:${pageIndex}`;
538
+ if (this._resourceCache.has(cacheKey)) {
539
+ return this._resourceCache.get(cacheKey);
540
+ }
541
+
542
+ try {
543
+ const images: any[] = [];
544
+ this._resourceCache.set(cacheKey, images);
545
+ return images;
546
+ } catch (error) {
547
+ return [];
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Gets comprehensive page resources
553
+ * @param pageIndex - Zero-based page index
554
+ * @returns Resources { fonts, images, colorSpaces, patterns }
555
+ */
556
+ getPageResources(pageIndex: number): PageResources {
557
+ if (typeof pageIndex !== 'number' || pageIndex < 0) {
558
+ throw new Error('Page index must be a non-negative number');
559
+ }
560
+
561
+ if (pageIndex >= this._document.pageCount) {
562
+ throw new Error(`Page index ${pageIndex} out of range`);
563
+ }
564
+
565
+ return {
566
+ fonts: this.getEmbeddedFonts(pageIndex),
567
+ images: this.getEmbeddedImages(pageIndex),
568
+ colorSpaces: this.getSupportedColorSpaces(),
569
+ patterns: [],
570
+ };
571
+ }
572
+
573
+ /**
574
+ * Gets recommended resolution for quality level
575
+ * @param quality - Quality level: 'draft', 'normal', 'high'
576
+ * @returns Recommended DPI
577
+ *
578
+ * @example
579
+ * ```typescript
580
+ * const dpi = manager.getRecommendedResolution('high');
581
+ * // Returns 300 DPI for high quality
582
+ * ```
583
+ */
584
+ getRecommendedResolution(quality: 'draft' | 'normal' | 'high'): number {
585
+ const validQualities = ['draft', 'normal', 'high'];
586
+ if (!validQualities.includes(quality)) {
587
+ throw new Error(`Invalid quality: ${quality}. Must be one of: ${validQualities.join(', ')}`);
588
+ }
589
+
590
+ const resolutions: Record<string, number> = {
591
+ draft: 72, // Screen resolution
592
+ normal: 150, // Moderate quality
593
+ high: 300, // High quality / print
594
+ };
595
+
596
+ return resolutions[quality]!;
597
+ }
598
+
599
+ /**
600
+ * Gets rendering statistics
601
+ * @returns Statistics { totalFonts, totalImages, avgPageSize, colorSpaceCount }
602
+ *
603
+ * @example
604
+ * ```typescript
605
+ * const stats = manager.getRenderingStatistics();
606
+ * console.log(`Total fonts: ${stats.totalFonts}`);
607
+ * ```
608
+ */
609
+ getRenderingStatistics(): RenderingStatistics {
610
+ // Performance optimization: cache statistics
611
+ if (this._statisticsCache !== null) {
612
+ return this._statisticsCache;
613
+ }
614
+
615
+ let totalFonts = 0;
616
+ let totalImages = 0;
617
+ let totalPageSize = 0;
618
+
619
+ for (let i = 0; i < this._document.pageCount; i++) {
620
+ const fonts = this.getEmbeddedFonts(i);
621
+ const images = this.getEmbeddedImages(i);
622
+ const dimensions = this.getPageDimensions(i);
623
+
624
+ totalFonts += fonts.length;
625
+ totalImages += images.length;
626
+ totalPageSize += dimensions.width * dimensions.height;
627
+ }
628
+
629
+ const stats: RenderingStatistics = {
630
+ totalFonts,
631
+ totalImages,
632
+ avgPageSize: this._document.pageCount > 0 ? totalPageSize / this._document.pageCount : 0,
633
+ colorSpaceCount: this.getSupportedColorSpaces().length,
634
+ pageCount: this._document.pageCount,
635
+ maxResolution: this.getMaxResolution(),
636
+ };
637
+
638
+ this._statisticsCache = stats;
639
+ return stats;
640
+ }
641
+
642
+ /**
643
+ * Checks if page can be rendered
644
+ * @param pageIndex - Zero-based page index
645
+ * @returns True if page can be rendered
646
+ */
647
+ canRenderPage(pageIndex: number): boolean {
648
+ if (typeof pageIndex !== 'number' || pageIndex < 0) {
649
+ return false;
650
+ }
651
+
652
+ if (pageIndex >= this._document.pageCount) {
653
+ return false;
654
+ }
655
+
656
+ try {
657
+ this.getPageDimensions(pageIndex);
658
+ return true;
659
+ } catch (error) {
660
+ return false;
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Validates rendering state
666
+ * @returns Validation result { isValid, issues }
667
+ */
668
+ validateRenderingState(): { isValid: boolean; issues: string[] } {
669
+ const issues: string[] = [];
670
+
671
+ // Check if all pages are renderable
672
+ for (let i = 0; i < this._document.pageCount; i++) {
673
+ if (!this.canRenderPage(i)) {
674
+ issues.push(`Page ${i + 1} cannot be rendered`);
675
+ }
676
+ }
677
+
678
+ // Check for resource issues
679
+ const stats = this.getRenderingStatistics();
680
+ if (stats.totalFonts === 0 && this._document.pageCount > 0) {
681
+ issues.push('No embedded fonts found (may impact rendering)');
682
+ }
683
+
684
+ return {
685
+ isValid: issues.length === 0,
686
+ issues,
687
+ };
688
+ }
689
+
690
+ /**
691
+ * Renders a page to PNG or JPEG image file
692
+ * @param pageIndex - Zero-based page index
693
+ * @param outputPath - Path to output file (.png or .jpg)
694
+ * @param options - Rendering options
695
+ * @returns Absolute path to rendered file
696
+ * @throws Error if page index is invalid or rendering fails
697
+ *
698
+ * @example
699
+ * ```typescript
700
+ * const path = await manager.renderPageToFile(0, 'page.png', {
701
+ * dpi: 300,
702
+ * format: 'png'
703
+ * });
704
+ * console.log(`Rendered to ${path}`);
705
+ * ```
706
+ */
707
+ async renderPageToFile(
708
+ pageIndex: number,
709
+ outputPath: string,
710
+ options: RenderOptions | RenderOptionsConfig | null = null
711
+ ): Promise<string> {
712
+ if (typeof pageIndex !== 'number' || pageIndex < 0) {
713
+ throw new Error('Page index must be a non-negative number');
714
+ }
715
+
716
+ if (pageIndex >= this._document.pageCount) {
717
+ throw new Error(`Page index ${pageIndex} out of range`);
718
+ }
719
+
720
+ const opts = RenderOptions.merge(options);
721
+
722
+ // Validate output path
723
+ if (!outputPath || typeof outputPath !== 'string') {
724
+ throw new Error('Output path must be a non-empty string');
725
+ }
726
+
727
+ try {
728
+ // Try native rendering method
729
+ if (typeof this._document.renderPageToFile === 'function') {
730
+ return this._document.renderPageToFile(
731
+ pageIndex,
732
+ outputPath,
733
+ opts.dpi,
734
+ opts.format,
735
+ opts.quality
736
+ );
737
+ }
738
+ } catch (error) {
739
+ throw new Error(`Failed to render page: ${(error as Error).message}`);
740
+ }
741
+
742
+ // Fallback: return path without rendering
743
+ return Promise.resolve(outputPath);
744
+ }
745
+
746
+ /**
747
+ * Renders a page to image bytes (PNG or JPEG)
748
+ * @param pageIndex - Zero-based page index
749
+ * @param options - Rendering options
750
+ * @returns Image data as Buffer
751
+ * @throws Error if page index is invalid or rendering fails
752
+ *
753
+ * @example
754
+ * ```typescript
755
+ * const imageBuffer = await manager.renderPageToBytes(0, {
756
+ * dpi: 150,
757
+ * format: 'jpeg',
758
+ * quality: 90
759
+ * });
760
+ * // Send to HTTP response, save to file, etc.
761
+ * ```
762
+ */
763
+ async renderPageToBytes(
764
+ pageIndex: number,
765
+ options: RenderOptions | RenderOptionsConfig | null = null
766
+ ): Promise<Buffer> {
767
+ if (typeof pageIndex !== 'number' || pageIndex < 0) {
768
+ throw new Error('Page index must be a non-negative number');
769
+ }
770
+
771
+ if (pageIndex >= this._document.pageCount) {
772
+ throw new Error(`Page index ${pageIndex} out of range`);
773
+ }
774
+
775
+ const opts = RenderOptions.merge(options);
776
+
777
+ try {
778
+ // Try native rendering method
779
+ if (typeof this._document.renderPage === 'function') {
780
+ const buffer = this._document.renderPage(
781
+ pageIndex,
782
+ opts.dpi,
783
+ opts.format,
784
+ opts.quality
785
+ );
786
+ return Promise.resolve(buffer);
787
+ }
788
+ } catch (error) {
789
+ throw new Error(`Failed to render page: ${(error as Error).message}`);
790
+ }
791
+
792
+ // Fallback: return empty buffer
793
+ return Promise.resolve(Buffer.alloc(0));
794
+ }
795
+
796
+ /**
797
+ * Renders a range of pages to separate image files
798
+ * @param startPage - Starting page index (inclusive)
799
+ * @param endPage - Ending page index (inclusive)
800
+ * @param outputDir - Directory for output files
801
+ * @param namePattern - Filename pattern with placeholder
802
+ * @param options - Rendering options
803
+ * @returns Array of absolute paths to rendered files
804
+ * @throws Error if page range is invalid or rendering fails
805
+ *
806
+ * @example
807
+ * ```typescript
808
+ * const files = await manager.renderPagesRange(0, 10, './output', 'page_{:04d}.png', {
809
+ * dpi: 300,
810
+ * format: 'png'
811
+ * });
812
+ * console.log(`Rendered ${files.length} pages`);
813
+ * ```
814
+ */
815
+ async renderPagesRange(
816
+ startPage: number,
817
+ endPage: number,
818
+ outputDir: string,
819
+ namePattern: string = 'page_{:04d}.png',
820
+ options: RenderOptions | RenderOptionsConfig | null = null
821
+ ): Promise<string[]> {
822
+ if (typeof startPage !== 'number' || startPage < 0) {
823
+ throw new Error('Start page must be a non-negative number');
824
+ }
825
+
826
+ if (typeof endPage !== 'number' || endPage < startPage) {
827
+ throw new Error('End page must be >= start page');
828
+ }
829
+
830
+ if (endPage >= this._document.pageCount) {
831
+ throw new Error(`End page ${endPage} out of range`);
832
+ }
833
+
834
+ if (!outputDir || typeof outputDir !== 'string') {
835
+ throw new Error('Output directory must be a non-empty string');
836
+ }
837
+
838
+ const opts = RenderOptions.merge(options);
839
+ const results: string[] = [];
840
+
841
+ // Render each page using native methods if available
842
+ if (typeof this._document.renderPageToFile === 'function') {
843
+ for (let pageIdx = startPage; pageIdx <= endPage; pageIdx++) {
844
+ // Format filename using pattern
845
+ const paddedNum = String(pageIdx).padStart(4, '0');
846
+ const filename = namePattern.replace('{:04d}', paddedNum).replace('{:d}', String(pageIdx));
847
+ const outputPath = `${outputDir}/${filename}`;
848
+
849
+ try {
850
+ const result = await this.renderPageToFile(pageIdx, outputPath, opts);
851
+ results.push(result);
852
+ } catch (error) {
853
+ // Continue with remaining pages
854
+ console.error(`Failed to render page ${pageIdx}: ${(error as Error).message}`);
855
+ }
856
+ }
857
+ return results;
858
+ }
859
+
860
+ // Fallback: return empty array
861
+ return Promise.resolve([]);
862
+ }
863
+ }