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.
- package/README.md +218 -0
- package/binding.gyp +35 -0
- package/package.json +78 -0
- package/src/builders/annotation-builder.ts +367 -0
- package/src/builders/conversion-options-builder.ts +257 -0
- package/src/builders/index.ts +12 -0
- package/src/builders/metadata-builder.ts +317 -0
- package/src/builders/pdf-builder.ts +386 -0
- package/src/builders/search-options-builder.ts +151 -0
- package/src/document-editor-manager.ts +318 -0
- package/src/errors.ts +1629 -0
- package/src/form-field-manager.ts +666 -0
- package/src/hybrid-ml-manager.ts +283 -0
- package/src/index.ts +453 -0
- package/src/managers/accessibility-manager.ts +338 -0
- package/src/managers/annotation-manager.ts +439 -0
- package/src/managers/barcode-manager.ts +235 -0
- package/src/managers/batch-manager.ts +533 -0
- package/src/managers/cache-manager.ts +486 -0
- package/src/managers/compliance-manager.ts +375 -0
- package/src/managers/content-manager.ts +339 -0
- package/src/managers/document-utility-manager.ts +922 -0
- package/src/managers/dom-pdf-creator.ts +365 -0
- package/src/managers/editing-manager.ts +514 -0
- package/src/managers/enterprise-manager.ts +478 -0
- package/src/managers/extended-managers.ts +437 -0
- package/src/managers/extraction-manager.ts +583 -0
- package/src/managers/final-utilities.ts +429 -0
- package/src/managers/hybrid-ml-advanced.ts +479 -0
- package/src/managers/index.ts +239 -0
- package/src/managers/layer-manager.ts +500 -0
- package/src/managers/metadata-manager.ts +303 -0
- package/src/managers/ocr-manager.ts +756 -0
- package/src/managers/optimization-manager.ts +262 -0
- package/src/managers/outline-manager.ts +196 -0
- package/src/managers/page-manager.ts +289 -0
- package/src/managers/pattern-detection.ts +440 -0
- package/src/managers/rendering-manager.ts +863 -0
- package/src/managers/search-manager.ts +385 -0
- package/src/managers/security-manager.ts +345 -0
- package/src/managers/signature-manager.ts +1664 -0
- package/src/managers/streams.ts +618 -0
- package/src/managers/xfa-manager.ts +500 -0
- package/src/pdf-creator-manager.ts +494 -0
- package/src/properties.ts +522 -0
- package/src/result-accessors-manager.ts +867 -0
- package/src/tests/advanced-features.test.ts +414 -0
- package/src/tests/advanced.test.ts +266 -0
- package/src/tests/extended-managers.test.ts +316 -0
- package/src/tests/final-utilities.test.ts +455 -0
- package/src/tests/foundation.test.ts +315 -0
- package/src/tests/high-demand.test.ts +257 -0
- package/src/tests/specialized.test.ts +97 -0
- package/src/thumbnail-manager.ts +272 -0
- package/src/types/common.ts +142 -0
- package/src/types/document-types.ts +457 -0
- package/src/types/index.ts +6 -0
- package/src/types/manager-types.ts +284 -0
- package/src/types/native-bindings.ts +517 -0
- package/src/workers/index.ts +7 -0
- package/src/workers/pool.ts +274 -0
- 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
|
+
}
|