pdfdancer-client-typescript 1.0.12 → 1.0.14

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 (73) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/README.md +11 -7
  3. package/dist/__tests__/e2e/pdf-assertions.d.ts +1 -0
  4. package/dist/__tests__/e2e/pdf-assertions.d.ts.map +1 -1
  5. package/dist/__tests__/e2e/pdf-assertions.js +9 -3
  6. package/dist/__tests__/e2e/pdf-assertions.js.map +1 -1
  7. package/dist/__tests__/e2e/test-helpers.d.ts.map +1 -1
  8. package/dist/__tests__/e2e/test-helpers.js +8 -3
  9. package/dist/__tests__/e2e/test-helpers.js.map +1 -1
  10. package/dist/fingerprint.d.ts +12 -0
  11. package/dist/fingerprint.d.ts.map +1 -0
  12. package/dist/fingerprint.js +196 -0
  13. package/dist/fingerprint.js.map +1 -0
  14. package/dist/image-builder.d.ts +4 -2
  15. package/dist/image-builder.d.ts.map +1 -1
  16. package/dist/image-builder.js +12 -3
  17. package/dist/image-builder.js.map +1 -1
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +10 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/models.d.ts +90 -20
  23. package/dist/models.d.ts.map +1 -1
  24. package/dist/models.js +199 -38
  25. package/dist/models.js.map +1 -1
  26. package/dist/page-builder.d.ts +24 -0
  27. package/dist/page-builder.d.ts.map +1 -0
  28. package/dist/page-builder.js +107 -0
  29. package/dist/page-builder.js.map +1 -0
  30. package/dist/paragraph-builder.d.ts +48 -54
  31. package/dist/paragraph-builder.d.ts.map +1 -1
  32. package/dist/paragraph-builder.js +408 -135
  33. package/dist/paragraph-builder.js.map +1 -1
  34. package/dist/pdfdancer_v1.d.ts +92 -10
  35. package/dist/pdfdancer_v1.d.ts.map +1 -1
  36. package/dist/pdfdancer_v1.js +597 -69
  37. package/dist/pdfdancer_v1.js.map +1 -1
  38. package/dist/types.d.ts +24 -3
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js +117 -2
  41. package/dist/types.js.map +1 -1
  42. package/docs/openapi.yml +2640 -0
  43. package/fixtures/Showcase.pdf +0 -0
  44. package/jest.config.js +1 -1
  45. package/package.json +1 -1
  46. package/src/__tests__/e2e/acroform.test.ts +5 -5
  47. package/src/__tests__/e2e/context-manager-showcase.test.ts +267 -0
  48. package/src/__tests__/e2e/form_x_object.test.ts +1 -1
  49. package/src/__tests__/e2e/image-showcase.test.ts +133 -0
  50. package/src/__tests__/e2e/image.test.ts +6 -6
  51. package/src/__tests__/e2e/line-showcase.test.ts +118 -0
  52. package/src/__tests__/e2e/line.test.ts +1 -16
  53. package/src/__tests__/e2e/page-showcase.test.ts +154 -0
  54. package/src/__tests__/e2e/page.test.ts +3 -3
  55. package/src/__tests__/e2e/paragraph-showcase.test.ts +523 -0
  56. package/src/__tests__/e2e/paragraph.test.ts +8 -8
  57. package/src/__tests__/e2e/path.test.ts +4 -4
  58. package/src/__tests__/e2e/pdf-assertions.ts +10 -3
  59. package/src/__tests__/e2e/pdfdancer-showcase.test.ts +40 -0
  60. package/src/__tests__/e2e/snapshot-showcase.test.ts +158 -0
  61. package/src/__tests__/e2e/snapshot.test.ts +296 -0
  62. package/src/__tests__/e2e/test-helpers.ts +8 -3
  63. package/src/__tests__/e2e/token_from_env.test.ts +0 -14
  64. package/src/__tests__/fingerprint.test.ts +36 -0
  65. package/src/fingerprint.ts +169 -0
  66. package/src/image-builder.ts +13 -6
  67. package/src/index.ts +9 -2
  68. package/src/models.ts +228 -40
  69. package/src/page-builder.ts +130 -0
  70. package/src/paragraph-builder.ts +517 -159
  71. package/src/pdfdancer_v1.ts +705 -77
  72. package/src/types.ts +145 -2
  73. package/update-api-spec.sh +3 -0
@@ -12,6 +12,7 @@ import {
12
12
  ValidationException
13
13
  } from './exceptions';
14
14
  import {
15
+ AddPageRequest,
15
16
  AddRequest,
16
17
  BoundingRect,
17
18
  ChangeFormFieldRequest,
@@ -19,9 +20,10 @@ import {
19
20
  CommandResult,
20
21
  CreatePdfRequest,
21
22
  DeleteRequest,
23
+ DocumentSnapshot,
22
24
  FindRequest,
23
25
  Font,
24
- FontRecommendation,
26
+ DocumentFontInfo,
25
27
  FontType,
26
28
  FormFieldRef,
27
29
  Image,
@@ -35,6 +37,7 @@ import {
35
37
  PageRef,
36
38
  PageSize,
37
39
  PageSizeInput,
40
+ PageSnapshot,
38
41
  Paragraph,
39
42
  Position,
40
43
  PositionMode,
@@ -43,11 +46,111 @@ import {
43
46
  TextStatus
44
47
  } from './models';
45
48
  import {ParagraphBuilder} from './paragraph-builder';
49
+ import {PageBuilder} from './page-builder';
46
50
  import {FormFieldObject, FormXObject, ImageObject, ParagraphObject, PathObject, TextLineObject} from "./types";
47
51
  import {ImageBuilder} from "./image-builder";
52
+ import {generateFingerprint} from "./fingerprint";
48
53
  import fs from "fs";
49
54
  import path from "node:path";
50
55
 
56
+ const DEFAULT_TOLERANCE = 0.01;
57
+
58
+ // Debug flag - set to true to enable timing logs
59
+ const DEBUG =
60
+ (process.env.PDFDANCER_CLIENT_DEBUG ?? '').toLowerCase() === 'true' ||
61
+ (process.env.PDFDANCER_CLIENT_DEBUG ?? '') === '1' ||
62
+ (process.env.PDFDANCER_CLIENT_DEBUG ?? '').toLowerCase() === 'yes';
63
+
64
+ /**
65
+ * Generate a timestamp string in the format expected by the API.
66
+ * Format: YYYY-MM-DDTHH:MM:SS.ffffffZ (with microseconds)
67
+ */
68
+ function generateTimestamp(): string {
69
+ const now = new Date();
70
+ const year = now.getUTCFullYear();
71
+ const month = String(now.getUTCMonth() + 1).padStart(2, '0');
72
+ const day = String(now.getUTCDate()).padStart(2, '0');
73
+ const hours = String(now.getUTCHours()).padStart(2, '0');
74
+ const minutes = String(now.getUTCMinutes()).padStart(2, '0');
75
+ const seconds = String(now.getUTCSeconds()).padStart(2, '0');
76
+ const milliseconds = String(now.getUTCMilliseconds()).padStart(3, '0');
77
+ // Add 3 more zeros for microseconds (JavaScript doesn't have microsecond precision)
78
+ const microseconds = milliseconds + '000';
79
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${microseconds}Z`;
80
+ }
81
+
82
+ /**
83
+ * Parse timestamp string, handling both microseconds and nanoseconds precision.
84
+ * @param timestampStr Timestamp string in format YYYY-MM-DDTHH:MM:SS.fffffffZ (with 6 or 9 fractional digits)
85
+ */
86
+ function parseTimestamp(timestampStr: string): Date {
87
+ // Remove the 'Z' suffix
88
+ let ts = timestampStr.replace(/Z$/, '');
89
+
90
+ // Handle nanoseconds (9 digits) by truncating to milliseconds (3 digits)
91
+ // JavaScript's Date only supports millisecond precision
92
+ if (ts.includes('.')) {
93
+ const [datePart, fracPart] = ts.split('.');
94
+ // Truncate to 3 digits (milliseconds)
95
+ const truncatedFrac = fracPart.substring(0, 3);
96
+ ts = `${datePart}.${truncatedFrac}`;
97
+ }
98
+
99
+ return new Date(ts + 'Z');
100
+ }
101
+
102
+ /**
103
+ * Check for X-Generated-At and X-Received-At headers and log timing information if DEBUG=true.
104
+ *
105
+ * Expected timestamp formats:
106
+ * - 2025-10-24T08:49:39.161945Z (microseconds - 6 digits)
107
+ * - 2025-10-24T08:58:45.468131265Z (nanoseconds - 9 digits)
108
+ */
109
+ function logGeneratedAtHeader(response: Response, method: string, path: string): void {
110
+ if (!DEBUG) {
111
+ return;
112
+ }
113
+
114
+ const generatedAt = response.headers.get('X-Generated-At');
115
+ const receivedAt = response.headers.get('X-Received-At');
116
+
117
+ if (generatedAt || receivedAt) {
118
+ try {
119
+ const logParts: string[] = [];
120
+ const currentTime = new Date();
121
+
122
+ // Parse and log X-Received-At
123
+ let receivedTime: Date | null = null;
124
+ if (receivedAt) {
125
+ receivedTime = parseTimestamp(receivedAt);
126
+ const timeSinceReceived = (currentTime.getTime() - receivedTime.getTime()) / 1000;
127
+ logParts.push(`X-Received-At: ${receivedAt}, time since received on backend: ${timeSinceReceived.toFixed(3)}s`);
128
+ }
129
+
130
+ // Parse and log X-Generated-At
131
+ let generatedTime: Date | null = null;
132
+ if (generatedAt) {
133
+ generatedTime = parseTimestamp(generatedAt);
134
+ const timeSinceGenerated = (currentTime.getTime() - generatedTime.getTime()) / 1000;
135
+ logParts.push(`X-Generated-At: ${generatedAt}, time since generated on backend: ${timeSinceGenerated.toFixed(3)}s`);
136
+ }
137
+
138
+ // Calculate processing time (X-Generated-At - X-Received-At)
139
+ if (receivedTime && generatedTime) {
140
+ const processingTime = (generatedTime.getTime() - receivedTime.getTime()) / 1000;
141
+ logParts.push(`processing time on backend: ${processingTime.toFixed(3)}s`);
142
+ }
143
+
144
+ if (logParts.length > 0) {
145
+ console.log(`${Date.now() / 1000}|${method} ${path} - ${logParts.join(', ')}`);
146
+ }
147
+ } catch (e) {
148
+ const errorMessage = e instanceof Error ? e.message : String(e);
149
+ console.log(`${Date.now() / 1000}|${method} ${path} - Header parse error: ${errorMessage}`);
150
+ }
151
+ }
152
+ }
153
+
51
154
  // 👇 Internal view of PDFDancer methods, not exported
52
155
  interface PDFDancerInternals {
53
156
 
@@ -98,8 +201,8 @@ class PageClient {
98
201
  this._internals = this._client as unknown as PDFDancerInternals;
99
202
  }
100
203
 
101
- async selectPathsAt(x: number, y: number) {
102
- return this._internals.toPathObjects(await this._internals.findPaths(Position.atPageCoordinates(this._pageIndex, x, y)));
204
+ async selectPathsAt(x: number, y: number, tolerance: number = 0) {
205
+ return this._internals.toPathObjects(await this._internals.findPaths(Position.atPageCoordinates(this._pageIndex, x, y, tolerance)));
103
206
  }
104
207
 
105
208
  async selectPaths() {
@@ -110,8 +213,8 @@ class PageClient {
110
213
  return this._internals.toImageObjects(await this._internals._findImages(Position.atPage(this._pageIndex)));
111
214
  }
112
215
 
113
- async selectImagesAt(x: number, y: number) {
114
- return this._internals.toImageObjects(await this._internals._findImages(Position.atPageCoordinates(this._pageIndex, x, y)));
216
+ async selectImagesAt(x: number, y: number, tolerance: number = 0) {
217
+ return this._internals.toImageObjects(await this._internals._findImages(Position.atPageCoordinates(this._pageIndex, x, y, tolerance)));
115
218
  }
116
219
 
117
220
  async delete(): Promise<boolean> {
@@ -133,16 +236,16 @@ class PageClient {
133
236
  return this._internals.toFormXObjects(await this._internals.findFormXObjects(Position.atPage(this._pageIndex)));
134
237
  }
135
238
 
136
- async selectFormsAt(x: number, y: number) {
137
- return this._internals.toFormXObjects(await this._internals.findFormXObjects(Position.atPageCoordinates(this._pageIndex, x, y)));
239
+ async selectFormsAt(x: number, y: number, tolerance: number = 0) {
240
+ return this._internals.toFormXObjects(await this._internals.findFormXObjects(Position.atPageCoordinates(this._pageIndex, x, y, tolerance)));
138
241
  }
139
242
 
140
243
  async selectFormFields() {
141
244
  return this._internals.toFormFields(await this._internals.findFormFields(Position.atPage(this._pageIndex)));
142
245
  }
143
246
 
144
- async selectFormFieldsAt(x: number, y: number) {
145
- return this._internals.toFormFields(await this._internals.findFormFields(Position.atPageCoordinates(this._pageIndex, x, y)));
247
+ async selectFormFieldsAt(x: number, y: number, tolerance: number = 0) {
248
+ return this._internals.toFormFields(await this._internals.findFormFields(Position.atPageCoordinates(this._pageIndex, x, y, tolerance)));
146
249
  }
147
250
 
148
251
  // noinspection JSUnusedGlobalSymbols
@@ -156,6 +259,11 @@ class PageClient {
156
259
  return this._internals.toParagraphObjects(await this._internals.findParagraphs(Position.atPage(this._pageIndex)));
157
260
  }
158
261
 
262
+ async selectElements(types?: ObjectType[]) {
263
+ const snapshot = await this._client.getPageSnapshot(this._pageIndex, types);
264
+ return snapshot.elements;
265
+ }
266
+
159
267
  async selectParagraphsStartingWith(text: string) {
160
268
  let pos = Position.atPage(this._pageIndex);
161
269
  pos.textStartsWith = text;
@@ -168,8 +276,10 @@ class PageClient {
168
276
  return this._internals.toParagraphObjects(await this._internals.findParagraphs(pos));
169
277
  }
170
278
 
171
- async selectParagraphsAt(x: number, y: number) {
172
- return this._internals.toParagraphObjects(await this._internals.findParagraphs(Position.atPageCoordinates(this._pageIndex, x, y)));
279
+ async selectParagraphsAt(x: number, y: number, tolerance: number = DEFAULT_TOLERANCE) {
280
+ return this._internals.toParagraphObjects(
281
+ await this._internals.findParagraphs(Position.atPageCoordinates(this._pageIndex, x, y, tolerance))
282
+ );
173
283
  }
174
284
 
175
285
  async selectTextLinesStartingWith(text: string) {
@@ -181,8 +291,14 @@ class PageClient {
181
291
  /**
182
292
  * Creates a new ParagraphBuilder for fluent paragraph construction.
183
293
  */
184
- newParagraph(): ParagraphBuilder {
185
- return new ParagraphBuilder(this._client, this.position.pageIndex);
294
+ newParagraph(pageIndex?: number): ParagraphBuilder {
295
+ const targetIndex = pageIndex ?? this.position.pageIndex;
296
+ return new ParagraphBuilder(this._client, targetIndex);
297
+ }
298
+
299
+ newImage(pageIndex?: number) {
300
+ const targetIndex = pageIndex ?? this.position.pageIndex;
301
+ return new ImageBuilder(this._client, targetIndex);
186
302
  }
187
303
 
188
304
  async selectTextLines() {
@@ -197,8 +313,18 @@ class PageClient {
197
313
  }
198
314
 
199
315
  // noinspection JSUnusedGlobalSymbols
200
- async selectTextLinesAt(x: number, y: number) {
201
- return this._internals.toTextLineObjects(await this._internals.findTextLines(Position.atPageCoordinates(this._pageIndex, x, y)));
316
+ async selectTextLinesAt(x: number, y: number, tolerance: number = DEFAULT_TOLERANCE) {
317
+ return this._internals.toTextLineObjects(
318
+ await this._internals.findTextLines(Position.atPageCoordinates(this._pageIndex, x, y, tolerance))
319
+ );
320
+ }
321
+
322
+ /**
323
+ * Gets a snapshot of this page, including all elements.
324
+ * Optionally filter by object types.
325
+ */
326
+ async getSnapshot(types?: ObjectType[]): Promise<PageSnapshot> {
327
+ return this._client.getPageSnapshot(this._pageIndex, types);
202
328
  }
203
329
  }
204
330
 
@@ -216,6 +342,13 @@ export class PDFDancer {
216
342
  private _readTimeout: number;
217
343
  private _pdfBytes: Uint8Array;
218
344
  private _sessionId!: string;
345
+ private _userId?: string;
346
+ private _fingerprintCache?: string;
347
+
348
+ // Snapshot caches for optimizing find operations
349
+ private _documentSnapshotCache: DocumentSnapshot | null = null;
350
+ private _pageSnapshotCache: Map<number, PageSnapshot> = new Map();
351
+ private _pagesCache: PageRef[] | null = null;
219
352
 
220
353
  /**
221
354
  * Creates a new client with PDF data.
@@ -226,7 +359,7 @@ export class PDFDancer {
226
359
  token: string,
227
360
  pdfData: Uint8Array | File | ArrayBuffer,
228
361
  baseUrl: string | null = null,
229
- readTimeout: number = 30000
362
+ readTimeout: number = 60000
230
363
  ) {
231
364
 
232
365
  if (!token || !token.trim()) {
@@ -253,6 +386,11 @@ export class PDFDancer {
253
386
 
254
387
  // Process PDF data with validation
255
388
  this._pdfBytes = this._processPdfData(pdfData);
389
+
390
+ // Initialize caches
391
+ this._documentSnapshotCache = null;
392
+ this._pageSnapshotCache = new Map();
393
+ this._pagesCache = null;
256
394
  }
257
395
 
258
396
  /**
@@ -265,15 +403,15 @@ export class PDFDancer {
265
403
  }
266
404
 
267
405
  static async open(pdfData: Uint8Array, token?: string, baseUrl?: string, timeout?: number): Promise<PDFDancer> {
268
- const resolvedToken = token ?? process.env.PDFDANCER_TOKEN;
269
406
  const resolvedBaseUrl =
270
407
  baseUrl ??
271
408
  process.env.PDFDANCER_BASE_URL ??
272
409
  "https://api.pdfdancer.com";
273
- const resolvedTimeout = timeout ?? 30000;
410
+ const resolvedTimeout = timeout ?? 60000;
274
411
 
412
+ let resolvedToken = token?.trim() ?? process.env.PDFDANCER_TOKEN?.trim() ?? null;
275
413
  if (!resolvedToken) {
276
- throw new ValidationException("Missing PDFDancer API token. Pass a token via the `token` argument or set the PDFDANCER_TOKEN environment variable.");
414
+ resolvedToken = await PDFDancer._obtainAnonymousToken(resolvedBaseUrl, resolvedTimeout);
277
415
  }
278
416
 
279
417
  const client = new PDFDancer(resolvedToken, pdfData, resolvedBaseUrl, resolvedTimeout);
@@ -289,7 +427,7 @@ export class PDFDancer {
289
427
  * @param options.initialPageCount Number of initial pages (default: 1)
290
428
  * @param token Authentication token (optional, can use PDFDANCER_TOKEN env var)
291
429
  * @param baseUrl Base URL for the PDFDancer API (optional)
292
- * @param timeout Request timeout in milliseconds (default: 30000)
430
+ * @param timeout Request timeout in milliseconds (default: 60000)
293
431
  */
294
432
  static async new(
295
433
  options?: {
@@ -301,15 +439,15 @@ export class PDFDancer {
301
439
  baseUrl?: string,
302
440
  timeout?: number
303
441
  ): Promise<PDFDancer> {
304
- const resolvedToken = token ?? process.env.PDFDANCER_TOKEN;
305
442
  const resolvedBaseUrl =
306
443
  baseUrl ??
307
444
  process.env.PDFDANCER_BASE_URL ??
308
445
  "https://api.pdfdancer.com";
309
- const resolvedTimeout = timeout ?? 30000;
446
+ const resolvedTimeout = timeout ?? 60000;
310
447
 
448
+ let resolvedToken = token?.trim() ?? process.env.PDFDANCER_TOKEN?.trim() ?? null;
311
449
  if (!resolvedToken) {
312
- throw new ValidationException("Missing PDFDancer token (pass it explicitly or set PDFDANCER_TOKEN in environment).");
450
+ resolvedToken = await PDFDancer._obtainAnonymousToken(resolvedBaseUrl, resolvedTimeout);
313
451
  }
314
452
 
315
453
  let createRequest: CreatePdfRequest;
@@ -330,17 +468,24 @@ export class PDFDancer {
330
468
  const endpoint = '/session/new'.replace(/^\/+/, '');
331
469
  const url = `${base}/${endpoint}`;
332
470
 
471
+ // Generate fingerprint for this request
472
+ const fingerprint = await generateFingerprint();
473
+
333
474
  // Make request to create endpoint
334
475
  const response = await fetch(url, {
335
476
  method: 'POST',
336
477
  headers: {
337
478
  'Authorization': `Bearer ${resolvedToken}`,
338
- 'Content-Type': 'application/json'
479
+ 'Content-Type': 'application/json',
480
+ 'X-Generated-At': generateTimestamp(),
481
+ 'X-Fingerprint': fingerprint
339
482
  },
340
483
  body: JSON.stringify(createRequest.toDict()),
341
484
  signal: resolvedTimeout > 0 ? AbortSignal.timeout(resolvedTimeout) : undefined
342
485
  });
343
486
 
487
+ logGeneratedAtHeader(response, 'POST', '/session/new');
488
+
344
489
  if (!response.ok) {
345
490
  const errorText = await response.text();
346
491
  throw new HttpClientException(`Failed to create new PDF: ${errorText}`, response);
@@ -358,6 +503,10 @@ export class PDFDancer {
358
503
  client._readTimeout = resolvedTimeout;
359
504
  client._pdfBytes = new Uint8Array();
360
505
  client._sessionId = sessionId;
506
+ // Initialize caches
507
+ client._documentSnapshotCache = null;
508
+ client._pageSnapshotCache = new Map();
509
+ client._pagesCache = null;
361
510
  return client;
362
511
  } catch (error) {
363
512
  if (error instanceof HttpClientException || error instanceof SessionException || error instanceof ValidationException) {
@@ -368,6 +517,47 @@ export class PDFDancer {
368
517
  }
369
518
  }
370
519
 
520
+ private static async _obtainAnonymousToken(baseUrl: string, timeout: number = 60000): Promise<string> {
521
+ const normalizedBaseUrl = (baseUrl || "https://api.pdfdancer.com").replace(/\/+$/, '');
522
+ const url = `${normalizedBaseUrl}/keys/anon`;
523
+
524
+ try {
525
+ const fingerprint = await generateFingerprint();
526
+ const response = await fetch(url, {
527
+ method: 'POST',
528
+ headers: {
529
+ 'Content-Type': 'application/json',
530
+ 'X-Fingerprint': fingerprint,
531
+ 'X-Generated-At': generateTimestamp()
532
+ },
533
+ signal: timeout > 0 ? AbortSignal.timeout(timeout) : undefined
534
+ });
535
+
536
+ if (!response.ok) {
537
+ const errorText = await response.text().catch(() => '');
538
+ throw new HttpClientException(
539
+ `Failed to obtain anonymous token: ${errorText || `HTTP ${response.status}`}`,
540
+ response
541
+ );
542
+ }
543
+
544
+ const tokenPayload: any = await response.json().catch(() => null);
545
+ const tokenValue = typeof tokenPayload?.token === 'string' ? tokenPayload.token.trim() : '';
546
+
547
+ if (!tokenValue) {
548
+ throw new HttpClientException("Invalid anonymous token response format", response);
549
+ }
550
+
551
+ return tokenValue;
552
+ } catch (error) {
553
+ if (error instanceof HttpClientException) {
554
+ throw error;
555
+ }
556
+ const errorMessage = error instanceof Error ? error.message : String(error);
557
+ throw new HttpClientException(`Failed to obtain anonymous token: ${errorMessage}`, undefined, error as Error);
558
+ }
559
+ }
560
+
371
561
  /**
372
562
  * Process PDF data from various input types with strict validation.
373
563
  */
@@ -470,15 +660,21 @@ export class PDFDancer {
470
660
  formData.append('pdf', blob, 'document.pdf');
471
661
  }
472
662
 
663
+ const fingerprint = await this._getFingerprint();
664
+
473
665
  const response = await fetch(this._buildUrl('/session/create'), {
474
666
  method: 'POST',
475
667
  headers: {
476
- 'Authorization': `Bearer ${this._token}`
668
+ 'Authorization': `Bearer ${this._token}`,
669
+ 'X-Generated-At': generateTimestamp(),
670
+ 'X-Fingerprint': fingerprint
477
671
  },
478
672
  body: formData,
479
673
  signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
480
674
  });
481
675
 
676
+ logGeneratedAtHeader(response, 'POST', '/session/create');
677
+
482
678
  if (!response.ok) {
483
679
  const errorMessage = await this._extractErrorMessage(response);
484
680
 
@@ -510,6 +706,16 @@ export class PDFDancer {
510
706
  }
511
707
  }
512
708
 
709
+ /**
710
+ * Get or generate the fingerprint for this client
711
+ */
712
+ private async _getFingerprint(): Promise<string> {
713
+ if (!this._fingerprintCache) {
714
+ this._fingerprintCache = await generateFingerprint(this._userId);
715
+ }
716
+ return this._fingerprintCache;
717
+ }
718
+
513
719
  /**
514
720
  * Make HTTP request with session headers and error handling.
515
721
  */
@@ -526,10 +732,14 @@ export class PDFDancer {
526
732
  });
527
733
  }
528
734
 
735
+ const fingerprint = await this._getFingerprint();
736
+
529
737
  const headers: Record<string, string> = {
530
738
  'Authorization': `Bearer ${this._token}`,
531
739
  'X-Session-Id': this._sessionId,
532
- 'Content-Type': 'application/json'
740
+ 'Content-Type': 'application/json',
741
+ 'X-Generated-At': generateTimestamp(),
742
+ 'X-Fingerprint': fingerprint
533
743
  };
534
744
 
535
745
  try {
@@ -540,6 +750,8 @@ export class PDFDancer {
540
750
  signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
541
751
  });
542
752
 
753
+ logGeneratedAtHeader(response, method, path);
754
+
543
755
  // Handle FontNotFoundException
544
756
  if (response.status === 404) {
545
757
  try {
@@ -576,13 +788,43 @@ export class PDFDancer {
576
788
  * Searches for PDF objects matching the specified criteria.
577
789
  * This method provides flexible search capabilities across all PDF content,
578
790
  * allowing filtering by object type and position constraints.
791
+ *
792
+ * Now uses snapshot caching for better performance.
579
793
  */
580
794
  private async find(objectType?: ObjectType, position?: Position): Promise<ObjectRef[]> {
581
- const requestData = new FindRequest(objectType, position).toDict();
582
- const response = await this._makeRequest('POST', '/pdf/find', requestData);
795
+ // Determine if we should use snapshot or fall back to HTTP
796
+ // For paths with coordinates, we need to use HTTP (backend requirement)
797
+ const isPathWithCoordinates = objectType === ObjectType.PATH &&
798
+ position?.shape === ShapeType.POINT;
799
+
800
+ if (isPathWithCoordinates) {
801
+ // Fall back to HTTP for path coordinate queries
802
+ const requestData = new FindRequest(objectType, position).toDict();
803
+ const response = await this._makeRequest('POST', '/pdf/find', requestData);
804
+ const objectsData = await response.json() as any[];
805
+ return objectsData.map((objData: any) => this._parseObjectRef(objData));
806
+ }
807
+
808
+ // Use snapshot-based search
809
+ let elements: ObjectRef[];
810
+
811
+ if (position?.pageIndex !== undefined) {
812
+ // Page-specific query - use page snapshot
813
+ const pageSnapshot = await this._getOrFetchPageSnapshot(position.pageIndex);
814
+ elements = pageSnapshot.elements;
815
+ } else {
816
+ // Document-wide query - use document snapshot
817
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
818
+ elements = docSnapshot.getAllElements();
819
+ }
583
820
 
584
- const objectsData = await response.json() as any[];
585
- return objectsData.map((objData: any) => this._parseObjectRef(objData));
821
+ // Filter by object type
822
+ if (objectType) {
823
+ elements = elements.filter(el => el.type === objectType);
824
+ }
825
+
826
+ // Apply position-based filtering
827
+ return this._filterByPosition(elements, position);
586
828
  }
587
829
 
588
830
  /**
@@ -643,43 +885,86 @@ export class PDFDancer {
643
885
  /**
644
886
  * Searches for form fields at the specified position.
645
887
  * Returns FormFieldRef objects with name and value properties.
888
+ *
889
+ * Now uses snapshot caching for better performance.
646
890
  */
647
891
  private async findFormFields(position?: Position): Promise<FormFieldRef[]> {
648
- const requestData = new FindRequest(ObjectType.FORM_FIELD, position).toDict();
649
- const response = await this._makeRequest('POST', '/pdf/find', requestData);
892
+ // Use snapshot-based search
893
+ let elements: ObjectRef[];
894
+
895
+ if (position?.pageIndex !== undefined) {
896
+ // Page-specific query - use page snapshot
897
+ const pageSnapshot = await this._getOrFetchPageSnapshot(position.pageIndex);
898
+ elements = pageSnapshot.elements;
899
+ } else {
900
+ // Document-wide query - use document snapshot
901
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
902
+ elements = docSnapshot.getAllElements();
903
+ }
904
+
905
+ // Filter by form field types (FORM_FIELD, TEXT_FIELD, CHECKBOX, RADIO_BUTTON)
906
+ const formFieldTypes = [
907
+ ObjectType.FORM_FIELD,
908
+ ObjectType.TEXT_FIELD,
909
+ ObjectType.CHECKBOX,
910
+ ObjectType.RADIO_BUTTON
911
+ ];
912
+ const formFields = elements.filter(el => formFieldTypes.includes(el.type)) as FormFieldRef[];
650
913
 
651
- const objectsData = await response.json() as any[];
652
- return objectsData.map((objData: any) => this._parseFormFieldRef(objData));
914
+ // Apply position-based filtering
915
+ return this._filterFormFieldsByPosition(formFields, position);
653
916
  }
654
917
 
655
918
  // Page Operations
656
919
 
657
920
  /**
658
921
  * Retrieves references to all pages in the PDF document.
922
+ * Now uses snapshot caching to avoid HTTP requests.
659
923
  */
660
924
  private async getPages(): Promise<PageRef[]> {
661
- const response = await this._makeRequest('POST', '/pdf/page/find');
662
- const pagesData = await response.json() as any[];
663
- return pagesData.map((pageData: any) => this._parsePageRef(pageData));
925
+ // Check if we have cached pages
926
+ if (this._pagesCache) {
927
+ return this._pagesCache;
928
+ }
929
+
930
+ // Try to get from document snapshot cache first
931
+ if (this._documentSnapshotCache) {
932
+ this._pagesCache = this._documentSnapshotCache.pages.map(p => p.pageRef);
933
+ return this._pagesCache;
934
+ }
935
+
936
+ // Fetch document snapshot to get pages (this will cache it)
937
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
938
+ this._pagesCache = docSnapshot.pages.map(p => p.pageRef);
939
+ return this._pagesCache;
664
940
  }
665
941
 
666
942
  /**
667
943
  * Retrieves a reference to a specific page by its page index.
944
+ * Now uses snapshot caching to avoid HTTP requests.
668
945
  */
669
946
  private async _getPage(pageIndex: number): Promise<PageRef | null> {
670
947
  if (pageIndex < 0) {
671
948
  throw new ValidationException(`Page index must be >= 0, got ${pageIndex}`);
672
949
  }
673
950
 
674
- const params = {pageIndex: pageIndex.toString()};
675
- const response = await this._makeRequest('POST', '/pdf/page/find', undefined, params);
951
+ // Try page snapshot cache first
952
+ if (this._pageSnapshotCache.has(pageIndex)) {
953
+ return this._pageSnapshotCache.get(pageIndex)!.pageRef;
954
+ }
676
955
 
677
- const pagesData = await response.json() as any[];
678
- if (!pagesData || pagesData.length === 0) {
679
- return null;
956
+ // Try document snapshot cache
957
+ if (this._documentSnapshotCache) {
958
+ const pageSnapshot = this._documentSnapshotCache.getPageSnapshot(pageIndex);
959
+ if (pageSnapshot) {
960
+ return pageSnapshot.pageRef;
961
+ }
680
962
  }
681
963
 
682
- return this._parsePageRef(pagesData[0]);
964
+ // Fetch document snapshot to get page (this will cache it)
965
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
966
+ const pageSnapshot = docSnapshot.getPageSnapshot(pageIndex);
967
+ return pageSnapshot?.pageRef ?? null;
683
968
  }
684
969
 
685
970
  /**
@@ -700,6 +985,9 @@ export class PDFDancer {
700
985
  throw new HttpClientException(`Failed to move page from ${pageIndex} to ${targetPageIndex}`, response);
701
986
  }
702
987
 
988
+ // Invalidate cache after mutation
989
+ this._invalidateCache();
990
+
703
991
  // Fetch the page again at its new position for up-to-date metadata
704
992
  return await this._requirePageRef(targetPageIndex);
705
993
  }
@@ -711,7 +999,12 @@ export class PDFDancer {
711
999
  this._validatePageIndex(pageIndex, 'pageIndex');
712
1000
 
713
1001
  const pageRef = await this._requirePageRef(pageIndex);
714
- return this._deletePage(pageRef);
1002
+ const result = await this._deletePage(pageRef);
1003
+
1004
+ // Invalidate cache after mutation
1005
+ this._invalidateCache();
1006
+
1007
+ return result;
715
1008
  }
716
1009
 
717
1010
  private _validatePageIndex(pageIndex: number, fieldName: string): void {
@@ -744,6 +1037,186 @@ export class PDFDancer {
744
1037
  return await response.json() as boolean;
745
1038
  }
746
1039
 
1040
+ // Snapshot Operations
1041
+
1042
+ /**
1043
+ * Gets a snapshot of the entire PDF document.
1044
+ * Returns page count, fonts, and snapshots of all pages with their elements.
1045
+ *
1046
+ * @param types Optional array of ObjectType to filter elements by type
1047
+ * @returns DocumentSnapshot containing all document information
1048
+ */
1049
+ async getDocumentSnapshot(types?: ObjectType[]): Promise<DocumentSnapshot> {
1050
+ const params: Record<string, string> = {};
1051
+ if (types && types.length > 0) {
1052
+ params.types = types.join(',');
1053
+ }
1054
+
1055
+ const response = await this._makeRequest('GET', '/pdf/document/snapshot', undefined, params);
1056
+ const data = await response.json() as any;
1057
+
1058
+ return this._parseDocumentSnapshot(data);
1059
+ }
1060
+
1061
+ /**
1062
+ * Gets a snapshot of a specific page.
1063
+ * Returns the page reference and all elements on that page.
1064
+ *
1065
+ * @param pageIndex Zero-based page index
1066
+ * @param types Optional array of ObjectType to filter elements by type
1067
+ * @returns PageSnapshot containing page information and elements
1068
+ */
1069
+ async getPageSnapshot(pageIndex: number, types?: ObjectType[]): Promise<PageSnapshot> {
1070
+ this._validatePageIndex(pageIndex, 'pageIndex');
1071
+
1072
+ const params: Record<string, string> = {};
1073
+ if (types && types.length > 0) {
1074
+ params.types = types.join(',');
1075
+ }
1076
+
1077
+ const response = await this._makeRequest('GET', `/pdf/page/${pageIndex}/snapshot`, undefined, params);
1078
+ const data = await response.json() as any;
1079
+
1080
+ return this._parsePageSnapshot(data);
1081
+ }
1082
+
1083
+ // Cache Management
1084
+
1085
+ /**
1086
+ * Gets a page snapshot from cache or fetches it.
1087
+ * First checks page cache, then document cache, then fetches from server.
1088
+ */
1089
+ private async _getOrFetchPageSnapshot(pageIndex: number): Promise<PageSnapshot> {
1090
+ // Check page cache first
1091
+ if (this._pageSnapshotCache.has(pageIndex)) {
1092
+ return this._pageSnapshotCache.get(pageIndex)!;
1093
+ }
1094
+
1095
+ // Check if we have document snapshot and can extract the page
1096
+ if (this._documentSnapshotCache) {
1097
+ const pageSnapshot = this._documentSnapshotCache.getPageSnapshot(pageIndex);
1098
+ if (pageSnapshot) {
1099
+ // Cache it for future use
1100
+ this._pageSnapshotCache.set(pageIndex, pageSnapshot);
1101
+ return pageSnapshot;
1102
+ }
1103
+ }
1104
+
1105
+ // Fetch page snapshot from server
1106
+ const pageSnapshot = await this.getPageSnapshot(pageIndex);
1107
+ this._pageSnapshotCache.set(pageIndex, pageSnapshot);
1108
+ return pageSnapshot;
1109
+ }
1110
+
1111
+ /**
1112
+ * Gets the document snapshot from cache or fetches it.
1113
+ */
1114
+ private async _getOrFetchDocumentSnapshot(): Promise<DocumentSnapshot> {
1115
+ if (!this._documentSnapshotCache) {
1116
+ this._documentSnapshotCache = await this.getDocumentSnapshot();
1117
+ }
1118
+ return this._documentSnapshotCache;
1119
+ }
1120
+
1121
+ /**
1122
+ * Invalidates all snapshot caches.
1123
+ * Called after any mutation operation.
1124
+ */
1125
+ private _invalidateCache(): void {
1126
+ this._documentSnapshotCache = null;
1127
+ this._pageSnapshotCache.clear();
1128
+ this._pagesCache = null;
1129
+ }
1130
+
1131
+ /**
1132
+ * Filters snapshot elements by Position criteria.
1133
+ * Handles coordinates, text matching, and field name filtering.
1134
+ */
1135
+ private _filterByPosition(elements: ObjectRef[], position?: Position): ObjectRef[] {
1136
+ if (!position) {
1137
+ return elements;
1138
+ }
1139
+
1140
+ let filtered = elements;
1141
+
1142
+ // Filter by page index
1143
+ if (position.pageIndex !== undefined) {
1144
+ filtered = filtered.filter(el => el.position.pageIndex === position.pageIndex);
1145
+ }
1146
+
1147
+ // Filter by coordinates (point containment with tolerance)
1148
+ if (position.boundingRect && position.shape === ShapeType.POINT) {
1149
+ const x = position.boundingRect.x;
1150
+ const y = position.boundingRect.y;
1151
+ const tolerance = position.tolerance || 0;
1152
+ filtered = filtered.filter(el => {
1153
+ const rect = el.position.boundingRect;
1154
+ if (!rect) return false;
1155
+ return x >= rect.x - tolerance && x <= rect.x + rect.width + tolerance &&
1156
+ y >= rect.y - tolerance && y <= rect.y + rect.height + tolerance;
1157
+ });
1158
+ }
1159
+
1160
+ // Filter by text starts with
1161
+ if (position.textStartsWith && filtered.length > 0) {
1162
+ const textLower = position.textStartsWith.toLowerCase();
1163
+ filtered = filtered.filter(el => {
1164
+ const textObj = el as TextObjectRef;
1165
+ return textObj.text && textObj.text.toLowerCase().startsWith(textLower);
1166
+ });
1167
+ }
1168
+
1169
+ // Filter by text pattern (regex)
1170
+ if (position.textPattern && filtered.length > 0) {
1171
+ const regex = this._compileTextPattern(position.textPattern);
1172
+ filtered = filtered.filter(el => {
1173
+ const textObj = el as TextObjectRef;
1174
+ return textObj.text && regex.test(textObj.text);
1175
+ });
1176
+ }
1177
+
1178
+ // Filter by name (for form fields)
1179
+ if (position.name && filtered.length > 0) {
1180
+ filtered = filtered.filter(el => {
1181
+ const formField = el as FormFieldRef;
1182
+ return formField.name === position.name;
1183
+ });
1184
+ }
1185
+
1186
+ return filtered;
1187
+ }
1188
+
1189
+ /**
1190
+ * Filters FormFieldRef elements by Position criteria.
1191
+ */
1192
+ private _filterFormFieldsByPosition(elements: FormFieldRef[], position?: Position): FormFieldRef[] {
1193
+ return this._filterByPosition(elements as ObjectRef[], position) as FormFieldRef[];
1194
+ }
1195
+
1196
+ private _compileTextPattern(pattern: string): RegExp {
1197
+ try {
1198
+ return new RegExp(pattern);
1199
+ } catch {
1200
+ const inlineMatch = pattern.match(/^\(\?([a-z]+)\)/i);
1201
+ if (inlineMatch) {
1202
+ const supportedFlags = inlineMatch[1]
1203
+ .toLowerCase()
1204
+ .split('')
1205
+ .filter(flag => 'gimsuy'.includes(flag));
1206
+ const flags = Array.from(new Set(supportedFlags)).join('');
1207
+ const source = pattern.slice(inlineMatch[0].length);
1208
+ try {
1209
+ return new RegExp(source, flags);
1210
+ } catch {
1211
+ // fall through to literal fallback
1212
+ }
1213
+ }
1214
+
1215
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1216
+ return new RegExp(escaped);
1217
+ }
1218
+ }
1219
+
747
1220
  // Manipulation Operations
748
1221
 
749
1222
  /**
@@ -756,7 +1229,12 @@ export class PDFDancer {
756
1229
 
757
1230
  const requestData = new DeleteRequest(objectRef).toDict();
758
1231
  const response = await this._makeRequest('DELETE', '/pdf/delete', requestData);
759
- return await response.json() as boolean;
1232
+ const result = await response.json() as boolean;
1233
+
1234
+ // Invalidate cache after mutation
1235
+ this._invalidateCache();
1236
+
1237
+ return result;
760
1238
  }
761
1239
 
762
1240
  /**
@@ -772,7 +1250,12 @@ export class PDFDancer {
772
1250
 
773
1251
  const requestData = new MoveRequest(objectRef, position).toDict();
774
1252
  const response = await this._makeRequest('PUT', '/pdf/move', requestData);
775
- return await response.json() as boolean;
1253
+ const result = await response.json() as boolean;
1254
+
1255
+ // Invalidate cache after mutation
1256
+ this._invalidateCache();
1257
+
1258
+ return result;
776
1259
  }
777
1260
 
778
1261
  /**
@@ -785,7 +1268,12 @@ export class PDFDancer {
785
1268
 
786
1269
  const requestData = new ChangeFormFieldRequest(formFieldRef, newValue).toDict();
787
1270
  const response = await this._makeRequest('PUT', '/pdf/modify/formField', requestData);
788
- return await response.json() as boolean;
1271
+ const result = await response.json() as boolean;
1272
+
1273
+ // Invalidate cache after mutation
1274
+ this._invalidateCache();
1275
+
1276
+ return result;
789
1277
  }
790
1278
 
791
1279
  // Add Operations
@@ -829,13 +1317,33 @@ export class PDFDancer {
829
1317
  return this._addObject(paragraph);
830
1318
  }
831
1319
 
1320
+ /**
1321
+ * Adds a page to the PDF document.
1322
+ */
1323
+ private async addPage(request?: AddPageRequest | null): Promise<PageRef> {
1324
+ const payload = request ? request.toDict() : {};
1325
+ const data = Object.keys(payload).length > 0 ? payload : undefined;
1326
+ const response = await this._makeRequest('POST', '/pdf/page/add', data);
1327
+ const result = await response.json();
1328
+ const pageRef = this._parsePageRef(result);
1329
+
1330
+ this._invalidateCache();
1331
+
1332
+ return pageRef;
1333
+ }
1334
+
832
1335
  /**
833
1336
  * Internal method to add any PDF object.
834
1337
  */
835
1338
  private async _addObject(pdfObject: Image | Paragraph): Promise<boolean> {
836
1339
  const requestData = new AddRequest(pdfObject).toDict();
837
1340
  const response = await this._makeRequest('POST', '/pdf/add', requestData);
838
- return await response.json() as boolean;
1341
+ const result = await response.json() as boolean;
1342
+
1343
+ // Invalidate cache after mutation
1344
+ this._invalidateCache();
1345
+
1346
+ return result;
839
1347
  }
840
1348
 
841
1349
  // Modify Operations
@@ -843,7 +1351,7 @@ export class PDFDancer {
843
1351
  /**
844
1352
  * Modifies a paragraph object or its text content.
845
1353
  */
846
- private async modifyParagraph(objectRef: ObjectRef, newParagraph: Paragraph | string): Promise<CommandResult> {
1354
+ private async modifyParagraph(objectRef: ObjectRef, newParagraph: Paragraph | string | null): Promise<CommandResult> {
847
1355
  if (!objectRef) {
848
1356
  throw new ValidationException("Object reference cannot be null");
849
1357
  }
@@ -851,17 +1359,23 @@ export class PDFDancer {
851
1359
  return CommandResult.empty("ModifyParagraph", objectRef.internalId);
852
1360
  }
853
1361
 
1362
+ let result: CommandResult;
854
1363
  if (typeof newParagraph === 'string') {
855
1364
  // Text modification - returns CommandResult
856
1365
  const requestData = new ModifyTextRequest(objectRef, newParagraph).toDict();
857
1366
  const response = await this._makeRequest('PUT', '/pdf/text/paragraph', requestData);
858
- return CommandResult.fromDict(await response.json());
1367
+ result = CommandResult.fromDict(await response.json());
859
1368
  } else {
860
1369
  // Object modification
861
1370
  const requestData = new ModifyRequest(objectRef, newParagraph).toDict();
862
1371
  const response = await this._makeRequest('PUT', '/pdf/modify', requestData);
863
- return CommandResult.fromDict(await response.json());
1372
+ result = CommandResult.fromDict(await response.json());
864
1373
  }
1374
+
1375
+ // Invalidate cache after mutation
1376
+ this._invalidateCache();
1377
+
1378
+ return result;
865
1379
  }
866
1380
 
867
1381
  /**
@@ -877,7 +1391,12 @@ export class PDFDancer {
877
1391
 
878
1392
  const requestData = new ModifyTextRequest(objectRef, newText).toDict();
879
1393
  const response = await this._makeRequest('PUT', '/pdf/text/line', requestData);
880
- return CommandResult.fromDict(await response.json());
1394
+ const result = CommandResult.fromDict(await response.json());
1395
+
1396
+ // Invalidate cache after mutation
1397
+ this._invalidateCache();
1398
+
1399
+ return result;
881
1400
  }
882
1401
 
883
1402
  // Font Operations
@@ -939,16 +1458,22 @@ export class PDFDancer {
939
1458
  const blob = new Blob([fontData.buffer as ArrayBuffer], {type: 'font/ttf'});
940
1459
  formData.append('ttfFile', blob, filename);
941
1460
 
1461
+ const fingerprint = await this._getFingerprint();
1462
+
942
1463
  const response = await fetch(this._buildUrl('/font/register'), {
943
1464
  method: 'POST',
944
1465
  headers: {
945
1466
  'Authorization': `Bearer ${this._token}`,
946
- 'X-Session-Id': this._sessionId
1467
+ 'X-Session-Id': this._sessionId,
1468
+ 'X-Generated-At': generateTimestamp(),
1469
+ 'X-Fingerprint': fingerprint
947
1470
  },
948
1471
  body: formData,
949
- signal: AbortSignal.timeout(30000)
1472
+ signal: AbortSignal.timeout(60000)
950
1473
  });
951
1474
 
1475
+ logGeneratedAtHeader(response, 'POST', '/font/register');
1476
+
952
1477
  if (!response.ok) {
953
1478
  const errorMessage = await this._extractErrorMessage(response);
954
1479
  throw new HttpClientException(`Font registration failed: ${errorMessage}`, response);
@@ -1011,6 +1536,17 @@ export class PDFDancer {
1011
1536
  return this._parseTextObjectRef(objData);
1012
1537
  }
1013
1538
 
1539
+ // Check if this is a form field type
1540
+ const formFieldTypes = [
1541
+ ObjectType.FORM_FIELD,
1542
+ ObjectType.TEXT_FIELD,
1543
+ ObjectType.CHECKBOX,
1544
+ ObjectType.RADIO_BUTTON
1545
+ ];
1546
+ if (formFieldTypes.includes(objectType)) {
1547
+ return this._parseFormFieldRef(objData);
1548
+ }
1549
+
1014
1550
  return new ObjectRef(
1015
1551
  objData.internalId,
1016
1552
  position,
@@ -1021,6 +1557,7 @@ export class PDFDancer {
1021
1557
  private _isTextObjectData(objData: any, objectType: ObjectType): boolean {
1022
1558
  return objectType === ObjectType.PARAGRAPH ||
1023
1559
  objectType === ObjectType.TEXT_LINE ||
1560
+ objectType === ObjectType.TEXT_ELEMENT ||
1024
1561
  typeof objData.text === 'string' ||
1025
1562
  typeof objData.fontName === 'string' ||
1026
1563
  Array.isArray(objData.children);
@@ -1038,25 +1575,30 @@ export class PDFDancer {
1038
1575
  let status: TextStatus | undefined;
1039
1576
  const statusData = objData.status;
1040
1577
  if (statusData && typeof statusData === 'object') {
1041
- // Parse font recommendation
1042
- const fontRecData = statusData.fontRecommendation;
1043
- let fontRec: FontRecommendation;
1044
- if (fontRecData && typeof fontRecData === 'object') {
1045
- fontRec = new FontRecommendation(
1046
- fontRecData.fontName || '',
1047
- (fontRecData.fontType as FontType) || FontType.SYSTEM,
1048
- fontRecData.similarityScore || 0.0
1049
- );
1050
- } else {
1051
- // Create empty font recommendation if not provided
1052
- fontRec = new FontRecommendation('', FontType.SYSTEM, 0.0);
1578
+ const fontInfoSource = statusData.fontInfoDto ?? statusData.fontRecommendation;
1579
+ let fontInfo: DocumentFontInfo | undefined;
1580
+ if (fontInfoSource && typeof fontInfoSource === 'object') {
1581
+ const documentFontName = typeof fontInfoSource.documentFontName === 'string'
1582
+ ? fontInfoSource.documentFontName
1583
+ : (typeof fontInfoSource.fontName === 'string' ? fontInfoSource.fontName : '');
1584
+ const systemFontName = typeof fontInfoSource.systemFontName === 'string'
1585
+ ? fontInfoSource.systemFontName
1586
+ : (typeof fontInfoSource.fontName === 'string' ? fontInfoSource.fontName : '');
1587
+ fontInfo = new DocumentFontInfo(documentFontName, systemFontName);
1053
1588
  }
1054
1589
 
1590
+ const modified = statusData.modified !== undefined ? Boolean(statusData.modified) : false;
1591
+ const encodable = statusData.encodable !== undefined ? Boolean(statusData.encodable) : true;
1592
+ const fontTypeValue = typeof statusData.fontType === 'string'
1593
+ && (Object.values(FontType) as string[]).includes(statusData.fontType)
1594
+ ? statusData.fontType as FontType
1595
+ : FontType.SYSTEM;
1596
+
1055
1597
  status = new TextStatus(
1056
- statusData.modified || false,
1057
- statusData.encodable !== undefined ? statusData.encodable : true,
1058
- (statusData.fontType as FontType) || FontType.SYSTEM,
1059
- fontRec
1598
+ modified,
1599
+ encodable,
1600
+ fontTypeValue,
1601
+ fontInfo
1060
1602
  );
1061
1603
  }
1062
1604
 
@@ -1074,10 +1616,15 @@ export class PDFDancer {
1074
1616
  );
1075
1617
 
1076
1618
  if (Array.isArray(objData.children) && objData.children.length > 0) {
1077
- textObject.children = objData.children.map((childData: any, index: number) => {
1078
- const childFallbackId = `${internalId || 'child'}-${index}`;
1079
- return this._parseTextObjectRef(childData, childFallbackId);
1080
- });
1619
+ try {
1620
+ textObject.children = objData.children.map((childData: any, index: number) => {
1621
+ const childFallbackId = `${internalId || 'child'}-${index}`;
1622
+ return this._parseTextObjectRef(childData, childFallbackId);
1623
+ });
1624
+ } catch (error) {
1625
+ const message = error instanceof Error ? error.message : String(error);
1626
+ console.error(`Failed to parse children of ${internalId}: ${message}`);
1627
+ }
1081
1628
  }
1082
1629
 
1083
1630
  return textObject;
@@ -1196,6 +1743,57 @@ export class PDFDancer {
1196
1743
  return position;
1197
1744
  }
1198
1745
 
1746
+ /**
1747
+ * Parse JSON data into DocumentSnapshot instance.
1748
+ */
1749
+ private _parseDocumentSnapshot(data: any): DocumentSnapshot {
1750
+ const pageCount = typeof data.pageCount === 'number' ? data.pageCount : 0;
1751
+
1752
+ // Parse fonts
1753
+ const fonts: DocumentFontInfo[] = [];
1754
+ if (Array.isArray(data.fonts)) {
1755
+ for (const fontData of data.fonts) {
1756
+ if (fontData && typeof fontData === 'object') {
1757
+ const documentFontName = typeof fontData.documentFontName === 'string'
1758
+ ? fontData.documentFontName
1759
+ : (typeof fontData.fontName === 'string' ? fontData.fontName : '');
1760
+ const systemFontName = typeof fontData.systemFontName === 'string'
1761
+ ? fontData.systemFontName
1762
+ : (typeof fontData.fontName === 'string' ? fontData.fontName : '');
1763
+ fonts.push(new DocumentFontInfo(documentFontName, systemFontName));
1764
+ }
1765
+ }
1766
+ }
1767
+
1768
+ // Parse pages
1769
+ const pages: PageSnapshot[] = [];
1770
+ if (Array.isArray(data.pages)) {
1771
+ for (const pageData of data.pages) {
1772
+ pages.push(this._parsePageSnapshot(pageData));
1773
+ }
1774
+ }
1775
+
1776
+ return new DocumentSnapshot(pageCount, fonts, pages);
1777
+ }
1778
+
1779
+ /**
1780
+ * Parse JSON data into PageSnapshot instance.
1781
+ */
1782
+ private _parsePageSnapshot(data: any): PageSnapshot {
1783
+ // Parse page reference
1784
+ const pageRef = this._parsePageRef(data.pageRef || {});
1785
+
1786
+ // Parse elements
1787
+ const elements: ObjectRef[] = [];
1788
+ if (Array.isArray(data.elements)) {
1789
+ for (const elementData of data.elements) {
1790
+ elements.push(this._parseObjectRef(elementData));
1791
+ }
1792
+ }
1793
+
1794
+ return new PageSnapshot(pageRef, elements);
1795
+ }
1796
+
1199
1797
  // Builder Pattern Support
1200
1798
 
1201
1799
 
@@ -1211,8 +1809,16 @@ export class PDFDancer {
1211
1809
  return objectRefs.map(ref => ImageObject.fromRef(this, ref));
1212
1810
  }
1213
1811
 
1214
- newImage() {
1215
- return new ImageBuilder(this);
1812
+ newImage(pageIndex?: number) {
1813
+ return new ImageBuilder(this, pageIndex);
1814
+ }
1815
+
1816
+ newParagraph(pageIndex?: number) {
1817
+ return new ParagraphBuilder(this, pageIndex);
1818
+ }
1819
+
1820
+ newPage() {
1821
+ return new PageBuilder(this);
1216
1822
  }
1217
1823
 
1218
1824
  page(pageIndex: number) {
@@ -1231,10 +1837,28 @@ export class PDFDancer {
1231
1837
  return objectRefs.map(ref => FormFieldObject.fromRef(this, ref));
1232
1838
  }
1233
1839
 
1840
+ async selectElements(types?: ObjectType[]) {
1841
+ const snapshot = await this.getDocumentSnapshot(types);
1842
+ const elements: ObjectRef[] = [];
1843
+ for (const pageSnapshot of snapshot.pages) {
1844
+ elements.push(...pageSnapshot.elements);
1845
+ }
1846
+ return elements;
1847
+ }
1848
+
1234
1849
  async selectParagraphs() {
1235
1850
  return this.toParagraphObjects(await this.findParagraphs());
1236
1851
  }
1237
1852
 
1853
+ async selectParagraphsMatching(pattern: string) {
1854
+ if (!pattern) {
1855
+ throw new ValidationException('Pattern cannot be empty');
1856
+ }
1857
+ const position = new Position();
1858
+ position.textPattern = pattern;
1859
+ return this.toParagraphObjects(await this.findParagraphs(position));
1860
+ }
1861
+
1238
1862
  private toParagraphObjects(objectRefs: TextObjectRef[]) {
1239
1863
  return objectRefs.map(ref => ParagraphObject.fromRef(this, ref));
1240
1864
  }
@@ -1243,7 +1867,11 @@ export class PDFDancer {
1243
1867
  return objectRefs.map(ref => TextLineObject.fromRef(this, ref));
1244
1868
  }
1245
1869
 
1246
- async selectLines() {
1870
+ async selectTextLines() {
1247
1871
  return this.toTextLineObjects(await this.findTextLines());
1248
1872
  }
1873
+
1874
+ async selectLines() {
1875
+ return this.selectTextLines();
1876
+ }
1249
1877
  }