import * as pdfjsLib from 'pdfjs-dist'; import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'; import { useCallback, useEffect, useRef, useState } from 'react'; // worker собирается вместе с приложением (без CDN) pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); const MIN_ZOOM = 0.5; const MAX_ZOOM = 3; const ZOOM_STEP = 0.25; const PAGE_MARGIN = 10; // left + right margin of each page wrapper export default function PdfPages({ data }: { data: ArrayBuffer }) { const containerRef = useRef(null); const canvasRefs = useRef<(HTMLCanvasElement | null)[]>([]); const [pdfDoc, setPdfDoc] = useState(null); const [numPages, setNumPages] = useState(0); const [zoom, setZoom] = useState(1); // 1 = fit wrapper width const [baseScale, setBaseScale] = useState(1); const [pageWidthPt, setPageWidthPt] = useState(0); const [error, setError] = useState(null); useEffect(() => { let pdf: PDFDocumentProxy | null = null; let cancelled = false; async function load() { // pdf.js забирает переданный буфер себе, поэтому отдаём копию (эффект может выполниться повторно) pdf = await pdfjsLib.getDocument({ data: data.slice(0) }).promise; if (cancelled) { pdf.destroy(); return; } const firstPage = await pdf.getPage(1); setPageWidthPt(firstPage.getViewport({ scale: 1 }).width); canvasRefs.current = new Array(pdf.numPages).fill(null); setPdfDoc(pdf); setNumPages(pdf.numPages); } load().catch(() => { if (!cancelled) { setError('Не удалось открыть PDF'); } }); return () => { cancelled = true; if (pdf) pdf.destroy(); }; }, [data]); // fit scale = wrapper inner width / page width. // wrapper width = container client width - container padding - page margins const updateBaseScale = useCallback(() => { if (!containerRef.current || !pageWidthPt) return; const CONTAINER_PADDING = 0; // keep 0 horizontally so margins control it const available = containerRef.current.clientWidth - CONTAINER_PADDING - PAGE_MARGIN * 2; if (available <= 0) return; setBaseScale(available / pageWidthPt); }, [pageWidthPt]); useEffect(() => { updateBaseScale(); const el = containerRef.current; if (!el) return; const ro = new ResizeObserver(updateBaseScale); ro.observe(el); return () => ro.disconnect(); }, [updateBaseScale]); const scale = baseScale * zoom; useEffect(() => { if (!pdfDoc || !scale) return; let cancelled = false; const doc = pdfDoc; const tasks: RenderTask[] = []; async function renderAll() { for (let i = 1; i <= doc.numPages; i++) { const canvas = canvasRefs.current[i - 1]; if (!canvas) continue; const page = await doc.getPage(i); if (cancelled) return; const outputScale = window.devicePixelRatio || 1; const viewport = page.getViewport({ scale }); const ctx = canvas.getContext('2d'); if (!ctx) continue; canvas.width = Math.floor(viewport.width * outputScale); canvas.height = Math.floor(viewport.height * outputScale); canvas.style.width = `${Math.floor(viewport.width)}px`; canvas.style.height = `${Math.floor(viewport.height)}px`; const transform = outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : undefined; const task = page.render({ canvas: null, canvasContext: ctx, viewport, transform, }); tasks.push(task); try { await task.promise; } catch { // отменённая отрисовка (смена масштаба) — не ошибка } } } renderAll(); return () => { cancelled = true; tasks.forEach((t) => t.cancel()); }; }, [pdfDoc, scale]); const zoomIn = useCallback(() => setZoom((z) => Math.min(MAX_ZOOM, +(z + ZOOM_STEP).toFixed(2))), []); const zoomOut = useCallback(() => setZoom((z) => Math.max(MIN_ZOOM, +(z - ZOOM_STEP).toFixed(2))), []); const resetZoom = useCallback(() => setZoom(1), []); if (error) return
{error}
; if (!pdfDoc) return
Загрузка PDF...
; return (
Исходный файл — страниц: {numPages} {Math.round(zoom * 100)}%
{/* outer vertical scroll of pages */}
{Array.from({ length: numPages }, (_, i) => ( // page wrapper: fixed width via margins, does NOT grow. // canvas larger than this scrolls horizontally inside here.
{/* sticky so header stays visible while scrolling the page horizontally */}
Страница {i + 1} из {numPages}
{ canvasRefs.current[i] = el; }} style={{ display: 'block' }} />
))}
); }