// Use the legacy build — the main build relies on browser-only APIs (DOMMatrix // etc.) that aren't available in a Bun/Node server context. // @ts-ignore — legacy path has no separate .d.ts; types are compatible import * as pdfjsLib from "pdfjs-dist/legacy/build/pdf.mjs"; // Point at the legacy worker. import.meta.resolve returns a file:// URL that // Bun can load as a Worker, which pdfjs requires even in server contexts. pdfjsLib.GlobalWorkerOptions.workerSrc = import.meta.resolve( "pdfjs-dist/legacy/build/pdf.worker.mjs", ); // ─── Types ──────────────────────────────────────────────────────────────────── /** Raw extracted field values keyed by PDF field name. */ export type FieldValues = Record; export interface FrageVorlage { id?: string; text: string; } export interface TemplateLeafObject { fieldName: string; label?: string; antwortFormat?: "einzelfrage" | "mehrfachfrage_ein_antwortfeld"; fragen?: FrageVorlage[]; } export type TemplateLeaf = string | TemplateLeafObject; export interface FieldIndexEntry { path: string; fieldName: string; label?: string; antwortFormat?: "einzelfrage" | "mehrfachfrage_ein_antwortfeld"; fragen?: FrageVorlage[]; isRich?: boolean; } /** * Flattened view of a template: PDF field name → metadata about the target path. */ export type FieldIndex = Record; // ─── Template helpers ───────────────────────────────────────────────────────── function isTemplateLeafObject(value: unknown): value is TemplateLeafObject { return typeof value === "object" && value !== null && typeof (value as Record).fieldName === "string"; } /** * Recursively flattens the nested template produced by templatebuilder. */ export function flattenTemplate( node: Record, prefix = "", ): FieldIndex { const index: FieldIndex = {}; for (const [key, value] of Object.entries(node)) { const path = prefix ? `${prefix}.${key}` : key; if (typeof value === "string") { index[value] = { path, fieldName: value, antwortFormat: "einzelfrage", isRich: false, }; } else if (isTemplateLeafObject(value)) { index[value.fieldName] = { path, fieldName: value.fieldName, label: value.label, antwortFormat: value.antwortFormat ?? "einzelfrage", fragen: value.fragen, isRich: true, }; } else if (typeof value === "object" && value !== null) { Object.assign(index, flattenTemplate(value as Record, path)); } } return index; } // ─── PDF extraction ─────────────────────────────────────────────────────────── export async function extractFields( data: ArrayBuffer, options: { maxPages?: number } = {}, ): Promise { const pdf = await pdfjsLib.getDocument({ data }).promise; if (options.maxPages && pdf.numPages > options.maxPages) { throw new Error(`PDF has ${pdf.numPages} pages; the configured limit is ${options.maxPages}.`); } const values: FieldValues = {}; for (let p = 1; p <= pdf.numPages; p++) { const page = await pdf.getPage(p); const annotations = await page.getAnnotations(); for (const ann of annotations) { if (ann.subtype !== "Widget" || !ann.fieldName) continue; const raw = ann.fieldValue; if (raw === undefined || raw === null) continue; values[ann.fieldName] = Array.isArray(raw) ? raw.join(", ") : String(raw); } } return values; } // ─── Output builder ─────────────────────────────────────────────────────────── export function buildOutput( values: FieldValues, index: FieldIndex, ): Record { const output: Record = {}; for (const entry of Object.values(index)) { const value = values[entry.fieldName]; if (value === undefined) continue; if (!entry.isRich) { setNested(output, entry.path, value); continue; } const antwortFormat = entry.antwortFormat ?? "einzelfrage"; const fragen = (entry.fragen ?? []).map((frage, i) => ({ id: frage.id ?? `${entry.path.split(".").at(-1) ?? "frage"}_${i + 1}`, text: frage.text, ...(antwortFormat === "einzelfrage" ? { antwort: value } : {}), })); setNested(output, entry.path, { fieldName: entry.fieldName, label: entry.label, antwortFormat, zuordnungsmodus: antwortFormat === "mehrfachfrage_ein_antwortfeld" ? "gemeinsame_antwort" : "einzelfrage", fragen, antwort: value, }); } return output; } export function getNested(obj: Record, path: string): unknown { let cur: unknown = obj; for (const key of path.split(".")) { if (typeof cur !== "object" || cur === null) return undefined; cur = (cur as Record)[key]; } return cur; } // ─── Internal helpers ───────────────────────────────────────────────────────── function setNested(obj: Record, path: string, value: unknown): void { const parts = path.split("."); let cur = obj; for (let i = 0; i < parts.length - 1; i++) { const key = parts[i]!; if (typeof cur[key] !== "object" || cur[key] === null) cur[key] = {}; cur = cur[key] as Record; } cur[parts[parts.length - 1]!] = value; }