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.
- package/.claude/commands/discuss.md +4 -0
- package/.github/workflows/ci.yml +2 -2
- package/README.md +2 -2
- package/dist/__tests__/e2e/pdf-assertions.d.ts +1 -0
- package/dist/__tests__/e2e/pdf-assertions.d.ts.map +1 -1
- package/dist/__tests__/e2e/pdf-assertions.js +9 -3
- package/dist/__tests__/e2e/pdf-assertions.js.map +1 -1
- package/dist/fingerprint.d.ts +12 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +196 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/image-builder.d.ts +4 -2
- package/dist/image-builder.d.ts.map +1 -1
- package/dist/image-builder.js +12 -3
- package/dist/image-builder.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +75 -8
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +179 -21
- package/dist/models.js.map +1 -1
- package/dist/page-builder.d.ts +24 -0
- package/dist/page-builder.d.ts.map +1 -0
- package/dist/page-builder.js +107 -0
- package/dist/page-builder.js.map +1 -0
- package/dist/paragraph-builder.d.ts +48 -54
- package/dist/paragraph-builder.d.ts.map +1 -1
- package/dist/paragraph-builder.js +408 -135
- package/dist/paragraph-builder.js.map +1 -1
- package/dist/pdfdancer_v1.d.ts +90 -9
- package/dist/pdfdancer_v1.d.ts.map +1 -1
- package/dist/pdfdancer_v1.js +559 -55
- package/dist/pdfdancer_v1.js.map +1 -1
- package/dist/types.d.ts +24 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +117 -2
- package/dist/types.js.map +1 -1
- package/docs/openapi.yml +2076 -0
- package/fixtures/Showcase.pdf +0 -0
- package/jest.config.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/e2e/acroform.test.ts +5 -5
- package/src/__tests__/e2e/context-manager-showcase.test.ts +267 -0
- package/src/__tests__/e2e/form_x_object.test.ts +1 -1
- package/src/__tests__/e2e/image-showcase.test.ts +133 -0
- package/src/__tests__/e2e/image.test.ts +1 -1
- package/src/__tests__/e2e/line-showcase.test.ts +118 -0
- package/src/__tests__/e2e/line.test.ts +1 -16
- package/src/__tests__/e2e/page-showcase.test.ts +154 -0
- package/src/__tests__/e2e/paragraph-showcase.test.ts +523 -0
- package/src/__tests__/e2e/paragraph.test.ts +8 -8
- package/src/__tests__/e2e/pdf-assertions.ts +10 -3
- package/src/__tests__/e2e/pdfdancer-showcase.test.ts +40 -0
- package/src/__tests__/e2e/snapshot-showcase.test.ts +158 -0
- package/src/__tests__/e2e/snapshot.test.ts +296 -0
- package/src/__tests__/e2e/token_from_env.test.ts +85 -25
- package/src/__tests__/fingerprint.test.ts +36 -0
- package/src/fingerprint.ts +169 -0
- package/src/image-builder.ts +13 -6
- package/src/index.ts +6 -1
- package/src/models.ts +208 -24
- package/src/page-builder.ts +130 -0
- package/src/paragraph-builder.ts +517 -159
- package/src/pdfdancer_v1.ts +662 -58
- package/src/types.ts +145 -2
- package/update-api-spec.sh +3 -0
package/src/pdfdancer_v1.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
557
|
-
|
|
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
|
-
|
|
560
|
-
return
|
|
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
|
-
|
|
624
|
-
|
|
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
|
-
|
|
627
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
650
|
-
|
|
910
|
+
// Try page snapshot cache first
|
|
911
|
+
if (this._pageSnapshotCache.has(pageIndex)) {
|
|
912
|
+
return this._pageSnapshotCache.get(pageIndex)!.pageRef;
|
|
913
|
+
}
|
|
651
914
|
|
|
652
|
-
|
|
653
|
-
if (
|
|
654
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
|
1821
|
+
async selectTextLines() {
|
|
1222
1822
|
return this.toTextLineObjects(await this.findTextLines());
|
|
1223
1823
|
}
|
|
1824
|
+
|
|
1825
|
+
async selectLines() {
|
|
1826
|
+
return this.selectTextLines();
|
|
1827
|
+
}
|
|
1224
1828
|
}
|