@anaralabs/lector 3.10.4 → 3.11.0

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.
@@ -452,7 +452,7 @@ declare const usePdf: <T>(selector: (state: PDFState) => T) => T;
452
452
  export { usePdf }
453
453
  export { usePdf as usePdf_alias_1 }
454
454
 
455
- export declare const usePDFDocumentContext: ({ onDocumentLoad, source, initialRotation, isZoomFitWidth, zoom, zoomOptions, documentOptions, }: usePDFDocumentParams) => {
455
+ export declare const usePDFDocumentContext: ({ onDocumentLoad, onError, source, initialRotation, isZoomFitWidth, zoom, zoomOptions, documentOptions, }: usePDFDocumentParams) => {
456
456
  initialState: InitialPDFState | null | undefined;
457
457
  };
458
458
 
@@ -465,6 +465,22 @@ export declare interface usePDFDocumentParams {
465
465
  proxy: PDFDocumentProxy;
466
466
  source: Source;
467
467
  }) => void;
468
+ /**
469
+ * Called when the document fails to load. Three failure modes:
470
+ * - `phase: "pdfjs-load"` — the PDF.js worker / runtime itself failed to load.
471
+ * - `phase: "document-load"` — `getDocument()` rejected (network error,
472
+ * parse error, password required, etc). `onDocumentLoad` has NOT fired.
473
+ * - `phase: "viewport-generation"` — the document loaded successfully and
474
+ * `onDocumentLoad` already fired, but resolving page proxies / viewports
475
+ * afterwards failed (e.g. corrupted page, transient pdf.js error).
476
+ * The callback fires in addition to the existing console.error, so existing
477
+ * consumers see no behavior change.
478
+ */
479
+ onError?: ({ error, phase, source, }: {
480
+ error: unknown;
481
+ phase: "pdfjs-load" | "document-load" | "viewport-generation";
482
+ source: Source;
483
+ }) => void;
468
484
  initialRotation?: number;
469
485
  isZoomFitWidth?: boolean;
470
486
  zoom?: number;
package/dist/index.js CHANGED
@@ -476,7 +476,9 @@ var AnnotationHighlightLayer = ({
476
476
  const pageNumber = usePDFPageNumber();
477
477
  const isPageRendered = usePdf((state) => !!state.renderedPages[pageNumber]);
478
478
  const pageAnnotations = useMemo(
479
- () => annotations.filter((a) => a.pageNumber === pageNumber),
479
+ () => annotations.filter(
480
+ (a) => a.pageNumber === pageNumber || a.highlights.some((h) => h.pageNumber === pageNumber) || a.underlines?.some((u) => u.pageNumber === pageNumber)
481
+ ),
480
482
  [annotations, pageNumber]
481
483
  );
482
484
  if (!isPageRendered) return null;
@@ -524,6 +526,92 @@ var AnnotationHighlightLayer = ({
524
526
  };
525
527
  };
526
528
  return /* @__PURE__ */ jsx("div", { className, style, children: pageAnnotations.map((annotation) => {
529
+ const pageHighlights = annotation.highlights.filter(
530
+ (h) => h.pageNumber === pageNumber
531
+ );
532
+ const pageUnderlines = annotation.underlines?.filter(
533
+ (u) => u.pageNumber === pageNumber
534
+ );
535
+ if (pageHighlights.length === 0 && (!pageUnderlines || pageUnderlines.length === 0)) {
536
+ return null;
537
+ }
538
+ const minPage = (rects) => rects?.reduce(
539
+ (m, r2) => m === null || r2.pageNumber < m ? r2.pageNumber : m,
540
+ null
541
+ ) ?? null;
542
+ const minHighlightPage = minPage(annotation.highlights);
543
+ const minUnderlinePage = minPage(annotation.underlines);
544
+ const firstPageWithRects = minHighlightPage === null ? minUnderlinePage : minUnderlinePage === null ? minHighlightPage : Math.min(minHighlightPage, minUnderlinePage);
545
+ const isPrimaryPage = firstPageWithRects === pageNumber;
546
+ const showCommentIcon = isPrimaryPage;
547
+ const rectsContent = /* @__PURE__ */ jsxs(
548
+ "div",
549
+ {
550
+ style: { cursor: "pointer" },
551
+ onClick: () => onAnnotationClick?.(annotation),
552
+ children: [
553
+ pageHighlights.map((highlight, index) => /* @__PURE__ */ jsx(
554
+ "div",
555
+ {
556
+ className: highlightClassName,
557
+ style: {
558
+ position: "absolute",
559
+ top: highlight.top,
560
+ left: highlight.left,
561
+ width: highlight.width,
562
+ height: highlight.height,
563
+ backgroundColor: annotation.color
564
+ },
565
+ "data-highlight-id": annotation.id
566
+ },
567
+ `highlight-${// biome-ignore lint/suspicious/noArrayIndexKey: <index>
568
+ index}`
569
+ )),
570
+ annotation.comment && pageUnderlines?.map((rect, index) => /* @__PURE__ */ jsx(
571
+ "div",
572
+ {
573
+ className: underlineClassName,
574
+ style: {
575
+ position: "absolute",
576
+ top: rect.top,
577
+ left: rect.left,
578
+ width: rect.width,
579
+ height: 1.1,
580
+ backgroundColor: annotation.borderColor
581
+ },
582
+ "data-comment-id": annotation.id
583
+ },
584
+ `underline-${// biome-ignore lint/suspicious/noArrayIndexKey: <index>
585
+ index}`
586
+ )),
587
+ (() => {
588
+ if (!annotation.comment || !commmentIcon || !showCommentIcon) {
589
+ return null;
590
+ }
591
+ const anchorRects = pageHighlights.length > 0 ? pageHighlights : pageUnderlines;
592
+ if (!anchorRects || anchorRects.length === 0) return null;
593
+ return /* @__PURE__ */ jsx(
594
+ "div",
595
+ {
596
+ className: commentIconClassName,
597
+ style: {
598
+ position: "absolute",
599
+ ...getCommentIconPosition(anchorRects),
600
+ color: "gray",
601
+ cursor: "pointer",
602
+ zIndex: 10
603
+ },
604
+ "data-comment-icon-id": annotation.id,
605
+ children: commmentIcon
606
+ }
607
+ );
608
+ })()
609
+ ]
610
+ }
611
+ );
612
+ if (!isPrimaryPage) {
613
+ return /* @__PURE__ */ jsx("div", { children: rectsContent }, annotation.id);
614
+ }
527
615
  return /* @__PURE__ */ jsx(
528
616
  AnnotationTooltip,
529
617
  {
@@ -547,64 +635,7 @@ var AnnotationHighlightLayer = ({
547
635
  onClose: () => {
548
636
  }
549
637
  }),
550
- children: /* @__PURE__ */ jsxs(
551
- "div",
552
- {
553
- style: { cursor: "pointer" },
554
- onClick: () => onAnnotationClick?.(annotation),
555
- children: [
556
- annotation.highlights.map((highlight, index) => /* @__PURE__ */ jsx(
557
- "div",
558
- {
559
- className: highlightClassName,
560
- style: {
561
- position: "absolute",
562
- top: highlight.top,
563
- left: highlight.left,
564
- width: highlight.width,
565
- height: highlight.height,
566
- backgroundColor: annotation.color
567
- },
568
- "data-highlight-id": annotation.id
569
- },
570
- `highlight-${// biome-ignore lint/suspicious/noArrayIndexKey: <index>
571
- index}`
572
- )),
573
- annotation.comment && annotation.underlines?.map((rect, index) => /* @__PURE__ */ jsx(
574
- "div",
575
- {
576
- className: underlineClassName,
577
- style: {
578
- position: "absolute",
579
- top: rect.top,
580
- left: rect.left,
581
- width: rect.width,
582
- height: 1.1,
583
- backgroundColor: annotation.borderColor
584
- },
585
- "data-comment-id": annotation.id
586
- },
587
- `underline-${// biome-ignore lint/suspicious/noArrayIndexKey: <index>
588
- index}`
589
- )),
590
- annotation.comment && commmentIcon && /* @__PURE__ */ jsx(
591
- "div",
592
- {
593
- className: commentIconClassName,
594
- style: {
595
- position: "absolute",
596
- ...getCommentIconPosition(annotation.highlights),
597
- color: "gray",
598
- cursor: "pointer",
599
- zIndex: 10
600
- },
601
- "data-comment-icon-id": annotation.id,
602
- children: commmentIcon
603
- }
604
- )
605
- ]
606
- }
607
- )
638
+ children: rectsContent
608
639
  },
609
640
  annotation.id
610
641
  );
@@ -1573,6 +1604,100 @@ CanvasLayer.displayName = "CanvasLayer";
1573
1604
 
1574
1605
  // src/hooks/useSelectionDimensions.tsx
1575
1606
  var MERGE_THRESHOLD = 2;
1607
+ var getTextNodeClientRects = (range) => {
1608
+ const root = range.commonAncestorContainer;
1609
+ const ownerDoc = root.ownerDocument ?? document;
1610
+ if (root.nodeType === Node.TEXT_NODE) {
1611
+ const parentElement = root.parentElement;
1612
+ return Array.from(range.getClientRects()).map((rect) => ({
1613
+ rect,
1614
+ element: parentElement
1615
+ }));
1616
+ }
1617
+ const walker = ownerDoc.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
1618
+ acceptNode(node) {
1619
+ if (!node.nodeValue || node.nodeValue.length === 0) {
1620
+ return NodeFilter.FILTER_REJECT;
1621
+ }
1622
+ try {
1623
+ return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
1624
+ } catch {
1625
+ return NodeFilter.FILTER_REJECT;
1626
+ }
1627
+ }
1628
+ });
1629
+ const results = [];
1630
+ let current = walker.nextNode();
1631
+ while (current) {
1632
+ const textNode = current;
1633
+ const parentElement = textNode.parentElement;
1634
+ const length = textNode.nodeValue?.length ?? 0;
1635
+ const isStartNode = textNode === range.startContainer;
1636
+ const isEndNode = textNode === range.endContainer;
1637
+ const start = isStartNode ? range.startOffset : 0;
1638
+ const end = isEndNode ? range.endOffset : length;
1639
+ if (end > start) {
1640
+ const sub = ownerDoc.createRange();
1641
+ try {
1642
+ sub.setStart(textNode, start);
1643
+ sub.setEnd(textNode, end);
1644
+ const subRects = sub.getClientRects();
1645
+ for (let i = 0; i < subRects.length; i++) {
1646
+ const r2 = subRects[i];
1647
+ if (r2) results.push({ rect: r2, element: parentElement });
1648
+ }
1649
+ } catch {
1650
+ } finally {
1651
+ sub.detach?.();
1652
+ }
1653
+ }
1654
+ current = walker.nextNode();
1655
+ }
1656
+ return results;
1657
+ };
1658
+ var mapSelectionRectsToLayers = (range) => {
1659
+ const clientRects = getTextNodeClientRects(range).filter(
1660
+ ({ rect }) => rect.width > 2 && rect.height > 2
1661
+ );
1662
+ const textLayerEntries = Array.from(
1663
+ document.querySelectorAll(".textLayer")
1664
+ ).map((el) => ({ el, rect: el.getBoundingClientRect() }));
1665
+ const layerForRect = (rect) => {
1666
+ const cx = rect.left + rect.width / 2;
1667
+ const cy = rect.top + rect.height / 2;
1668
+ const geomMatch = textLayerEntries.find(
1669
+ ({ rect: lr }) => cx >= lr.left - 1 && cx <= lr.right + 1 && cy >= lr.top - 1 && cy <= lr.bottom + 1
1670
+ );
1671
+ if (geomMatch) return geomMatch.el;
1672
+ const points = [
1673
+ [rect.left + 1, cy],
1674
+ [cx, cy],
1675
+ [rect.right - 1, cy],
1676
+ [cx, rect.top + 1],
1677
+ [cx, rect.bottom - 1]
1678
+ ];
1679
+ for (const [px, py] of points) {
1680
+ const el = document.elementFromPoint(px, py);
1681
+ const layer = el?.closest(".textLayer");
1682
+ if (layer) return layer;
1683
+ }
1684
+ return null;
1685
+ };
1686
+ const result = [];
1687
+ for (const { rect: clientRect, element: sourceElement } of clientRects) {
1688
+ const layer = layerForRect(clientRect);
1689
+ if (!layer) continue;
1690
+ const layerRect = layer.getBoundingClientRect();
1691
+ const fitsInLayer = clientRect.top >= layerRect.top - 1 && clientRect.bottom <= layerRect.bottom + 1 && clientRect.left >= layerRect.left - 1 && clientRect.right <= layerRect.right + 1;
1692
+ if (!fitsInLayer) continue;
1693
+ const pageNumber = parseInt(
1694
+ layer.getAttribute("data-page-number") || "1",
1695
+ 10
1696
+ );
1697
+ result.push({ clientRect, sourceElement, layer, layerRect, pageNumber });
1698
+ }
1699
+ return result;
1700
+ };
1576
1701
  var shouldMergeRects = (rect1, rect2) => {
1577
1702
  const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height);
1578
1703
  const horizontallyConnected = Math.abs(rect1.left + rect1.width - rect2.left) <= MERGE_THRESHOLD || Math.abs(rect2.left + rect2.width - rect1.left) <= MERGE_THRESHOLD || rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
@@ -1699,40 +1824,10 @@ var useSelectionDimensions = () => {
1699
1824
  const underlineRects = [];
1700
1825
  const textLayerMapHighlight = /* @__PURE__ */ new Map();
1701
1826
  const textLayerMapUnderline = /* @__PURE__ */ new Map();
1702
- const clientRects = Array.from(range.getClientRects()).filter(
1703
- (rect) => rect.width > 2 && rect.height > 2
1704
- );
1705
- clientRects.forEach((clientRect) => {
1706
- let element = document.elementFromPoint(
1707
- clientRect.left + 1,
1708
- clientRect.top + clientRect.height / 2
1709
- );
1710
- if (!element) {
1711
- element = document.elementFromPoint(
1712
- clientRect.left + clientRect.width / 2,
1713
- clientRect.top + clientRect.height / 2
1714
- );
1715
- }
1716
- if (!element) {
1717
- element = document.elementFromPoint(
1718
- clientRect.right - 1,
1719
- clientRect.top + clientRect.height / 2
1720
- );
1721
- }
1722
- if (!element) {
1723
- element = document.elementFromPoint(
1724
- clientRect.left + clientRect.width / 2,
1725
- clientRect.top + 1
1726
- );
1727
- }
1728
- if (!element) {
1729
- element = document.elementFromPoint(
1730
- clientRect.left + clientRect.width / 2,
1731
- clientRect.bottom - 1
1732
- );
1733
- }
1734
- const textLayer = element?.closest(".textLayer");
1735
- if (!textLayer) return;
1827
+ const mapped = mapSelectionRectsToLayers(range);
1828
+ const zoom = store.getState().zoom;
1829
+ mapped.forEach(({ clientRect, sourceElement, layerRect, pageNumber }) => {
1830
+ const element = sourceElement;
1736
1831
  const isSuperOrSubScript = (el) => {
1737
1832
  if (!el) return false;
1738
1833
  if (el.tagName.toLowerCase() === "sup" || el.tagName.toLowerCase() === "sub") {
@@ -1762,17 +1857,11 @@ var useSelectionDimensions = () => {
1762
1857
  }
1763
1858
  return false;
1764
1859
  };
1765
- const pageNumber = parseInt(
1766
- textLayer.getAttribute("data-page-number") || "1",
1767
- 10
1768
- );
1769
- const textLayerRect = textLayer.getBoundingClientRect();
1770
- const zoom = store.getState().zoom;
1771
1860
  const highlightRect = {
1772
1861
  width: clientRect.width / zoom,
1773
1862
  height: clientRect.height / zoom,
1774
- top: (clientRect.top - textLayerRect.top) / zoom,
1775
- left: (clientRect.left - textLayerRect.left) / zoom,
1863
+ top: (clientRect.top - layerRect.top) / zoom,
1864
+ left: (clientRect.left - layerRect.left) / zoom,
1776
1865
  pageNumber
1777
1866
  };
1778
1867
  if (!textLayerMapHighlight.has(pageNumber)) {
@@ -1786,8 +1875,8 @@ var useSelectionDimensions = () => {
1786
1875
  const underlineRect = {
1787
1876
  width: clientRect.width / zoom,
1788
1877
  height: underlineHeight / zoom,
1789
- top: (clientRect.top - textLayerRect.top + baselineOffset) / zoom,
1790
- left: (clientRect.left - textLayerRect.left) / zoom,
1878
+ top: (clientRect.top - layerRect.top + baselineOffset) / zoom,
1879
+ left: (clientRect.left - layerRect.left) / zoom,
1791
1880
  pageNumber
1792
1881
  };
1793
1882
  if (!textLayerMapUnderline.has(pageNumber)) {
@@ -1934,27 +2023,14 @@ var useSelectionDimensions = () => {
1934
2023
  const range = selection.getRangeAt(0);
1935
2024
  const highlights = [];
1936
2025
  const textLayerMap = /* @__PURE__ */ new Map();
1937
- const clientRects = Array.from(range.getClientRects()).filter(
1938
- (rect) => rect.width > 2 && rect.height > 2
1939
- );
1940
- clientRects.forEach((clientRect) => {
1941
- const element = document.elementFromPoint(
1942
- clientRect.left + 1,
1943
- clientRect.top + clientRect.height / 2
1944
- );
1945
- const textLayer = element?.closest(".textLayer");
1946
- if (!textLayer) return;
1947
- const pageNumber = parseInt(
1948
- textLayer.getAttribute("data-page-number") || "1",
1949
- 10
1950
- );
1951
- const textLayerRect = textLayer.getBoundingClientRect();
1952
- const zoom = store.getState().zoom;
2026
+ const mapped = mapSelectionRectsToLayers(range);
2027
+ const zoom = store.getState().zoom;
2028
+ mapped.forEach(({ clientRect, layerRect, pageNumber }) => {
1953
2029
  const rect = {
1954
2030
  width: clientRect.width / zoom,
1955
2031
  height: clientRect.height / zoom,
1956
- top: (clientRect.top - textLayerRect.top) / zoom,
1957
- left: (clientRect.left - textLayerRect.left) / zoom,
2032
+ top: (clientRect.top - layerRect.top) / zoom,
2033
+ left: (clientRect.left - layerRect.left) / zoom,
1958
2034
  pageNumber
1959
2035
  };
1960
2036
  if (!textLayerMap.has(pageNumber)) {
@@ -2204,8 +2280,18 @@ var ColoredHighlightComponent = ({
2204
2280
  (state) => state.deleteColoredHighlight
2205
2281
  );
2206
2282
  const [showButton, setShowButton] = useState(false);
2283
+ const pageNumber = usePDFPageNumber();
2284
+ const pageRectangles = useMemo(
2285
+ () => selection.rectangles.filter((r2) => r2.pageNumber === pageNumber),
2286
+ [selection.rectangles, pageNumber]
2287
+ );
2288
+ if (pageRectangles.length === 0) return null;
2289
+ const buttonAnchor = {
2290
+ ...selection,
2291
+ rectangles: pageRectangles
2292
+ };
2207
2293
  return /* @__PURE__ */ jsxs("div", { className: "colored-highlight", children: [
2208
- selection.rectangles.map((rect, index) => /* @__PURE__ */ jsx(
2294
+ pageRectangles.map((rect, index) => /* @__PURE__ */ jsx(
2209
2295
  "span",
2210
2296
  {
2211
2297
  onClick: () => setShowButton(!showButton),
@@ -2238,8 +2324,8 @@ var ColoredHighlightComponent = ({
2238
2324
  cursor: "pointer",
2239
2325
  boxShadow: "2px 2px 5px black",
2240
2326
  position: "absolute",
2241
- top: getMidHeightOfHighlightLine(selection),
2242
- left: getEndOfHighlight(selection),
2327
+ top: getMidHeightOfHighlightLine(buttonAnchor),
2328
+ left: getEndOfHighlight(buttonAnchor),
2243
2329
  zIndex: 30,
2244
2330
  transform: "translateY(-50%)"
2245
2331
  },
@@ -2280,7 +2366,9 @@ var ColoredHighlightLayer = ({
2280
2366
  );
2281
2367
  const addColoredHighlight = usePdf((state) => state.addColoredHighlight);
2282
2368
  const pageHighlights = useMemo(
2283
- () => highlights.filter((s) => s.pageNumber === pageNumber),
2369
+ () => highlights.filter(
2370
+ (s) => s.pageNumber === pageNumber || s.rectangles.some((r2) => r2.pageNumber === pageNumber)
2371
+ ),
2284
2372
  [highlights, pageNumber]
2285
2373
  );
2286
2374
  const handleHighlighting = useCallback(
@@ -5300,6 +5388,7 @@ function buildDocumentInitParams(source, pdfJsVersion, overrides) {
5300
5388
  }
5301
5389
  var usePDFDocumentContext = ({
5302
5390
  onDocumentLoad,
5391
+ onError,
5303
5392
  source,
5304
5393
  initialRotation = 0,
5305
5394
  isZoomFitWidth,
@@ -5312,6 +5401,8 @@ var usePDFDocumentContext = ({
5312
5401
  const [rotation] = useState(initialRotation);
5313
5402
  const documentOptionsRef = useRef(documentOptions);
5314
5403
  documentOptionsRef.current = documentOptions;
5404
+ const onErrorRef = useRef(onError);
5405
+ onErrorRef.current = onError;
5315
5406
  useEffect(() => {
5316
5407
  const generateViewports = async (pdf) => {
5317
5408
  const pageProxies = [];
@@ -5364,24 +5455,46 @@ var usePDFDocumentContext = ({
5364
5455
  }
5365
5456
  setProgress(progressEvent.loaded / progressEvent.total);
5366
5457
  };
5367
- return loadingTask.promise.then((proxy) => {
5458
+ return loadingTask.promise.then(async (proxy) => {
5368
5459
  if (isDisposed || loadingTask?.destroyed) {
5369
5460
  return;
5370
5461
  }
5371
5462
  onDocumentLoad?.({ proxy, source });
5372
5463
  setProgress(1);
5373
- return generateViewports(proxy);
5464
+ try {
5465
+ await generateViewports(proxy);
5466
+ } catch (error) {
5467
+ if (isDisposed || loadingTask?.destroyed) {
5468
+ return;
5469
+ }
5470
+ console.error("Error generating PDF viewports", error);
5471
+ onErrorRef.current?.({
5472
+ error,
5473
+ phase: "viewport-generation",
5474
+ source
5475
+ });
5476
+ }
5374
5477
  }).catch((error) => {
5375
5478
  if (isDisposed || loadingTask?.destroyed) {
5376
5479
  return;
5377
5480
  }
5378
5481
  console.error("Error loading PDF document", error);
5482
+ onErrorRef.current?.({
5483
+ error,
5484
+ phase: "document-load",
5485
+ source
5486
+ });
5379
5487
  });
5380
5488
  }).catch((error) => {
5381
5489
  if (isDisposed) {
5382
5490
  return;
5383
5491
  }
5384
5492
  console.error("Error loading PDF.js", error);
5493
+ onErrorRef.current?.({
5494
+ error,
5495
+ phase: "pdfjs-load",
5496
+ source
5497
+ });
5385
5498
  });
5386
5499
  return () => {
5387
5500
  isDisposed = true;
@@ -5400,6 +5513,7 @@ var Root = forwardRef(
5400
5513
  source,
5401
5514
  loader,
5402
5515
  onDocumentLoad,
5516
+ onError,
5403
5517
  isZoomFitWidth,
5404
5518
  zoom,
5405
5519
  zoomOptions,
@@ -5409,6 +5523,7 @@ var Root = forwardRef(
5409
5523
  const { initialState } = usePDFDocumentContext({
5410
5524
  source,
5411
5525
  onDocumentLoad,
5526
+ onError,
5412
5527
  isZoomFitWidth,
5413
5528
  zoom,
5414
5529
  zoomOptions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anaralabs/lector",
3
- "version": "3.10.4",
3
+ "version": "3.11.0",
4
4
  "description": "A headless PDF viewer for React",
5
5
  "author": "andrewdr (https://github.com/andrewdoro)",
6
6
  "license": "MIT",