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,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manager for PDF page annotations (comments, highlights, etc.)
|
|
3
|
+
*
|
|
4
|
+
* Provides methods to work with annotations on PDF pages including
|
|
5
|
+
* comments, highlights, underlines, and other markup.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { AnnotationManager } from 'pdf_oxide';
|
|
10
|
+
*
|
|
11
|
+
* const doc = PdfDocument.open('document.pdf');
|
|
12
|
+
* const page = doc.getPage(0);
|
|
13
|
+
* const annotationManager = new AnnotationManager(page);
|
|
14
|
+
*
|
|
15
|
+
* // Get annotations on the page
|
|
16
|
+
* const annotations = annotationManager.getAnnotations();
|
|
17
|
+
* console.log(`Page has ${annotations.length} annotations`);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface Annotation {
|
|
22
|
+
type: string;
|
|
23
|
+
content?: string;
|
|
24
|
+
author?: string;
|
|
25
|
+
modificationDate?: Date;
|
|
26
|
+
bounds?: {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
};
|
|
32
|
+
opacity?: number;
|
|
33
|
+
[key: string]: any;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AnnotationStatistics {
|
|
37
|
+
total: number;
|
|
38
|
+
byType: Record<string, number>;
|
|
39
|
+
byAuthor: Record<string, number>;
|
|
40
|
+
authors: string[];
|
|
41
|
+
types: string[];
|
|
42
|
+
hasComments: boolean;
|
|
43
|
+
hasHighlights: boolean;
|
|
44
|
+
averageOpacity: number;
|
|
45
|
+
recentModifications: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AnnotationValidation {
|
|
49
|
+
isValid: boolean;
|
|
50
|
+
issues: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class AnnotationManager {
|
|
54
|
+
private _page: any;
|
|
55
|
+
private _annotationCache: Annotation[] | null;
|
|
56
|
+
private _statisticsCache: AnnotationStatistics | null;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a new AnnotationManager for the given page
|
|
60
|
+
* @param page - The PDF page
|
|
61
|
+
* @throws Error if page is null or undefined
|
|
62
|
+
*/
|
|
63
|
+
constructor(page: any) {
|
|
64
|
+
if (!page) {
|
|
65
|
+
throw new Error('Page is required');
|
|
66
|
+
}
|
|
67
|
+
this._page = page;
|
|
68
|
+
// Performance optimization: cache annotations to avoid redundant fetches
|
|
69
|
+
this._annotationCache = null;
|
|
70
|
+
this._statisticsCache = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Clears the annotation cache
|
|
75
|
+
* Useful when page content might have changed
|
|
76
|
+
*/
|
|
77
|
+
clearCache(): void {
|
|
78
|
+
this._annotationCache = null;
|
|
79
|
+
this._statisticsCache = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Gets all annotations on the page
|
|
84
|
+
* Performance optimization: caches results to avoid redundant fetches
|
|
85
|
+
* @returns Array of annotations
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const annotations = manager.getAnnotations();
|
|
90
|
+
* annotations.forEach(ann => {
|
|
91
|
+
* console.log(`${ann.type}: ${ann.content}`);
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
getAnnotations(): Annotation[] {
|
|
96
|
+
// Performance optimization: return cached annotations if available
|
|
97
|
+
if (this._annotationCache !== null) {
|
|
98
|
+
return this._annotationCache;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// This would require native API support for getting annotations
|
|
103
|
+
const annotations: Annotation[] = [];
|
|
104
|
+
// Cache the result
|
|
105
|
+
this._annotationCache = annotations;
|
|
106
|
+
return annotations;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Gets annotations by type
|
|
114
|
+
* @param type - Annotation type ('text', 'highlight', 'underline', 'strikeout', 'squiggly')
|
|
115
|
+
* @returns Matching annotations
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* const highlights = manager.getAnnotationsByType('highlight');
|
|
120
|
+
* console.log(`Page has ${highlights.length} highlights`);
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
getAnnotationsByType(type: string): Annotation[] {
|
|
124
|
+
if (!type || typeof type !== 'string') {
|
|
125
|
+
throw new Error('Type must be a non-empty string');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const validTypes = ['text', 'highlight', 'underline', 'strikeout', 'squiggly', 'link', 'ink'];
|
|
129
|
+
if (!validTypes.includes(type.toLowerCase())) {
|
|
130
|
+
throw new Error(`Invalid annotation type: ${type}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const annotations = this.getAnnotations();
|
|
134
|
+
return annotations.filter(ann => ann.type === type.toLowerCase());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Gets number of annotations on page
|
|
139
|
+
* @returns Annotation count
|
|
140
|
+
*/
|
|
141
|
+
getAnnotationCount(): number {
|
|
142
|
+
return this.getAnnotations().length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Gets annotations by author
|
|
147
|
+
* @param author - Author name to filter
|
|
148
|
+
* @returns Annotations by specified author
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* const johnDoeAnnotations = manager.getAnnotationsByAuthor('John Doe');
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
getAnnotationsByAuthor(author: string): Annotation[] {
|
|
156
|
+
if (!author || typeof author !== 'string') {
|
|
157
|
+
throw new Error('Author must be a non-empty string');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const annotations = this.getAnnotations();
|
|
161
|
+
return annotations.filter(ann => ann.author === author);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Gets unique authors of annotations
|
|
166
|
+
* @returns Array of author names
|
|
167
|
+
*/
|
|
168
|
+
getAnnotationAuthors(): string[] {
|
|
169
|
+
const annotations = this.getAnnotations();
|
|
170
|
+
const authors = new Set<string>();
|
|
171
|
+
|
|
172
|
+
annotations.forEach(ann => {
|
|
173
|
+
if (ann.author) {
|
|
174
|
+
authors.add(ann.author);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return Array.from(authors).sort();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Gets annotations modified after a date
|
|
183
|
+
* @param date - Filter date
|
|
184
|
+
* @returns Annotations modified after date
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* const recentAnnotations = manager.getAnnotationsAfter(new Date('2024-01-01'));
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
getAnnotationsAfter(date: Date): Annotation[] {
|
|
192
|
+
if (!(date instanceof Date)) {
|
|
193
|
+
throw new Error('Date must be a Date object');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const annotations = this.getAnnotations();
|
|
197
|
+
return annotations.filter(ann =>
|
|
198
|
+
ann.modificationDate && new Date(ann.modificationDate) > date
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Gets annotations modified before a date
|
|
204
|
+
* @param date - Filter date
|
|
205
|
+
* @returns Annotations modified before date
|
|
206
|
+
*/
|
|
207
|
+
getAnnotationsBefore(date: Date): Annotation[] {
|
|
208
|
+
if (!(date instanceof Date)) {
|
|
209
|
+
throw new Error('Date must be a Date object');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const annotations = this.getAnnotations();
|
|
213
|
+
return annotations.filter(ann =>
|
|
214
|
+
ann.modificationDate && new Date(ann.modificationDate) < date
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Gets annotations with specific content
|
|
220
|
+
* @param contentFragment - Text fragment to search for
|
|
221
|
+
* @returns Matching annotations
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* const reviewComments = manager.getAnnotationsWithContent('review');
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
getAnnotationsWithContent(contentFragment: string): Annotation[] {
|
|
229
|
+
if (!contentFragment || typeof contentFragment !== 'string') {
|
|
230
|
+
throw new Error('Content fragment must be a non-empty string');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const annotations = this.getAnnotations();
|
|
234
|
+
const fragment = contentFragment.toLowerCase();
|
|
235
|
+
|
|
236
|
+
return annotations.filter(ann =>
|
|
237
|
+
ann.content && ann.content.toLowerCase().includes(fragment)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Gets highlights (most common annotation type)
|
|
243
|
+
* @returns Array of highlight annotations
|
|
244
|
+
*/
|
|
245
|
+
getHighlights(): Annotation[] {
|
|
246
|
+
return this.getAnnotationsByType('highlight');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Gets text comments/notes
|
|
251
|
+
* @returns Array of text annotations
|
|
252
|
+
*/
|
|
253
|
+
getComments(): Annotation[] {
|
|
254
|
+
return this.getAnnotationsByType('text');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Gets underlines
|
|
259
|
+
* @returns Array of underline annotations
|
|
260
|
+
*/
|
|
261
|
+
getUnderlines(): Annotation[] {
|
|
262
|
+
return this.getAnnotationsByType('underline');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Gets strikeouts
|
|
267
|
+
* @returns Array of strikeout annotations
|
|
268
|
+
*/
|
|
269
|
+
getStrikeouts(): Annotation[] {
|
|
270
|
+
return this.getAnnotationsByType('strikeout');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Gets squiggly underlines
|
|
275
|
+
* @returns Array of squiggly annotations
|
|
276
|
+
*/
|
|
277
|
+
getSquigglies(): Annotation[] {
|
|
278
|
+
return this.getAnnotationsByType('squiggly');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Gets annotations statistics
|
|
283
|
+
* Performance optimization: caches computed statistics
|
|
284
|
+
* @returns Statistics about annotations
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* const stats = manager.getAnnotationStatistics();
|
|
289
|
+
* console.log(`Total: ${stats.total}, Highlights: ${stats.byType.highlight}`);
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
getAnnotationStatistics(): AnnotationStatistics {
|
|
293
|
+
// Performance optimization: return cached statistics if available
|
|
294
|
+
if (this._statisticsCache !== null) {
|
|
295
|
+
return this._statisticsCache;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const annotations = this.getAnnotations();
|
|
299
|
+
const byType: Record<string, number> = {};
|
|
300
|
+
const byAuthor: Record<string, number> = {};
|
|
301
|
+
|
|
302
|
+
annotations.forEach(ann => {
|
|
303
|
+
// Count by type
|
|
304
|
+
byType[ann.type] = (byType[ann.type] || 0) + 1;
|
|
305
|
+
|
|
306
|
+
// Count by author
|
|
307
|
+
if (ann.author) {
|
|
308
|
+
byAuthor[ann.author] = (byAuthor[ann.author] || 0) + 1;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const stats: AnnotationStatistics = {
|
|
313
|
+
total: annotations.length,
|
|
314
|
+
byType,
|
|
315
|
+
byAuthor,
|
|
316
|
+
authors: Object.keys(byAuthor),
|
|
317
|
+
types: Object.keys(byType),
|
|
318
|
+
hasComments: annotations.some(ann => ann.type === 'text'),
|
|
319
|
+
hasHighlights: annotations.some(ann => ann.type === 'highlight'),
|
|
320
|
+
averageOpacity: this.getAverageOpacity(),
|
|
321
|
+
recentModifications: this.getRecentAnnotations(7).length,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Cache the results
|
|
325
|
+
this._statisticsCache = stats;
|
|
326
|
+
return stats;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Gets average opacity of annotations
|
|
331
|
+
* @returns Average opacity value (0-1)
|
|
332
|
+
* @private
|
|
333
|
+
*/
|
|
334
|
+
private getAverageOpacity(): number {
|
|
335
|
+
const annotations = this.getAnnotations();
|
|
336
|
+
if (annotations.length === 0) return 1;
|
|
337
|
+
|
|
338
|
+
const sum = annotations.reduce((acc, ann) => acc + (ann.opacity || 1), 0);
|
|
339
|
+
return sum / annotations.length;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Gets annotations modified within last N days
|
|
344
|
+
* @param days - Number of days to look back
|
|
345
|
+
* @returns Recent annotations
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```typescript
|
|
349
|
+
* const lastWeek = manager.getRecentAnnotations(7);
|
|
350
|
+
* console.log(`${lastWeek.length} annotations modified in last 7 days`);
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
getRecentAnnotations(days: number): Annotation[] {
|
|
354
|
+
if (typeof days !== 'number' || days < 0) {
|
|
355
|
+
throw new Error('Days must be a non-negative number');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const cutoffDate = new Date();
|
|
359
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
360
|
+
|
|
361
|
+
return this.getAnnotationsAfter(cutoffDate);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Generates annotation summary
|
|
366
|
+
* @returns Human-readable annotation summary
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* console.log(manager.generateAnnotationSummary());
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
generateAnnotationSummary(): string {
|
|
374
|
+
const stats = this.getAnnotationStatistics();
|
|
375
|
+
const lines: string[] = [];
|
|
376
|
+
|
|
377
|
+
lines.push(`Annotation Summary (Page ${(this._page as any).pageIndex + 1}):`);
|
|
378
|
+
lines.push(`Total Annotations: ${stats.total}`);
|
|
379
|
+
|
|
380
|
+
if (stats.total > 0) {
|
|
381
|
+
lines.push('\nBy Type:');
|
|
382
|
+
Object.entries(stats.byType).forEach(([type, count]) => {
|
|
383
|
+
lines.push(` ${type}: ${count}`);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (stats.authors.length > 0) {
|
|
387
|
+
lines.push('\nBy Author:');
|
|
388
|
+
Object.entries(stats.byAuthor).forEach(([author, count]) => {
|
|
389
|
+
lines.push(` ${author}: ${count}`);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return lines.join('\n');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Validates annotation bounds
|
|
399
|
+
* @param annotation - Annotation to validate
|
|
400
|
+
* @returns Validation result
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```typescript
|
|
404
|
+
* const annotation = manager.getAnnotations()[0];
|
|
405
|
+
* const validation = manager.validateAnnotation(annotation);
|
|
406
|
+
* if (!validation.isValid) {
|
|
407
|
+
* console.log('Invalid:', validation.issues);
|
|
408
|
+
* }
|
|
409
|
+
* ```
|
|
410
|
+
*/
|
|
411
|
+
validateAnnotation(annotation: any): AnnotationValidation {
|
|
412
|
+
const issues: string[] = [];
|
|
413
|
+
|
|
414
|
+
if (!annotation) {
|
|
415
|
+
issues.push('Annotation is null or undefined');
|
|
416
|
+
} else {
|
|
417
|
+
if (!annotation.type) issues.push('Missing annotation type');
|
|
418
|
+
if (!['text', 'highlight', 'underline', 'strikeout', 'squiggly'].includes(annotation.type)) {
|
|
419
|
+
issues.push(`Unknown annotation type: ${annotation.type}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (annotation.bounds) {
|
|
423
|
+
if (annotation.bounds.x < 0) issues.push('Invalid bounds: x must be non-negative');
|
|
424
|
+
if (annotation.bounds.y < 0) issues.push('Invalid bounds: y must be non-negative');
|
|
425
|
+
if (annotation.bounds.width < 0) issues.push('Invalid bounds: width must be non-negative');
|
|
426
|
+
if (annotation.bounds.height < 0) issues.push('Invalid bounds: height must be non-negative');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (annotation.opacity && (annotation.opacity < 0 || annotation.opacity > 1)) {
|
|
430
|
+
issues.push('Invalid opacity: must be between 0 and 1');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
isValid: issues.length === 0,
|
|
436
|
+
issues,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarcodeManager - Canonical Barcode Manager (merged from 2 implementations)
|
|
3
|
+
*
|
|
4
|
+
* Consolidates:
|
|
5
|
+
* - src/barcode-manager.ts BarcodeManager (detection + counting + format-based query)
|
|
6
|
+
* - src/managers/barcode-signature-rendering.ts BarcodesManager (generation + conversion + page embedding)
|
|
7
|
+
*
|
|
8
|
+
* Provides complete barcode operations.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { EventEmitter } from 'events';
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Type Definitions
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export enum BarcodeFormat {
|
|
18
|
+
CODE128 = 'CODE128',
|
|
19
|
+
CODE39 = 'CODE39',
|
|
20
|
+
EAN13 = 'EAN13',
|
|
21
|
+
EAN8 = 'EAN8',
|
|
22
|
+
UPCA = 'UPCA',
|
|
23
|
+
UPCE = 'UPCE',
|
|
24
|
+
QR = 'QR',
|
|
25
|
+
PDF417 = 'PDF417',
|
|
26
|
+
DATAMATRIX = 'DATAMATRIX',
|
|
27
|
+
AZTEC = 'AZTEC',
|
|
28
|
+
// Numeric format codes from BarcodesManager
|
|
29
|
+
QR_CODE = 'QR_CODE',
|
|
30
|
+
DATA_MATRIX = 'DATA_MATRIX',
|
|
31
|
+
CODE_93 = 'CODE_93',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export enum BarcodeErrorCorrection {
|
|
35
|
+
L = 'L',
|
|
36
|
+
M = 'M',
|
|
37
|
+
Q = 'Q',
|
|
38
|
+
H = 'H',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export enum QrErrorCorrection {
|
|
42
|
+
L = 0, M = 1, Q = 2, H = 3,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DetectedBarcode {
|
|
46
|
+
format: BarcodeFormat;
|
|
47
|
+
rawValue: string;
|
|
48
|
+
decodedValue: string;
|
|
49
|
+
confidence: number;
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
width: number;
|
|
53
|
+
height: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BarcodeGenerationConfig {
|
|
57
|
+
format?: BarcodeFormat;
|
|
58
|
+
width?: number;
|
|
59
|
+
height?: number;
|
|
60
|
+
errorCorrection?: BarcodeErrorCorrection;
|
|
61
|
+
margin?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// Canonical BarcodeManager
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
export class BarcodeManager extends EventEmitter {
|
|
69
|
+
private document: any;
|
|
70
|
+
private resultCache = new Map<string, any>();
|
|
71
|
+
private maxCacheSize = 100;
|
|
72
|
+
private native: any;
|
|
73
|
+
|
|
74
|
+
constructor(document: any) {
|
|
75
|
+
super();
|
|
76
|
+
this.document = document;
|
|
77
|
+
try { this.native = require('../../index.node'); } catch { this.native = null; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ===========================================================================
|
|
81
|
+
// Detection (from root BarcodeManager)
|
|
82
|
+
// ===========================================================================
|
|
83
|
+
|
|
84
|
+
async detectBarcodes(pageIndex: number): Promise<DetectedBarcode[]> {
|
|
85
|
+
const cacheKey = `barcodes:detect:${pageIndex}`;
|
|
86
|
+
if (this.resultCache.has(cacheKey)) return this.resultCache.get(cacheKey);
|
|
87
|
+
let barcodes: DetectedBarcode[] = [];
|
|
88
|
+
if (this.native?.detect_barcodes) {
|
|
89
|
+
try {
|
|
90
|
+
const barcodesJson = this.native.detect_barcodes(pageIndex) ?? [];
|
|
91
|
+
barcodes = barcodesJson.length > 0 ? barcodesJson.map((json: string) => JSON.parse(json)) : [];
|
|
92
|
+
} catch { barcodes = []; }
|
|
93
|
+
}
|
|
94
|
+
this.setCached(cacheKey, barcodes);
|
|
95
|
+
this.emit('barcodesDetected', { page: pageIndex, count: barcodes.length });
|
|
96
|
+
return barcodes;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async detectAllBarcodes(): Promise<Map<number, DetectedBarcode[]>> {
|
|
100
|
+
const cacheKey = 'barcodes:detect_all';
|
|
101
|
+
if (this.resultCache.has(cacheKey)) return this.resultCache.get(cacheKey);
|
|
102
|
+
let barcodes = new Map<number, DetectedBarcode[]>();
|
|
103
|
+
if (this.native?.detect_all_barcodes) {
|
|
104
|
+
try {
|
|
105
|
+
const barcodesJson = this.native.detect_all_barcodes();
|
|
106
|
+
const parsed = JSON.parse(barcodesJson);
|
|
107
|
+
for (const [page, barcodesArray] of Object.entries(parsed)) {
|
|
108
|
+
barcodes.set(parseInt(page), (barcodesArray as any[]).map(b => JSON.parse(typeof b === 'string' ? b : JSON.stringify(b))));
|
|
109
|
+
}
|
|
110
|
+
} catch { barcodes = new Map(); }
|
|
111
|
+
}
|
|
112
|
+
this.setCached(cacheKey, barcodes);
|
|
113
|
+
this.emit('allBarcodesDetected', { pages: barcodes.size });
|
|
114
|
+
return barcodes;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async getBarcodesOfFormat(format: BarcodeFormat, pageIndex?: number): Promise<DetectedBarcode[]> {
|
|
118
|
+
const cacheKey = pageIndex ? `barcodes:format:${format}:${pageIndex}` : `barcodes:format:${format}:all`;
|
|
119
|
+
if (this.resultCache.has(cacheKey)) return this.resultCache.get(cacheKey);
|
|
120
|
+
let barcodes: DetectedBarcode[] = [];
|
|
121
|
+
if (this.native?.get_barcodes_of_format) {
|
|
122
|
+
try {
|
|
123
|
+
const page = pageIndex ?? -1;
|
|
124
|
+
const barcodesJson = this.native.get_barcodes_of_format(format, page) ?? [];
|
|
125
|
+
barcodes = barcodesJson.length > 0 ? barcodesJson.map((json: string) => JSON.parse(json)) : [];
|
|
126
|
+
} catch { barcodes = []; }
|
|
127
|
+
}
|
|
128
|
+
this.setCached(cacheKey, barcodes);
|
|
129
|
+
return barcodes;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getBarcodeCount(): Promise<number> {
|
|
133
|
+
const cacheKey = 'barcodes:count';
|
|
134
|
+
if (this.resultCache.has(cacheKey)) return this.resultCache.get(cacheKey);
|
|
135
|
+
const count = this.native?.get_barcode_count?.() ?? 0;
|
|
136
|
+
this.setCached(cacheKey, count);
|
|
137
|
+
return count;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async getCountByFormat(format: BarcodeFormat): Promise<number> {
|
|
141
|
+
const cacheKey = `barcodes:count:${format}`;
|
|
142
|
+
if (this.resultCache.has(cacheKey)) return this.resultCache.get(cacheKey);
|
|
143
|
+
const count = this.native?.get_count_by_format?.(format) ?? 0;
|
|
144
|
+
this.setCached(cacheKey, count);
|
|
145
|
+
return count;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async hasBarcode(pageIndex: number): Promise<boolean> {
|
|
149
|
+
const barcodes = await this.detectBarcodes(pageIndex);
|
|
150
|
+
return barcodes.length > 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ===========================================================================
|
|
154
|
+
// Generation (from root BarcodeManager + BarcodesManager)
|
|
155
|
+
// ===========================================================================
|
|
156
|
+
|
|
157
|
+
async generateBarcode(data: string, config?: BarcodeGenerationConfig): Promise<Buffer> {
|
|
158
|
+
const format = config?.format ?? BarcodeFormat.QR;
|
|
159
|
+
const cacheKey = `barcodes:generate:${data}:${format}`;
|
|
160
|
+
if (this.resultCache.has(cacheKey)) return this.resultCache.get(cacheKey);
|
|
161
|
+
let imageData = Buffer.alloc(0);
|
|
162
|
+
if (this.native?.generate_barcode) {
|
|
163
|
+
try {
|
|
164
|
+
const configJson = config ? JSON.stringify(config) : '{}';
|
|
165
|
+
const result = this.native.generate_barcode(data, configJson);
|
|
166
|
+
imageData = Buffer.from(result);
|
|
167
|
+
} catch { imageData = Buffer.alloc(0); }
|
|
168
|
+
}
|
|
169
|
+
this.setCached(cacheKey, imageData);
|
|
170
|
+
this.emit('barcodeGenerated', { format, dataLength: data.length });
|
|
171
|
+
return imageData;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async generateQrCode(data: string, errorCorrection: QrErrorCorrection = QrErrorCorrection.M, sizePx: number = 256): Promise<Buffer> {
|
|
175
|
+
try {
|
|
176
|
+
if (!data || typeof data !== 'string') throw new Error('Data must be a non-empty string');
|
|
177
|
+
if (sizePx < 1 || sizePx > 10000) throw new Error('Size must be between 1 and 10000 pixels');
|
|
178
|
+
const barcodeData = await this.document?.generateQrCode?.(data, errorCorrection, sizePx);
|
|
179
|
+
this.emit('barcode-generated', { format: 'qr', size: sizePx });
|
|
180
|
+
return barcodeData || Buffer.alloc(0);
|
|
181
|
+
} catch (error) { this.emit('error', error); throw error; }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ===========================================================================
|
|
185
|
+
// Conversion (from BarcodesManager)
|
|
186
|
+
// ===========================================================================
|
|
187
|
+
|
|
188
|
+
async barcodeToPng(barcodeData: Buffer, sizePx: number = 256): Promise<Buffer> {
|
|
189
|
+
return barcodeData;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async barcodeToSvg(barcodeData: Buffer, sizePx: number = 256): Promise<string> {
|
|
193
|
+
const encoded = barcodeData.toString('base64');
|
|
194
|
+
return `<svg xmlns="http://www.w3.org/2000/svg"><image href="data:image/png;base64,${encoded}"/></svg>`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async addBarcodeToPage(pageIndex: number, barcodeData: Buffer, x: number, y: number, width: number, height: number): Promise<boolean> {
|
|
198
|
+
try {
|
|
199
|
+
if (!this.document) throw new Error('Document required for adding barcode to page');
|
|
200
|
+
return false;
|
|
201
|
+
} catch (error) { this.emit('error', error); throw error; }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
detectBarcodeFormat(barcodeData: Buffer): BarcodeFormat {
|
|
205
|
+
return BarcodeFormat.QR;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
decodeBarcodeData(barcodeData: Buffer): string {
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getDetectionConfidence(barcodeData: Buffer): number {
|
|
213
|
+
return 1.0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ===========================================================================
|
|
217
|
+
// Cache
|
|
218
|
+
// ===========================================================================
|
|
219
|
+
|
|
220
|
+
clearCache(): void { this.resultCache.clear(); this.emit('cacheCleared'); }
|
|
221
|
+
|
|
222
|
+
getCacheStats(): Record<string, any> {
|
|
223
|
+
return { cacheSize: this.resultCache.size, maxCacheSize: this.maxCacheSize, entries: Array.from(this.resultCache.keys()) };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private setCached(key: string, value: any): void {
|
|
227
|
+
this.resultCache.set(key, value);
|
|
228
|
+
if (this.resultCache.size > this.maxCacheSize) {
|
|
229
|
+
const firstKey = this.resultCache.keys().next().value;
|
|
230
|
+
if (firstKey !== undefined) this.resultCache.delete(firstKey);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default BarcodeManager;
|