@@ -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+
98164export interface Rename {
99165 from : string ;
100166 to : string ;
0 commit comments