@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,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
|
+
}
|