182 lines
4.9 KiB
TypeScript
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;
|