diff --git a/package-lock.json b/package-lock.json index 6914021..801d984 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "docfast-api", - "version": "0.4.3", + "version": "0.4.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "docfast-api", - "version": "0.4.3", + "version": "0.4.5", "dependencies": { "compression": "^1.8.1", "express": "^4.21.0", diff --git a/package.json b/package.json index 04fc4ef..e33a90a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docfast-api", - "version": "0.4.4", + "version": "0.4.5", "description": "Markdown/HTML to PDF API with built-in invoice templates", "main": "dist/index.js", "scripts": { diff --git a/src/routes/convert.ts b/src/routes/convert.ts index de7b984..18752d6 100644 --- a/src/routes/convert.ts +++ b/src/routes/convert.ts @@ -47,6 +47,14 @@ interface ConvertBody { margin?: { top?: string; right?: string; bottom?: string; left?: string }; printBackground?: boolean; filename?: string; + headerTemplate?: string; + footerTemplate?: string; + displayHeaderFooter?: boolean; + scale?: number; + pageRanges?: string; + preferCSSPageSize?: boolean; + width?: string; + height?: string; } /** @@ -131,6 +139,14 @@ convertRouter.post("/html", async (req: Request & { acquirePdfSlot?: () => Promi landscape: body.landscape, margin: body.margin, printBackground: body.printBackground, + headerTemplate: body.headerTemplate, + footerTemplate: body.footerTemplate, + displayHeaderFooter: body.displayHeaderFooter, + scale: body.scale, + pageRanges: body.pageRanges, + preferCSSPageSize: body.preferCSSPageSize, + width: body.width, + height: body.height, }); const filename = sanitizeFilename(body.filename || "document.pdf"); @@ -228,6 +244,14 @@ convertRouter.post("/markdown", async (req: Request & { acquirePdfSlot?: () => P landscape: body.landscape, margin: body.margin, printBackground: body.printBackground, + headerTemplate: body.headerTemplate, + footerTemplate: body.footerTemplate, + displayHeaderFooter: body.displayHeaderFooter, + scale: body.scale, + pageRanges: body.pageRanges, + preferCSSPageSize: body.preferCSSPageSize, + width: body.width, + height: body.height, }); const filename = sanitizeFilename(body.filename || "document.pdf"); @@ -310,7 +334,7 @@ convertRouter.post("/url", async (req: Request & { acquirePdfSlot?: () => Promis res.status(415).json({ error: "Unsupported Content-Type. Use application/json." }); return; } - const body = req.body as { url?: string; format?: string; landscape?: boolean; margin?: any; printBackground?: boolean; waitUntil?: string; filename?: string }; + const body = req.body as { url?: string; format?: string; landscape?: boolean; margin?: any; printBackground?: boolean; waitUntil?: string; filename?: string; headerTemplate?: string; footerTemplate?: string; displayHeaderFooter?: boolean; scale?: number; pageRanges?: string; preferCSSPageSize?: boolean; width?: string; height?: string }; if (!body.url) { res.status(400).json({ error: "Missing 'url' field" }); @@ -355,6 +379,14 @@ convertRouter.post("/url", async (req: Request & { acquirePdfSlot?: () => Promis landscape: body.landscape, margin: body.margin, printBackground: body.printBackground, + headerTemplate: body.headerTemplate, + footerTemplate: body.footerTemplate, + displayHeaderFooter: body.displayHeaderFooter, + scale: body.scale, + pageRanges: body.pageRanges, + preferCSSPageSize: body.preferCSSPageSize, + width: body.width, + height: body.height, waitUntil: body.waitUntil, hostResolverRules: `MAP ${parsed.hostname} ${resolvedAddress}`, }); diff --git a/src/services/browser.ts b/src/services/browser.ts index 9ff97a7..3b79ff1 100644 --- a/src/services/browser.ts +++ b/src/services/browser.ts @@ -218,17 +218,24 @@ export async function closeBrowser(): Promise { instances.length = 0; } +export interface PdfRenderOptions { + format?: string; + landscape?: boolean; + margin?: { top?: string; right?: string; bottom?: string; left?: string }; + printBackground?: boolean; + headerTemplate?: string; + footerTemplate?: string; + displayHeaderFooter?: boolean; + scale?: number; + pageRanges?: string; + preferCSSPageSize?: boolean; + width?: string; + height?: string; +} + export async function renderPdf( html: string, - options: { - format?: string; - landscape?: boolean; - margin?: { top?: string; right?: string; bottom?: string; left?: string }; - printBackground?: boolean; - headerTemplate?: string; - footerTemplate?: string; - displayHeaderFooter?: boolean; - } = {} + options: PdfRenderOptions = {} ): Promise { const { page, instance } = await acquirePage(); try { @@ -245,6 +252,11 @@ export async function renderPdf( headerTemplate: options.headerTemplate, footerTemplate: options.footerTemplate, displayHeaderFooter: options.displayHeaderFooter || false, + ...(options.scale !== undefined && { scale: options.scale }), + ...(options.pageRanges && { pageRanges: options.pageRanges }), + ...(options.preferCSSPageSize !== undefined && { preferCSSPageSize: options.preferCSSPageSize }), + ...(options.width && { width: options.width }), + ...(options.height && { height: options.height }), }); return Buffer.from(pdf); })(), @@ -260,11 +272,7 @@ export async function renderPdf( export async function renderUrlPdf( url: string, - options: { - format?: string; - landscape?: boolean; - margin?: { top?: string; right?: string; bottom?: string; left?: string }; - printBackground?: boolean; + options: PdfRenderOptions & { waitUntil?: string; hostResolverRules?: string; } = {} @@ -316,6 +324,14 @@ export async function renderUrlPdf( landscape: options.landscape || false, printBackground: options.printBackground !== false, margin: options.margin || { top: "0", right: "0", bottom: "0", left: "0" }, + ...(options.headerTemplate && { headerTemplate: options.headerTemplate }), + ...(options.footerTemplate && { footerTemplate: options.footerTemplate }), + ...(options.displayHeaderFooter !== undefined && { displayHeaderFooter: options.displayHeaderFooter }), + ...(options.scale !== undefined && { scale: options.scale }), + ...(options.pageRanges && { pageRanges: options.pageRanges }), + ...(options.preferCSSPageSize !== undefined && { preferCSSPageSize: options.preferCSSPageSize }), + ...(options.width && { width: options.width }), + ...(options.height && { height: options.height }), }); return Buffer.from(pdf); })(), diff --git a/src/swagger.ts b/src/swagger.ts index fdf31d6..60994f5 100644 --- a/src/swagger.ts +++ b/src/swagger.ts @@ -49,7 +49,7 @@ const options: swaggerJsdoc.Options = { type: "string", enum: ["A4", "Letter", "Legal", "A3", "A5", "Tabloid"], default: "A4", - description: "Page size", + description: "Page size. Ignored if width/height are set.", }, landscape: { type: "boolean", @@ -58,6 +58,7 @@ const options: swaggerJsdoc.Options = { }, margin: { type: "object", + description: "Page margins. Accepts CSS units (e.g. '20mm', '1in', '72px').", properties: { top: { type: "string", example: "20mm" }, bottom: { type: "string", example: "20mm" }, @@ -68,12 +69,53 @@ const options: swaggerJsdoc.Options = { printBackground: { type: "boolean", default: true, - description: "Print background graphics", + description: "Print background graphics and colors", }, filename: { type: "string", default: "document.pdf", - description: "Suggested filename for the PDF", + description: "Suggested filename for the PDF download", + }, + headerTemplate: { + type: "string", + description: "HTML template for the page header. Requires displayHeaderFooter: true. Use these CSS classes for dynamic values: date, title, url, pageNumber, totalPages. Example: ' / '", + }, + footerTemplate: { + type: "string", + description: "HTML template for the page footer. Requires displayHeaderFooter: true. Supports the same CSS classes as headerTemplate.", + }, + displayHeaderFooter: { + type: "boolean", + default: false, + description: "Whether to show header and footer templates. Must be true for headerTemplate/footerTemplate to render.", + }, + scale: { + type: "number", + minimum: 0.1, + maximum: 2, + default: 1, + description: "Scale of the webpage rendering. 1 = 100%, 0.5 = 50%, 2 = 200%.", + example: 1, + }, + pageRanges: { + type: "string", + description: "Paper ranges to print, e.g. '1-5', '1,3,5', '2-4,6'. Empty string means all pages.", + example: "1-3", + }, + preferCSSPageSize: { + type: "boolean", + default: false, + description: "Give any CSS @page size declared in the page priority over the format option.", + }, + width: { + type: "string", + description: "Paper width with units. Overrides format. Accepts CSS units (e.g. '10in', '210mm', '8.5in').", + example: "8.5in", + }, + height: { + type: "string", + description: "Paper height with units. Overrides format. Accepts CSS units (e.g. '11in', '297mm').", + example: "11in", }, }, },