( pdfDoc: PDFDocumentProxy, fieldObjects: Record<string, PdfJsFieldObject[]> | null, )
| 1136 | } |
| 1137 | |
| 1138 | export async function extractFormSchema( |
| 1139 | pdfDoc: PDFDocumentProxy, |
| 1140 | fieldObjects: Record<string, PdfJsFieldObject[]> | null, |
| 1141 | ): Promise<{ |
| 1142 | type: "object"; |
| 1143 | properties: Record<string, PrimitiveSchemaDefinition>; |
| 1144 | required?: string[]; |
| 1145 | } | null> { |
| 1146 | if (!fieldObjects || Object.keys(fieldObjects).length === 0) { |
| 1147 | return null; |
| 1148 | } |
| 1149 | |
| 1150 | const properties: Record<string, PrimitiveSchemaDefinition> = {}; |
| 1151 | for (const [name, fields] of Object.entries(fieldObjects)) { |
| 1152 | // pdfjs returns the full field-tree array: for separated structures |
| 1153 | // (pdf-lib) the typed widget is at [1+] behind a container at [0]; for |
| 1154 | // merged/leaf entries (W-9, most authoring tools) it's at [0]. Pick the |
| 1155 | // first entry that actually has a field type. |
| 1156 | const field = fields.find((f) => f.type) ?? fields[0]; |
| 1157 | if (!field.editable) continue; |
| 1158 | |
| 1159 | switch (field.type) { |
| 1160 | case "text": |
| 1161 | properties[name] = { type: "string", title: name }; |
| 1162 | break; |
| 1163 | case "checkbox": |
| 1164 | properties[name] = { type: "boolean", title: name }; |
| 1165 | break; |
| 1166 | case "radiobutton": { |
| 1167 | const options = fields |
| 1168 | .map((f) => f.exportValues) |
| 1169 | .filter((v): v is string => !!v && v !== "Off"); |
| 1170 | properties[name] = |
| 1171 | options.length > 0 |
| 1172 | ? { type: "string", title: name, enum: options } |
| 1173 | : { type: "string", title: name }; |
| 1174 | break; |
| 1175 | } |
| 1176 | case "combobox": |
| 1177 | case "listbox": { |
| 1178 | const items = field.items?.map((i) => i.exportValue).filter(Boolean); |
| 1179 | properties[name] = |
| 1180 | items && items.length > 0 |
| 1181 | ? { type: "string", title: name, enum: items } |
| 1182 | : { type: "string", title: name }; |
| 1183 | break; |
| 1184 | } |
| 1185 | // Skip "button" (push buttons) and unknown types |
| 1186 | } |
| 1187 | } |
| 1188 | |
| 1189 | // Collect alternativeText labels from per-page annotations |
| 1190 | // (getFieldObjects doesn't include them) |
| 1191 | const fieldLabels = new Map<string, string>(); |
| 1192 | try { |
| 1193 | for (let i = 1; i <= pdfDoc.numPages; i++) { |
| 1194 | const page = await pdfDoc.getPage(i); |
| 1195 | const annotations = await page.getAnnotations(); |
no outgoing calls
searching dependent graphs…