Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 142 additions & 1 deletion src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -1884,7 +1884,148 @@ class WidgetAnnotation extends Annotation {
key: "V",
getArray: true,
});
data.fieldValue = this._decodeFormValue(fieldValue);

let decodedValue = this._decodeFormValue(fieldValue);

// Handle Name objects, Arrays, and force numeric conversion
if (decodedValue instanceof Name) {
decodedValue = decodedValue.name;
}
if (Array.isArray(decodedValue)) {
decodedValue = decodedValue[0];
}

class WidgetAnnotation extends Annotation {
Copy link
Contributor

@timvandermeij timvandermeij Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this class repeated inside the class with the exact same name and same of the exact same code? Does that actually work?

constructor(params) {
super(params);

const { dict, xref, annotationGlobals } = params;
const data = this.data;
this._needAppearances = params.needAppearances;

data.annotationType = AnnotationType.WIDGET;
if (data.fieldName === undefined) {
data.fieldName = this._constructFieldName(dict);
}

if (data.actions === undefined) {
data.actions = collectActions(xref, dict, AnnotationActionEventType);
}

let fieldValue = getInheritableProperty({
dict,
key: "V",
getArray: true,
});

let decodedValue = this._decodeFormValue(fieldValue);

if (decodedValue instanceof Name) {
decodedValue = decodedValue.name;
}
if (Array.isArray(decodedValue)) {
decodedValue = decodedValue[0];
}

if (typeof decodedValue === "string" && decodedValue.length > 0) {
// Handle potential calculation errors that result in repeating patterns
const fixedValue = this._fixRepeatingCalculationValue(decodedValue);
if (fixedValue !== null) {
decodedValue = fixedValue;
} else {
let numericStr = decodedValue.replace(/,/g, "");
if (!isNaN(numericStr) && numericStr !== "") {
decodedValue = Number(numericStr);
}
}
}

data.fieldValue = decodedValue;
}
/**
* Fix calculation values that show repeating patterns due to JavaScript execution errors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit arbitrary to me. How do we know for sure that the repeated value wasn't intended by the user? Is there anything in the PDF specification that explains how to deal with this situation?

* @private
* @param {string} value - The potentially malformed calculation result
* @returns {number|null} - Fixed numeric value or null if no fix needed
*/
_fixRepeatingCalculationValue(value) {
if (!/^[\d.,]+$/.test(value)) {
return null;
}

let match = value.match(/^(\d{1,4})\1{2,}$/);
if (match) {
const basePattern = match[1];
const numericValue = Number(basePattern);
if (!isNaN(numericValue)) {
warn(
`PDF.js: Fixed repeating calculation value "${value}" -> "${basePattern}"`
);
return numericValue;
}
}

if (value.includes(".")) {
const parts = value.split(".");
if (parts.length > 2) {
const firstPart = parts[0];
const secondPart = parts[1];

if (
firstPart.length > 0 &&
value.startsWith(firstPart + "." + firstPart)
) {
const candidate = firstPart;
const numericValue = Number(candidate);
if (!isNaN(numericValue)) {
warn(
`PDF.js: Fixed repeating decimal calculation "${value}" -> "${candidate}"`
);
return numericValue;
}
}

const firstDecimal = parts[0] + "." + parts[1];
const numericValue = Number(firstDecimal);
if (!isNaN(numericValue)) {
warn(
`PDF.js: Fixed multiple decimal calculation "${value}" -> "${firstDecimal}"`
);
return numericValue;
}
}
}

if (value.length >= 6) {
for (
let patternLen = 1;
patternLen <= Math.floor(value.length / 3);
patternLen++
) {
const pattern = value.substring(0, patternLen);
const expectedRepeated = pattern.repeat(
Math.floor(value.length / patternLen)
);

if (
value.startsWith(expectedRepeated) &&
expectedRepeated.length >= value.length * 0.8
) {
const numericValue = Number(pattern);
if (!isNaN(numericValue) && pattern !== "0") {
warn(
`PDF.js: Fixed repeating pattern calculation "${value}" -> "${pattern}"`
);
return numericValue;
}
}
}
}

return null;
}
}
data.fieldValue = decodedValue;

const defaultFieldValue = getInheritableProperty({
dict,
Expand Down
95 changes: 95 additions & 0 deletions test/unit/annotation_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,101 @@ describe("annotation", function () {
}
}

describe("WidgetAnnotation repeating calculation value fix", function () {
let dict, xref, annotationGlobals;

beforeEach(function () {
dict = new Dict();
xref = new XRefMock();
annotationGlobals = {
acroForm: new Dict(),
};
});

it("should fix pure repeating digit patterns", function () {
dict.set("Subtype", Name.get("Widget"));
dict.set("V", "37037037");

const widgetAnnotation = new WidgetAnnotation({
dict,
xref,
annotationGlobals,
});

expect(widgetAnnotation.data.fieldValue).toEqual(37);
});

it("should fix repeating decimal patterns", function () {
dict.set("Subtype", Name.get("Widget"));
dict.set("V", "37.037.03");

const widgetAnnotation = new WidgetAnnotation({
dict,
xref,
annotationGlobals,
});

expect(widgetAnnotation.data.fieldValue).toEqual(37);
});

it("should fix multiple repeating patterns", function () {
dict.set("Subtype", Name.get("Widget"));
dict.set("V", "333333");

const widgetAnnotation = new WidgetAnnotation({
dict,
xref,
annotationGlobals,
});

expect(widgetAnnotation.data.fieldValue).toEqual(3);
});

it("should not modify valid numeric strings", function () {
dict.set("Subtype", Name.get("Widget"));
dict.set("V", "123.45");

const widgetAnnotation = new WidgetAnnotation({
dict,
xref,
annotationGlobals,
});

expect(widgetAnnotation.data.fieldValue).toEqual(123.45);
});

it("should not modify non-numeric strings", function () {
dict.set("Subtype", Name.get("Widget"));
dict.set("V", "ABC123");

const widgetAnnotation = new WidgetAnnotation({
dict,
xref,
annotationGlobals,
});

expect(widgetAnnotation.data.fieldValue).toEqual("ABC123");
});

it("should handle edge cases safely", function () {
const testCases = [
{ input: "", expected: "" },
{ input: "0", expected: 0 },
{ input: "000", expected: 0 },
{ input: "12", expected: 12 },
];

testCases.forEach(({ input, expected }) => {
dict.set("V", input);
const widgetAnnotation = new WidgetAnnotation({
dict,
xref,
annotationGlobals,
});
expect(widgetAnnotation.data.fieldValue).toEqual(expected);
});
});
});
const fontDataReader = new DefaultStandardFontDataFactory({
baseUrl: STANDARD_FONT_DATA_URL,
});
Expand Down
Loading