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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Unreleased

- Add a `hidden` option to form annotation methods, for a field that should start hidden (e.g. one an interactive action reveals later) instead of the usual default of visible and printable
- Fix annotations placed under `doc.rotate()` marking the wrong area, because `_convertRect` derived each corner's y from the already transformed x and mapped only two of the four corners, so the rectangle a viewer makes interactive did not follow the rotated content. Fixes #1153

### [v0.20.2] - 2026-08-29

Expand Down
28 changes: 20 additions & 8 deletions lib/mixins/annotations.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,19 +180,31 @@ export default {

_convertRect(x1, y1, w, h) {
// flip y1 and y2
let y2 = y1;
const y2 = y1;
y1 += h;

// make x2
let x2 = x1 + w;
const x2 = x1 + w;

// apply current transformation matrix to points
const [m0, m1, m2, m3, m4, m5] = this._ctm;
x1 = m0 * x1 + m2 * y1 + m4;
y1 = m1 * x1 + m3 * y1 + m5;
x2 = m0 * x2 + m2 * y2 + m4;
y2 = m1 * x2 + m3 * y2 + m5;

return [x1, y1, x2, y2];
const transform = (x, y) => [m0 * x + m2 * y + m4, m1 * x + m3 * y + m5];

// ISO 32000-1 12.5.2 defines Rect as an axis aligned rectangle in default
// user space, so a rotated or skewed matrix needs the bounding box of all
// four transformed corners. Two corners are enough only while the matrix
// keeps the axes aligned, and picking the extremes also keeps the result
// normalized ([llx lly urx ury], ISO 32000-1 7.9.5) when the matrix
// mirrors an axis.
const corners = [
transform(x1, y1),
transform(x2, y1),
transform(x2, y2),
transform(x1, y2),
];
const xs = corners.map((corner) => corner[0]);
const ys = corners.map((corner) => corner[1]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you refactor to remove the inner function and extra map and Math calls?

return [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];
},
};
22 changes: 22 additions & 0 deletions tests/unit/annotations.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@ describe('Annotations', () => {
});
});

describe('rectangle under a transformation matrix', () => {
test('covers the whole rotated area', () => {
const docData = logData(document);

// Turning a 100x100 box a quarter turn about its own top left corner
// swings it from x 100..200 across to x 0..100 and leaves it spanning
// y 672..772 in default user space.
document.rotate(90, { origin: [100, 20] });
document.link(100, 20, 100, 100, 'http://www.example.com');

expect(docData.join('\n')).toContain('/Rect [0 672 100 772]');
});

test('leaves an untransformed rectangle alone', () => {
const docData = logData(document);

document.link(100, 20, 100, 100, 'http://www.example.com');

expect(docData.join('\n')).toContain('/Rect [100 672 200 772]');
});
});

describe('undefined option values', () => {
// `doc.annotate()` passes arbitrary dictionary keys straight through by
// design, so unlike the acroform options there is no call site at which an
Expand Down
Loading