UNPKG

@anaralabs/lector

Version:

Headless PDF viewer for React

1,696 lines (1,679 loc) 142 kB
'use client'; import React, { createContext, forwardRef, useRef, useState, useCallback, useContext, useEffect, cloneElement, createRef, useLayoutEffect } from 'react'; import { createPortal } from 'react-dom'; import { useFloating, autoUpdate, offset, shift, useDismiss, useInteractions, inline, flip } from '@floating-ui/react'; import { useVirtualizer, elementScroll, debounce } from '@tanstack/react-virtual'; 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'; // src/components/annotation-tooltip.tsx // src/lib/clamp.ts var clamp = (value, min, max) => { return Math.min(Math.max(value, min), max); }; // 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, scale, controlledIsOpen]); return { isOpen: effectiveIsOpen, setIsOpen, refs, floatingStyles, getFloatingProps, getReferenceProps }; }; var AnnotationTooltip = ({ annotation, children, renderTooltipContent, hoverTooltipContent, onOpenChange, className, hoverClassName, isOpen: controlledIsOpen, tooltipBubbleSize, 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 (hoverTooltipContent) { if (closeTimeoutRef.current) { clearTimeout(closeTimeoutRef.current); closeTimeoutRef.current = null; } setHoverIsOpen(true); } }, [hoverTooltipContent, setHoverIsOpen]); 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(() => { isMouseInTooltipRef.current = true; if (closeTimeoutRef.current) { clearTimeout(closeTimeoutRef.current); closeTimeoutRef.current = null; } }, []); const handleTooltipMouseLeave = useCallback(() => { isMouseInTooltipRef.current = false; setHoverIsOpen(false); }, [setHoverIsOpen]); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs( "div", { ref: (node) => { refs.setReference(node); hoverRefs.setReference(node); }, onClick: handleClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...getReferenceProps(), ...getHoverReferenceProps(), children: [ !isOpen && !hoverIsOpen && annotation.comment && /* @__PURE__ */ jsx("div", { className: "rounded-full", style: { position: "absolute", top: annotation.highlights[0]?.top ? annotation.highlights[0]?.top - tooltipBubbleSize / 2 : 0, left: annotation.highlights[0]?.left ? annotation.highlights[0]?.left - tooltipBubbleSize / 2 : 0, backgroundColor: annotation.borderColor, height: `${tooltipBubbleSize}px`, width: `${tooltipBubbleSize}px`, zIndex: 50 } }), 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, focusedAnnotationId, focusedHoverAnnotationId, onAnnotationClick, hoverTooltipClassName, tooltipBubbleSize = 6 }) => { const { annotations } = useAnnotations(); const pageNumber = usePDFPageNumber(); const pageAnnotations = annotations.filter( (annotation) => annotation.pageNumber === pageNumber ); return /* @__PURE__ */ jsx("div", { className, style, children: pageAnnotations.map((annotation) => /* @__PURE__ */ jsx( AnnotationTooltip, { annotation, className: tooltipClassName, hoverClassName: hoverTooltipClassName, isOpen: focusedAnnotationId === annotation.id, tooltipBubbleSize, hoverIsOpen: focusedHoverAnnotationId === annotation.id, onOpenChange: (open) => { if (open && onAnnotationClick) { onAnnotationClick(annotation); } }, renderTooltipContent, hoverTooltipContent: renderHoverTooltipContent({ annotation, onClose: () => { } }), children: /* @__PURE__ */ jsx( "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 }, index )) } ) }, 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; } }; }; // src/hooks/pages/usePdfJump.tsx var usePdfJump = () => { const virtualizer = usePdf((state) => state.virtualizer); const setHighlight = usePdf((state) => state.setHighlight); const jumpToPage = (pageIndex, options) => { if (!virtualizer) return; const defaultOptions = { align: "start", behavior: "smooth" }; const finalOptions = { ...defaultOptions, ...options }; virtualizer.scrollToIndex(pageIndex - 1, finalOptions); }; const jumpToOffset = (offset3) => { if (!virtualizer) return; virtualizer.scrollToOffset(offset3, { align: "start", behavior: "smooth" }); }; const scrollToHighlightRects = (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" }); }; const jumpToHighlightRects = (rects, type, align = "start", additionalOffset = 0) => { if (!virtualizer) return; setHighlight(rects); scrollToHighlightRects(rects, type, align, additionalOffset); }; 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; } constructor() { } 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(pageNumber) { if (pageNumber >= 1 && pageNumber <= this.pagesCount) { if (this._pageNavigationCallback) { this._pageNavigationCallback(pageNumber); } } } setHash(hash) { if (hash.startsWith("#page=")) { const pageNumber = parseInt(hash.substring(6), 10); if (!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 = { ...defaultAnnotationLayerParams, ...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 (!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); }; }, []); return dpr; }; // src/hooks/layers/useCanvasLayer.tsx 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); useLayoutEffect(() => { if (!canvasRef.current) { return; } const viewport = pdfPageProxy.getViewport({ scale: 1 }); const canvas = canvasRef.current; const scale = dpr * zoom; canvas.height = viewport.height * scale; canvas.width = viewport.width * scale; canvas.style.height = `${viewport.height}px`; canvas.style.width = `${viewport.width}px`; const canvasContext = canvas.getContext("2d"); canvasContext.scale(scale, scale); const renderingTask = pdfPageProxy.render({ canvasContext, viewport, background }); renderingTask.promise.catch((error) => { if (error.name === "RenderingCancelledException") { return; } throw error; }); return () => { void renderingTask.cancel(); }; }, [pdfPageProxy, dpr, zoom]); return { canvasRef }; }; var CanvasLayer = ({ style, background, ...props }) => { const { canvasRef } = useCanvasLayer({ background }); return /* @__PURE__ */ jsx( "canvas", { style: { ...style, position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }, ...props, ref: canvasRef } ); }; // src/hooks/useSelectionDimensions.tsx var MERGE_THRESHOLD = 5; var shouldMergeRects = (rect1, rect2) => { const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height); const horizontallyClose = 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 && horizontallyClose; }; var mergeRects = (rect1, rect2) => { return { left: Math.min(rect1.left, rect2.left), top: Math.min(rect1.top, rect2.top), width: Math.max(rect1.left + rect1.width, rect2.left + rect2.width) - Math.min(rect1.left, rect2.left), height: Math.max(rect1.top + rect1.height, rect2.top + rect2.height) - Math.min(rect1.top, rect2.top), pageNumber: rect1.pageNumber }; }; var consolidateRects = (rects) => { if (rects.length <= 1) return rects; const sortedRects = [...rects].sort((a, b) => a.top - b.top); let hasChanges; do { hasChanges = false; let currentRect = sortedRects[0]; const tempResult = []; for (let i = 1; i < sortedRects.length; i++) { const sorted = sortedRects[i]; if (!currentRect || !sorted) continue; if (shouldMergeRects(currentRect, sorted)) { currentRect = mergeRects(currentRect, sorted); hasChanges = true; } else { tempResult.push(currentRect); currentRect = sorted; } } if (currentRect) tempResult.push(currentRect); sortedRects.length = 0; sortedRects.push(...tempResult); } while (hasChanges); return sortedRects; }; var useSelectionDimensions = () => { const store = PDFStore.useContext(); const getDimension = () => { const selection = window.getSelection(); if (!selection || selection.isCollapsed) return; const range = selection.getRangeAt(0); const highlights = []; const textLayerMap = /* @__PURE__ */ new Map(); const clientRects = Array.from(range.getClientRects()).filter( (rect) => rect.width > 2 && rect.height > 2 ); clientRects.forEach((clientRect) => { const element = document.elementFromPoint( clientRect.left + 1, clientRect.top + clientRect.height / 2 ); const textLayer = element?.closest(".textLayer"); if (!textLayer) return; const pageNumber = parseInt( textLayer.getAttribute("data-page-number") || "1", 10 ); const textLayerRect = textLayer.getBoundingClientRect(); const zoom = store.getState().zoom; const rect = { width: clientRect.width / zoom, height: clientRect.height / zoom, top: (clientRect.top - textLayerRect.top) / zoom, left: (clientRect.left - textLayerRect.left) / zoom, pageNumber }; if (!textLayerMap.has(pageNumber)) { textLayerMap.set(pageNumber, []); } textLayerMap.get(pageNumber)?.push(rect); }); textLayerMap.forEach((rects) => { if (rects.length > 0) { highlights.push(...consolidateRects(rects)); } }); return { highlights: highlights.sort((a, b) => a.pageNumber - b.pageNumber), text: range.toString().trim(), isCollapsed: false }; }; const getSelection = () => getDimension(); return { getDimension, getSelection }; }; var SelectionTooltip = ({ children }) => { const [isOpen, setIsOpen] = useState(false); const lastSelectionRef = useRef(null); const viewportRef = usePdf((state) => state.viewportRef); const { refs, floatingStyles, context } = useFloating({ placement: "bottom", open: isOpen, onOpenChange: setIsOpen, whileElementsMounted: autoUpdate, middleware: [offset(10), shift({ padding: 8 })] }); const dismiss = useDismiss(context); const { getFloatingProps } = useInteractions([dismiss]); const updateTooltipPosition = useCallback(() => { const selection = document.getSelection(); if (!selection || selection.isCollapsed) { setIsOpen(false); lastSelectionRef.current = null; return; } const range = selection.getRangeAt(0); if (!range) return; const rects = range.getClientRects(); const lastRect = rects[rects.length - 1]; lastSelectionRef.current = range; if (lastRect) { refs.setReference({ getBoundingClientRect: () => ({ width: lastRect.width, height: lastRect.height, x: lastRect.left, y: lastRect.bottom, // Position below the last line of selection top: lastRect.bottom, right: lastRect.right, bottom: lastRect.bottom + lastRect.height, left: lastRect.left }), getClientRects: () => [lastRect] }); setIsOpen(true); } else { setIsOpen(false); } }, [refs]); useEffect(() => { const handleSelectionChange = () => { const selection = document.getSelection(); if (selection && viewportRef.current?.contains(selection.anchorNode)) { const anchorNode = selection.anchorNode; const focusNode = selection.focusNode; const isInUnselectableArea = (node) => { if (!node) return false; let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement; while (element) { if (element.getAttribute("data-annotation-tooltip")) { return true; } if (element.hasAttribute("data-floating-ui-portal")) { return true; } element = element.parentElement; } return false; }; if (!isInUnselectableArea(anchorNode) && !isInUnselectableArea(focusNode)) { requestAnimationFrame(updateTooltipPosition); } else { setIsOpen(false); } } else { setIsOpen(false); } }; const handleScroll = () => { if (!isOpen || !lastSelectionRef.current) return; requestAnimationFrame(updateTooltipPosition); }; document.addEventListener("selectionchange", handleSelectionChange); if (viewportRef.current) { viewportRef.current.addEventListener("scroll", handleScroll, { passive: true }); } return () => { document.removeEventListener("selectionchange", handleSelectionChange); if (viewportRef.current) { viewportRef.current.removeEventListener("scroll", handleScroll); } }; }, [refs, isOpen, viewportRef, updateTooltipPosition]); useEffect(() => { const handleFloatingClick = (e) => { if (refs.floating.current?.contains(e.target)) { e.stopPropagation(); } }; document.addEventListener("click", handleFloatingClick); return () => document.removeEventListener("click", handleFloatingClick); }, [refs.floating]); return /* @__PURE__ */ jsx(Fragment, { children: isOpen && /* @__PURE__ */ jsx( "div", { ref: refs.setFloating, style: { ...floatingStyles }, ...getFloatingProps(), children } ) }); }; var defaultColors = [ { color: "#e3b127", localization: { id: "yellow", defaultMessage: "Yellow" } }, { color: "#419931", localization: { id: "green", defaultMessage: "Green" } }, { color: "#4286c9", localization: { id: "blue", defaultMessage: "Blue" } }, { color: "#f246b6", localization: { id: "pink", defaultMessage: "Pink" } }, { color: "#a53dd1", localization: { id: "purple", defaultMessage: "Purple" } }, { color: "#f09037", localization: { id: "orange", defaultMessage: "Orange" } }, { color: "#37f0d4", localization: { id: "teal", defaultMessage: "Teal" } }, { color: "#3d0ff5", localization: { id: "purple", defaultMessage: "Purple" } }, { color: "#f50f26", localization: { id: "red", defaultMessage: "Red" } } ]; var ColorSelectionTool = ({ highlighterColors = defaultColors, onColorSelection }) => { return /* @__PURE__ */ jsx(SelectionTooltip, { children: /* @__PURE__ */ jsx( "div", { style: { display: "flex", gap: "0.5rem", padding: "0.5rem", backgroundColor: "#363636", borderRadius: "0.5rem" }, children: highlighterColors.map((colorItem, index) => /* @__PURE__ */ jsx( "button", { onClick: () => onColorSelection(colorItem), title: colorItem.localization.defaultMessage, "aria-label": colorItem.localization.defaultMessage, style: { width: "1.25rem", height: "1.25rem", borderRadius: "0.25rem", cursor: "pointer", backgroundColor: colorItem.color } }, index )) } ) }); }; // src/utils/selectionUtils.ts var getEndOfHighlight = (selection) => { const lastRectangle = selection.rectangles[selection.rectangles.length - 1]; return lastRectangle.left + lastRectangle.width + 10; }; var getMidHeightOfHighlightLine = (selection) => { const lastRectangle = selection.rectangles[selection.rectangles.length - 1]; return lastRectangle.top + lastRectangle.height / 2; }; var ColoredHighlightComponent = ({ selection }) => { const deleteColoredHighlight = usePdf( (state) => state.deleteColoredHighlight ); const [showButton, setShowButton] = useState(false); return /* @__PURE__ */ jsxs("div", { className: "colored-highlight", children: [ selection.rectangles.map((rect, index) => /* @__PURE__ */ jsx( "span", { onClick: () => setShowButton(!showButton), style: { position: "absolute", top: rect.top, left: rect.left, height: rect.height, width: rect.width, cursor: "pointer", zIndex: 30, backgroundColor: selection.color, // mixBlendMode: "lighten", // changes the color of the text mixBlendMode: "darken", // best results // mixBlendMode: "multiply", // works but coloring has some inconsistencies borderRadius: "0.2rem" } }, `${selection.uuid}-${index}` )), showButton && /* @__PURE__ */ jsx( "button", { style: { backgroundColor: "white", color: "white", borderRadius: "5px", padding: "5px", cursor: "pointer", boxShadow: "2px 2px 5px black", position: "absolute", top: getMidHeightOfHighlightLine(selection), left: getEndOfHighlight(selection), zIndex: 30, transform: "translateY(-50%)" }, onClick: () => deleteColoredHighlight(selection.uuid), children: /* @__PURE__ */ jsx( "svg", { fill: "#000000", version: "1.1", id: "Capa_1", xmlns: "http://www.w3.org/2000/svg", width: "15px", height: "15px", viewBox: "0 0 485 485", children: /* @__PURE__ */ jsx("g", { children: /* @__PURE__ */ jsxs("g", { children: [ /* @__PURE__ */ jsx("rect", { x: "67.224", width: "350.535", height: "71.81" }), /* @__PURE__ */ jsx( "path", { d: "M417.776,92.829H67.237V485h350.537V92.829H417.776z M165.402,431.447h-28.362V146.383h28.362V431.447z M256.689,431.447\n h-28.363V146.383h28.363V431.447z M347.97,431.447h-28.361V146.383h28.361V431.447z" } ) ] }) }) } ) }, `${selection.uuid}-delete-button` ) ] }); }; var ColoredHighlightLayer = ({ onHighlight }) => { const pageNumber = usePDFPageNumber(); const { getDimension } = useSelectionDimensions(); const highlights = usePdf( (state) => state.coloredHighlights ); const addColoredHighlight = usePdf((state) => state.addColoredHighlight); const handleHighlighting = useCallback((color) => { const dimension = getDimension(); if (!dimension) return; const { highlights: highlights2, text } = dimension; if (highlights2[0]) { const highlight = { uuid: v4(), pageNumber: highlights2[0].pageNumber, // usePDFPageNumber() doesn't return the correct page number, so i'm getting the number directly from the first highlight color, rectangles: highlights2, text }; addColoredHighlight(highlight); if (onHighlight) onHighlight(highlight); } }, []); return /* @__PURE__ */ jsxs("div", { className: "colored-highlights-layer", children: [ highlights.filter((selection) => selection.pageNumber === pageNumber).map((selection) => /* @__PURE__ */ jsx( ColoredHighlightComponent, { selection }, selection.uuid )), /* @__PURE__ */ jsx( ColorSelectionTool, { onColorSelection: (colorItem) => handleHighlighting(colorItem.color) } ) ] }); }; // src/components/layers/custom-layer.tsx var CustomLayer = ({ children }) => { const pageNumber = usePDFPageNumber(); return children(pageNumber); }; var convertToPercentString = (rect) => { return { top: `${rect.top}%`, left: `${rect.left}%`, height: `${rect.height}%`, width: `${rect.width}%` }; }; var HighlightLayer = forwardRef(({ asChild, className, style, ...props }, ref) => { const pageNumber = usePDFPageNumber(); const highlights = usePdf((state) => state.highlights); const Comp = asChild ? Slot : "div"; const rects = highlights.filter((area) => area.pageNumber === pageNumber); if (!rects?.length) return null; return /* @__PURE__ */ jsx(Fragment, { children: rects.map((rect, index) => { const { pageNumber: pageNumber2, type, ...coordinates } = rect; let dimensions = coordinates; if (type === "percent") { dimensions = convertToPercentString(rect); } return /* @__PURE__ */ jsx( Comp, { ref, className, style: { position: "absolute", ...dimensions, pointerEvents: "none", zIndex: 30, ...style }, ...props, children: props.children }, `highlight-${pageNumber2}-${index}` ); }) }); }); HighlightLayer.displayName = "HighlightLayer"; var useTextLayer = () => { const textContainerRef = useRef(null); const pageNumber = usePDFPageNumber(); const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber)); useEffect(() => { if (!textContainerRef.current) { return; } const textLayer = new TextLayer({ textContentSource: pdfPageProxy.streamTextContent(), container: textContainerRef.current, viewport: pdfPageProxy.getViewport({ scale: 1 }) }); void textLayer.render(); return () => { textLayer.cancel(); }; }, [pdfPageProxy]); return { textContainerRef, pageNumber: pdfPageProxy.pageNumber }; }; var TextLayer2 = ({ className, style, ...props }) => { const { textContainerRef, pageNumber } = useTextLayer(); return /* @__PURE__ */ jsx( "div", { className: clsx_default("textLayer", className), style: { ...style, position: "absolute", top: 0, left: 0 }, ...props, ...{ ["data-page-number"]: pageNumber }, ref: textContainerRef } ); }; var usePDFOutline = () => { const pdfDocumentProxy = usePdf((state) => state.pdfDocumentProxy); const [outline, setOutline] = useState(); useEffect(() => { const { promise: outline2, cancel } = cancellable( pdfDocumentProxy.getOutline() ); outline2.then( (result) => { setOutline(result); }, () => { } ); return () => { cancel(); }; }, [pdfDocumentProxy]); return outline; }; var HTMLTags = [ "a", "button", "div", "aside", "section", "main", "ul", "li", "input", "canvas" ]; var makePrimitive = (htmlTag) => { const primitive = forwardRef( (props, ref) => { const Renderer = htmlTag; return /* @__PURE__ */ jsx(Renderer, { ...props, ref }); } ); primitive.displayName = `PDFReader.${htmlTag}`; return primitive; }; var Primitive = HTMLTags.reduce((acc, tag) => { acc[tag] = makePrimitive(tag); return acc; }, {}); var OutlineChildItems = ({ ...props }) => { return /* @__PURE__ */ jsx(Primitive.ul, { ...props }); }; var OutlineItem = ({ level = 0, item, children, outlineItem, ...props }) => { if (!item || !outlineItem || !children) { throw new Error("Outline item is required"); } const pdfDocumentProxy = usePdf((state) => state.pdfDocumentProxy); const { jumpToPage } = usePdfJump(); const getDestinationPage = useCallback( async (dest) => { let explicitDest; if (typeof dest === "string") { explicitDest = await pdfDocumentProxy.getDestination(dest); } else if (Array.isArray(dest)) { explicitDest = dest; } else { explicitDest = await dest; } if (!explicitDest) { return; } const explicitRef = explicitDest[0]; const page = await pdfDocumentProxy.getPageIndex(explicitRef); return page; }, [pdfDocumentProxy] ); const navigate = useCallback(() => { if (!item.dest) { return; } getDestinationPage(item.dest).then((page) => { if (!page) { return; } jumpToPage(page, { behavior: "smooth" }); }); }, [item.dest, jumpToPage, getDestinationPage]); return /* @__PURE__ */ jsxs(Primitive.li, { ...props, children: [ /* @__PURE__ */ jsx( "a", { role: "button", tabIndex: 0, onClick: navigate, onKeyUp: (e) => { if (e.key === "Enter") { navigate(); } }, "data-level": level, children: item.title } ), item.items && item.items.length > 0 && cloneElement(children, { // @ts-expect-error we are missing the corect props types children: item.items.map( (item2, index) => cloneElement(outlineItem, { // @ts-expect-error we are missing the corect props types level: level + 1, item: item2, outlineItem, key: index }) ) }) ] }); }; var Outline = ({ chi