Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
110 changes: 109 additions & 1 deletion src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { JpegStream } from "./jpeg_stream.js";
import { ObjectLoader } from "./object_loader.js";
import { OperatorList } from "./operator_list.js";
import { XFAFactory } from "./xfa/factory.js";
import { warn } from "../shared/util.js";

class AnnotationFactory {
static createGlobals(pdfManager) {
Expand Down Expand Up @@ -1884,7 +1885,31 @@ 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];
}

// Handle potential calculation errors that result in repeating patterns
if (typeof decodedValue === "string" && decodedValue.length > 0) {
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;

const defaultFieldValue = getInheritableProperty({
dict,
Expand Down Expand Up @@ -1949,6 +1974,89 @@ class WidgetAnnotation extends Annotation {
this._hasFlag(data.annotationFlags, AnnotationFlag.NOVIEW);
}

/**
* Fix calculation values that show repeating patterns due to JavaScript execution errors
* @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;
}

/**
* Decode the given form value.
*
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