From e0672cc2b9a35fdc6d22b45c67d68e0a587adcdd Mon Sep 17 00:00:00 2001 From: Duncan Hsu Date: Mon, 4 May 2026 15:38:31 -0700 Subject: [PATCH] fix(document): apply rotation transform to annotation layers Mirror the ImageAnnotator pattern by applying rotation transforms to annotation layers in DocumentAnnotator.renderPage(). When rotation is non-zero, layers are sized to original (unrotated) page dimensions, centered, and rotated to stay anchored to the rotated page content. Co-Authored-By: Claude Opus 4.6 --- src/document/DocumentAnnotator.ts | 29 +++++- .../__tests__/DocumentAnnotator-test.ts | 90 ++++++++++++++++++- 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/document/DocumentAnnotator.ts b/src/document/DocumentAnnotator.ts index 5164caca6..477bf28ca 100644 --- a/src/document/DocumentAnnotator.ts +++ b/src/document/DocumentAnnotator.ts @@ -15,7 +15,7 @@ import { getAnnotation } from '../store/annotations'; import { BoundingBox } from '../store/boundingBoxHighlights'; import { getSelection } from './docUtil'; import { Manager } from '../common/BaseManager'; -import { getFileId, getIsCurrentFileVersion, getViewMode, Mode } from '../store'; +import { getFileId, getIsCurrentFileVersion, getRotation, getViewMode, Mode } from '../store'; import { scrollToLocation } from '../utils/scroll'; import './DocumentAnnotator.scss'; @@ -196,14 +196,35 @@ export default class DocumentAnnotator extends BaseAnnotator { renderPage(pageEl: HTMLElement): void { const pageManagers = this.getPageManagers(pageEl); const pageNumber = this.getPageNumber(pageEl); + const rotation = getRotation(this.store.getState()) || 0; + + // Calculate original (unrotated) page dimensions for annotation layer sizing + const pageWidth = pageEl.clientWidth; + const pageHeight = pageEl.clientHeight; + const isOrthogonal = rotation % 180 !== 0; + const origWidth = isOrthogonal ? pageHeight : pageWidth; + const origHeight = isOrthogonal ? pageWidth : pageHeight; // Render annotations for every page - pageManagers.forEach(manager => + pageManagers.forEach(manager => { + // Apply rotation transform to annotation layers + if (rotation) { + manager.style({ + height: `${origHeight}px`, + left: '50%', + top: '50%', + transform: `translate(-50%, -50%) rotate(${rotation}deg)`, + width: `${origWidth}px`, + }); + } else { + manager.style({height: '', left: '', top: '', transform: '', width: ''}); + } + manager.render({ intl: this.intl, store: this.store, - }), - ); + }); + }); this.managers.set(pageNumber, pageManagers); } diff --git a/src/document/__tests__/DocumentAnnotator-test.ts b/src/document/__tests__/DocumentAnnotator-test.ts index 04123e1ab..3742d5d2a 100644 --- a/src/document/__tests__/DocumentAnnotator-test.ts +++ b/src/document/__tests__/DocumentAnnotator-test.ts @@ -12,7 +12,7 @@ import { annotation as highlight } from '../../highlight/__mocks__/data'; import { annotations as drawings } from '../../drawing/__mocks__/drawingData'; import { annotations as regions } from '../../region/__mocks__/data'; import { fetchAnnotationsAction, Mode } from '../../store'; -import { setViewModeAction } from '../../store/options'; +import { setRotationAction, setViewModeAction } from '../../store/options'; import { HighlightCreatorManager, HighlightManager } from '../../highlight'; import { Manager } from '../../common/BaseManager'; import { scrollToLocation } from '../../utils/scroll'; @@ -314,7 +314,7 @@ describe('DocumentAnnotator', () => { describe('renderPage()', () => { test('should initialize a manager for a new page', () => { - const mockManager = ({ destroy: jest.fn(), render: jest.fn() } as unknown) as Manager; + const mockManager = ({ destroy: jest.fn(), render: jest.fn(), style: jest.fn() } as unknown) as Manager; const pageNumber = 1; const pageEl = getPage(pageNumber); @@ -329,6 +329,92 @@ describe('DocumentAnnotator', () => { store: expect.any(Object), }); }); + + test('should apply rotation styles when rotation is non-zero', () => { + const mockManager = ({ destroy: jest.fn(), render: jest.fn(), style: jest.fn() } as unknown) as Manager; + const pageNumber = 1; + const pageEl = getPage(pageNumber); + + // Mock page dimensions (after PDF.js rotation, dimensions are swapped) + Object.defineProperty(pageEl, 'clientWidth', { value: 400, configurable: true }); + Object.defineProperty(pageEl, 'clientHeight', { value: 800, configurable: true }); + + annotator.getPageManagers = jest.fn(() => new Set([mockManager])); + annotator.getPageNumber = jest.fn(() => pageNumber); + annotator.store.dispatch(setRotationAction(90)); + annotator.renderPage(pageEl); + + expect(mockManager.style).toHaveBeenCalledWith({ + height: '400px', + left: '50%', + top: '50%', + transform: 'translate(-50%, -50%) rotate(90deg)', + width: '800px', + }); + }); + + test('should swap dimensions for 270 degree rotation', () => { + const mockManager = ({ destroy: jest.fn(), render: jest.fn(), style: jest.fn() } as unknown) as Manager; + const pageNumber = 1; + const pageEl = getPage(pageNumber); + + Object.defineProperty(pageEl, 'clientWidth', { value: 400, configurable: true }); + Object.defineProperty(pageEl, 'clientHeight', { value: 800, configurable: true }); + + annotator.getPageManagers = jest.fn(() => new Set([mockManager])); + annotator.getPageNumber = jest.fn(() => pageNumber); + annotator.store.dispatch(setRotationAction(270)); + annotator.renderPage(pageEl); + + expect(mockManager.style).toHaveBeenCalledWith({ + height: '400px', + left: '50%', + top: '50%', + transform: 'translate(-50%, -50%) rotate(270deg)', + width: '800px', + }); + }); + + test('should not swap dimensions for 180 degree rotation', () => { + const mockManager = ({ destroy: jest.fn(), render: jest.fn(), style: jest.fn() } as unknown) as Manager; + const pageNumber = 1; + const pageEl = getPage(pageNumber); + + Object.defineProperty(pageEl, 'clientWidth', { value: 800, configurable: true }); + Object.defineProperty(pageEl, 'clientHeight', { value: 600, configurable: true }); + + annotator.getPageManagers = jest.fn(() => new Set([mockManager])); + annotator.getPageNumber = jest.fn(() => pageNumber); + annotator.store.dispatch(setRotationAction(180)); + annotator.renderPage(pageEl); + + expect(mockManager.style).toHaveBeenCalledWith({ + height: '600px', + left: '50%', + top: '50%', + transform: 'translate(-50%, -50%) rotate(180deg)', + width: '800px', + }); + }); + + test('should reset styles when rotation is 0', () => { + const mockManager = ({ destroy: jest.fn(), render: jest.fn(), style: jest.fn() } as unknown) as Manager; + const pageNumber = 1; + const pageEl = getPage(pageNumber); + + annotator.getPageManagers = jest.fn(() => new Set([mockManager])); + annotator.getPageNumber = jest.fn(() => pageNumber); + annotator.store.dispatch(setRotationAction(0)); + annotator.renderPage(pageEl); + + expect(mockManager.style).toHaveBeenCalledWith({ + height: '', + left: '', + top: '', + transform: '', + width: '', + }); + }); }); describe('scrollToAnnotation()', () => {