pdfdancer-client-typescript 1.0.11 → 1.0.13

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 (68) hide show
  1. package/.claude/commands/discuss.md +4 -0
  2. package/.github/workflows/ci.yml +2 -2
  3. package/README.md +2 -2
  4. package/dist/__tests__/e2e/pdf-assertions.d.ts +1 -0
  5. package/dist/__tests__/e2e/pdf-assertions.d.ts.map +1 -1
  6. package/dist/__tests__/e2e/pdf-assertions.js +9 -3
  7. package/dist/__tests__/e2e/pdf-assertions.js.map +1 -1
  8. package/dist/fingerprint.d.ts +12 -0
  9. package/dist/fingerprint.d.ts.map +1 -0
  10. package/dist/fingerprint.js +196 -0
  11. package/dist/fingerprint.js.map +1 -0
  12. package/dist/image-builder.d.ts +4 -2
  13. package/dist/image-builder.d.ts.map +1 -1
  14. package/dist/image-builder.js +12 -3
  15. package/dist/image-builder.js.map +1 -1
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +7 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/models.d.ts +75 -8
  21. package/dist/models.d.ts.map +1 -1
  22. package/dist/models.js +179 -21
  23. package/dist/models.js.map +1 -1
  24. package/dist/page-builder.d.ts +24 -0
  25. package/dist/page-builder.d.ts.map +1 -0
  26. package/dist/page-builder.js +107 -0
  27. package/dist/page-builder.js.map +1 -0
  28. package/dist/paragraph-builder.d.ts +48 -54
  29. package/dist/paragraph-builder.d.ts.map +1 -1
  30. package/dist/paragraph-builder.js +408 -135
  31. package/dist/paragraph-builder.js.map +1 -1
  32. package/dist/pdfdancer_v1.d.ts +90 -9
  33. package/dist/pdfdancer_v1.d.ts.map +1 -1
  34. package/dist/pdfdancer_v1.js +559 -55
  35. package/dist/pdfdancer_v1.js.map +1 -1
  36. package/dist/types.d.ts +24 -3
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js +117 -2
  39. package/dist/types.js.map +1 -1
  40. package/docs/openapi.yml +2076 -0
  41. package/fixtures/Showcase.pdf +0 -0
  42. package/jest.config.js +1 -1
  43. package/package.json +1 -1
  44. package/src/__tests__/e2e/acroform.test.ts +5 -5
  45. package/src/__tests__/e2e/context-manager-showcase.test.ts +267 -0
  46. package/src/__tests__/e2e/form_x_object.test.ts +1 -1
  47. package/src/__tests__/e2e/image-showcase.test.ts +133 -0
  48. package/src/__tests__/e2e/image.test.ts +1 -1
  49. package/src/__tests__/e2e/line-showcase.test.ts +118 -0
  50. package/src/__tests__/e2e/line.test.ts +1 -16
  51. package/src/__tests__/e2e/page-showcase.test.ts +154 -0
  52. package/src/__tests__/e2e/paragraph-showcase.test.ts +523 -0
  53. package/src/__tests__/e2e/paragraph.test.ts +8 -8
  54. package/src/__tests__/e2e/pdf-assertions.ts +10 -3
  55. package/src/__tests__/e2e/pdfdancer-showcase.test.ts +40 -0
  56. package/src/__tests__/e2e/snapshot-showcase.test.ts +158 -0
  57. package/src/__tests__/e2e/snapshot.test.ts +296 -0
  58. package/src/__tests__/e2e/token_from_env.test.ts +85 -25
  59. package/src/__tests__/fingerprint.test.ts +36 -0
  60. package/src/fingerprint.ts +169 -0
  61. package/src/image-builder.ts +13 -6
  62. package/src/index.ts +6 -1
  63. package/src/models.ts +208 -24
  64. package/src/page-builder.ts +130 -0
  65. package/src/paragraph-builder.ts +517 -159
  66. package/src/pdfdancer_v1.ts +662 -58
  67. package/src/types.ts +145 -2
  68. 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,6 +20,7 @@ import {
19
20
  CommandResult,
20
21
  CreatePdfRequest,
21
22
  DeleteRequest,
23
+ DocumentSnapshot,
22
24
  FindRequest,
23
25
  Font,
24
26
  FontRecommendation,
@@ -27,14 +29,15 @@ import {
27
29
  Image,
28
30
  ModifyRequest,
29
31
  ModifyTextRequest,
30
- MoveRequest,
31
32
  MovePageRequest,
33
+ MoveRequest,
32
34
  ObjectRef,
33
35
  ObjectType,
36
+ Orientation,
34
37
  PageRef,
35
38
  PageSize,
36
39
  PageSizeInput,
37
- Orientation,
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.
@@ -225,19 +358,39 @@ export class PDFDancer {
225
358
  private constructor(
226
359
  token: string,
227
360
  pdfData: Uint8Array | File | ArrayBuffer,
228
- baseUrl: string = "http://localhost:8080",
361
+ baseUrl: string | null = null,
229
362
  readTimeout: number = 30000
230
363
  ) {
364
+
231
365
  if (!token || !token.trim()) {
232
366
  throw new ValidationException("Authentication token cannot be null or empty");
233
367
  }
234
368
 
369
+
370
+ // Normalize baseUrl
371
+ const resolvedBaseUrl =
372
+ (baseUrl && baseUrl.trim()) ||
373
+ process.env.PDFDANCER_BASE_URL ||
374
+ "https://api.pdfdancer.com";
375
+
376
+ // Basic validation — ensures it's a valid absolute URL
377
+ try {
378
+ new URL(resolvedBaseUrl);
379
+ } catch {
380
+ throw new ValidationException(`Invalid base URL: ${resolvedBaseUrl}`);
381
+ }
382
+
235
383
  this._token = token.trim();
236
- this._baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
384
+ this._baseUrl = resolvedBaseUrl.replace(/\/$/, ''); // Remove trailing slash
237
385
  this._readTimeout = readTimeout;
238
386
 
239
387
  // Process PDF data with validation
240
388
  this._pdfBytes = this._processPdfData(pdfData);
389
+
390
+ // Initialize caches
391
+ this._documentSnapshotCache = null;
392
+ this._pageSnapshotCache = new Map();
393
+ this._pagesCache = null;
241
394
  }
242
395
 
243
396
  /**
@@ -258,7 +411,7 @@ export class PDFDancer {
258
411
  const resolvedTimeout = timeout ?? 30000;
259
412
 
260
413
  if (!resolvedToken) {
261
- throw new Error("Missing PDFDancer token (pass it explicitly or set PDFDANCER_TOKEN in environment).");
414
+ throw new ValidationException("Missing PDFDancer API token. Pass a token via the `token` argument or set the PDFDANCER_TOKEN environment variable.");
262
415
  }
263
416
 
264
417
  const client = new PDFDancer(resolvedToken, pdfData, resolvedBaseUrl, resolvedTimeout);
@@ -294,7 +447,7 @@ export class PDFDancer {
294
447
  const resolvedTimeout = timeout ?? 30000;
295
448
 
296
449
  if (!resolvedToken) {
297
- throw new Error("Missing PDFDancer token (pass it explicitly or set PDFDANCER_TOKEN in environment).");
450
+ throw new ValidationException("Missing PDFDancer token (pass it explicitly or set PDFDANCER_TOKEN in environment).");
298
451
  }
299
452
 
300
453
  let createRequest: CreatePdfRequest;
@@ -315,17 +468,24 @@ export class PDFDancer {
315
468
  const endpoint = '/session/new'.replace(/^\/+/, '');
316
469
  const url = `${base}/${endpoint}`;
317
470
 
471
+ // Generate fingerprint for this request
472
+ const fingerprint = await generateFingerprint();
473
+
318
474
  // Make request to create endpoint
319
475
  const response = await fetch(url, {
320
476
  method: 'POST',
321
477
  headers: {
322
478
  'Authorization': `Bearer ${resolvedToken}`,
323
- 'Content-Type': 'application/json'
479
+ 'Content-Type': 'application/json',
480
+ 'X-Generated-At': generateTimestamp(),
481
+ 'X-Fingerprint': fingerprint
324
482
  },
325
483
  body: JSON.stringify(createRequest.toDict()),
326
484
  signal: resolvedTimeout > 0 ? AbortSignal.timeout(resolvedTimeout) : undefined
327
485
  });
328
486
 
487
+ logGeneratedAtHeader(response, 'POST', '/session/new');
488
+
329
489
  if (!response.ok) {
330
490
  const errorText = await response.text();
331
491
  throw new HttpClientException(`Failed to create new PDF: ${errorText}`, response);
@@ -343,6 +503,10 @@ export class PDFDancer {
343
503
  client._readTimeout = resolvedTimeout;
344
504
  client._pdfBytes = new Uint8Array();
345
505
  client._sessionId = sessionId;
506
+ // Initialize caches
507
+ client._documentSnapshotCache = null;
508
+ client._pageSnapshotCache = new Map();
509
+ client._pagesCache = null;
346
510
  return client;
347
511
  } catch (error) {
348
512
  if (error instanceof HttpClientException || error instanceof SessionException || error instanceof ValidationException) {
@@ -455,17 +619,33 @@ export class PDFDancer {
455
619
  formData.append('pdf', blob, 'document.pdf');
456
620
  }
457
621
 
622
+ const fingerprint = await this._getFingerprint();
623
+
458
624
  const response = await fetch(this._buildUrl('/session/create'), {
459
625
  method: 'POST',
460
626
  headers: {
461
- 'Authorization': `Bearer ${this._token}`
627
+ 'Authorization': `Bearer ${this._token}`,
628
+ 'X-Generated-At': generateTimestamp(),
629
+ 'X-Fingerprint': fingerprint
462
630
  },
463
631
  body: formData,
464
632
  signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
465
633
  });
466
634
 
635
+ logGeneratedAtHeader(response, 'POST', '/session/create');
636
+
467
637
  if (!response.ok) {
468
638
  const errorMessage = await this._extractErrorMessage(response);
639
+
640
+ if (response.status === 401 || response.status === 403) {
641
+ const defaultMessage = "Authentication with the PDFDancer API failed. Confirm that your API token is valid, has not expired, and is authorized for the requested environment.";
642
+ const normalized = errorMessage?.trim() ?? "";
643
+ const message = normalized && normalized !== "Unauthorized" && normalized !== "Forbidden"
644
+ ? normalized
645
+ : defaultMessage;
646
+ throw new ValidationException(message);
647
+ }
648
+
469
649
  throw new HttpClientException(`Failed to create session: ${errorMessage}`, response);
470
650
  }
471
651
 
@@ -477,7 +657,7 @@ export class PDFDancer {
477
657
 
478
658
  return sessionId;
479
659
  } catch (error) {
480
- if (error instanceof HttpClientException || error instanceof SessionException) {
660
+ if (error instanceof HttpClientException || error instanceof SessionException || error instanceof ValidationException) {
481
661
  throw error;
482
662
  }
483
663
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -485,6 +665,16 @@ export class PDFDancer {
485
665
  }
486
666
  }
487
667
 
668
+ /**
669
+ * Get or generate the fingerprint for this client
670
+ */
671
+ private async _getFingerprint(): Promise<string> {
672
+ if (!this._fingerprintCache) {
673
+ this._fingerprintCache = await generateFingerprint(this._userId);
674
+ }
675
+ return this._fingerprintCache;
676
+ }
677
+
488
678
  /**
489
679
  * Make HTTP request with session headers and error handling.
490
680
  */
@@ -501,10 +691,14 @@ export class PDFDancer {
501
691
  });
502
692
  }
503
693
 
694
+ const fingerprint = await this._getFingerprint();
695
+
504
696
  const headers: Record<string, string> = {
505
697
  'Authorization': `Bearer ${this._token}`,
506
698
  'X-Session-Id': this._sessionId,
507
- 'Content-Type': 'application/json'
699
+ 'Content-Type': 'application/json',
700
+ 'X-Generated-At': generateTimestamp(),
701
+ 'X-Fingerprint': fingerprint
508
702
  };
509
703
 
510
704
  try {
@@ -515,6 +709,8 @@ export class PDFDancer {
515
709
  signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
516
710
  });
517
711
 
712
+ logGeneratedAtHeader(response, method, path);
713
+
518
714
  // Handle FontNotFoundException
519
715
  if (response.status === 404) {
520
716
  try {
@@ -551,13 +747,43 @@ export class PDFDancer {
551
747
  * Searches for PDF objects matching the specified criteria.
552
748
  * This method provides flexible search capabilities across all PDF content,
553
749
  * allowing filtering by object type and position constraints.
750
+ *
751
+ * Now uses snapshot caching for better performance.
554
752
  */
555
753
  private async find(objectType?: ObjectType, position?: Position): Promise<ObjectRef[]> {
556
- const requestData = new FindRequest(objectType, position).toDict();
557
- const response = await this._makeRequest('POST', '/pdf/find', requestData);
754
+ // Determine if we should use snapshot or fall back to HTTP
755
+ // For paths with coordinates, we need to use HTTP (backend requirement)
756
+ const isPathWithCoordinates = objectType === ObjectType.PATH &&
757
+ position?.shape === ShapeType.POINT;
758
+
759
+ if (isPathWithCoordinates) {
760
+ // Fall back to HTTP for path coordinate queries
761
+ const requestData = new FindRequest(objectType, position).toDict();
762
+ const response = await this._makeRequest('POST', '/pdf/find', requestData);
763
+ const objectsData = await response.json() as any[];
764
+ return objectsData.map((objData: any) => this._parseObjectRef(objData));
765
+ }
766
+
767
+ // Use snapshot-based search
768
+ let elements: ObjectRef[];
769
+
770
+ if (position?.pageIndex !== undefined) {
771
+ // Page-specific query - use page snapshot
772
+ const pageSnapshot = await this._getOrFetchPageSnapshot(position.pageIndex);
773
+ elements = pageSnapshot.elements;
774
+ } else {
775
+ // Document-wide query - use document snapshot
776
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
777
+ elements = docSnapshot.getAllElements();
778
+ }
779
+
780
+ // Filter by object type
781
+ if (objectType) {
782
+ elements = elements.filter(el => el.type === objectType);
783
+ }
558
784
 
559
- const objectsData = await response.json() as any[];
560
- return objectsData.map((objData: any) => this._parseObjectRef(objData));
785
+ // Apply position-based filtering
786
+ return this._filterByPosition(elements, position);
561
787
  }
562
788
 
563
789
  /**
@@ -618,43 +844,86 @@ export class PDFDancer {
618
844
  /**
619
845
  * Searches for form fields at the specified position.
620
846
  * Returns FormFieldRef objects with name and value properties.
847
+ *
848
+ * Now uses snapshot caching for better performance.
621
849
  */
622
850
  private async findFormFields(position?: Position): Promise<FormFieldRef[]> {
623
- const requestData = new FindRequest(ObjectType.FORM_FIELD, position).toDict();
624
- const response = await this._makeRequest('POST', '/pdf/find', requestData);
851
+ // Use snapshot-based search
852
+ let elements: ObjectRef[];
853
+
854
+ if (position?.pageIndex !== undefined) {
855
+ // Page-specific query - use page snapshot
856
+ const pageSnapshot = await this._getOrFetchPageSnapshot(position.pageIndex);
857
+ elements = pageSnapshot.elements;
858
+ } else {
859
+ // Document-wide query - use document snapshot
860
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
861
+ elements = docSnapshot.getAllElements();
862
+ }
625
863
 
626
- const objectsData = await response.json() as any[];
627
- return objectsData.map((objData: any) => this._parseFormFieldRef(objData));
864
+ // Filter by form field types (FORM_FIELD, TEXT_FIELD, CHECKBOX, RADIO_BUTTON)
865
+ const formFieldTypes = [
866
+ ObjectType.FORM_FIELD,
867
+ ObjectType.TEXT_FIELD,
868
+ ObjectType.CHECKBOX,
869
+ ObjectType.RADIO_BUTTON
870
+ ];
871
+ const formFields = elements.filter(el => formFieldTypes.includes(el.type)) as FormFieldRef[];
872
+
873
+ // Apply position-based filtering
874
+ return this._filterFormFieldsByPosition(formFields, position);
628
875
  }
629
876
 
630
877
  // Page Operations
631
878
 
632
879
  /**
633
880
  * Retrieves references to all pages in the PDF document.
881
+ * Now uses snapshot caching to avoid HTTP requests.
634
882
  */
635
883
  private async getPages(): Promise<PageRef[]> {
636
- const response = await this._makeRequest('POST', '/pdf/page/find');
637
- const pagesData = await response.json() as any[];
638
- return pagesData.map((pageData: any) => this._parsePageRef(pageData));
884
+ // Check if we have cached pages
885
+ if (this._pagesCache) {
886
+ return this._pagesCache;
887
+ }
888
+
889
+ // Try to get from document snapshot cache first
890
+ if (this._documentSnapshotCache) {
891
+ this._pagesCache = this._documentSnapshotCache.pages.map(p => p.pageRef);
892
+ return this._pagesCache;
893
+ }
894
+
895
+ // Fetch document snapshot to get pages (this will cache it)
896
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
897
+ this._pagesCache = docSnapshot.pages.map(p => p.pageRef);
898
+ return this._pagesCache;
639
899
  }
640
900
 
641
901
  /**
642
902
  * Retrieves a reference to a specific page by its page index.
903
+ * Now uses snapshot caching to avoid HTTP requests.
643
904
  */
644
905
  private async _getPage(pageIndex: number): Promise<PageRef | null> {
645
906
  if (pageIndex < 0) {
646
907
  throw new ValidationException(`Page index must be >= 0, got ${pageIndex}`);
647
908
  }
648
909
 
649
- const params = {pageIndex: pageIndex.toString()};
650
- const response = await this._makeRequest('POST', '/pdf/page/find', undefined, params);
910
+ // Try page snapshot cache first
911
+ if (this._pageSnapshotCache.has(pageIndex)) {
912
+ return this._pageSnapshotCache.get(pageIndex)!.pageRef;
913
+ }
651
914
 
652
- const pagesData = await response.json() as any[];
653
- if (!pagesData || pagesData.length === 0) {
654
- return null;
915
+ // Try document snapshot cache
916
+ if (this._documentSnapshotCache) {
917
+ const pageSnapshot = this._documentSnapshotCache.getPageSnapshot(pageIndex);
918
+ if (pageSnapshot) {
919
+ return pageSnapshot.pageRef;
920
+ }
655
921
  }
656
922
 
657
- return this._parsePageRef(pagesData[0]);
923
+ // Fetch document snapshot to get page (this will cache it)
924
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
925
+ const pageSnapshot = docSnapshot.getPageSnapshot(pageIndex);
926
+ return pageSnapshot?.pageRef ?? null;
658
927
  }
659
928
 
660
929
  /**
@@ -675,6 +944,9 @@ export class PDFDancer {
675
944
  throw new HttpClientException(`Failed to move page from ${pageIndex} to ${targetPageIndex}`, response);
676
945
  }
677
946
 
947
+ // Invalidate cache after mutation
948
+ this._invalidateCache();
949
+
678
950
  // Fetch the page again at its new position for up-to-date metadata
679
951
  return await this._requirePageRef(targetPageIndex);
680
952
  }
@@ -686,7 +958,12 @@ export class PDFDancer {
686
958
  this._validatePageIndex(pageIndex, 'pageIndex');
687
959
 
688
960
  const pageRef = await this._requirePageRef(pageIndex);
689
- return this._deletePage(pageRef);
961
+ const result = await this._deletePage(pageRef);
962
+
963
+ // Invalidate cache after mutation
964
+ this._invalidateCache();
965
+
966
+ return result;
690
967
  }
691
968
 
692
969
  private _validatePageIndex(pageIndex: number, fieldName: string): void {
@@ -719,6 +996,186 @@ export class PDFDancer {
719
996
  return await response.json() as boolean;
720
997
  }
721
998
 
999
+ // Snapshot Operations
1000
+
1001
+ /**
1002
+ * Gets a snapshot of the entire PDF document.
1003
+ * Returns page count, fonts, and snapshots of all pages with their elements.
1004
+ *
1005
+ * @param types Optional array of ObjectType to filter elements by type
1006
+ * @returns DocumentSnapshot containing all document information
1007
+ */
1008
+ async getDocumentSnapshot(types?: ObjectType[]): Promise<DocumentSnapshot> {
1009
+ const params: Record<string, string> = {};
1010
+ if (types && types.length > 0) {
1011
+ params.types = types.join(',');
1012
+ }
1013
+
1014
+ const response = await this._makeRequest('GET', '/pdf/document/snapshot', undefined, params);
1015
+ const data = await response.json() as any;
1016
+
1017
+ return this._parseDocumentSnapshot(data);
1018
+ }
1019
+
1020
+ /**
1021
+ * Gets a snapshot of a specific page.
1022
+ * Returns the page reference and all elements on that page.
1023
+ *
1024
+ * @param pageIndex Zero-based page index
1025
+ * @param types Optional array of ObjectType to filter elements by type
1026
+ * @returns PageSnapshot containing page information and elements
1027
+ */
1028
+ async getPageSnapshot(pageIndex: number, types?: ObjectType[]): Promise<PageSnapshot> {
1029
+ this._validatePageIndex(pageIndex, 'pageIndex');
1030
+
1031
+ const params: Record<string, string> = {};
1032
+ if (types && types.length > 0) {
1033
+ params.types = types.join(',');
1034
+ }
1035
+
1036
+ const response = await this._makeRequest('GET', `/pdf/page/${pageIndex}/snapshot`, undefined, params);
1037
+ const data = await response.json() as any;
1038
+
1039
+ return this._parsePageSnapshot(data);
1040
+ }
1041
+
1042
+ // Cache Management
1043
+
1044
+ /**
1045
+ * Gets a page snapshot from cache or fetches it.
1046
+ * First checks page cache, then document cache, then fetches from server.
1047
+ */
1048
+ private async _getOrFetchPageSnapshot(pageIndex: number): Promise<PageSnapshot> {
1049
+ // Check page cache first
1050
+ if (this._pageSnapshotCache.has(pageIndex)) {
1051
+ return this._pageSnapshotCache.get(pageIndex)!;
1052
+ }
1053
+
1054
+ // Check if we have document snapshot and can extract the page
1055
+ if (this._documentSnapshotCache) {
1056
+ const pageSnapshot = this._documentSnapshotCache.getPageSnapshot(pageIndex);
1057
+ if (pageSnapshot) {
1058
+ // Cache it for future use
1059
+ this._pageSnapshotCache.set(pageIndex, pageSnapshot);
1060
+ return pageSnapshot;
1061
+ }
1062
+ }
1063
+
1064
+ // Fetch page snapshot from server
1065
+ const pageSnapshot = await this.getPageSnapshot(pageIndex);
1066
+ this._pageSnapshotCache.set(pageIndex, pageSnapshot);
1067
+ return pageSnapshot;
1068
+ }
1069
+
1070
+ /**
1071
+ * Gets the document snapshot from cache or fetches it.
1072
+ */
1073
+ private async _getOrFetchDocumentSnapshot(): Promise<DocumentSnapshot> {
1074
+ if (!this._documentSnapshotCache) {
1075
+ this._documentSnapshotCache = await this.getDocumentSnapshot();
1076
+ }
1077
+ return this._documentSnapshotCache;
1078
+ }
1079
+
1080
+ /**
1081
+ * Invalidates all snapshot caches.
1082
+ * Called after any mutation operation.
1083
+ */
1084
+ private _invalidateCache(): void {
1085
+ this._documentSnapshotCache = null;
1086
+ this._pageSnapshotCache.clear();
1087
+ this._pagesCache = null;
1088
+ }
1089
+
1090
+ /**
1091
+ * Filters snapshot elements by Position criteria.
1092
+ * Handles coordinates, text matching, and field name filtering.
1093
+ */
1094
+ private _filterByPosition(elements: ObjectRef[], position?: Position): ObjectRef[] {
1095
+ if (!position) {
1096
+ return elements;
1097
+ }
1098
+
1099
+ let filtered = elements;
1100
+
1101
+ // Filter by page index
1102
+ if (position.pageIndex !== undefined) {
1103
+ filtered = filtered.filter(el => el.position.pageIndex === position.pageIndex);
1104
+ }
1105
+
1106
+ // Filter by coordinates (point containment with tolerance)
1107
+ if (position.boundingRect && position.shape === ShapeType.POINT) {
1108
+ const x = position.boundingRect.x;
1109
+ const y = position.boundingRect.y;
1110
+ const tolerance = position.tolerance || 0;
1111
+ filtered = filtered.filter(el => {
1112
+ const rect = el.position.boundingRect;
1113
+ if (!rect) return false;
1114
+ return x >= rect.x - tolerance && x <= rect.x + rect.width + tolerance &&
1115
+ y >= rect.y - tolerance && y <= rect.y + rect.height + tolerance;
1116
+ });
1117
+ }
1118
+
1119
+ // Filter by text starts with
1120
+ if (position.textStartsWith && filtered.length > 0) {
1121
+ const textLower = position.textStartsWith.toLowerCase();
1122
+ filtered = filtered.filter(el => {
1123
+ const textObj = el as TextObjectRef;
1124
+ return textObj.text && textObj.text.toLowerCase().startsWith(textLower);
1125
+ });
1126
+ }
1127
+
1128
+ // Filter by text pattern (regex)
1129
+ if (position.textPattern && filtered.length > 0) {
1130
+ const regex = this._compileTextPattern(position.textPattern);
1131
+ filtered = filtered.filter(el => {
1132
+ const textObj = el as TextObjectRef;
1133
+ return textObj.text && regex.test(textObj.text);
1134
+ });
1135
+ }
1136
+
1137
+ // Filter by name (for form fields)
1138
+ if (position.name && filtered.length > 0) {
1139
+ filtered = filtered.filter(el => {
1140
+ const formField = el as FormFieldRef;
1141
+ return formField.name === position.name;
1142
+ });
1143
+ }
1144
+
1145
+ return filtered;
1146
+ }
1147
+
1148
+ /**
1149
+ * Filters FormFieldRef elements by Position criteria.
1150
+ */
1151
+ private _filterFormFieldsByPosition(elements: FormFieldRef[], position?: Position): FormFieldRef[] {
1152
+ return this._filterByPosition(elements as ObjectRef[], position) as FormFieldRef[];
1153
+ }
1154
+
1155
+ private _compileTextPattern(pattern: string): RegExp {
1156
+ try {
1157
+ return new RegExp(pattern);
1158
+ } catch {
1159
+ const inlineMatch = pattern.match(/^\(\?([a-z]+)\)/i);
1160
+ if (inlineMatch) {
1161
+ const supportedFlags = inlineMatch[1]
1162
+ .toLowerCase()
1163
+ .split('')
1164
+ .filter(flag => 'gimsuy'.includes(flag));
1165
+ const flags = Array.from(new Set(supportedFlags)).join('');
1166
+ const source = pattern.slice(inlineMatch[0].length);
1167
+ try {
1168
+ return new RegExp(source, flags);
1169
+ } catch {
1170
+ // fall through to literal fallback
1171
+ }
1172
+ }
1173
+
1174
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1175
+ return new RegExp(escaped);
1176
+ }
1177
+ }
1178
+
722
1179
  // Manipulation Operations
723
1180
 
724
1181
  /**
@@ -731,7 +1188,12 @@ export class PDFDancer {
731
1188
 
732
1189
  const requestData = new DeleteRequest(objectRef).toDict();
733
1190
  const response = await this._makeRequest('DELETE', '/pdf/delete', requestData);
734
- return await response.json() as boolean;
1191
+ const result = await response.json() as boolean;
1192
+
1193
+ // Invalidate cache after mutation
1194
+ this._invalidateCache();
1195
+
1196
+ return result;
735
1197
  }
736
1198
 
737
1199
  /**
@@ -747,7 +1209,12 @@ export class PDFDancer {
747
1209
 
748
1210
  const requestData = new MoveRequest(objectRef, position).toDict();
749
1211
  const response = await this._makeRequest('PUT', '/pdf/move', requestData);
750
- return await response.json() as boolean;
1212
+ const result = await response.json() as boolean;
1213
+
1214
+ // Invalidate cache after mutation
1215
+ this._invalidateCache();
1216
+
1217
+ return result;
751
1218
  }
752
1219
 
753
1220
  /**
@@ -760,7 +1227,12 @@ export class PDFDancer {
760
1227
 
761
1228
  const requestData = new ChangeFormFieldRequest(formFieldRef, newValue).toDict();
762
1229
  const response = await this._makeRequest('PUT', '/pdf/modify/formField', requestData);
763
- return await response.json() as boolean;
1230
+ const result = await response.json() as boolean;
1231
+
1232
+ // Invalidate cache after mutation
1233
+ this._invalidateCache();
1234
+
1235
+ return result;
764
1236
  }
765
1237
 
766
1238
  // Add Operations
@@ -804,13 +1276,33 @@ export class PDFDancer {
804
1276
  return this._addObject(paragraph);
805
1277
  }
806
1278
 
1279
+ /**
1280
+ * Adds a page to the PDF document.
1281
+ */
1282
+ private async addPage(request?: AddPageRequest | null): Promise<PageRef> {
1283
+ const payload = request ? request.toDict() : {};
1284
+ const data = Object.keys(payload).length > 0 ? payload : undefined;
1285
+ const response = await this._makeRequest('POST', '/pdf/page/add', data);
1286
+ const result = await response.json();
1287
+ const pageRef = this._parsePageRef(result);
1288
+
1289
+ this._invalidateCache();
1290
+
1291
+ return pageRef;
1292
+ }
1293
+
807
1294
  /**
808
1295
  * Internal method to add any PDF object.
809
1296
  */
810
1297
  private async _addObject(pdfObject: Image | Paragraph): Promise<boolean> {
811
1298
  const requestData = new AddRequest(pdfObject).toDict();
812
1299
  const response = await this._makeRequest('POST', '/pdf/add', requestData);
813
- return await response.json() as boolean;
1300
+ const result = await response.json() as boolean;
1301
+
1302
+ // Invalidate cache after mutation
1303
+ this._invalidateCache();
1304
+
1305
+ return result;
814
1306
  }
815
1307
 
816
1308
  // Modify Operations
@@ -818,7 +1310,7 @@ export class PDFDancer {
818
1310
  /**
819
1311
  * Modifies a paragraph object or its text content.
820
1312
  */
821
- private async modifyParagraph(objectRef: ObjectRef, newParagraph: Paragraph | string): Promise<CommandResult> {
1313
+ private async modifyParagraph(objectRef: ObjectRef, newParagraph: Paragraph | string | null): Promise<CommandResult> {
822
1314
  if (!objectRef) {
823
1315
  throw new ValidationException("Object reference cannot be null");
824
1316
  }
@@ -826,17 +1318,23 @@ export class PDFDancer {
826
1318
  return CommandResult.empty("ModifyParagraph", objectRef.internalId);
827
1319
  }
828
1320
 
1321
+ let result: CommandResult;
829
1322
  if (typeof newParagraph === 'string') {
830
1323
  // Text modification - returns CommandResult
831
1324
  const requestData = new ModifyTextRequest(objectRef, newParagraph).toDict();
832
1325
  const response = await this._makeRequest('PUT', '/pdf/text/paragraph', requestData);
833
- return CommandResult.fromDict(await response.json());
1326
+ result = CommandResult.fromDict(await response.json());
834
1327
  } else {
835
1328
  // Object modification
836
1329
  const requestData = new ModifyRequest(objectRef, newParagraph).toDict();
837
1330
  const response = await this._makeRequest('PUT', '/pdf/modify', requestData);
838
- return CommandResult.fromDict(await response.json());
1331
+ result = CommandResult.fromDict(await response.json());
839
1332
  }
1333
+
1334
+ // Invalidate cache after mutation
1335
+ this._invalidateCache();
1336
+
1337
+ return result;
840
1338
  }
841
1339
 
842
1340
  /**
@@ -852,7 +1350,12 @@ export class PDFDancer {
852
1350
 
853
1351
  const requestData = new ModifyTextRequest(objectRef, newText).toDict();
854
1352
  const response = await this._makeRequest('PUT', '/pdf/text/line', requestData);
855
- return CommandResult.fromDict(await response.json());
1353
+ const result = CommandResult.fromDict(await response.json());
1354
+
1355
+ // Invalidate cache after mutation
1356
+ this._invalidateCache();
1357
+
1358
+ return result;
856
1359
  }
857
1360
 
858
1361
  // Font Operations
@@ -914,16 +1417,22 @@ export class PDFDancer {
914
1417
  const blob = new Blob([fontData.buffer as ArrayBuffer], {type: 'font/ttf'});
915
1418
  formData.append('ttfFile', blob, filename);
916
1419
 
1420
+ const fingerprint = await this._getFingerprint();
1421
+
917
1422
  const response = await fetch(this._buildUrl('/font/register'), {
918
1423
  method: 'POST',
919
1424
  headers: {
920
1425
  'Authorization': `Bearer ${this._token}`,
921
- 'X-Session-Id': this._sessionId
1426
+ 'X-Session-Id': this._sessionId,
1427
+ 'X-Generated-At': generateTimestamp(),
1428
+ 'X-Fingerprint': fingerprint
922
1429
  },
923
1430
  body: formData,
924
1431
  signal: AbortSignal.timeout(30000)
925
1432
  });
926
1433
 
1434
+ logGeneratedAtHeader(response, 'POST', '/font/register');
1435
+
927
1436
  if (!response.ok) {
928
1437
  const errorMessage = await this._extractErrorMessage(response);
929
1438
  throw new HttpClientException(`Font registration failed: ${errorMessage}`, response);
@@ -986,6 +1495,17 @@ export class PDFDancer {
986
1495
  return this._parseTextObjectRef(objData);
987
1496
  }
988
1497
 
1498
+ // Check if this is a form field type
1499
+ const formFieldTypes = [
1500
+ ObjectType.FORM_FIELD,
1501
+ ObjectType.TEXT_FIELD,
1502
+ ObjectType.CHECKBOX,
1503
+ ObjectType.RADIO_BUTTON
1504
+ ];
1505
+ if (formFieldTypes.includes(objectType)) {
1506
+ return this._parseFormFieldRef(objData);
1507
+ }
1508
+
989
1509
  return new ObjectRef(
990
1510
  objData.internalId,
991
1511
  position,
@@ -996,6 +1516,7 @@ export class PDFDancer {
996
1516
  private _isTextObjectData(objData: any, objectType: ObjectType): boolean {
997
1517
  return objectType === ObjectType.PARAGRAPH ||
998
1518
  objectType === ObjectType.TEXT_LINE ||
1519
+ objectType === ObjectType.TEXT_ELEMENT ||
999
1520
  typeof objData.text === 'string' ||
1000
1521
  typeof objData.fontName === 'string' ||
1001
1522
  Array.isArray(objData.children);
@@ -1049,10 +1570,15 @@ export class PDFDancer {
1049
1570
  );
1050
1571
 
1051
1572
  if (Array.isArray(objData.children) && objData.children.length > 0) {
1052
- textObject.children = objData.children.map((childData: any, index: number) => {
1053
- const childFallbackId = `${internalId || 'child'}-${index}`;
1054
- return this._parseTextObjectRef(childData, childFallbackId);
1055
- });
1573
+ try {
1574
+ textObject.children = objData.children.map((childData: any, index: number) => {
1575
+ const childFallbackId = `${internalId || 'child'}-${index}`;
1576
+ return this._parseTextObjectRef(childData, childFallbackId);
1577
+ });
1578
+ } catch (error) {
1579
+ const message = error instanceof Error ? error.message : String(error);
1580
+ console.error(`Failed to parse children of ${internalId}: ${message}`);
1581
+ }
1056
1582
  }
1057
1583
 
1058
1584
  return textObject;
@@ -1171,6 +1697,54 @@ export class PDFDancer {
1171
1697
  return position;
1172
1698
  }
1173
1699
 
1700
+ /**
1701
+ * Parse JSON data into DocumentSnapshot instance.
1702
+ */
1703
+ private _parseDocumentSnapshot(data: any): DocumentSnapshot {
1704
+ const pageCount = typeof data.pageCount === 'number' ? data.pageCount : 0;
1705
+
1706
+ // Parse fonts
1707
+ const fonts: FontRecommendation[] = [];
1708
+ if (Array.isArray(data.fonts)) {
1709
+ for (const fontData of data.fonts) {
1710
+ if (fontData && typeof fontData === 'object') {
1711
+ const fontName = fontData.fontName || '';
1712
+ const fontType = fontData.fontType as FontType || FontType.SYSTEM;
1713
+ const similarityScore = typeof fontData.similarityScore === 'number' ? fontData.similarityScore : 0;
1714
+ fonts.push(new FontRecommendation(fontName, fontType, similarityScore));
1715
+ }
1716
+ }
1717
+ }
1718
+
1719
+ // Parse pages
1720
+ const pages: PageSnapshot[] = [];
1721
+ if (Array.isArray(data.pages)) {
1722
+ for (const pageData of data.pages) {
1723
+ pages.push(this._parsePageSnapshot(pageData));
1724
+ }
1725
+ }
1726
+
1727
+ return new DocumentSnapshot(pageCount, fonts, pages);
1728
+ }
1729
+
1730
+ /**
1731
+ * Parse JSON data into PageSnapshot instance.
1732
+ */
1733
+ private _parsePageSnapshot(data: any): PageSnapshot {
1734
+ // Parse page reference
1735
+ const pageRef = this._parsePageRef(data.pageRef || {});
1736
+
1737
+ // Parse elements
1738
+ const elements: ObjectRef[] = [];
1739
+ if (Array.isArray(data.elements)) {
1740
+ for (const elementData of data.elements) {
1741
+ elements.push(this._parseObjectRef(elementData));
1742
+ }
1743
+ }
1744
+
1745
+ return new PageSnapshot(pageRef, elements);
1746
+ }
1747
+
1174
1748
  // Builder Pattern Support
1175
1749
 
1176
1750
 
@@ -1186,8 +1760,16 @@ export class PDFDancer {
1186
1760
  return objectRefs.map(ref => ImageObject.fromRef(this, ref));
1187
1761
  }
1188
1762
 
1189
- newImage() {
1190
- return new ImageBuilder(this);
1763
+ newImage(pageIndex?: number) {
1764
+ return new ImageBuilder(this, pageIndex);
1765
+ }
1766
+
1767
+ newParagraph(pageIndex?: number) {
1768
+ return new ParagraphBuilder(this, pageIndex);
1769
+ }
1770
+
1771
+ newPage() {
1772
+ return new PageBuilder(this);
1191
1773
  }
1192
1774
 
1193
1775
  page(pageIndex: number) {
@@ -1206,10 +1788,28 @@ export class PDFDancer {
1206
1788
  return objectRefs.map(ref => FormFieldObject.fromRef(this, ref));
1207
1789
  }
1208
1790
 
1791
+ async selectElements(types?: ObjectType[]) {
1792
+ const snapshot = await this.getDocumentSnapshot(types);
1793
+ const elements: ObjectRef[] = [];
1794
+ for (const pageSnapshot of snapshot.pages) {
1795
+ elements.push(...pageSnapshot.elements);
1796
+ }
1797
+ return elements;
1798
+ }
1799
+
1209
1800
  async selectParagraphs() {
1210
1801
  return this.toParagraphObjects(await this.findParagraphs());
1211
1802
  }
1212
1803
 
1804
+ async selectParagraphsMatching(pattern: string) {
1805
+ if (!pattern) {
1806
+ throw new ValidationException('Pattern cannot be empty');
1807
+ }
1808
+ const position = new Position();
1809
+ position.textPattern = pattern;
1810
+ return this.toParagraphObjects(await this.findParagraphs(position));
1811
+ }
1812
+
1213
1813
  private toParagraphObjects(objectRefs: TextObjectRef[]) {
1214
1814
  return objectRefs.map(ref => ParagraphObject.fromRef(this, ref));
1215
1815
  }
@@ -1218,7 +1818,11 @@ export class PDFDancer {
1218
1818
  return objectRefs.map(ref => TextLineObject.fromRef(this, ref));
1219
1819
  }
1220
1820
 
1221
- async selectLines() {
1821
+ async selectTextLines() {
1222
1822
  return this.toTextLineObjects(await this.findTextLines());
1223
1823
  }
1824
+
1825
+ async selectLines() {
1826
+ return this.selectTextLines();
1827
+ }
1224
1828
  }