Skip to content
Merged
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
38 changes: 23 additions & 15 deletions lib/mixins/annotations.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,21 +178,29 @@ export default {
return this.annotate(x, y, w, h, annotationOptions);
},

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

// make x2
let x2 = x1 + w;

// apply current transformation matrix to points
_convertRect(x, y, w, h) {
// apply the current transformation matrix to the corner at (x, y) and to
// the two edge vectors that reach the other three corners from it
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 ox = m0 * x + m2 * y + m4;
const oy = m1 * x + m3 * y + m5;
const wx = m0 * w;
const wy = m1 * w;
const hx = m2 * h;
const hy = m3 * h;

// ISO 32000-1 12.5.2 defines Rect as an axis aligned rectangle in default
// user space, so under a rotated or skewed matrix it is the bounding box of
// all four corners rather than one opposing pair. Each corner is (ox, oy)
// plus either edge or both, so the low bound adds the negative part of each
// edge and the high bound the positive part. That also returns the
// normalized [llx lly urx ury] ISO 32000-1 7.9.5 asks for when the matrix
// mirrors an axis.
return [
ox + (wx < 0 ? wx : 0) + (hx < 0 ? hx : 0),
oy + (wy < 0 ? wy : 0) + (hy < 0 ? hy : 0),
ox + (wx > 0 ? wx : 0) + (hx > 0 ? hx : 0),
oy + (wy > 0 ? wy : 0) + (hy > 0 ? hy : 0),
];
},
};
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