@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/README.md ADDED
@@ -0,0 +1,580 @@
1
+ # xPDF
2
+
3
+ > Fastify v5 plugin for PDF generation and manipulation with a simple, intuitive library of methods.
4
+
5
+ Generate PDFs from HTML and Markdown, fill PDF forms, and merge PDFs. Works with any Fastify application and optionally integrates with [xStorage](https://github.com/x-enterprises/fastify-plugins/tree/main/fastify-xstorage) for automatic cloud storage.
6
+
7
+ ## Requirements
8
+
9
+ - **Fastify v5.0.0+**
10
+ - **Node.js v20+**
11
+
12
+ ## Features
13
+
14
+ - 📄 **HTML to PDF** - Convert HTML to PDF using Puppeteer
15
+ - 📝 **Markdown to PDF** - Convert Markdown to PDF with styled formatting
16
+ - 📋 **Form Filling** - Fill PDF form fields with values
17
+ - ✏️ **Form Flattening** - Make form fields non-editable
18
+ - 🔗 **PDF Merging** - Combine multiple PDFs into one
19
+ - 💾 **Optional Storage** - Save PDFs to S3-compatible storage via xStorage
20
+ - 🔌 **Simple API** - Methods decorated on Fastify instance
21
+ - 🎯 **Library Pattern** - Use as a service, not as HTTP routes
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @xenterprises/fastify-xpdf puppeteer marked pdf-lib fastify@5
27
+ ```
28
+
29
+ For optional storage integration:
30
+
31
+ ```bash
32
+ npm install @xenterprises/fastify-xstorage
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```javascript
38
+ import Fastify from "fastify";
39
+ import xPDF from "@xenterprises/fastify-xpdf";
40
+
41
+ const fastify = Fastify({ logger: true });
42
+
43
+ // Register xPDF
44
+ await fastify.register(xPDF, {
45
+ headless: true,
46
+ useStorage: false,
47
+ defaultFolder: "pdfs",
48
+ });
49
+
50
+ // Generate PDF from HTML
51
+ const result = await fastify.xPDF.generateFromHtml("<h1>Hello World</h1>", {
52
+ filename: "hello.pdf",
53
+ });
54
+
55
+ console.log(result);
56
+ // {
57
+ // buffer: Buffer,
58
+ // filename: "hello.pdf",
59
+ // size: 12345,
60
+ // storageKey?: "pdfs/hello.pdf", // Only if saveToStorage: true
61
+ // url?: "https://..." // Only if saveToStorage: true
62
+ // }
63
+
64
+ await fastify.listen({ port: 3000 });
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ ### Plugin Options
70
+
71
+ ```javascript
72
+ await fastify.register(xPDF, {
73
+ // Puppeteer options
74
+ headless: true, // Default: true
75
+ args: ["--no-sandbox"], // Chrome launch args
76
+
77
+ // xStorage integration (optional)
78
+ useStorage: false, // Default: false
79
+ defaultFolder: "pdfs", // Default storage folder
80
+
81
+ // PDF generation defaults
82
+ format: "A4", // Default page format
83
+ printBackground: true, // Include background in PDFs
84
+ margin: { // Default margins
85
+ top: "1cm",
86
+ right: "1cm",
87
+ bottom: "1cm",
88
+ left: "1cm",
89
+ },
90
+ });
91
+ ```
92
+
93
+ ## Core API
94
+
95
+ All methods are available on the `fastify.xPDF` namespace.
96
+
97
+ ### `fastify.xPDF.generateFromHtml(html, options)`
98
+
99
+ Convert HTML to PDF.
100
+
101
+ ```javascript
102
+ const result = await fastify.xPDF.generateFromHtml("<h1>Invoice</h1>", {
103
+ filename: "invoice.pdf",
104
+ format: "A4",
105
+ landscape: false,
106
+ margin: { top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" },
107
+ printBackground: true,
108
+ saveToStorage: false,
109
+ folder: "pdfs",
110
+ });
111
+
112
+ console.log(result);
113
+ // {
114
+ // buffer: Buffer, // PDF content as Buffer
115
+ // filename: "invoice.pdf", // Output filename
116
+ // size: 245678, // File size in bytes
117
+ // storageKey?: "pdfs/...", // Only if saveToStorage: true
118
+ // url?: "https://..." // Only if saveToStorage: true
119
+ // }
120
+ ```
121
+
122
+ **Options:**
123
+ - `filename` - Output filename (default: `generated-{timestamp}.pdf`)
124
+ - `format` - Page format: A4, Letter, Legal, A3, A5, etc. (default: A4)
125
+ - `landscape` - Landscape orientation (default: false)
126
+ - `margin` - Page margins (default: 1cm all sides)
127
+ - `printBackground` - Include background graphics (default: true)
128
+ - `saveToStorage` - Save to xStorage (default: false)
129
+ - `folder` - Storage folder (default: 'pdfs')
130
+
131
+ ### `fastify.xPDF.generateFromMarkdown(markdown, options)`
132
+
133
+ Convert Markdown to PDF.
134
+
135
+ ```javascript
136
+ const markdown = `
137
+ # Invoice
138
+
139
+ **Date:** January 1, 2024
140
+
141
+ ## Items
142
+
143
+ 1. Item 1: $100
144
+ 2. Item 2: $200
145
+
146
+ **Total:** $300
147
+ `;
148
+
149
+ const result = await fastify.xPDF.generateFromMarkdown(markdown, {
150
+ filename: "invoice.pdf",
151
+ saveToStorage: true,
152
+ folder: "invoices",
153
+ });
154
+
155
+ console.log(result);
156
+ // {
157
+ // buffer: Buffer,
158
+ // filename: "invoice.pdf",
159
+ // size: 234567,
160
+ // storageKey: "invoices/invoice-1234567890.pdf",
161
+ // url: "https://storage.example.com/invoices/invoice-1234567890.pdf"
162
+ // }
163
+ ```
164
+
165
+ **Supports:**
166
+ - Headings (H1-H6)
167
+ - Paragraphs and line breaks
168
+ - Lists (ordered and unordered)
169
+ - Code blocks with syntax highlighting
170
+ - Tables
171
+ - Blockquotes
172
+ - Links
173
+ - Emphasis (bold, italic)
174
+
175
+ ### `fastify.xPDF.fillForm(pdfBuffer, fieldValues, options)`
176
+
177
+ Fill PDF form fields and optionally flatten.
178
+
179
+ ```javascript
180
+ // Download PDF from storage
181
+ const pdfBuffer = await fastify.xStorage.download("forms/application.pdf");
182
+
183
+ // Fill form
184
+ const result = await fastify.xPDF.fillForm(
185
+ pdfBuffer,
186
+ {
187
+ firstName: "John",
188
+ lastName: "Doe",
189
+ email: "john@example.com",
190
+ agreeToTerms: true,
191
+ },
192
+ {
193
+ flatten: true, // Make fields non-editable
194
+ filename: "application-filled.pdf",
195
+ saveToStorage: true,
196
+ folder: "submitted-forms",
197
+ }
198
+ );
199
+
200
+ console.log(result);
201
+ // {
202
+ // buffer: Buffer,
203
+ // filename: "application-filled.pdf",
204
+ // size: 256789,
205
+ // storageKey: "submitted-forms/...",
206
+ // url: "https://..."
207
+ // }
208
+ ```
209
+
210
+ **Options:**
211
+ - `flatten` - Make form fields non-editable (default: true)
212
+ - `filename` - Output filename
213
+ - `saveToStorage` - Save to xStorage (default: false)
214
+ - `folder` - Storage folder (default: 'pdfs')
215
+
216
+ **Supported Field Types:**
217
+ - Text fields: `field.setText(value)`
218
+ - Checkboxes: `field.check()` or `field.uncheck()`
219
+ - Radio buttons: `field.select(value)`
220
+ - Dropdowns: `field.select(value)`
221
+
222
+ ### `fastify.xPDF.listFormFields(pdfBuffer)`
223
+
224
+ List all form fields in a PDF.
225
+
226
+ ```javascript
227
+ const pdfBuffer = await fastify.xStorage.download("forms/application.pdf");
228
+
229
+ const fields = await fastify.xPDF.listFormFields(pdfBuffer);
230
+
231
+ console.log(fields);
232
+ // [
233
+ // { name: "firstName", type: "text", value: null },
234
+ // { name: "lastName", type: "text", value: null },
235
+ // { name: "email", type: "text", value: null },
236
+ // { name: "agreeToTerms", type: "checkbox", value: false }
237
+ // ]
238
+ ```
239
+
240
+ **Returns:**
241
+ - `name` - Field name
242
+ - `type` - Field type (text, checkbox, radio, dropdown, option)
243
+ - `value` - Current field value
244
+
245
+ ### `fastify.xPDF.mergePDFs(pdfBuffers, options)`
246
+
247
+ Merge multiple PDFs into one.
248
+
249
+ ```javascript
250
+ // Download PDFs from storage
251
+ const page1 = await fastify.xStorage.download("documents/page1.pdf");
252
+ const page2 = await fastify.xStorage.download("documents/page2.pdf");
253
+ const page3 = await fastify.xStorage.download("documents/page3.pdf");
254
+
255
+ const result = await fastify.xPDF.mergePDFs([page1, page2, page3], {
256
+ filename: "complete-document.pdf",
257
+ saveToStorage: true,
258
+ folder: "merged",
259
+ });
260
+
261
+ console.log(result);
262
+ // {
263
+ // buffer: Buffer,
264
+ // filename: "complete-document.pdf",
265
+ // size: 512345,
266
+ // pageCount: 15, // Total pages from all 3 PDFs
267
+ // storageKey: "merged/complete-document-1234567890.pdf",
268
+ // url: "https://..."
269
+ // }
270
+ ```
271
+
272
+ **Options:**
273
+ - `filename` - Output filename (default: `merged-{timestamp}.pdf`)
274
+ - `saveToStorage` - Save to xStorage (default: false)
275
+ - `folder` - Storage folder (default: 'pdfs')
276
+
277
+ **Returns:**
278
+ - `buffer` - Merged PDF buffer
279
+ - `filename` - Output filename
280
+ - `size` - File size in bytes
281
+ - `pageCount` - Total number of pages
282
+ - `storageKey` - Storage key (if saved)
283
+ - `url` - Public URL (if saved)
284
+
285
+ ## xStorage Integration
286
+
287
+ xPDF works seamlessly with xStorage for automatic cloud storage of generated PDFs.
288
+
289
+ ### Basic Integration
290
+
291
+ ```javascript
292
+ import Fastify from "fastify";
293
+ import xStorage from "@xenterprises/fastify-xstorage";
294
+ import xPDF from "@xenterprises/fastify-xpdf";
295
+
296
+ const fastify = Fastify();
297
+
298
+ // Register xStorage first
299
+ await fastify.register(xStorage, {
300
+ endpoint: "https://nyc3.digitaloceanspaces.com",
301
+ region: "nyc3",
302
+ accessKeyId: process.env.DO_SPACES_KEY,
303
+ secretAccessKey: process.env.DO_SPACES_SECRET,
304
+ bucket: "my-bucket",
305
+ publicUrl: "https://my-bucket.nyc3.digitaloceanspaces.com",
306
+ });
307
+
308
+ // Register xPDF (will auto-detect xStorage)
309
+ await fastify.register(xPDF, {
310
+ useStorage: true,
311
+ defaultFolder: "pdfs",
312
+ });
313
+
314
+ // Now all PDFs can be saved to storage
315
+ const result = await fastify.xPDF.generateFromHtml("<h1>Report</h1>", {
316
+ saveToStorage: true,
317
+ folder: "reports",
318
+ });
319
+
320
+ console.log(result.url); // https://my-bucket.nyc3.digitaloceanspaces.com/reports/...
321
+ ```
322
+
323
+ ### Advanced: Retrieve and Process Stored PDFs
324
+
325
+ ```javascript
326
+ // Get PDF from storage
327
+ const storedUrl = "https://my-bucket.nyc3.digitaloceanspaces.com/forms/template.pdf";
328
+ const key = storedUrl.replace(process.env.STORAGE_PUBLIC_URL + "/", "");
329
+ const pdfBuffer = await fastify.xStorage.download(key);
330
+
331
+ // Fill form and save result
332
+ const result = await fastify.xPDF.fillForm(pdfBuffer, { name: "John" }, {
333
+ saveToStorage: true,
334
+ folder: "submitted",
335
+ });
336
+
337
+ console.log(result.url); // Direct link to filled form
338
+ ```
339
+
340
+ ## Helper Utilities
341
+
342
+ ```javascript
343
+ import { helpers } from "@xenterprises/fastify-xpdf";
344
+
345
+ // Generate unique PDF filename
346
+ helpers.generatePdfFilename("invoice");
347
+ // "invoice-1704067200000.pdf"
348
+
349
+ // Validate PDF buffer
350
+ helpers.isValidPdfBuffer(buffer);
351
+ // true/false
352
+
353
+ // Get PDF metadata
354
+ helpers.getPdfMetadata(buffer);
355
+ // { size: 12345 }
356
+
357
+ // Sanitize filename
358
+ helpers.sanitizeFilename("My File (2024).pdf");
359
+ // "my_file_2024.pdf"
360
+
361
+ // Get page format dimensions
362
+ helpers.getPageFormat("A4");
363
+ // { width: 8.27, height: 11.7 }
364
+ ```
365
+
366
+ ## Usage Examples
367
+
368
+ ### Document Generation
369
+
370
+ ```javascript
371
+ fastify.post("/invoices", async (request, reply) => {
372
+ const { items, total, date } = request.body;
373
+
374
+ const html = `
375
+ <h1>Invoice</h1>
376
+ <p>Date: ${date}</p>
377
+ <ul>
378
+ ${items.map((item) => `<li>${item.name}: $${item.price}</li>`).join("")}
379
+ </ul>
380
+ <h2>Total: $${total}</h2>
381
+ `;
382
+
383
+ const result = await fastify.xPDF.generateFromHtml(html, {
384
+ filename: "invoice.pdf",
385
+ saveToStorage: true,
386
+ folder: "invoices",
387
+ });
388
+
389
+ return { success: true, url: result.url };
390
+ });
391
+ ```
392
+
393
+ ### Form Processing
394
+
395
+ ```javascript
396
+ fastify.post("/applications/:id/submit", async (request, reply) => {
397
+ const { id } = request.params;
398
+ const formData = request.body;
399
+
400
+ // Get form template
401
+ const template = await fastify.xStorage.download("forms/application-template.pdf");
402
+
403
+ // Fill with submitted data
404
+ const result = await fastify.xPDF.fillForm(template, formData, {
405
+ flatten: true,
406
+ filename: `application-${id}.pdf`,
407
+ saveToStorage: true,
408
+ folder: "applications",
409
+ });
410
+
411
+ // Save record in database
412
+ await fastify.db.application.create({
413
+ id,
414
+ pdfKey: result.storageKey,
415
+ pdfUrl: result.url,
416
+ submittedAt: new Date(),
417
+ });
418
+
419
+ return { success: true, pdfUrl: result.url };
420
+ });
421
+ ```
422
+
423
+ ### Report Compilation
424
+
425
+ ```javascript
426
+ fastify.post("/reports/compile", async (request, reply) => {
427
+ const { reportIds } = request.body;
428
+
429
+ // Get all report PDFs
430
+ const pdfBuffers = await Promise.all(
431
+ reportIds.map((id) =>
432
+ fastify.xStorage.download(`reports/${id}.pdf`)
433
+ )
434
+ );
435
+
436
+ // Merge into single document
437
+ const result = await fastify.xPDF.mergePDFs(pdfBuffers, {
438
+ filename: "compiled-report.pdf",
439
+ saveToStorage: true,
440
+ folder: "compiled",
441
+ });
442
+
443
+ return {
444
+ success: true,
445
+ filename: result.filename,
446
+ pages: result.pageCount,
447
+ url: result.url,
448
+ };
449
+ });
450
+ ```
451
+
452
+ ## Best Practices
453
+
454
+ 1. **Use xStorage for Production** - Automatically manage PDF storage and access
455
+ 2. **Validate Input** - Sanitize HTML/Markdown to prevent XSS in PDF generation
456
+ 3. **Handle Large PDFs** - Set reasonable timeouts for Puppeteer operations
457
+ 4. **Organize with Folders** - Use meaningful folder structures (e.g., "invoices/2024")
458
+ 5. **Store Metadata** - Keep records of storage keys in your database
459
+ 6. **Error Handling** - Always wrap PDF operations in try-catch blocks
460
+ 7. **Memory Management** - PDFs are loaded in memory; handle size carefully
461
+ 8. **Reuse Browser** - Plugin maintains single browser instance for efficiency
462
+
463
+ ## Configuration Profiles
464
+
465
+ ### Development
466
+
467
+ ```javascript
468
+ await fastify.register(xPDF, {
469
+ headless: true,
470
+ useStorage: false, // Save to disk instead
471
+ format: "A4",
472
+ });
473
+ ```
474
+
475
+ ### Production with Storage
476
+
477
+ ```javascript
478
+ await fastify.register(xPDF, {
479
+ headless: true,
480
+ useStorage: true,
481
+ defaultFolder: "pdfs",
482
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
483
+ });
484
+ ```
485
+
486
+ ### High Performance
487
+
488
+ ```javascript
489
+ await fastify.register(xPDF, {
490
+ headless: true,
491
+ args: [
492
+ "--no-sandbox",
493
+ "--disable-setuid-sandbox",
494
+ "--disable-gpu",
495
+ "--disable-dev-shm-usage",
496
+ ],
497
+ useStorage: true,
498
+ });
499
+ ```
500
+
501
+ ## Troubleshooting
502
+
503
+ ### Puppeteer Launch Fails
504
+
505
+ **Error:** `Timeout waiting for browser to start`
506
+
507
+ **Solution:** Increase available memory and ensure Chrome dependencies are installed
508
+ ```bash
509
+ # macOS
510
+ brew install chromium
511
+
512
+ # Linux
513
+ apt-get install -y chromium-browser
514
+
515
+ # Or use args to help with sandboxing
516
+ headless: true,
517
+ args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]
518
+ ```
519
+
520
+ ### Form Fields Not Filling
521
+
522
+ **Error:** `Form field not found: fieldName`
523
+
524
+ **Solution:** List form fields first to verify correct names
525
+ ```javascript
526
+ const fields = await fastify.xPDF.listFormFields(pdfBuffer);
527
+ console.log(fields); // Check exact field names
528
+ ```
529
+
530
+ ### PDF Generation Times Out
531
+
532
+ **Error:** `Timeout waiting for page to load`
533
+
534
+ **Solution:** Simplify HTML, remove external resources, or optimize content
535
+ ```javascript
536
+ // ❌ Avoid external resources
537
+ const html = '<img src="https://example.com/image.jpg">';
538
+
539
+ // ✅ Use inline or relative paths
540
+ const html = '<img src="/images/logo.png">';
541
+ ```
542
+
543
+ ### Storage Integration Not Working
544
+
545
+ **Error:** `xStorage plugin not registered`
546
+
547
+ **Solution:** Register xStorage before xPDF
548
+ ```javascript
549
+ // Register xStorage FIRST
550
+ await fastify.register(xStorage, {...});
551
+
552
+ // Then register xPDF
553
+ await fastify.register(xPDF, {
554
+ useStorage: true,
555
+ });
556
+ ```
557
+
558
+ ## Performance Tips
559
+
560
+ - Use `headless: true` (already default)
561
+ - Enable `printBackground: false` for simpler documents
562
+ - Avoid large images or external resources in HTML
563
+ - Use Markdown for simple, styled documents
564
+ - Batch operations when merging many PDFs
565
+
566
+ ## Testing
567
+
568
+ ```bash
569
+ npm test
570
+ ```
571
+
572
+ See [TESTING.md](./TESTING.md) for comprehensive testing guide.
573
+
574
+ ## Examples
575
+
576
+ See [EXAMPLES.md](./EXAMPLES.md) for complete real-world examples.
577
+
578
+ ## License
579
+
580
+ ISC