Files
pdf-tools/src/components/PagePreviewModal.tsx
2025-11-27 10:52:44 +01:00

182 lines
4.9 KiB
TypeScript

import React, { useEffect, useRef } from 'react';
import type { PdfFile } from '../pdf/pdfTypes';
import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url';
// pdf.js worker setup
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(pdfjsLib as any).GlobalWorkerOptions.workerSrc = pdfjsWorker;
interface PagePreviewModalProps {
isOpen: boolean;
pdf: PdfFile | null;
pageIndex: number | null; // original page index (0-based)
rotation: number; // degrees
onClose: () => void;
}
const PagePreviewModal: React.FC<PagePreviewModalProps> = ({
isOpen,
pdf,
pageIndex,
rotation,
onClose,
}) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!isOpen || !pdf || pageIndex == null) return;
let cancelled = false;
(async () => {
try {
// copy data for pdf.js (avoid detaching original ArrayBuffer)
const src = new Uint8Array(pdf.arrayBuffer);
const copy = new Uint8Array(src.byteLength);
copy.set(src);
const loadingTask = pdfjsLib.getDocument({ data: copy });
const doc = await loadingTask.promise;
if (cancelled) return;
const page = await doc.getPage(pageIndex + 1);
if (cancelled) return;
const viewport = page.getViewport({ scale: 1 });
const maxWidth = Math.min(window.innerWidth * 0.9, 800);
const scale = maxWidth / viewport.width;
const scaledViewport = page.getViewport({ scale });
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// base size
let canvasWidth = scaledViewport.width;
let canvasHeight = scaledViewport.height;
const angle = ((rotation % 360) + 360) % 360;
if (angle === 90 || angle === 270) {
canvasWidth = scaledViewport.height;
canvasHeight = scaledViewport.width;
}
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// render into an offscreen canvas first
const baseCanvas = document.createElement('canvas');
const baseCtx = baseCanvas.getContext('2d');
if (!baseCtx) return;
baseCanvas.width = scaledViewport.width;
baseCanvas.height = scaledViewport.height;
const renderTask = page.render({
canvasContext: baseCtx,
viewport: scaledViewport,
});
await renderTask.promise;
if (cancelled) return;
// draw rotated onto visible canvas
ctx.save();
switch (angle) {
case 90:
ctx.translate(canvasWidth, 0);
ctx.rotate((angle * Math.PI) / 180);
break;
case 180:
ctx.translate(canvasWidth, canvasHeight);
ctx.rotate((angle * Math.PI) / 180);
break;
case 270:
ctx.translate(0, canvasHeight);
ctx.rotate((angle * Math.PI) / 180);
break;
default:
break;
}
ctx.drawImage(baseCanvas, 0, 0);
ctx.restore();
} catch (e) {
console.error('Error rendering preview', e);
}
})();
return () => {
cancelled = true;
};
}, [isOpen, pdf, pageIndex, rotation]);
if (!isOpen || !pdf || pageIndex == null) return null;
return (
<div
onClick={onClose}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(15, 23, 42, 0.8)',
zIndex: 50,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '1rem',
}}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
background: '#111827',
borderRadius: '0.75rem',
padding: '0.75rem',
maxWidth: '90vw',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.5rem',
}}
>
<div style={{ alignSelf: 'flex-end' }}>
<button
type="button"
onClick={onClose}
style={{
border: 'none',
borderRadius: '999px',
padding: '0.25rem 0.5rem',
fontSize: '0.8rem',
background: '#374151',
color: '#e5e7eb',
cursor: 'pointer',
}}
>
Close
</button>
</div>
<canvas
ref={canvasRef}
style={{
maxWidth: '100%',
maxHeight: '75vh',
background: 'white',
borderRadius: '0.5rem',
}}
/>
<div style={{ color: '#e5e7eb', fontSize: '0.85rem' }}>
Page {pageIndex + 1} · Rot {rotation}°
</div>
</div>
</div>
);
};
export default PagePreviewModal;