From df02a1e3f7377600b756db1f3d8ff33faa8211bf Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Sat, 21 Mar 2026 16:20:25 -0600 Subject: [PATCH] feat(12.1-01): replace positional text fill with field-ID-keyed lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove AcroForm Strategy A (getForm/flatten) — no longer needed - Remove positional sorting loop (textFields_sorted, remainingEntries, fieldConsumedKeys) - Remove Strategy B top-of-page UUID stamp (unstampedEntries) - Add Phase 12.1 field-ID-keyed loop: textFields[field.id] direct lookup - Update JSDoc to document new keying strategy --- .../src/lib/pdf/prepare-document.ts | 112 ++---------------- 1 file changed, 12 insertions(+), 100 deletions(-) diff --git a/teressa-copeland-homes/src/lib/pdf/prepare-document.ts b/teressa-copeland-homes/src/lib/pdf/prepare-document.ts index 07de73d..30eedc3 100644 --- a/teressa-copeland-homes/src/lib/pdf/prepare-document.ts +++ b/teressa-copeland-homes/src/lib/pdf/prepare-document.ts @@ -4,18 +4,16 @@ import type { SignatureFieldData } from '@/lib/db/schema'; import { getFieldType } from '@/lib/db/schema'; /** - * Fills AcroForm text fields and draws signature rectangles on a PDF. + * Draws signature/field placeholders and text values 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. + * Text fill: textFields is keyed by SignatureFieldData.id (UUID). Each text-type + * placed field box has its value looked up directly by field ID and drawn at + * the field's coordinates. * * @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) + * @param textFields - Key/value map: { [fieldId (UUID)]: value } (agent-provided) * @param sigFields - Signature field array from SignatureFieldData[] */ export async function preparePdf( @@ -43,62 +41,13 @@ export async function preparePdf( agentInitialsImage = await pdfDoc.embedPng(agentInitialsData); } - // 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 — will fall through to Strategy B - } - } - form.flatten(); - } catch { - // No AcroForm in this PDF — all entries fall through to Strategy B - hasAcroForm = false; - } - - // Bug 3 fix: assign textFillData values to placed `text` type field boxes in reading - // order (page asc, y desc within page = top-to-bottom in PDF user space). Each text - // field box consumes one textFillData entry so the value is rendered AT the correct - // position on the document rather than only via Strategy B at the top of page 1. - // - // Build an ordered list of textFillData entries not already handled by AcroForm (Strategy A). - const remainingEntries: Array<[string, string]> = Object.entries(textFields).filter( - ([key]) => !acroFilledKeys.has(key), - ); - - // Sort placed `text` fields by page asc, then y desc (topmost field first in reading order). - const textFields_sorted = sigFields - .filter((f) => getFieldType(f) === 'text') - .sort((a, b) => a.page !== b.page ? a.page - b.page : b.y - a.y); - - // Track which textFillData keys are consumed by field-box rendering so Strategy B - // only stamps any leftover entries (keys that had no corresponding placed text field). - const fieldConsumedKeys = new Set(); - - textFields_sorted.forEach((field, idx) => { - const entry = remainingEntries[idx]; - if (!entry) return; // More text fields than textFillData entries — nothing to draw - - const [key, value] = entry; - if (!value) return; // Skip blank values — leave the box visually empty - + // Phase 12.1: Draw text values at placed text field boxes — keyed by field ID (UUID) + for (const field of sigFields) { + if (getFieldType(field) !== 'text') continue; + const value = textFields[field.id]; + if (!value) continue; const page = pages[field.page - 1]; - if (!page) return; - - // Draw value text at the field's bottom-left corner with 4pt padding. - // Font size is capped to fit within the field height (min 6pt, max 11pt). + if (!page) continue; const fontSize = Math.max(6, Math.min(11, field.height - 4)); page.drawText(value, { x: field.x + 4, @@ -107,43 +56,6 @@ export async function preparePdf( font: helvetica, color: rgb(0.05, 0.05, 0.05), }); - - fieldConsumedKeys.add(key); - }); - - // 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). - // Only entries NOT consumed by field-box rendering are stamped here. - const unstampedEntries = remainingEntries.filter( - ([key]) => !fieldConsumedKeys.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 - 60; // 60pt (~0.83 inch) from top - const lineHeight = 14; - 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: 10, - 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 field placeholders — rendering varies by field type @@ -191,7 +103,7 @@ export async function preparePdf( // No placeholder drawn — actual signing date stamped at POST time in route.ts } else if (fieldType === 'text') { - // Text value drawn at field coordinates above (Bug 3 fix — textFields_sorted loop). + // Text value drawn in field-ID loop above (Phase 12.1). // No additional marker drawn here. } else if (fieldType === 'agent-signature') {