diff --git a/examples/pdf-server/src/annotation-panel.ts b/examples/pdf-server/src/annotation-panel.ts index 6b410952..081d5c85 100644 --- a/examples/pdf-server/src/annotation-panel.ts +++ b/examples/pdf-server/src/annotation-panel.ts @@ -831,8 +831,15 @@ function clearFieldInStorage(name: string): void { meta?.[0]?.defaultValue ?? ""; const type = meta?.find((f) => f.type)?.type; - const clearValue = - type === "checkbox" || type === "radiobutton" ? (dv ?? "Off") : (dv ?? ""); + // Radio: per-widget BOOLEANS, never a string. pdf.js's + // RadioButtonWidgetAnnotation render() has inverted string coercion (see + // setFieldInStorage), so writing the same string to every widget checks + // the wrong one. {value:false} on all = nothing selected. + if (type === "radiobutton") { + for (const id of ids) storage.setValue(id, { value: false }); + return; + } + const clearValue = type === "checkbox" ? (dv ?? "Off") : (dv ?? ""); for (const id of ids) storage.setValue(id, { value: clearValue }); } diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index fe30b14f..678721d0 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -2971,15 +2971,23 @@ async function getAnnotatedPdfBytes(): Promise { } } - // buildAnnotatedPdfBytes gates on formFields.size > 0 and only writes - // entries present in the map. After clearAllItems() the map is empty → - // zero setText/uncheck calls → pdf-lib leaves original /V intact → - // the "stripped PDF" we promised keeps all its form data. To actually - // clear, send an explicit sentinel for every baseline field the user - // dropped: "" for text, false for checkbox (matching baseline type). - const formFieldsOut = new Map(formFieldValues); + // Only write fields that actually changed vs. what's already in the PDF. + // Unchanged fields are no-ops at best, and at worst trip pdf-lib edge + // cases (max-length text, missing /Yes appearance, …) on fields the user + // never touched — which, before the per-field catch in + // buildAnnotatedPdfBytes, aborted every subsequent field. + // + // Fields the user cleared (present in baseline, absent from formFieldValues + // after clearAllItems()) still need an explicit "" / false so pdf-lib + // overwrites the original /V instead of leaving it intact. + const formFieldsOut = new Map(); + for (const [name, value] of formFieldValues) { + if (pdfBaselineFormValues.get(name) !== value) { + formFieldsOut.set(name, value); + } + } for (const [name, baselineValue] of pdfBaselineFormValues) { - if (!formFieldsOut.has(name)) { + if (!formFieldValues.has(name)) { formFieldsOut.set(name, typeof baselineValue === "boolean" ? false : ""); } } @@ -3499,6 +3507,10 @@ formLayerEl.addEventListener("input", (e) => { if (!target.checked) return; // unchecking siblings — ignore const wid = target.getAttribute("data-element-id"); value = (wid && radioButtonValues.get(wid)) ?? target.value; + } else if (target instanceof HTMLSelectElement && target.multiple) { + // .value on a