Skip to content

Commit fb12c50

Browse files
committed
fix: flatten.js could throw a misleading error on PDFs with non-Latin field values
form.flatten() defaults to updateFieldAppearances:true, which regenerates appearance streams (using WinAnsi-encoded Helvetica) for any field lacking one -- including fields the user never touched, e.g. a PDF authored by another tool that set field values but relied on NeedAppearances instead of writing its own appearance streams. A non-Latin field value (CJK, emoji, etc.) on such a field threw an uncaught WinAnsi encoding error, shown as the generic 'may be corrupted or password-protected' fallback. Pre-validate every field's own pre-existing value before calling flatten(), same pattern as fill-form.js (#323) and watermark.js (#322).
1 parent 48f3b3a commit fb12c50

1 file changed

Lines changed: 31 additions & 2 deletions

File tree

flatten.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@ function baseName(fileName) {
137137
return fileName.replace(/\.pdf$/i, "");
138138
}
139139

140+
class ValidationError extends Error {}
141+
140142
async function flattenPdf() {
141-
const { PDFDocument } = await getPdfLib();
143+
const { PDFDocument, StandardFonts, PDFTextField, PDFDropdown, PDFOptionList } = await getPdfLib();
142144
const doc = await PDFDocument.load(await loaded.file.arrayBuffer(), { ignoreEncryption: true });
143145
const form = doc.getForm();
144146
const fieldCount = form.getFields().length;
@@ -147,6 +149,33 @@ async function flattenPdf() {
147149
return { noop: true };
148150
}
149151

152+
// form.flatten() regenerates appearance streams (using this same default
153+
// font) for any field lacking a valid one — including fields the user never
154+
// touched, e.g. a PDF authored by another tool that set field values but
155+
// relied on NeedAppearances instead of writing its own appearance streams.
156+
// Pre-checking every field's own pre-existing value here catches unrenderable
157+
// characters (CJK, emoji, other non-Latin scripts) before flatten()'s
158+
// internal save-time throw, which would otherwise be misread as a
159+
// corrupted/password-protected file.
160+
const font = await doc.embedFont(StandardFonts.Helvetica);
161+
function assertEncodable(text, fieldName) {
162+
if (!text) return;
163+
try {
164+
font.widthOfTextAtSize(text, 1);
165+
} catch (e) {
166+
throw new ValidationError(
167+
`The value for "${fieldName}" has a character the form's font can't render (likely emoji, CJK, or another non-Latin script) — flattening would fail. Fix it in Fill Form first, or remove the field's value, and try again.`
168+
);
169+
}
170+
}
171+
form.getFields().forEach((field) => {
172+
if (field instanceof PDFTextField) {
173+
assertEncodable(field.getText(), field.getName());
174+
} else if (field instanceof PDFDropdown || field instanceof PDFOptionList) {
175+
field.getSelected().forEach((value) => assertEncodable(value, field.getName()));
176+
}
177+
});
178+
150179
form.flatten();
151180
const bytes = await doc.save();
152181

@@ -200,7 +229,7 @@ flattenBtn.addEventListener("click", async () => {
200229
resultEl.setAttribute("role", "alert");
201230
resultEl.setAttribute("aria-live", "assertive");
202231
resultEl.innerHTML = `<span><strong>Flatten failed.</strong> ${escapeHtml(
203-
"This file may be corrupted or password-protected."
232+
err instanceof ValidationError ? err.message : "This file may be corrupted or password-protected."
204233
)}</span>`;
205234
} finally {
206235
flattenBtn.disabled = false;

0 commit comments

Comments
 (0)