Skip to content

Commit

Permalink
Merge pull request #15060 from calixteman/annotation_rotation
Browse files Browse the repository at this point in the history
Rotate annotations based on the MK::R value (bug 1675139)
  • Loading branch information
calixteman authored Jun 21, 2022
2 parents 05cab5c + cdc58b7 commit 8d466f5
Show file tree
Hide file tree
Showing 10 changed files with 562 additions and 78 deletions.
340 changes: 265 additions & 75 deletions src/core/annotation.js

Large diffs are not rendered by default.

38 changes: 36 additions & 2 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,39 @@ class AnnotationElement {

container.style.left = `${(100 * (rect[0] - pageLLx)) / pageWidth}%`;
container.style.top = `${(100 * (rect[1] - pageLLy)) / pageHeight}%`;
container.style.width = `${(100 * width) / pageWidth}%`;
container.style.height = `${(100 * height) / 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];
Expand Down Expand Up @@ -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,
});
},
});
}

Expand Down
18 changes: 17 additions & 1 deletion src/scripting_api/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
43 changes: 43 additions & 0 deletions test/integration/scripting_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
})
);
});
});
});
1 change: 1 addition & 0 deletions test/pdfs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,4 @@
!bug1771477.pdf
!bug1724918.pdf
!issue15053.pdf
!bug1675139.pdf
Binary file added test/pdfs/bug1675139.pdf
Binary file not shown.
39 changes: 39 additions & 0 deletions test/test_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
]
158 changes: 158 additions & 0 deletions test/unit/annotation_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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");
Expand Down
2 changes: 2 additions & 0 deletions test/unit/api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,7 @@ describe("api", function () {
page: 0,
strokeColor: null,
fillColor: null,
rotation: 0,
type: "text",
},
],
Expand All @@ -1354,6 +1355,7 @@ describe("api", function () {
page: 0,
strokeColor: null,
fillColor: new Uint8ClampedArray([192, 192, 192]),
rotation: 0,
type: "button",
},
],
Expand Down
1 change: 1 addition & 0 deletions web/annotation_layer_builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
text-align: initial;
pointer-events: auto;
box-sizing: border-box;
transform-origin: 0 0;
}

.annotationLayer .linkAnnotation > a,
Expand Down

0 comments on commit 8d466f5

Please sign in to comment.