Rotate annotations based on the MK::R value (bug 1675139)

- it aims to fix: https://bugzilla.mozilla.org/show_bug.cgi?id=1675139;
- An annotation can be rotated (counterclockwise);
- the rotation can be set in using JS.
This commit is contained in:
Calixte Denizet 2022-06-19 16:39:54 +02:00
parent 54777b42c2
commit cdc58b7a52
10 changed files with 562 additions and 78 deletions

View File

@ -24,6 +24,7 @@ import {
assert,
escapeString,
getModificationDate,
IDENTITY_MATRIX,
isAscii,
LINE_DESCENT_FACTOR,
LINE_FACTOR,
@ -430,9 +431,11 @@ class Annotation {
this.setColor(dict.getArray("C"));
this.setBorderStyle(dict);
this.setAppearance(dict);
this.setBorderAndBackgroundColors(dict.get("MK"));
this._hasOwnCanvas = false;
const MK = dict.get("MK");
this.setBorderAndBackgroundColors(MK);
this.setRotation(MK);
this._streams = [];
if (this.appearance) {
this._streams.push(this.appearance);
@ -445,12 +448,14 @@ class Annotation {
color: this.color,
backgroundColor: this.backgroundColor,
borderColor: this.borderColor,
rotation: this.rotation,
contentsObj: this._contents,
hasAppearance: !!this.appearance,
id: params.id,
modificationDate: this.modificationDate,
rect: this.rectangle,
subtype: params.subtype,
hasOwnCanvas: false,
};
if (params.collectFields) {
@ -704,6 +709,22 @@ class Annotation {
}
}
setRotation(mk) {
this.rotation = 0;
if (mk instanceof Dict) {
let angle = mk.get("R") || 0;
if (Number.isInteger(angle) && angle !== 0) {
angle %= 360;
if (angle < 0) {
angle += 360;
}
if (angle % 90 === 0) {
this.rotation = angle;
}
}
}
}
/**
* Set the color for background and border if any.
* The default values are transparent.
@ -721,33 +742,6 @@ class Annotation {
}
}
getBorderAndBackgroundAppearances() {
if (!this.backgroundColor && !this.borderColor) {
return "";
}
const width = this.data.rect[2] - this.data.rect[0];
const height = this.data.rect[3] - this.data.rect[1];
const rect = `0 0 ${width} ${height} re`;
let str = "";
if (this.backgroundColor) {
str = `${getPdfColor(
this.backgroundColor,
/* isFill */ true
)} ${rect} f `;
}
if (this.borderColor) {
const borderWidth = this.borderStyle.width || 1;
str += `${borderWidth} w ${getPdfColor(
this.borderColor,
/* isFill */ false
)} ${rect} S `;
}
return str;
}
/**
* Set the border style (as AnnotationBorderStyle object).
*
@ -849,7 +843,7 @@ class Annotation {
const data = this.data;
let appearance = this.appearance;
const isUsingOwnCanvas =
this._hasOwnCanvas && intent & RenderingIntentFlag.DISPLAY;
this.data.hasOwnCanvas && intent & RenderingIntentFlag.DISPLAY;
if (!appearance) {
if (!isUsingOwnCanvas) {
return Promise.resolve(new OperatorList());
@ -918,6 +912,7 @@ class Annotation {
type: "",
kidIds: this.data.kidIds,
page: this.data.pageIndex,
rotation: this.rotation,
};
}
return null;
@ -1466,6 +1461,72 @@ class WidgetAnnotation extends Annotation {
return !!(this.data.fieldFlags & flag);
}
getRotationMatrix(annotationStorage) {
const storageEntry = annotationStorage
? annotationStorage.get(this.data.id)
: undefined;
let rotation = storageEntry && storageEntry.rotation;
if (rotation === undefined) {
rotation = this.rotation;
}
if (rotation === 0) {
return IDENTITY_MATRIX;
}
const width = this.data.rect[2] - this.data.rect[0];
const height = this.data.rect[3] - this.data.rect[1];
switch (rotation) {
case 90:
return [0, 1, -1, 0, width, 0];
case 180:
return [-1, 0, 0, -1, width, height];
case 270:
return [0, -1, 1, 0, 0, height];
default:
throw new Error("Invalid rotation");
}
}
getBorderAndBackgroundAppearances(annotationStorage) {
const storageEntry = annotationStorage
? annotationStorage.get(this.data.id)
: undefined;
let rotation = storageEntry && storageEntry.rotation;
if (rotation === undefined) {
rotation = this.rotation;
}
if (!this.backgroundColor && !this.borderColor) {
return "";
}
const width = this.data.rect[2] - this.data.rect[0];
const height = this.data.rect[3] - this.data.rect[1];
const rect =
rotation === 0 || rotation === 180
? `0 0 ${width} ${height} re`
: `0 0 ${height} ${width} re`;
let str = "";
if (this.backgroundColor) {
str = `${getPdfColor(
this.backgroundColor,
/* isFill */ true
)} ${rect} f `;
}
if (this.borderColor) {
const borderWidth = this.borderStyle.width || 1;
str += `${borderWidth} w ${getPdfColor(
this.borderColor,
/* isFill */ false
)} ${rect} S `;
}
return str;
}
getOperatorList(evaluator, task, intent, renderForms, annotationStorage) {
// Do not render form elements on the canvas when interactive forms are
// enabled. The display layer is responsible for rendering them instead.
@ -1516,7 +1577,7 @@ class WidgetAnnotation extends Annotation {
this.data.id,
this.data.rect,
transform,
matrix,
this.getRotationMatrix(annotationStorage),
]);
const stream = new StringStream(content);
@ -1535,13 +1596,34 @@ class WidgetAnnotation extends Annotation {
);
}
_getMKDict(rotation) {
const mk = new Dict(null);
if (rotation) {
mk.set("R", rotation);
}
if (this.borderColor) {
mk.set(
"BC",
Array.from(this.borderColor).map(c => c / 255)
);
}
if (this.backgroundColor) {
mk.set(
"BG",
Array.from(this.backgroundColor).map(c => c / 255)
);
}
return mk.size > 0 ? mk : null;
}
async save(evaluator, task, annotationStorage) {
const storageEntry = annotationStorage
? annotationStorage.get(this.data.id)
: undefined;
let value = storageEntry && storageEntry.value;
let rotation = storageEntry && storageEntry.rotation;
if (value === this.data.fieldValue || value === undefined) {
if (!this._hasValueFromXFA) {
if (!this._hasValueFromXFA && rotation === undefined) {
return null;
}
value = value || this.data.fieldValue;
@ -1549,6 +1631,7 @@ class WidgetAnnotation extends Annotation {
// Value can be an array (with choice list and multiple selections)
if (
rotation === undefined &&
!this._hasValueFromXFA &&
Array.isArray(value) &&
Array.isArray(this.data.fieldValue) &&
@ -1558,6 +1641,10 @@ class WidgetAnnotation extends Annotation {
return null;
}
if (rotation === undefined) {
rotation = this.rotation;
}
let appearance = await this._getAppearance(
evaluator,
task,
@ -1606,12 +1693,23 @@ class WidgetAnnotation extends Annotation {
dict.set("AP", AP);
dict.set("M", `D:${getModificationDate()}`);
const maybeMK = this._getMKDict(rotation);
if (maybeMK) {
dict.set("MK", maybeMK);
}
const appearanceDict = new Dict(xref);
appearanceDict.set("Length", appearance.length);
appearanceDict.set("Subtype", Name.get("Form"));
appearanceDict.set("Resources", this._getSaveFieldResources(xref));
appearanceDict.set("BBox", bbox);
const rotationMatrix = this.getRotationMatrix(annotationStorage);
if (rotationMatrix !== IDENTITY_MATRIX) {
// The matrix isn't the identity one.
appearanceDict.set("Matrix", rotationMatrix);
}
const bufferOriginal = [`${this.ref.num} ${this.ref.gen} obj\n`];
writeDict(dict, bufferOriginal, originalTransform);
bufferOriginal.push("\nendobj\n");
@ -1637,13 +1735,21 @@ class WidgetAnnotation extends Annotation {
const storageEntry = annotationStorage
? annotationStorage.get(this.data.id)
: undefined;
let value =
storageEntry && (storageEntry.formattedValue || storageEntry.value);
if (value === undefined) {
let value, rotation;
if (storageEntry) {
value = storageEntry.formattedValue || storageEntry.value;
rotation = storageEntry.rotation;
}
if (rotation === undefined && value === undefined) {
if (!this._hasValueFromXFA || this.appearance) {
// The annotation hasn't been rendered so use the appearance.
return null;
}
}
if (value === undefined) {
// The annotation has its value in XFA datasets but not in the V field.
value = this.data.fieldValue;
if (!value) {
@ -1651,6 +1757,10 @@ class WidgetAnnotation extends Annotation {
}
}
if (Array.isArray(value) && value.length === 1) {
value = value[0];
}
assert(typeof value === "string", "Expected `value` to be a string.");
value = value.trim();
@ -1660,6 +1770,10 @@ class WidgetAnnotation extends Annotation {
return "";
}
if (rotation === undefined) {
rotation = this.rotation;
}
let lineCount = -1;
if (this.data.multiLine) {
lineCount = value.split(/\r\n|\r|\n/).length;
@ -1667,8 +1781,12 @@ class WidgetAnnotation extends Annotation {
const defaultPadding = 2;
const hPadding = defaultPadding;
const totalHeight = this.data.rect[3] - this.data.rect[1];
const totalWidth = this.data.rect[2] - this.data.rect[0];
let totalHeight = this.data.rect[3] - this.data.rect[1];
let totalWidth = this.data.rect[2] - this.data.rect[0];
if (rotation === 90 || rotation === 270) {
[totalWidth, totalHeight] = [totalHeight, totalWidth];
}
if (!this._defaultAppearance) {
// The DA is required and must be a string.
@ -1719,7 +1837,8 @@ class WidgetAnnotation extends Annotation {
totalHeight,
alignment,
hPadding,
vPadding
vPadding,
annotationStorage
);
}
@ -1733,12 +1852,13 @@ class WidgetAnnotation extends Annotation {
encodedString,
totalWidth,
hPadding,
vPadding
vPadding,
annotationStorage
);
}
// Empty or it has a trailing whitespace.
const colors = this.getBorderAndBackgroundAppearances();
const colors = this.getBorderAndBackgroundAppearances(annotationStorage);
if (alignment === 0 || alignment > 2) {
// Left alignment: nothing to do
@ -1989,7 +2109,15 @@ class TextWidgetAnnotation extends WidgetAnnotation {
this.data.doNotScroll = this.hasFieldFlag(AnnotationFieldFlag.DONOTSCROLL);
}
_getCombAppearance(defaultAppearance, font, text, width, hPadding, vPadding) {
_getCombAppearance(
defaultAppearance,
font,
text,
width,
hPadding,
vPadding,
annotationStorage
) {
const combWidth = numberToString(width / this.data.maxLen);
const buf = [];
const positions = font.getCharPositions(text);
@ -1998,7 +2126,7 @@ class TextWidgetAnnotation extends WidgetAnnotation {
}
// Empty or it has a trailing whitespace.
const colors = this.getBorderAndBackgroundAppearances();
const colors = this.getBorderAndBackgroundAppearances(annotationStorage);
const renderedComb = buf.join(` ${combWidth} 0 Td `);
return (
`/Tx BMC q ${colors}BT ` +
@ -2017,7 +2145,8 @@ class TextWidgetAnnotation extends WidgetAnnotation {
height,
alignment,
hPadding,
vPadding
vPadding,
annotationStorage
) {
const lines = text.split(/\r\n?|\n/);
const buf = [];
@ -2043,7 +2172,7 @@ class TextWidgetAnnotation extends WidgetAnnotation {
const renderedText = buf.join("\n");
// Empty or it has a trailing whitespace.
const colors = this.getBorderAndBackgroundAppearances();
const colors = this.getBorderAndBackgroundAppearances(annotationStorage);
return (
`/Tx BMC q ${colors}BT ` +
@ -2137,6 +2266,7 @@ class TextWidgetAnnotation extends WidgetAnnotation {
page: this.data.pageIndex,
strokeColor: this.data.borderColor,
fillColor: this.data.backgroundColor,
rotation: this.rotation,
type: "text",
};
}
@ -2163,7 +2293,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
} else if (this.data.radioButton) {
this._processRadioButton(params);
} else if (this.data.pushButton) {
this._hasOwnCanvas = true;
this.data.hasOwnCanvas = true;
this._processPushButton(params);
} else {
warn("Invalid field flags for button widget annotation");
@ -2188,14 +2318,15 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
}
let value = null;
let rotation = null;
if (annotationStorage) {
const storageEntry = annotationStorage.get(this.data.id);
value = storageEntry ? storageEntry.value : null;
rotation = storageEntry ? storageEntry.rotation : null;
}
if (value === null) {
if (value === null && this.appearance) {
// Nothing in the annotationStorage.
if (this.appearance) {
// But we've a default appearance so use it.
return super.getOperatorList(
evaluator,
@ -2206,6 +2337,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
);
}
if (value === null || value === undefined) {
// There is no default appearance so use the one derived
// from the field value.
if (this.data.checkBox) {
@ -2220,6 +2352,15 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
: this.uncheckedAppearance;
if (appearance) {
const savedAppearance = this.appearance;
const savedMatrix = appearance.dict.getArray("Matrix") || IDENTITY_MATRIX;
if (rotation) {
appearance.dict.set(
"Matrix",
this.getRotationMatrix(annotationStorage)
);
}
this.appearance = appearance;
const operatorList = super.getOperatorList(
evaluator,
@ -2229,6 +2370,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
annotationStorage
);
this.appearance = savedAppearance;
appearance.dict.set("Matrix", savedMatrix);
return operatorList;
}
@ -2254,7 +2396,10 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
return null;
}
const storageEntry = annotationStorage.get(this.data.id);
const value = storageEntry && storageEntry.value;
let rotation = storageEntry && storageEntry.rotation;
let value = storageEntry && storageEntry.value;
if (rotation === undefined) {
if (value === undefined) {
return null;
}
@ -2263,12 +2408,20 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
if (defaultValue === value) {
return null;
}
}
const dict = evaluator.xref.fetchIfRef(this.ref);
if (!(dict instanceof Dict)) {
return null;
}
if (rotation === undefined) {
rotation = this.rotation;
}
if (value === undefined) {
value = this.data.fieldValue === this.data.exportValue;
}
const xfa = {
path: stringToPDFString(dict.get("T") || ""),
value: value ? this.data.exportValue : "",
@ -2279,6 +2432,11 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
dict.set("AS", name);
dict.set("M", `D:${getModificationDate()}`);
const maybeMK = this._getMKDict(rotation);
if (maybeMK) {
dict.set("MK", maybeMK);
}
const encrypt = evaluator.xref.encrypt;
let originalTransform = null;
if (encrypt) {
@ -2300,7 +2458,10 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
return null;
}
const storageEntry = annotationStorage.get(this.data.id);
const value = storageEntry && storageEntry.value;
let rotation = storageEntry && storageEntry.rotation;
let value = storageEntry && storageEntry.value;
if (rotation === undefined) {
if (value === undefined) {
return null;
}
@ -2309,12 +2470,21 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
if (defaultValue === value) {
return null;
}
}
const dict = evaluator.xref.fetchIfRef(this.ref);
if (!(dict instanceof Dict)) {
return null;
}
if (value === undefined) {
value = this.data.fieldValue === this.data.buttonValue;
}
if (rotation === undefined) {
rotation = this.rotation;
}
const xfa = {
path: stringToPDFString(dict.get("T") || ""),
value: value ? this.data.buttonValue : "",
@ -2346,6 +2516,11 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
dict.set("AS", name);
dict.set("M", `D:${getModificationDate()}`);
const maybeMK = this._getMKDict(rotation);
if (maybeMK) {
dict.set("MK", maybeMK);
}
let originalTransform = null;
if (encrypt) {
originalTransform = encrypt.createCipherTransform(
@ -2579,6 +2754,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
page: this.data.pageIndex,
strokeColor: this.data.borderColor,
fillColor: this.data.backgroundColor,
rotation: this.rotation,
type,
};
}
@ -2662,6 +2838,7 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
page: this.data.pageIndex,
strokeColor: this.data.borderColor,
fillColor: this.data.backgroundColor,
rotation: this.rotation,
type,
};
}
@ -2674,21 +2851,34 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
if (!annotationStorage) {
return null;
}
const storageEntry = annotationStorage.get(this.data.id);
let exportedValue = storageEntry && storageEntry.value;
if (exportedValue === undefined) {
if (!storageEntry) {
return null;
}
const rotation = storageEntry.rotation;
let exportedValue = storageEntry.value;
if (rotation === undefined && exportedValue === undefined) {
// The annotation hasn't been rendered so use the appearance
return null;
}
if (!Array.isArray(exportedValue)) {
if (exportedValue === undefined) {
exportedValue = this.data.fieldValue;
} else if (!Array.isArray(exportedValue)) {
exportedValue = [exportedValue];
}
const defaultPadding = 2;
const hPadding = defaultPadding;
const totalHeight = this.data.rect[3] - this.data.rect[1];
const totalWidth = this.data.rect[2] - this.data.rect[0];
let totalHeight = this.data.rect[3] - this.data.rect[1];
let totalWidth = this.data.rect[2] - this.data.rect[0];
if (rotation === 90 || rotation === 270) {
[totalWidth, totalHeight] = [totalHeight, totalWidth];
}
const lineCount = this.data.options.length;
const valueIndices = [];
for (let i = 0; i < lineCount; i++) {

View File

@ -264,12 +264,39 @@ class AnnotationElement {
container.style.left = `${(100 * (rect[0] - pageLLx)) / pageWidth}%`;
container.style.top = `${(100 * (rect[1] - pageLLy)) / pageHeight}%`;
const { rotation } = data;
if (data.hasOwnCanvas || rotation === 0) {
container.style.width = `${(100 * width) / pageWidth}%`;
container.style.height = `${(100 * height) / pageHeight}%`;
} else {
this.setRotation(rotation, container);
}
return container;
}
setRotation(angle, container = this.container) {
const [pageLLx, pageLLy, pageURx, pageURy] = this.viewport.viewBox;
const pageWidth = pageURx - pageLLx;
const pageHeight = pageURy - pageLLy;
const { width, height } = getRectDims(this.data.rect);
let elementWidth, elementHeight;
if (angle % 180 === 0) {
elementWidth = (100 * width) / pageWidth;
elementHeight = (100 * height) / pageHeight;
} else {
elementWidth = (100 * height) / pageWidth;
elementHeight = (100 * width) / pageHeight;
}
container.style.width = `${elementWidth}%`;
container.style.height = `${elementHeight}%`;
container.setAttribute("data-annotation-rotation", (360 - angle) % 360);
}
get _commonActions() {
const setColor = (jsName, styleName, event) => {
const color = event.detail[jsName];
@ -335,6 +362,13 @@ class AnnotationElement {
strokeColor: event => {
setColor("strokeColor", "borderColor", event);
},
rotation: event => {
const angle = event.detail.rotation;
this.setRotation(angle);
this.annotationStorage.setValue(this.data.id, {
rotation: angle,
});
},
});
}

View File

@ -57,7 +57,6 @@ class Field extends PDFObject {
this.required = data.required;
this.richText = data.richText;
this.richValue = data.richValue;
this.rotation = data.rotation;
this.style = data.style;
this.submitName = data.submitName;
this.textFont = data.textFont;
@ -84,6 +83,7 @@ class Field extends PDFObject {
this._kidIds = data.kidIds || null;
this._fieldType = getFieldType(this._actions);
this._siblings = data.siblings || null;
this._rotation = data.rotation || 0;
this._globalEval = data.globalEval;
this._appObjects = data.appObjects;
@ -188,6 +188,22 @@ class Field extends PDFObject {
throw new Error("field.page is read-only");
}
get rotation() {
return this._rotation;
}
set rotation(angle) {
angle = Math.floor(angle);
if (angle % 90 !== 0) {
throw new Error("Invalid rotation: must be a multiple of 90");
}
angle %= 360;
if (angle < 0) {
angle += 360;
}
this._rotation = angle;
}
get textColor() {
return this._textColor;
}

View File

@ -1401,4 +1401,47 @@ describe("Interaction", () => {
);
});
});
describe("in bug1675139.pdf", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("bug1675139.pdf", getSelector("48R"));
});
afterAll(async () => {
await closePages(pages);
});
it("must check that data-annotation-rotation is correc", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForFunction(
"window.PDFViewerApplication.scriptingReady === true"
);
let base = 0;
while (base !== 360) {
for (const [ref, angle] of [
[47, 0],
[42, 90],
[45, 180],
[46, 270],
]) {
const rotation = await page.$eval(
`[data-annotation-id='${ref}R']`,
el => parseInt(el.getAttribute("data-annotation-rotation") || 0)
);
expect(rotation)
.withContext(`In ${browserName}`)
.toEqual((360 + ((360 - (base + angle)) % 360)) % 360);
}
base += 90;
await page.click(getSelector("48R"));
}
})
);
});
});
});

View File

@ -528,3 +528,4 @@
!bug1771477.pdf
!bug1724918.pdf
!issue15053.pdf
!bug1675139.pdf

BIN
test/pdfs/bug1675139.pdf Executable file

Binary file not shown.

View File

@ -6583,5 +6583,44 @@
"rounds": 1,
"type": "eq",
"annotations": true
},
{ "id": "bug1675139",
"file": "pdfs/bug1675139.pdf",
"md5": "052c2c3dcc7ef4d4ac622282cb0fb17a",
"rounds": 1,
"type": "eq",
"annotations": true
},
{ "id": "bug1675139-print",
"file": "pdfs/bug1675139.pdf",
"md5": "052c2c3dcc7ef4d4ac622282cb0fb17a",
"rounds": 1,
"type": "eq",
"print": true,
"annotationStorage": {
"42R": {
"value": "pi/2"
},
"46R": {
"value": "3*pi/2",
"rotation": 180
},
"47R": {
"value": "0*pi/2"
},
"45R": {
"value": "pi"
},
"55R": {
"value": "C",
"rotation": 90
},
"52R": {
"value": "Yes"
},
"56R": {
"rotation": 270
}
}
}
]

View File

@ -2058,6 +2058,52 @@ describe("annotation", function () {
);
});
it("should save rotated text", async function () {
const textWidgetRef = Ref.get(123, 0);
const xref = new XRefMock([
{ ref: textWidgetRef, data: textWidgetDict },
helvRefObj,
]);
partialEvaluator.xref = xref;
const task = new WorkerTask("test save");
const annotation = await AnnotationFactory.create(
xref,
textWidgetRef,
pdfManagerMock,
idFactoryMock
);
const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, {
value: "hello world",
rotation: 90,
});
const data = await annotation.save(
partialEvaluator,
task,
annotationStorage
);
expect(data.length).toEqual(2);
const [oldData, newData] = data;
expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(newData.ref).toEqual(Ref.get(2, 0));
oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
expect(oldData.data).toEqual(
"123 0 obj\n" +
"<< /Type /Annot /Subtype /Widget /FT /Tx /DA (/Helv 5 Tf) /DR " +
"<< /Font << /Helv 314 0 R>>>> /Rect [0 0 32 10] " +
"/V (hello world) /AP << /N 2 0 R>> /M (date) /MK << /R 90>>>>\nendobj\n"
);
expect(newData.data).toEqual(
"2 0 obj\n<< /Length 74 /Subtype /Form /Resources " +
"<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10] /Matrix [0 1 -1 0 32 0]>> stream\n" +
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.04 Td (hello world) Tj " +
"ET Q EMC\nendstream\nendobj\n"
);
});
it("should get field object for usage in JS sandbox", async function () {
const textWidgetRef = Ref.get(123, 0);
const xDictRef = Ref.get(141, 0);
@ -2612,6 +2658,57 @@ describe("annotation", function () {
expect(data).toEqual(null);
});
it("should save rotated checkboxes", async function () {
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
normalAppearanceDict.set("Checked", Ref.get(314, 0));
normalAppearanceDict.set("Off", Ref.get(271, 0));
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
buttonWidgetDict.set("V", Name.get("Off"));
const buttonWidgetRef = Ref.get(123, 0);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
]);
partialEvaluator.xref = xref;
const task = new WorkerTask("test save");
const annotation = await AnnotationFactory.create(
xref,
buttonWidgetRef,
pdfManagerMock,
idFactoryMock
);
const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: true, rotation: 180 });
const [oldData] = await annotation.save(
partialEvaluator,
task,
annotationStorage
);
oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(oldData.data).toEqual(
"123 0 obj\n" +
"<< /Type /Annot /Subtype /Widget /FT /Btn " +
"/AP << /N << /Checked 314 0 R /Off 271 0 R>>>> " +
"/V /Checked /AS /Checked /M (date) /MK << /R 180>>>>\nendobj\n"
);
annotationStorage.set(annotation.data.id, { value: false });
const data = await annotation.save(
partialEvaluator,
task,
annotationStorage
);
expect(data).toEqual(null);
});
it("should handle radio buttons with a field value", async function () {
const parentDict = new Dict();
parentDict.set("V", Name.get("1"));
@ -3485,6 +3582,67 @@ describe("annotation", function () {
);
});
it("should save rotated choice", async function () {
choiceWidgetDict.set("Opt", ["A", "B", "C"]);
choiceWidgetDict.set("V", "A");
const choiceWidgetRef = Ref.get(123, 0);
const xref = new XRefMock([
{ ref: choiceWidgetRef, data: choiceWidgetDict },
fontRefObj,
]);
partialEvaluator.xref = xref;
const task = new WorkerTask("test save");
const annotation = await AnnotationFactory.create(
xref,
choiceWidgetRef,
pdfManagerMock,
idFactoryMock
);
const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: "C", rotation: 270 });
const data = await annotation.save(
partialEvaluator,
task,
annotationStorage
);
expect(data.length).toEqual(2);
const [oldData, newData] = data;
expect(oldData.ref).toEqual(Ref.get(123, 0));
expect(newData.ref).toEqual(Ref.get(2, 0));
oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
expect(oldData.data).toEqual(
"123 0 obj\n" +
"<< /Type /Annot /Subtype /Widget /FT /Ch /DA (/Helv 5 Tf) /DR " +
"<< /Font << /Helv 314 0 R>>>> " +
"/Rect [0 0 32 10] /Opt [(A) (B) (C)] /V (C) " +
"/AP << /N 2 0 R>> /M (date) /MK << /R 270>>>>\nendobj\n"
);
expect(newData.data).toEqual(
[
"2 0 obj",
"<< /Length 170 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " +
"/BBox [0 0 32 10] /Matrix [0 -1 1 0 0 10]>> stream",
"/Tx BMC q",
"1 1 10 32 re W n",
"0.600006 0.756866 0.854904 rg",
"1 11.75 10 6.75 re f",
"BT",
"/Helv 5 Tf",
"1 0 0 1 0 32 Tm",
"2 -5.88 Td (A) Tj",
"0 -6.75 Td (B) Tj",
"0 -6.75 Td (C) Tj",
"ET Q EMC",
"endstream",
"endobj\n",
].join("\n")
);
});
it("should save choice", async function () {
choiceWidgetDict.set("Opt", ["A", "B", "C"]);
choiceWidgetDict.set("V", "A");

View File

@ -1333,6 +1333,7 @@ describe("api", function () {
page: 0,
strokeColor: null,
fillColor: null,
rotation: 0,
type: "text",
},
],
@ -1354,6 +1355,7 @@ describe("api", function () {
page: 0,
strokeColor: null,
fillColor: new Uint8ClampedArray([192, 192, 192]),
rotation: 0,
type: "button",
},
],

View File

@ -51,6 +51,7 @@
text-align: initial;
pointer-events: auto;
box-sizing: border-box;
transform-origin: 0 0;
}
.annotationLayer .linkAnnotation > a,