@xenterprises/fastify-xpdf 1.0.0
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/.env.example +11 -0
- package/CHANGELOG.md +106 -0
- package/LICENSE +15 -0
- package/QUICK_START.md +462 -0
- package/README.md +580 -0
- package/SECURITY.md +417 -0
- package/package.json +57 -0
- package/server/app.js +151 -0
- package/src/index.js +7 -0
- package/src/services/forms.js +163 -0
- package/src/services/generator.js +147 -0
- package/src/services/merger.js +115 -0
- package/src/utils/helpers.js +220 -0
- package/src/xPDF.js +126 -0
- package/test/xPDF.test.js +903 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
// test/xPDF.test.js
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import assert from "node:assert";
|
|
4
|
+
import Fastify from "fastify";
|
|
5
|
+
import xPDF from "../src/xPDF.js";
|
|
6
|
+
import { PDFDocument } from "pdf-lib";
|
|
7
|
+
|
|
8
|
+
// Helper to create a simple PDF buffer for testing
|
|
9
|
+
async function createSimplePDF() {
|
|
10
|
+
const pdfDoc = await PDFDocument.create();
|
|
11
|
+
const page = pdfDoc.addPage();
|
|
12
|
+
page.drawText("Test PDF");
|
|
13
|
+
const pdfBytes = await pdfDoc.save();
|
|
14
|
+
return Buffer.from(pdfBytes);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Helper to create a form PDF
|
|
18
|
+
async function createFormPDF() {
|
|
19
|
+
const pdfDoc = await PDFDocument.create();
|
|
20
|
+
const page = pdfDoc.addPage();
|
|
21
|
+
|
|
22
|
+
// Add form fields
|
|
23
|
+
const form = pdfDoc.getForm();
|
|
24
|
+
const textField = form.createTextField("firstName");
|
|
25
|
+
textField.addToPage(page, { x: 50, y: 500, width: 200, height: 25 });
|
|
26
|
+
|
|
27
|
+
const checkboxField = form.createCheckBox("agreeToTerms");
|
|
28
|
+
checkboxField.addToPage(page, { x: 50, y: 400, width: 25, height: 25 });
|
|
29
|
+
|
|
30
|
+
const pdfBytes = await pdfDoc.save();
|
|
31
|
+
return Buffer.from(pdfBytes);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test("Plugin Registration", async (t) => {
|
|
35
|
+
await t.test("registers successfully", async () => {
|
|
36
|
+
const fastify = Fastify();
|
|
37
|
+
await fastify.register(xPDF);
|
|
38
|
+
assert.ok(fastify.xPDF);
|
|
39
|
+
assert.equal(typeof fastify.xPDF.generateFromHtml, "function");
|
|
40
|
+
assert.equal(typeof fastify.xPDF.generateFromMarkdown, "function");
|
|
41
|
+
assert.equal(typeof fastify.xPDF.fillForm, "function");
|
|
42
|
+
assert.equal(typeof fastify.xPDF.listFormFields, "function");
|
|
43
|
+
assert.equal(typeof fastify.xPDF.mergePDFs, "function");
|
|
44
|
+
await fastify.close();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await t.test("uses default configuration", async () => {
|
|
48
|
+
const fastify = Fastify();
|
|
49
|
+
await fastify.register(xPDF);
|
|
50
|
+
assert.ok(fastify.xPDF);
|
|
51
|
+
await fastify.close();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await t.test("accepts custom configuration", async () => {
|
|
55
|
+
const fastify = Fastify();
|
|
56
|
+
await fastify.register(xPDF, {
|
|
57
|
+
headless: true,
|
|
58
|
+
format: "Letter",
|
|
59
|
+
margin: { top: "0.5in", right: "0.5in", bottom: "0.5in", left: "0.5in" },
|
|
60
|
+
});
|
|
61
|
+
assert.ok(fastify.xPDF);
|
|
62
|
+
await fastify.close();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await t.test("cleans up browser on close", async () => {
|
|
66
|
+
const fastify = Fastify();
|
|
67
|
+
await fastify.register(xPDF);
|
|
68
|
+
|
|
69
|
+
// Generate one PDF to initialize browser
|
|
70
|
+
await fastify.xPDF.generateFromHtml("<h1>Test</h1>");
|
|
71
|
+
|
|
72
|
+
// Browser should exist
|
|
73
|
+
assert.ok(fastify.xPDF);
|
|
74
|
+
|
|
75
|
+
// Close should not throw
|
|
76
|
+
await fastify.close();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("HTML to PDF Generation", async (t) => {
|
|
81
|
+
let fastify;
|
|
82
|
+
|
|
83
|
+
t.before(async () => {
|
|
84
|
+
fastify = Fastify();
|
|
85
|
+
await fastify.register(xPDF);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
t.after(async () => {
|
|
89
|
+
await fastify.close();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await t.test("generates PDF from simple HTML", async () => {
|
|
93
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Hello</h1>");
|
|
94
|
+
|
|
95
|
+
assert.ok(result.buffer);
|
|
96
|
+
assert.ok(Buffer.isBuffer(result.buffer));
|
|
97
|
+
assert.ok(result.filename.includes(".pdf"));
|
|
98
|
+
assert.ok(result.size > 0);
|
|
99
|
+
assert.equal(result.buffer[0], 0x25); // PDF header: %
|
|
100
|
+
assert.equal(result.buffer[1], 0x50); // PDF header: P
|
|
101
|
+
assert.equal(result.buffer[2], 0x44); // PDF header: D
|
|
102
|
+
assert.equal(result.buffer[3], 0x46); // PDF header: F
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await t.test("accepts custom filename", async () => {
|
|
106
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Test</h1>", {
|
|
107
|
+
filename: "custom-name.pdf",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
assert.equal(result.filename, "custom-name.pdf");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await t.test("accepts custom format", async () => {
|
|
114
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Test</h1>", {
|
|
115
|
+
format: "Letter",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
assert.ok(result.buffer);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await t.test("accepts landscape orientation", async () => {
|
|
122
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Test</h1>", {
|
|
123
|
+
landscape: true,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
assert.ok(result.buffer);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await t.test("accepts custom margins", async () => {
|
|
130
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Test</h1>", {
|
|
131
|
+
margin: {
|
|
132
|
+
top: "2cm",
|
|
133
|
+
right: "1cm",
|
|
134
|
+
bottom: "2cm",
|
|
135
|
+
left: "1cm",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
assert.ok(result.buffer);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await t.test("rejects empty HTML", async () => {
|
|
143
|
+
await assert.rejects(
|
|
144
|
+
() => fastify.xPDF.generateFromHtml(""),
|
|
145
|
+
{ message: /HTML content must be a non-empty string/ }
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await t.test("rejects non-string HTML", async () => {
|
|
150
|
+
await assert.rejects(
|
|
151
|
+
() => fastify.xPDF.generateFromHtml(null),
|
|
152
|
+
{ message: /HTML content must be a non-empty string/ }
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("Markdown to PDF Generation", async (t) => {
|
|
158
|
+
let fastify;
|
|
159
|
+
|
|
160
|
+
t.before(async () => {
|
|
161
|
+
fastify = Fastify();
|
|
162
|
+
await fastify.register(xPDF);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
t.after(async () => {
|
|
166
|
+
await fastify.close();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await t.test("generates PDF from Markdown", async () => {
|
|
170
|
+
const markdown = "# Heading\n\nParagraph with **bold** and *italic*.";
|
|
171
|
+
const result = await fastify.xPDF.generateFromMarkdown(markdown);
|
|
172
|
+
|
|
173
|
+
assert.ok(result.buffer);
|
|
174
|
+
assert.ok(Buffer.isBuffer(result.buffer));
|
|
175
|
+
assert.ok(result.filename.includes(".pdf"));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await t.test("converts common Markdown features", async () => {
|
|
179
|
+
const markdown = `
|
|
180
|
+
# Heading 1
|
|
181
|
+
## Heading 2
|
|
182
|
+
|
|
183
|
+
Paragraph with **bold** and *italic* and \`code\`.
|
|
184
|
+
|
|
185
|
+
- List item 1
|
|
186
|
+
- List item 2
|
|
187
|
+
|
|
188
|
+
1. Numbered item 1
|
|
189
|
+
2. Numbered item 2
|
|
190
|
+
|
|
191
|
+
\`\`\`javascript
|
|
192
|
+
console.log("Code block");
|
|
193
|
+
\`\`\`
|
|
194
|
+
|
|
195
|
+
> Blockquote
|
|
196
|
+
|
|
197
|
+
[Link](https://example.com)
|
|
198
|
+
`;
|
|
199
|
+
const result = await fastify.xPDF.generateFromMarkdown(markdown);
|
|
200
|
+
|
|
201
|
+
assert.ok(result.buffer);
|
|
202
|
+
assert.ok(result.size > 0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await t.test("accepts custom filename", async () => {
|
|
206
|
+
const result = await fastify.xPDF.generateFromMarkdown("# Test", {
|
|
207
|
+
filename: "custom.pdf",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
assert.equal(result.filename, "custom.pdf");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await t.test("rejects empty Markdown", async () => {
|
|
214
|
+
await assert.rejects(
|
|
215
|
+
() => fastify.xPDF.generateFromMarkdown(""),
|
|
216
|
+
{ message: /Markdown content must be a non-empty string/ }
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await t.test("rejects non-string Markdown", async () => {
|
|
221
|
+
await assert.rejects(
|
|
222
|
+
() => fastify.xPDF.generateFromMarkdown(null),
|
|
223
|
+
{ message: /Markdown content must be a non-empty string/ }
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("PDF Form Filling", async (t) => {
|
|
229
|
+
let fastify;
|
|
230
|
+
|
|
231
|
+
t.before(async () => {
|
|
232
|
+
fastify = Fastify();
|
|
233
|
+
await fastify.register(xPDF);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
t.after(async () => {
|
|
237
|
+
await fastify.close();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await t.test("fills text field in PDF form", async () => {
|
|
241
|
+
const pdfBuffer = await createFormPDF();
|
|
242
|
+
|
|
243
|
+
const result = await fastify.xPDF.fillForm(
|
|
244
|
+
pdfBuffer,
|
|
245
|
+
{ firstName: "John" },
|
|
246
|
+
{
|
|
247
|
+
flatten: false,
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
assert.ok(result.buffer);
|
|
252
|
+
assert.ok(Buffer.isBuffer(result.buffer));
|
|
253
|
+
assert.ok(result.filename.includes(".pdf"));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await t.test("fills checkbox field in PDF form", async () => {
|
|
257
|
+
const pdfBuffer = await createFormPDF();
|
|
258
|
+
|
|
259
|
+
const result = await fastify.xPDF.fillForm(
|
|
260
|
+
pdfBuffer,
|
|
261
|
+
{ agreeToTerms: true },
|
|
262
|
+
{
|
|
263
|
+
flatten: false,
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
assert.ok(result.buffer);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await t.test("flattens form after filling", async () => {
|
|
271
|
+
const pdfBuffer = await createFormPDF();
|
|
272
|
+
|
|
273
|
+
const result = await fastify.xPDF.fillForm(
|
|
274
|
+
pdfBuffer,
|
|
275
|
+
{ firstName: "Jane" },
|
|
276
|
+
{
|
|
277
|
+
flatten: true,
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
assert.ok(result.buffer);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await t.test("accepts custom filename", async () => {
|
|
285
|
+
const pdfBuffer = await createFormPDF();
|
|
286
|
+
|
|
287
|
+
const result = await fastify.xPDF.fillForm(
|
|
288
|
+
pdfBuffer,
|
|
289
|
+
{ firstName: "Test" },
|
|
290
|
+
{
|
|
291
|
+
filename: "custom-form.pdf",
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
assert.equal(result.filename, "custom-form.pdf");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await t.test("rejects invalid PDF buffer", async () => {
|
|
299
|
+
await assert.rejects(
|
|
300
|
+
() => fastify.xPDF.fillForm(Buffer.from("not a pdf"), {}),
|
|
301
|
+
{ message: /Invalid PDF buffer/ }
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await t.test("rejects non-buffer input", async () => {
|
|
306
|
+
await assert.rejects(
|
|
307
|
+
() => fastify.xPDF.fillForm(null, {}),
|
|
308
|
+
{ message: /Invalid PDF buffer/ }
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("PDF Form Field Listing", async (t) => {
|
|
314
|
+
let fastify;
|
|
315
|
+
|
|
316
|
+
t.before(async () => {
|
|
317
|
+
fastify = Fastify();
|
|
318
|
+
await fastify.register(xPDF);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
t.after(async () => {
|
|
322
|
+
await fastify.close();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await t.test("lists form fields in PDF", async () => {
|
|
326
|
+
const pdfBuffer = await createFormPDF();
|
|
327
|
+
|
|
328
|
+
const fields = await fastify.xPDF.listFormFields(pdfBuffer);
|
|
329
|
+
|
|
330
|
+
assert.ok(Array.isArray(fields));
|
|
331
|
+
assert.ok(fields.length > 0);
|
|
332
|
+
assert.ok(fields.some((f) => f.name === "firstName"));
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
await t.test("returns field information", async () => {
|
|
336
|
+
const pdfBuffer = await createFormPDF();
|
|
337
|
+
|
|
338
|
+
const fields = await fastify.xPDF.listFormFields(pdfBuffer);
|
|
339
|
+
|
|
340
|
+
for (const field of fields) {
|
|
341
|
+
assert.ok(field.name);
|
|
342
|
+
assert.ok(field.type);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
await t.test("rejects invalid PDF buffer", async () => {
|
|
347
|
+
await assert.rejects(
|
|
348
|
+
() => fastify.xPDF.listFormFields(Buffer.from("not a pdf")),
|
|
349
|
+
{ message: /Invalid PDF buffer/ }
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("PDF Merging", async (t) => {
|
|
355
|
+
let fastify;
|
|
356
|
+
|
|
357
|
+
t.before(async () => {
|
|
358
|
+
fastify = Fastify();
|
|
359
|
+
await fastify.register(xPDF);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
t.after(async () => {
|
|
363
|
+
await fastify.close();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
await t.test("merges two PDFs", async () => {
|
|
367
|
+
const pdf1 = await createSimplePDF();
|
|
368
|
+
const pdf2 = await createSimplePDF();
|
|
369
|
+
|
|
370
|
+
const result = await fastify.xPDF.mergePDFs([pdf1, pdf2]);
|
|
371
|
+
|
|
372
|
+
assert.ok(result.buffer);
|
|
373
|
+
assert.ok(Buffer.isBuffer(result.buffer));
|
|
374
|
+
assert.ok(result.filename.includes(".pdf"));
|
|
375
|
+
assert.ok(result.pageCount > 0);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
await t.test("merges multiple PDFs", async () => {
|
|
379
|
+
const pdfs = [
|
|
380
|
+
await createSimplePDF(),
|
|
381
|
+
await createSimplePDF(),
|
|
382
|
+
await createSimplePDF(),
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
const result = await fastify.xPDF.mergePDFs(pdfs);
|
|
386
|
+
|
|
387
|
+
assert.ok(result.buffer);
|
|
388
|
+
assert.ok(result.pageCount >= 3);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await t.test("accepts custom filename", async () => {
|
|
392
|
+
const pdfs = [await createSimplePDF(), await createSimplePDF()];
|
|
393
|
+
|
|
394
|
+
const result = await fastify.xPDF.mergePDFs(pdfs, {
|
|
395
|
+
filename: "custom-merged.pdf",
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
assert.equal(result.filename, "custom-merged.pdf");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await t.test("rejects empty array", async () => {
|
|
402
|
+
await assert.rejects(
|
|
403
|
+
() => fastify.xPDF.mergePDFs([]),
|
|
404
|
+
{ message: /pdfBuffers must be a non-empty array/ }
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
await t.test("rejects invalid PDF in array", async () => {
|
|
409
|
+
const validPDF = await createSimplePDF();
|
|
410
|
+
const invalidPDF = Buffer.from("not a pdf");
|
|
411
|
+
|
|
412
|
+
await assert.rejects(
|
|
413
|
+
() => fastify.xPDF.mergePDFs([validPDF, invalidPDF]),
|
|
414
|
+
{ message: /invalid PDF buffers/ }
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await t.test("rejects non-array input", async () => {
|
|
419
|
+
await assert.rejects(
|
|
420
|
+
() => fastify.xPDF.mergePDFs(null),
|
|
421
|
+
{ message: /pdfBuffers must be a non-empty array/ }
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("Error Handling", async (t) => {
|
|
427
|
+
let fastify;
|
|
428
|
+
|
|
429
|
+
t.before(async () => {
|
|
430
|
+
fastify = Fastify();
|
|
431
|
+
await fastify.register(xPDF);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
t.after(async () => {
|
|
435
|
+
await fastify.close();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
await t.test("handles empty string gracefully", async () => {
|
|
439
|
+
await assert.rejects(
|
|
440
|
+
() => fastify.xPDF.generateFromHtml(""),
|
|
441
|
+
{ message: /HTML content must be a non-empty string/ }
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await t.test("handles multiple concurrent operations", async () => {
|
|
446
|
+
const operations = Array(5)
|
|
447
|
+
.fill(null)
|
|
448
|
+
.map(() => fastify.xPDF.generateFromHtml("<h1>Test</h1>"));
|
|
449
|
+
|
|
450
|
+
const results = await Promise.all(operations);
|
|
451
|
+
|
|
452
|
+
assert.equal(results.length, 5);
|
|
453
|
+
assert.ok(results.every((r) => Buffer.isBuffer(r.buffer)));
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("Configuration Options", async (t) => {
|
|
458
|
+
await t.test("uses default configuration", async () => {
|
|
459
|
+
const fastify = Fastify();
|
|
460
|
+
await fastify.register(xPDF);
|
|
461
|
+
assert.ok(fastify.xPDF);
|
|
462
|
+
await fastify.close();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
await t.test("uses custom headless setting", async () => {
|
|
466
|
+
const fastify = Fastify();
|
|
467
|
+
await fastify.register(xPDF, {
|
|
468
|
+
headless: true,
|
|
469
|
+
});
|
|
470
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Test</h1>");
|
|
471
|
+
assert.ok(result.buffer);
|
|
472
|
+
await fastify.close();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
await t.test("uses custom page format", async () => {
|
|
476
|
+
const fastify = Fastify();
|
|
477
|
+
await fastify.register(xPDF, {
|
|
478
|
+
format: "Letter",
|
|
479
|
+
});
|
|
480
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Test</h1>");
|
|
481
|
+
assert.ok(result.buffer);
|
|
482
|
+
await fastify.close();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
await t.test("uses custom margins", async () => {
|
|
486
|
+
const fastify = Fastify();
|
|
487
|
+
await fastify.register(xPDF, {
|
|
488
|
+
margin: {
|
|
489
|
+
top: "2cm",
|
|
490
|
+
right: "1cm",
|
|
491
|
+
bottom: "2cm",
|
|
492
|
+
left: "1cm",
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Test</h1>");
|
|
496
|
+
assert.ok(result.buffer);
|
|
497
|
+
await fastify.close();
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("Return Value Structure", async (t) => {
|
|
502
|
+
let fastify;
|
|
503
|
+
|
|
504
|
+
t.before(async () => {
|
|
505
|
+
fastify = Fastify();
|
|
506
|
+
await fastify.register(xPDF);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
t.after(async () => {
|
|
510
|
+
await fastify.close();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
await t.test("generateFromHtml returns correct structure", async () => {
|
|
514
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Test</h1>");
|
|
515
|
+
|
|
516
|
+
assert.ok(result.buffer);
|
|
517
|
+
assert.ok(result.filename);
|
|
518
|
+
assert.ok(typeof result.size === "number");
|
|
519
|
+
assert.ok(result.size > 0);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await t.test("generateFromMarkdown returns correct structure", async () => {
|
|
523
|
+
const result = await fastify.xPDF.generateFromMarkdown("# Test");
|
|
524
|
+
|
|
525
|
+
assert.ok(result.buffer);
|
|
526
|
+
assert.ok(result.filename);
|
|
527
|
+
assert.ok(typeof result.size === "number");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
await t.test("fillForm returns correct structure", async () => {
|
|
531
|
+
const pdfBuffer = await createFormPDF();
|
|
532
|
+
const result = await fastify.xPDF.fillForm(pdfBuffer, {});
|
|
533
|
+
|
|
534
|
+
assert.ok(result.buffer);
|
|
535
|
+
assert.ok(result.filename);
|
|
536
|
+
assert.ok(typeof result.size === "number");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await t.test("mergePDFs returns correct structure", async () => {
|
|
540
|
+
const pdfs = [await createSimplePDF(), await createSimplePDF()];
|
|
541
|
+
const result = await fastify.xPDF.mergePDFs(pdfs);
|
|
542
|
+
|
|
543
|
+
assert.ok(result.buffer);
|
|
544
|
+
assert.ok(result.filename);
|
|
545
|
+
assert.ok(typeof result.size === "number");
|
|
546
|
+
assert.ok(typeof result.pageCount === "number");
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("Helper Utilities", async (t) => {
|
|
551
|
+
const { helpers } = await import("../src/index.js");
|
|
552
|
+
|
|
553
|
+
await t.test("generatePdfFilename creates unique names", async () => {
|
|
554
|
+
const name1 = helpers.generatePdfFilename("test");
|
|
555
|
+
// Add small delay to ensure different timestamp
|
|
556
|
+
await new Promise(resolve => setTimeout(resolve, 2));
|
|
557
|
+
const name2 = helpers.generatePdfFilename("test");
|
|
558
|
+
|
|
559
|
+
assert.ok(name1.startsWith("test-"));
|
|
560
|
+
assert.ok(name2.startsWith("test-"));
|
|
561
|
+
assert.ok(name1.endsWith(".pdf"));
|
|
562
|
+
assert.notEqual(name1, name2); // Should be unique (different timestamps)
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
await t.test("isValidPdfBuffer validates PDF buffers", async () => {
|
|
566
|
+
const validPDF = await createSimplePDF();
|
|
567
|
+
const invalidBuffer = Buffer.from("not a pdf");
|
|
568
|
+
const notBuffer = "string";
|
|
569
|
+
|
|
570
|
+
assert.ok(helpers.isValidPdfBuffer(validPDF));
|
|
571
|
+
assert.ok(!helpers.isValidPdfBuffer(invalidBuffer));
|
|
572
|
+
assert.ok(!helpers.isValidPdfBuffer(notBuffer));
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
await t.test("getPdfMetadata returns correct info", async () => {
|
|
576
|
+
const pdfBuffer = await createSimplePDF();
|
|
577
|
+
const metadata = helpers.getPdfMetadata(pdfBuffer);
|
|
578
|
+
|
|
579
|
+
assert.ok(metadata.size > 0);
|
|
580
|
+
assert.equal(metadata.size, pdfBuffer.length);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await t.test("sanitizeFilename sanitizes unsafe characters", async () => {
|
|
584
|
+
const result = helpers.sanitizeFilename("My File (2024) @#$.pdf");
|
|
585
|
+
|
|
586
|
+
assert.ok(result.length > 0);
|
|
587
|
+
assert.ok(result === result.toLowerCase());
|
|
588
|
+
assert.ok(!result.includes("@"));
|
|
589
|
+
assert.ok(!result.includes("#"));
|
|
590
|
+
assert.ok(!result.includes("$"));
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await t.test("getPageFormat returns correct dimensions", async () => {
|
|
594
|
+
const a4 = helpers.getPageFormat("A4");
|
|
595
|
+
const letter = helpers.getPageFormat("Letter");
|
|
596
|
+
const a3 = helpers.getPageFormat("A3");
|
|
597
|
+
|
|
598
|
+
assert.ok(a4.width > 0 && a4.height > 0);
|
|
599
|
+
assert.ok(letter.width > 0 && letter.height > 0);
|
|
600
|
+
assert.ok(a3.width > 0 && a3.height > 0);
|
|
601
|
+
assert.ok(a3.width > a4.width); // A3 should be larger
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
await t.test("parseMargin handles different formats", async () => {
|
|
605
|
+
const objectMargin = helpers.parseMargin({
|
|
606
|
+
top: "1cm",
|
|
607
|
+
right: "2cm",
|
|
608
|
+
bottom: "1cm",
|
|
609
|
+
left: "2cm",
|
|
610
|
+
});
|
|
611
|
+
const stringMargin = helpers.parseMargin("1cm");
|
|
612
|
+
|
|
613
|
+
assert.equal(objectMargin.top, "1cm");
|
|
614
|
+
assert.equal(objectMargin.right, "2cm");
|
|
615
|
+
assert.equal(stringMargin.top, "1cm");
|
|
616
|
+
assert.equal(stringMargin.left, "1cm");
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("Additional PDF Generation Options", async (t) => {
|
|
621
|
+
let fastify;
|
|
622
|
+
|
|
623
|
+
t.before(async () => {
|
|
624
|
+
fastify = Fastify();
|
|
625
|
+
await fastify.register(xPDF);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
t.after(async () => {
|
|
629
|
+
await fastify.close();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
await t.test("printBackground option affects PDF", async () => {
|
|
633
|
+
const html = "<style>body { background: red; }</style><h1>Test</h1>";
|
|
634
|
+
|
|
635
|
+
const withBackground = await fastify.xPDF.generateFromHtml(html, {
|
|
636
|
+
printBackground: true,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const withoutBackground = await fastify.xPDF.generateFromHtml(html, {
|
|
640
|
+
printBackground: false,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Both should generate valid PDFs (size may differ due to background)
|
|
644
|
+
assert.ok(withBackground.buffer);
|
|
645
|
+
assert.ok(withoutBackground.buffer);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
await t.test("generates with A3 format", async () => {
|
|
649
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Test</h1>", {
|
|
650
|
+
format: "A3",
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
assert.ok(result.buffer);
|
|
654
|
+
assert.ok(result.size > 0);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
await t.test("generates with small A5 format", async () => {
|
|
658
|
+
const result = await fastify.xPDF.generateFromHtml("<h1>Test</h1>", {
|
|
659
|
+
format: "A5",
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
assert.ok(result.buffer);
|
|
663
|
+
assert.ok(result.size > 0);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
await t.test("handles complex HTML with tables", async () => {
|
|
667
|
+
const html = `
|
|
668
|
+
<table>
|
|
669
|
+
<tr><th>Col 1</th><th>Col 2</th></tr>
|
|
670
|
+
<tr><td>Data 1</td><td>Data 2</td></tr>
|
|
671
|
+
<tr><td>Data 3</td><td>Data 4</td></tr>
|
|
672
|
+
</table>
|
|
673
|
+
`;
|
|
674
|
+
|
|
675
|
+
const result = await fastify.xPDF.generateFromHtml(html);
|
|
676
|
+
|
|
677
|
+
assert.ok(result.buffer);
|
|
678
|
+
assert.ok(result.size > 0);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
await t.test("handles HTML with inline styles", async () => {
|
|
682
|
+
const html = `
|
|
683
|
+
<h1 style="color: blue; text-align: center;">Styled Title</h1>
|
|
684
|
+
<p style="font-size: 14px; line-height: 1.8;">Paragraph</p>
|
|
685
|
+
`;
|
|
686
|
+
|
|
687
|
+
const result = await fastify.xPDF.generateFromHtml(html);
|
|
688
|
+
|
|
689
|
+
assert.ok(result.buffer);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
await t.test("handles Markdown with code blocks", async () => {
|
|
693
|
+
const markdown = `
|
|
694
|
+
# Code Example
|
|
695
|
+
|
|
696
|
+
\`\`\`javascript
|
|
697
|
+
function test() {
|
|
698
|
+
console.log("Hello");
|
|
699
|
+
}
|
|
700
|
+
\`\`\`
|
|
701
|
+
|
|
702
|
+
\`\`\`python
|
|
703
|
+
def test():
|
|
704
|
+
print("Hello")
|
|
705
|
+
\`\`\`
|
|
706
|
+
`;
|
|
707
|
+
|
|
708
|
+
const result = await fastify.xPDF.generateFromMarkdown(markdown);
|
|
709
|
+
|
|
710
|
+
assert.ok(result.buffer);
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("PDF Form Advanced Tests", async (t) => {
|
|
715
|
+
let fastify;
|
|
716
|
+
|
|
717
|
+
t.before(async () => {
|
|
718
|
+
fastify = Fastify();
|
|
719
|
+
await fastify.register(xPDF);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
t.after(async () => {
|
|
723
|
+
await fastify.close();
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
await t.test("handles missing form fields gracefully", async () => {
|
|
727
|
+
const pdfBuffer = await createFormPDF();
|
|
728
|
+
|
|
729
|
+
const result = await fastify.xPDF.fillForm(
|
|
730
|
+
pdfBuffer,
|
|
731
|
+
{
|
|
732
|
+
firstName: "John",
|
|
733
|
+
nonExistentField: "value",
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
flatten: false,
|
|
737
|
+
}
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
assert.ok(result.buffer); // Should still return valid PDF
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
await t.test("can fill form without flattening", async () => {
|
|
744
|
+
const pdfBuffer = await createFormPDF();
|
|
745
|
+
|
|
746
|
+
const result = await fastify.xPDF.fillForm(
|
|
747
|
+
pdfBuffer,
|
|
748
|
+
{ firstName: "Jane" },
|
|
749
|
+
{
|
|
750
|
+
flatten: false,
|
|
751
|
+
}
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
assert.ok(result.buffer);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
await t.test("fillForm with empty field values", async () => {
|
|
758
|
+
const pdfBuffer = await createFormPDF();
|
|
759
|
+
|
|
760
|
+
const result = await fastify.xPDF.fillForm(pdfBuffer, {}, {
|
|
761
|
+
flatten: true,
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
assert.ok(result.buffer);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
await t.test("multiple concurrent form fills", async () => {
|
|
768
|
+
const pdfBuffer = await createFormPDF();
|
|
769
|
+
|
|
770
|
+
const operations = Array(3)
|
|
771
|
+
.fill(null)
|
|
772
|
+
.map((_, i) =>
|
|
773
|
+
fastify.xPDF.fillForm(
|
|
774
|
+
pdfBuffer,
|
|
775
|
+
{ firstName: `Person${i}` },
|
|
776
|
+
{
|
|
777
|
+
flatten: true,
|
|
778
|
+
filename: `form-${i}.pdf`,
|
|
779
|
+
}
|
|
780
|
+
)
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
const results = await Promise.all(operations);
|
|
784
|
+
|
|
785
|
+
assert.equal(results.length, 3);
|
|
786
|
+
assert.ok(results.every((r) => Buffer.isBuffer(r.buffer)));
|
|
787
|
+
assert.ok(results.every((r) => r.filename.startsWith("form-")));
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test("PDF Merging Advanced Tests", async (t) => {
|
|
792
|
+
let fastify;
|
|
793
|
+
|
|
794
|
+
t.before(async () => {
|
|
795
|
+
fastify = Fastify();
|
|
796
|
+
await fastify.register(xPDF);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
t.after(async () => {
|
|
800
|
+
await fastify.close();
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
await t.test("merges large number of PDFs", async () => {
|
|
804
|
+
const pdfs = await Promise.all(
|
|
805
|
+
Array(10)
|
|
806
|
+
.fill(null)
|
|
807
|
+
.map(() => createSimplePDF())
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
const result = await fastify.xPDF.mergePDFs(pdfs);
|
|
811
|
+
|
|
812
|
+
assert.ok(result.buffer);
|
|
813
|
+
assert.ok(result.pageCount >= 10);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
await t.test("merge result is valid PDF", async () => {
|
|
817
|
+
const pdfs = [await createSimplePDF(), await createSimplePDF()];
|
|
818
|
+
const result = await fastify.xPDF.mergePDFs(pdfs);
|
|
819
|
+
|
|
820
|
+
// Verify PDF header
|
|
821
|
+
assert.equal(result.buffer[0], 0x25); // %
|
|
822
|
+
assert.equal(result.buffer[1], 0x50); // P
|
|
823
|
+
assert.equal(result.buffer[2], 0x44); // D
|
|
824
|
+
assert.equal(result.buffer[3], 0x46); // F
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
await t.test("merge preserves content order", async () => {
|
|
828
|
+
const pdf1 = await createSimplePDF();
|
|
829
|
+
const pdf2 = await createSimplePDF();
|
|
830
|
+
|
|
831
|
+
const result = await fastify.xPDF.mergePDFs([pdf1, pdf2], {
|
|
832
|
+
filename: "ordered-merge.pdf",
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
assert.ok(result.buffer);
|
|
836
|
+
assert.equal(result.filename, "ordered-merge.pdf");
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test("Edge Cases and Stress Tests", async (t) => {
|
|
841
|
+
let fastify;
|
|
842
|
+
|
|
843
|
+
t.before(async () => {
|
|
844
|
+
fastify = Fastify();
|
|
845
|
+
await fastify.register(xPDF);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
t.after(async () => {
|
|
849
|
+
await fastify.close();
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
await t.test("handles high volume of concurrent HTML to PDF", async () => {
|
|
853
|
+
const operations = Array(5)
|
|
854
|
+
.fill(null)
|
|
855
|
+
.map(() => fastify.xPDF.generateFromHtml("<h1>Test</h1>"));
|
|
856
|
+
|
|
857
|
+
const results = await Promise.all(operations);
|
|
858
|
+
|
|
859
|
+
assert.equal(results.length, 5);
|
|
860
|
+
assert.ok(results.every((r) => Buffer.isBuffer(r.buffer)));
|
|
861
|
+
assert.ok(results.every((r) => r.size > 0));
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
await t.test("handles very long HTML content", async () => {
|
|
865
|
+
const longContent = `<h1>Start</h1>${Array(100)
|
|
866
|
+
.fill('<p>Paragraph content</p>')
|
|
867
|
+
.join("")}<h1>End</h1>`;
|
|
868
|
+
|
|
869
|
+
const result = await fastify.xPDF.generateFromHtml(longContent);
|
|
870
|
+
|
|
871
|
+
assert.ok(result.buffer);
|
|
872
|
+
assert.ok(result.size > 1000); // Should be reasonably large
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
await t.test("handles Unicode content in HTML", async () => {
|
|
876
|
+
const unicodeHtml = `
|
|
877
|
+
<h1>Unicode Test</h1>
|
|
878
|
+
<p>English: Hello</p>
|
|
879
|
+
<p>Japanese: こんにちは</p>
|
|
880
|
+
<p>Arabic: مرحبا</p>
|
|
881
|
+
<p>Emoji: 🎉 🚀 ✨</p>
|
|
882
|
+
`;
|
|
883
|
+
|
|
884
|
+
const result = await fastify.xPDF.generateFromHtml(unicodeHtml);
|
|
885
|
+
|
|
886
|
+
assert.ok(result.buffer);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
await t.test("handles Unicode content in Markdown", async () => {
|
|
890
|
+
const unicodeMarkdown = `
|
|
891
|
+
# Unicode Test
|
|
892
|
+
|
|
893
|
+
- English: Hello
|
|
894
|
+
- Japanese: こんにちは
|
|
895
|
+
- Arabic: مرحبا
|
|
896
|
+
- Emoji: 🎉 🚀 ✨
|
|
897
|
+
`;
|
|
898
|
+
|
|
899
|
+
const result = await fastify.xPDF.generateFromMarkdown(unicodeMarkdown);
|
|
900
|
+
|
|
901
|
+
assert.ok(result.buffer);
|
|
902
|
+
});
|
|
903
|
+
});
|