@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.
@@ -0,0 +1,163 @@
1
+ // src/services/forms.js
2
+ import { PDFDocument } from "pdf-lib";
3
+ import { generatePdfFilename, isValidPdfBuffer } from "../utils/helpers.js";
4
+
5
+ /**
6
+ * Save PDF to xStorage if configured
7
+ */
8
+ async function saveToStorage(fastify, buffer, filename, folder) {
9
+ if (!fastify.xStorage) {
10
+ return null;
11
+ }
12
+
13
+ try {
14
+ const result = await fastify.xStorage.upload(buffer, filename, {
15
+ folder,
16
+ contentType: "application/pdf",
17
+ });
18
+ return {
19
+ storageKey: result.key,
20
+ url: result.url,
21
+ };
22
+ } catch (error) {
23
+ fastify.log.error("Failed to save PDF to storage:", error);
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export function getFormsMethods(config) {
29
+ const { fastify, defaultFolder } = config;
30
+
31
+ return {
32
+ /**
33
+ * Fill PDF form fields
34
+ * @param {Buffer} pdfBuffer - Source PDF buffer
35
+ * @param {object} fieldValues - Key-value pairs of field names and values
36
+ * @param {object} options - Fill options
37
+ * @returns {Promise<{buffer: Buffer, filename: string, size: number, storageKey?: string, url?: string}>}
38
+ */
39
+ fillForm: async (pdfBuffer, fieldValues = {}, options = {}) => {
40
+ try {
41
+ if (!isValidPdfBuffer(pdfBuffer)) {
42
+ throw new Error("Invalid PDF buffer");
43
+ }
44
+
45
+ const {
46
+ flatten = true,
47
+ filename = generatePdfFilename("filled-form"),
48
+ saveToStorage: save = false,
49
+ folder = defaultFolder,
50
+ } = options;
51
+
52
+ // Load PDF
53
+ const pdfDoc = await PDFDocument.load(pdfBuffer);
54
+ const form = pdfDoc.getForm();
55
+
56
+ // Fill form fields
57
+ for (const [fieldName, value] of Object.entries(fieldValues)) {
58
+ try {
59
+ const field = form.getFieldMaybe(fieldName);
60
+ if (!field) {
61
+ fastify.log.warn(`Form field not found: ${fieldName}`);
62
+ continue;
63
+ }
64
+
65
+ // Handle different field types
66
+ if (field.isText?.()) {
67
+ field.setText(String(value || ""));
68
+ } else if (field.isCheckBox?.()) {
69
+ if (value) {
70
+ field.check();
71
+ } else {
72
+ field.uncheck();
73
+ }
74
+ } else if (field.isRadioGroup?.()) {
75
+ field.select(String(value));
76
+ } else if (field.isDropdown?.()) {
77
+ field.select(String(value));
78
+ } else if (field.isOption?.()) {
79
+ field.select(String(value));
80
+ }
81
+ } catch (error) {
82
+ fastify.log.warn(`Error filling field ${fieldName}:`, error.message);
83
+ }
84
+ }
85
+
86
+ // Flatten form if requested (make non-editable)
87
+ if (flatten) {
88
+ form.flatten();
89
+ }
90
+
91
+ // Save PDF
92
+ const pdfBytes = await pdfDoc.save();
93
+ const filledBuffer = Buffer.from(pdfBytes);
94
+
95
+ // Save to storage if requested
96
+ let storageResult = null;
97
+ if (save && fastify.xStorage) {
98
+ storageResult = await saveToStorage(fastify, filledBuffer, filename, folder);
99
+ }
100
+
101
+ fastify.log.info(`PDF form filled: ${filename}`);
102
+
103
+ return {
104
+ buffer: filledBuffer,
105
+ filename,
106
+ size: filledBuffer.length,
107
+ ...(storageResult && {
108
+ storageKey: storageResult.storageKey,
109
+ url: storageResult.url,
110
+ }),
111
+ };
112
+ } catch (error) {
113
+ fastify.log.error("Form fill failed:", error);
114
+ throw new Error(`Failed to fill PDF form: ${error.message}`);
115
+ }
116
+ },
117
+
118
+ /**
119
+ * List all form fields in a PDF
120
+ * @param {Buffer} pdfBuffer - Source PDF buffer
121
+ * @returns {Promise<Array<{name: string, type: string, value: any}>>}
122
+ */
123
+ listFormFields: async (pdfBuffer) => {
124
+ try {
125
+ if (!isValidPdfBuffer(pdfBuffer)) {
126
+ throw new Error("Invalid PDF buffer");
127
+ }
128
+
129
+ // Load PDF
130
+ const pdfDoc = await PDFDocument.load(pdfBuffer);
131
+ const form = pdfDoc.getForm();
132
+
133
+ // Get all fields
134
+ const fields = form.getFields();
135
+ const fieldList = fields.map((field) => {
136
+ const fieldName = field.getName();
137
+ const value = field.getValue?.() || null;
138
+
139
+ // Determine field type
140
+ let type = "unknown";
141
+ if (field.isText?.()) type = "text";
142
+ else if (field.isCheckBox?.()) type = "checkbox";
143
+ else if (field.isRadioGroup?.()) type = "radio";
144
+ else if (field.isDropdown?.()) type = "dropdown";
145
+ else if (field.isOption?.()) type = "option";
146
+
147
+ return {
148
+ name: fieldName,
149
+ type,
150
+ value,
151
+ };
152
+ });
153
+
154
+ fastify.log.info(`Listed ${fieldList.length} form fields`);
155
+
156
+ return fieldList;
157
+ } catch (error) {
158
+ fastify.log.error("List form fields failed:", error);
159
+ throw new Error(`Failed to list form fields: ${error.message}`);
160
+ }
161
+ },
162
+ };
163
+ }
@@ -0,0 +1,147 @@
1
+ // src/services/generator.js
2
+ import { marked } from "marked";
3
+ import {
4
+ generatePdfFilename,
5
+ formatPdfOptions,
6
+ wrapHtmlTemplate,
7
+ parseMargin,
8
+ getPageFormat,
9
+ } from "../utils/helpers.js";
10
+
11
+ /**
12
+ * Save PDF to xStorage if configured
13
+ */
14
+ async function saveToStorage(fastify, buffer, filename, folder) {
15
+ if (!fastify.xStorage) {
16
+ return null;
17
+ }
18
+
19
+ try {
20
+ const result = await fastify.xStorage.upload(buffer, filename, {
21
+ folder,
22
+ contentType: "application/pdf",
23
+ });
24
+ return {
25
+ storageKey: result.key,
26
+ url: result.url,
27
+ };
28
+ } catch (error) {
29
+ fastify.log.error("Failed to save PDF to storage:", error);
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export function getGeneratorMethods(config) {
35
+ const { getBrowser, fastify, format, printBackground, margin, defaultFolder } =
36
+ config;
37
+
38
+ /**
39
+ * Generate PDF from HTML
40
+ * @param {string} html - HTML content
41
+ * @param {object} options - Generation options
42
+ * @returns {Promise<{buffer: Buffer, filename: string, size: number, storageKey?: string, url?: string}>}
43
+ */
44
+ const generateFromHtml = async (html, options = {}) => {
45
+ try {
46
+ if (!html || typeof html !== "string") {
47
+ throw new Error("HTML content must be a non-empty string");
48
+ }
49
+
50
+ const {
51
+ filename = generatePdfFilename("document"),
52
+ format: pageFormat = format,
53
+ landscape = false,
54
+ margin: pageMargin = margin,
55
+ printBackground: background = printBackground,
56
+ saveToStorage: save = false,
57
+ folder = defaultFolder,
58
+ } = options;
59
+
60
+ // Get browser instance
61
+ const browser = await getBrowser();
62
+ const page = await browser.newPage();
63
+
64
+ try {
65
+ // Set content
66
+ await page.setContent(html, {
67
+ waitUntil: "networkidle2",
68
+ timeout: 30000,
69
+ });
70
+
71
+ // Generate PDF
72
+ const pdfData = await page.pdf({
73
+ format: pageFormat,
74
+ landscape,
75
+ margin: parseMargin(pageMargin),
76
+ printBackground: background,
77
+ timeout: 30000,
78
+ });
79
+
80
+ // Convert to Buffer if needed
81
+ const pdfBuffer = Buffer.isBuffer(pdfData) ? pdfData : Buffer.from(pdfData);
82
+
83
+ // Save to storage if requested
84
+ let storageResult = null;
85
+ if (save && fastify.xStorage) {
86
+ storageResult = await saveToStorage(fastify, pdfBuffer, filename, folder);
87
+ }
88
+
89
+ fastify.log.info(`PDF generated: ${filename}`);
90
+
91
+ return {
92
+ buffer: pdfBuffer,
93
+ filename,
94
+ size: pdfBuffer.length,
95
+ ...(storageResult && {
96
+ storageKey: storageResult.storageKey,
97
+ url: storageResult.url,
98
+ }),
99
+ };
100
+ } finally {
101
+ // Clean up page
102
+ await page.close();
103
+ }
104
+ } catch (error) {
105
+ fastify.log.error("HTML to PDF generation failed:", error);
106
+ throw new Error(`Failed to generate PDF from HTML: ${error.message}`);
107
+ }
108
+ };
109
+
110
+ /**
111
+ * Generate PDF from Markdown
112
+ * @param {string} markdown - Markdown content
113
+ * @param {object} options - Generation options (same as generateFromHtml)
114
+ * @returns {Promise<{buffer: Buffer, filename: string, size: number, storageKey?: string, url?: string}>}
115
+ */
116
+ const generateFromMarkdown = async (markdown, options = {}) => {
117
+ try {
118
+ if (!markdown || typeof markdown !== "string") {
119
+ throw new Error("Markdown content must be a non-empty string");
120
+ }
121
+
122
+ // Convert markdown to HTML
123
+ const htmlContent = marked(markdown);
124
+
125
+ // Wrap in template
126
+ const fullHtml = wrapHtmlTemplate(htmlContent);
127
+
128
+ // Generate PDF from HTML
129
+ const result = await generateFromHtml(fullHtml, {
130
+ ...options,
131
+ filename: options.filename || generatePdfFilename("markdown"),
132
+ });
133
+
134
+ fastify.log.info(`PDF generated from Markdown: ${result.filename}`);
135
+
136
+ return result;
137
+ } catch (error) {
138
+ fastify.log.error("Markdown to PDF generation failed:", error);
139
+ throw new Error(`Failed to generate PDF from Markdown: ${error.message}`);
140
+ }
141
+ };
142
+
143
+ return {
144
+ generateFromHtml,
145
+ generateFromMarkdown,
146
+ };
147
+ }
@@ -0,0 +1,115 @@
1
+ // src/services/merger.js
2
+ import { PDFDocument } from "pdf-lib";
3
+ import { generatePdfFilename, isValidPdfBuffer } from "../utils/helpers.js";
4
+
5
+ /**
6
+ * Save PDF to xStorage if configured
7
+ */
8
+ async function saveToStorage(fastify, buffer, filename, folder) {
9
+ if (!fastify.xStorage) {
10
+ return null;
11
+ }
12
+
13
+ try {
14
+ const result = await fastify.xStorage.upload(buffer, filename, {
15
+ folder,
16
+ contentType: "application/pdf",
17
+ });
18
+ return {
19
+ storageKey: result.key,
20
+ url: result.url,
21
+ };
22
+ } catch (error) {
23
+ fastify.log.error("Failed to save PDF to storage:", error);
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export function getMergerMethods(config) {
29
+ const { fastify, defaultFolder } = config;
30
+
31
+ return {
32
+ /**
33
+ * Merge multiple PDF files into one
34
+ * @param {Array<Buffer>} pdfBuffers - Array of PDF buffers to merge
35
+ * @param {object} options - Merge options
36
+ * @returns {Promise<{buffer: Buffer, filename: string, size: number, pageCount: number, storageKey?: string, url?: string}>}
37
+ */
38
+ mergePDFs: async (pdfBuffers, options = {}) => {
39
+ try {
40
+ if (!Array.isArray(pdfBuffers) || pdfBuffers.length === 0) {
41
+ throw new Error("pdfBuffers must be a non-empty array of PDF buffers");
42
+ }
43
+
44
+ // Validate all buffers
45
+ for (const buffer of pdfBuffers) {
46
+ if (!isValidPdfBuffer(buffer)) {
47
+ throw new Error("One or more invalid PDF buffers provided");
48
+ }
49
+ }
50
+
51
+ const {
52
+ filename = generatePdfFilename("merged"),
53
+ saveToStorage: save = false,
54
+ folder = defaultFolder,
55
+ } = options;
56
+
57
+ // Create new PDF document
58
+ const mergedDoc = await PDFDocument.create();
59
+
60
+ let totalPages = 0;
61
+
62
+ // Process each PDF
63
+ for (const buffer of pdfBuffers) {
64
+ try {
65
+ // Load source PDF
66
+ const srcDoc = await PDFDocument.load(buffer);
67
+
68
+ // Get page indices
69
+ const pageIndices = srcDoc.getPageIndices();
70
+ totalPages += pageIndices.length;
71
+
72
+ // Copy pages from source to merged
73
+ const copiedPages = await mergedDoc.copyPages(srcDoc, pageIndices);
74
+
75
+ // Add copied pages to merged document
76
+ for (const copiedPage of copiedPages) {
77
+ mergedDoc.addPage(copiedPage);
78
+ }
79
+ } catch (error) {
80
+ fastify.log.warn("Error processing PDF buffer during merge:", error.message);
81
+ throw new Error(`Failed to process PDF during merge: ${error.message}`);
82
+ }
83
+ }
84
+
85
+ // Save merged PDF
86
+ const pdfBytes = await mergedDoc.save();
87
+ const mergedBuffer = Buffer.from(pdfBytes);
88
+
89
+ // Save to storage if requested
90
+ let storageResult = null;
91
+ if (save && fastify.xStorage) {
92
+ storageResult = await saveToStorage(fastify, mergedBuffer, filename, folder);
93
+ }
94
+
95
+ fastify.log.info(
96
+ `PDFs merged: ${filename} (${totalPages} pages from ${pdfBuffers.length} files)`
97
+ );
98
+
99
+ return {
100
+ buffer: mergedBuffer,
101
+ filename,
102
+ size: mergedBuffer.length,
103
+ pageCount: totalPages,
104
+ ...(storageResult && {
105
+ storageKey: storageResult.storageKey,
106
+ url: storageResult.url,
107
+ }),
108
+ };
109
+ } catch (error) {
110
+ fastify.log.error("PDF merge failed:", error);
111
+ throw new Error(`Failed to merge PDFs: ${error.message}`);
112
+ }
113
+ },
114
+ };
115
+ }
@@ -0,0 +1,220 @@
1
+ // src/utils/helpers.js
2
+
3
+ /**
4
+ * Generate unique PDF filename with timestamp
5
+ * @param {string} baseName - Base filename (without extension)
6
+ * @returns {string} - Unique filename with timestamp
7
+ */
8
+ export function generatePdfFilename(baseName = "document") {
9
+ const timestamp = Date.now();
10
+ return `${baseName}-${timestamp}.pdf`;
11
+ }
12
+
13
+ /**
14
+ * Validate PDF buffer
15
+ * @param {Buffer} buffer - PDF buffer to validate
16
+ * @returns {boolean} - True if buffer looks like a valid PDF
17
+ */
18
+ export function isValidPdfBuffer(buffer) {
19
+ if (!Buffer.isBuffer(buffer)) {
20
+ return false;
21
+ }
22
+
23
+ // Check for PDF header: %PDF
24
+ const pdfHeader = buffer.toString("utf8", 0, 4);
25
+ return pdfHeader === "%PDF";
26
+ }
27
+
28
+ /**
29
+ * Get PDF metadata (page count and file size)
30
+ * @param {Buffer} buffer - PDF buffer
31
+ * @returns {object} - Metadata object {size: number}
32
+ */
33
+ export function getPdfMetadata(buffer) {
34
+ if (!isValidPdfBuffer(buffer)) {
35
+ throw new Error("Invalid PDF buffer");
36
+ }
37
+
38
+ return {
39
+ size: buffer.length,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Format PDF generation options with defaults
45
+ * @param {object} options - User-provided options
46
+ * @param {object} defaults - Default options
47
+ * @returns {object} - Merged options
48
+ */
49
+ export function formatPdfOptions(options = {}, defaults = {}) {
50
+ return {
51
+ ...defaults,
52
+ ...options,
53
+ // Ensure margin is properly merged
54
+ margin: {
55
+ ...defaults.margin,
56
+ ...options.margin,
57
+ },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Sanitize filename for safe storage
63
+ * @param {string} filename - Original filename
64
+ * @returns {string} - Sanitized filename
65
+ */
66
+ export function sanitizeFilename(filename) {
67
+ return filename
68
+ .replace(/[^a-zA-Z0-9._-]/g, "_") // Replace unsafe chars
69
+ .replace(/_{2,}/g, "_") // Replace multiple underscores
70
+ .toLowerCase();
71
+ }
72
+
73
+ /**
74
+ * Convert HTML template to PDF-ready HTML
75
+ * @param {string} content - HTML or content
76
+ * @returns {string} - Full HTML document
77
+ */
78
+ export function wrapHtmlTemplate(content) {
79
+ return `<!DOCTYPE html>
80
+ <html>
81
+ <head>
82
+ <meta charset="utf-8">
83
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
84
+ <style>
85
+ * {
86
+ margin: 0;
87
+ padding: 0;
88
+ box-sizing: border-box;
89
+ }
90
+ body {
91
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
92
+ line-height: 1.6;
93
+ color: #333;
94
+ background: white;
95
+ padding: 0;
96
+ margin: 0;
97
+ }
98
+ h1, h2, h3, h4, h5, h6 {
99
+ margin-top: 1em;
100
+ margin-bottom: 0.5em;
101
+ font-weight: 600;
102
+ }
103
+ h1 { font-size: 2em; }
104
+ h2 { font-size: 1.5em; }
105
+ h3 { font-size: 1.25em; }
106
+ p {
107
+ margin-bottom: 1em;
108
+ }
109
+ ul, ol {
110
+ margin-left: 2em;
111
+ margin-bottom: 1em;
112
+ }
113
+ li {
114
+ margin-bottom: 0.5em;
115
+ }
116
+ code {
117
+ background: #f4f4f4;
118
+ padding: 2px 6px;
119
+ border-radius: 3px;
120
+ font-family: 'Courier New', monospace;
121
+ font-size: 0.9em;
122
+ }
123
+ pre {
124
+ background: #f4f4f4;
125
+ padding: 1em;
126
+ border-radius: 5px;
127
+ overflow-x: auto;
128
+ margin-bottom: 1em;
129
+ line-height: 1.4;
130
+ }
131
+ pre code {
132
+ background: none;
133
+ padding: 0;
134
+ border-radius: 0;
135
+ }
136
+ a {
137
+ color: #0066cc;
138
+ text-decoration: none;
139
+ }
140
+ a:hover {
141
+ text-decoration: underline;
142
+ }
143
+ blockquote {
144
+ border-left: 4px solid #ddd;
145
+ padding-left: 1em;
146
+ margin-left: 0;
147
+ margin-bottom: 1em;
148
+ color: #666;
149
+ }
150
+ table {
151
+ border-collapse: collapse;
152
+ width: 100%;
153
+ margin-bottom: 1em;
154
+ }
155
+ th, td {
156
+ border: 1px solid #ddd;
157
+ padding: 8px;
158
+ text-align: left;
159
+ }
160
+ th {
161
+ background: #f4f4f4;
162
+ font-weight: 600;
163
+ }
164
+ img {
165
+ max-width: 100%;
166
+ height: auto;
167
+ }
168
+ </style>
169
+ </head>
170
+ <body>
171
+ ${content}
172
+ </body>
173
+ </html>`;
174
+ }
175
+
176
+ /**
177
+ * Parse margin string to Puppeteer format
178
+ * @param {object|string} margin - Margin value
179
+ * @returns {object} - Puppeteer margin format
180
+ */
181
+ export function parseMargin(margin) {
182
+ if (typeof margin === "object") {
183
+ return margin;
184
+ }
185
+
186
+ // If string, apply to all sides
187
+ if (typeof margin === "string") {
188
+ return {
189
+ top: margin,
190
+ right: margin,
191
+ bottom: margin,
192
+ left: margin,
193
+ };
194
+ }
195
+
196
+ return {};
197
+ }
198
+
199
+ /**
200
+ * Get page format dimensions
201
+ * @param {string} format - Format name (A4, Letter, etc.)
202
+ * @returns {object} - Width and height
203
+ */
204
+ export function getPageFormat(format = "A4") {
205
+ const formats = {
206
+ Letter: { width: 8.5, height: 11 },
207
+ Legal: { width: 8.5, height: 14 },
208
+ Tabloid: { width: 11, height: 17 },
209
+ Ledger: { width: 17, height: 11 },
210
+ A0: { width: 33.1, height: 46.8 },
211
+ A1: { width: 23.4, height: 33.1 },
212
+ A2: { width: 16.54, height: 23.4 },
213
+ A3: { width: 11.7, height: 16.54 },
214
+ A4: { width: 8.27, height: 11.7 },
215
+ A5: { width: 5.83, height: 8.27 },
216
+ A6: { width: 4.13, height: 5.83 },
217
+ };
218
+
219
+ return formats[format] || formats.A4;
220
+ }