@file-viewer/renderer-pdf 2.2.4 → 2.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pdf.js CHANGED
@@ -5,6 +5,8 @@ import { DEFAULT_FILE_VIEWER_PDF_WORKER_PATH, resolveFileViewerPdfAssetUrls, res
5
5
  import { pdfViewerStyle } from './pdfStyles.js';
6
6
  import { collectMalformedIdentityFontNames, createPdfCjkFontFallbackManager, detectMalformedIdentityCjkFontFamilies, } from './pdfFontFallback.js';
7
7
  import { PDF_FIT_HORIZONTAL_PADDING, PDF_PAGE_BORDER_WIDTH, resolvePdfFitViewportSize, } from './pdfFit.js';
8
+ import { createPdfBoundingBoxController, } from './pdfBboxController.js';
9
+ import { clampPdfScale, normalizePdfRotation, resolvePdfViewStateUpdate, } from './pdfViewState.js';
8
10
  import { capturePdfJsWorkerGlobal, scopePdfJsWorkerMessageHandler, } from './pdfWorkerGlobal.js';
9
11
  import { readPdfJsWorkerVersion } from './pdfWorkerVersion.js';
10
12
  export const DEFAULT_FILE_VIEWER_PDF_WORKER_URL = DEFAULT_FILE_VIEWER_PDF_WORKER_PATH;
@@ -38,6 +40,8 @@ const createStyle = (documentRef) => {
38
40
  .pdf-page-thumb--thumbnail{width:46px;height:60px;overflow:hidden;background:#fff}
39
41
  .pdf-page-thumb--thumbnail img{display:block;width:100%;height:100%;object-fit:contain}
40
42
  .pdf-page-thumb--thumbnail span{display:inline-flex;align-items:center;justify-content:center;width:100%;height:100%}
43
+ .pdf-bbox-layer{position:absolute;inset:0;z-index:20;pointer-events:none;overflow:hidden}
44
+ .pdf-bbox-highlight{position:absolute;box-sizing:border-box;border:2px solid var(--pdf-bbox-color,#f97316);border-radius:3px;background:rgba(249,115,22,.16);background:color-mix(in srgb,var(--pdf-bbox-color,#f97316) 18%,transparent);box-shadow:0 0 0 1px rgba(255,255,255,.8),0 2px 8px rgba(15,23,42,.16)}
41
45
  [data-viewer-theme='dark'] .pdf-shell{background:#101820;color:#e5eef8}
42
46
  [data-viewer-theme='dark'] .pdf-toolbar,[data-viewer-theme='dark'] .pdf-nav-pane,[data-viewer-theme='dark'] .pdf-nav-head,[data-viewer-theme='dark'] .pdf-nav-tabs{border-color:rgba(148,163,184,.18);background:#111827;box-shadow:none}
43
47
  [data-viewer-theme='dark'] .pdf-toolbar-group,[data-viewer-theme='dark'] .pdf-page-button,[data-viewer-theme='dark'] .pdf-outline-empty,[data-viewer-theme='dark'] .pdf-state{border-color:rgba(148,163,184,.18);background:#151f2b;color:#cbd5e1}
@@ -86,11 +90,8 @@ const createButton = (documentRef, className, title, label) => {
86
90
  }
87
91
  return button;
88
92
  };
89
- const normalizeRotation = (rotation) => {
90
- const normalized = ((Math.round(rotation / 90) * 90) % 360 + 360) % 360;
91
- return (normalized === 90 || normalized === 180 || normalized === 270 ? normalized : 0);
92
- };
93
- const clampScale = (scale) => Number(Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale)).toFixed(2));
93
+ const normalizeRotation = normalizePdfRotation;
94
+ const clampScale = (scale) => clampPdfScale(scale, MIN_SCALE, MAX_SCALE);
94
95
  const createPdfSearchState = (query = '') => ({
95
96
  query,
96
97
  total: 0,
@@ -340,6 +341,8 @@ export default async function renderPdf(buffer, target, context) {
340
341
  let suppressScrollEventUntil = 0;
341
342
  let userScrollIntentUntil = 0;
342
343
  let scrollStateFrame = 0;
344
+ let rotationOperationVersion = 0;
345
+ let pendingUserRotationAnchor = null;
343
346
  let pdfSearchState = createPdfSearchState();
344
347
  let pdfMatchesCount = { current: 0, total: 0 };
345
348
  let pdfSearchOptions;
@@ -456,9 +459,22 @@ export default async function renderPdf(buffer, target, context) {
456
459
  visit(outlineItems, 0);
457
460
  return result;
458
461
  };
462
+ const navScrollTopByMode = {
463
+ pages: 0,
464
+ outline: 0,
465
+ };
459
466
  const renderNavList = () => {
467
+ if (navList.classList.contains('pdf-page-list')) {
468
+ navScrollTopByMode.pages = navList.scrollTop;
469
+ }
470
+ else if (navList.classList.contains('pdf-outline-list')) {
471
+ navScrollTopByMode.outline = navList.scrollTop;
472
+ }
460
473
  navList.replaceChildren();
461
474
  navList.className = navMode === 'pages' ? 'pdf-page-list' : 'pdf-outline-list';
475
+ const restoreNavScrollTop = () => {
476
+ navList.scrollTop = navScrollTopByMode[navMode];
477
+ };
462
478
  if (navMode === 'pages') {
463
479
  thumbnailObserver === null || thumbnailObserver === void 0 ? void 0 : thumbnailObserver.disconnect();
464
480
  for (let page = 1; page <= pageCount; page += 1) {
@@ -478,6 +494,7 @@ export default async function renderPdf(buffer, target, context) {
478
494
  button.addEventListener('click', () => goToPage(page, 'page-click', 'user'));
479
495
  navList.append(button);
480
496
  }
497
+ restoreNavScrollTop();
481
498
  return;
482
499
  }
483
500
  const entries = flattenedOutlineItems();
@@ -500,6 +517,7 @@ export default async function renderPdf(buffer, target, context) {
500
517
  if (!entries.length) {
501
518
  navList.append(createElement(documentRef, 'div', 'pdf-outline-empty', t('pdf.nav.outlineEmpty')));
502
519
  }
520
+ restoreNavScrollTop();
503
521
  };
504
522
  const paintPdfThumbnail = (pageNumber, thumb) => {
505
523
  const imageUrl = pdfThumbnails.get(pageNumber);
@@ -957,6 +975,7 @@ export default async function renderPdf(buffer, target, context) {
957
975
  };
958
976
  const getPdfViewState = () => {
959
977
  const zoom = getPdfZoomState();
978
+ const bbox = pdfBoundingBoxController.getStateValue();
960
979
  return {
961
980
  renderer: 'pdf',
962
981
  page: currentPage,
@@ -969,6 +988,7 @@ export default async function renderPdf(buffer, target, context) {
969
988
  visible: navigationEnabled ? navVisible : false,
970
989
  mode: navMode,
971
990
  },
991
+ extra: bbox ? { bbox } : undefined,
972
992
  };
973
993
  };
974
994
  const emitViewStateChange = (action, source = 'viewer') => {
@@ -993,6 +1013,20 @@ export default async function renderPdf(buffer, target, context) {
993
1013
  const suppressProgrammaticScrollEvents = () => {
994
1014
  suppressScrollEventUntil = Math.max(suppressScrollEventUntil, Date.now() + 180);
995
1015
  };
1016
+ const pdfBoundingBoxController = createPdfBoundingBoxController({
1017
+ documentRef,
1018
+ targetWindow,
1019
+ viewerRoot: pdfViewerRoot,
1020
+ scrollContainer: container,
1021
+ initial: options === null || options === void 0 ? void 0 : options.bbox,
1022
+ getDocument: () => pdfContext.document,
1023
+ getPageCount: () => pageCount,
1024
+ getCurrentPage: () => currentPage,
1025
+ getRotation: () => currentRotation,
1026
+ goToPage: (page, source) => goToPage(page, 'bbox-focus', source, false),
1027
+ suppressProgrammaticScrollEvents,
1028
+ waitForPaint,
1029
+ });
996
1030
  const markFitInteraction = (source) => {
997
1031
  if (source !== 'user' && source !== 'api') {
998
1032
  return;
@@ -1003,7 +1037,83 @@ export default async function renderPdf(buffer, target, context) {
1003
1037
  autoFitWidth = false;
1004
1038
  activeFitRequest = null;
1005
1039
  };
1040
+ const getPdfPageElement = (pageNumber) => {
1041
+ var _a;
1042
+ const pageView = (_a = pdfContext.viewer) === null || _a === void 0 ? void 0 : _a.getPageView(pageNumber - 1);
1043
+ return (pageView === null || pageView === void 0 ? void 0 : pageView.div) ||
1044
+ pdfViewerRoot.querySelector(`.page[data-page-number="${pageNumber}"]`);
1045
+ };
1046
+ const captureCurrentPdfPageAnchor = () => {
1047
+ const pageElement = getPdfPageElement(currentPage);
1048
+ if (!pageElement) {
1049
+ return null;
1050
+ }
1051
+ const containerRect = container.getBoundingClientRect();
1052
+ const pageRect = pageElement.getBoundingClientRect();
1053
+ const pageHeight = pageElement.offsetHeight || pageRect.height;
1054
+ if (pageHeight <= 0) {
1055
+ return null;
1056
+ }
1057
+ const pageTop = pageRect.top - containerRect.top + container.scrollTop;
1058
+ const inPageRatio = (container.scrollTop - pageTop) / pageHeight;
1059
+ return {
1060
+ page: currentPage,
1061
+ inPageRatio: Math.max(0, Math.min(1, inPageRatio)),
1062
+ };
1063
+ };
1064
+ const cancelPendingUserRotationRestore = () => {
1065
+ if (!pendingUserRotationAnchor) {
1066
+ return;
1067
+ }
1068
+ pendingUserRotationAnchor = null;
1069
+ rotationOperationVersion += 1;
1070
+ };
1071
+ const restoreUserRotationAnchor = (anchor, operationVersion) => {
1072
+ const apply = () => {
1073
+ if (destroyed ||
1074
+ operationVersion !== rotationOperationVersion ||
1075
+ pendingUserRotationAnchor !== anchor) {
1076
+ return false;
1077
+ }
1078
+ const pageElement = getPdfPageElement(anchor.page);
1079
+ if (!pageElement) {
1080
+ return false;
1081
+ }
1082
+ const containerRect = container.getBoundingClientRect();
1083
+ const pageRect = pageElement.getBoundingClientRect();
1084
+ const pageHeight = pageElement.offsetHeight || pageRect.height;
1085
+ if (pageHeight <= 0) {
1086
+ return false;
1087
+ }
1088
+ const pageTop = pageRect.top - containerRect.top + container.scrollTop;
1089
+ const maxTop = Math.max(0, container.scrollHeight - container.clientHeight);
1090
+ suppressProgrammaticScrollEvents();
1091
+ container.scrollTop = Math.max(0, Math.min(maxTop, pageTop + anchor.inPageRatio * pageHeight));
1092
+ currentPage = anchor.page;
1093
+ syncUi();
1094
+ return true;
1095
+ };
1096
+ apply();
1097
+ void waitForPaint(targetWindow)
1098
+ .then(() => {
1099
+ apply();
1100
+ return waitForPaint(targetWindow);
1101
+ })
1102
+ .then(apply);
1103
+ targetWindow.requestAnimationFrame(() => {
1104
+ apply();
1105
+ targetWindow.requestAnimationFrame(apply);
1106
+ });
1107
+ targetWindow.setTimeout(() => {
1108
+ apply();
1109
+ if (operationVersion === rotationOperationVersion &&
1110
+ pendingUserRotationAnchor === anchor) {
1111
+ pendingUserRotationAnchor = null;
1112
+ }
1113
+ }, 180);
1114
+ };
1006
1115
  const recordUserScrollIntent = () => {
1116
+ cancelPendingUserRotationRestore();
1007
1117
  userScrollIntentUntil = Date.now() + 750;
1008
1118
  suppressScrollEventUntil = 0;
1009
1119
  markFitInteraction('user');
@@ -1027,7 +1137,7 @@ export default async function renderPdf(buffer, target, context) {
1027
1137
  }
1028
1138
  };
1029
1139
  const applyPdfViewState = async (state, applyOptions = {}) => {
1030
- var _a, _b;
1140
+ var _a;
1031
1141
  if (!pdfContext.viewer || loadStatus !== 'ready') {
1032
1142
  pendingInitialViewState = state;
1033
1143
  return getPdfViewState();
@@ -1039,29 +1149,46 @@ export default async function renderPdf(buffer, target, context) {
1039
1149
  const applyVersion = ++viewStateApplyVersion;
1040
1150
  activeViewStateApplyVersion = applyVersion;
1041
1151
  suppressProgrammaticScrollEvents();
1042
- const nextRotation = Number(state.rotation);
1043
- const nextScale = Number((_a = state.scale) !== null && _a !== void 0 ? _a : (_b = state.zoom) === null || _b === void 0 ? void 0 : _b.scale);
1044
- const nextPage = Number(state.page);
1152
+ const update = resolvePdfViewStateUpdate(state, {
1153
+ rotation: currentRotation,
1154
+ scale: currentScale,
1155
+ page: currentPage,
1156
+ pageCount,
1157
+ }, {
1158
+ minScale: MIN_SCALE,
1159
+ maxScale: MAX_SCALE,
1160
+ });
1161
+ const hasBboxUpdate = !!state.extra && Object.prototype.hasOwnProperty.call(state.extra, 'bbox');
1162
+ const bboxUpdate = hasBboxUpdate ? (_a = state.extra) === null || _a === void 0 ? void 0 : _a.bbox : undefined;
1045
1163
  try {
1046
1164
  if (state.navigation) {
1165
+ let navigationChanged = false;
1047
1166
  if (navigationEnabled && typeof state.navigation.visible === 'boolean') {
1167
+ navigationChanged = navVisible !== state.navigation.visible;
1048
1168
  navVisible = state.navigation.visible;
1049
1169
  }
1050
1170
  if (state.navigation.mode === 'pages' || state.navigation.mode === 'outline') {
1171
+ navigationChanged = navigationChanged || navMode !== state.navigation.mode;
1051
1172
  navMode = state.navigation.mode;
1052
1173
  }
1053
- syncUi();
1174
+ if (navigationChanged) {
1175
+ syncUi();
1176
+ }
1054
1177
  }
1055
- if (Number.isFinite(nextRotation)) {
1056
- applyRotation(nextRotation, 'rotation-change', source, false);
1178
+ if (update.rotation !== undefined) {
1179
+ applyRotation(update.rotation, 'rotation-change', source, false);
1057
1180
  }
1058
- if (Number.isFinite(nextScale)) {
1181
+ if (update.scale !== undefined) {
1059
1182
  autoFitWidth = false;
1060
- setScale(nextScale, 'zoom-change', source, false);
1183
+ setScale(update.scale, 'zoom-change', source, false);
1061
1184
  }
1062
- if (Number.isFinite(nextPage)) {
1063
- goToPage(nextPage, 'page-change', source, false);
1185
+ if (update.page !== undefined) {
1186
+ goToPage(update.page, 'page-change', source, false);
1064
1187
  }
1188
+ // A remote presenter can send scroll snapshots faster than one animation
1189
+ // frame. Apply the latest offset before yielding so superseded promises
1190
+ // cannot starve the projected screen until the presenter stops scrolling.
1191
+ restoreScrollState(state.scroll, false);
1065
1192
  await waitForPaint(targetWindow);
1066
1193
  if (applyVersion !== viewStateApplyVersion) {
1067
1194
  return getPdfViewState();
@@ -1072,6 +1199,9 @@ export default async function renderPdf(buffer, target, context) {
1072
1199
  return getPdfViewState();
1073
1200
  }
1074
1201
  restoreScrollState(state.scroll, false);
1202
+ if (hasBboxUpdate) {
1203
+ await pdfBoundingBoxController.set(bboxUpdate, { focus: true, source });
1204
+ }
1075
1205
  syncUi();
1076
1206
  if (notify && applyVersion === viewStateApplyVersion) {
1077
1207
  emitViewStateChange(action, source);
@@ -1276,6 +1406,18 @@ export default async function renderPdf(buffer, target, context) {
1276
1406
  const applyRotation = (rotation, action = 'rotation-change', source = 'viewer', notifyViewState = true) => {
1277
1407
  markFitInteraction(source);
1278
1408
  const normalized = normalizeRotation(rotation);
1409
+ if (source === 'user' && pdfContext.viewer) {
1410
+ pendingUserRotationAnchor || (pendingUserRotationAnchor = captureCurrentPdfPageAnchor());
1411
+ }
1412
+ else {
1413
+ cancelPendingUserRotationRestore();
1414
+ }
1415
+ const pageAnchor = source === 'user' ? pendingUserRotationAnchor : null;
1416
+ const operationVersion = ++rotationOperationVersion;
1417
+ if (pageAnchor) {
1418
+ suppressProgrammaticScrollEvents();
1419
+ currentPage = pageAnchor.page;
1420
+ }
1279
1421
  currentRotation = normalized;
1280
1422
  pdfThumbnails.clear();
1281
1423
  pendingPdfThumbnails.clear();
@@ -1286,18 +1428,36 @@ export default async function renderPdf(buffer, target, context) {
1286
1428
  pdfContext.viewer.pagesRotation = normalized;
1287
1429
  void waitForPaint(targetWindow).then(() => {
1288
1430
  var _a;
1431
+ if (operationVersion !== rotationOperationVersion) {
1432
+ return;
1433
+ }
1434
+ const refocusBoundingBoxes = () => {
1435
+ if (pdfBoundingBoxController.hasBoxes()) {
1436
+ void waitForPaint(targetWindow)
1437
+ .then(() => waitForPaint(targetWindow))
1438
+ .then(() => pdfBoundingBoxController.render({ focus: true, source }));
1439
+ }
1440
+ };
1289
1441
  if (reapplyFitAfterLayout(source, notifyViewState)) {
1442
+ if (pageAnchor) {
1443
+ restoreUserRotationAnchor(pageAnchor, operationVersion);
1444
+ }
1290
1445
  if (notifyViewState) {
1291
1446
  emitViewStateChange(action, source);
1292
1447
  }
1448
+ refocusBoundingBoxes();
1293
1449
  return;
1294
1450
  }
1295
1451
  (_a = pdfContext.viewer) === null || _a === void 0 ? void 0 : _a.update();
1296
1452
  scheduleLegacyPageDimensionPatch();
1297
1453
  syncUi();
1454
+ if (pageAnchor) {
1455
+ restoreUserRotationAnchor(pageAnchor, operationVersion);
1456
+ }
1298
1457
  if (notifyViewState) {
1299
1458
  emitViewStateChange(action, source);
1300
1459
  }
1460
+ refocusBoundingBoxes();
1301
1461
  });
1302
1462
  };
1303
1463
  const runWithStableHorizontalScroll = (action) => {
@@ -1312,6 +1472,9 @@ export default async function renderPdf(buffer, target, context) {
1312
1472
  if (!pdfContext.viewer || !pageCount) {
1313
1473
  return;
1314
1474
  }
1475
+ if (source === 'user') {
1476
+ cancelPendingUserRotationRestore();
1477
+ }
1315
1478
  markFitInteraction(source);
1316
1479
  const nextPage = Math.min(pageCount, Math.max(1, pageNumber));
1317
1480
  runWithStableHorizontalScroll(() => {
@@ -1550,8 +1713,19 @@ export default async function renderPdf(buffer, target, context) {
1550
1713
  if (pdfContext.search) {
1551
1714
  eventBus.dispatch('find', { type: '', query: pdfContext.search });
1552
1715
  }
1716
+ if (pdfBoundingBoxController.hasBoxes()) {
1717
+ void waitForPaint(targetWindow).then(() => pdfBoundingBoxController.render({
1718
+ focus: true,
1719
+ source: 'initial',
1720
+ }));
1721
+ }
1553
1722
  });
1554
1723
  eventBus.on('pagechanging', ({ pageNumber }) => {
1724
+ if (pendingUserRotationAnchor && pageNumber !== pendingUserRotationAnchor.page) {
1725
+ currentPage = pendingUserRotationAnchor.page;
1726
+ syncUi();
1727
+ return;
1728
+ }
1555
1729
  const previousPage = currentPage;
1556
1730
  currentPage = pageNumber;
1557
1731
  syncUi();
@@ -1571,6 +1745,9 @@ export default async function renderPdf(buffer, target, context) {
1571
1745
  });
1572
1746
  eventBus.on('pagerendered', ({ pageNumber }) => {
1573
1747
  scheduleLegacyPageDimensionPatch();
1748
+ if (pdfBoundingBoxController.hasBoxes()) {
1749
+ void pdfBoundingBoxController.render({ pageNumber, source: 'viewer' });
1750
+ }
1574
1751
  if (!pdfCjkFontFallbackManager ||
1575
1752
  pdfCjkFontFallbackRenderHandledPages.has(pageNumber)) {
1576
1753
  return;
@@ -1839,6 +2016,7 @@ export default async function renderPdf(buffer, target, context) {
1839
2016
  unregisterFileViewerSearchProvider(root);
1840
2017
  unregisterFileViewerZoomProvider(root);
1841
2018
  unregisterFileViewerViewStateProvider(root);
2019
+ pdfBoundingBoxController.destroy();
1842
2020
  outlineItems = [];
1843
2021
  (_a = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _a === void 0 ? void 0 : _a.call(context, null);
1844
2022
  (_b = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _b === void 0 ? void 0 : _b.call(context, null);
@@ -0,0 +1,21 @@
1
+ import type { FileViewerPdfBoundingBox } from '@file-viewer/core';
2
+ export interface NormalizedPdfBoundingBox {
3
+ id?: string;
4
+ page: number;
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ color?: string;
10
+ label?: string;
11
+ }
12
+ export interface PdfPageBox {
13
+ x: number;
14
+ y: number;
15
+ width: number;
16
+ height: number;
17
+ }
18
+ export declare const normalizePdfBoundingBoxInput: (input: unknown) => FileViewerPdfBoundingBox[];
19
+ export declare const normalizePdfBoundingBox: (input: FileViewerPdfBoundingBox, pageBox: PdfPageBox, fallbackPage?: number) => NormalizedPdfBoundingBox | null;
20
+ export declare const rotateNormalizedPdfBoundingBox: (box: NormalizedPdfBoundingBox, rotation: number) => NormalizedPdfBoundingBox;
21
+ export declare const serializePdfBoundingBoxes: (input: unknown) => string;
@@ -0,0 +1,93 @@
1
+ const clamp = (value, min = 0, max = 1) => Math.min(max, Math.max(min, value));
2
+ const finitePositive = (value) => Number.isFinite(value) && Number(value) > 0;
3
+ export const normalizePdfBoundingBoxInput = (input) => {
4
+ const values = input ? (Array.isArray(input) ? input : [input]) : [];
5
+ return values.filter(value => !!value && typeof value === 'object');
6
+ };
7
+ export const normalizePdfBoundingBox = (input, pageBox, fallbackPage = 1) => {
8
+ if (!Number.isFinite(input.x) ||
9
+ !Number.isFinite(input.y) ||
10
+ !finitePositive(input.width) ||
11
+ !finitePositive(input.height) ||
12
+ !finitePositive(pageBox.width) ||
13
+ !finitePositive(pageBox.height)) {
14
+ return null;
15
+ }
16
+ const unit = input.unit || 'pdf-point';
17
+ let x = input.x;
18
+ let y = input.y;
19
+ let width = input.width;
20
+ let height = input.height;
21
+ if (unit === 'percent') {
22
+ x /= 100;
23
+ y /= 100;
24
+ width /= 100;
25
+ height /= 100;
26
+ }
27
+ else if (unit === 'pixel') {
28
+ if (!finitePositive(input.sourceWidth) || !finitePositive(input.sourceHeight)) {
29
+ return null;
30
+ }
31
+ x /= Number(input.sourceWidth);
32
+ y /= Number(input.sourceHeight);
33
+ width /= Number(input.sourceWidth);
34
+ height /= Number(input.sourceHeight);
35
+ }
36
+ else if (unit === 'pdf-point') {
37
+ x = (x - pageBox.x) / pageBox.width;
38
+ y = (y - pageBox.y) / pageBox.height;
39
+ width /= pageBox.width;
40
+ height /= pageBox.height;
41
+ }
42
+ const origin = input.origin || (unit === 'pdf-point' ? 'bottom-left' : 'top-left');
43
+ if (origin === 'bottom-left') {
44
+ y = 1 - y - height;
45
+ }
46
+ const left = clamp(x);
47
+ const top = clamp(y);
48
+ const right = clamp(x + width);
49
+ const bottom = clamp(y + height);
50
+ if (right <= left || bottom <= top) {
51
+ return null;
52
+ }
53
+ return {
54
+ id: input.id,
55
+ page: Math.max(1, Math.round(Number(input.page) || fallbackPage || 1)),
56
+ x: left,
57
+ y: top,
58
+ width: right - left,
59
+ height: bottom - top,
60
+ color: input.color,
61
+ label: input.label,
62
+ };
63
+ };
64
+ export const rotateNormalizedPdfBoundingBox = (box, rotation) => {
65
+ const normalizedRotation = ((Math.round(rotation / 90) * 90) % 360 + 360) % 360;
66
+ if (normalizedRotation === 90) {
67
+ return {
68
+ ...box,
69
+ x: 1 - box.y - box.height,
70
+ y: box.x,
71
+ width: box.height,
72
+ height: box.width,
73
+ };
74
+ }
75
+ if (normalizedRotation === 180) {
76
+ return {
77
+ ...box,
78
+ x: 1 - box.x - box.width,
79
+ y: 1 - box.y - box.height,
80
+ };
81
+ }
82
+ if (normalizedRotation === 270) {
83
+ return {
84
+ ...box,
85
+ x: box.y,
86
+ y: 1 - box.x - box.width,
87
+ width: box.height,
88
+ height: box.width,
89
+ };
90
+ }
91
+ return { ...box };
92
+ };
93
+ export const serializePdfBoundingBoxes = (input) => JSON.stringify(normalizePdfBoundingBoxInput(input));
@@ -0,0 +1,42 @@
1
+ import type { FileViewerPdfBoundingBox, FileViewerViewStateChangeSource } from '@file-viewer/core';
2
+ interface PdfBoundingBoxPage {
3
+ view?: number[];
4
+ getViewport: (options: {
5
+ scale: number;
6
+ rotation: number;
7
+ }) => {
8
+ width: number;
9
+ height: number;
10
+ };
11
+ }
12
+ interface PdfBoundingBoxDocument {
13
+ getPage: (pageNumber: number) => Promise<PdfBoundingBoxPage>;
14
+ }
15
+ export interface CreatePdfBoundingBoxControllerOptions {
16
+ documentRef: Document;
17
+ targetWindow: Window;
18
+ viewerRoot: HTMLElement;
19
+ scrollContainer: HTMLElement;
20
+ initial?: FileViewerPdfBoundingBox | readonly FileViewerPdfBoundingBox[];
21
+ getDocument: () => PdfBoundingBoxDocument | null;
22
+ getPageCount: () => number;
23
+ getCurrentPage: () => number;
24
+ getRotation: () => number;
25
+ goToPage: (page: number, source: FileViewerViewStateChangeSource) => void;
26
+ suppressProgrammaticScrollEvents: () => void;
27
+ waitForPaint: (view?: Window | null) => Promise<void>;
28
+ }
29
+ export interface PdfBoundingBoxRenderOptions {
30
+ focus?: boolean;
31
+ pageNumber?: number;
32
+ source?: FileViewerViewStateChangeSource;
33
+ }
34
+ export interface PdfBoundingBoxController {
35
+ hasBoxes(): boolean;
36
+ getStateValue(): FileViewerPdfBoundingBox | FileViewerPdfBoundingBox[] | null;
37
+ set(input: unknown, options?: Pick<PdfBoundingBoxRenderOptions, 'focus' | 'source'>): Promise<boolean>;
38
+ render(options?: PdfBoundingBoxRenderOptions): Promise<void>;
39
+ destroy(): void;
40
+ }
41
+ export declare const createPdfBoundingBoxController: ({ documentRef, targetWindow, viewerRoot, scrollContainer, initial, getDocument, getPageCount, getCurrentPage, getRotation, goToPage, suppressProgrammaticScrollEvents, waitForPaint, }: CreatePdfBoundingBoxControllerOptions) => PdfBoundingBoxController;
42
+ export {};
@@ -0,0 +1,159 @@
1
+ import { normalizePdfBoundingBox, normalizePdfBoundingBoxInput, rotateNormalizedPdfBoundingBox, serializePdfBoundingBoxes, } from './pdfBbox.js';
2
+ export const createPdfBoundingBoxController = ({ documentRef, targetWindow, viewerRoot, scrollContainer, initial, getDocument, getPageCount, getCurrentPage, getRotation, goToPage, suppressProgrammaticScrollEvents, waitForPaint, }) => {
3
+ let renderVersion = 0;
4
+ let destroyed = false;
5
+ let active = normalizePdfBoundingBoxInput(initial);
6
+ let fingerprint = serializePdfBoundingBoxes(active);
7
+ const removeLayers = (pageNumber) => {
8
+ const selector = pageNumber
9
+ ? `.page[data-page-number="${pageNumber}"] > .pdf-bbox-layer`
10
+ : '.pdf-bbox-layer';
11
+ viewerRoot.querySelectorAll(selector).forEach(layer => layer.remove());
12
+ };
13
+ const getPageBox = async (pageNumber) => {
14
+ const document = getDocument();
15
+ if (!document) {
16
+ return null;
17
+ }
18
+ const page = await document.getPage(pageNumber);
19
+ const view = page.view;
20
+ if (Array.isArray(view) && view.length >= 4) {
21
+ return {
22
+ x: Number(view[0]) || 0,
23
+ y: Number(view[1]) || 0,
24
+ width: Math.abs(Number(view[2]) - Number(view[0])),
25
+ height: Math.abs(Number(view[3]) - Number(view[1])),
26
+ };
27
+ }
28
+ const viewport = page.getViewport({ scale: 1, rotation: 0 });
29
+ return { x: 0, y: 0, width: viewport.width, height: viewport.height };
30
+ };
31
+ const focusNodes = (pageNode, nodes) => {
32
+ if (!nodes.length) {
33
+ return;
34
+ }
35
+ const pageRect = pageNode.getBoundingClientRect();
36
+ const containerRect = scrollContainer.getBoundingClientRect();
37
+ const nodeRects = nodes.map(node => node.getBoundingClientRect());
38
+ const left = Math.min(...nodeRects.map(rect => rect.left));
39
+ const top = Math.min(...nodeRects.map(rect => rect.top));
40
+ const right = Math.max(...nodeRects.map(rect => rect.right));
41
+ const bottom = Math.max(...nodeRects.map(rect => rect.bottom));
42
+ suppressProgrammaticScrollEvents();
43
+ scrollContainer.scrollTop = Math.max(0, scrollContainer.scrollTop + (top + bottom) / 2 - (containerRect.top + containerRect.bottom) / 2);
44
+ if (pageRect.width > containerRect.width) {
45
+ scrollContainer.scrollLeft = Math.max(0, scrollContainer.scrollLeft + (left + right) / 2 - (containerRect.left + containerRect.right) / 2);
46
+ }
47
+ };
48
+ const render = async ({ focus = false, pageNumber, source = 'api', } = {}) => {
49
+ var _a;
50
+ const currentVersion = pageNumber ? renderVersion : ++renderVersion;
51
+ removeLayers(pageNumber);
52
+ const document = getDocument();
53
+ if (!active.length || !document || destroyed) {
54
+ return;
55
+ }
56
+ const fallbackPage = getCurrentPage() || 1;
57
+ const pageCount = getPageCount();
58
+ const grouped = new Map();
59
+ active.forEach(box => {
60
+ const page = Math.min(pageCount || Number.MAX_SAFE_INTEGER, Math.max(1, Math.round(Number(box.page) || fallbackPage)));
61
+ if (pageNumber && page !== pageNumber) {
62
+ return;
63
+ }
64
+ const boxes = grouped.get(page) || [];
65
+ boxes.push(box);
66
+ grouped.set(page, boxes);
67
+ });
68
+ const focusPage = Math.min(...grouped.keys());
69
+ if (focus && Number.isFinite(focusPage)) {
70
+ goToPage(focusPage, source);
71
+ await waitForPaint(targetWindow);
72
+ }
73
+ for (const [page, boxes] of grouped) {
74
+ if (currentVersion !== renderVersion || destroyed) {
75
+ return;
76
+ }
77
+ const pageNode = viewerRoot.querySelector(`.page[data-page-number="${page}"]`);
78
+ const pageBox = await getPageBox(page);
79
+ if (!pageNode || !pageBox || currentVersion !== renderVersion || destroyed) {
80
+ continue;
81
+ }
82
+ (_a = pageNode.querySelector(':scope > .pdf-bbox-layer')) === null || _a === void 0 ? void 0 : _a.remove();
83
+ const layer = documentRef.createElement('div');
84
+ layer.className = 'pdf-bbox-layer';
85
+ layer.dataset.pdfBboxPage = String(page);
86
+ const nodes = [];
87
+ boxes.forEach((box, index) => {
88
+ const normalized = normalizePdfBoundingBox(box, pageBox, fallbackPage);
89
+ if (!normalized) {
90
+ return;
91
+ }
92
+ const rotated = rotateNormalizedPdfBoundingBox(normalized, getRotation());
93
+ const node = documentRef.createElement('div');
94
+ node.className = 'pdf-bbox-highlight';
95
+ node.dataset.pdfBboxId = box.id || `${page}-${index}`;
96
+ node.style.left = `${rotated.x * 100}%`;
97
+ node.style.top = `${rotated.y * 100}%`;
98
+ node.style.width = `${rotated.width * 100}%`;
99
+ node.style.height = `${rotated.height * 100}%`;
100
+ if (box.color) {
101
+ node.style.setProperty('--pdf-bbox-color', box.color);
102
+ }
103
+ if (box.label) {
104
+ node.setAttribute('role', 'note');
105
+ node.setAttribute('aria-label', box.label);
106
+ }
107
+ else {
108
+ node.setAttribute('aria-hidden', 'true');
109
+ }
110
+ layer.append(node);
111
+ nodes.push(node);
112
+ });
113
+ if (!nodes.length) {
114
+ continue;
115
+ }
116
+ pageNode.append(layer);
117
+ if (focus && page === focusPage) {
118
+ await waitForPaint(targetWindow);
119
+ focusNodes(pageNode, nodes);
120
+ // PDF.js can settle dimensions one frame after fit or rotation.
121
+ await waitForPaint(targetWindow);
122
+ focusNodes(pageNode, nodes);
123
+ }
124
+ }
125
+ };
126
+ return {
127
+ hasBoxes: () => active.length > 0,
128
+ getStateValue: () => {
129
+ if (!active.length) {
130
+ return null;
131
+ }
132
+ return active.length === 1
133
+ ? { ...active[0] }
134
+ : active.map(box => ({ ...box }));
135
+ },
136
+ async set(input, options = {}) {
137
+ var _a;
138
+ const next = normalizePdfBoundingBoxInput(input);
139
+ const nextFingerprint = serializePdfBoundingBoxes(next);
140
+ if (nextFingerprint === fingerprint) {
141
+ return false;
142
+ }
143
+ active = next;
144
+ fingerprint = nextFingerprint;
145
+ await render({
146
+ focus: (_a = options.focus) !== null && _a !== void 0 ? _a : true,
147
+ source: options.source,
148
+ });
149
+ return true;
150
+ },
151
+ render,
152
+ destroy() {
153
+ destroyed = true;
154
+ renderVersion += 1;
155
+ removeLayers();
156
+ active = [];
157
+ },
158
+ };
159
+ };
@@ -0,0 +1,17 @@
1
+ import type { FileViewerViewState } from '@file-viewer/core';
2
+ export type PdfRotation = 0 | 90 | 180 | 270;
3
+ export declare const normalizePdfRotation: (rotation: number) => PdfRotation;
4
+ export declare const clampPdfScale: (scale: number, minScale: number, maxScale: number) => number;
5
+ export declare const resolvePdfViewStateUpdate: (state: FileViewerViewState, current: {
6
+ rotation: number;
7
+ scale: number;
8
+ page: number;
9
+ pageCount: number;
10
+ }, limits: {
11
+ minScale: number;
12
+ maxScale: number;
13
+ }) => {
14
+ rotation: number | undefined;
15
+ scale: number | undefined;
16
+ page: number | undefined;
17
+ };
@@ -0,0 +1,27 @@
1
+ export const normalizePdfRotation = (rotation) => {
2
+ const normalized = (((Math.round(rotation / 90) * 90) % 360) + 360) % 360;
3
+ return (normalized === 90 || normalized === 180 || normalized === 270 ? normalized : 0);
4
+ };
5
+ export const clampPdfScale = (scale, minScale, maxScale) => {
6
+ return Number(Math.min(maxScale, Math.max(minScale, scale)).toFixed(2));
7
+ };
8
+ export const resolvePdfViewStateUpdate = (state, current, limits) => {
9
+ var _a, _b;
10
+ const requestedRotation = Number(state.rotation);
11
+ const requestedScale = Number((_a = state.scale) !== null && _a !== void 0 ? _a : (_b = state.zoom) === null || _b === void 0 ? void 0 : _b.scale);
12
+ const requestedPage = Number(state.page);
13
+ const rotation = Number.isFinite(requestedRotation)
14
+ ? normalizePdfRotation(requestedRotation)
15
+ : current.rotation;
16
+ const scale = Number.isFinite(requestedScale)
17
+ ? clampPdfScale(requestedScale, limits.minScale, limits.maxScale)
18
+ : current.scale;
19
+ const page = Number.isFinite(requestedPage)
20
+ ? Math.min(current.pageCount, Math.max(1, Math.round(requestedPage)))
21
+ : current.page;
22
+ return {
23
+ rotation: rotation !== current.rotation ? rotation : undefined,
24
+ scale: Math.abs(scale - current.scale) > 0.001 ? scale : undefined,
25
+ page: page !== current.page ? page : undefined
26
+ };
27
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-pdf",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone PDF renderer plugin for File Viewer powered by PDF.js.",
@@ -54,7 +54,7 @@
54
54
  "LICENSE"
55
55
  ],
56
56
  "dependencies": {
57
- "@file-viewer/core": "2.2.4",
57
+ "@file-viewer/core": "2.2.6",
58
58
  "@fontsource-variable/noto-sans-sc": "5.2.10",
59
59
  "pdf-lib": "1.17.1",
60
60
  "pdfjs-dist": "5.4.624"