fix(12-02): draw text fill values at placed text field box coordinates

- 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
This commit is contained in:
Chandler Copeland
2026-03-21 15:50:30 -06:00
parent 43f396b4c5
commit bce2a980d2

View File

@@ -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<string>();
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) {