From bce2a980d243dce8f336a7187052c9866d274179 Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Sat, 21 Mar 2026 15:50:30 -0600 Subject: [PATCH] fix(12-02): draw text fill values at placed text field box coordinates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bug 3: text type fields previously drew nothing at field coordinates; textFillData values were only stamped at top of page 1 via Strategy B, making them invisible to the agent inspecting the placed boxes - Sort placed text fields by page asc / y desc (reading order) and assign textFillData entries sequentially to each field box position - Text drawn at field.x+4, field.y+4 with font size capped 6–11pt to fit within the field height; dark near-black color for legibility - fieldConsumedKeys set tracks which entries were rendered at field coords; Strategy B only stamps remaining entries not consumed by a field box - TypeScript compiles clean; zero errors --- .../src/lib/pdf/prepare-document.ts | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/teressa-copeland-homes/src/lib/pdf/prepare-document.ts b/teressa-copeland-homes/src/lib/pdf/prepare-document.ts index bb7c72d..07de73d 100644 --- a/teressa-copeland-homes/src/lib/pdf/prepare-document.ts +++ b/teressa-copeland-homes/src/lib/pdf/prepare-document.ts @@ -68,11 +68,55 @@ export async function preparePdf( 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 + + 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). + const fontSize = Math.max(6, Math.min(11, field.height - 4)); + page.drawText(value, { + x: field.x + 4, + y: field.y + 4, + size: fontSize, + 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). - const unstampedEntries = Object.entries(textFields).filter( - ([key]) => !acroFilledKeys.has(key), + // Only entries NOT consumed by field-box rendering are stamped here. + const unstampedEntries = remainingEntries.filter( + ([key]) => !fieldConsumedKeys.has(key), ); if (unstampedEntries.length > 0) { @@ -147,7 +191,8 @@ export async function preparePdf( // No placeholder drawn — actual signing date stamped at POST time in route.ts } else if (fieldType === 'text') { - // No marker drawn — text content is provided via textFillData (separate pipeline) + // Text value drawn at field coordinates above (Bug 3 fix — textFields_sorted loop). + // No additional marker drawn here. } else if (fieldType === 'agent-signature') { if (agentSigImage) {