Andrey Epifancev 0aa73f2289 fix(review): legacy-сборка pdf.js — «Не удалось открыть PDF» в Яндекс Браузере (#22)
Обычная сборка pdfjs-dist 5.7 в воркере вызывает Uint8Array.prototype.toHex (отпечаток документа) —
метод есть только в Chromium 140+, в Яндекс Браузере getDocument падает «a.toHex is not a function».
PdfPages берёт модуль и воркер из pdfjs-dist/legacy/build (с полифиллами core-js) через новый
widgets/DocumentReview/pdfjs.ts; регрессионный тест открывает PDF без Uint8Array.prototype.toHex.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 19:41:16 +04:00

181 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist';
import { useCallback, useEffect, useRef, useState } from 'react';
import { pdfjsLib } from './pdfjs';
export const MIN_ZOOM = 0.5;
export const MAX_ZOOM = 3;
export const ZOOM_STEP = 0.25;
const PAGE_MARGIN = 10; // left + right margin of each page wrapper
/** zoom: 1 = по ширине области; управляется кнопками в шапке карточки (SourcePreview). */
export default function PdfPages({ data, zoom }: { data: ArrayBuffer; zoom: number }) {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRefs = useRef<(HTMLCanvasElement | null)[]>([]);
const [pdfDoc, setPdfDoc] = useState<PDFDocumentProxy | null>(null);
const [numPages, setNumPages] = useState(0);
const [baseScale, setBaseScale] = useState(1);
const [pageWidthPt, setPageWidthPt] = useState(0);
const [error, setError] = useState<string | null>(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]);
if (error) return <div>{error}</div>;
if (!pdfDoc) return <div>Загрузка PDF...</div>;
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
minWidth: 0,
}}>
{/* outer vertical scroll of pages */}
<div
ref={containerRef}
style={{
flex: 1,
minHeight: 0,
minWidth: 0,
overflowY: 'auto',
overflowX: 'hidden', // layout never widens
background: '#f5f7f8',
borderRadius: 8,
paddingTop: 16,
paddingBottom: 16,
}}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
alignItems: 'stretch',
}}>
{Array.from({ length: numPages }, (_, i) => (
// page wrapper: fixed width via margins, does NOT grow.
// canvas larger than this scrolls horizontally inside here.
<div
// biome-ignore lint/suspicious/noArrayIndexKey: страницы PDF идут по порядку и не переупорядочиваются
key={i}
style={{
margin: `0 ${PAGE_MARGIN}px`,
background: '#fff',
border: '1px solid #e4e7ec',
borderRadius: 12,
overflowX: 'auto', // internal horizontal scroll when zoomed
overflowY: 'hidden',
}}>
{/* sticky so header stays visible while scrolling the page horizontally */}
<div
style={{
position: 'sticky',
left: 0,
padding: '6px 12px',
fontSize: 14,
fontWeight: 500,
background: '#f8f9fa',
color: '#344054',
}}>
Страница {i + 1}
</div>
<canvas
ref={(el) => {
canvasRefs.current[i] = el;
}}
style={{ display: 'block' }}
/>
</div>
))}
</div>
</div>
</div>
);
}