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