Skip to content

Commit 34ba918

Browse files
committed
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.
1 parent 54777b4 commit 34ba918

File tree

10 files changed

+562
-78
lines changed

10 files changed

+562
-78
lines changed

src/core/annotation.js

+265-75
Large diffs are not rendered by default.

src/display/annotation_layer.js

+36-2
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,39 @@ class AnnotationElement {
264264

265265
container.style.left = `${(100 * (rect[0] - pageLLx)) / pageWidth}%`;
266266
container.style.top = `${(100 * (rect[1] - pageLLy)) / pageHeight}%`;
267-
container.style.width = `${(100 * width) / pageWidth}%`;
268-
container.style.height = `${(100 * height) / pageHeight}%`;
267+
268+
const { rotation } = data;
269+
if (data.hasOwnCanvas || rotation === 0) {
270+
container.style.width = `${(100 * width) / pageWidth}%`;
271+
container.style.height = `${(100 * height) / pageHeight}%`;
272+
} else {
273+
this.setRotation(rotation, container);
274+
}
269275

270276
return container;
271277
}
272278

279+
setRotation(angle, container = this.container) {
280+
const [pageLLx, pageLLy, pageURx, pageURy] = this.viewport.viewBox;
281+
const pageWidth = pageURx - pageLLx;
282+
const pageHeight = pageURy - pageLLy;
283+
const { width, height } = getRectDims(this.data.rect);
284+
285+
let elementWidth, elementHeight;
286+
if (angle % 180 === 0) {
287+
elementWidth = (100 * width) / pageWidth;
288+
elementHeight = (100 * height) / pageHeight;
289+
} else {
290+
elementWidth = (100 * height) / pageWidth;
291+
elementHeight = (100 * width) / pageHeight;
292+
}
293+
294+
container.style.width = `${elementWidth}%`;
295+
container.style.height = `${elementHeight}%`;
296+
297+
container.setAttribute("data-annotation-rotation", (360 - angle) % 360);
298+
}
299+
273300
get _commonActions() {
274301
const setColor = (jsName, styleName, event) => {
275302
const color = event.detail[jsName];
@@ -335,6 +362,13 @@ class AnnotationElement {
335362
strokeColor: event => {
336363
setColor("strokeColor", "borderColor", event);
337364
},
365+
rotation: event => {
366+
const angle = event.detail.rotation;
367+
this.setRotation(angle);
368+
this.annotationStorage.setValue(this.data.id, {
369+
rotation: angle,
370+
});
371+
},
338372
});
339373
}
340374

src/scripting_api/field.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ class Field extends PDFObject {
5757
this.required = data.required;
5858
this.richText = data.richText;
5959
this.richValue = data.richValue;
60-
this.rotation = data.rotation;
6160
this.style = data.style;
6261
this.submitName = data.submitName;
6362
this.textFont = data.textFont;
@@ -84,6 +83,7 @@ class Field extends PDFObject {
8483
this._kidIds = data.kidIds || null;
8584
this._fieldType = getFieldType(this._actions);
8685
this._siblings = data.siblings || null;
86+
this._rotation = data.rotation || 0;
8787

8888
this._globalEval = data.globalEval;
8989
this._appObjects = data.appObjects;
@@ -188,6 +188,22 @@ class Field extends PDFObject {
188188
throw new Error("field.page is read-only");
189189
}
190190

191+
get rotation() {
192+
return this._rotation;
193+
}
194+
195+
set rotation(angle) {
196+
angle = Math.floor(angle);
197+
if (angle % 90 !== 0) {
198+
throw new Error("Invalid rotation: must be a multiple of 90");
199+
}
200+
angle %= 360;
201+
if (angle < 0) {
202+
angle += 360;
203+
}
204+
this._rotation = angle;
205+
}
206+
191207
get textColor() {
192208
return this._textColor;
193209
}

test/integration/scripting_spec.js

+43
Original file line numberDiff line numberDiff line change
@@ -1401,4 +1401,47 @@ describe("Interaction", () => {
14011401
);
14021402
});
14031403
});
1404+
1405+
describe("in bug1675139.pdf", () => {
1406+
let pages;
1407+
1408+
beforeAll(async () => {
1409+
pages = await loadAndWait("bug1675139.pdf", getSelector("48R"));
1410+
});
1411+
1412+
afterAll(async () => {
1413+
await closePages(pages);
1414+
});
1415+
1416+
it("must check that data-annotation-rotation is correc", async () => {
1417+
await Promise.all(
1418+
pages.map(async ([browserName, page]) => {
1419+
await page.waitForFunction(
1420+
"window.PDFViewerApplication.scriptingReady === true"
1421+
);
1422+
1423+
let base = 0;
1424+
1425+
while (base !== 360) {
1426+
for (const [ref, angle] of [
1427+
[47, 0],
1428+
[42, 90],
1429+
[45, 180],
1430+
[46, 270],
1431+
]) {
1432+
const rotation = await page.$eval(
1433+
`[data-annotation-id='${ref}R']`,
1434+
el => parseInt(el.getAttribute("data-annotation-rotation") || 0)
1435+
);
1436+
expect(rotation)
1437+
.withContext(`In ${browserName}`)
1438+
.toEqual((360 + ((360 - (base + angle)) % 360)) % 360);
1439+
}
1440+
base += 90;
1441+
await page.click(getSelector("48R"));
1442+
}
1443+
})
1444+
);
1445+
});
1446+
});
14041447
});

test/pdfs/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -528,3 +528,4 @@
528528
!bug1771477.pdf
529529
!bug1724918.pdf
530530
!issue15053.pdf
531+
!bug1675139.pdf

test/pdfs/bug1675139.pdf

14.5 KB
Binary file not shown.

test/test_manifest.json

+39
Original file line numberDiff line numberDiff line change
@@ -6583,5 +6583,44 @@
65836583
"rounds": 1,
65846584
"type": "eq",
65856585
"annotations": true
6586+
},
6587+
{ "id": "bug1675139",
6588+
"file": "pdfs/bug1675139.pdf",
6589+
"md5": "052c2c3dcc7ef4d4ac622282cb0fb17a",
6590+
"rounds": 1,
6591+
"type": "eq",
6592+
"annotations": true
6593+
},
6594+
{ "id": "bug1675139-print",
6595+
"file": "pdfs/bug1675139.pdf",
6596+
"md5": "052c2c3dcc7ef4d4ac622282cb0fb17a",
6597+
"rounds": 1,
6598+
"type": "eq",
6599+
"print": true,
6600+
"annotationStorage": {
6601+
"42R": {
6602+
"value": "pi/2"
6603+
},
6604+
"46R": {
6605+
"value": "3*pi/2",
6606+
"rotation": 180
6607+
},
6608+
"47R": {
6609+
"value": "0*pi/2"
6610+
},
6611+
"45R": {
6612+
"value": "pi"
6613+
},
6614+
"55R": {
6615+
"value": "C",
6616+
"rotation": 90
6617+
},
6618+
"52R": {
6619+
"value": "Yes"
6620+
},
6621+
"56R": {
6622+
"rotation": 270
6623+
}
6624+
}
65866625
}
65876626
]

test/unit/annotation_spec.js

+158
Original file line numberDiff line numberDiff line change
@@ -2058,6 +2058,52 @@ describe("annotation", function () {
20582058
);
20592059
});
20602060

2061+
it("should save rotated text", async function () {
2062+
const textWidgetRef = Ref.get(123, 0);
2063+
const xref = new XRefMock([
2064+
{ ref: textWidgetRef, data: textWidgetDict },
2065+
helvRefObj,
2066+
]);
2067+
partialEvaluator.xref = xref;
2068+
const task = new WorkerTask("test save");
2069+
2070+
const annotation = await AnnotationFactory.create(
2071+
xref,
2072+
textWidgetRef,
2073+
pdfManagerMock,
2074+
idFactoryMock
2075+
);
2076+
const annotationStorage = new Map();
2077+
annotationStorage.set(annotation.data.id, {
2078+
value: "hello world",
2079+
rotation: 90,
2080+
});
2081+
2082+
const data = await annotation.save(
2083+
partialEvaluator,
2084+
task,
2085+
annotationStorage
2086+
);
2087+
expect(data.length).toEqual(2);
2088+
const [oldData, newData] = data;
2089+
expect(oldData.ref).toEqual(Ref.get(123, 0));
2090+
expect(newData.ref).toEqual(Ref.get(2, 0));
2091+
2092+
oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
2093+
expect(oldData.data).toEqual(
2094+
"123 0 obj\n" +
2095+
"<< /Type /Annot /Subtype /Widget /FT /Tx /DA (/Helv 5 Tf) /DR " +
2096+
"<< /Font << /Helv 314 0 R>>>> /Rect [0 0 32 10] " +
2097+
"/V (hello world) /AP << /N 2 0 R>> /M (date) /MK << /R 90>>>>\nendobj\n"
2098+
);
2099+
expect(newData.data).toEqual(
2100+
"2 0 obj\n<< /Length 74 /Subtype /Form /Resources " +
2101+
"<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10] /Matrix [0 1 -1 0 32 0]>> stream\n" +
2102+
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.04 Td (hello world) Tj " +
2103+
"ET Q EMC\nendstream\nendobj\n"
2104+
);
2105+
});
2106+
20612107
it("should get field object for usage in JS sandbox", async function () {
20622108
const textWidgetRef = Ref.get(123, 0);
20632109
const xDictRef = Ref.get(141, 0);
@@ -2612,6 +2658,57 @@ describe("annotation", function () {
26122658
expect(data).toEqual(null);
26132659
});
26142660

2661+
it("should save rotated checkboxes", async function () {
2662+
const appearanceStatesDict = new Dict();
2663+
const normalAppearanceDict = new Dict();
2664+
2665+
normalAppearanceDict.set("Checked", Ref.get(314, 0));
2666+
normalAppearanceDict.set("Off", Ref.get(271, 0));
2667+
appearanceStatesDict.set("N", normalAppearanceDict);
2668+
2669+
buttonWidgetDict.set("AP", appearanceStatesDict);
2670+
buttonWidgetDict.set("V", Name.get("Off"));
2671+
2672+
const buttonWidgetRef = Ref.get(123, 0);
2673+
const xref = new XRefMock([
2674+
{ ref: buttonWidgetRef, data: buttonWidgetDict },
2675+
]);
2676+
partialEvaluator.xref = xref;
2677+
const task = new WorkerTask("test save");
2678+
2679+
const annotation = await AnnotationFactory.create(
2680+
xref,
2681+
buttonWidgetRef,
2682+
pdfManagerMock,
2683+
idFactoryMock
2684+
);
2685+
const annotationStorage = new Map();
2686+
annotationStorage.set(annotation.data.id, { value: true, rotation: 180 });
2687+
2688+
const [oldData] = await annotation.save(
2689+
partialEvaluator,
2690+
task,
2691+
annotationStorage
2692+
);
2693+
oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
2694+
expect(oldData.ref).toEqual(Ref.get(123, 0));
2695+
expect(oldData.data).toEqual(
2696+
"123 0 obj\n" +
2697+
"<< /Type /Annot /Subtype /Widget /FT /Btn " +
2698+
"/AP << /N << /Checked 314 0 R /Off 271 0 R>>>> " +
2699+
"/V /Checked /AS /Checked /M (date) /MK << /R 180>>>>\nendobj\n"
2700+
);
2701+
2702+
annotationStorage.set(annotation.data.id, { value: false });
2703+
2704+
const data = await annotation.save(
2705+
partialEvaluator,
2706+
task,
2707+
annotationStorage
2708+
);
2709+
expect(data).toEqual(null);
2710+
});
2711+
26152712
it("should handle radio buttons with a field value", async function () {
26162713
const parentDict = new Dict();
26172714
parentDict.set("V", Name.get("1"));
@@ -3485,6 +3582,67 @@ describe("annotation", function () {
34853582
);
34863583
});
34873584

3585+
it("should save rotated choice", async function () {
3586+
choiceWidgetDict.set("Opt", ["A", "B", "C"]);
3587+
choiceWidgetDict.set("V", "A");
3588+
3589+
const choiceWidgetRef = Ref.get(123, 0);
3590+
const xref = new XRefMock([
3591+
{ ref: choiceWidgetRef, data: choiceWidgetDict },
3592+
fontRefObj,
3593+
]);
3594+
partialEvaluator.xref = xref;
3595+
const task = new WorkerTask("test save");
3596+
3597+
const annotation = await AnnotationFactory.create(
3598+
xref,
3599+
choiceWidgetRef,
3600+
pdfManagerMock,
3601+
idFactoryMock
3602+
);
3603+
const annotationStorage = new Map();
3604+
annotationStorage.set(annotation.data.id, { value: "C", rotation: 270 });
3605+
3606+
const data = await annotation.save(
3607+
partialEvaluator,
3608+
task,
3609+
annotationStorage
3610+
);
3611+
expect(data.length).toEqual(2);
3612+
const [oldData, newData] = data;
3613+
expect(oldData.ref).toEqual(Ref.get(123, 0));
3614+
expect(newData.ref).toEqual(Ref.get(2, 0));
3615+
3616+
oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
3617+
expect(oldData.data).toEqual(
3618+
"123 0 obj\n" +
3619+
"<< /Type /Annot /Subtype /Widget /FT /Ch /DA (/Helv 5 Tf) /DR " +
3620+
"<< /Font << /Helv 314 0 R>>>> " +
3621+
"/Rect [0 0 32 10] /Opt [(A) (B) (C)] /V (C) " +
3622+
"/AP << /N 2 0 R>> /M (date) /MK << /R 270>>>>\nendobj\n"
3623+
);
3624+
expect(newData.data).toEqual(
3625+
[
3626+
"2 0 obj",
3627+
"<< /Length 170 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " +
3628+
"/BBox [0 0 32 10] /Matrix [0 -1 1 0 0 10]>> stream",
3629+
"/Tx BMC q",
3630+
"1 1 10 32 re W n",
3631+
"0.600006 0.756866 0.854904 rg",
3632+
"1 11.75 10 6.75 re f",
3633+
"BT",
3634+
"/Helv 5 Tf",
3635+
"1 0 0 1 0 32 Tm",
3636+
"2 -5.88 Td (A) Tj",
3637+
"0 -6.75 Td (B) Tj",
3638+
"0 -6.75 Td (C) Tj",
3639+
"ET Q EMC",
3640+
"endstream",
3641+
"endobj\n",
3642+
].join("\n")
3643+
);
3644+
});
3645+
34883646
it("should save choice", async function () {
34893647
choiceWidgetDict.set("Opt", ["A", "B", "C"]);
34903648
choiceWidgetDict.set("V", "A");

test/unit/api_spec.js

+2
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,7 @@ describe("api", function () {
13331333
page: 0,
13341334
strokeColor: null,
13351335
fillColor: null,
1336+
rotation: 0,
13361337
type: "text",
13371338
},
13381339
],
@@ -1354,6 +1355,7 @@ describe("api", function () {
13541355
page: 0,
13551356
strokeColor: null,
13561357
fillColor: new Uint8ClampedArray([192, 192, 192]),
1358+
rotation: 0,
13571359
type: "button",
13581360
},
13591361
],

web/annotation_layer_builder.css

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
text-align: initial;
5252
pointer-events: auto;
5353
box-sizing: border-box;
54+
transform-origin: 0 0;
5455
}
5556

5657
.annotationLayer .linkAnnotation > a,

0 commit comments

Comments
 (0)