@anaralabs/lector 3.13.1 → 3.13.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.
@@ -168,6 +168,8 @@ export declare const cancellable: <T extends Promise<unknown>>(promise: T) => {
168
168
  cancel: () => void;
169
169
  };
170
170
 
171
+ export declare const CANVAS_SUPERSAMPLE = 1.3;
172
+
171
173
  declare interface CanvasAndContext {
172
174
  canvas: HTMLCanvasElement | null;
173
175
  context: CanvasRenderingContext2D | null;
@@ -179,7 +181,7 @@ background?: string;
179
181
 
180
182
  export declare const clamp: (value: number, minimum: number, maximum: number) => number;
181
183
 
182
- export declare function clampScaleForPage(targetScale: number, pageWidth: number, pageHeight: number): number;
184
+ export declare function clampScaleForPage(targetScale: number, pageWidth: number, pageHeight: number, maxPixels?: number): number;
183
185
 
184
186
  declare type CollapsibleSelection = {
185
187
  highlights: HighlightRect[];
@@ -208,6 +210,10 @@ declare type ColorScheme = "light" | "dark";
208
210
  export { ColorScheme }
209
211
  export { ColorScheme as ColorScheme_alias_1 }
210
212
 
213
+ export declare function computeBaseScale(dpr: number, zoom: number, pageWidth: number, pageHeight: number): number;
214
+
215
+ export declare function computeTargetScale(dpr: number, zoom: number): number;
216
+
211
217
  /**
212
218
  * Builds a memoized color map that flips perceived lightness onto the
213
219
  * background<->foreground ramp while preserving hue and chroma (OKLab).
@@ -272,6 +278,8 @@ export declare const ensureAnnotationLayerStyles: () => void;
272
278
 
273
279
  export declare const firstMemo: <T>(first: boolean, memo: unknown, initializer: () => T) => T;
274
280
 
281
+ export declare function getCanvasPixelBudget(): number;
282
+
275
283
  export declare const getDefaultPdfJsAssetUrls: (version: string) => PdfJsAssetUrls;
276
284
 
277
285
  export declare const getEndOfHighlight: (selection: ColoredHighlight) => number;
@@ -321,6 +329,8 @@ export declare interface InitialPDFState {
321
329
  renderColorMapRef?: RenderColorMapRef;
322
330
  }
323
331
 
332
+ export declare const IS_MOBILE_DEVICE: boolean;
333
+
324
334
  export declare class LinkService {
325
335
  _pdfDocumentProxy?: PDFDocumentProxy;
326
336
  externalLinkEnabled: boolean;
package/dist/index.js CHANGED
@@ -1375,13 +1375,29 @@ AnnotationLayer.displayName = "AnnotationLayer";
1375
1375
  // src/lib/canvas-utils.ts
1376
1376
  var MAX_CANVAS_PIXELS = 16777216;
1377
1377
  var MAX_CANVAS_DIMENSION = 32767;
1378
- function clampScaleForPage(targetScale, pageWidth, pageHeight) {
1378
+ var CANVAS_SUPERSAMPLE = 1.3;
1379
+ function computeTargetScale(dpr, zoom) {
1380
+ const safeZoom = Math.max(Number.isFinite(zoom) ? zoom : 0, 0.01);
1381
+ return dpr * safeZoom * (safeZoom > 1 ? CANVAS_SUPERSAMPLE : 1);
1382
+ }
1383
+ var IS_MOBILE_DEVICE = typeof navigator !== "undefined" && (/Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
1384
+ var CANVAS_AREA_FACTOR = IS_MOBILE_DEVICE ? 1.5 : 3;
1385
+ function getCanvasPixelBudget() {
1386
+ if (typeof window === "undefined" || typeof screen === "undefined") {
1387
+ return MAX_CANVAS_PIXELS;
1388
+ }
1389
+ const dpr = window.devicePixelRatio || 1;
1390
+ const screenArea = (screen.width || 0) * (screen.height || 0) * dpr * dpr;
1391
+ if (!Number.isFinite(screenArea) || screenArea <= 0) {
1392
+ return MAX_CANVAS_PIXELS;
1393
+ }
1394
+ return Math.min(MAX_CANVAS_PIXELS, screenArea * CANVAS_AREA_FACTOR);
1395
+ }
1396
+ function clampScaleForPage(targetScale, pageWidth, pageHeight, maxPixels = MAX_CANVAS_PIXELS) {
1379
1397
  if (!targetScale) {
1380
1398
  return 0;
1381
1399
  }
1382
- const areaLimit = Math.sqrt(
1383
- MAX_CANVAS_PIXELS / Math.max(pageWidth * pageHeight, 1)
1384
- );
1400
+ const areaLimit = Math.sqrt(maxPixels / Math.max(pageWidth * pageHeight, 1));
1385
1401
  const widthLimit = MAX_CANVAS_DIMENSION / Math.max(pageWidth, 1);
1386
1402
  const heightLimit = MAX_CANVAS_DIMENSION / Math.max(pageHeight, 1);
1387
1403
  const safeScale = Math.min(
@@ -1392,6 +1408,14 @@ function clampScaleForPage(targetScale, pageWidth, pageHeight) {
1392
1408
  );
1393
1409
  return Math.max(safeScale, 0);
1394
1410
  }
1411
+ function computeBaseScale(dpr, zoom, pageWidth, pageHeight) {
1412
+ return clampScaleForPage(
1413
+ computeTargetScale(dpr, zoom),
1414
+ pageWidth,
1415
+ pageHeight,
1416
+ getCanvasPixelBudget()
1417
+ );
1418
+ }
1395
1419
 
1396
1420
  // src/lib/recolor-context.ts
1397
1421
  var RECOLOR_CLEANUP = Symbol("lectorRecolorCleanup");
@@ -1515,47 +1539,89 @@ function applyContextRecolor(ctx, map) {
1515
1539
  function removeContextRecolor(ctx) {
1516
1540
  ctx[RECOLOR_CLEANUP]?.();
1517
1541
  }
1542
+ var DPR_CAP = 3;
1543
+ var readDpr = () => typeof window === "undefined" ? 1 : Math.min(window.devicePixelRatio || 1, DPR_CAP);
1518
1544
  var useDpr = () => {
1519
- const [dpr, setDPR] = useState(
1520
- typeof window === "undefined" ? 1 : Math.min(window.devicePixelRatio, 2)
1521
- );
1545
+ const [dpr, setDpr] = useState(readDpr);
1522
1546
  useEffect(() => {
1523
1547
  if (typeof window === "undefined") {
1524
1548
  return;
1525
1549
  }
1526
- const handleDPRChange = () => {
1527
- setDPR(Math.min(window.devicePixelRatio, 2));
1550
+ let mediaQuery = null;
1551
+ let cancelled = false;
1552
+ const handleChange = () => {
1553
+ if (cancelled) {
1554
+ return;
1555
+ }
1556
+ setDpr(readDpr());
1557
+ subscribe();
1528
1558
  };
1529
- const windowMatch = window.matchMedia(
1530
- `screen and (min-resolution: ${dpr}dppx)`
1531
- );
1532
- windowMatch.addEventListener("change", handleDPRChange);
1559
+ const unlisten = (mql) => {
1560
+ if (!mql) return;
1561
+ if (typeof mql.removeEventListener === "function") {
1562
+ mql.removeEventListener("change", handleChange);
1563
+ } else {
1564
+ mql.removeListener(handleChange);
1565
+ }
1566
+ };
1567
+ const subscribe = () => {
1568
+ unlisten(mediaQuery);
1569
+ const ratio = window.devicePixelRatio || 1;
1570
+ mediaQuery = window.matchMedia(
1571
+ `(resolution: ${ratio}dppx), (-webkit-device-pixel-ratio: ${ratio})`
1572
+ );
1573
+ if (typeof mediaQuery.addEventListener === "function") {
1574
+ mediaQuery.addEventListener("change", handleChange, { once: true });
1575
+ } else {
1576
+ mediaQuery.addListener(handleChange);
1577
+ }
1578
+ };
1579
+ setDpr(readDpr());
1580
+ subscribe();
1533
1581
  return () => {
1534
- windowMatch.removeEventListener("change", handleDPRChange);
1582
+ cancelled = true;
1583
+ unlisten(mediaQuery);
1535
1584
  };
1536
- }, [dpr]);
1585
+ }, []);
1537
1586
  return dpr;
1538
1587
  };
1539
1588
 
1540
1589
  // src/hooks/layers/useCanvasLayer.tsx
1541
- var CACHE_MAX = 60;
1590
+ var CACHE_MAX_ENTRIES = 60;
1591
+ var CACHE_MAX_BYTES = (IS_MOBILE_DEVICE ? 64 : 192) * 1024 * 1024;
1592
+ var CACHE_MAX_ENTRY_BYTES = IS_MOBILE_DEVICE ? CACHE_MAX_BYTES / 3 : CACHE_MAX_BYTES / 4;
1593
+ var MAX_VARIANTS_PER_PROXY = 2;
1542
1594
  var cacheEntries = [];
1543
1595
  var canvasBitmapCache = /* @__PURE__ */ new WeakMap();
1596
+ var cacheBytes = 0;
1597
+ var documentIdFallbacks = /* @__PURE__ */ new WeakMap();
1598
+ var documentIdCounter = 0;
1599
+ function getDocumentCacheId(pdfDocumentProxy) {
1600
+ const fingerprint = pdfDocumentProxy.fingerprints?.[0];
1601
+ if (fingerprint) return fingerprint;
1602
+ let id = documentIdFallbacks.get(pdfDocumentProxy);
1603
+ if (!id) {
1604
+ documentIdCounter += 1;
1605
+ id = `anonymous-document-${documentIdCounter}`;
1606
+ documentIdFallbacks.set(pdfDocumentProxy, id);
1607
+ }
1608
+ return id;
1609
+ }
1610
+ function evictEntryAt(index) {
1611
+ const [entry] = cacheEntries.splice(index, 1);
1612
+ if (!entry) return;
1613
+ cacheBytes -= entry.bytes;
1614
+ const map = canvasBitmapCache.get(entry.proxy);
1615
+ if (map?.get(entry.key) === entry.bitmap) {
1616
+ map.delete(entry.key);
1617
+ }
1618
+ entry.bitmap.close();
1619
+ }
1544
1620
  function clearBitmapCache(documentId) {
1545
- if (documentId === void 0) {
1546
- for (const entry of cacheEntries) {
1547
- entry.bitmap.close();
1548
- canvasBitmapCache.get(entry.proxy)?.delete(entry.key);
1549
- }
1550
- cacheEntries.length = 0;
1551
- return;
1552
- }
1553
1621
  for (let i = cacheEntries.length - 1; i >= 0; i--) {
1554
- const entry = cacheEntries[i];
1555
- if (entry.docId !== documentId) continue;
1556
- entry.bitmap.close();
1557
- canvasBitmapCache.get(entry.proxy)?.delete(entry.key);
1558
- cacheEntries.splice(i, 1);
1622
+ if (documentId === void 0 || cacheEntries[i].docId === documentId) {
1623
+ evictEntryAt(i);
1624
+ }
1559
1625
  }
1560
1626
  }
1561
1627
  function cacheKey(scale, background, recolorKey) {
@@ -1578,40 +1644,49 @@ function getCachedBitmap(proxy, scale, background, recolorKey) {
1578
1644
  return bitmap;
1579
1645
  }
1580
1646
  function setCachedBitmap(docId, proxy, scale, background, recolorKey, bitmap) {
1647
+ const bytes = bitmap.width * bitmap.height * 4;
1648
+ if (bytes > CACHE_MAX_ENTRY_BYTES) {
1649
+ bitmap.close();
1650
+ return;
1651
+ }
1581
1652
  const key = cacheKey(scale, background, recolorKey);
1653
+ const existingIdx = cacheEntries.findIndex(
1654
+ (e) => e.proxy === proxy && e.key === key
1655
+ );
1656
+ if (existingIdx !== -1) {
1657
+ if (cacheEntries[existingIdx].bitmap === bitmap) return;
1658
+ evictEntryAt(existingIdx);
1659
+ }
1582
1660
  let map = canvasBitmapCache.get(proxy);
1583
1661
  if (!map) {
1584
1662
  map = /* @__PURE__ */ new Map();
1585
1663
  canvasBitmapCache.set(proxy, map);
1586
1664
  }
1587
- const existing = map.get(key);
1588
- if (existing && existing !== bitmap) {
1589
- existing.close();
1590
- const idx = cacheEntries.findIndex(
1591
- (e) => e.proxy === proxy && e.key === key
1592
- );
1593
- if (idx !== -1) cacheEntries.splice(idx, 1);
1594
- }
1595
1665
  map.set(key, bitmap);
1596
- cacheEntries.push({ docId, proxy, key, bitmap });
1597
- while (cacheEntries.length > CACHE_MAX) {
1598
- const evicted = cacheEntries.shift();
1599
- const evictedMap = canvasBitmapCache.get(evicted.proxy);
1600
- if (evictedMap?.get(evicted.key) === evicted.bitmap) {
1601
- evictedMap.delete(evicted.key);
1602
- evicted.bitmap.close();
1666
+ cacheEntries.push({ docId, proxy, key, bitmap, bytes });
1667
+ cacheBytes += bytes;
1668
+ let variants = 0;
1669
+ for (let i = cacheEntries.length - 1; i >= 0; i--) {
1670
+ if (cacheEntries[i].proxy !== proxy) continue;
1671
+ variants++;
1672
+ if (variants > MAX_VARIANTS_PER_PROXY) {
1673
+ evictEntryAt(i);
1603
1674
  }
1604
1675
  }
1676
+ while (cacheEntries.length > 0 && (cacheEntries.length > CACHE_MAX_ENTRIES || cacheBytes > CACHE_MAX_BYTES)) {
1677
+ evictEntryAt(0);
1678
+ }
1605
1679
  }
1606
1680
  var useCanvasLayer = ({ background }) => {
1607
1681
  const canvasRef = useRef(null);
1608
- const paintedKeyRef = useRef(null);
1682
+ const paintedRef = useRef(null);
1609
1683
  const pageNumber = usePDFPageNumber();
1610
1684
  const store = PDFStore.useContext();
1611
1685
  const dpr = useDpr();
1612
1686
  const bouncyZoom = usePdf((state) => state.zoom);
1613
1687
  const isResizing = usePdf((state) => state.isResizing);
1614
- const docId = usePdf((state) => state.pdfDocumentProxy.fingerprints[0] ?? "");
1688
+ const isPinching = usePdf((state) => state.isPinching);
1689
+ const docId = usePdf((state) => getDocumentCacheId(state.pdfDocumentProxy));
1615
1690
  const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
1616
1691
  const markPageRendered = usePdf((state) => state.markPageRendered);
1617
1692
  const unmarkPageRendered = usePdf((state) => state.unmarkPageRendered);
@@ -1627,29 +1702,52 @@ var useCanvasLayer = ({ background }) => {
1627
1702
  const pageWidth = baseViewport.width;
1628
1703
  const pageHeight = baseViewport.height;
1629
1704
  const contentKey = `${background ?? "white"}|${recolorKey ?? ""}`;
1630
- if (isResizing && paintedKeyRef.current === contentKey) {
1705
+ const baseScale = computeBaseScale(dpr, zoom, pageWidth, pageHeight);
1706
+ const hasCurrentFrame = paintedRef.current?.proxy === pdfPageProxy && paintedRef.current?.key === contentKey;
1707
+ const geometryZoom = Math.max(Number.isFinite(zoom) ? zoom : 0, 0.01);
1708
+ const applyGeometry = () => {
1709
+ if (geometryZoom === 1) {
1710
+ baseCanvas.style.width = `${pageWidth}px`;
1711
+ baseCanvas.style.height = `${pageHeight}px`;
1712
+ baseCanvas.style.transform = "translate(0px, 0px)";
1713
+ } else {
1714
+ baseCanvas.style.width = `${pageWidth * geometryZoom}px`;
1715
+ baseCanvas.style.height = `${pageHeight * geometryZoom}px`;
1716
+ baseCanvas.style.transform = `scale3d(${1 / geometryZoom},${1 / geometryZoom},1)`;
1717
+ baseCanvas.style.transformOrigin = "0 0";
1718
+ }
1719
+ };
1720
+ if (hasCurrentFrame && paintedRef.current?.scale === baseScale) {
1721
+ applyGeometry();
1722
+ return;
1723
+ }
1724
+ if ((isResizing || isPinching) && hasCurrentFrame) {
1631
1725
  baseCanvas.style.visibility = "";
1632
- baseCanvas.style.width = `${pageWidth}px`;
1633
- baseCanvas.style.height = `${pageHeight}px`;
1634
1726
  return;
1635
1727
  }
1636
- paintedKeyRef.current = null;
1637
- const targetBaseScale = dpr * Math.min(zoom, 1);
1638
- const baseScale = clampScaleForPage(targetBaseScale, pageWidth, pageHeight);
1639
- baseCanvas.width = Math.floor(pageWidth * baseScale);
1640
- baseCanvas.height = Math.floor(pageHeight * baseScale);
1641
1728
  baseCanvas.style.position = "absolute";
1642
1729
  baseCanvas.style.top = "0";
1643
1730
  baseCanvas.style.left = "0";
1644
- baseCanvas.style.width = `${pageWidth}px`;
1645
- baseCanvas.style.height = `${pageHeight}px`;
1646
- baseCanvas.style.transform = "translate(0px, 0px)";
1731
+ applyGeometry();
1647
1732
  baseCanvas.style.zIndex = "0";
1648
1733
  baseCanvas.style.pointerEvents = "none";
1649
1734
  baseCanvas.style.backgroundColor = recolor ? recolor(background ?? "#ffffff") : background ?? "white";
1650
- baseCanvas.style.visibility = "";
1735
+ const backingWidth = Math.floor(pageWidth * baseScale);
1736
+ const backingHeight = Math.floor(pageHeight * baseScale);
1651
1737
  const context = baseCanvas.getContext("2d");
1652
- if (!context) return;
1738
+ if (!context) {
1739
+ if (!hasCurrentFrame) {
1740
+ paintedRef.current = null;
1741
+ baseCanvas.width = backingWidth;
1742
+ baseCanvas.height = backingHeight;
1743
+ baseCanvas.style.visibility = "hidden";
1744
+ }
1745
+ return;
1746
+ }
1747
+ const applyBackingSize = () => {
1748
+ baseCanvas.width = backingWidth;
1749
+ baseCanvas.height = backingHeight;
1750
+ };
1653
1751
  const cached = getCachedBitmap(
1654
1752
  pdfPageProxy,
1655
1753
  baseScale,
@@ -1657,23 +1755,41 @@ var useCanvasLayer = ({ background }) => {
1657
1755
  recolorKey
1658
1756
  );
1659
1757
  if (cached) {
1758
+ applyBackingSize();
1660
1759
  context.drawImage(cached, 0, 0);
1760
+ baseCanvas.style.visibility = "";
1661
1761
  markPageRendered(pageNumber);
1662
- paintedKeyRef.current = contentKey;
1762
+ paintedRef.current = {
1763
+ proxy: pdfPageProxy,
1764
+ key: contentKey,
1765
+ scale: baseScale
1766
+ };
1663
1767
  return;
1664
1768
  }
1665
- context.setTransform(1, 0, 0, 1, 0, 0);
1666
- context.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
1667
- baseCanvas.style.visibility = "hidden";
1769
+ const hideUntilPainted = () => {
1770
+ paintedRef.current = null;
1771
+ applyBackingSize();
1772
+ baseCanvas.style.visibility = "hidden";
1773
+ };
1774
+ if (!hasCurrentFrame) {
1775
+ hideUntilPainted();
1776
+ }
1668
1777
  let cancelled = false;
1669
1778
  const viewport = pdfPageProxy.getViewport({ scale: baseScale });
1670
1779
  const buffer = document.createElement("canvas");
1671
- buffer.width = baseCanvas.width;
1672
- buffer.height = baseCanvas.height;
1780
+ buffer.width = backingWidth;
1781
+ buffer.height = backingHeight;
1673
1782
  const bufferCtx = buffer.getContext("2d");
1674
1783
  const useBuffer = bufferCtx !== null;
1784
+ if (!useBuffer && hasCurrentFrame) {
1785
+ hideUntilPainted();
1786
+ }
1675
1787
  const renderCanvas = useBuffer ? buffer : baseCanvas;
1676
1788
  const renderCtx = bufferCtx ?? context;
1789
+ const releaseBuffer = () => {
1790
+ buffer.width = 0;
1791
+ buffer.height = 0;
1792
+ };
1677
1793
  let restoreRecolor = null;
1678
1794
  if (recolor) {
1679
1795
  restoreRecolor = applyContextRecolor(renderCtx, recolor);
@@ -1690,12 +1806,9 @@ var useCanvasLayer = ({ background }) => {
1690
1806
  });
1691
1807
  } catch (error) {
1692
1808
  restoreRecolor?.();
1809
+ releaseBuffer();
1693
1810
  throw error;
1694
1811
  }
1695
- const releaseBuffer = () => {
1696
- buffer.width = 0;
1697
- buffer.height = 0;
1698
- };
1699
1812
  renderingTask.promise.finally(() => {
1700
1813
  restoreRecolor?.();
1701
1814
  }).then(() => {
@@ -1710,13 +1823,17 @@ var useCanvasLayer = ({ background }) => {
1710
1823
  return;
1711
1824
  }
1712
1825
  if (useBuffer) {
1713
- context.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
1826
+ applyBackingSize();
1714
1827
  context.drawImage(buffer, 0, 0);
1715
1828
  }
1716
1829
  baseCanvas.style.visibility = "";
1717
1830
  markPageRendered(pageNumber);
1718
- paintedKeyRef.current = contentKey;
1719
- if (typeof createImageBitmap !== "undefined") {
1831
+ paintedRef.current = {
1832
+ proxy: pdfPageProxy,
1833
+ key: contentKey,
1834
+ scale: baseScale
1835
+ };
1836
+ if (typeof createImageBitmap !== "undefined" && renderCanvas.width * renderCanvas.height * 4 <= CACHE_MAX_ENTRY_BYTES) {
1720
1837
  createImageBitmap(renderCanvas).then((bitmap) => {
1721
1838
  if (cancelled) {
1722
1839
  bitmap.close();
@@ -1752,6 +1869,7 @@ var useCanvasLayer = ({ background }) => {
1752
1869
  dpr,
1753
1870
  zoom,
1754
1871
  isResizing,
1872
+ isPinching,
1755
1873
  pageNumber,
1756
1874
  markPageRendered,
1757
1875
  docId,
@@ -1767,6 +1885,7 @@ var useCanvasLayer = ({ background }) => {
1767
1885
  canvas.width = 0;
1768
1886
  canvas.height = 0;
1769
1887
  }
1888
+ paintedRef.current = null;
1770
1889
  };
1771
1890
  }, [pageNumber, unmarkPageRendered]);
1772
1891
  return {
@@ -1839,7 +1958,12 @@ var subscribeToViewportInvalidation = (viewport, callback) => {
1839
1958
 
1840
1959
  // src/hooks/layers/useDetailCanvasLayer.tsx
1841
1960
  var DETAIL_RENDER_IDLE_MS = 80;
1842
- var ZOOM_SETTLE_MS = 250;
1961
+ var SCROLL_POLL_MS = 160;
1962
+ var ZOOM_DEBOUNCE_MS = 200;
1963
+ var ZOOM_SETTLE_MS = ZOOM_DEBOUNCE_MS + 50;
1964
+ var OVERSCAN_X = 0.25;
1965
+ var OVERSCAN_Y = 0.5;
1966
+ var RELEASE_HIDDEN_MS = 2e3;
1843
1967
  var useDetailCanvasLayer = ({
1844
1968
  background,
1845
1969
  baseCanvasRef
@@ -1850,15 +1974,15 @@ var useDetailCanvasLayer = ({
1850
1974
  const dpr = useDpr();
1851
1975
  const store = PDFStore.useContext();
1852
1976
  const bouncyZoom = usePdf((state) => state.zoom);
1853
- const isPinching = usePdf((state) => state.isPinching);
1854
1977
  const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
1855
1978
  const viewportRef = usePdf((state) => state.viewportRef);
1856
1979
  const colorScheme = usePdf((state) => state.colorScheme);
1857
1980
  const darkModeColors = usePdf((state) => state.darkModeColors);
1858
1981
  const recolor = colorScheme === "dark" ? createDarkModeColorMap(darkModeColors) : null;
1859
1982
  const recolorKey = recolor ? `dark:${darkModeColors.background},${darkModeColors.foreground}` : "light";
1860
- const paintedKeyRef = useRef(null);
1861
- const [zoom] = useDebounce(bouncyZoom, 200);
1983
+ const paintedRef = useRef(null);
1984
+ const releaseTimerRef = useRef(null);
1985
+ const [zoom] = useDebounce(bouncyZoom, ZOOM_DEBOUNCE_MS);
1862
1986
  const lastZoomChangeRef = useRef(
1863
1987
  store.getState().isPinching ? performance.now() : 0
1864
1988
  );
@@ -1888,39 +2012,52 @@ var useDetailCanvasLayer = ({
1888
2012
  }
1889
2013
  let renderingTask = null;
1890
2014
  let renderTimeoutId = null;
1891
- const bgColor = recolor ? recolor(background ?? "#ffffff") : background ?? "white";
1892
2015
  const contentKey = `${background ?? "white"}|${recolorKey}`;
1893
- const detailBaseStyle = `position:absolute;top:0;left:0;pointer-events:none;z-index:1;background-color:${bgColor}`;
2016
+ const detailBaseStyle = "position:absolute;top:0;left:0;pointer-events:none;z-index:1";
1894
2017
  const hideDetailCanvas = () => {
1895
2018
  renderingTask?.cancel();
2019
+ renderingTask = null;
1896
2020
  detailCanvas.style.cssText = `${detailBaseStyle};display:block;opacity:0`;
1897
2021
  container.style.cssText = "";
2022
+ paintedRef.current = null;
2023
+ if (releaseTimerRef.current === null) {
2024
+ releaseTimerRef.current = setTimeout(() => {
2025
+ releaseTimerRef.current = null;
2026
+ detailCanvas.width = 1;
2027
+ detailCanvas.height = 1;
2028
+ }, RELEASE_HIDDEN_MS);
2029
+ }
1898
2030
  };
2031
+ const pageDims = pageDimsRef.current;
2032
+ if (!pageDims) {
2033
+ hideDetailCanvas();
2034
+ return;
2035
+ }
2036
+ const { width: pageWidth, height: pageHeight } = pageDims;
2037
+ const targetDetailScale = computeTargetScale(dpr, zoom);
2038
+ const baseScale = computeBaseScale(dpr, zoom, pageWidth, pageHeight);
2039
+ const needsDetail = targetDetailScale - baseScale > 1e-3;
1899
2040
  const renderDetailCanvas = () => {
2041
+ if (targetDetailScale - computeBaseScale(dpr, zoom, pageWidth, pageHeight) <= 1e-3) {
2042
+ hideDetailCanvas();
2043
+ return;
2044
+ }
1900
2045
  const virtualizer = store.getState().virtualizer;
1901
2046
  if (virtualizer?.isScrolling) {
1902
- scheduleRender(160);
2047
+ scheduleRender(SCROLL_POLL_MS);
1903
2048
  return;
1904
2049
  }
1905
- if (store.getState().isPinching || performance.now() - lastZoomChangeRef.current < ZOOM_SETTLE_MS) {
1906
- scheduleRender(160);
2050
+ const settleElapsed = performance.now() - lastZoomChangeRef.current;
2051
+ if (store.getState().isPinching || settleElapsed < ZOOM_SETTLE_MS) {
2052
+ scheduleRender(Math.max(50, ZOOM_SETTLE_MS - settleElapsed + 10));
1907
2053
  return;
1908
2054
  }
1909
- const pageContainer = baseCanvasRef.current?.parentElement ?? null;
1910
- if (!pageContainer) {
1911
- hideDetailCanvas();
2055
+ if (store.getState().zoom !== zoom) {
2056
+ scheduleRender(ZOOM_SETTLE_MS);
1912
2057
  return;
1913
2058
  }
1914
- const { width: pageWidth, height: pageHeight } = pageDimsRef.current;
1915
- const targetDetailScale = dpr * zoom * 1.3;
1916
- const baseTargetScale = dpr * Math.min(zoom, 1);
1917
- const baseScale = clampScaleForPage(
1918
- baseTargetScale,
1919
- pageWidth,
1920
- pageHeight
1921
- );
1922
- const needsDetail = zoom > 1 && targetDetailScale - baseScale > 1e-3;
1923
- if (isPinching || !needsDetail) {
2059
+ const pageContainer = baseCanvasRef.current?.parentElement ?? null;
2060
+ if (!pageContainer) {
1924
2061
  hideDetailCanvas();
1925
2062
  return;
1926
2063
  }
@@ -1948,17 +2085,72 @@ var useDetailCanvasLayer = ({
1948
2085
  hideDetailCanvas();
1949
2086
  return;
1950
2087
  }
2088
+ const budget = getCanvasPixelBudget();
2089
+ let effectiveScale = clampScaleForPage(
2090
+ targetDetailScale,
2091
+ visibleWidth,
2092
+ visibleHeight,
2093
+ budget
2094
+ );
2095
+ const padX = OVERSCAN_X * viewportWidth;
2096
+ const padY = OVERSCAN_Y * viewportHeight;
2097
+ const maxRectWidth = Math.min(pageWidth, visibleWidth + 2 * padX);
2098
+ const maxRectHeight = Math.min(pageHeight, visibleHeight + 2 * padY);
2099
+ effectiveScale = Math.min(
2100
+ effectiveScale,
2101
+ MAX_CANVAS_DIMENSION / Math.max(maxRectWidth, 1),
2102
+ MAX_CANVAS_DIMENSION / Math.max(maxRectHeight, 1)
2103
+ );
2104
+ const eps = 1 / effectiveScale;
2105
+ const painted = paintedRef.current;
2106
+ if (painted && painted.proxy === pdfPageProxy && painted.key === contentKey && painted.scale === effectiveScale && visibleLeft >= painted.left - eps && visibleTop >= painted.top - eps && visibleRight <= painted.left + painted.width + eps && visibleBottom <= painted.top + painted.height + eps) {
2107
+ renderingTask?.cancel();
2108
+ renderingTask = null;
2109
+ return;
2110
+ }
2111
+ const maxArea = budget / (effectiveScale * effectiveScale);
2112
+ const a = 4 * padX * padY;
2113
+ const b = 2 * (visibleWidth * padY + visibleHeight * padX);
2114
+ const c = visibleWidth * visibleHeight - maxArea;
2115
+ let padFactor = 1;
2116
+ if (a > 0) {
2117
+ padFactor = Math.min(
2118
+ 1,
2119
+ Math.max(
2120
+ 0,
2121
+ (-b + Math.sqrt(Math.max(b * b - 4 * a * c, 0))) / (2 * a)
2122
+ )
2123
+ );
2124
+ } else if (b > 0) {
2125
+ padFactor = Math.min(1, Math.max(0, -c / b));
2126
+ }
2127
+ const rectLeft = Math.max(0, visibleLeft - padX * padFactor);
2128
+ const rectTop = Math.max(0, visibleTop - padY * padFactor);
2129
+ const rectRight = Math.min(pageWidth, visibleRight + padX * padFactor);
2130
+ const rectBottom = Math.min(pageHeight, visibleBottom + padY * padFactor);
2131
+ const rectWidth = rectRight - rectLeft;
2132
+ const rectHeight = rectBottom - rectTop;
1951
2133
  renderingTask?.cancel();
1952
- const pdfOffsetX = visibleLeft;
1953
- const pdfOffsetY = visibleTop;
1954
- const effectiveScale = targetDetailScale;
1955
- const actualWidth = visibleWidth * effectiveScale;
1956
- const actualHeight = visibleHeight * effectiveScale;
2134
+ if (releaseTimerRef.current !== null) {
2135
+ clearTimeout(releaseTimerRef.current);
2136
+ releaseTimerRef.current = null;
2137
+ }
2138
+ const actualWidth = Math.max(1, Math.floor(rectWidth * effectiveScale));
2139
+ const actualHeight = Math.max(1, Math.floor(rectHeight * effectiveScale));
1957
2140
  const buffer = document.createElement("canvas");
1958
2141
  buffer.width = actualWidth;
1959
2142
  buffer.height = actualHeight;
1960
2143
  const bufferCtx = buffer.getContext("2d");
1961
2144
  if (!bufferCtx) {
2145
+ buffer.width = 0;
2146
+ buffer.height = 0;
2147
+ hideDetailCanvas();
2148
+ if (releaseTimerRef.current !== null) {
2149
+ clearTimeout(releaseTimerRef.current);
2150
+ releaseTimerRef.current = null;
2151
+ }
2152
+ detailCanvas.width = 1;
2153
+ detailCanvas.height = 1;
1962
2154
  return;
1963
2155
  }
1964
2156
  bufferCtx.setTransform(1, 0, 0, 1, 0, 0);
@@ -1967,21 +2159,35 @@ var useDetailCanvasLayer = ({
1967
2159
  0,
1968
2160
  0,
1969
2161
  1,
1970
- -pdfOffsetX * effectiveScale,
1971
- -pdfOffsetY * effectiveScale
2162
+ -rectLeft * effectiveScale,
2163
+ -rectTop * effectiveScale
1972
2164
  ];
1973
2165
  const detailViewport = pdfPageProxy.getViewport({
1974
2166
  scale: effectiveScale
1975
2167
  });
1976
2168
  if (recolor) applyContextRecolor(bufferCtx, recolor);
1977
- const currentRenderingTask = pdfPageProxy.render({
1978
- canvas: buffer,
1979
- canvasContext: bufferCtx,
1980
- viewport: detailViewport,
1981
- background,
1982
- transform
1983
- });
2169
+ const releaseBuffer = () => {
2170
+ buffer.width = 0;
2171
+ buffer.height = 0;
2172
+ };
2173
+ let currentRenderingTask;
2174
+ try {
2175
+ currentRenderingTask = pdfPageProxy.render({
2176
+ canvas: buffer,
2177
+ canvasContext: bufferCtx,
2178
+ viewport: detailViewport,
2179
+ background,
2180
+ transform
2181
+ });
2182
+ } catch (error) {
2183
+ releaseBuffer();
2184
+ hideDetailCanvas();
2185
+ console.error("PDF detail render error:", error);
2186
+ return;
2187
+ }
1984
2188
  renderingTask = currentRenderingTask;
2189
+ const cssWidth = actualWidth / effectiveScale * zoom;
2190
+ const cssHeight = actualHeight / effectiveScale * zoom;
1985
2191
  currentRenderingTask.promise.then(() => {
1986
2192
  if (renderingTask !== currentRenderingTask) return;
1987
2193
  const state = store.getState();
@@ -1989,13 +2195,36 @@ var useDetailCanvasLayer = ({
1989
2195
  if (currentRecolorKey !== recolorKey) return;
1990
2196
  detailCanvas.width = actualWidth;
1991
2197
  detailCanvas.height = actualHeight;
1992
- detailCanvas.style.cssText = `${detailBaseStyle};display:block;opacity:1;width:${visibleWidth * zoom}px;height:${visibleHeight * zoom}px;transform-origin:center center;transform:translate(${visibleLeft * zoom}px,${visibleTop * zoom}px)`;
2198
+ 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)`;
1993
2199
  container.style.cssText = `transform:scale3d(${1 / zoom},${1 / zoom},1);transform-origin:0 0`;
1994
2200
  const ctx = detailCanvas.getContext("2d");
1995
- if (ctx) {
2201
+ if (!ctx) {
2202
+ hideDetailCanvas();
2203
+ return;
2204
+ }
2205
+ try {
1996
2206
  ctx.drawImage(buffer, 0, 0);
2207
+ } catch (error) {
2208
+ hideDetailCanvas();
2209
+ console.error("PDF detail canvas blit error:", error);
2210
+ return;
1997
2211
  }
1998
- paintedKeyRef.current = contentKey;
2212
+ paintedRef.current = {
2213
+ proxy: pdfPageProxy,
2214
+ key: contentKey,
2215
+ scale: effectiveScale,
2216
+ left: rectLeft,
2217
+ top: rectTop,
2218
+ // The truly painted extent comes from the floored backing,
2219
+ // not the requested rect.
2220
+ width: actualWidth / effectiveScale,
2221
+ height: actualHeight / effectiveScale
2222
+ };
2223
+ if (releaseTimerRef.current !== null) {
2224
+ clearTimeout(releaseTimerRef.current);
2225
+ releaseTimerRef.current = null;
2226
+ }
2227
+ scheduleRender();
1999
2228
  }).catch((error) => {
2000
2229
  if (error.name === "RenderingCancelledException") {
2001
2230
  return;
@@ -2005,15 +2234,13 @@ var useDetailCanvasLayer = ({
2005
2234
  if (renderingTask === currentRenderingTask) {
2006
2235
  renderingTask = null;
2007
2236
  }
2008
- buffer.width = 0;
2009
- buffer.height = 0;
2237
+ releaseBuffer();
2010
2238
  });
2011
2239
  };
2012
2240
  const scheduleRender = (delay = DETAIL_RENDER_IDLE_MS) => {
2013
2241
  if (renderTimeoutId !== null) {
2014
2242
  clearTimeout(renderTimeoutId);
2015
2243
  }
2016
- renderingTask?.cancel();
2017
2244
  renderTimeoutId = setTimeout(() => {
2018
2245
  renderTimeoutId = null;
2019
2246
  renderDetailCanvas();
@@ -2023,12 +2250,11 @@ var useDetailCanvasLayer = ({
2023
2250
  scrollContainer,
2024
2251
  scheduleRender
2025
2252
  );
2026
- if (zoom <= 1 || isPinching) {
2253
+ if (!needsDetail) {
2027
2254
  hideDetailCanvas();
2028
2255
  } else {
2029
- if (paintedKeyRef.current !== null && paintedKeyRef.current !== contentKey) {
2256
+ if (paintedRef.current !== null && (paintedRef.current.key !== contentKey || paintedRef.current.proxy !== pdfPageProxy)) {
2030
2257
  hideDetailCanvas();
2031
- paintedKeyRef.current = null;
2032
2258
  }
2033
2259
  scheduleRender(0);
2034
2260
  }
@@ -2042,7 +2268,6 @@ var useDetailCanvasLayer = ({
2042
2268
  }, [
2043
2269
  pdfPageProxy,
2044
2270
  zoom,
2045
- isPinching,
2046
2271
  background,
2047
2272
  dpr,
2048
2273
  viewportRef,
@@ -2054,10 +2279,15 @@ var useDetailCanvasLayer = ({
2054
2279
  useEffect(() => {
2055
2280
  const canvas = detailCanvasRef.current;
2056
2281
  return () => {
2282
+ if (releaseTimerRef.current !== null) {
2283
+ clearTimeout(releaseTimerRef.current);
2284
+ releaseTimerRef.current = null;
2285
+ }
2057
2286
  if (canvas) {
2058
2287
  canvas.width = 1;
2059
2288
  canvas.height = 1;
2060
2289
  }
2290
+ paintedRef.current = null;
2061
2291
  };
2062
2292
  }, []);
2063
2293
  return {
@@ -5639,10 +5869,10 @@ var useViewportContainer = ({
5639
5869
  const transformations = useRef({
5640
5870
  translateX: 0,
5641
5871
  translateY: 0,
5642
- zoom
5872
+ zoom: 1
5643
5873
  });
5644
5874
  const zoomRafRef = useRef(null);
5645
- const lastPushedZoomRef = useRef(zoom);
5875
+ const lastPushedZoomRef = useRef(null);
5646
5876
  const updateTransform = useCallback(
5647
5877
  (zoomUpdate) => {
5648
5878
  if (!elementRef.current || !containerRef.current || !elementWrapperRef.current) {
@@ -5679,8 +5909,14 @@ var useViewportContainer = ({
5679
5909
  return;
5680
5910
  }
5681
5911
  if (zoom === lastPushedZoomRef.current) {
5912
+ lastPushedZoomRef.current = null;
5682
5913
  return;
5683
5914
  }
5915
+ if (zoomRafRef.current !== null) {
5916
+ cancelAnimationFrame(zoomRafRef.current);
5917
+ zoomRafRef.current = null;
5918
+ }
5919
+ lastPushedZoomRef.current = null;
5684
5920
  const prevZoom = transformations.current.zoom;
5685
5921
  if (!prevZoom || !Number.isFinite(prevZoom)) {
5686
5922
  transformations.current = {
@@ -5910,7 +6146,13 @@ var Pages = ({
5910
6146
  overscan: virtualizerOptions?.overscan ?? 0,
5911
6147
  scrollToFn,
5912
6148
  gap,
5913
- initialOffset
6149
+ initialOffset,
6150
+ // Never trust scrollend alone to reset isScrolling: a single missed
6151
+ // event (a documented browser flake — TanStack flipped this default to
6152
+ // false in later 3.x) would pin isScrolling=true forever, and every
6153
+ // consumer gated on it (text layer, detail canvas) would stop painting.
6154
+ // false keeps the isScrollingResetDelay debounce active as a fallback.
6155
+ useScrollendEvent: false
5914
6156
  });
5915
6157
  useEffect(() => {
5916
6158
  if (onOffsetChange && virtualizer.scrollOffset)
@@ -6337,7 +6579,8 @@ var Root = forwardRef(
6337
6579
  const linkService = useCreatePDFLinkService(
6338
6580
  initialState?.pdfDocumentProxy ?? null
6339
6581
  );
6340
- const documentId = initialState?.pdfDocumentProxy?.fingerprints?.[0];
6582
+ const pdfDocumentProxy = initialState?.pdfDocumentProxy ?? null;
6583
+ const documentId = pdfDocumentProxy ? getDocumentCacheId(pdfDocumentProxy) : null;
6341
6584
  useEffect(() => {
6342
6585
  if (!documentId) return;
6343
6586
  return () => {
@@ -6396,6 +6639,7 @@ var useThumbnail = (pageNumber, config = {}) => {
6396
6639
  const containerRef = useRef(null);
6397
6640
  const canvasRef = useRef(null);
6398
6641
  const renderTaskRef = useRef(null);
6642
+ const renderedProxyRef = useRef(null);
6399
6643
  const pageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
6400
6644
  const { visible } = useVisibility({ elementRef: containerRef });
6401
6645
  const [debouncedVisible] = useDebounce(visible, 50);
@@ -6405,6 +6649,8 @@ var useThumbnail = (pageNumber, config = {}) => {
6405
6649
  const recolor = colorScheme === "dark" ? createDarkModeColorMap(darkModeColors) : null;
6406
6650
  const isVisible = isFirstPage || debouncedVisible;
6407
6651
  useEffect(() => {
6652
+ if (!isVisible && renderedProxyRef.current !== pageProxy) return;
6653
+ renderedProxyRef.current = pageProxy;
6408
6654
  const renderThumbnail = async () => {
6409
6655
  const canvas2 = canvasRef.current;
6410
6656
  if (!canvas2 || !pageProxy) return;
@@ -6413,7 +6659,11 @@ var useThumbnail = (pageNumber, config = {}) => {
6413
6659
  renderTaskRef.current.cancel();
6414
6660
  }
6415
6661
  const viewport = pageProxy.getViewport({ scale: 1 });
6416
- const scale = Math.min(maxWidth / viewport.width, maxHeight / viewport.height) * (isVisible ? dpr : 0.5);
6662
+ const scale = clampScaleForPage(
6663
+ Math.min(maxWidth / viewport.width, maxHeight / viewport.height) * (isVisible ? dpr : 0.5),
6664
+ viewport.width,
6665
+ viewport.height
6666
+ );
6417
6667
  const scaledViewport = pageProxy.getViewport({ scale });
6418
6668
  canvas2.width = scaledViewport.width;
6419
6669
  canvas2.height = scaledViewport.height;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anaralabs/lector",
3
- "version": "3.13.1",
3
+ "version": "3.13.2",
4
4
  "description": "A headless PDF viewer for React",
5
5
  "author": "andrewdr (https://github.com/andrewdoro)",
6
6
  "license": "MIT",