Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions pdf-manager/lib/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,72 @@ export async function convertDropdownsToTextFields(
if (changed) writeFileSync(pdfPath, await doc.save());
}

/**
* Renames each radio button's appearance states from opaque numeric keys
* (e.g. "0", "1", "2") to the human-readable labels stored in the field's
* Opt array (e.g. "Male", "Female", "X"). After this runs, fillPdf can pass
* the label string directly and form.fill() will accept it without translation.
*/
export async function normalizeRadioOptionNames(
pdfPath: string,
): Promise<void> {
const bytes = readFileSync(pdfPath);
const doc = await PDF.load(bytes);
const form = doc.getForm();
let changed = false;

for (const field of form?.getFields() ?? []) {
if (field.type !== "radio") continue;

const acro = field.acroField();
// Opt: ordered human-readable export values, one per radio button
const opt = acro.getArray("Opt");
if (!opt?.length) continue;

const labels = opt
.toArray()
.map((item) => (item instanceof PdfString ? item.asString() : null));

// Each widget is one radio button; its on-value is its selected state name
for (const widget of field.getWidgets()) {
const onValue = widget.getOnValue();
if (!onValue) continue;

const idx = Number(onValue);
if (!Number.isFinite(idx) || idx >= labels.length) continue;
const label = labels[idx];
if (!label || label === onValue) continue;

// Get appearance streams dictionary
const ap = widget.dict.getDict("AP");
if (ap) {
// Rename the state key in normal (N) and down/pressed (D) appearances
for (const subKey of ["N", "D"]) {
const sub = ap.getDict(subKey);
if (!sub?.has(onValue)) continue;
const val = sub.get(onValue);
if (val) {
sub.set(label, val);
sub.delete(onValue);
}
}
}

// Update current appearance state if this button is selected
const as = widget.dict.get("AS");
if (as instanceof PdfName && as.value === onValue) {
widget.dict.set("AS", PdfName.of(label));
}
}

// Opt is now redundant — state names are the labels
acro.delete("Opt");
changed = true;
}

if (changed) writeFileSync(pdfPath, await doc.save());
}

export interface Rename {
from: string;
to: string;
Expand Down
2 changes: 2 additions & 0 deletions pdf-manager/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PDFS_DIR } from "./catalog";
import {
convertDropdownsToTextFields,
extractFields,
normalizeRadioOptionNames,
type PdfFieldInfo,
} from "./pdf";
import { escapeKey } from "./utils";
Expand Down Expand Up @@ -122,6 +123,7 @@ export async function processPdf(
for (const n of exclude) excluded.add(n);

await convertDropdownsToTextFields(pdfPath);
await normalizeRadioOptionNames(pdfPath);
const allFields = await extractFields(pdfPath);

// Always exclude non-fillable field types (signature, listbox, etc.)
Expand Down
38 changes: 38 additions & 0 deletions web/src/constants/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,44 @@ export const FIELD_DEFS = [
label: "Substitution details (Section 3)",
type: "string",
},
{
name: "oldGender",
label: "Existing gender marker",
type: "string",
},
{
name: "newGender",
label: "Desired gender marker",
type: "string",
},
{
// Specific to R-116 in MA
name: "nameChangeDecreeIncluded",
label:
"Applicant will include court-certified copy of legal name-change decree",
type: "boolean",
},
{
// Specific to R-116 in MA
name: "paymentIncluded",
label: "Applicant will include all fees with the application",
type: "boolean",
},
{
name: "guardianOneFullName",
label: "Guardian/parent 1's full name",
type: "string",
},
{
name: "guardianTwoFullName",
label: "Guardian/parent 2's full name",
type: "string",
},
{
name: "waiveDocumentFees",
label: "Waive fees?",
type: "boolean",
},
] as const;

export type FieldName = (typeof FIELD_DEFS)[number]["name"];
Expand Down
1 change: 1 addition & 0 deletions web/src/constants/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const FORM_SLUGS = [
"court-order-ma-minor",
"court-order-ri",
"social-security",
"birth-certificate-affidavit-ma",
] as const;

export type FormSlug = (typeof FORM_SLUGS)[number];
Expand Down
1 change: 1 addition & 0 deletions web/src/constants/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const PDF_IDS = [
"ss5-application-for-social-security-card",
"pc8.1-change-of-name",
"background-check-authorization-of-release",
"r116-applicant-affidavit-in-support-of-amendment-of-a-birth-certificate-for-sex",
] as const;

export type PDFId = (typeof PDF_IDS)[number];
Expand Down
Loading
Loading