pdfdancer-client-typescript 1.0.12 → 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 (65) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/README.md +1 -1
  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/fingerprint.d.ts +12 -0
  8. package/dist/fingerprint.d.ts.map +1 -0
  9. package/dist/fingerprint.js +196 -0
  10. package/dist/fingerprint.js.map +1 -0
  11. package/dist/image-builder.d.ts +4 -2
  12. package/dist/image-builder.d.ts.map +1 -1
  13. package/dist/image-builder.js +12 -3
  14. package/dist/image-builder.js.map +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +7 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/models.d.ts +75 -8
  20. package/dist/models.d.ts.map +1 -1
  21. package/dist/models.js +179 -21
  22. package/dist/models.js.map +1 -1
  23. package/dist/page-builder.d.ts +24 -0
  24. package/dist/page-builder.d.ts.map +1 -0
  25. package/dist/page-builder.js +107 -0
  26. package/dist/page-builder.js.map +1 -0
  27. package/dist/paragraph-builder.d.ts +48 -54
  28. package/dist/paragraph-builder.d.ts.map +1 -1
  29. package/dist/paragraph-builder.js +408 -135
  30. package/dist/paragraph-builder.js.map +1 -1
  31. package/dist/pdfdancer_v1.d.ts +90 -9
  32. package/dist/pdfdancer_v1.d.ts.map +1 -1
  33. package/dist/pdfdancer_v1.js +535 -50
  34. package/dist/pdfdancer_v1.js.map +1 -1
  35. package/dist/types.d.ts +24 -3
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js +117 -2
  38. package/dist/types.js.map +1 -1
  39. package/docs/openapi.yml +2076 -0
  40. package/fixtures/Showcase.pdf +0 -0
  41. package/package.json +1 -1
  42. package/src/__tests__/e2e/acroform.test.ts +5 -5
  43. package/src/__tests__/e2e/context-manager-showcase.test.ts +267 -0
  44. package/src/__tests__/e2e/form_x_object.test.ts +1 -1
  45. package/src/__tests__/e2e/image-showcase.test.ts +133 -0
  46. package/src/__tests__/e2e/image.test.ts +1 -1
  47. package/src/__tests__/e2e/line-showcase.test.ts +118 -0
  48. package/src/__tests__/e2e/line.test.ts +1 -16
  49. package/src/__tests__/e2e/page-showcase.test.ts +154 -0
  50. package/src/__tests__/e2e/paragraph-showcase.test.ts +523 -0
  51. package/src/__tests__/e2e/paragraph.test.ts +8 -8
  52. package/src/__tests__/e2e/pdf-assertions.ts +10 -3
  53. package/src/__tests__/e2e/pdfdancer-showcase.test.ts +40 -0
  54. package/src/__tests__/e2e/snapshot-showcase.test.ts +158 -0
  55. package/src/__tests__/e2e/snapshot.test.ts +296 -0
  56. package/src/__tests__/fingerprint.test.ts +36 -0
  57. package/src/fingerprint.ts +169 -0
  58. package/src/image-builder.ts +13 -6
  59. package/src/index.ts +6 -1
  60. package/src/models.ts +208 -24
  61. package/src/page-builder.ts +130 -0
  62. package/src/paragraph-builder.ts +517 -159
  63. package/src/pdfdancer_v1.ts +630 -51
  64. package/src/types.ts +145 -2
  65. package/update-api-spec.sh +3 -0
@@ -12,10 +12,97 @@ exports.PDFDancer = void 0;
12
12
  const exceptions_1 = require("./exceptions");
13
13
  const models_1 = require("./models");
14
14
  const paragraph_builder_1 = require("./paragraph-builder");
15
+ const page_builder_1 = require("./page-builder");
15
16
  const types_1 = require("./types");
16
17
  const image_builder_1 = require("./image-builder");
18
+ const fingerprint_1 = require("./fingerprint");
17
19
  const fs_1 = __importDefault(require("fs"));
18
20
  const node_path_1 = __importDefault(require("node:path"));
21
+ const DEFAULT_TOLERANCE = 0.01;
22
+ // Debug flag - set to true to enable timing logs
23
+ const DEBUG = (process.env.PDFDANCER_CLIENT_DEBUG ?? '').toLowerCase() === 'true' ||
24
+ (process.env.PDFDANCER_CLIENT_DEBUG ?? '') === '1' ||
25
+ (process.env.PDFDANCER_CLIENT_DEBUG ?? '').toLowerCase() === 'yes';
26
+ /**
27
+ * Generate a timestamp string in the format expected by the API.
28
+ * Format: YYYY-MM-DDTHH:MM:SS.ffffffZ (with microseconds)
29
+ */
30
+ function generateTimestamp() {
31
+ const now = new Date();
32
+ const year = now.getUTCFullYear();
33
+ const month = String(now.getUTCMonth() + 1).padStart(2, '0');
34
+ const day = String(now.getUTCDate()).padStart(2, '0');
35
+ const hours = String(now.getUTCHours()).padStart(2, '0');
36
+ const minutes = String(now.getUTCMinutes()).padStart(2, '0');
37
+ const seconds = String(now.getUTCSeconds()).padStart(2, '0');
38
+ const milliseconds = String(now.getUTCMilliseconds()).padStart(3, '0');
39
+ // Add 3 more zeros for microseconds (JavaScript doesn't have microsecond precision)
40
+ const microseconds = milliseconds + '000';
41
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${microseconds}Z`;
42
+ }
43
+ /**
44
+ * Parse timestamp string, handling both microseconds and nanoseconds precision.
45
+ * @param timestampStr Timestamp string in format YYYY-MM-DDTHH:MM:SS.fffffffZ (with 6 or 9 fractional digits)
46
+ */
47
+ function parseTimestamp(timestampStr) {
48
+ // Remove the 'Z' suffix
49
+ let ts = timestampStr.replace(/Z$/, '');
50
+ // Handle nanoseconds (9 digits) by truncating to milliseconds (3 digits)
51
+ // JavaScript's Date only supports millisecond precision
52
+ if (ts.includes('.')) {
53
+ const [datePart, fracPart] = ts.split('.');
54
+ // Truncate to 3 digits (milliseconds)
55
+ const truncatedFrac = fracPart.substring(0, 3);
56
+ ts = `${datePart}.${truncatedFrac}`;
57
+ }
58
+ return new Date(ts + 'Z');
59
+ }
60
+ /**
61
+ * Check for X-Generated-At and X-Received-At headers and log timing information if DEBUG=true.
62
+ *
63
+ * Expected timestamp formats:
64
+ * - 2025-10-24T08:49:39.161945Z (microseconds - 6 digits)
65
+ * - 2025-10-24T08:58:45.468131265Z (nanoseconds - 9 digits)
66
+ */
67
+ function logGeneratedAtHeader(response, method, path) {
68
+ if (!DEBUG) {
69
+ return;
70
+ }
71
+ const generatedAt = response.headers.get('X-Generated-At');
72
+ const receivedAt = response.headers.get('X-Received-At');
73
+ if (generatedAt || receivedAt) {
74
+ try {
75
+ const logParts = [];
76
+ const currentTime = new Date();
77
+ // Parse and log X-Received-At
78
+ let receivedTime = null;
79
+ if (receivedAt) {
80
+ receivedTime = parseTimestamp(receivedAt);
81
+ const timeSinceReceived = (currentTime.getTime() - receivedTime.getTime()) / 1000;
82
+ logParts.push(`X-Received-At: ${receivedAt}, time since received on backend: ${timeSinceReceived.toFixed(3)}s`);
83
+ }
84
+ // Parse and log X-Generated-At
85
+ let generatedTime = null;
86
+ if (generatedAt) {
87
+ generatedTime = parseTimestamp(generatedAt);
88
+ const timeSinceGenerated = (currentTime.getTime() - generatedTime.getTime()) / 1000;
89
+ logParts.push(`X-Generated-At: ${generatedAt}, time since generated on backend: ${timeSinceGenerated.toFixed(3)}s`);
90
+ }
91
+ // Calculate processing time (X-Generated-At - X-Received-At)
92
+ if (receivedTime && generatedTime) {
93
+ const processingTime = (generatedTime.getTime() - receivedTime.getTime()) / 1000;
94
+ logParts.push(`processing time on backend: ${processingTime.toFixed(3)}s`);
95
+ }
96
+ if (logParts.length > 0) {
97
+ console.log(`${Date.now() / 1000}|${method} ${path} - ${logParts.join(', ')}`);
98
+ }
99
+ }
100
+ catch (e) {
101
+ const errorMessage = e instanceof Error ? e.message : String(e);
102
+ console.log(`${Date.now() / 1000}|${method} ${path} - Header parse error: ${errorMessage}`);
103
+ }
104
+ }
105
+ }
19
106
  class PageClient {
20
107
  constructor(client, pageIndex, pageRef) {
21
108
  this.type = models_1.ObjectType.PAGE;
@@ -28,8 +115,8 @@ class PageClient {
28
115
  // Cast to the internal interface to get access
29
116
  this._internals = this._client;
30
117
  }
31
- async selectPathsAt(x, y) {
32
- return this._internals.toPathObjects(await this._internals.findPaths(models_1.Position.atPageCoordinates(this._pageIndex, x, y)));
118
+ async selectPathsAt(x, y, tolerance = 0) {
119
+ return this._internals.toPathObjects(await this._internals.findPaths(models_1.Position.atPageCoordinates(this._pageIndex, x, y, tolerance)));
33
120
  }
34
121
  async selectPaths() {
35
122
  return this._internals.toPathObjects(await this._internals.findPaths(models_1.Position.atPage(this._pageIndex)));
@@ -37,8 +124,8 @@ class PageClient {
37
124
  async selectImages() {
38
125
  return this._internals.toImageObjects(await this._internals._findImages(models_1.Position.atPage(this._pageIndex)));
39
126
  }
40
- async selectImagesAt(x, y) {
41
- return this._internals.toImageObjects(await this._internals._findImages(models_1.Position.atPageCoordinates(this._pageIndex, x, y)));
127
+ async selectImagesAt(x, y, tolerance = 0) {
128
+ return this._internals.toImageObjects(await this._internals._findImages(models_1.Position.atPageCoordinates(this._pageIndex, x, y, tolerance)));
42
129
  }
43
130
  async delete() {
44
131
  return this._client.deletePage(this._pageIndex);
@@ -56,14 +143,14 @@ class PageClient {
56
143
  async selectForms() {
57
144
  return this._internals.toFormXObjects(await this._internals.findFormXObjects(models_1.Position.atPage(this._pageIndex)));
58
145
  }
59
- async selectFormsAt(x, y) {
60
- return this._internals.toFormXObjects(await this._internals.findFormXObjects(models_1.Position.atPageCoordinates(this._pageIndex, x, y)));
146
+ async selectFormsAt(x, y, tolerance = 0) {
147
+ return this._internals.toFormXObjects(await this._internals.findFormXObjects(models_1.Position.atPageCoordinates(this._pageIndex, x, y, tolerance)));
61
148
  }
62
149
  async selectFormFields() {
63
150
  return this._internals.toFormFields(await this._internals.findFormFields(models_1.Position.atPage(this._pageIndex)));
64
151
  }
65
- async selectFormFieldsAt(x, y) {
66
- return this._internals.toFormFields(await this._internals.findFormFields(models_1.Position.atPageCoordinates(this._pageIndex, x, y)));
152
+ async selectFormFieldsAt(x, y, tolerance = 0) {
153
+ return this._internals.toFormFields(await this._internals.findFormFields(models_1.Position.atPageCoordinates(this._pageIndex, x, y, tolerance)));
67
154
  }
68
155
  // noinspection JSUnusedGlobalSymbols
69
156
  async selectFormFieldsByName(fieldName) {
@@ -74,6 +161,10 @@ class PageClient {
74
161
  async selectParagraphs() {
75
162
  return this._internals.toParagraphObjects(await this._internals.findParagraphs(models_1.Position.atPage(this._pageIndex)));
76
163
  }
164
+ async selectElements(types) {
165
+ const snapshot = await this._client.getPageSnapshot(this._pageIndex, types);
166
+ return snapshot.elements;
167
+ }
77
168
  async selectParagraphsStartingWith(text) {
78
169
  let pos = models_1.Position.atPage(this._pageIndex);
79
170
  pos.textStartsWith = text;
@@ -84,8 +175,8 @@ class PageClient {
84
175
  pos.textPattern = pattern;
85
176
  return this._internals.toParagraphObjects(await this._internals.findParagraphs(pos));
86
177
  }
87
- async selectParagraphsAt(x, y) {
88
- return this._internals.toParagraphObjects(await this._internals.findParagraphs(models_1.Position.atPageCoordinates(this._pageIndex, x, y)));
178
+ async selectParagraphsAt(x, y, tolerance = DEFAULT_TOLERANCE) {
179
+ return this._internals.toParagraphObjects(await this._internals.findParagraphs(models_1.Position.atPageCoordinates(this._pageIndex, x, y, tolerance)));
89
180
  }
90
181
  async selectTextLinesStartingWith(text) {
91
182
  let pos = models_1.Position.atPage(this._pageIndex);
@@ -95,8 +186,13 @@ class PageClient {
95
186
  /**
96
187
  * Creates a new ParagraphBuilder for fluent paragraph construction.
97
188
  */
98
- newParagraph() {
99
- return new paragraph_builder_1.ParagraphBuilder(this._client, this.position.pageIndex);
189
+ newParagraph(pageIndex) {
190
+ const targetIndex = pageIndex ?? this.position.pageIndex;
191
+ return new paragraph_builder_1.ParagraphBuilder(this._client, targetIndex);
192
+ }
193
+ newImage(pageIndex) {
194
+ const targetIndex = pageIndex ?? this.position.pageIndex;
195
+ return new image_builder_1.ImageBuilder(this._client, targetIndex);
100
196
  }
101
197
  async selectTextLines() {
102
198
  return this._internals.toTextLineObjects(await this._internals.findTextLines(models_1.Position.atPage(this._pageIndex)));
@@ -108,8 +204,15 @@ class PageClient {
108
204
  return this._internals.toTextLineObjects(await this._internals.findTextLines(pos));
109
205
  }
110
206
  // noinspection JSUnusedGlobalSymbols
111
- async selectTextLinesAt(x, y) {
112
- return this._internals.toTextLineObjects(await this._internals.findTextLines(models_1.Position.atPageCoordinates(this._pageIndex, x, y)));
207
+ async selectTextLinesAt(x, y, tolerance = DEFAULT_TOLERANCE) {
208
+ return this._internals.toTextLineObjects(await this._internals.findTextLines(models_1.Position.atPageCoordinates(this._pageIndex, x, y, tolerance)));
209
+ }
210
+ /**
211
+ * Gets a snapshot of this page, including all elements.
212
+ * Optionally filter by object types.
213
+ */
214
+ async getSnapshot(types) {
215
+ return this._client.getPageSnapshot(this._pageIndex, types);
113
216
  }
114
217
  }
115
218
  // noinspection ExceptionCaughtLocallyJS,JSUnusedLocalSymbols
@@ -127,6 +230,10 @@ class PDFDancer {
127
230
  * a new session, and prepares the client for PDF manipulation operations.
128
231
  */
129
232
  constructor(token, pdfData, baseUrl = null, readTimeout = 30000) {
233
+ // Snapshot caches for optimizing find operations
234
+ this._documentSnapshotCache = null;
235
+ this._pageSnapshotCache = new Map();
236
+ this._pagesCache = null;
130
237
  if (!token || !token.trim()) {
131
238
  throw new exceptions_1.ValidationException("Authentication token cannot be null or empty");
132
239
  }
@@ -146,6 +253,10 @@ class PDFDancer {
146
253
  this._readTimeout = readTimeout;
147
254
  // Process PDF data with validation
148
255
  this._pdfBytes = this._processPdfData(pdfData);
256
+ // Initialize caches
257
+ this._documentSnapshotCache = null;
258
+ this._pageSnapshotCache = new Map();
259
+ this._pagesCache = null;
149
260
  }
150
261
  /**
151
262
  * Initialize the client by creating a session.
@@ -200,16 +311,21 @@ class PDFDancer {
200
311
  const base = resolvedBaseUrl.replace(/\/+$/, '');
201
312
  const endpoint = '/session/new'.replace(/^\/+/, '');
202
313
  const url = `${base}/${endpoint}`;
314
+ // Generate fingerprint for this request
315
+ const fingerprint = await (0, fingerprint_1.generateFingerprint)();
203
316
  // Make request to create endpoint
204
317
  const response = await fetch(url, {
205
318
  method: 'POST',
206
319
  headers: {
207
320
  'Authorization': `Bearer ${resolvedToken}`,
208
- 'Content-Type': 'application/json'
321
+ 'Content-Type': 'application/json',
322
+ 'X-Generated-At': generateTimestamp(),
323
+ 'X-Fingerprint': fingerprint
209
324
  },
210
325
  body: JSON.stringify(createRequest.toDict()),
211
326
  signal: resolvedTimeout > 0 ? AbortSignal.timeout(resolvedTimeout) : undefined
212
327
  });
328
+ logGeneratedAtHeader(response, 'POST', '/session/new');
213
329
  if (!response.ok) {
214
330
  const errorText = await response.text();
215
331
  throw new exceptions_1.HttpClientException(`Failed to create new PDF: ${errorText}`, response);
@@ -224,6 +340,10 @@ class PDFDancer {
224
340
  client._readTimeout = resolvedTimeout;
225
341
  client._pdfBytes = new Uint8Array();
226
342
  client._sessionId = sessionId;
343
+ // Initialize caches
344
+ client._documentSnapshotCache = null;
345
+ client._pageSnapshotCache = new Map();
346
+ client._pagesCache = null;
227
347
  return client;
228
348
  }
229
349
  catch (error) {
@@ -332,14 +452,18 @@ class PDFDancer {
332
452
  const blob = new Blob([this._pdfBytes.buffer], { type: 'application/pdf' });
333
453
  formData.append('pdf', blob, 'document.pdf');
334
454
  }
455
+ const fingerprint = await this._getFingerprint();
335
456
  const response = await fetch(this._buildUrl('/session/create'), {
336
457
  method: 'POST',
337
458
  headers: {
338
- 'Authorization': `Bearer ${this._token}`
459
+ 'Authorization': `Bearer ${this._token}`,
460
+ 'X-Generated-At': generateTimestamp(),
461
+ 'X-Fingerprint': fingerprint
339
462
  },
340
463
  body: formData,
341
464
  signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
342
465
  });
466
+ logGeneratedAtHeader(response, 'POST', '/session/create');
343
467
  if (!response.ok) {
344
468
  const errorMessage = await this._extractErrorMessage(response);
345
469
  if (response.status === 401 || response.status === 403) {
@@ -366,6 +490,15 @@ class PDFDancer {
366
490
  throw new exceptions_1.HttpClientException(`Failed to create session: ${errorMessage}`, undefined, error);
367
491
  }
368
492
  }
493
+ /**
494
+ * Get or generate the fingerprint for this client
495
+ */
496
+ async _getFingerprint() {
497
+ if (!this._fingerprintCache) {
498
+ this._fingerprintCache = await (0, fingerprint_1.generateFingerprint)(this._userId);
499
+ }
500
+ return this._fingerprintCache;
501
+ }
369
502
  /**
370
503
  * Make HTTP request with session headers and error handling.
371
504
  */
@@ -376,10 +509,13 @@ class PDFDancer {
376
509
  url.searchParams.append(key, value);
377
510
  });
378
511
  }
512
+ const fingerprint = await this._getFingerprint();
379
513
  const headers = {
380
514
  'Authorization': `Bearer ${this._token}`,
381
515
  'X-Session-Id': this._sessionId,
382
- 'Content-Type': 'application/json'
516
+ 'Content-Type': 'application/json',
517
+ 'X-Generated-At': generateTimestamp(),
518
+ 'X-Fingerprint': fingerprint
383
519
  };
384
520
  try {
385
521
  const response = await fetch(url.toString(), {
@@ -388,6 +524,7 @@ class PDFDancer {
388
524
  body: data ? JSON.stringify(data) : undefined,
389
525
  signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
390
526
  });
527
+ logGeneratedAtHeader(response, method, path);
391
528
  // Handle FontNotFoundException
392
529
  if (response.status === 404) {
393
530
  try {
@@ -422,12 +559,39 @@ class PDFDancer {
422
559
  * Searches for PDF objects matching the specified criteria.
423
560
  * This method provides flexible search capabilities across all PDF content,
424
561
  * allowing filtering by object type and position constraints.
562
+ *
563
+ * Now uses snapshot caching for better performance.
425
564
  */
426
565
  async find(objectType, position) {
427
- const requestData = new models_1.FindRequest(objectType, position).toDict();
428
- const response = await this._makeRequest('POST', '/pdf/find', requestData);
429
- const objectsData = await response.json();
430
- return objectsData.map((objData) => this._parseObjectRef(objData));
566
+ // Determine if we should use snapshot or fall back to HTTP
567
+ // For paths with coordinates, we need to use HTTP (backend requirement)
568
+ const isPathWithCoordinates = objectType === models_1.ObjectType.PATH &&
569
+ position?.shape === models_1.ShapeType.POINT;
570
+ if (isPathWithCoordinates) {
571
+ // Fall back to HTTP for path coordinate queries
572
+ const requestData = new models_1.FindRequest(objectType, position).toDict();
573
+ const response = await this._makeRequest('POST', '/pdf/find', requestData);
574
+ const objectsData = await response.json();
575
+ return objectsData.map((objData) => this._parseObjectRef(objData));
576
+ }
577
+ // Use snapshot-based search
578
+ let elements;
579
+ if (position?.pageIndex !== undefined) {
580
+ // Page-specific query - use page snapshot
581
+ const pageSnapshot = await this._getOrFetchPageSnapshot(position.pageIndex);
582
+ elements = pageSnapshot.elements;
583
+ }
584
+ else {
585
+ // Document-wide query - use document snapshot
586
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
587
+ elements = docSnapshot.getAllElements();
588
+ }
589
+ // Filter by object type
590
+ if (objectType) {
591
+ elements = elements.filter(el => el.type === objectType);
592
+ }
593
+ // Apply position-based filtering
594
+ return this._filterByPosition(elements, position);
431
595
  }
432
596
  /**
433
597
  * Searches for paragraph objects at the specified position.
@@ -477,36 +641,76 @@ class PDFDancer {
477
641
  /**
478
642
  * Searches for form fields at the specified position.
479
643
  * Returns FormFieldRef objects with name and value properties.
644
+ *
645
+ * Now uses snapshot caching for better performance.
480
646
  */
481
647
  async findFormFields(position) {
482
- const requestData = new models_1.FindRequest(models_1.ObjectType.FORM_FIELD, position).toDict();
483
- const response = await this._makeRequest('POST', '/pdf/find', requestData);
484
- const objectsData = await response.json();
485
- return objectsData.map((objData) => this._parseFormFieldRef(objData));
648
+ // Use snapshot-based search
649
+ let elements;
650
+ if (position?.pageIndex !== undefined) {
651
+ // Page-specific query - use page snapshot
652
+ const pageSnapshot = await this._getOrFetchPageSnapshot(position.pageIndex);
653
+ elements = pageSnapshot.elements;
654
+ }
655
+ else {
656
+ // Document-wide query - use document snapshot
657
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
658
+ elements = docSnapshot.getAllElements();
659
+ }
660
+ // Filter by form field types (FORM_FIELD, TEXT_FIELD, CHECKBOX, RADIO_BUTTON)
661
+ const formFieldTypes = [
662
+ models_1.ObjectType.FORM_FIELD,
663
+ models_1.ObjectType.TEXT_FIELD,
664
+ models_1.ObjectType.CHECKBOX,
665
+ models_1.ObjectType.RADIO_BUTTON
666
+ ];
667
+ const formFields = elements.filter(el => formFieldTypes.includes(el.type));
668
+ // Apply position-based filtering
669
+ return this._filterFormFieldsByPosition(formFields, position);
486
670
  }
487
671
  // Page Operations
488
672
  /**
489
673
  * Retrieves references to all pages in the PDF document.
674
+ * Now uses snapshot caching to avoid HTTP requests.
490
675
  */
491
676
  async getPages() {
492
- const response = await this._makeRequest('POST', '/pdf/page/find');
493
- const pagesData = await response.json();
494
- return pagesData.map((pageData) => this._parsePageRef(pageData));
677
+ // Check if we have cached pages
678
+ if (this._pagesCache) {
679
+ return this._pagesCache;
680
+ }
681
+ // Try to get from document snapshot cache first
682
+ if (this._documentSnapshotCache) {
683
+ this._pagesCache = this._documentSnapshotCache.pages.map(p => p.pageRef);
684
+ return this._pagesCache;
685
+ }
686
+ // Fetch document snapshot to get pages (this will cache it)
687
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
688
+ this._pagesCache = docSnapshot.pages.map(p => p.pageRef);
689
+ return this._pagesCache;
495
690
  }
496
691
  /**
497
692
  * Retrieves a reference to a specific page by its page index.
693
+ * Now uses snapshot caching to avoid HTTP requests.
498
694
  */
499
695
  async _getPage(pageIndex) {
500
696
  if (pageIndex < 0) {
501
697
  throw new exceptions_1.ValidationException(`Page index must be >= 0, got ${pageIndex}`);
502
698
  }
503
- const params = { pageIndex: pageIndex.toString() };
504
- const response = await this._makeRequest('POST', '/pdf/page/find', undefined, params);
505
- const pagesData = await response.json();
506
- if (!pagesData || pagesData.length === 0) {
507
- return null;
699
+ // Try page snapshot cache first
700
+ if (this._pageSnapshotCache.has(pageIndex)) {
701
+ return this._pageSnapshotCache.get(pageIndex).pageRef;
702
+ }
703
+ // Try document snapshot cache
704
+ if (this._documentSnapshotCache) {
705
+ const pageSnapshot = this._documentSnapshotCache.getPageSnapshot(pageIndex);
706
+ if (pageSnapshot) {
707
+ return pageSnapshot.pageRef;
708
+ }
508
709
  }
509
- return this._parsePageRef(pagesData[0]);
710
+ // Fetch document snapshot to get page (this will cache it)
711
+ const docSnapshot = await this._getOrFetchDocumentSnapshot();
712
+ const pageSnapshot = docSnapshot.getPageSnapshot(pageIndex);
713
+ return pageSnapshot?.pageRef ?? null;
510
714
  }
511
715
  /**
512
716
  * Moves an existing page to a new index.
@@ -522,6 +726,8 @@ class PDFDancer {
522
726
  if (!success) {
523
727
  throw new exceptions_1.HttpClientException(`Failed to move page from ${pageIndex} to ${targetPageIndex}`, response);
524
728
  }
729
+ // Invalidate cache after mutation
730
+ this._invalidateCache();
525
731
  // Fetch the page again at its new position for up-to-date metadata
526
732
  return await this._requirePageRef(targetPageIndex);
527
733
  }
@@ -531,7 +737,10 @@ class PDFDancer {
531
737
  async deletePage(pageIndex) {
532
738
  this._validatePageIndex(pageIndex, 'pageIndex');
533
739
  const pageRef = await this._requirePageRef(pageIndex);
534
- return this._deletePage(pageRef);
740
+ const result = await this._deletePage(pageRef);
741
+ // Invalidate cache after mutation
742
+ this._invalidateCache();
743
+ return result;
535
744
  }
536
745
  _validatePageIndex(pageIndex, fieldName) {
537
746
  if (!Number.isInteger(pageIndex)) {
@@ -559,6 +768,164 @@ class PDFDancer {
559
768
  const response = await this._makeRequest('DELETE', '/pdf/page/delete', requestData);
560
769
  return await response.json();
561
770
  }
771
+ // Snapshot Operations
772
+ /**
773
+ * Gets a snapshot of the entire PDF document.
774
+ * Returns page count, fonts, and snapshots of all pages with their elements.
775
+ *
776
+ * @param types Optional array of ObjectType to filter elements by type
777
+ * @returns DocumentSnapshot containing all document information
778
+ */
779
+ async getDocumentSnapshot(types) {
780
+ const params = {};
781
+ if (types && types.length > 0) {
782
+ params.types = types.join(',');
783
+ }
784
+ const response = await this._makeRequest('GET', '/pdf/document/snapshot', undefined, params);
785
+ const data = await response.json();
786
+ return this._parseDocumentSnapshot(data);
787
+ }
788
+ /**
789
+ * Gets a snapshot of a specific page.
790
+ * Returns the page reference and all elements on that page.
791
+ *
792
+ * @param pageIndex Zero-based page index
793
+ * @param types Optional array of ObjectType to filter elements by type
794
+ * @returns PageSnapshot containing page information and elements
795
+ */
796
+ async getPageSnapshot(pageIndex, types) {
797
+ this._validatePageIndex(pageIndex, 'pageIndex');
798
+ const params = {};
799
+ if (types && types.length > 0) {
800
+ params.types = types.join(',');
801
+ }
802
+ const response = await this._makeRequest('GET', `/pdf/page/${pageIndex}/snapshot`, undefined, params);
803
+ const data = await response.json();
804
+ return this._parsePageSnapshot(data);
805
+ }
806
+ // Cache Management
807
+ /**
808
+ * Gets a page snapshot from cache or fetches it.
809
+ * First checks page cache, then document cache, then fetches from server.
810
+ */
811
+ async _getOrFetchPageSnapshot(pageIndex) {
812
+ // Check page cache first
813
+ if (this._pageSnapshotCache.has(pageIndex)) {
814
+ return this._pageSnapshotCache.get(pageIndex);
815
+ }
816
+ // Check if we have document snapshot and can extract the page
817
+ if (this._documentSnapshotCache) {
818
+ const pageSnapshot = this._documentSnapshotCache.getPageSnapshot(pageIndex);
819
+ if (pageSnapshot) {
820
+ // Cache it for future use
821
+ this._pageSnapshotCache.set(pageIndex, pageSnapshot);
822
+ return pageSnapshot;
823
+ }
824
+ }
825
+ // Fetch page snapshot from server
826
+ const pageSnapshot = await this.getPageSnapshot(pageIndex);
827
+ this._pageSnapshotCache.set(pageIndex, pageSnapshot);
828
+ return pageSnapshot;
829
+ }
830
+ /**
831
+ * Gets the document snapshot from cache or fetches it.
832
+ */
833
+ async _getOrFetchDocumentSnapshot() {
834
+ if (!this._documentSnapshotCache) {
835
+ this._documentSnapshotCache = await this.getDocumentSnapshot();
836
+ }
837
+ return this._documentSnapshotCache;
838
+ }
839
+ /**
840
+ * Invalidates all snapshot caches.
841
+ * Called after any mutation operation.
842
+ */
843
+ _invalidateCache() {
844
+ this._documentSnapshotCache = null;
845
+ this._pageSnapshotCache.clear();
846
+ this._pagesCache = null;
847
+ }
848
+ /**
849
+ * Filters snapshot elements by Position criteria.
850
+ * Handles coordinates, text matching, and field name filtering.
851
+ */
852
+ _filterByPosition(elements, position) {
853
+ if (!position) {
854
+ return elements;
855
+ }
856
+ let filtered = elements;
857
+ // Filter by page index
858
+ if (position.pageIndex !== undefined) {
859
+ filtered = filtered.filter(el => el.position.pageIndex === position.pageIndex);
860
+ }
861
+ // Filter by coordinates (point containment with tolerance)
862
+ if (position.boundingRect && position.shape === models_1.ShapeType.POINT) {
863
+ const x = position.boundingRect.x;
864
+ const y = position.boundingRect.y;
865
+ const tolerance = position.tolerance || 0;
866
+ filtered = filtered.filter(el => {
867
+ const rect = el.position.boundingRect;
868
+ if (!rect)
869
+ return false;
870
+ return x >= rect.x - tolerance && x <= rect.x + rect.width + tolerance &&
871
+ y >= rect.y - tolerance && y <= rect.y + rect.height + tolerance;
872
+ });
873
+ }
874
+ // Filter by text starts with
875
+ if (position.textStartsWith && filtered.length > 0) {
876
+ const textLower = position.textStartsWith.toLowerCase();
877
+ filtered = filtered.filter(el => {
878
+ const textObj = el;
879
+ return textObj.text && textObj.text.toLowerCase().startsWith(textLower);
880
+ });
881
+ }
882
+ // Filter by text pattern (regex)
883
+ if (position.textPattern && filtered.length > 0) {
884
+ const regex = this._compileTextPattern(position.textPattern);
885
+ filtered = filtered.filter(el => {
886
+ const textObj = el;
887
+ return textObj.text && regex.test(textObj.text);
888
+ });
889
+ }
890
+ // Filter by name (for form fields)
891
+ if (position.name && filtered.length > 0) {
892
+ filtered = filtered.filter(el => {
893
+ const formField = el;
894
+ return formField.name === position.name;
895
+ });
896
+ }
897
+ return filtered;
898
+ }
899
+ /**
900
+ * Filters FormFieldRef elements by Position criteria.
901
+ */
902
+ _filterFormFieldsByPosition(elements, position) {
903
+ return this._filterByPosition(elements, position);
904
+ }
905
+ _compileTextPattern(pattern) {
906
+ try {
907
+ return new RegExp(pattern);
908
+ }
909
+ catch {
910
+ const inlineMatch = pattern.match(/^\(\?([a-z]+)\)/i);
911
+ if (inlineMatch) {
912
+ const supportedFlags = inlineMatch[1]
913
+ .toLowerCase()
914
+ .split('')
915
+ .filter(flag => 'gimsuy'.includes(flag));
916
+ const flags = Array.from(new Set(supportedFlags)).join('');
917
+ const source = pattern.slice(inlineMatch[0].length);
918
+ try {
919
+ return new RegExp(source, flags);
920
+ }
921
+ catch {
922
+ // fall through to literal fallback
923
+ }
924
+ }
925
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
926
+ return new RegExp(escaped);
927
+ }
928
+ }
562
929
  // Manipulation Operations
563
930
  /**
564
931
  * Deletes the specified PDF object from the document.
@@ -569,7 +936,10 @@ class PDFDancer {
569
936
  }
570
937
  const requestData = new models_1.DeleteRequest(objectRef).toDict();
571
938
  const response = await this._makeRequest('DELETE', '/pdf/delete', requestData);
572
- return await response.json();
939
+ const result = await response.json();
940
+ // Invalidate cache after mutation
941
+ this._invalidateCache();
942
+ return result;
573
943
  }
574
944
  /**
575
945
  * Moves a PDF object to a new position within the document.
@@ -583,7 +953,10 @@ class PDFDancer {
583
953
  }
584
954
  const requestData = new models_1.MoveRequest(objectRef, position).toDict();
585
955
  const response = await this._makeRequest('PUT', '/pdf/move', requestData);
586
- return await response.json();
956
+ const result = await response.json();
957
+ // Invalidate cache after mutation
958
+ this._invalidateCache();
959
+ return result;
587
960
  }
588
961
  /**
589
962
  * Changes the value of a form field.
@@ -594,7 +967,10 @@ class PDFDancer {
594
967
  }
595
968
  const requestData = new models_1.ChangeFormFieldRequest(formFieldRef, newValue).toDict();
596
969
  const response = await this._makeRequest('PUT', '/pdf/modify/formField', requestData);
597
- return await response.json();
970
+ const result = await response.json();
971
+ // Invalidate cache after mutation
972
+ this._invalidateCache();
973
+ return result;
598
974
  }
599
975
  // Add Operations
600
976
  /**
@@ -630,13 +1006,28 @@ class PDFDancer {
630
1006
  }
631
1007
  return this._addObject(paragraph);
632
1008
  }
1009
+ /**
1010
+ * Adds a page to the PDF document.
1011
+ */
1012
+ async addPage(request) {
1013
+ const payload = request ? request.toDict() : {};
1014
+ const data = Object.keys(payload).length > 0 ? payload : undefined;
1015
+ const response = await this._makeRequest('POST', '/pdf/page/add', data);
1016
+ const result = await response.json();
1017
+ const pageRef = this._parsePageRef(result);
1018
+ this._invalidateCache();
1019
+ return pageRef;
1020
+ }
633
1021
  /**
634
1022
  * Internal method to add any PDF object.
635
1023
  */
636
1024
  async _addObject(pdfObject) {
637
1025
  const requestData = new models_1.AddRequest(pdfObject).toDict();
638
1026
  const response = await this._makeRequest('POST', '/pdf/add', requestData);
639
- return await response.json();
1027
+ const result = await response.json();
1028
+ // Invalidate cache after mutation
1029
+ this._invalidateCache();
1030
+ return result;
640
1031
  }
641
1032
  // Modify Operations
642
1033
  /**
@@ -649,18 +1040,22 @@ class PDFDancer {
649
1040
  if (newParagraph === null || newParagraph === undefined) {
650
1041
  return models_1.CommandResult.empty("ModifyParagraph", objectRef.internalId);
651
1042
  }
1043
+ let result;
652
1044
  if (typeof newParagraph === 'string') {
653
1045
  // Text modification - returns CommandResult
654
1046
  const requestData = new models_1.ModifyTextRequest(objectRef, newParagraph).toDict();
655
1047
  const response = await this._makeRequest('PUT', '/pdf/text/paragraph', requestData);
656
- return models_1.CommandResult.fromDict(await response.json());
1048
+ result = models_1.CommandResult.fromDict(await response.json());
657
1049
  }
658
1050
  else {
659
1051
  // Object modification
660
1052
  const requestData = new models_1.ModifyRequest(objectRef, newParagraph).toDict();
661
1053
  const response = await this._makeRequest('PUT', '/pdf/modify', requestData);
662
- return models_1.CommandResult.fromDict(await response.json());
1054
+ result = models_1.CommandResult.fromDict(await response.json());
663
1055
  }
1056
+ // Invalidate cache after mutation
1057
+ this._invalidateCache();
1058
+ return result;
664
1059
  }
665
1060
  /**
666
1061
  * Modifies a text line object.
@@ -674,7 +1069,10 @@ class PDFDancer {
674
1069
  }
675
1070
  const requestData = new models_1.ModifyTextRequest(objectRef, newText).toDict();
676
1071
  const response = await this._makeRequest('PUT', '/pdf/text/line', requestData);
677
- return models_1.CommandResult.fromDict(await response.json());
1072
+ const result = models_1.CommandResult.fromDict(await response.json());
1073
+ // Invalidate cache after mutation
1074
+ this._invalidateCache();
1075
+ return result;
678
1076
  }
679
1077
  // Font Operations
680
1078
  /**
@@ -730,15 +1128,19 @@ class PDFDancer {
730
1128
  const formData = new FormData();
731
1129
  const blob = new Blob([fontData.buffer], { type: 'font/ttf' });
732
1130
  formData.append('ttfFile', blob, filename);
1131
+ const fingerprint = await this._getFingerprint();
733
1132
  const response = await fetch(this._buildUrl('/font/register'), {
734
1133
  method: 'POST',
735
1134
  headers: {
736
1135
  'Authorization': `Bearer ${this._token}`,
737
- 'X-Session-Id': this._sessionId
1136
+ 'X-Session-Id': this._sessionId,
1137
+ 'X-Generated-At': generateTimestamp(),
1138
+ 'X-Fingerprint': fingerprint
738
1139
  },
739
1140
  body: formData,
740
1141
  signal: AbortSignal.timeout(30000)
741
1142
  });
1143
+ logGeneratedAtHeader(response, 'POST', '/font/register');
742
1144
  if (!response.ok) {
743
1145
  const errorMessage = await this._extractErrorMessage(response);
744
1146
  throw new exceptions_1.HttpClientException(`Font registration failed: ${errorMessage}`, response);
@@ -792,11 +1194,22 @@ class PDFDancer {
792
1194
  if (this._isTextObjectData(objData, objectType)) {
793
1195
  return this._parseTextObjectRef(objData);
794
1196
  }
1197
+ // Check if this is a form field type
1198
+ const formFieldTypes = [
1199
+ models_1.ObjectType.FORM_FIELD,
1200
+ models_1.ObjectType.TEXT_FIELD,
1201
+ models_1.ObjectType.CHECKBOX,
1202
+ models_1.ObjectType.RADIO_BUTTON
1203
+ ];
1204
+ if (formFieldTypes.includes(objectType)) {
1205
+ return this._parseFormFieldRef(objData);
1206
+ }
795
1207
  return new models_1.ObjectRef(objData.internalId, position, objectType);
796
1208
  }
797
1209
  _isTextObjectData(objData, objectType) {
798
1210
  return objectType === models_1.ObjectType.PARAGRAPH ||
799
1211
  objectType === models_1.ObjectType.TEXT_LINE ||
1212
+ objectType === models_1.ObjectType.TEXT_ELEMENT ||
800
1213
  typeof objData.text === 'string' ||
801
1214
  typeof objData.fontName === 'string' ||
802
1215
  Array.isArray(objData.children);
@@ -825,10 +1238,16 @@ class PDFDancer {
825
1238
  }
826
1239
  const textObject = new models_1.TextObjectRef(internalId, position, objectType, typeof objData.text === 'string' ? objData.text : undefined, typeof objData.fontName === 'string' ? objData.fontName : undefined, typeof objData.fontSize === 'number' ? objData.fontSize : undefined, lineSpacings, undefined, this._parseColor(objData.color), status);
827
1240
  if (Array.isArray(objData.children) && objData.children.length > 0) {
828
- textObject.children = objData.children.map((childData, index) => {
829
- const childFallbackId = `${internalId || 'child'}-${index}`;
830
- return this._parseTextObjectRef(childData, childFallbackId);
831
- });
1241
+ try {
1242
+ textObject.children = objData.children.map((childData, index) => {
1243
+ const childFallbackId = `${internalId || 'child'}-${index}`;
1244
+ return this._parseTextObjectRef(childData, childFallbackId);
1245
+ });
1246
+ }
1247
+ catch (error) {
1248
+ const message = error instanceof Error ? error.message : String(error);
1249
+ console.error(`Failed to parse children of ${internalId}: ${message}`);
1250
+ }
832
1251
  }
833
1252
  return textObject;
834
1253
  }
@@ -906,6 +1325,47 @@ class PDFDancer {
906
1325
  }
907
1326
  return position;
908
1327
  }
1328
+ /**
1329
+ * Parse JSON data into DocumentSnapshot instance.
1330
+ */
1331
+ _parseDocumentSnapshot(data) {
1332
+ const pageCount = typeof data.pageCount === 'number' ? data.pageCount : 0;
1333
+ // Parse fonts
1334
+ const fonts = [];
1335
+ if (Array.isArray(data.fonts)) {
1336
+ for (const fontData of data.fonts) {
1337
+ if (fontData && typeof fontData === 'object') {
1338
+ const fontName = fontData.fontName || '';
1339
+ const fontType = fontData.fontType || models_1.FontType.SYSTEM;
1340
+ const similarityScore = typeof fontData.similarityScore === 'number' ? fontData.similarityScore : 0;
1341
+ fonts.push(new models_1.FontRecommendation(fontName, fontType, similarityScore));
1342
+ }
1343
+ }
1344
+ }
1345
+ // Parse pages
1346
+ const pages = [];
1347
+ if (Array.isArray(data.pages)) {
1348
+ for (const pageData of data.pages) {
1349
+ pages.push(this._parsePageSnapshot(pageData));
1350
+ }
1351
+ }
1352
+ return new models_1.DocumentSnapshot(pageCount, fonts, pages);
1353
+ }
1354
+ /**
1355
+ * Parse JSON data into PageSnapshot instance.
1356
+ */
1357
+ _parsePageSnapshot(data) {
1358
+ // Parse page reference
1359
+ const pageRef = this._parsePageRef(data.pageRef || {});
1360
+ // Parse elements
1361
+ const elements = [];
1362
+ if (Array.isArray(data.elements)) {
1363
+ for (const elementData of data.elements) {
1364
+ elements.push(this._parseObjectRef(elementData));
1365
+ }
1366
+ }
1367
+ return new models_1.PageSnapshot(pageRef, elements);
1368
+ }
909
1369
  // Builder Pattern Support
910
1370
  toPathObjects(objectRefs) {
911
1371
  return objectRefs.map(ref => types_1.PathObject.fromRef(this, ref));
@@ -916,8 +1376,14 @@ class PDFDancer {
916
1376
  toImageObjects(objectRefs) {
917
1377
  return objectRefs.map(ref => types_1.ImageObject.fromRef(this, ref));
918
1378
  }
919
- newImage() {
920
- return new image_builder_1.ImageBuilder(this);
1379
+ newImage(pageIndex) {
1380
+ return new image_builder_1.ImageBuilder(this, pageIndex);
1381
+ }
1382
+ newParagraph(pageIndex) {
1383
+ return new paragraph_builder_1.ParagraphBuilder(this, pageIndex);
1384
+ }
1385
+ newPage() {
1386
+ return new page_builder_1.PageBuilder(this);
921
1387
  }
922
1388
  page(pageIndex) {
923
1389
  if (pageIndex < 0) {
@@ -932,18 +1398,37 @@ class PDFDancer {
932
1398
  toFormFields(objectRefs) {
933
1399
  return objectRefs.map(ref => types_1.FormFieldObject.fromRef(this, ref));
934
1400
  }
1401
+ async selectElements(types) {
1402
+ const snapshot = await this.getDocumentSnapshot(types);
1403
+ const elements = [];
1404
+ for (const pageSnapshot of snapshot.pages) {
1405
+ elements.push(...pageSnapshot.elements);
1406
+ }
1407
+ return elements;
1408
+ }
935
1409
  async selectParagraphs() {
936
1410
  return this.toParagraphObjects(await this.findParagraphs());
937
1411
  }
1412
+ async selectParagraphsMatching(pattern) {
1413
+ if (!pattern) {
1414
+ throw new exceptions_1.ValidationException('Pattern cannot be empty');
1415
+ }
1416
+ const position = new models_1.Position();
1417
+ position.textPattern = pattern;
1418
+ return this.toParagraphObjects(await this.findParagraphs(position));
1419
+ }
938
1420
  toParagraphObjects(objectRefs) {
939
1421
  return objectRefs.map(ref => types_1.ParagraphObject.fromRef(this, ref));
940
1422
  }
941
1423
  toTextLineObjects(objectRefs) {
942
1424
  return objectRefs.map(ref => types_1.TextLineObject.fromRef(this, ref));
943
1425
  }
944
- async selectLines() {
1426
+ async selectTextLines() {
945
1427
  return this.toTextLineObjects(await this.findTextLines());
946
1428
  }
1429
+ async selectLines() {
1430
+ return this.selectTextLines();
1431
+ }
947
1432
  }
948
1433
  exports.PDFDancer = PDFDancer;
949
1434
  //# sourceMappingURL=pdfdancer_v1.js.map