From df81d0342f6d553a49ca9de53c013357f015d87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Martin?= Date: Wed, 4 Mar 2026 20:38:02 +0100 Subject: [PATCH] fix(printer): make PDF margins match resume background color (#2770) * fix(printer): match PDF margins to resume background color * fix(printer): preserve vertical margins when content wraps pages --- src/integrations/orpc/services/printer.ts | 87 ++++++++++++++++++----- 1 file changed, 68 insertions(+), 19 deletions(-) diff --git a/src/integrations/orpc/services/printer.ts b/src/integrations/orpc/services/printer.ts index 0818f07f4..df72d3663 100644 --- a/src/integrations/orpc/services/printer.ts +++ b/src/integrations/orpc/services/printer.ts @@ -99,15 +99,15 @@ export const printerService = { const token = generatePrinterToken(id); const url = `${baseUrl}/printer/${id}?token=${token}`; - // Step 3: Calculate PDF margins - // Some templates require margins to be applied via PDF (they use print:p-0 to remove CSS padding) - // Convert from CSS pixels to PDF points (divide by 0.75 since 1pt = 0.75px at 72dpi) - let marginX = 0; - let marginY = 0; + // Step 3: Calculate print paddings for templates that disable CSS padding in print mode. + // We render these margins inside the page (not via Puppeteer's PDF margins), so the margin + // area matches the resume background color instead of staying white. + let pagePaddingX = 0; + let pagePaddingY = 0; if (printMarginTemplates.includes(template)) { - marginX = Math.round(data.metadata.page.marginX / 0.75); - marginY = Math.round(data.metadata.page.marginY / 0.75); + pagePaddingX = data.metadata.page.marginX; + pagePaddingY = data.metadata.page.marginY; } let browser: Browser | null = null; @@ -135,21 +135,65 @@ export const printerService = { const isFreeForm = format === "free-form"; const contentHeight = await page.evaluate( - (marginY: number, isFreeForm: boolean, minPageHeight: number) => { + ( + pagePaddingX: number, + pagePaddingY: number, + isFreeForm: boolean, + minPageHeight: number, + backgroundColor: string, + ) => { const root = document.documentElement; + const body = document.body; const pageElements = document.querySelectorAll("[data-page-index]"); + const pageContentElements = document.querySelectorAll(".page-content"); const container = document.querySelector(".resume-preview-container") as HTMLElement | null; + // Ensure PDF margins inherit the resume background color instead of defaulting to white. + root.style.backgroundColor = backgroundColor; + body.style.backgroundColor = backgroundColor; + root.style.margin = "0"; + body.style.margin = "0"; + root.style.padding = "0"; + body.style.padding = "0"; + + for (const el of pageElements) { + const pageWrapper = el as HTMLElement; + const pageSurface = pageWrapper.querySelector(".page") as HTMLElement | null; + + pageWrapper.style.backgroundColor = backgroundColor; + pageWrapper.style.breakInside = "auto"; + + if (pageSurface) pageSurface.style.backgroundColor = backgroundColor; + } + + // Apply print-only margins as padding inside each page's content surface. + if (pagePaddingX > 0 || pagePaddingY > 0) { + for (const el of pageContentElements) { + const pageContent = el as HTMLElement; + + pageContent.style.boxSizing = "border-box"; + // Ensure padding is repeated on every printed fragment when content + // flows across physical PDF pages (not just the first fragment). + pageContent.style.boxDecorationBreak = "clone"; + pageContent.style.setProperty("-webkit-box-decoration-break", "clone"); + if (pagePaddingX > 0) { + pageContent.style.paddingLeft = `${pagePaddingX}pt`; + pageContent.style.paddingRight = `${pagePaddingX}pt`; + } + if (pagePaddingY > 0) { + pageContent.style.paddingTop = `${pagePaddingY}pt`; + pageContent.style.paddingBottom = `${pagePaddingY}pt`; + } + } + } + if (isFreeForm) { - // For free-form: add visual gaps between pages, then measure total height - // Convert marginY from PDF points to CSS pixels (1pt = 0.75px) - const marginYAsPixels = marginY * 0.75; const numberOfPages = pageElements.length; // Add margin between pages (except the last one) for (let i = 0; i < numberOfPages - 1; i++) { const pageEl = pageElements[i] as HTMLElement; - pageEl.style.marginBottom = `${marginYAsPixels}px`; + if (pagePaddingY > 0) pageEl.style.marginBottom = `${pagePaddingY}pt`; } // Now measure the total height (margins are now part of the DOM) @@ -169,8 +213,8 @@ export const printerService = { // For A4/Letter const heightValue = minPageHeight; - // Subtract top + bottom margins from page height - const newHeight = `${heightValue - marginY}px`; + // Keep page height fixed and let in-page padding (if any) define content bounds. + const newHeight = `${heightValue}px`; if (container) container.style.setProperty("--page-height", newHeight); root.style.setProperty("--page-height", newHeight); @@ -181,7 +225,10 @@ export const printerService = { const index = Number.parseInt(element.getAttribute("data-page-index") ?? "0", 10); // Force a page break before each page except the first - if (index > 0) element.style.breakBefore = "page"; + if (index > 0) { + element.style.breakBefore = "page"; + element.style.pageBreakBefore = "always"; + } // Allow content within a page to break naturally if it overflows // (e.g., if a single page has more content than fits on one PDF page) @@ -190,9 +237,11 @@ export const printerService = { return null; // Fixed height from pageDimensionsAsPixels for A4/Letter }, - marginY, + pagePaddingX, + pagePaddingY, isFreeForm, pageDimensionsAsPixels[format].height, + data.metadata.design.colors.background, ); // Step 6: Generate the PDF with the specified dimensions and margins @@ -208,9 +257,9 @@ export const printerService = { printBackground: true, // Includes background colors and images margin: { bottom: 0, - top: marginY, - right: marginX, - left: marginX, + top: 0, + right: 0, + left: 0, }, });