diff --git a/teressa-copeland-homes/src/lib/pdf/prepare-document.ts b/teressa-copeland-homes/src/lib/pdf/prepare-document.ts index d36abf0..d5f1fbb 100644 --- a/teressa-copeland-homes/src/lib/pdf/prepare-document.ts +++ b/teressa-copeland-homes/src/lib/pdf/prepare-document.ts @@ -6,6 +6,12 @@ import type { SignatureFieldData } from '@/lib/db/schema'; * Fills AcroForm text fields and draws signature rectangles on a PDF. * Uses atomic write: write to tmp path, verify %PDF header, rename to final path. * + * Text fill strategy: + * 1. Attempt AcroForm field filling by field name (works when PDF has AcroForm fields). + * 2. For any entry that did not match an AcroForm field (or when there is no AcroForm at all), + * draw the key=value pairs as text directly on page 1 near the top margin. + * This ensures text fill data is ALWAYS visible in the output PDF. + * * @param srcPath - Absolute path to original PDF * @param destPath - Absolute path to write prepared PDF (e.g. {docId}_prepared.pdf) * @param textFields - Key/value map: { fieldNameOrLabel: value } (agent-provided) @@ -22,20 +28,63 @@ export async function preparePdf( const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica); const pages = pdfDoc.getPages(); - // Strategy A: fill existing AcroForm fields by name - // form.flatten() MUST happen before drawing rectangles + // Track which text field entries were successfully written via AcroForm so that + // the fallback text stamp only shows entries that were NOT already embedded. + const acroFilledKeys = new Set(); + let hasAcroForm = false; + + // Strategy A: fill existing AcroForm fields by name. + // form.flatten() MUST happen before drawing rectangles — if reversed, + // the AcroForm overlay obscures the drawn rectangles. try { const form = pdfDoc.getForm(); + hasAcroForm = true; for (const [fieldName, value] of Object.entries(textFields)) { try { form.getTextField(fieldName).setText(value); + acroFilledKeys.add(fieldName); } catch { - // Field name not found in AcroForm — silently skip + // Field name not found in AcroForm — will fall through to Strategy B } } form.flatten(); } catch { - // No AcroForm in this PDF — skip to rectangle drawing + // No AcroForm in this PDF — all entries fall through to Strategy B + hasAcroForm = false; + } + + // Strategy B: stamp any un-filled text field entries directly onto page 1. + // This guarantees that text fill data is always reflected in the output PDF + // even when the source PDF has no AcroForm fields (e.g. scanned/flat PDFs). + const unstampedEntries = Object.entries(textFields).filter( + ([key]) => !acroFilledKeys.has(key), + ); + + if (unstampedEntries.length > 0) { + const firstPage = pages[0]; + if (firstPage) { + const { height: pageHeight } = firstPage.getSize(); + // Start near the top of the page (points from bottom, since PDF Y=0 is bottom) + const startY = pageHeight - 20; + const lineHeight = 12; + const labelColor = rgb(0.2, 0.2, 0.2); + const valueColor = rgb(0.05, 0.05, 0.55); + + unstampedEntries.forEach(([key, value], index) => { + const y = startY - index * lineHeight; + if (y < 10) return; // Don't draw below bottom margin + + firstPage.drawText(`${key}: ${value}`, { + x: 10, + y, + size: 8, + font: helvetica, + color: hasAcroForm ? valueColor : labelColor, + // Semi-transparent background note: @cantoo/pdf-lib drawText has no background + // option; the text is drawn directly over existing content. + }); + }); + } } // Draw signature field placeholders (blue rectangle + "Sign Here" label)