Spaces:
Runtime error
Runtime error
| import React, { useEffect, useRef, useState } from 'react' | |
| import * as pdfjsLib from 'pdfjs-dist' | |
| import workerSrc from 'pdfjs-dist/build/pdf.worker.min.mjs?url' | |
| pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc | |
| const SCALE = 1.35 | |
| /** | |
| * Renders all pages of the PDF and draws highlight overlays from | |
| * {page, rects[]} coming from the backend. The backend emits rects in | |
| * PDF points with a TOP-LEFT origin (PyMuPDF convention), which maps to a | |
| * rendered canvas by a plain multiply with the zoom factor — no coordinate | |
| * flipping needed. | |
| */ | |
| export default function PdfViewer({ fileUrl, highlight }) { | |
| const containerRef = useRef(null) | |
| const pageRefs = useRef({}) | |
| const [numPages, setNumPages] = useState(0) | |
| const [pdf, setPdf] = useState(null) | |
| useEffect(() => { | |
| let cancelled = false | |
| setPdf(null) | |
| setNumPages(0) | |
| pdfjsLib.getDocument(fileUrl).promise.then((doc) => { | |
| if (cancelled) return | |
| setPdf(doc) | |
| setNumPages(doc.numPages) | |
| }) | |
| return () => { cancelled = true } | |
| }, [fileUrl]) | |
| useEffect(() => { | |
| if (!highlight) return | |
| const el = pageRefs.current[highlight.page] | |
| if (el) { | |
| // scroll the first highlight rect into view, not just the page top | |
| const firstRect = highlight.rects?.[0] | |
| const offset = firstRect ? firstRect.y0 * SCALE - 120 : 0 | |
| const container = containerRef.current | |
| container.scrollTo({ top: el.offsetTop + offset, behavior: 'smooth' }) | |
| } | |
| }, [highlight]) | |
| return ( | |
| <div className="pdf-container" ref={containerRef}> | |
| {!pdf && <div className="pdf-loading">Loading PDF…</div>} | |
| {pdf && | |
| Array.from({ length: numPages }, (_, i) => ( | |
| <Page | |
| key={i} | |
| pdf={pdf} | |
| pageNumber={i + 1} | |
| refCb={(el) => (pageRefs.current[i] = el)} | |
| rects={highlight ? highlight.rects.filter((r) => r.page === i) : []} | |
| /> | |
| ))} | |
| </div> | |
| ) | |
| } | |
| function Page({ pdf, pageNumber, rects, refCb }) { | |
| const canvasRef = useRef(null) | |
| const [size, setSize] = useState({ w: 0, h: 0 }) | |
| useEffect(() => { | |
| let cancelled = false | |
| let renderTask = null | |
| pdf.getPage(pageNumber).then((page) => { | |
| if (cancelled) return | |
| const viewport = page.getViewport({ scale: SCALE }) | |
| const canvas = canvasRef.current | |
| if (!canvas) return | |
| const dpr = window.devicePixelRatio || 1 | |
| canvas.width = viewport.width * dpr | |
| canvas.height = viewport.height * dpr | |
| canvas.style.width = `${viewport.width}px` | |
| canvas.style.height = `${viewport.height}px` | |
| setSize({ w: viewport.width, h: viewport.height }) | |
| const ctx = canvas.getContext('2d') | |
| ctx.scale(dpr, dpr) | |
| renderTask = page.render({ canvasContext: ctx, viewport }) | |
| }) | |
| return () => { | |
| cancelled = true | |
| renderTask?.cancel() | |
| } | |
| }, [pdf, pageNumber]) | |
| return ( | |
| <div className="pdf-page" ref={refCb} style={{ width: size.w || undefined }}> | |
| <canvas ref={canvasRef} /> | |
| {rects.map((r, i) => ( | |
| <div | |
| key={i} | |
| className="hl" | |
| style={{ | |
| left: r.x0 * SCALE - 2, | |
| top: r.y0 * SCALE - 2, | |
| width: (r.x1 - r.x0) * SCALE + 4, | |
| height: (r.y1 - r.y0) * SCALE + 4, | |
| }} | |
| /> | |
| ))} | |
| <div className="page-num">{pageNumber}</div> | |
| </div> | |
| ) | |
| } | |