pdfdancer-client-typescript 1.0.13 → 1.0.15

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 (45) hide show
  1. package/.eslintrc.js +26 -18
  2. package/.github/workflows/ci.yml +51 -2
  3. package/.github/workflows/daily-tests.yml +54 -0
  4. package/README.md +50 -6
  5. package/dist/__tests__/e2e/test-helpers.d.ts.map +1 -1
  6. package/dist/__tests__/e2e/test-helpers.js +17 -5
  7. package/dist/__tests__/e2e/test-helpers.js.map +1 -1
  8. package/dist/fingerprint.d.ts.map +1 -1
  9. package/dist/fingerprint.js +16 -5
  10. package/dist/fingerprint.js.map +1 -1
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +4 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/models.d.ts +18 -15
  16. package/dist/models.d.ts.map +1 -1
  17. package/dist/models.js +21 -18
  18. package/dist/models.js.map +1 -1
  19. package/dist/pdfdancer_v1.d.ts +71 -3
  20. package/dist/pdfdancer_v1.d.ts.map +1 -1
  21. package/dist/pdfdancer_v1.js +301 -35
  22. package/dist/pdfdancer_v1.js.map +1 -1
  23. package/docs/openapi.yml +637 -73
  24. package/jest.config.js +1 -1
  25. package/package.json +2 -2
  26. package/src/__tests__/e2e/acroform.test.ts +58 -0
  27. package/src/__tests__/e2e/form_x_object.test.ts +29 -0
  28. package/src/__tests__/e2e/image-showcase.test.ts +6 -6
  29. package/src/__tests__/e2e/image.test.ts +39 -5
  30. package/src/__tests__/e2e/line-showcase.test.ts +6 -11
  31. package/src/__tests__/e2e/line.test.ts +63 -7
  32. package/src/__tests__/e2e/page-showcase.test.ts +12 -12
  33. package/src/__tests__/e2e/page.test.ts +3 -3
  34. package/src/__tests__/e2e/paragraph-showcase.test.ts +0 -8
  35. package/src/__tests__/e2e/paragraph.test.ts +64 -8
  36. package/src/__tests__/e2e/path.test.ts +33 -4
  37. package/src/__tests__/e2e/snapshot-showcase.test.ts +10 -10
  38. package/src/__tests__/e2e/snapshot.test.ts +18 -18
  39. package/src/__tests__/e2e/test-helpers.ts +16 -5
  40. package/src/__tests__/e2e/token_from_env.test.ts +0 -15
  41. package/src/__tests__/retry-mechanism.test.ts +420 -0
  42. package/src/fingerprint.ts +20 -7
  43. package/src/index.ts +3 -1
  44. package/src/models.ts +21 -17
  45. package/src/pdfdancer_v1.ts +467 -71
@@ -23,6 +23,93 @@ const DEFAULT_TOLERANCE = 0.01;
23
23
  const DEBUG = (process.env.PDFDANCER_CLIENT_DEBUG ?? '').toLowerCase() === 'true' ||
24
24
  (process.env.PDFDANCER_CLIENT_DEBUG ?? '') === '1' ||
25
25
  (process.env.PDFDANCER_CLIENT_DEBUG ?? '').toLowerCase() === 'yes';
26
+ /**
27
+ * Default retry configuration
28
+ */
29
+ const DEFAULT_RETRY_CONFIG = {
30
+ maxRetries: 3,
31
+ initialDelay: 1000,
32
+ maxDelay: 10000,
33
+ retryableStatusCodes: [429, 500, 502, 503, 504],
34
+ retryOnNetworkError: true,
35
+ backoffMultiplier: 2,
36
+ useJitter: true
37
+ };
38
+ /**
39
+ * Static helper function for retry logic with exponential backoff.
40
+ * Used by static methods that don't have access to instance retry config.
41
+ */
42
+ async function fetchWithRetry(url,
43
+ // eslint-disable-next-line no-undef
44
+ options, retryConfig, context = 'request') {
45
+ let lastError = null;
46
+ let lastResponse = null;
47
+ for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
48
+ try {
49
+ const response = await fetch(url, options);
50
+ // Check if we should retry based on status code
51
+ if (!response.ok && retryConfig.retryableStatusCodes.includes(response.status)) {
52
+ lastResponse = response;
53
+ // If this is not the last attempt, wait and retry
54
+ if (attempt < retryConfig.maxRetries) {
55
+ const delay = calculateRetryDelay(attempt, retryConfig);
56
+ if (DEBUG) {
57
+ console.log(`${Date.now() / 1000}|Retry attempt ${attempt + 1}/${retryConfig.maxRetries} for ${context} after ${delay}ms (status: ${response.status})`);
58
+ }
59
+ await sleep(delay);
60
+ continue;
61
+ }
62
+ }
63
+ // Request succeeded or non-retryable error
64
+ return response;
65
+ }
66
+ catch (error) {
67
+ lastError = error;
68
+ // Check if this is a network error and we should retry
69
+ if (retryConfig.retryOnNetworkError && attempt < retryConfig.maxRetries) {
70
+ const delay = calculateRetryDelay(attempt, retryConfig);
71
+ if (DEBUG) {
72
+ const errorMessage = error instanceof Error ? error.message : String(error);
73
+ console.log(`${Date.now() / 1000}|Retry attempt ${attempt + 1}/${retryConfig.maxRetries} for ${context} after ${delay}ms (error: ${errorMessage})`);
74
+ }
75
+ await sleep(delay);
76
+ continue;
77
+ }
78
+ // Non-retryable error or last attempt
79
+ throw error;
80
+ }
81
+ }
82
+ // If we exhausted all retries due to retryable status codes, return the last response
83
+ if (lastResponse) {
84
+ return lastResponse;
85
+ }
86
+ // If we exhausted all retries due to network errors, throw the last error
87
+ if (lastError) {
88
+ throw lastError;
89
+ }
90
+ // This should never happen, but just in case
91
+ throw new Error('Unexpected retry exhaustion');
92
+ }
93
+ /**
94
+ * Calculates the delay for the next retry attempt using exponential backoff.
95
+ */
96
+ function calculateRetryDelay(attemptNumber, retryConfig) {
97
+ // Calculate base delay: initialDelay * (backoffMultiplier ^ attemptNumber)
98
+ let delay = retryConfig.initialDelay * Math.pow(retryConfig.backoffMultiplier, attemptNumber);
99
+ // Cap at maxDelay
100
+ delay = Math.min(delay, retryConfig.maxDelay);
101
+ // Add jitter if enabled (randomize between 50% and 100% of calculated delay)
102
+ if (retryConfig.useJitter) {
103
+ delay = delay * (0.5 + Math.random() * 0.5);
104
+ }
105
+ return Math.floor(delay);
106
+ }
107
+ /**
108
+ * Sleep for the specified number of milliseconds.
109
+ */
110
+ function sleep(ms) {
111
+ return new Promise(resolve => setTimeout(resolve, ms));
112
+ }
26
113
  /**
27
114
  * Generate a timestamp string in the format expected by the API.
28
115
  * Format: YYYY-MM-DDTHH:MM:SS.ffffffZ (with microseconds)
@@ -214,6 +301,75 @@ class PageClient {
214
301
  async getSnapshot(types) {
215
302
  return this._client.getPageSnapshot(this._pageIndex, types);
216
303
  }
304
+ // Singular convenience methods - return the first element or null
305
+ async selectPath() {
306
+ const paths = await this.selectPaths();
307
+ return paths.length > 0 ? paths[0] : null;
308
+ }
309
+ async selectPathAt(x, y, tolerance = 0) {
310
+ const paths = await this.selectPathsAt(x, y, tolerance);
311
+ return paths.length > 0 ? paths[0] : null;
312
+ }
313
+ async selectImage() {
314
+ const images = await this.selectImages();
315
+ return images.length > 0 ? images[0] : null;
316
+ }
317
+ async selectImageAt(x, y, tolerance = 0) {
318
+ const images = await this.selectImagesAt(x, y, tolerance);
319
+ return images.length > 0 ? images[0] : null;
320
+ }
321
+ async selectForm() {
322
+ const forms = await this.selectForms();
323
+ return forms.length > 0 ? forms[0] : null;
324
+ }
325
+ async selectFormAt(x, y, tolerance = 0) {
326
+ const forms = await this.selectFormsAt(x, y, tolerance);
327
+ return forms.length > 0 ? forms[0] : null;
328
+ }
329
+ async selectFormField() {
330
+ const fields = await this.selectFormFields();
331
+ return fields.length > 0 ? fields[0] : null;
332
+ }
333
+ async selectFormFieldAt(x, y, tolerance = 0) {
334
+ const fields = await this.selectFormFieldsAt(x, y, tolerance);
335
+ return fields.length > 0 ? fields[0] : null;
336
+ }
337
+ async selectFormFieldByName(fieldName) {
338
+ const fields = await this.selectFormFieldsByName(fieldName);
339
+ return fields.length > 0 ? fields[0] : null;
340
+ }
341
+ async selectParagraph() {
342
+ const paragraphs = await this.selectParagraphs();
343
+ return paragraphs.length > 0 ? paragraphs[0] : null;
344
+ }
345
+ async selectParagraphStartingWith(text) {
346
+ const paragraphs = await this.selectParagraphsStartingWith(text);
347
+ return paragraphs.length > 0 ? paragraphs[0] : null;
348
+ }
349
+ async selectParagraphMatching(pattern) {
350
+ const paragraphs = await this.selectParagraphsMatching(pattern);
351
+ return paragraphs.length > 0 ? paragraphs[0] : null;
352
+ }
353
+ async selectParagraphAt(x, y, tolerance = DEFAULT_TOLERANCE) {
354
+ const paragraphs = await this.selectParagraphsAt(x, y, tolerance);
355
+ return paragraphs.length > 0 ? paragraphs[0] : null;
356
+ }
357
+ async selectTextLine() {
358
+ const lines = await this.selectTextLines();
359
+ return lines.length > 0 ? lines[0] : null;
360
+ }
361
+ async selectTextLineStartingWith(text) {
362
+ const lines = await this.selectTextLinesStartingWith(text);
363
+ return lines.length > 0 ? lines[0] : null;
364
+ }
365
+ async selectTextLineMatching(pattern) {
366
+ const lines = await this.selectTextLinesMatching(pattern);
367
+ return lines.length > 0 ? lines[0] : null;
368
+ }
369
+ async selectTextLineAt(x, y, tolerance = DEFAULT_TOLERANCE) {
370
+ const lines = await this.selectTextLinesAt(x, y, tolerance);
371
+ return lines.length > 0 ? lines[0] : null;
372
+ }
217
373
  }
218
374
  // noinspection ExceptionCaughtLocallyJS,JSUnusedLocalSymbols
219
375
  /**
@@ -229,7 +385,7 @@ class PDFDancer {
229
385
  * This constructor initializes the client, uploads the PDF data to open
230
386
  * a new session, and prepares the client for PDF manipulation operations.
231
387
  */
232
- constructor(token, pdfData, baseUrl = null, readTimeout = 30000) {
388
+ constructor(token, pdfData, baseUrl = null, readTimeout = 60000, retryConfig) {
233
389
  // Snapshot caches for optimizing find operations
234
390
  this._documentSnapshotCache = null;
235
391
  this._pageSnapshotCache = new Map();
@@ -251,6 +407,11 @@ class PDFDancer {
251
407
  this._token = token.trim();
252
408
  this._baseUrl = resolvedBaseUrl.replace(/\/$/, ''); // Remove trailing slash
253
409
  this._readTimeout = readTimeout;
410
+ // Merge retry config with defaults
411
+ this._retryConfig = {
412
+ ...DEFAULT_RETRY_CONFIG,
413
+ ...retryConfig
414
+ };
254
415
  // Process PDF data with validation
255
416
  this._pdfBytes = this._processPdfData(pdfData);
256
417
  // Initialize caches
@@ -266,16 +427,16 @@ class PDFDancer {
266
427
  this._sessionId = await this._createSession();
267
428
  return this;
268
429
  }
269
- static async open(pdfData, token, baseUrl, timeout) {
270
- const resolvedToken = token ?? process.env.PDFDANCER_TOKEN;
430
+ static async open(pdfData, token, baseUrl, timeout, retryConfig) {
271
431
  const resolvedBaseUrl = baseUrl ??
272
432
  process.env.PDFDANCER_BASE_URL ??
273
433
  "https://api.pdfdancer.com";
274
- const resolvedTimeout = timeout ?? 30000;
434
+ const resolvedTimeout = timeout ?? 60000;
435
+ let resolvedToken = token?.trim() ?? process.env.PDFDANCER_TOKEN?.trim() ?? null;
275
436
  if (!resolvedToken) {
276
- throw new exceptions_1.ValidationException("Missing PDFDancer API token. Pass a token via the `token` argument or set the PDFDANCER_TOKEN environment variable.");
437
+ resolvedToken = await PDFDancer._obtainAnonymousToken(resolvedBaseUrl, resolvedTimeout);
277
438
  }
278
- const client = new PDFDancer(resolvedToken, pdfData, resolvedBaseUrl, resolvedTimeout);
439
+ const client = new PDFDancer(resolvedToken, pdfData, resolvedBaseUrl, resolvedTimeout, retryConfig);
279
440
  return await client.init();
280
441
  }
281
442
  /**
@@ -287,16 +448,17 @@ class PDFDancer {
287
448
  * @param options.initialPageCount Number of initial pages (default: 1)
288
449
  * @param token Authentication token (optional, can use PDFDANCER_TOKEN env var)
289
450
  * @param baseUrl Base URL for the PDFDancer API (optional)
290
- * @param timeout Request timeout in milliseconds (default: 30000)
451
+ * @param timeout Request timeout in milliseconds (default: 60000)
452
+ * @param retryConfig Retry configuration (optional, uses defaults if not specified)
291
453
  */
292
- static async new(options, token, baseUrl, timeout) {
293
- const resolvedToken = token ?? process.env.PDFDANCER_TOKEN;
454
+ static async new(options, token, baseUrl, timeout, retryConfig) {
294
455
  const resolvedBaseUrl = baseUrl ??
295
456
  process.env.PDFDANCER_BASE_URL ??
296
457
  "https://api.pdfdancer.com";
297
- const resolvedTimeout = timeout ?? 30000;
458
+ const resolvedTimeout = timeout ?? 60000;
459
+ let resolvedToken = token?.trim() ?? process.env.PDFDANCER_TOKEN?.trim() ?? null;
298
460
  if (!resolvedToken) {
299
- throw new exceptions_1.ValidationException("Missing PDFDancer token (pass it explicitly or set PDFDANCER_TOKEN in environment).");
461
+ resolvedToken = await PDFDancer._obtainAnonymousToken(resolvedBaseUrl, resolvedTimeout);
300
462
  }
301
463
  let createRequest;
302
464
  try {
@@ -313,8 +475,8 @@ class PDFDancer {
313
475
  const url = `${base}/${endpoint}`;
314
476
  // Generate fingerprint for this request
315
477
  const fingerprint = await (0, fingerprint_1.generateFingerprint)();
316
- // Make request to create endpoint
317
- const response = await fetch(url, {
478
+ // Make request to create endpoint with retry logic
479
+ const response = await fetchWithRetry(url, {
318
480
  method: 'POST',
319
481
  headers: {
320
482
  'Authorization': `Bearer ${resolvedToken}`,
@@ -324,7 +486,7 @@ class PDFDancer {
324
486
  },
325
487
  body: JSON.stringify(createRequest.toDict()),
326
488
  signal: resolvedTimeout > 0 ? AbortSignal.timeout(resolvedTimeout) : undefined
327
- });
489
+ }, DEFAULT_RETRY_CONFIG, 'POST /session/new');
328
490
  logGeneratedAtHeader(response, 'POST', '/session/new');
329
491
  if (!response.ok) {
330
492
  const errorText = await response.text();
@@ -340,6 +502,11 @@ class PDFDancer {
340
502
  client._readTimeout = resolvedTimeout;
341
503
  client._pdfBytes = new Uint8Array();
342
504
  client._sessionId = sessionId;
505
+ // Initialize retry config
506
+ client._retryConfig = {
507
+ ...DEFAULT_RETRY_CONFIG,
508
+ ...retryConfig
509
+ };
343
510
  // Initialize caches
344
511
  client._documentSnapshotCache = null;
345
512
  client._pageSnapshotCache = new Map();
@@ -354,6 +521,39 @@ class PDFDancer {
354
521
  throw new exceptions_1.HttpClientException(`Failed to create new PDF: ${errorMessage}`, undefined, error);
355
522
  }
356
523
  }
524
+ static async _obtainAnonymousToken(baseUrl, timeout = 60000) {
525
+ const normalizedBaseUrl = (baseUrl || "https://api.pdfdancer.com").replace(/\/+$/, '');
526
+ const url = `${normalizedBaseUrl}/keys/anon`;
527
+ try {
528
+ const fingerprint = await (0, fingerprint_1.generateFingerprint)();
529
+ const response = await fetchWithRetry(url, {
530
+ method: 'POST',
531
+ headers: {
532
+ 'Content-Type': 'application/json',
533
+ 'X-Fingerprint': fingerprint,
534
+ 'X-Generated-At': generateTimestamp()
535
+ },
536
+ signal: timeout > 0 ? AbortSignal.timeout(timeout) : undefined
537
+ }, DEFAULT_RETRY_CONFIG, 'POST /keys/anon');
538
+ if (!response.ok) {
539
+ const errorText = await response.text().catch(() => '');
540
+ throw new exceptions_1.HttpClientException(`Failed to obtain anonymous token: ${errorText || `HTTP ${response.status}`}`, response);
541
+ }
542
+ const tokenPayload = await response.json().catch(() => null);
543
+ const tokenValue = typeof tokenPayload?.token === 'string' ? tokenPayload.token.trim() : '';
544
+ if (!tokenValue) {
545
+ throw new exceptions_1.HttpClientException("Invalid anonymous token response format", response);
546
+ }
547
+ return tokenValue;
548
+ }
549
+ catch (error) {
550
+ if (error instanceof exceptions_1.HttpClientException) {
551
+ throw error;
552
+ }
553
+ const errorMessage = error instanceof Error ? error.message : String(error);
554
+ throw new exceptions_1.HttpClientException(`Failed to obtain anonymous token: ${errorMessage}`, undefined, error);
555
+ }
556
+ }
357
557
  /**
358
558
  * Process PDF data from various input types with strict validation.
359
559
  */
@@ -379,6 +579,17 @@ class PDFDancer {
379
579
  // Note: File reading will be handled asynchronously in the session creation
380
580
  return new Uint8Array(); // Placeholder, will be replaced in _createSession
381
581
  }
582
+ else if (typeof pdfData === 'string') {
583
+ // Handle string as filepath
584
+ if (!fs_1.default.existsSync(pdfData)) {
585
+ throw new exceptions_1.ValidationException(`PDF file not found: ${pdfData}`);
586
+ }
587
+ const fileData = new Uint8Array(fs_1.default.readFileSync(pdfData));
588
+ if (fileData.length === 0) {
589
+ throw new exceptions_1.ValidationException("PDF file is empty");
590
+ }
591
+ return fileData;
592
+ }
382
593
  else {
383
594
  throw new exceptions_1.ValidationException(`Unsupported PDF data type: ${typeof pdfData}`);
384
595
  }
@@ -453,7 +664,7 @@ class PDFDancer {
453
664
  formData.append('pdf', blob, 'document.pdf');
454
665
  }
455
666
  const fingerprint = await this._getFingerprint();
456
- const response = await fetch(this._buildUrl('/session/create'), {
667
+ const response = await this._fetchWithRetry(this._buildUrl('/session/create'), {
457
668
  method: 'POST',
458
669
  headers: {
459
670
  'Authorization': `Bearer ${this._token}`,
@@ -462,7 +673,7 @@ class PDFDancer {
462
673
  },
463
674
  body: formData,
464
675
  signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
465
- });
676
+ }, 'POST /session/create');
466
677
  logGeneratedAtHeader(response, 'POST', '/session/create');
467
678
  if (!response.ok) {
468
679
  const errorMessage = await this._extractErrorMessage(response);
@@ -499,6 +710,15 @@ class PDFDancer {
499
710
  }
500
711
  return this._fingerprintCache;
501
712
  }
713
+ /**
714
+ * Executes a fetch request with retry logic based on the configured retry policy.
715
+ * Implements exponential backoff with optional jitter.
716
+ */
717
+ async _fetchWithRetry(url,
718
+ // eslint-disable-next-line no-undef
719
+ options, context = 'request') {
720
+ return fetchWithRetry(url, options, this._retryConfig, context);
721
+ }
502
722
  /**
503
723
  * Make HTTP request with session headers and error handling.
504
724
  */
@@ -518,12 +738,12 @@ class PDFDancer {
518
738
  'X-Fingerprint': fingerprint
519
739
  };
520
740
  try {
521
- const response = await fetch(url.toString(), {
741
+ const response = await this._fetchWithRetry(url.toString(), {
522
742
  method,
523
743
  headers,
524
744
  body: data ? JSON.stringify(data) : undefined,
525
745
  signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
526
- });
746
+ }, `${method} ${path}`);
527
747
  logGeneratedAtHeader(response, method, path);
528
748
  // Handle FontNotFoundException
529
749
  if (response.status === 404) {
@@ -1129,7 +1349,7 @@ class PDFDancer {
1129
1349
  const blob = new Blob([fontData.buffer], { type: 'font/ttf' });
1130
1350
  formData.append('ttfFile', blob, filename);
1131
1351
  const fingerprint = await this._getFingerprint();
1132
- const response = await fetch(this._buildUrl('/font/register'), {
1352
+ const response = await this._fetchWithRetry(this._buildUrl('/font/register'), {
1133
1353
  method: 'POST',
1134
1354
  headers: {
1135
1355
  'Authorization': `Bearer ${this._token}`,
@@ -1138,8 +1358,8 @@ class PDFDancer {
1138
1358
  'X-Fingerprint': fingerprint
1139
1359
  },
1140
1360
  body: formData,
1141
- signal: AbortSignal.timeout(30000)
1142
- });
1361
+ signal: AbortSignal.timeout(60000)
1362
+ }, 'POST /font/register');
1143
1363
  logGeneratedAtHeader(response, 'POST', '/font/register');
1144
1364
  if (!response.ok) {
1145
1365
  const errorMessage = await this._extractErrorMessage(response);
@@ -1224,17 +1444,24 @@ class PDFDancer {
1224
1444
  let status;
1225
1445
  const statusData = objData.status;
1226
1446
  if (statusData && typeof statusData === 'object') {
1227
- // Parse font recommendation
1228
- const fontRecData = statusData.fontRecommendation;
1229
- let fontRec;
1230
- if (fontRecData && typeof fontRecData === 'object') {
1231
- fontRec = new models_1.FontRecommendation(fontRecData.fontName || '', fontRecData.fontType || models_1.FontType.SYSTEM, fontRecData.similarityScore || 0.0);
1232
- }
1233
- else {
1234
- // Create empty font recommendation if not provided
1235
- fontRec = new models_1.FontRecommendation('', models_1.FontType.SYSTEM, 0.0);
1447
+ const fontInfoSource = statusData.fontInfoDto ?? statusData.fontRecommendation;
1448
+ let fontInfo;
1449
+ if (fontInfoSource && typeof fontInfoSource === 'object') {
1450
+ const documentFontName = typeof fontInfoSource.documentFontName === 'string'
1451
+ ? fontInfoSource.documentFontName
1452
+ : (typeof fontInfoSource.fontName === 'string' ? fontInfoSource.fontName : '');
1453
+ const systemFontName = typeof fontInfoSource.systemFontName === 'string'
1454
+ ? fontInfoSource.systemFontName
1455
+ : (typeof fontInfoSource.fontName === 'string' ? fontInfoSource.fontName : '');
1456
+ fontInfo = new models_1.DocumentFontInfo(documentFontName, systemFontName);
1236
1457
  }
1237
- status = new models_1.TextStatus(statusData.modified || false, statusData.encodable !== undefined ? statusData.encodable : true, statusData.fontType || models_1.FontType.SYSTEM, fontRec);
1458
+ const modified = statusData.modified !== undefined ? Boolean(statusData.modified) : false;
1459
+ const encodable = statusData.encodable !== undefined ? Boolean(statusData.encodable) : true;
1460
+ const fontTypeValue = typeof statusData.fontType === 'string'
1461
+ && Object.values(models_1.FontType).includes(statusData.fontType)
1462
+ ? statusData.fontType
1463
+ : models_1.FontType.SYSTEM;
1464
+ status = new models_1.TextStatus(modified, encodable, fontTypeValue, fontInfo);
1238
1465
  }
1239
1466
  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);
1240
1467
  if (Array.isArray(objData.children) && objData.children.length > 0) {
@@ -1335,10 +1562,13 @@ class PDFDancer {
1335
1562
  if (Array.isArray(data.fonts)) {
1336
1563
  for (const fontData of data.fonts) {
1337
1564
  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));
1565
+ const documentFontName = typeof fontData.documentFontName === 'string'
1566
+ ? fontData.documentFontName
1567
+ : (typeof fontData.fontName === 'string' ? fontData.fontName : '');
1568
+ const systemFontName = typeof fontData.systemFontName === 'string'
1569
+ ? fontData.systemFontName
1570
+ : (typeof fontData.fontName === 'string' ? fontData.fontName : '');
1571
+ fonts.push(new models_1.DocumentFontInfo(documentFontName, systemFontName));
1342
1572
  }
1343
1573
  }
1344
1574
  }
@@ -1429,6 +1659,42 @@ class PDFDancer {
1429
1659
  async selectLines() {
1430
1660
  return this.selectTextLines();
1431
1661
  }
1662
+ // Singular convenience methods - return the first element or null
1663
+ async selectImage() {
1664
+ const images = await this.selectImages();
1665
+ return images.length > 0 ? images[0] : null;
1666
+ }
1667
+ async selectPath() {
1668
+ const paths = await this.selectPaths();
1669
+ return paths.length > 0 ? paths[0] : null;
1670
+ }
1671
+ async selectForm() {
1672
+ const forms = await this.selectForms();
1673
+ return forms.length > 0 ? forms[0] : null;
1674
+ }
1675
+ async selectFormField() {
1676
+ const fields = await this.selectFormFields();
1677
+ return fields.length > 0 ? fields[0] : null;
1678
+ }
1679
+ async selectFieldByName(fieldName) {
1680
+ const fields = await this.selectFieldsByName(fieldName);
1681
+ return fields.length > 0 ? fields[0] : null;
1682
+ }
1683
+ async selectParagraph() {
1684
+ const paragraphs = await this.selectParagraphs();
1685
+ return paragraphs.length > 0 ? paragraphs[0] : null;
1686
+ }
1687
+ async selectParagraphMatching(pattern) {
1688
+ const paragraphs = await this.selectParagraphsMatching(pattern);
1689
+ return paragraphs.length > 0 ? paragraphs[0] : null;
1690
+ }
1691
+ async selectTextLine() {
1692
+ const lines = await this.selectTextLines();
1693
+ return lines.length > 0 ? lines[0] : null;
1694
+ }
1695
+ async selectLine() {
1696
+ return this.selectTextLine();
1697
+ }
1432
1698
  }
1433
1699
  exports.PDFDancer = PDFDancer;
1434
1700
  //# sourceMappingURL=pdfdancer_v1.js.map