UNPKG

@anaralabs/lector

Version:

Headless PDF viewer for React

1,549 lines (1,534 loc) 174 kB
'use client'; import React, { createContext, forwardRef, useRef, useState, useCallback, useContext, useEffect, cloneElement, createRef, useMemo, useLayoutEffect } from 'react'; import { createPortal } from 'react-dom'; import { useFloating, autoUpdate, offset, shift, useDismiss, useInteractions, inline, flip } from '@floating-ui/react'; import { create, useStore, createStore } from 'zustand'; import { jsx, Fragment, jsxs } from 'react/jsx-runtime'; import { AnnotationLayer, TextLayer, getDocument } from 'pdfjs-dist'; import { useDebounce } from 'use-debounce'; import { v4 } from 'uuid'; import { Slot } from '@radix-ui/react-slot'; import { useVirtualizer, elementScroll, debounce } from '@tanstack/react-virtual'; // src/components/annotation-tooltip.tsx // src/lib/clamp.ts var clamp = (value, minimum, maximum) => { return Math.min(Math.max(value, minimum), maximum); }; // src/lib/zoom.ts var getFitWidthZoom = (containerWidth, viewports, zoomOptions) => { const { minZoom, maxZoom } = zoomOptions; const maxPageWidth = Math.max(...viewports.map((viewport) => viewport.width)); const targetZoom = containerWidth / maxPageWidth; const clampedZoom = Math.min(Math.max(targetZoom, minZoom), maxZoom); return clampedZoom; }; var createZustandContext = (getStore) => { const Context = React.createContext(null); const Provider = (props) => { const [store] = React.useState(() => getStore(props.initialValue)); return /* @__PURE__ */ jsx(Context.Provider, { value: store, children: props.children }); }; return { useContext: () => React.useContext(Context), Context, Provider }; }; // src/internal.ts var PDFStore = createZustandContext( (initialState) => { return createStore((set, get) => ({ pdfDocumentProxy: initialState.pdfDocumentProxy, zoom: initialState.zoom, isZoomFitWidth: initialState.isZoomFitWidth ?? false, zoomOptions: { minZoom: initialState.zoomOptions?.minZoom ?? 0.5, maxZoom: initialState.zoomOptions?.maxZoom ?? 10 }, viewportRef: createRef(), viewports: initialState.viewports, updateZoom: (zoom, isZoomFitWidth = false) => { const { minZoom, maxZoom } = get().zoomOptions; set((state) => { if (typeof zoom === "function") { const newZoom2 = clamp(zoom(state.zoom), minZoom, maxZoom); return { zoom: newZoom2, isZoomFitWidth }; } const newZoom = clamp(zoom, minZoom, maxZoom); return { zoom: newZoom, isZoomFitWidth }; }); }, zoomFitWidth: () => { const { viewportRef, zoomOptions, viewports } = get(); if (!viewportRef.current) return; const clampedZoom = getFitWidthZoom( viewportRef.current.clientWidth, viewports, zoomOptions ); set({ zoom: clampedZoom, isZoomFitWidth: true }); return clampedZoom; }, currentPage: 1, setCurrentPage: (val) => { set({ currentPage: val }); }, isPinching: false, setIsPinching: (val) => { set({ isPinching: val }); }, virtualizer: null, setVirtualizer: (val) => { set({ virtualizer: val }); }, pageProxies: initialState.pageProxies, getPdfPageProxy: (pageNumber) => { const proxy = get().pageProxies[pageNumber - 1]; if (!proxy) throw new Error(`Page ${pageNumber} does not exist`); return proxy; }, textContent: [], setTextContent: (val) => { set({ textContent: val }); }, highlights: [], setHighlight: (val) => { set({ highlights: val }); }, customSelectionRects: [], setCustomSelectionRects: (val) => { set({ customSelectionRects: val }); }, coloredHighlights: [], addColoredHighlight: (value) => set((prevState) => ({ coloredHighlights: [...prevState.coloredHighlights, value] })), deleteColoredHighlight: (uuid) => set((prevState) => ({ coloredHighlights: prevState.coloredHighlights.filter( (rect) => rect.uuid !== uuid ) })) })); } ); var usePdf = (selector) => useStore(PDFStore.useContext(), selector); // src/hooks/useAnnotationTooltip.ts var useAnnotationTooltip = ({ annotation, onOpenChange, position = "top", isOpen: controlledIsOpen }) => { const isNewAnnotation = Date.now() - new Date(annotation.createdAt).getTime() < 1e3; const [isPositionCalculated, setIsPositionCalculated] = useState(false); const [isOpen, setIsOpen] = useState(false); const viewportRef = usePdf((state) => state.viewportRef); const scale = usePdf((state) => state.zoom); const effectiveIsOpen = (isOpen && isPositionCalculated || controlledIsOpen) ?? false; const { refs, floatingStyles, context } = useFloating({ placement: position, open: effectiveIsOpen, onOpenChange: (open) => { setIsOpen(open); onOpenChange?.(open); }, whileElementsMounted: autoUpdate, middleware: [ inline(), offset(10), flip({ crossAxis: false, fallbackAxisSideDirection: "end" }), shift({ padding: 8 }) ] }); const dismiss = useDismiss(context); const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]); const updateTooltipPosition = useCallback(() => { if (!annotation.highlights.length) { setIsPositionCalculated(false); return; } const highlightRects = annotation.highlights; let minLeft = Infinity; let maxRight = -Infinity; let minTop = Infinity; let maxBottom = -Infinity; const viewportElement = viewportRef.current; if (!viewportElement) { setIsPositionCalculated(false); return; } const pageElement = viewportElement.querySelector( `[data-page-number="${annotation.pageNumber}"]` ); if (!pageElement) { setIsPositionCalculated(false); return; } refs.setReference({ getBoundingClientRect() { const pageRect = pageElement.getBoundingClientRect(); highlightRects.forEach((highlight) => { const scaledLeft = highlight.left * scale; const scaledWidth = highlight.width * scale; const scaledTop = highlight.top * scale; const scaledHeight = highlight.height * scale; const left = pageRect.left + scaledLeft; const right = left + scaledWidth; const top = pageRect.top + scaledTop; const bottom = top + scaledHeight; minLeft = Math.min(minLeft, left); maxRight = Math.max(maxRight, right); minTop = Math.min(minTop, top); maxBottom = Math.max(maxBottom, bottom); }); const width = maxRight - minLeft; const height = maxBottom - minTop; const centerX = minLeft + width / 2; const centerY = minTop + height / 2; const rect = { width, height, x: centerX - width / 2, y: centerY - height / 2, top: centerY - height / 2, right: centerX + width / 2, bottom: centerY + height / 2, left: centerX - width / 2 }; return rect; }, getClientRects() { return [this.getBoundingClientRect()]; }, contextElement: viewportRef.current || void 0 }); setIsPositionCalculated(true); if (isNewAnnotation) { setIsOpen(true); } }, [ annotation.highlights, annotation.pageNumber, refs, viewportRef, scale, isNewAnnotation ]); useEffect(() => { const viewport = viewportRef.current; setIsPositionCalculated(false); requestAnimationFrame(() => { updateTooltipPosition(); }); const handleScroll = () => { requestAnimationFrame(updateTooltipPosition); }; const handleResize = () => { requestAnimationFrame(updateTooltipPosition); }; if (viewport) { viewport.addEventListener("scroll", handleScroll, { passive: true }); } window.addEventListener("resize", handleResize, { passive: true }); return () => { if (viewport) { viewport.removeEventListener("scroll", handleScroll); } window.removeEventListener("resize", handleResize); }; }, [updateTooltipPosition, viewportRef]); return { isOpen: effectiveIsOpen, setIsOpen, refs, floatingStyles, getFloatingProps, getReferenceProps }; }; var AnnotationTooltip = ({ annotation, children, renderTooltipContent, hoverTooltipContent, onOpenChange, className, focusedOpenId, focusedHoverOpenId, hoverClassName, isOpen: controlledIsOpen, hoverIsOpen: controlledHoverIsOpen }) => { const viewportRef = usePdf((state) => state.viewportRef); const closeTimeoutRef = useRef(null); const isMouseInTooltipRef = useRef(false); const [triggeredPosition, setTriggeredPosition] = useState(); const { isOpen: uncontrolledIsOpen, setIsOpen, refs, floatingStyles, getFloatingProps, getReferenceProps } = useAnnotationTooltip({ annotation, onOpenChange, position: triggeredPosition, isOpen: controlledIsOpen }); const { isOpen: uncontrolledHoverIsOpen, setIsOpen: setHoverIsOpen, refs: hoverRefs, floatingStyles: hoverFloatingStyles, getFloatingProps: getHoverFloatingProps, getReferenceProps: getHoverReferenceProps } = useAnnotationTooltip({ position: "bottom", annotation, isOpen: controlledHoverIsOpen }); const isOpen = controlledIsOpen ?? uncontrolledIsOpen; const hoverIsOpen = controlledHoverIsOpen || uncontrolledHoverIsOpen; const handleClick = useCallback(() => { if (controlledIsOpen === void 0) { setIsOpen(!isOpen); } }, [controlledIsOpen, isOpen, setIsOpen]); const handleMouseEnter = useCallback(() => { if (focusedOpenId && focusedOpenId !== annotation.id) return; if (focusedHoverOpenId && focusedHoverOpenId !== annotation.id) return; if (hoverTooltipContent) { if (closeTimeoutRef.current) { clearTimeout(closeTimeoutRef.current); closeTimeoutRef.current = null; } setHoverIsOpen(true); } }, [ hoverTooltipContent, setHoverIsOpen, annotation.id, focusedHoverOpenId, focusedOpenId ]); const closeTooltip = useCallback(() => { if (!isMouseInTooltipRef.current) { setHoverIsOpen(false); } }, [setHoverIsOpen]); const handleMouseLeave = useCallback(() => { if (!hoverTooltipContent) return; closeTimeoutRef.current = setTimeout(closeTooltip, 100); }, [hoverTooltipContent, closeTooltip]); const handleTooltipMouseEnter = useCallback(() => { if (focusedOpenId && focusedOpenId !== annotation.id) return; if (focusedHoverOpenId && focusedHoverOpenId !== annotation.id) return; isMouseInTooltipRef.current = true; if (closeTimeoutRef.current) { clearTimeout(closeTimeoutRef.current); closeTimeoutRef.current = null; } }, [annotation.id, focusedOpenId, focusedHoverOpenId]); const handleTooltipMouseLeave = useCallback(() => { isMouseInTooltipRef.current = false; setHoverIsOpen(false); }, [setHoverIsOpen]); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "div", { ref: (node) => { refs.setReference(node); hoverRefs.setReference(node); }, onClick: handleClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...getReferenceProps(), ...getHoverReferenceProps(), children } ), isOpen && viewportRef.current && createPortal( /* @__PURE__ */ jsx( "div", { ref: refs.setFloating, className, "data-annotation-tooltip": "click", style: { ...floatingStyles, position: "absolute", pointerEvents: "auto", zIndex: 50 }, ...getFloatingProps(), children: renderTooltipContent({ annotation, onClose: () => setIsOpen(false), setPosition: (position) => setTriggeredPosition(position) }) } ), viewportRef.current ), !isOpen && hoverIsOpen && annotation.comment && hoverTooltipContent && viewportRef.current && createPortal( /* @__PURE__ */ jsx( "div", { ref: hoverRefs.setFloating, className: hoverClassName, "data-annotation-tooltip": "hover", style: { ...hoverFloatingStyles, position: "absolute", pointerEvents: "auto", zIndex: 51 }, onMouseEnter: handleTooltipMouseEnter, onMouseLeave: handleTooltipMouseLeave, ...getHoverFloatingProps(), children: hoverTooltipContent } ), viewportRef.current ) ] }); }; var useAnnotations = create((set) => ({ annotations: [], addAnnotation: (annotation) => set((state) => ({ annotations: [...state.annotations, annotation] })), updateAnnotation: (id, updates) => set((state) => ({ annotations: state.annotations.map( (annotation) => annotation.id === id ? { ...annotation, ...updates } : annotation ) })), deleteAnnotation: (id) => set((state) => ({ annotations: state.annotations.filter( (annotation) => annotation.id !== id ) })), setAnnotations: (annotations) => set({ annotations }) })); var PDFPageNumberContext = createContext(0); var usePDFPageNumber = () => { return useContext(PDFPageNumberContext); }; var AnnotationHighlightLayer = ({ className, style, renderTooltipContent, renderHoverTooltipContent, tooltipClassName, highlightClassName, underlineClassName, commentIconPosition, commmentIcon, commentIconClassName, focusedAnnotationId, focusedHoverAnnotationId, onAnnotationClick, onAnnotationTooltipClose, hoverTooltipClassName }) => { const { annotations } = useAnnotations(); const pageNumber = usePDFPageNumber(); const pageAnnotations = annotations.filter( (annotation) => annotation.pageNumber === pageNumber ); const getCommentIconPosition = (highlights) => { if (!highlights.length) return { top: 0, right: 10 }; const sortedHighlights = [...highlights].sort((a, b) => { const topDiff = a.top - b.top; return Math.abs(topDiff) < 3 ? a.left - b.left : topDiff; }); const lines = []; let currentLine = []; sortedHighlights.forEach((highlight) => { if (currentLine.length === 0) { currentLine.push(highlight); } else { const firstInLine = currentLine[0]; if (Math.abs(highlight.top - firstInLine.top) <= 3) { currentLine.push(highlight); } else { lines.push([...currentLine]); currentLine = [highlight]; } } }); if (currentLine.length > 0) { lines.push(currentLine); } const PAGE_WIDTH = 600; const hasLongLine = lines.some((line) => { if (line.length === 0) return false; const rightmost2 = Math.max(...line.map((h) => h.left + h.width)); return rightmost2 > PAGE_WIDTH * 0.8; }); const firstHighlight = highlights[0]; const firstLine = lines[0] || []; const leftmost = Math.min(...firstLine.map((h) => h.left)); const rightmost = Math.max(...firstLine.map((h) => h.left + h.width)); const lineCenter = leftmost + (rightmost - leftmost) / 2; const shouldPositionRight = hasLongLine || lineCenter > PAGE_WIDTH * 0.5; const rightPosition = commentIconPosition === "highlight" ? { left: rightmost + 8 } : { right: 10 }; const leftPosition = commentIconPosition === "highlight" ? { left: leftmost - 18 } : { left: 20 }; return { top: firstHighlight.top + firstHighlight.height / 2 - 6, ...shouldPositionRight ? rightPosition : leftPosition }; }; return /* @__PURE__ */ jsx("div", { className, style, children: pageAnnotations.map((annotation) => { return /* @__PURE__ */ jsx( AnnotationTooltip, { annotation, className: tooltipClassName, hoverClassName: hoverTooltipClassName, focusedOpenId: focusedAnnotationId, focusedHoverOpenId: focusedHoverAnnotationId, isOpen: focusedAnnotationId === annotation.id, hoverIsOpen: focusedHoverAnnotationId === annotation.id, onOpenChange: (open) => { if (open && onAnnotationClick) { onAnnotationClick(annotation); } else if (!open && onAnnotationTooltipClose) { onAnnotationTooltipClose(annotation); } }, renderTooltipContent, hoverTooltipContent: renderHoverTooltipContent({ annotation, onClose: () => { } }), children: /* @__PURE__ */ jsxs( "div", { style: { cursor: "pointer" }, onClick: () => onAnnotationClick?.(annotation), children: [ annotation.highlights.map((highlight, index) => /* @__PURE__ */ jsx( "div", { className: highlightClassName, style: { position: "absolute", top: highlight.top, left: highlight.left, width: highlight.width, height: highlight.height, backgroundColor: annotation.color }, "data-highlight-id": annotation.id }, `highlight-${// biome-ignore lint/suspicious/noArrayIndexKey: <index> index}` )), annotation.comment && annotation.underlines?.map((rect, index) => /* @__PURE__ */ jsx( "div", { className: underlineClassName, style: { position: "absolute", top: rect.top, left: rect.left, width: rect.width, height: 1.1, backgroundColor: annotation.borderColor }, "data-comment-id": annotation.id }, `underline-${// biome-ignore lint/suspicious/noArrayIndexKey: <index> index}` )), annotation.comment && commmentIcon && /* @__PURE__ */ jsx( "div", { className: commentIconClassName, style: { position: "absolute", ...getCommentIconPosition(annotation.highlights), color: "gray", cursor: "pointer", zIndex: 10 }, "data-comment-icon-id": annotation.id, children: commmentIcon } ) ] } ) }, annotation.id ); }) }); }; // ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs function r(e) { var t, f, n = ""; if ("string" == typeof e || "number" == typeof e) n += e; else if ("object" == typeof e) if (Array.isArray(e)) { var o = e.length; for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f); } else for (f in e) e[f] && (n && (n += " "), n += f); return n; } function clsx() { for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t); return n; } var clsx_default = clsx; // src/lib/cancellable.ts var cancellable = (promise) => { let isCancelled = false; const wrappedPromise = new Promise((resolve, reject) => { promise.then( (value) => { if (!isCancelled) { resolve(value); } }, (error) => { if (!isCancelled) { reject(error); } } ); }); return { promise: wrappedPromise, cancel() { isCancelled = true; } }; }; var usePdfJump = () => { const virtualizer = usePdf((state) => state.virtualizer); const setHighlight = usePdf((state) => state.setHighlight); const jumpToPage = useCallback( (pageIndex, options) => { if (!virtualizer) return; const defaultOptions = { align: "start", behavior: "smooth" }; const finalOptions = { ...defaultOptions, ...options }; virtualizer.scrollToIndex(pageIndex - 1, finalOptions); }, [virtualizer] ); const jumpToOffset = useCallback( (offset3) => { if (!virtualizer) return; virtualizer.scrollToOffset(offset3, { align: "start", behavior: "smooth" }); }, [virtualizer] ); const scrollToHighlightRects = useCallback( (rects, type, align = "start", additionalOffset = 0) => { if (!virtualizer) return; const firstPage = Math.min(...rects.map((rect) => rect.pageNumber)); const pageOffset = virtualizer.getOffsetForIndex(firstPage - 1, "start"); if (pageOffset === null) return; const targetRect = rects.find((rect) => rect.pageNumber === firstPage); if (!targetRect) return; const isNumber = pageOffset?.[0] != null; if (!isNumber) return; const pageStart = pageOffset[0] ?? 0; let rectTop; let rectHeight; if (type === "percent") { const estimatePageHeight = virtualizer.options.estimateSize( firstPage - 1 ); rectTop = targetRect.top / 100 * estimatePageHeight; rectHeight = targetRect.height / 100 * estimatePageHeight; } else { rectTop = targetRect.top; rectHeight = targetRect.height; } let scrollOffset; if (align === "center") { const viewportHeight = virtualizer.scrollElement?.clientHeight || 0; const rectCenter = pageStart + rectTop + rectHeight / 2; scrollOffset = rectCenter - viewportHeight / 2; } else { scrollOffset = pageStart + rectTop; } scrollOffset += additionalOffset; const adjustedOffset = Math.max(0, scrollOffset); virtualizer.scrollToOffset(adjustedOffset, { align: "start", // Always use start when we've calculated our own centering behavior: "smooth" }); }, [virtualizer] ); const jumpToHighlightRects = useCallback( (rects, type, align = "start", additionalOffset = 0) => { if (!virtualizer) return; setHighlight(rects); scrollToHighlightRects(rects, type, align, additionalOffset); }, [virtualizer, setHighlight, scrollToHighlightRects] ); return { jumpToPage, jumpToOffset, jumpToHighlightRects, scrollToHighlightRects }; }; var LinkService = class { _pdfDocumentProxy; externalLinkEnabled = true; isInPresentationMode = false; _currentPageNumber = 0; _pageNavigationCallback; get pdfDocumentProxy() { if (!this._pdfDocumentProxy) { throw new Error("pdfDocumentProxy is not set"); } return this._pdfDocumentProxy; } get pagesCount() { return this._pdfDocumentProxy?.numPages || 0; } get page() { return this._currentPageNumber; } set page(value) { this._currentPageNumber = value; if (this._pageNavigationCallback) { this._pageNavigationCallback(value); } } // Required for link annotations to work setDocument(pdfDocument) { this._pdfDocumentProxy = pdfDocument; } setViewer() { } getDestinationHash(dest) { if (!dest) return ""; const destRef = dest[0]; if (dest.length > 1 && typeof dest[1] === "object" && dest[1] !== null && "url" in dest[1]) { const urlDest = dest[1]; return urlDest.url; } if (destRef && typeof destRef === "object") { if ("num" in destRef) { const numRef = destRef; return `#page=${numRef.num + 1}`; } if ("gen" in destRef) { const genRef = destRef; const refNum = genRef.num ?? 0; return `#page=${refNum + 1}`; } } if (typeof destRef === "number") { return `#page=${destRef + 1}`; } return `#dest-${String(dest)}`; } getAnchorUrl(hash) { if (hash.startsWith("http://") || hash.startsWith("https://")) { return hash; } return `#${hash}`; } addLinkAttributes(link, url, newWindow) { if (!link) return; const isExternalLink = url.startsWith("http://") || url.startsWith("https://"); if (isExternalLink && this.externalLinkEnabled) { link.href = url; link.target = newWindow === false ? "" : "_blank"; link.rel = "noopener noreferrer"; } else if (!isExternalLink) { link.href = url; link.target = ""; } else { link.href = "#"; link.target = ""; } } async goToDestination(dest) { let explicitDest; if (typeof dest === "string") { explicitDest = await this.pdfDocumentProxy.getDestination(dest); } else if (Array.isArray(dest)) { explicitDest = dest; } else { explicitDest = await dest; } if (!explicitDest) { return; } if (explicitDest.length > 1 && typeof explicitDest[1] === "object" && explicitDest[1] !== null && "url" in explicitDest[1]) { return; } const destRef = explicitDest[0]; let pageIndex; if (destRef && typeof destRef === "object") { if ("num" in destRef) { try { const refProxy = destRef; pageIndex = await this.pdfDocumentProxy.getPageIndex(refProxy); } catch (_error) { return; } } else { return; } } else if (typeof destRef === "number") { pageIndex = destRef; } else { return; } const pageNumber = pageIndex + 1; if (this._pageNavigationCallback) { this._pageNavigationCallback(pageNumber); } } executeNamedAction() { } navigateTo(dest) { this.goToDestination(dest); } get rotation() { return 0; } set rotation(_value) { } goToPage(_page_valuer) { } setHash(hash) { if (hash.startsWith("#page=")) { const pageNumber = parseInt(hash.substring(6), 10); if (!Number.isNaN(pageNumber)) { this.goToPage(pageNumber); } } } executeSetOCGState() { } // Method to register navigation callback registerPageNavigationCallback(callback) { this._pageNavigationCallback = callback; } // Method to unregister navigation callback unregisterPageNavigationCallback() { this._pageNavigationCallback = void 0; } }; var defaultLinkService = new LinkService(); var PDFLinkServiceContext = createContext(defaultLinkService); var usePDFLinkService = () => { return useContext(PDFLinkServiceContext); }; var useVisibility = ({ elementRef }) => { const [visible, setVisible] = useState(false); useEffect(() => { if (!elementRef.current) { return; } const observer = new IntersectionObserver(([entry]) => { setVisible(entry?.isIntersecting ?? false); }); observer.observe(elementRef.current); return () => { observer.disconnect(); }; }, [elementRef]); return { visible }; }; // src/hooks/layers/useAnnotationLayer.tsx var defaultAnnotationLayerParams = { renderForms: true, externalLinksEnabled: true, jumpOptions: { behavior: "smooth", align: "start" } }; var useAnnotationLayer = (params) => { const mergedParams = useMemo(() => { return { ...defaultAnnotationLayerParams, ...params }; }, [params]); const annotationLayerRef = useRef(null); const annotationLayerObjectRef = useRef(null); const linkService = usePDFLinkService(); const { visible } = useVisibility({ elementRef: annotationLayerRef }); const pageNumber = usePDFPageNumber(); const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber)); const pdfDocumentProxy = usePdf((state) => state.pdfDocumentProxy); useEffect(() => { linkService.externalLinkEnabled = mergedParams.externalLinksEnabled; }, [linkService, mergedParams.externalLinksEnabled]); const { jumpToPage } = usePdfJump(); useEffect(() => { if (!jumpToPage) return; const handlePageNavigation = (pageNumber2) => { jumpToPage(pageNumber2, mergedParams.jumpOptions); }; linkService.registerPageNavigationCallback(handlePageNavigation); return () => { linkService.unregisterPageNavigationCallback(); }; }, [jumpToPage, linkService, mergedParams.jumpOptions]); useEffect(() => { const style = document.createElement("style"); style.textContent = ` .annotationLayer { position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; opacity: 1; z-index: 3; } .annotationLayer section { position: absolute; } .annotationLayer .linkAnnotation > a, .annotationLayer .buttonWidgetAnnotation.pushButton > a { position: absolute; font-size: 1em; top: 0; left: 0; width: 100%; height: 100%; background: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7") 0 0 repeat; cursor: pointer; } .annotationLayer .linkAnnotation > a:hover, .annotationLayer .buttonWidgetAnnotation.pushButton > a:hover { opacity: 0.2; background: rgba(255, 255, 0, 1); box-shadow: 0 2px 10px rgba(255, 255, 0, 1); } `; document.head.appendChild(style); return () => { document.head.removeChild(style); }; }, []); useEffect(() => { if (!annotationLayerRef.current) return; const element = annotationLayerRef.current; const handleLinkClick = (e) => { if (!e.target || !(e.target instanceof HTMLAnchorElement)) return; const target = e.target; const href = target.getAttribute("href") || ""; if (href.startsWith("#page=")) { e.preventDefault(); const pageNumber2 = parseInt(href.substring(6), 10); if (!Number.isNaN(pageNumber2)) { linkService.goToPage(pageNumber2); } } }; element.addEventListener("click", handleLinkClick); return () => { element.removeEventListener("click", handleLinkClick); }; }, [linkService]); useEffect(() => { if (!annotationLayerRef.current) { return; } if (visible) { annotationLayerRef.current.style.contentVisibility = "visible"; } else { annotationLayerRef.current.style.contentVisibility = "hidden"; } }, [visible]); useEffect(() => { if (!annotationLayerRef.current || !pdfPageProxy || !pdfDocumentProxy) { return; } if (linkService._pdfDocumentProxy !== pdfDocumentProxy) { linkService.setDocument(pdfDocumentProxy); } annotationLayerRef.current.innerHTML = ""; annotationLayerRef.current.className = "annotationLayer"; const viewport = pdfPageProxy.getViewport({ scale: 1 }); const annotationLayerConfig = { div: annotationLayerRef.current, viewport, page: pdfPageProxy, linkService, accessibilityManager: void 0, annotationCanvasMap: void 0, annotationEditorUIManager: void 0, structTreeLayer: void 0 }; const annotationLayer = new AnnotationLayer(annotationLayerConfig); annotationLayerObjectRef.current = annotationLayer; const { cancel } = cancellable( (async () => { try { const annotations = await pdfPageProxy.getAnnotations(); await annotationLayer.render({ ...annotationLayerConfig, ...mergedParams, annotations, linkService }); } catch (_error) { } })() ); return () => { cancel(); }; }, [pdfPageProxy, pdfDocumentProxy, mergedParams, linkService]); return { annotationLayerRef }; }; var AnnotationLayer2 = ({ renderForms = true, externalLinksEnabled = true, jumpOptions = { behavior: "smooth", align: "start" }, className, style, ...props }) => { const { annotationLayerRef } = useAnnotationLayer({ renderForms, externalLinksEnabled, jumpOptions }); return /* @__PURE__ */ jsx( "div", { className: clsx_default("annotationLayer", className), style: { ...style, position: "absolute", top: 0, left: 0 }, ...props, ref: annotationLayerRef } ); }; var useDpr = () => { const [dpr, setDPR] = useState( !window ? 1 : Math.min(window.devicePixelRatio, 2) ); useEffect(() => { if (!window) { return; } const handleDPRChange = () => { setDPR(window.devicePixelRatio); }; const windowMatch = window.matchMedia( `screen and (min-resolution: ${dpr}dppx)` ); windowMatch.addEventListener("change", handleDPRChange); return () => { windowMatch.removeEventListener("change", handleDPRChange); }; }, [dpr]); return dpr; }; // src/hooks/layers/useCanvasLayer.tsx var MAX_CANVAS_PIXELS = 16777216; var MAX_CANVAS_DIMENSION = 32767; var useCanvasLayer = ({ background }) => { const canvasRef = useRef(null); const pageNumber = usePDFPageNumber(); const dpr = useDpr(); const bouncyZoom = usePdf((state) => state.zoom); const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber)); const [zoom] = useDebounce(bouncyZoom, 100); const clampScaleForPage = useCallback( (targetScale, pageWidth, pageHeight) => { if (!targetScale) { return 0; } const areaLimit = Math.sqrt( MAX_CANVAS_PIXELS / Math.max(pageWidth * pageHeight, 1) ); const widthLimit = MAX_CANVAS_DIMENSION / Math.max(pageWidth, 1); const heightLimit = MAX_CANVAS_DIMENSION / Math.max(pageHeight, 1); const safeScale = Math.min( targetScale, Number.isFinite(areaLimit) ? areaLimit : targetScale, Number.isFinite(widthLimit) ? widthLimit : targetScale, Number.isFinite(heightLimit) ? heightLimit : targetScale ); return Math.max(safeScale, 0); }, [] ); useLayoutEffect(() => { if (!canvasRef.current) { return; } const baseCanvas = canvasRef.current; const baseViewport = pdfPageProxy.getViewport({ scale: 1 }); const pageWidth = baseViewport.width; const pageHeight = baseViewport.height; const targetBaseScale = dpr * Math.min(zoom, 1); const baseScale = clampScaleForPage(targetBaseScale, pageWidth, pageHeight); baseCanvas.width = Math.floor(pageWidth * baseScale); baseCanvas.height = Math.floor(pageHeight * baseScale); baseCanvas.style.position = "absolute"; baseCanvas.style.top = "0"; baseCanvas.style.left = "0"; baseCanvas.style.width = `${pageWidth}px`; baseCanvas.style.height = `${pageHeight}px`; baseCanvas.style.transform = "translate(0px, 0px)"; baseCanvas.style.zIndex = "0"; baseCanvas.style.pointerEvents = "none"; const context = baseCanvas.getContext("2d"); if (!context) { return; } context.setTransform(1, 0, 0, 1, 0, 0); context.clearRect(0, 0, baseCanvas.width, baseCanvas.height); const viewport = pdfPageProxy.getViewport({ scale: baseScale }); const renderingTask = pdfPageProxy.render({ canvasContext: context, viewport, background }); renderingTask.promise.catch((error) => { if (error.name === "RenderingCancelledException") { return; } throw error; }); return () => { void renderingTask.cancel(); }; }, [pdfPageProxy, background, dpr, zoom, clampScaleForPage]); return { canvasRef }; }; var MAX_CANVAS_PIXELS2 = 16777216; var MAX_CANVAS_DIMENSION2 = 32767; var useDetailCanvasLayer = ({ background, baseCanvasRef }) => { const containerRef = useRef(null); const detailCanvasRef = useRef(null); const pageNumber = usePDFPageNumber(); const dpr = useDpr(); const bouncyZoom = usePdf((state) => state.zoom); const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber)); const viewportRef = usePdf((state) => state.viewportRef); const [zoom] = useDebounce(bouncyZoom, 200); const [scrollTick, setScrollTick] = useState(0); const [debouncedScrollTick] = useDebounce(scrollTick, 20); const ensureDetailCanvas = useCallback(() => { let detailCanvas = detailCanvasRef.current; if (!detailCanvas) { const parent = baseCanvasRef.current?.parentElement; if (!parent) { return null; } detailCanvas = document.createElement("canvas"); parent.appendChild(detailCanvas); detailCanvasRef.current = detailCanvas; } detailCanvas.style.position = "absolute"; detailCanvas.style.top = "0"; detailCanvas.style.left = "0"; detailCanvas.style.pointerEvents = "none"; detailCanvas.style.zIndex = "0"; return detailCanvas; }, [baseCanvasRef]); const clampScaleForPage = useCallback( (targetScale, pageWidth, pageHeight) => { if (!targetScale) { return 0; } const areaLimit = Math.sqrt( MAX_CANVAS_PIXELS2 / Math.max(pageWidth * pageHeight, 1) ); const widthLimit = MAX_CANVAS_DIMENSION2 / Math.max(pageWidth, 1); const heightLimit = MAX_CANVAS_DIMENSION2 / Math.max(pageHeight, 1); const safeScale = Math.min( targetScale, Number.isFinite(areaLimit) ? areaLimit : targetScale, Number.isFinite(widthLimit) ? widthLimit : targetScale, Number.isFinite(heightLimit) ? heightLimit : targetScale ); return Math.max(safeScale, 0); }, [] ); useLayoutEffect(() => { const scrollContainer = viewportRef?.current; if (!scrollContainer) return; const handleScroll = () => { setScrollTick((prev) => prev + 1); }; scrollContainer.addEventListener("scroll", handleScroll, { passive: true }); return () => { scrollContainer.removeEventListener("scroll", handleScroll); }; }, [viewportRef?.current]); useLayoutEffect(() => { if (!viewportRef?.current) { return; } const detailCanvas = ensureDetailCanvas(); const container = containerRef.current; if (!detailCanvas || !container) { return; } const scrollContainer = viewportRef.current; const pageContainer = baseCanvasRef.current?.parentElement ?? null; if (!pageContainer) { detailCanvas.style.display = "none"; detailCanvas.width = 0; detailCanvas.height = 0; return; } const baseViewport = pdfPageProxy.getViewport({ scale: 1 }); const pageWidth = baseViewport.width; const pageHeight = baseViewport.height; const scrollX = scrollContainer.scrollLeft / zoom; const scrollY = scrollContainer.scrollTop / zoom; const viewportWidth = scrollContainer.clientWidth / zoom; const viewportHeight = scrollContainer.clientHeight / zoom; const pageRect = pageContainer.getBoundingClientRect(); const containerRect = scrollContainer.getBoundingClientRect(); const pageLeft = (pageRect.left - containerRect.left) / zoom + scrollX; const pageTop = (pageRect.top - containerRect.top) / zoom + scrollY; const visibleLeft = Math.max(0, scrollX - pageLeft); const visibleTop = Math.max(0, scrollY - pageTop); const visibleRight = Math.min( pageWidth, scrollX + viewportWidth - pageLeft ); const visibleBottom = Math.min( pageHeight, scrollY + viewportHeight - pageTop ); const visibleWidth = Math.max(0, visibleRight - visibleLeft); const visibleHeight = Math.max(0, visibleBottom - visibleTop); const targetDetailScale = dpr * zoom * 1.3; const baseTargetScale = dpr * Math.min(zoom, 1); const baseScale = clampScaleForPage(baseTargetScale, pageWidth, pageHeight); const needsDetail = zoom > 1 && targetDetailScale - baseScale > 1e-3; if (!needsDetail || visibleWidth <= 0 || visibleHeight <= 0) { detailCanvas.style.display = "none"; detailCanvas.width = 0; detailCanvas.height = 0; return; } detailCanvas.style.display = "block"; const pdfOffsetX = visibleLeft; const pdfOffsetY = visibleTop; const pdfWidth = visibleWidth * targetDetailScale; const pdfHeight = visibleHeight * targetDetailScale; const effectiveScale = targetDetailScale; const actualWidth = pdfWidth; const actualHeight = pdfHeight; detailCanvas.width = actualWidth; detailCanvas.height = actualHeight; const scaledWidth = visibleWidth * zoom; const scaledHeight = visibleHeight * zoom; detailCanvas.style.width = `${scaledWidth}px`; detailCanvas.style.height = `${scaledHeight}px`; detailCanvas.style.transformOrigin = "center center"; detailCanvas.style.transform = `translate(${visibleLeft * zoom}px, ${visibleTop * zoom}px) `; container.style.transform = `scale3d(${1 / zoom}, ${1 / zoom}, 1)`; container.style.transformOrigin = `0 0`; const context = detailCanvas.getContext("2d"); if (!context) { return; } context.setTransform(1, 0, 0, 1, 0, 0); context.clearRect(0, 0, detailCanvas.width, detailCanvas.height); const transform = [ 1, 0, 0, 1, -pdfOffsetX * effectiveScale, -pdfOffsetY * effectiveScale ]; const detailViewport = pdfPageProxy.getViewport({ scale: effectiveScale }); const renderingTask = pdfPageProxy.render({ canvasContext: context, viewport: detailViewport, background, transform }); renderingTask.promise.catch((error) => { if (error.name === "RenderingCancelledException") { return; } throw error; }); return () => { void renderingTask.cancel(); }; }, [ pdfPageProxy, zoom, background, dpr, viewportRef, ensureDetailCanvas, clampScaleForPage, baseCanvasRef, debouncedScrollTick ]); return { detailCanvasRef, containerRef }; }; var CanvasLayer = ({ style, background, ...props }) => { const { canvasRef } = useCanvasLayer({ background }); const { detailCanvasRef, containerRef } = useDetailCanvasLayer({ background, baseCanvasRef: canvasRef }); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("canvas", { ...props, ref: canvasRef, style }), /* @__PURE__ */ jsx( "div", { ref: containerRef, className: "absolute top-0 left-0 w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsx("canvas", { ref: detailCanvasRef }) } ) ] }); }; // src/hooks/useSelectionDimensions.tsx var MERGE_THRESHOLD = 2; var shouldMergeRects = (rect1, rect2) => { const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height); 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; return verticalOverlap && horizontallyConnected; }; var consolidateHighlightRects = (rects) => { if (rects.length <= 1) return rects; const consolidated = []; const sorted = [...rects].sort((a, b) => { const pageCompare = a.pageNumber - b.pageNumber; if (pageCompare !== 0) return pageCompare; const topDiff = a.top - b.top; return Math.abs(topDiff) < 2 ? a.left - b.left : topDiff; }); let current = sorted[0]; if (!current) return rects; for (let i = 1; i < sorted.length; i++) { const next = sorted[i]; if (!next) continue; const samePageAndLine = current.pageNumber === next.pageNumber && Math.abs(current.top - next.top) < Math.max(current.height, next.height) * 0.5; const horizontallyConnected = samePageAndLine && // Adjacent (touching or very close) (Math.abs(current.left + current.width - next.left) <= MERGE_THRESHOLD || Math.abs(next.left + next.width - current.left) <= MERGE_THRESHOLD || // Overlapping current.left < next.left + next.width && next.left < current.left + current.width || // Very close (small gap) Math.abs(current.left + current.width - next.left) <= current.height * 0.2); if (horizontallyConnected) { const newLeft = Math.min(current.left, next.left); const newRight = Math.max( current.left + current.width, next.left + next.width ); const newTop = Math.min(current.top, next.top); const newBottom = Math.max( current.top + current.height, next.top + next.height ); current = { left: newLeft, top: newTop, width: newRight - newLeft, height: newBottom - newTop, pageNumber: current.pageNumber }; } else { consolidated.push(current); current = next; } } if (current) { consolidated.push(current); } return consolidated; }; var consolidateRects = (rects) => { if (rects.length <= 1) return rects; const result = []; const visited = /* @__PURE__ */ new Set(); for (let i = 0; i < rects.length; i++) { if (visited.has(i)) continue; const currentRect = rects[i]; if (!currentRect) continue; const currentGroup = [currentRect]; visited.add(i); let foundNew = true; while (foundNew) { foundNew = false; for (let j = 0; j < rects.length; j++) { if (visited.has(j)) continue; const candidateRect = rects[j]; if (!candidateRect) continue; const shouldMergeWithGroup = currentGroup.some( (groupRect) => doRectsOverlap(groupRect, candidateRect) ); if (shouldMergeWithGroup) { currentGroup.push(candidateRect); visited.add(j); foundNew = true; } } } result.push(mergeRectGroup(currentGroup)); } return result; }; var doRectsOverlap = (rect1, rect2) => { const horizontalOverlap = rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width; const verticalOverlap = rect1.top < rect2.top + rect2.height && rect2.top < rect1.top + rect1.height; const closeEnough = shouldMergeRects(rect1, rect2); return horizontalOverlap && verticalOverlap || closeEnough; }; var mergeRectGroup = (rects) => { if (rects.length === 1) { const rect = rects[0]; if (!rect) throw new Error("Invalid rect in group"); return rect; } const firstRect = rects[0]; if (!firstRect) throw new Error("Invalid first rect in group"); let minLeft = firstRect.left; let minTop = firstRect.top; let maxRight = firstRect.left + firstRect.width; let maxBottom = firstRect.top + firstRect.height; rects.forEach((rect) => { if (!rect) return; minLeft = Math.min(minLeft, rect.left); minTop = Math.min(minTop, rect.top); maxRight = Math.max(maxRight, rect.left + rect.width); maxBottom = Math.max(maxBottom, rect.top + rect.height); }); return { left: minLeft, top: minTop, width: maxRight - minLeft, height: maxBottom - minTop, pageNumber: firstRect.pageNumber }; }; var useSelectionDimensions = () => { const store = PDFStore.useContext(); const getAnnotationDimension = () => { const selection = window.getSelection(); if (!selection || selection.isCollapsed) return; const range = selection.getRangeAt(0); const highlightRects = []; const underlineRects = []; const textLayerMapHighlight = /* @__PURE__ */ new Map(); const textLayerMapUnderline = /* @__PURE__ */ new Map(); const clientRects = Array.from(range.getClientRects()).filter( (rect) => rect.width > 2 && rect.height > 2 ); clientRects.forEach((clientRect) => { let element = document.elementFromPoint( clientRect.left + 1, clientRect.top + clientRect.height / 2 ); if (!element) { element = document.elementFromPoint( clientRect.left + clientRect.width / 2, clientRect.top + clientRect.height / 2 ); } if (!element) { element = document.elementFromPoint( clientRect.right - 1, clientRect.top + clientRect.height / 2 ); } if (!element) { element = document.elementFromPoint( clientRect.left + clientRect.width / 2, clientRect.top + 1 ); } if (!element) { element = document.elementFromPoint( clientRect.left + clientRect.width / 2, clientRect.bottom - 1 ); } const textLayer = element?.closest(".textLayer"); if (!textLayer) return; const isSuperOrSubScript = (el) => { if (!el) return false; if (el.tagName.toLowerCase() === "sup" || el.tagName.toLowerCase() === "sub") { return true; } const classes = el.className; if (typeof classes === "string") { const superSubClasses = ["superscript", "subscript", "sup", "sub"]; if (superSubClasses.some((c) => classes.includes(c))) { return true; } } const elementRect = el.getBoundingClientRect(); if (elementRect.height < 6 && elementRect.width < 15) { const textContent = el.textContent?.trim() || ""; if (textContent.length <= 2 && /^[\d\w]{1,2}$/.test(textContent)) { const parentRect = el.parentElement?.getBoundingClientRect(); if (parentRect && parentRect.height > elementRect.height * 2) { const elementCenter = elementRect.top + elementRect.height / 2; const parentCenter = parentRect.top + parentRect.height / 2;