UNPKG

@nyazkhan/react-pdf-viewer

Version:

A comprehensive React TypeScript component library for viewing and interacting with PDF files using Mozilla PDF.js. Features include text selection, highlighting, search, sidebar, multiple view modes, and complete PDF.js web viewer functionality.

1,713 lines (1,705 loc) 83.5 kB
"use client" import { configurePDFWorker, getWorkerRetryCount, getWorkerSrc, isWorkerConfiguredProperly, resetWorkerConfiguration, retryWorkerConfiguration } from "./chunk-NHSWJHVN.mjs"; // src/lib/PDFViewer.tsx import React4, { useEffect as useEffect4, useRef as useRef4, useState as useState4, useCallback as useCallback4 } from "react"; // src/lib/PDFToolbar.tsx import React, { useState, useRef } from "react"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; var PDFToolbar = ({ currentPage, totalPages, scale, viewMode = "single", zoomMode = "auto", activeTool = "none", searchTerm = "", searchResults = 0, currentSearchResult = 0, sidebarOpen = false, onPageChange, onScaleChange, onPrevPage, onNextPage, onZoomIn, onZoomOut, onRotate, onOpenFile, onPrint, onDownload, onSearch, onSearchNext, onSearchPrevious, onClearSearch, onToolChange, onZoomToFit, onZoomToWidth, onViewModeChange, onZoomModeChange, onSidebarToggle, onPresentationMode, onDocumentInfo, className = "", style, showPageControls = true, showZoomControls = true, showRotateControls = true, showViewModeControls = true, showOpenOption = true, showSearchOption = true, showPrintOption = true, showDownloadOption = true, showToolSelection = true, showFitOptions = true, showPresentationMode = true }) => { const [pageInput, setPageInput] = useState(currentPage.toString()); const [searchInput, setSearchInput] = useState(searchTerm); const [showSearchBar, setShowSearchBar] = useState(false); const fileInputRef = useRef(null); const handlePageInputChange = (e) => { setPageInput(e.target.value); }; const handlePageInputSubmit = (e) => { e.preventDefault(); const pageNumber = parseInt(pageInput, 10); if (!isNaN(pageNumber) && pageNumber >= 1 && pageNumber <= totalPages) { onPageChange(pageNumber); } else { setPageInput(currentPage.toString()); } }; const handleScaleSelect = (e) => { const newScale = parseFloat(e.target.value); onScaleChange(newScale); }; const handleSearchSubmit = (e) => { e.preventDefault(); if (onSearch && searchInput.trim()) { onSearch(searchInput.trim()); } }; const handleOpenFile = () => { if (fileInputRef.current) { fileInputRef.current.click(); } if (onOpenFile) { onOpenFile(); } }; const handleToolChange = (tool) => { if (onToolChange) { onToolChange(tool); } }; const toggleSearchBar = () => { setShowSearchBar(!showSearchBar); if (!showSearchBar && onClearSearch) { onClearSearch(); setSearchInput(""); } }; React.useEffect(() => { setPageInput(currentPage.toString()); }, [currentPage]); React.useEffect(() => { setSearchInput(searchTerm); }, [searchTerm]); const toolbarStyle = { display: "flex", alignItems: "center", padding: "8px 16px", backgroundColor: "#2c3e50", color: "white", borderBottom: "1px solid #34495e", gap: "8px", flexWrap: "wrap", boxShadow: "0 2px 4px rgba(0,0,0,0.1)", ...style }; const buttonStyle = { padding: "8px 12px", border: "none", borderRadius: "6px", backgroundColor: "#34495e", color: "white", cursor: "pointer", fontSize: "14px", display: "flex", alignItems: "center", gap: "6px", transition: "all 0.2s ease", minWidth: "40px", justifyContent: "center" }; const activeButtonStyle = { ...buttonStyle, backgroundColor: "#3498db", color: "white" }; const disabledButtonStyle = { ...buttonStyle, opacity: 0.4, cursor: "not-allowed", backgroundColor: "#34495e" }; const inputStyle = { padding: "6px 8px", border: "1px solid #495057", borderRadius: "4px", fontSize: "14px", backgroundColor: "#495057", color: "white", textAlign: "center", width: "60px" }; const searchInputStyle = { padding: "6px 12px", border: "1px solid #495057", borderRadius: "4px", fontSize: "14px", backgroundColor: "#495057", color: "white", width: "200px" }; const selectStyle = { padding: "6px 8px", border: "1px solid #495057", borderRadius: "4px", fontSize: "14px", backgroundColor: "#495057", color: "white", minWidth: "80px" }; const separatorStyle = { width: "1px", height: "32px", backgroundColor: "#495057", margin: "0 8px" }; const toolGroupStyle = { display: "flex", gap: "4px", alignItems: "center" }; const labelStyle = { fontSize: "12px", color: "#bdc3c7", fontWeight: "500" }; return /* @__PURE__ */ jsxs("div", { className: `pdf-toolbar ${className}`, style: toolbarStyle, children: [ /* @__PURE__ */ jsx( "input", { ref: fileInputRef, type: "file", accept: ".pdf,application/pdf", style: { display: "none" }, onChange: (e) => { } } ), /* @__PURE__ */ jsx("div", { style: toolGroupStyle, children: /* @__PURE__ */ jsx( "button", { onClick: onSidebarToggle, style: sidebarOpen ? activeButtonStyle : buttonStyle, title: "Toggle sidebar (F4)", children: "\u{1F5C2}\uFE0F" } ) }), /* @__PURE__ */ jsx("div", { style: separatorStyle }), showOpenOption && /* @__PURE__ */ jsx("div", { style: toolGroupStyle, children: /* @__PURE__ */ jsx( "button", { onClick: handleOpenFile, style: buttonStyle, title: "Open PDF file (Ctrl+O)", children: "\u{1F4C1} Open" } ) }), showOpenOption && /* @__PURE__ */ jsx("div", { style: separatorStyle }), showPageControls && /* @__PURE__ */ jsxs("div", { style: toolGroupStyle, children: [ /* @__PURE__ */ jsx("span", { style: labelStyle, children: "PAGE" }), /* @__PURE__ */ jsx( "button", { onClick: onPrevPage, disabled: currentPage <= 1, style: currentPage <= 1 ? disabledButtonStyle : buttonStyle, title: "Previous page (\u2190)", children: "\u25C0" } ), /* @__PURE__ */ jsxs("form", { onSubmit: handlePageInputSubmit, style: { display: "flex", alignItems: "center", gap: "4px" }, children: [ /* @__PURE__ */ jsx( "input", { type: "text", value: pageInput, onChange: handlePageInputChange, style: inputStyle, title: "Go to page" } ), /* @__PURE__ */ jsxs("span", { style: { fontSize: "14px", color: "#bdc3c7" }, children: [ "/ ", totalPages ] }) ] }), /* @__PURE__ */ jsx( "button", { onClick: onNextPage, disabled: currentPage >= totalPages, style: currentPage >= totalPages ? disabledButtonStyle : buttonStyle, title: "Next page (\u2192)", children: "\u25B6" } ) ] }), showPageControls && showZoomControls && /* @__PURE__ */ jsx("div", { style: separatorStyle }), showZoomControls && /* @__PURE__ */ jsxs("div", { style: toolGroupStyle, children: [ /* @__PURE__ */ jsx("span", { style: labelStyle, children: "ZOOM" }), /* @__PURE__ */ jsx( "button", { onClick: onZoomOut, style: buttonStyle, title: "Zoom out (-)", children: "\u{1F50D}\u2212" } ), /* @__PURE__ */ jsxs( "select", { value: scale, onChange: handleScaleSelect, style: selectStyle, title: "Zoom level", children: [ /* @__PURE__ */ jsx("option", { value: 0.25, children: "25%" }), /* @__PURE__ */ jsx("option", { value: 0.5, children: "50%" }), /* @__PURE__ */ jsx("option", { value: 0.75, children: "75%" }), /* @__PURE__ */ jsx("option", { value: 1, children: "100%" }), /* @__PURE__ */ jsx("option", { value: 1.25, children: "125%" }), /* @__PURE__ */ jsx("option", { value: 1.5, children: "150%" }), /* @__PURE__ */ jsx("option", { value: 2, children: "200%" }), /* @__PURE__ */ jsx("option", { value: 3, children: "300%" }), /* @__PURE__ */ jsx("option", { value: 4, children: "400%" }) ] } ), /* @__PURE__ */ jsx( "button", { onClick: onZoomIn, style: buttonStyle, title: "Zoom in (+)", children: "\u{1F50D}+" } ), showFitOptions && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "button", { onClick: onZoomToFit, style: zoomMode === "page-fit" ? activeButtonStyle : buttonStyle, title: "Fit to page", children: "\u{1F4C4} Fit" } ), /* @__PURE__ */ jsx( "button", { onClick: onZoomToWidth, style: zoomMode === "page-width" ? activeButtonStyle : buttonStyle, title: "Fit to width", children: "\u2194 Width" } ), /* @__PURE__ */ jsx( "button", { onClick: () => onZoomModeChange?.("actual"), style: zoomMode === "actual" ? activeButtonStyle : buttonStyle, title: "Actual size", children: "\u{1F3AF} Actual" } ) ] }) ] }), showViewModeControls && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("div", { style: separatorStyle }), /* @__PURE__ */ jsxs("div", { style: toolGroupStyle, children: [ /* @__PURE__ */ jsx("span", { style: labelStyle, children: "VIEW" }), /* @__PURE__ */ jsx( "button", { onClick: () => onViewModeChange?.("single"), style: viewMode === "single" ? activeButtonStyle : buttonStyle, title: "Single page view", children: "\u{1F4C4} Single" } ), /* @__PURE__ */ jsx( "button", { onClick: () => onViewModeChange?.("continuous"), style: viewMode === "continuous" ? activeButtonStyle : buttonStyle, title: "Continuous scroll view", children: "\u{1F4DC} Scroll" } ), /* @__PURE__ */ jsx( "button", { onClick: () => onViewModeChange?.("two-page"), style: viewMode === "two-page" ? activeButtonStyle : buttonStyle, title: "Two-page view", children: "\u{1F4D6} Two Page" } ), /* @__PURE__ */ jsx( "button", { onClick: () => onViewModeChange?.("book"), style: viewMode === "book" ? activeButtonStyle : buttonStyle, title: "Book view", children: "\u{1F4DA} Book" } ) ] }) ] }), showZoomControls && showToolSelection && /* @__PURE__ */ jsx("div", { style: separatorStyle }), showToolSelection && /* @__PURE__ */ jsxs("div", { style: toolGroupStyle, children: [ /* @__PURE__ */ jsx("span", { style: labelStyle, children: "TOOLS" }), /* @__PURE__ */ jsx( "button", { onClick: () => handleToolChange("pan"), style: activeTool === "pan" ? activeButtonStyle : buttonStyle, title: "Pan tool - Click and drag to move around", children: "\u270B Pan" } ), /* @__PURE__ */ jsx( "button", { onClick: () => handleToolChange("selection"), style: activeTool === "selection" ? activeButtonStyle : buttonStyle, title: "Selection tool - Select text and areas", children: "\u{1F4C4} Select" } ), /* @__PURE__ */ jsx( "button", { onClick: () => handleToolChange("annotation"), style: activeTool === "annotation" ? activeButtonStyle : buttonStyle, title: "Annotation tool - Add and edit annotations", children: "\u270F\uFE0F Annotate" } ) ] }), showToolSelection && showSearchOption && /* @__PURE__ */ jsx("div", { style: separatorStyle }), showSearchOption && /* @__PURE__ */ jsxs("div", { style: toolGroupStyle, children: [ /* @__PURE__ */ jsx( "button", { onClick: toggleSearchBar, style: showSearchBar ? activeButtonStyle : buttonStyle, title: "Search in document", children: "\u{1F50D} Search" } ), showSearchBar && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs("form", { onSubmit: handleSearchSubmit, style: { display: "flex", gap: "4px", alignItems: "center" }, children: [ /* @__PURE__ */ jsx( "input", { type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), placeholder: "Search in PDF...", style: searchInputStyle } ), /* @__PURE__ */ jsx( "button", { type: "submit", style: buttonStyle, title: "Search", children: "\u{1F50D}" } ) ] }), searchResults > 0 && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "4px", alignItems: "center" }, children: [ /* @__PURE__ */ jsxs("span", { style: { fontSize: "12px", color: "#bdc3c7" }, children: [ currentSearchResult + 1, " of ", searchResults ] }), /* @__PURE__ */ jsx( "button", { onClick: onSearchPrevious, disabled: currentSearchResult <= 0, style: currentSearchResult <= 0 ? disabledButtonStyle : buttonStyle, title: "Previous result", children: "\u25B2" } ), /* @__PURE__ */ jsx( "button", { onClick: onSearchNext, disabled: currentSearchResult >= searchResults - 1, style: currentSearchResult >= searchResults - 1 ? disabledButtonStyle : buttonStyle, title: "Next result", children: "\u25BC" } ), /* @__PURE__ */ jsx( "button", { onClick: () => { if (onClearSearch) onClearSearch(); setSearchInput(""); setShowSearchBar(false); }, style: buttonStyle, title: "Clear search", children: "\u2715" } ) ] }) ] }) ] }), showSearchOption && (showRotateControls || showPrintOption || showDownloadOption) && /* @__PURE__ */ jsx("div", { style: separatorStyle }), /* @__PURE__ */ jsxs("div", { style: toolGroupStyle, children: [ showRotateControls && onRotate && /* @__PURE__ */ jsx( "button", { onClick: onRotate, style: buttonStyle, title: "Rotate document clockwise (R)", children: "\u21BB Rotate" } ), showPresentationMode && onPresentationMode && /* @__PURE__ */ jsx( "button", { onClick: onPresentationMode, style: buttonStyle, title: "Presentation mode (Ctrl+Alt+P)", children: "\u{1F3A6} Present" } ), /* @__PURE__ */ jsx( "button", { onClick: onDocumentInfo, style: buttonStyle, title: "Document properties", children: "\u2139\uFE0F Info" } ), showPrintOption && onPrint && /* @__PURE__ */ jsx( "button", { onClick: onPrint, style: buttonStyle, title: "Print document (Ctrl+P)", children: "\u{1F5A8}\uFE0F Print" } ), showDownloadOption && onDownload && /* @__PURE__ */ jsx( "button", { onClick: onDownload, style: buttonStyle, title: "Download PDF (Ctrl+S)", children: "\u{1F4BE} Download" } ) ] }) ] }); }; // src/lib/PDFSidebar.tsx import { useState as useState2, useEffect, useCallback, useRef as useRef2 } from "react"; import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime"; var PDFSidebar = ({ isOpen, activeView, pdf, currentPage, outline, attachments, onToggle, onViewChange, onPageSelect, onOutlineClick, className = "", style }) => { const [thumbnails, setThumbnails] = useState2({}); const [loadingThumbnails, setLoadingThumbnails] = useState2(/* @__PURE__ */ new Set()); const [expandedOutlineItems, setExpandedOutlineItems] = useState2(/* @__PURE__ */ new Set()); const thumbnailsContainerRef = useRef2(null); const observer = useRef2(null); const loadThumbnail = useCallback(async (pageNum) => { if (!pdf || thumbnails[pageNum] || loadingThumbnails.has(pageNum)) return; setLoadingThumbnails((prev) => new Set(prev).add(pageNum)); try { const page = await pdf.getPage(pageNum); const viewport = page.getViewport({ scale: 0.2 }); const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); canvas.height = viewport.height; canvas.width = viewport.width; const renderContext = { canvasContext: context, viewport }; await page.render(renderContext).promise; const thumbnailUrl = canvas.toDataURL(); setThumbnails((prev) => ({ ...prev, [pageNum]: thumbnailUrl })); } catch (error) { console.warn(`Failed to load thumbnail for page ${pageNum}:`, error); } finally { setLoadingThumbnails((prev) => { const newSet = new Set(prev); newSet.delete(pageNum); return newSet; }); } }, [pdf, thumbnails, loadingThumbnails]); useEffect(() => { if (activeView !== "thumbnails" || !thumbnailsContainerRef.current) return; observer.current = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const pageNum = parseInt(entry.target.getAttribute("data-page") || "0"); if (pageNum > 0) { loadThumbnail(pageNum); } } }); }, { rootMargin: "50px" } ); const thumbnailElements = thumbnailsContainerRef.current.querySelectorAll("[data-page]"); thumbnailElements.forEach((el) => observer.current?.observe(el)); return () => { if (observer.current) { observer.current.disconnect(); } }; }, [activeView, pdf, loadThumbnail]); const handleOutlineToggle = useCallback((itemId) => { setExpandedOutlineItems((prev) => { const newSet = new Set(prev); if (newSet.has(itemId)) { newSet.delete(itemId); } else { newSet.add(itemId); } return newSet; }); }, []); const renderOutlineItems = useCallback((items, level = 0) => { return items.map((item, index) => { const itemId = `${level}-${index}`; const hasChildren = item.items && item.items.length > 0; const isExpanded = expandedOutlineItems.has(itemId); return /* @__PURE__ */ jsxs2("div", { className: "pdf-outline-item", children: [ /* @__PURE__ */ jsxs2( "div", { className: "pdf-outline-item-content", style: { paddingLeft: `${level * 16 + 8}px`, display: "flex", alignItems: "center", padding: "4px 8px", cursor: "pointer", fontSize: "14px", fontWeight: item.bold ? "bold" : "normal", fontStyle: item.italic ? "italic" : "normal", color: item.color ? `rgb(${item.color.join(",")})` : "inherit" }, onClick: () => { if (item.dest) { onOutlineClick(item.dest); } if (hasChildren) { handleOutlineToggle(itemId); } }, children: [ hasChildren && /* @__PURE__ */ jsx2( "span", { style: { marginRight: "4px", transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)", transition: "transform 0.2s ease" }, children: "\u25B6" } ), /* @__PURE__ */ jsx2("span", { children: item.title }) ] } ), hasChildren && isExpanded && /* @__PURE__ */ jsx2("div", { className: "pdf-outline-children", children: renderOutlineItems(item.items, level + 1) }) ] }, itemId); }); }, [expandedOutlineItems, handleOutlineToggle, onOutlineClick]); if (!isOpen) return null; const sidebarStyle = { width: "250px", height: "100%", backgroundColor: "#f8f9fa", borderRight: "1px solid #dee2e6", display: "flex", flexDirection: "column", overflow: "hidden", ...style }; const headerStyle = { display: "flex", borderBottom: "1px solid #dee2e6", backgroundColor: "#e9ecef" }; const tabStyle = { flex: 1, padding: "8px 12px", border: "none", backgroundColor: "transparent", cursor: "pointer", fontSize: "12px", textAlign: "center", textTransform: "uppercase", fontWeight: "500", transition: "background-color 0.2s ease" }; const activeTabStyle = { ...tabStyle, backgroundColor: "#fff", borderBottom: "2px solid #007bff" }; const contentStyle = { flex: 1, overflow: "auto", padding: "8px" }; const closeButtonStyle = { position: "absolute", top: "8px", right: "8px", background: "none", border: "none", fontSize: "16px", cursor: "pointer", color: "#6c757d", padding: "4px", borderRadius: "4px" }; return /* @__PURE__ */ jsxs2("div", { className: `pdf-sidebar ${className}`, style: sidebarStyle, children: [ /* @__PURE__ */ jsx2("button", { style: closeButtonStyle, onClick: onToggle, title: "Close sidebar", children: "\u2715" }), /* @__PURE__ */ jsxs2("div", { style: headerStyle, children: [ /* @__PURE__ */ jsx2( "button", { style: activeView === "thumbnails" ? activeTabStyle : tabStyle, onClick: () => onViewChange("thumbnails"), title: "Show page thumbnails", children: "\u{1F4C4} Pages" } ), /* @__PURE__ */ jsx2( "button", { style: activeView === "outline" ? activeTabStyle : tabStyle, onClick: () => onViewChange("outline"), title: "Show document outline", children: "\u{1F4CB} Outline" } ), /* @__PURE__ */ jsx2( "button", { style: activeView === "attachments" ? activeTabStyle : tabStyle, onClick: () => onViewChange("attachments"), title: "Show attachments", children: "\u{1F4CE} Files" } ), /* @__PURE__ */ jsx2( "button", { style: activeView === "layers" ? activeTabStyle : tabStyle, onClick: () => onViewChange("layers"), title: "Show layers", children: "\u{1F5C2}\uFE0F Layers" } ) ] }), /* @__PURE__ */ jsxs2("div", { style: contentStyle, children: [ activeView === "thumbnails" && /* @__PURE__ */ jsx2("div", { ref: thumbnailsContainerRef, className: "pdf-thumbnails", children: pdf && Array.from({ length: pdf.numPages }, (_, i) => i + 1).map((pageNum) => /* @__PURE__ */ jsxs2( "div", { "data-page": pageNum, className: `pdf-thumbnail ${pageNum === currentPage ? "active" : ""}`, style: { marginBottom: "8px", padding: "8px", border: pageNum === currentPage ? "2px solid #007bff" : "1px solid #dee2e6", borderRadius: "4px", cursor: "pointer", backgroundColor: pageNum === currentPage ? "#e3f2fd" : "white", textAlign: "center" }, onClick: () => onPageSelect(pageNum), children: [ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: "4px", fontSize: "12px", fontWeight: "500" }, children: [ "Page ", pageNum ] }), thumbnails[pageNum] ? /* @__PURE__ */ jsx2( "img", { src: thumbnails[pageNum], alt: `Page ${pageNum}`, style: { maxWidth: "100%", height: "auto", border: "1px solid #dee2e6" } } ) : loadingThumbnails.has(pageNum) ? /* @__PURE__ */ jsx2("div", { style: { height: "120px", display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "#f8f9fa", border: "1px solid #dee2e6" }, children: "Loading..." }) : /* @__PURE__ */ jsx2("div", { style: { height: "120px", backgroundColor: "#f8f9fa", border: "1px solid #dee2e6" } }) ] }, pageNum )) }), activeView === "outline" && /* @__PURE__ */ jsx2("div", { className: "pdf-outline", children: outline && outline.length > 0 ? renderOutlineItems(outline) : /* @__PURE__ */ jsx2("div", { style: { textAlign: "center", color: "#6c757d", fontSize: "14px", padding: "20px" }, children: "No document outline available" }) }), activeView === "attachments" && /* @__PURE__ */ jsx2("div", { className: "pdf-attachments", children: attachments && attachments.length > 0 ? attachments.map((attachment, index) => /* @__PURE__ */ jsxs2( "div", { style: { padding: "8px", marginBottom: "4px", border: "1px solid #dee2e6", borderRadius: "4px", cursor: "pointer", display: "flex", alignItems: "center", gap: "8px" }, onClick: () => { const blob = new Blob([attachment.content]); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = attachment.filename; link.click(); URL.revokeObjectURL(url); }, children: [ /* @__PURE__ */ jsx2("span", { children: "\u{1F4CE}" }), /* @__PURE__ */ jsx2("span", { style: { fontSize: "14px" }, children: attachment.filename }) ] }, index )) : /* @__PURE__ */ jsx2("div", { style: { textAlign: "center", color: "#6c757d", fontSize: "14px", padding: "20px" }, children: "No attachments found" }) }), activeView === "layers" && /* @__PURE__ */ jsx2("div", { className: "pdf-layers", children: /* @__PURE__ */ jsx2("div", { style: { textAlign: "center", color: "#6c757d", fontSize: "14px", padding: "20px" }, children: "Layers functionality coming soon" }) }) ] }) ] }); }; // src/lib/PDFPage.tsx import { useEffect as useEffect2, useRef as useRef3, useState as useState3, useCallback as useCallback2 } from "react"; // src/lib/PDFHighlight.tsx import { jsx as jsx3 } from "react/jsx-runtime"; var PDFHighlight = ({ highlights, pageNumber, viewport, onHighlightClick, className = "", style }) => { const handleHighlightClick = (highlight) => { if (onHighlightClick) { onHighlightClick(highlight); } if (highlight.onClick) { highlight.onClick(highlight); } }; const renderHighlight = (highlight) => { const { rects, color = "#ffff00", opacity = 0.3 } = highlight; return rects.map((rect, index) => { const canvasRect = { left: rect.left * viewport.scale, top: (viewport.height - rect.top - rect.height) * viewport.scale, width: rect.width * viewport.scale, height: rect.height * viewport.scale }; console.log(`PDFHighlight render:`, { original: rect, viewport: { width: viewport.width, height: viewport.height, scale: viewport.scale }, transformed: canvasRect }); const highlightStyle = { position: "absolute", left: `${canvasRect.left}px`, top: `${canvasRect.top}px`, width: `${canvasRect.width}px`, height: `${canvasRect.height}px`, backgroundColor: color, opacity, pointerEvents: "auto", cursor: highlight.onClick || onHighlightClick ? "pointer" : "default", zIndex: 10, border: "1px solid red" // Debug border to make highlights visible }; return /* @__PURE__ */ jsx3( "div", { style: highlightStyle, onClick: () => handleHighlightClick(highlight), title: highlight.content || `Highlight ${highlight.id}`, className: "pdf-highlight-rect" }, `${highlight.id}-${index}` ); }); }; const containerStyle = { position: "absolute", top: 0, left: 0, width: `${viewport.width}px`, height: `${viewport.height}px`, pointerEvents: "none", ...style }; return /* @__PURE__ */ jsx3("div", { className: `pdf-highlights ${className}`, style: containerStyle, children: highlights.filter((highlight) => highlight.pageNumber === pageNumber).map((highlight) => /* @__PURE__ */ jsx3("div", { className: "pdf-highlight", children: renderHighlight(highlight) }, highlight.id)) }); }; // src/lib/PDFPage.tsx import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime"; var PDFPage = ({ pageNumber, scale = 1, rotation = 0, pdf, onPageRender, onError, className = "", style, enableTextSelection = true, highlights = [] }) => { const canvasRef = useRef3(null); const textLayerRef = useRef3(null); const containerRef = useRef3(null); const renderingRef = useRef3(false); const renderTaskRef = useRef3(null); const [page, setPage] = useState3(null); const [viewport, setViewport] = useState3(null); const [rendering, setRendering] = useState3(false); const cancelRender = useCallback2(() => { if (renderTaskRef.current) { try { renderTaskRef.current.cancel(); } catch (error) { console.warn("Error canceling render task:", error); } renderTaskRef.current = null; } }, []); const renderPage = useCallback2(async () => { if (!pdf || !canvasRef.current) return; if (renderingRef.current) { console.debug("Render already in progress, skipping"); return; } try { renderingRef.current = true; setRendering(true); cancelRender(); const pdfPage = await pdf.getPage(pageNumber); setPage(pdfPage); const pdfViewport = pdfPage.getViewport({ scale, rotation }); setViewport(pdfViewport); const canvas = canvasRef.current; const context = canvas.getContext("2d"); if (!context) { throw new Error("Could not get canvas context"); } canvas.height = pdfViewport.height; canvas.width = pdfViewport.width; if (enableTextSelection && textLayerRef.current) { textLayerRef.current.innerHTML = ""; textLayerRef.current.style.width = `${pdfViewport.width}px`; textLayerRef.current.style.height = `${pdfViewport.height}px`; } const renderContext = { canvasContext: context, viewport: pdfViewport, background: "white" }; const task = pdfPage.render(renderContext); renderTaskRef.current = task; await task.promise; await new Promise((resolve) => setTimeout(resolve, 100)); if (enableTextSelection && textLayerRef.current) { await renderTextLayer(pdfPage, pdfViewport); } if (onPageRender) { onPageRender(pdfPage); } } catch (error) { if (error && typeof error === "object" && "name" in error && error.name === "RenderingCancelledException") { return; } const errorMessage = error instanceof Error ? error.message : "Failed to render page"; if (errorMessage.includes("Transport destroyed") || errorMessage.includes("Worker was terminated") || errorMessage.includes("destroyed")) { console.warn("PDF page render interrupted due to worker termination:", errorMessage); return; } console.error("PDF page render error:", errorMessage); if (onError) { onError(new Error(errorMessage)); } } finally { renderingRef.current = false; setRendering(false); renderTaskRef.current = null; } }, [pdf, pageNumber, scale, rotation, enableTextSelection, onPageRender, onError]); const renderTextLayer = useCallback2(async (pdfPage, pdfViewport) => { if (!textLayerRef.current) return; try { console.debug(`Rendering text layer for page ${pageNumber} at scale ${pdfViewport.scale}`); textLayerRef.current.innerHTML = ""; textLayerRef.current.style.width = `${pdfViewport.width}px`; textLayerRef.current.style.height = `${pdfViewport.height}px`; const textContent = await pdfPage.getTextContent({ includeMarkedContent: false, disableNormalization: false }); if (textContent.items.length === 0) return; textContent.items.forEach((textItem, index) => { if (!textItem.str || typeof textItem.str !== "string" || textItem.str.trim() === "") return; if (!textItem.transform || textItem.transform.length < 6) return; const [scaleX, skewY, skewX, scaleY, translateX, translateY] = textItem.transform; const style2 = textContent.styles[textItem.fontName] || {}; const fontSize = Math.sqrt(scaleX * scaleX + skewY * skewY) * pdfViewport.scale; const fontHeight = Math.sqrt(skewX * skewX + scaleY * scaleY) * pdfViewport.scale; const left = translateX * pdfViewport.scale; const top = (translateY - Math.sqrt(skewX * skewX + scaleY * scaleY)) * pdfViewport.scale; const textSpan = document.createElement("span"); textSpan.textContent = textItem.str; textSpan.style.position = "absolute"; textSpan.style.whiteSpace = "pre"; textSpan.style.color = "transparent"; textSpan.style.fontSize = `${fontSize}px`; textSpan.style.fontFamily = style2.fontFamily || "sans-serif"; textSpan.style.left = `${left}px`; textSpan.style.top = `${top}px`; if (textItem.width > 0) { const textWidth = textItem.width * pdfViewport.scale; textSpan.style.width = `${textWidth}px`; const ctx = document.createElement("canvas").getContext("2d"); ctx.font = `${fontSize}px ${style2.fontFamily || "sans-serif"}`; const measuredWidth = ctx.measureText(textItem.str).width; if (measuredWidth > 0) { const scaleFactorX = textWidth / measuredWidth; if (Math.abs(scaleFactorX - 1) > 0.01) { textSpan.style.transform = `scaleX(${scaleFactorX})`; textSpan.style.transformOrigin = "0% 0%"; } } } textSpan.style.userSelect = "text"; textSpan.style.pointerEvents = "auto"; textSpan.style.cursor = "text"; textLayerRef.current?.appendChild(textSpan); }); } catch (error) { console.error("Error rendering text layer:", error); if (onError) { onError(new Error(`Text layer rendering failed: ${error instanceof Error ? error.message : "Unknown error"}`)); } } }, [pageNumber, enableTextSelection, onError]); useEffect2(() => { renderPage(); }, [renderPage]); useEffect2(() => { if (page && viewport && enableTextSelection && textLayerRef.current && !rendering) { const timeoutId = setTimeout(() => { console.debug(`Scale changed to ${scale}, re-rendering text layer`); renderTextLayer(page, viewport).catch((error) => { console.warn("Failed to re-render text layer after scale change:", error); }); }, 50); return () => clearTimeout(timeoutId); } }, [scale, page, viewport, enableTextSelection, rendering, renderTextLayer]); useEffect2(() => { return cancelRender; }, [cancelRender]); const containerStyle = { position: "relative", display: "inline-block", backgroundColor: "white", boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)", margin: "10px", ...style }; const canvasStyle = { display: "block", maxWidth: "100%", height: "auto" }; const textLayerStyle = { position: "absolute", top: 0, left: 0, width: viewport ? `${viewport.width}px` : "100%", height: viewport ? `${viewport.height}px` : "100%", overflow: "hidden", lineHeight: 1, userSelect: enableTextSelection ? "text" : "none", pointerEvents: enableTextSelection ? "auto" : "none", cursor: enableTextSelection ? "text" : "default", // Ensure text layer is above canvas but below highlights zIndex: 1, // Ensure crisp text rendering without GPU acceleration that can break alignment fontSmooth: "always", WebkitFontSmoothing: "antialiased", // Prevent text selection highlighting from showing (since text is transparent) WebkitUserSelect: enableTextSelection ? "text" : "none", MozUserSelect: enableTextSelection ? "text" : "none", msUserSelect: enableTextSelection ? "text" : "none" }; const highlightLayerStyle = { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, pointerEvents: "none", // Ensure highlights are above text layer zIndex: 2 }; return /* @__PURE__ */ jsxs3( "div", { className: `pdf-page ${className}`, style: containerStyle, ref: containerRef, children: [ /* @__PURE__ */ jsx4( "canvas", { ref: canvasRef, style: canvasStyle } ), enableTextSelection && /* @__PURE__ */ jsx4( "div", { ref: textLayerRef, style: textLayerStyle, className: "pdf-text-layer" } ), highlights.length > 0 && viewport && (() => { console.log(`PDFPage rendering highlights for page ${pageNumber}:`, { totalHighlights: highlights.length, pageHighlights: highlights.filter((h) => h.pageNumber === pageNumber).length, viewport: { width: viewport.width, height: viewport.height, scale: viewport.scale } }); return /* @__PURE__ */ jsx4("div", { style: highlightLayerStyle, className: "pdf-highlight-layer", children: /* @__PURE__ */ jsx4( PDFHighlight, { highlights: highlights.filter((h) => h.pageNumber === pageNumber), pageNumber, viewport } ) }); })() ] } ); }; // src/lib/PDFLoader.ts import * as pdfjsLib from "pdfjs-dist"; var PDFLoader = class { // 30 seconds /** * Load PDF with automatic retry on worker failures */ static async loadPDF(options) { const { file, retries = this.DEFAULT_RETRIES, timeout = this.DEFAULT_TIMEOUT } = options; let lastError = null; let retriesUsed = 0; for (let attempt = 0; attempt <= retries; attempt++) { try { console.log(`PDF Load attempt ${attempt + 1}/${retries + 1}, worker: ${getWorkerSrc()}`); const loadingTask = await this.createLoadingTask(file); const pdf = await Promise.race([ loadingTask.promise, new Promise( (_, reject) => setTimeout(() => reject(new Error("PDF loading timeout")), timeout) ) ]); console.log("PDF loaded successfully"); return { pdf, error: null, retriesUsed: attempt }; } catch (error) { retriesUsed = attempt + 1; lastError = error instanceof Error ? error : new Error(String(error)); console.warn(`PDF load attempt ${attempt + 1} failed:`, lastError.message); if (this.isWorkerError(lastError) && attempt < retries) { console.log("Worker error detected, trying alternative worker..."); await new Promise((resolve) => setTimeout(resolve, 1e3)); const retrySuccess = retryWorkerConfiguration(); if (!retrySuccess) { console.warn("No more worker fallbacks available"); break; } await new Promise((resolve) => setTimeout(resolve, 1e3)); } else if (attempt < retries) { await new Promise((resolve) => setTimeout(resolve, 500)); } } } if (!lastError || this.isWorkerError(lastError)) { console.log("All attempts failed, trying complete worker reset..."); try { const { resetWorkerConfiguration: resetWorkerConfiguration2 } = await import("./worker-CE6ODOT6.mjs"); resetWorkerConfiguration2(); await new Promise((resolve) => setTimeout(resolve, 2e3)); const loadingTask = await this.createLoadingTask(file); const pdf = await Promise.race([ loadingTask.promise, new Promise( (_, reject) => setTimeout(() => reject(new Error("Final attempt timeout")), 15e3) ) ]); console.log("PDF loaded successfully after complete worker reset"); return { pdf, error: null, retriesUsed: retriesUsed + 1 }; } catch (finalError) { console.error("Final attempt after worker reset also failed:", finalError); lastError = finalError instanceof Error ? finalError : new Error(String(finalError)); } } return { pdf: null, error: lastError, retriesUsed }; } /** * Create PDF loading task based on file type */ static async createLoadingTask(file) { if (typeof file === "string") { return pdfjsLib.getDocument({ url: file, // Add some additional options for better reliability stopAtErrors: false, maxImageSize: 1024 * 1024, // 1MB max image size cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/cmaps/`, cMapPacked: true }); } else if (file instanceof File) { const arrayBuffer = await file.arrayBuffer(); return pdfjsLib.getDocument({ data: arrayBuffer, stopAtErrors: false }); } else { return pdfjsLib.getDocument({ data: file, stopAtErrors: false }); } } /** * Check if an error is related to worker issues */ static isWorkerError(error) { const workerErrorKeywords = [ "worker", "terminated", "destroyed", "Worker", "Transport destroyed", "Cannot resolve module", "Failed to fetch", "NetworkError", "CORS", "Invalid `workerSrc`", "workerSrc" ]; return workerErrorKeywords.some( (keyword) => error.message.toLowerCase().includes(keyword.toLowerCase()) ); } /** * Preload worker to check if it's working */ static async testWorker() { try { const testPdfData = new Uint8Array([ 37, 80, 68, 70, 45, 49, 46, 52, 10, 37, 226, 227, 207, 211, 10, 49, 32, 48, 32, 111, 98, 106, 10, 60, 60, 10, 47, 84, 121, 112, 101, 32, 47, 67, 97, 116, 97, 108, 111, 103, 10, 47, 80, 97, 103, 101, 115, 32, 50, 32, 48, 32, 82, 10, 62, 62, 10, 101, 110, 100, 111, 98, 106, 10, 50, 32, 48, 32, 111, 98, 106, 10, 60, 60, 10, 47, 84, 121, 112, 101, 32, 47, 80, 97, 103, 101, 115, 10, 47, 75, 105, 100, 115, 32, 91, 51, 32, 48, 32, 82, 93, 10, 47, 67, 111, 117, 110, 116, 32, 49, 10, 62, 62, 10, 101, 110, 100, 111, 98, 106, 10, 51, 32, 48, 32, 111, 98, 106, 10, 60, 60, 10, 47, 84, 121, 112, 101, 32, 47, 80, 97, 103, 101, 10, 47, 80, 97, 114, 101, 110, 116, 32, 50, 32, 48, 32, 82, 10, 47, 77, 101, 100, 105, 97, 66, 111, 120, 32, 91, 48, 32, 48, 32, 54, 49, 50, 32, 55, 57, 50, 93, 10, 62, 62, 10, 101, 110, 100, 111, 98, 106, 10, 120, 114, 101, 102, 10, 48, 32, 52, 10, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 32, 54, 53, 53, 51, 53, 32, 102, 32, 10, 48, 48, 48, 48, 48, 48, 48, 48, 49, 53, 32, 48, 48, 48, 48, 48, 32, 110, 32, 10, 48, 48, 48, 48, 48, 48, 48, 48, 55, 52, 32, 48, 48, 48, 48, 48, 32, 110, 32, 10, 48, 48, 48, 48, 48, 48, 48, 49, 50, 49, 32, 48, 48, 48, 48, 48, 32, 110, 32, 10, 116, 114, 97, 105, 108, 101, 114, 10, 60, 60, 10, 47, 83, 105, 122, 101, 32, 52, 10, 47, 82, 111, 111, 116, 32, 49, 32, 48, 32, 82, 10, 62, 62, 10, 115, 116, 97, 114, 116, 120, 114, 101, 102, 10, 49, 55, 56, 10, 37, 37, 69, 79, 70 ]); const loadingTask = pdfjsLib.getDocument({ data: testPdfData }); const testPdf = await Promise.race([ loadingTask.promise, new Promise( (_, reject) => setTimeout(() => reject(new Error("Worker test timeout")), 5e3) ) ]); await testPdf.destroy(); return true; } catch (error) { console.warn("Worker test failed:", error); return false; } } }; PDFLoader.DEFAULT_RETRIES = 3; PDFLoader.DEFAULT_TIMEOUT = 3e4; // src/lib/useKeyboardShortcuts.ts import { useEffect as useEffect3, useCallback as useCallback3 } from "react"; var useKeyboardShortcuts = (enabled = true, handlers = {}) => { const handleKeyDown = useCallback3((event) => { if (!enabled) return; const target = event.target; if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) { return; } const { key, ctrlKey, metaKey, shiftKey, altKey } = event; const modKey = ctrlKey || metaKey; let preventDefault = true; switch (key.toLowerCase()) { // Navigation shortcuts case "n": case "j": if (!modKey) { handlers.onNextPage?.(); } else { preventDefault = false; } break; case "p": case "k": if (!modKey) { handlers.onPreviousPage?.(); } else if (modKey && altKey && key === "p") { handlers.onPresentationMode?.(); } else if (modKey && key === "p") { handlers.onPrint?.(); } else { preventDefault = false; } break; case "home": handlers.onFirstPage?.(); break; case "end": handlers.onLastPage?.(); break; case "arrowleft": if (!modKey) { handlers.onPreviousPage?.(); } else { preventDefault = false; } break; case "arrowright": if (!modKey) { handlers.onNextPage?.(); } else { preventDefault = false; } break; case "arrowup": case "pageup": handlers.onPreviousPage?.(); break; case "arrowdown": case "pagedown": handlers.onNe