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,359 lines (1,350 loc) 94.2 kB
"use client" "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/lib/worker.ts var worker_exports = {}; __export(worker_exports, { WorkerSources: () => WorkerSources, configurePDFWorker: () => configurePDFWorker, getWorkerRetryCount: () => getWorkerRetryCount, getWorkerSrc: () => getWorkerSrc, isWorkerConfiguredProperly: () => isWorkerConfiguredProperly, resetWorkerConfiguration: () => resetWorkerConfiguration, retryWorkerConfiguration: () => retryWorkerConfiguration }); var pdfjsLib, isWorkerConfigured, workerRetryCount, MAX_WORKER_RETRIES, configurePDFWorker, getWorkerSrc, WorkerSources, resetWorkerConfiguration, retryWorkerConfiguration, isWorkerConfiguredProperly, getWorkerRetryCount; var init_worker = __esm({ "src/lib/worker.ts"() { "use strict"; pdfjsLib = __toESM(require("pdfjs-dist")); isWorkerConfigured = false; workerRetryCount = 0; MAX_WORKER_RETRIES = 3; configurePDFWorker = (workerSrc, force = false) => { if (isWorkerConfigured && !force) { return; } if (force) { try { delete pdfjsLib.GlobalWorkerOptions.workerSrc; } catch (error) { console.warn("Error clearing existing worker:", error); } } if (workerSrc && workerSrc.trim()) { pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc; } else if (!pdfjsLib.GlobalWorkerOptions.workerSrc || pdfjsLib.GlobalWorkerOptions.workerSrc === "") { const fallbackWorkers = [ `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`, `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`, `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js` ]; const workerIndex = workerRetryCount % fallbackWorkers.length; pdfjsLib.GlobalWorkerOptions.workerSrc = fallbackWorkers[workerIndex]; console.log(`Configuring PDF worker (attempt ${workerRetryCount + 1}):`, pdfjsLib.GlobalWorkerOptions.workerSrc); } isWorkerConfigured = true; }; getWorkerSrc = () => pdfjsLib.GlobalWorkerOptions.workerSrc; WorkerSources = { // Local worker (copy pdf.worker.min.mjs to your public directory) LOCAL: "/pdf.worker.min.js", // jsDelivr CDN (recommended - more reliable) JSDELIVR: `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`, // unpkg CDN (alternative) UNPKG: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`, // Mozilla CDN (if available) MOZILLA: `https://mozilla.github.io/pdf.js/build/pdf.worker.mjs` }; resetWorkerConfiguration = () => { isWorkerConfigured = false; workerRetryCount = 0; try { delete pdfjsLib.GlobalWorkerOptions.workerSrc; } catch (error) { pdfjsLib.GlobalWorkerOptions.workerSrc = ""; } configurePDFWorker(void 0, true); }; retryWorkerConfiguration = () => { if (workerRetryCount < MAX_WORKER_RETRIES) { workerRetryCount++; isWorkerConfigured = false; configurePDFWorker(void 0, true); return true; } return false; }; isWorkerConfiguredProperly = () => { const workerSrc = pdfjsLib.GlobalWorkerOptions.workerSrc; return !!(workerSrc && workerSrc.trim() && workerSrc !== "undefined") && isWorkerConfigured; }; getWorkerRetryCount = () => workerRetryCount; configurePDFWorker(); } }); // src/lib/index.ts var lib_exports = {}; __export(lib_exports, { PDFHighlight: () => PDFHighlight, PDFPage: () => PDFPage, PDFSidebar: () => PDFSidebar, PDFToolbar: () => PDFToolbar, PDFUtils: () => PDFUtils, PDFViewer: () => PDFViewer, SAMPLE_HIGHLIGHT_TEXTS: () => SAMPLE_HIGHLIGHT_TEXTS, SAMPLE_PDF_URLS: () => SAMPLE_PDF_URLS, cleanupBlobUrl: () => cleanupBlobUrl, configurePDFWorker: () => configurePDFWorker, createBlobUrl: () => createBlobUrl, createHighlightsForText: () => createHighlightsForText, default: () => PDFViewer, downloadPDFFromUrl: () => downloadPDFFromUrl, extractDocumentInfo: () => extractDocumentInfo, getPageTextContent: () => getPageTextContent, getWorkerInfo: () => getWorkerInfo, getWorkerRetryCount: () => getWorkerRetryCount, getWorkerSrc: () => getWorkerSrc, isWorkerConfiguredProperly: () => isWorkerConfiguredProperly, resetWorker: () => resetWorker, resetWorkerConfiguration: () => resetWorkerConfiguration, retryWorkerConfiguration: () => retryWorkerConfiguration, searchTextInPage: () => searchTextInPage, testWorker: () => testWorker, useKeyboardShortcuts: () => useKeyboardShortcuts }); module.exports = __toCommonJS(lib_exports); // src/lib/PDFViewer.tsx var import_react5 = __toESM(require("react")); // src/lib/PDFToolbar.tsx var import_react = __toESM(require("react")); var import_jsx_runtime = require("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] = (0, import_react.useState)(currentPage.toString()); const [searchInput, setSearchInput] = (0, import_react.useState)(searchTerm); const [showSearchBar, setShowSearchBar] = (0, import_react.useState)(false); const fileInputRef = (0, import_react.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(""); } }; import_react.default.useEffect(() => { setPageInput(currentPage.toString()); }, [currentPage]); import_react.default.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__ */ (0, import_jsx_runtime.jsxs)("div", { className: `pdf-toolbar ${className}`, style: toolbarStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "input", { ref: fileInputRef, type: "file", accept: ".pdf,application/pdf", style: { display: "none" }, onChange: (e) => { } } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: toolGroupStyle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onSidebarToggle, style: sidebarOpen ? activeButtonStyle : buttonStyle, title: "Toggle sidebar (F4)", children: "\u{1F5C2}\uFE0F" } ) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: separatorStyle }), showOpenOption && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: toolGroupStyle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: handleOpenFile, style: buttonStyle, title: "Open PDF file (Ctrl+O)", children: "\u{1F4C1} Open" } ) }), showOpenOption && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: separatorStyle }), showPageControls && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: toolGroupStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: labelStyle, children: "PAGE" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onPrevPage, disabled: currentPage <= 1, style: currentPage <= 1 ? disabledButtonStyle : buttonStyle, title: "Previous page (\u2190)", children: "\u25C0" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: handlePageInputSubmit, style: { display: "flex", alignItems: "center", gap: "4px" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "input", { type: "text", value: pageInput, onChange: handlePageInputChange, style: inputStyle, title: "Go to page" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontSize: "14px", color: "#bdc3c7" }, children: [ "/ ", totalPages ] }) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onNextPage, disabled: currentPage >= totalPages, style: currentPage >= totalPages ? disabledButtonStyle : buttonStyle, title: "Next page (\u2192)", children: "\u25B6" } ) ] }), showPageControls && showZoomControls && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: separatorStyle }), showZoomControls && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: toolGroupStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: labelStyle, children: "ZOOM" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onZoomOut, style: buttonStyle, title: "Zoom out (-)", children: "\u{1F50D}\u2212" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "select", { value: scale, onChange: handleScaleSelect, style: selectStyle, title: "Zoom level", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: 0.25, children: "25%" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: 0.5, children: "50%" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: 0.75, children: "75%" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: 1, children: "100%" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: 1.25, children: "125%" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: 1.5, children: "150%" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: 2, children: "200%" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: 3, children: "300%" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: 4, children: "400%" }) ] } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onZoomIn, style: buttonStyle, title: "Zoom in (+)", children: "\u{1F50D}+" } ), showFitOptions && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onZoomToFit, style: zoomMode === "page-fit" ? activeButtonStyle : buttonStyle, title: "Fit to page", children: "\u{1F4C4} Fit" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onZoomToWidth, style: zoomMode === "page-width" ? activeButtonStyle : buttonStyle, title: "Fit to width", children: "\u2194 Width" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: () => onZoomModeChange?.("actual"), style: zoomMode === "actual" ? activeButtonStyle : buttonStyle, title: "Actual size", children: "\u{1F3AF} Actual" } ) ] }) ] }), showViewModeControls && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: separatorStyle }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: toolGroupStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: labelStyle, children: "VIEW" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: () => onViewModeChange?.("single"), style: viewMode === "single" ? activeButtonStyle : buttonStyle, title: "Single page view", children: "\u{1F4C4} Single" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: () => onViewModeChange?.("continuous"), style: viewMode === "continuous" ? activeButtonStyle : buttonStyle, title: "Continuous scroll view", children: "\u{1F4DC} Scroll" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: () => onViewModeChange?.("two-page"), style: viewMode === "two-page" ? activeButtonStyle : buttonStyle, title: "Two-page view", children: "\u{1F4D6} Two Page" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: () => onViewModeChange?.("book"), style: viewMode === "book" ? activeButtonStyle : buttonStyle, title: "Book view", children: "\u{1F4DA} Book" } ) ] }) ] }), showZoomControls && showToolSelection && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: separatorStyle }), showToolSelection && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: toolGroupStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: labelStyle, children: "TOOLS" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: () => handleToolChange("pan"), style: activeTool === "pan" ? activeButtonStyle : buttonStyle, title: "Pan tool - Click and drag to move around", children: "\u270B Pan" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: () => handleToolChange("selection"), style: activeTool === "selection" ? activeButtonStyle : buttonStyle, title: "Selection tool - Select text and areas", children: "\u{1F4C4} Select" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: () => handleToolChange("annotation"), style: activeTool === "annotation" ? activeButtonStyle : buttonStyle, title: "Annotation tool - Add and edit annotations", children: "\u270F\uFE0F Annotate" } ) ] }), showToolSelection && showSearchOption && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: separatorStyle }), showSearchOption && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: toolGroupStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: toggleSearchBar, style: showSearchBar ? activeButtonStyle : buttonStyle, title: "Search in document", children: "\u{1F50D} Search" } ), showSearchBar && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: handleSearchSubmit, style: { display: "flex", gap: "4px", alignItems: "center" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "input", { type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), placeholder: "Search in PDF...", style: searchInputStyle } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { type: "submit", style: buttonStyle, title: "Search", children: "\u{1F50D}" } ) ] }), searchResults > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "4px", alignItems: "center" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontSize: "12px", color: "#bdc3c7" }, children: [ currentSearchResult + 1, " of ", searchResults ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onSearchPrevious, disabled: currentSearchResult <= 0, style: currentSearchResult <= 0 ? disabledButtonStyle : buttonStyle, title: "Previous result", children: "\u25B2" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onSearchNext, disabled: currentSearchResult >= searchResults - 1, style: currentSearchResult >= searchResults - 1 ? disabledButtonStyle : buttonStyle, title: "Next result", children: "\u25BC" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: () => { if (onClearSearch) onClearSearch(); setSearchInput(""); setShowSearchBar(false); }, style: buttonStyle, title: "Clear search", children: "\u2715" } ) ] }) ] }) ] }), showSearchOption && (showRotateControls || showPrintOption || showDownloadOption) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: separatorStyle }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: toolGroupStyle, children: [ showRotateControls && onRotate && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onRotate, style: buttonStyle, title: "Rotate document clockwise (R)", children: "\u21BB Rotate" } ), showPresentationMode && onPresentationMode && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onPresentationMode, style: buttonStyle, title: "Presentation mode (Ctrl+Alt+P)", children: "\u{1F3A6} Present" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onDocumentInfo, style: buttonStyle, title: "Document properties", children: "\u2139\uFE0F Info" } ), showPrintOption && onPrint && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onPrint, style: buttonStyle, title: "Print document (Ctrl+P)", children: "\u{1F5A8}\uFE0F Print" } ), showDownloadOption && onDownload && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { onClick: onDownload, style: buttonStyle, title: "Download PDF (Ctrl+S)", children: "\u{1F4BE} Download" } ) ] }) ] }); }; // src/lib/PDFSidebar.tsx var import_react2 = require("react"); var import_jsx_runtime2 = require("react/jsx-runtime"); var PDFSidebar = ({ isOpen, activeView, pdf, currentPage, outline, attachments, onToggle, onViewChange, onPageSelect, onOutlineClick, className = "", style }) => { const [thumbnails, setThumbnails] = (0, import_react2.useState)({}); const [loadingThumbnails, setLoadingThumbnails] = (0, import_react2.useState)(/* @__PURE__ */ new Set()); const [expandedOutlineItems, setExpandedOutlineItems] = (0, import_react2.useState)(/* @__PURE__ */ new Set()); const thumbnailsContainerRef = (0, import_react2.useRef)(null); const observer = (0, import_react2.useRef)(null); const loadThumbnail = (0, import_react2.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]); (0, import_react2.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 = (0, import_react2.useCallback)((itemId) => { setExpandedOutlineItems((prev) => { const newSet = new Set(prev); if (newSet.has(itemId)) { newSet.delete(itemId); } else { newSet.add(itemId); } return newSet; }); }, []); const renderOutlineItems = (0, import_react2.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__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "pdf-outline-item", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "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__ */ (0, import_jsx_runtime2.jsx)( "span", { style: { marginRight: "4px", transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)", transition: "transform 0.2s ease" }, children: "\u25B6" } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: item.title }) ] } ), hasChildren && isExpanded && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `pdf-sidebar ${className}`, style: sidebarStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: closeButtonStyle, onClick: onToggle, title: "Close sidebar", children: "\u2715" }), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: headerStyle, children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "button", { style: activeView === "thumbnails" ? activeTabStyle : tabStyle, onClick: () => onViewChange("thumbnails"), title: "Show page thumbnails", children: "\u{1F4C4} Pages" } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "button", { style: activeView === "outline" ? activeTabStyle : tabStyle, onClick: () => onViewChange("outline"), title: "Show document outline", children: "\u{1F4CB} Outline" } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "button", { style: activeView === "attachments" ? activeTabStyle : tabStyle, onClick: () => onViewChange("attachments"), title: "Show attachments", children: "\u{1F4CE} Files" } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "button", { style: activeView === "layers" ? activeTabStyle : tabStyle, onClick: () => onViewChange("layers"), title: "Show layers", children: "\u{1F5C2}\uFE0F Layers" } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: contentStyle, children: [ activeView === "thumbnails" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: thumbnailsContainerRef, className: "pdf-thumbnails", children: pdf && Array.from({ length: pdf.numPages }, (_, i) => i + 1).map((pageNum) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "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__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: "4px", fontSize: "12px", fontWeight: "500" }, children: [ "Page ", pageNum ] }), thumbnails[pageNum] ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "img", { src: thumbnails[pageNum], alt: `Page ${pageNum}`, style: { maxWidth: "100%", height: "auto", border: "1px solid #dee2e6" } } ) : loadingThumbnails.has(pageNum) ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { height: "120px", display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "#f8f9fa", border: "1px solid #dee2e6" }, children: "Loading..." }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { height: "120px", backgroundColor: "#f8f9fa", border: "1px solid #dee2e6" } }) ] }, pageNum )) }), activeView === "outline" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "pdf-outline", children: outline && outline.length > 0 ? renderOutlineItems(outline) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { textAlign: "center", color: "#6c757d", fontSize: "14px", padding: "20px" }, children: "No document outline available" }) }), activeView === "attachments" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "pdf-attachments", children: attachments && attachments.length > 0 ? attachments.map((attachment, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "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__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u{1F4CE}" }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: "14px" }, children: attachment.filename }) ] }, index )) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { textAlign: "center", color: "#6c757d", fontSize: "14px", padding: "20px" }, children: "No attachments found" }) }), activeView === "layers" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "pdf-layers", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { textAlign: "center", color: "#6c757d", fontSize: "14px", padding: "20px" }, children: "Layers functionality coming soon" }) }) ] }) ] }); }; // src/lib/PDFPage.tsx var import_react3 = require("react"); // src/lib/PDFHighlight.tsx var import_jsx_runtime3 = require("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__ */ (0, import_jsx_runtime3.jsx)( "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__ */ (0, import_jsx_runtime3.jsx)("div", { className: `pdf-highlights ${className}`, style: containerStyle, children: highlights.filter((highlight) => highlight.pageNumber === pageNumber).map((highlight) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pdf-highlight", children: renderHighlight(highlight) }, highlight.id)) }); }; // src/lib/PDFPage.tsx var import_jsx_runtime4 = require("react/jsx-runtime"); var PDFPage = ({ pageNumber, scale = 1, rotation = 0, pdf, onPageRender, onError, className = "", style, enableTextSelection = true, highlights = [] }) => { const canvasRef = (0, import_react3.useRef)(null); const textLayerRef = (0, import_react3.useRef)(null); const containerRef = (0, import_react3.useRef)(null); const renderingRef = (0, import_react3.useRef)(false); const renderTaskRef = (0, import_react3.useRef)(null); const [page, setPage] = (0, import_react3.useState)(null); const [viewport, setViewport] = (0, import_react3.useState)(null); const [rendering, setRendering] = (0, import_react3.useState)(false); const cancelRender = (0, import_react3.useCallback)(() => { if (renderTaskRef.current) { try { renderTaskRef.current.cancel(); } catch (error) { console.warn("Error canceling render task:", error); } renderTaskRef.current = null; } }, []); const renderPage = (0, import_react3.useCallback)(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 = (0, import_react3.useCallback)(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]); (0, import_react3.useEffect)(() => { renderPage(); }, [renderPage]); (0, import_react3.useEffect)(() => { 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]); (0, import_react3.useEffect)(() => { 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__ */ (0, import_jsx_runtime4.jsxs)( "div", { className: `pdf-page ${className}`, style: containerStyle, ref: containerRef, children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "canvas", { ref: canvasRef, style: canvasStyle } ), enableTextSelection && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "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__ */ (0, import_jsx_runtime4.jsx)("div", { style: highlightLayerStyle, className: "pdf-highlight-layer", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( PDFHighlight, { highlights: highlights.filter((h) => h.pageNumber === pageNumber), pageNumber, viewport } ) }); })() ] } ); }; // src/lib/PDFLoader.ts var pdfjsLib2 = __toESM(require("pdfjs-dist")); init_worker(); 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