@anaralabs/lector 3.14.0 → 3.14.2

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.
@@ -147,9 +147,11 @@ declare interface AnnotationTooltipProps {
147
147
  * (policy in ./scan-invert.ts).
148
148
  *
149
149
  * Wrapping installs own properties on the context instance (the prototype is
150
- * never modified). Returns a cleanup that restores the pristine context.
150
+ * never modified). Returns a cleanup that restores the pristine context; pass
151
+ * finalizeRender=true only when the render completed naturally, which lets a
152
+ * papered-but-inkless page finalize as a genuinely blank scanned page.
151
153
  */
152
- export declare function applyContextRecolor(ctx: CanvasRenderingContext2D, map: RenderColorMap, options?: RecolorContextOptions): () => void;
154
+ export declare function applyContextRecolor(ctx: CanvasRenderingContext2D, map: RenderColorMap, options?: RecolorContextOptions): (finalizeRender?: boolean) => void;
153
155
 
154
156
  /**
155
157
  * Calculates the highlight rectangles for a given text match.
@@ -342,7 +344,8 @@ export declare class LinkService {
342
344
  externalLinkEnabled: boolean;
343
345
  isInPresentationMode: boolean;
344
346
  _currentPageNumber: number;
345
- _pageNavigationCallback?: (pageNumber: number) => void;
347
+ _pageNavigationCallbacks: Set<PageNavigationCallback>;
348
+ _dispatchPageNavigation(pageNumber: number, explicitDest?: unknown[]): void;
346
349
  get pdfDocumentProxy(): PDFDocumentProxy;
347
350
  get pagesCount(): number;
348
351
  get page(): number;
@@ -357,11 +360,11 @@ export declare class LinkService {
357
360
  navigateTo(dest: string | unknown[] | Promise<unknown[]>): void;
358
361
  get rotation(): number;
359
362
  set rotation(_value: number);
360
- goToPage(_page_valuer: number): void;
363
+ goToPage(pageNumber: number): void;
361
364
  setHash(hash: string): void;
362
365
  executeSetOCGState(): void;
363
- registerPageNavigationCallback(callback: (pageNumber: number) => void): void;
364
- unregisterPageNavigationCallback(): void;
366
+ registerPageNavigationCallback(callback: PageNavigationCallback): void;
367
+ unregisterPageNavigationCallback(callback?: PageNavigationCallback): void;
365
368
  }
366
369
 
367
370
  export declare const loadPdfJs: () => Promise<pdfjsDist>;
@@ -396,6 +399,8 @@ children: ReactNode;
396
399
  pageNumber?: number;
397
400
  }) => JSX.Element>;
398
401
 
402
+ declare type PageNavigationCallback = (pageNumber: number, explicitDest?: unknown[]) => void;
403
+
399
404
  export declare const Pages: ({ children, gap, virtualizerOptions, initialOffset, onOffsetChange, ...props }: HTMLProps<HTMLDivElement> & {
400
405
  virtualizerOptions?: {
401
406
  overscan?: number;
@@ -727,7 +732,7 @@ export declare const usePdfJump: () => {
727
732
  }) => void;
728
733
  jumpToOffset: (offset: number) => void;
729
734
  jumpToHighlightRects: (rects: HighlightRect[], type: "pixels" | "percent", align?: "start" | "center", additionalOffset?: number) => void;
730
- scrollToHighlightRects: (rects: HighlightRect[], type: "pixels" | "percent", align?: "start" | "center", additionalOffset?: number) => void;
735
+ scrollToHighlightRects: (rects: HighlightRect[], type: "pixels" | "percent", align?: "start" | "center", additionalOffset?: number, behavior?: "auto" | "smooth") => boolean;
731
736
  };
732
737
 
733
738
  export declare const usePDFLinkService: () => LinkService;
package/dist/index.js CHANGED
@@ -985,15 +985,15 @@ var usePdfJump = () => {
985
985
  [virtualizer]
986
986
  );
987
987
  const scrollToHighlightRects = useCallback(
988
- (rects, type, align = "start", additionalOffset = 0) => {
989
- if (!virtualizer) return;
988
+ (rects, type, align = "start", additionalOffset = 0, behavior = "smooth") => {
989
+ if (!virtualizer || rects.length === 0) return false;
990
990
  const firstPage = Math.min(...rects.map((rect) => rect.pageNumber));
991
991
  const pageOffset = virtualizer.getOffsetForIndex(firstPage - 1, "start");
992
- if (pageOffset === null) return;
992
+ if (pageOffset === null) return false;
993
993
  const targetRect = rects.find((rect) => rect.pageNumber === firstPage);
994
- if (!targetRect) return;
994
+ if (!targetRect) return false;
995
995
  const isNumber = pageOffset?.[0] != null;
996
- if (!isNumber) return;
996
+ if (!isNumber) return false;
997
997
  const pageStart = pageOffset[0] ?? 0;
998
998
  let rectTop;
999
999
  let rectHeight;
@@ -1020,8 +1020,9 @@ var usePdfJump = () => {
1020
1020
  virtualizer.scrollToOffset(adjustedOffset, {
1021
1021
  align: "start",
1022
1022
  // Always use start when we've calculated our own centering
1023
- behavior: "smooth"
1023
+ behavior
1024
1024
  });
1025
+ return true;
1025
1026
  },
1026
1027
  [virtualizer]
1027
1028
  );
@@ -1045,7 +1046,18 @@ var LinkService = class {
1045
1046
  externalLinkEnabled = true;
1046
1047
  isInPresentationMode = false;
1047
1048
  _currentPageNumber = 0;
1048
- _pageNavigationCallback;
1049
+ // Every mounted AnnotationLayer registers a callback here. A single slot
1050
+ // breaks under page virtualization: any layer unmounting would wipe the
1051
+ // callback registered by a still-mounted layer, silently killing internal
1052
+ // link navigation. Keep them all and dispatch to the latest registrant.
1053
+ _pageNavigationCallbacks = /* @__PURE__ */ new Set();
1054
+ _dispatchPageNavigation(pageNumber, explicitDest) {
1055
+ let latest;
1056
+ for (const callback of this._pageNavigationCallbacks) {
1057
+ latest = callback;
1058
+ }
1059
+ latest?.(pageNumber, explicitDest);
1060
+ }
1049
1061
  get pdfDocumentProxy() {
1050
1062
  if (!this._pdfDocumentProxy) {
1051
1063
  throw new Error("pdfDocumentProxy is not set");
@@ -1060,9 +1072,7 @@ var LinkService = class {
1060
1072
  }
1061
1073
  set page(value) {
1062
1074
  this._currentPageNumber = value;
1063
- if (this._pageNavigationCallback) {
1064
- this._pageNavigationCallback(value);
1065
- }
1075
+ this._dispatchPageNavigation(value);
1066
1076
  }
1067
1077
  // Required for link annotations to work
1068
1078
  setDocument(pdfDocument) {
@@ -1148,9 +1158,8 @@ var LinkService = class {
1148
1158
  return;
1149
1159
  }
1150
1160
  const pageNumber = pageIndex + 1;
1151
- if (this._pageNavigationCallback) {
1152
- this._pageNavigationCallback(pageNumber);
1153
- }
1161
+ this._currentPageNumber = pageNumber;
1162
+ this._dispatchPageNavigation(pageNumber, explicitDest);
1154
1163
  }
1155
1164
  executeNamedAction() {
1156
1165
  }
@@ -1162,7 +1171,12 @@ var LinkService = class {
1162
1171
  }
1163
1172
  set rotation(_value) {
1164
1173
  }
1165
- goToPage(_page_valuer) {
1174
+ goToPage(pageNumber) {
1175
+ if (!Number.isInteger(pageNumber) || pageNumber < 1 || this.pagesCount > 0 && pageNumber > this.pagesCount) {
1176
+ return;
1177
+ }
1178
+ this._currentPageNumber = pageNumber;
1179
+ this._dispatchPageNavigation(pageNumber);
1166
1180
  }
1167
1181
  setHash(hash) {
1168
1182
  if (hash.startsWith("#page=")) {
@@ -1174,13 +1188,20 @@ var LinkService = class {
1174
1188
  }
1175
1189
  executeSetOCGState() {
1176
1190
  }
1177
- // Method to register navigation callback
1191
+ // Method to register navigation callback. Delete-then-add so re-registering
1192
+ // an already-known callback still makes it the latest registrant.
1178
1193
  registerPageNavigationCallback(callback) {
1179
- this._pageNavigationCallback = callback;
1180
- }
1181
- // Method to unregister navigation callback
1182
- unregisterPageNavigationCallback() {
1183
- this._pageNavigationCallback = void 0;
1194
+ this._pageNavigationCallbacks.delete(callback);
1195
+ this._pageNavigationCallbacks.add(callback);
1196
+ }
1197
+ // Method to unregister navigation callback. Only removes the caller's own
1198
+ // callback so one layer's cleanup can't drop another layer's registration.
1199
+ unregisterPageNavigationCallback(callback) {
1200
+ if (!callback) {
1201
+ this._pageNavigationCallbacks.clear();
1202
+ } else {
1203
+ this._pageNavigationCallbacks.delete(callback);
1204
+ }
1184
1205
  }
1185
1206
  };
1186
1207
  var useCreatePDFLinkService = (pdfDocumentProxy) => {
@@ -1218,6 +1239,48 @@ var useVisibility = ({
1218
1239
  };
1219
1240
 
1220
1241
  // src/hooks/layers/useAnnotationLayer.tsx
1242
+ var getDestinationScrollTop = (viewport, explicitDest) => {
1243
+ if (!viewport || !explicitDest) return null;
1244
+ const destType = explicitDest[1];
1245
+ const destName = destType && typeof destType === "object" && "name" in destType ? destType.name : null;
1246
+ if (destName === "FitR") {
1247
+ const [x1, y1, x2, y2] = explicitDest.slice(2, 6);
1248
+ if (typeof x1 !== "number" || typeof y1 !== "number" || typeof x2 !== "number" || typeof y2 !== "number") {
1249
+ return null;
1250
+ }
1251
+ const [, t1] = viewport.convertToViewportPoint(x1, y1);
1252
+ const [, t2] = viewport.convertToViewportPoint(x2, y2);
1253
+ const top2 = Math.min(t1, t2);
1254
+ if (typeof top2 !== "number" || Number.isNaN(top2)) return null;
1255
+ return Math.min(Math.max(top2, 0), viewport.height);
1256
+ }
1257
+ let x = null;
1258
+ let y = null;
1259
+ switch (destName) {
1260
+ case "XYZ":
1261
+ x = explicitDest[2];
1262
+ y = explicitDest[3];
1263
+ break;
1264
+ case "FitH":
1265
+ case "FitBH":
1266
+ y = explicitDest[2];
1267
+ break;
1268
+ case "FitV":
1269
+ case "FitBV":
1270
+ x = explicitDest[2];
1271
+ break;
1272
+ default:
1273
+ return null;
1274
+ }
1275
+ const sideways = viewport.rotation % 180 !== 0;
1276
+ if (typeof (sideways ? x : y) !== "number") return null;
1277
+ const [, top] = viewport.convertToViewportPoint(
1278
+ typeof x === "number" ? x : 0,
1279
+ typeof y === "number" ? y : 0
1280
+ );
1281
+ if (typeof top !== "number" || Number.isNaN(top)) return null;
1282
+ return Math.min(Math.max(top, 0), viewport.height);
1283
+ };
1221
1284
  var defaultAnnotationLayerParams = {
1222
1285
  renderForms: true,
1223
1286
  externalLinksEnabled: true,
@@ -1244,17 +1307,37 @@ var useAnnotationLayer = (params) => {
1244
1307
  useEffect(() => {
1245
1308
  linkService.externalLinkEnabled = mergedParams.externalLinksEnabled;
1246
1309
  }, [linkService, mergedParams.externalLinksEnabled]);
1247
- const { jumpToPage } = usePdfJump();
1310
+ const { jumpToPage, scrollToHighlightRects } = usePdfJump();
1311
+ const viewports = usePdf((state) => state.viewports);
1248
1312
  useEffect(() => {
1249
1313
  if (!jumpToPage) return;
1250
- const handlePageNavigation = (pageNumber2) => {
1251
- jumpToPage(pageNumber2, mergedParams.jumpOptions);
1314
+ const handlePageNavigation = (targetPageNumber, explicitDest) => {
1315
+ const top = getDestinationScrollTop(
1316
+ viewports?.[targetPageNumber - 1],
1317
+ explicitDest
1318
+ );
1319
+ const scrolled = top !== null && scrollToHighlightRects(
1320
+ [{ pageNumber: targetPageNumber, top, left: 0, width: 0, height: 0 }],
1321
+ "pixels",
1322
+ mergedParams.jumpOptions?.align === "center" ? "center" : "start",
1323
+ 0,
1324
+ mergedParams.jumpOptions?.behavior ?? "smooth"
1325
+ );
1326
+ if (!scrolled) {
1327
+ jumpToPage(targetPageNumber, mergedParams.jumpOptions);
1328
+ }
1252
1329
  };
1253
1330
  linkService.registerPageNavigationCallback(handlePageNavigation);
1254
1331
  return () => {
1255
- linkService.unregisterPageNavigationCallback();
1332
+ linkService.unregisterPageNavigationCallback(handlePageNavigation);
1256
1333
  };
1257
- }, [jumpToPage, linkService, mergedParams.jumpOptions]);
1334
+ }, [
1335
+ jumpToPage,
1336
+ scrollToHighlightRects,
1337
+ viewports,
1338
+ linkService,
1339
+ mergedParams.jumpOptions
1340
+ ]);
1258
1341
  useEffect(() => {
1259
1342
  ensureAnnotationLayerStyles();
1260
1343
  }, []);
@@ -1267,9 +1350,11 @@ var useAnnotationLayer = (params) => {
1267
1350
  const href = target.getAttribute("href") || "";
1268
1351
  if (href.startsWith("#page=")) {
1269
1352
  e.preventDefault();
1270
- const pageNumber2 = parseInt(href.substring(6), 10);
1271
- if (!Number.isNaN(pageNumber2)) {
1272
- linkService.goToPage(pageNumber2);
1353
+ if (!target.closest("[data-internal-link]")) {
1354
+ const pageNumber2 = parseInt(href.substring(6), 10);
1355
+ if (!Number.isNaN(pageNumber2)) {
1356
+ linkService.goToPage(pageNumber2);
1357
+ }
1273
1358
  }
1274
1359
  }
1275
1360
  };
@@ -1726,8 +1811,9 @@ function applyContextRecolor(ctx, map, options) {
1726
1811
  );
1727
1812
  }
1728
1813
  };
1729
- const pendingTriggerReady = () => {
1730
- if (pendingTileArea < SCAN_COVERAGE_MIN || !pendingHasInk) return false;
1814
+ const pendingTriggerReady = (requireInk = true) => {
1815
+ if (pendingTileArea < SCAN_COVERAGE_MIN) return false;
1816
+ if (requireInk && !pendingHasInk) return false;
1731
1817
  const union = [
1732
1818
  Math.min(...pendingTiles.map((t) => t.bounds[0])),
1733
1819
  Math.min(...pendingTiles.map((t) => t.bounds[1])),
@@ -1738,14 +1824,18 @@ function applyContextRecolor(ctx, map, options) {
1738
1824
  if (unionArea <= 0) return false;
1739
1825
  return pendingTileArea * (pageArea ?? 0) / unionArea >= SCAN_TILE_DENSITY_MIN;
1740
1826
  };
1827
+ const prunePendingToCleanSuffix = () => {
1828
+ if (paintSerial === pendingBaselineSerial) return;
1829
+ const kept = pendingTiles.filter((t) => t.serial === paintSerial);
1830
+ pendingTiles.length = 0;
1831
+ pendingTiles.push(...kept);
1832
+ pendingTileArea = kept.reduce((sum, t) => sum + t.area, 0);
1833
+ pendingHasInk = kept.some((t) => t.inked);
1834
+ pendingBaselineSerial = paintSerial;
1835
+ };
1741
1836
  const flushPendingTiles = (self) => {
1742
1837
  if (paintSerial !== pendingBaselineSerial) {
1743
- const kept = pendingTiles.filter((t) => t.serial === paintSerial);
1744
- pendingTiles.length = 0;
1745
- pendingTiles.push(...kept);
1746
- pendingTileArea = kept.reduce((sum, t) => sum + t.area, 0);
1747
- pendingHasInk = kept.some((t) => t.inked);
1748
- pendingBaselineSerial = paintSerial;
1838
+ prunePendingToCleanSuffix();
1749
1839
  if (pendingTiles.length === 0 || !pendingTriggerReady()) return;
1750
1840
  }
1751
1841
  for (const pending of pendingTiles) {
@@ -1933,8 +2023,22 @@ function applyContextRecolor(ctx, map, options) {
1933
2023
  clipFlags[clipFlags.length - 1] = true;
1934
2024
  return pristineClip.apply(this, args);
1935
2025
  };
1936
- const cleanup = () => {
2026
+ const finalizeBlankScanPage = () => {
2027
+ if (pendingTiles.length === 0) return;
2028
+ prunePendingToCleanSuffix();
2029
+ if (pendingTiles.length === 0) return;
2030
+ if (!pendingTriggerReady(false)) return;
2031
+ for (const pending of pendingTiles) {
2032
+ invertTileRemainder(target, pending, coveredBounds);
2033
+ coveredBounds.push(pending.bounds);
2034
+ }
2035
+ pendingTiles.length = 0;
2036
+ pendingTileArea = 0;
2037
+ pendingHasInk = false;
2038
+ };
2039
+ const cleanup = (finalizeRender = false) => {
1937
2040
  if (target[RECOLOR_CLEANUP] !== cleanup) return;
2041
+ if (finalizeRender) finalizeBlankScanPage();
1938
2042
  for (const name of [
1939
2043
  ...FILL_METHODS,
1940
2044
  ...STROKE_METHODS,
@@ -2228,19 +2332,20 @@ var useCanvasLayer = ({ background }) => {
2228
2332
  releaseBuffer();
2229
2333
  throw error;
2230
2334
  }
2231
- renderingTask.promise.finally(() => {
2232
- restoreRecolor?.();
2233
- }).then(() => {
2335
+ renderingTask.promise.then(() => {
2234
2336
  if (cancelled) {
2337
+ restoreRecolor?.();
2235
2338
  releaseBuffer();
2236
2339
  return;
2237
2340
  }
2238
2341
  const state = store.getState();
2239
2342
  const currentRecolorKey = state.colorScheme === "dark" ? `dark:${state.darkModeColors.background},${state.darkModeColors.foreground}` : void 0;
2240
2343
  if (currentRecolorKey !== recolorKey) {
2344
+ restoreRecolor?.();
2241
2345
  releaseBuffer();
2242
2346
  return;
2243
2347
  }
2348
+ restoreRecolor?.(true);
2244
2349
  if (useBuffer) {
2245
2350
  applyBackingSize();
2246
2351
  context.drawImage(buffer, 0, 0);
@@ -2272,6 +2377,7 @@ var useCanvasLayer = ({ background }) => {
2272
2377
  releaseBuffer();
2273
2378
  }
2274
2379
  }).catch((error) => {
2380
+ restoreRecolor?.();
2275
2381
  releaseBuffer();
2276
2382
  if (cancelled) return;
2277
2383
  if (error?.name === "RenderingCancelledException") return;
@@ -2584,10 +2690,9 @@ var useDetailCanvasLayer = ({
2584
2690
  const detailViewport = pdfPageProxy.getViewport({
2585
2691
  scale: effectiveScale
2586
2692
  });
2587
- if (recolor)
2588
- applyContextRecolor(bufferCtx, recolor, {
2589
- pageArea: detailViewport.width * detailViewport.height
2590
- });
2693
+ const restoreRecolor = recolor ? applyContextRecolor(bufferCtx, recolor, {
2694
+ pageArea: detailViewport.width * detailViewport.height
2695
+ }) : null;
2591
2696
  const releaseBuffer = () => {
2592
2697
  buffer.width = 0;
2593
2698
  buffer.height = 0;
@@ -2615,6 +2720,7 @@ var useDetailCanvasLayer = ({
2615
2720
  const state = store.getState();
2616
2721
  const currentRecolorKey = state.colorScheme === "dark" ? `dark:${state.darkModeColors.background},${state.darkModeColors.foreground}` : "light";
2617
2722
  if (currentRecolorKey !== recolorKey) return;
2723
+ restoreRecolor?.(true);
2618
2724
  detailCanvas.width = actualWidth;
2619
2725
  detailCanvas.height = actualHeight;
2620
2726
  detailCanvas.style.cssText = `${detailBaseStyle};display:block;opacity:1;width:${cssWidth}px;height:${cssHeight}px;transform-origin:center center;transform:translate(${rectLeft * zoom}px,${rectTop * zoom}px)`;
@@ -2686,6 +2792,7 @@ var useDetailCanvasLayer = ({
2686
2792
  clearTimeout(renderTimeoutId);
2687
2793
  }
2688
2794
  void renderingTask?.cancel();
2795
+ renderingTask = null;
2689
2796
  };
2690
2797
  }, [
2691
2798
  pdfPageProxy,
@@ -7073,6 +7180,7 @@ var useThumbnail = (pageNumber, config = {}) => {
7073
7180
  useEffect(() => {
7074
7181
  if (!isVisible && renderedProxyRef.current !== pageProxy) return;
7075
7182
  renderedProxyRef.current = pageProxy;
7183
+ let stale = false;
7076
7184
  const renderThumbnail = async () => {
7077
7185
  const canvas2 = canvasRef.current;
7078
7186
  if (!canvas2 || !pageProxy) return;
@@ -7107,6 +7215,7 @@ var useThumbnail = (pageNumber, config = {}) => {
7107
7215
  });
7108
7216
  renderTaskRef.current = renderTask;
7109
7217
  await renderTask.promise;
7218
+ restoreRecolor?.(!stale);
7110
7219
  } finally {
7111
7220
  restoreRecolor?.();
7112
7221
  }
@@ -7121,6 +7230,7 @@ var useThumbnail = (pageNumber, config = {}) => {
7121
7230
  renderThumbnail();
7122
7231
  const canvas = canvasRef.current;
7123
7232
  return () => {
7233
+ stale = true;
7124
7234
  renderTaskRef.current?.cancel();
7125
7235
  if (canvas) {
7126
7236
  canvas.width = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anaralabs/lector",
3
- "version": "3.14.0",
3
+ "version": "3.14.2",
4
4
  "description": "A headless PDF viewer for React",
5
5
  "author": "andrewdr (https://github.com/andrewdoro)",
6
6
  "license": "MIT",