Skip to content

Commit ba1304c

Browse files
deammerkydecker
andauthored
Add the birth certificate amendment form for MA (#656)
Co-authored-by: Ky Decker <itsevadecker@gmail.com>
1 parent b1ed56b commit ba1304c

28 files changed

Lines changed: 873 additions & 22 deletions

File tree

pdf-manager/lib/pdf.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,72 @@ export async function convertDropdownsToTextFields(
9595
if (changed) writeFileSync(pdfPath, await doc.save());
9696
}
9797

98+
/**
99+
* Renames each radio button's appearance states from opaque numeric keys
100+
* (e.g. "0", "1", "2") to the human-readable labels stored in the field's
101+
* Opt array (e.g. "Male", "Female", "X"). After this runs, fillPdf can pass
102+
* the label string directly and form.fill() will accept it without translation.
103+
*/
104+
export async function normalizeRadioOptionNames(
105+
pdfPath: string,
106+
): Promise<void> {
107+
const bytes = readFileSync(pdfPath);
108+
const doc = await PDF.load(bytes);
109+
const form = doc.getForm();
110+
let changed = false;
111+
112+
for (const field of form?.getFields() ?? []) {
113+
if (field.type !== "radio") continue;
114+
115+
const acro = field.acroField();
116+
// Opt: ordered human-readable export values, one per radio button
117+
const opt = acro.getArray("Opt");
118+
if (!opt?.length) continue;
119+
120+
const labels = opt
121+
.toArray()
122+
.map((item) => (item instanceof PdfString ? item.asString() : null));
123+
124+
// Each widget is one radio button; its on-value is its selected state name
125+
for (const widget of field.getWidgets()) {
126+
const onValue = widget.getOnValue();
127+
if (!onValue) continue;
128+
129+
const idx = Number(onValue);
130+
if (!Number.isFinite(idx) || idx >= labels.length) continue;
131+
const label = labels[idx];
132+
if (!label || label === onValue) continue;
133+
134+
// Get appearance streams dictionary
135+
const ap = widget.dict.getDict("AP");
136+
if (ap) {
137+
// Rename the state key in normal (N) and down/pressed (D) appearances
138+
for (const subKey of ["N", "D"]) {
139+
const sub = ap.getDict(subKey);
140+
if (!sub?.has(onValue)) continue;
141+
const val = sub.get(onValue);
142+
if (val) {
143+
sub.set(label, val);
144+
sub.delete(onValue);
145+
}
146+
}
147+
}
148+
149+
// Update current appearance state if this button is selected
150+
const as = widget.dict.get("AS");
151+
if (as instanceof PdfName && as.value === onValue) {
152+
widget.dict.set("AS", PdfName.of(label));
153+
}
154+
}
155+
156+
// Opt is now redundant — state names are the labels
157+
acro.delete("Opt");
158+
changed = true;
159+
}
160+
161+
if (changed) writeFileSync(pdfPath, await doc.save());
162+
}
163+
98164
export interface Rename {
99165
from: string;
100166
to: string;

pdf-manager/lib/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PDFS_DIR } from "./catalog";
44
import {
55
convertDropdownsToTextFields,
66
extractFields,
7+
normalizeRadioOptionNames,
78
type PdfFieldInfo,
89
} from "./pdf";
910
import { escapeKey } from "./utils";
@@ -122,6 +123,7 @@ export async function processPdf(
122123
for (const n of exclude) excluded.add(n);
123124

124125
await convertDropdownsToTextFields(pdfPath);
126+
await normalizeRadioOptionNames(pdfPath);
125127
const allFields = await extractFields(pdfPath);
126128

127129
// Always exclude non-fillable field types (signature, listbox, etc.)

web/src/constants/fields.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,44 @@ export const FIELD_DEFS = [
652652
label: "Substitution details (Section 3)",
653653
type: "string",
654654
},
655+
{
656+
name: "oldGender",
657+
label: "Existing gender marker",
658+
type: "string",
659+
},
660+
{
661+
name: "newGender",
662+
label: "Desired gender marker",
663+
type: "string",
664+
},
665+
{
666+
// Specific to R-116 in MA
667+
name: "nameChangeDecreeIncluded",
668+
label:
669+
"Applicant will include court-certified copy of legal name-change decree",
670+
type: "boolean",
671+
},
672+
{
673+
// Specific to R-116 in MA
674+
name: "paymentIncluded",
675+
label: "Applicant will include all fees with the application",
676+
type: "boolean",
677+
},
678+
{
679+
name: "guardianOneFullName",
680+
label: "Guardian/parent 1's full name",
681+
type: "string",
682+
},
683+
{
684+
name: "guardianTwoFullName",
685+
label: "Guardian/parent 2's full name",
686+
type: "string",
687+
},
688+
{
689+
name: "waiveDocumentFees",
690+
label: "Waive fees?",
691+
type: "boolean",
692+
},
655693
] as const;
656694

657695
export type FieldName = (typeof FIELD_DEFS)[number]["name"];

web/src/constants/forms.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const FORM_SLUGS = [
1010
"court-order-ma-minor",
1111
"court-order-ri",
1212
"social-security",
13+
"birth-certificate-affidavit-ma",
1314
] as const;
1415

1516
export type FormSlug = (typeof FORM_SLUGS)[number];

web/src/constants/pdf.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const PDF_IDS = [
1515
"ss5-application-for-social-security-card",
1616
"pc8.1-change-of-name",
1717
"background-check-authorization-of-release",
18+
"r116-applicant-affidavit-in-support-of-amendment-of-a-birth-certificate-for-sex",
1819
] as const;
1920

2021
export type PDFId = (typeof PDF_IDS)[number];

0 commit comments

Comments
 (0)