Rescue Paperjet v1 implementation
This commit is contained in:
parent
7ff5c8130d
commit
db9e2ca51b
83 changed files with 6186 additions and 3894 deletions
|
|
@ -1,87 +1,127 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
|
||||
import { AnnotationLayer } from '../canvas/AnnotationLayer';
|
||||
|
||||
interface PageRendererProps {
|
||||
pdfDoc: PDFDocumentProxy;
|
||||
pageNumber: number;
|
||||
pageIndex: number;
|
||||
scale: number;
|
||||
dpr: number;
|
||||
}
|
||||
|
||||
export function PageRenderer({ pdfDoc, pageNumber, scale, dpr }: PageRendererProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
export function PageRenderer({ pdfDoc, pageIndex, scale, dpr }: PageRendererProps) {
|
||||
const [page, setPage] = useState<PDFPageProxy | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
pdfDoc.getPage(pageNumber).then(p => {
|
||||
if (active) setPage(p);
|
||||
const pageNumber = pageIndex + 1;
|
||||
pdfDoc.getPage(pageNumber).then((nextPage) => {
|
||||
if (active) setPage(nextPage);
|
||||
});
|
||||
return () => { active = false; };
|
||||
}, [pdfDoc, pageNumber]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!page || !canvasRef.current) return;
|
||||
|
||||
const viewport = page.getViewport({ scale: scale * dpr });
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) return;
|
||||
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
canvas.style.width = `${viewport.width / dpr}px`;
|
||||
canvas.style.height = `${viewport.height / dpr}px`;
|
||||
|
||||
const renderContext = {
|
||||
canvas: canvas,
|
||||
viewport: viewport,
|
||||
};
|
||||
|
||||
let renderTask = page.render(renderContext);
|
||||
|
||||
return () => {
|
||||
renderTask.cancel();
|
||||
active = false;
|
||||
};
|
||||
}, [page, scale, dpr]);
|
||||
}, [pdfDoc, pageIndex]);
|
||||
|
||||
if (!page) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white shadow-sm flex items-center justify-center text-neutral-400 border border-neutral-200"
|
||||
<div
|
||||
className="flex items-center justify-center border border-neutral-200 bg-white text-neutral-400 shadow-sm"
|
||||
style={{ width: 612 * scale, height: 792 * scale }}
|
||||
>
|
||||
Loading page {pageNumber}...
|
||||
Loading page {pageIndex + 1}...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const baseViewport = page ? page.getViewport({ scale: 1, rotation: 0 }) : null;
|
||||
const canonicalViewport = page.getViewport({ scale: 1, rotation: 0 });
|
||||
const renderedViewport = page.getViewport({
|
||||
scale: scale * dpr,
|
||||
rotation: page.rotate,
|
||||
});
|
||||
const cssWidth = renderedViewport.width / dpr;
|
||||
const cssHeight = renderedViewport.height / dpr;
|
||||
|
||||
return (
|
||||
<div className="relative shadow-xl bg-white border border-neutral-200 group">
|
||||
<canvas ref={canvasRef} className="block" />
|
||||
|
||||
{baseViewport && (
|
||||
<AnnotationLayer
|
||||
pageNumber={pageNumber}
|
||||
width={baseViewport.width * scale}
|
||||
height={baseViewport.height * scale}
|
||||
viewportParams={{
|
||||
scale,
|
||||
rotation: page.rotate,
|
||||
canonicalWidth: baseViewport.width,
|
||||
canonicalHeight: baseViewport.height,
|
||||
dpr
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="absolute -left-10 top-2 text-xs font-bold text-neutral-400 bg-white/80 rounded-md px-1.5 py-0.5 shadow-sm border border-neutral-200">
|
||||
{pageNumber}
|
||||
<RenderedPage
|
||||
page={page}
|
||||
pageIndex={pageIndex}
|
||||
scale={scale}
|
||||
dpr={dpr}
|
||||
canonicalWidth={canonicalViewport.width}
|
||||
canonicalHeight={canonicalViewport.height}
|
||||
renderedWidth={renderedViewport.width}
|
||||
renderedHeight={renderedViewport.height}
|
||||
cssWidth={cssWidth}
|
||||
cssHeight={cssHeight}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface RenderedPageProps {
|
||||
page: PDFPageProxy;
|
||||
pageIndex: number;
|
||||
scale: number;
|
||||
dpr: number;
|
||||
canonicalWidth: number;
|
||||
canonicalHeight: number;
|
||||
renderedWidth: number;
|
||||
renderedHeight: number;
|
||||
cssWidth: number;
|
||||
cssHeight: number;
|
||||
}
|
||||
|
||||
function RenderedPage({
|
||||
page,
|
||||
pageIndex,
|
||||
scale,
|
||||
dpr,
|
||||
canonicalWidth,
|
||||
canonicalHeight,
|
||||
renderedWidth,
|
||||
renderedHeight,
|
||||
cssWidth,
|
||||
cssHeight,
|
||||
}: RenderedPageProps) {
|
||||
const viewportParams = useMemo(() => ({
|
||||
scale,
|
||||
rotation: page.rotate,
|
||||
canonicalWidth,
|
||||
canonicalHeight,
|
||||
dpr,
|
||||
}), [canonicalHeight, canonicalWidth, dpr, page.rotate, scale]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = document.querySelector<HTMLCanvasElement>(
|
||||
`[data-paperjet-page="${pageIndex}"]`,
|
||||
);
|
||||
if (!canvas) return;
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) return;
|
||||
canvas.width = renderedWidth;
|
||||
canvas.height = renderedHeight;
|
||||
canvas.style.width = `${cssWidth}px`;
|
||||
canvas.style.height = `${cssHeight}px`;
|
||||
|
||||
const renderTask = page.render({ canvas, viewport: page.getViewport({ scale: scale * dpr, rotation: page.rotate }) });
|
||||
renderTask.promise.catch(() => undefined);
|
||||
return () => {
|
||||
renderTask.cancel();
|
||||
};
|
||||
}, [page, pageIndex, scale, dpr, renderedWidth, renderedHeight, cssWidth, cssHeight]);
|
||||
|
||||
return (
|
||||
<div className="group relative border border-neutral-200 bg-white shadow-xl" style={{ width: cssWidth, height: cssHeight }}>
|
||||
<canvas data-paperjet-page={pageIndex} className="block" />
|
||||
<AnnotationLayer
|
||||
pageNumber={pageIndex}
|
||||
width={cssWidth}
|
||||
height={cssHeight}
|
||||
viewportParams={viewportParams}
|
||||
/>
|
||||
<div className="absolute -left-10 top-2 rounded-md border border-neutral-200 bg-white/80 px-1.5 py-0.5 text-xs font-bold text-neutral-400 shadow-sm">
|
||||
{pageIndex + 1}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ interface PageStackProps {
|
|||
|
||||
export function PageStack({ pdfDoc }: PageStackProps) {
|
||||
const numPages = pdfDoc.numPages;
|
||||
const pages = Array.from({ length: numPages }, (_, i) => i + 1);
|
||||
const pages = Array.from({ length: numPages }, (_, i) => i);
|
||||
|
||||
const zoom = useEditorStore(state => state.zoom);
|
||||
|
||||
// For now, render all pages vertically. Virtualization comes in Phase 7.
|
||||
const scale = zoom * 2.0;
|
||||
// PDF points map directly to CSS pixels at 100%. Device pixel ratio is
|
||||
// applied only to the PDF.js backing canvas, not the annotation overlay.
|
||||
const scale = zoom;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
return (
|
||||
|
|
@ -22,7 +23,7 @@ export function PageStack({ pdfDoc }: PageStackProps) {
|
|||
<PageRenderer
|
||||
key={pageNum}
|
||||
pdfDoc={pdfDoc}
|
||||
pageNumber={pageNum}
|
||||
pageIndex={pageNum}
|
||||
scale={scale}
|
||||
dpr={dpr}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,50 +4,61 @@ import { PageStack } from './PageStack';
|
|||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.mjs',
|
||||
import.meta.url
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
interface LoadedDocument {
|
||||
id: string;
|
||||
document: pdfjsLib.PDFDocumentProxy;
|
||||
}
|
||||
|
||||
interface DocumentError {
|
||||
id: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function PdfDocument({ documentId }: { documentId: string }) {
|
||||
const [pdfDoc, setPdfDoc] = useState<pdfjsLib.PDFDocumentProxy | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [loaded, setLoaded] = useState<LoadedDocument | null>(null);
|
||||
const [error, setError] = useState<DocumentError | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const url = `/api/v1/documents/${documentId}/file`;
|
||||
|
||||
const loadingTask = pdfjsLib.getDocument({ url });
|
||||
|
||||
loadingTask.promise.then((doc) => {
|
||||
if (active) setPdfDoc(doc);
|
||||
}).catch(err => {
|
||||
console.error('Failed to load PDF', err);
|
||||
if (active) setError(err.message);
|
||||
const loadingTask = pdfjsLib.getDocument({ url: `/api/v1/documents/${documentId}/file` });
|
||||
|
||||
loadingTask.promise.then((document) => {
|
||||
if (active) setLoaded({ id: documentId, document });
|
||||
}).catch((reason: unknown) => {
|
||||
if (active) {
|
||||
const message = reason instanceof Error ? reason.message : 'Unable to load this PDF.';
|
||||
setError({ id: documentId, message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
loadingTask.destroy();
|
||||
void loadingTask.destroy();
|
||||
};
|
||||
}, [documentId]);
|
||||
|
||||
if (error) {
|
||||
|
||||
const currentError = error?.id === documentId ? error.message : null;
|
||||
const currentDocument = loaded?.id === documentId ? loaded.document : null;
|
||||
|
||||
if (currentError) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<div className="text-red-500 bg-red-50 px-4 py-3 rounded-lg border border-red-200">
|
||||
Error loading PDF: {error}
|
||||
</div>
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-red-600">Error loading PDF: {currentError}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!pdfDoc) {
|
||||
|
||||
if (!currentDocument) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full w-full gap-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accent-500"></div>
|
||||
<div className="text-neutral-500 font-medium animate-pulse">Loading document...</div>
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-accent-500" />
|
||||
<div className="font-medium animate-pulse text-neutral-500">Loading document…</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <PageStack pdfDoc={pdfDoc} />;
|
||||
|
||||
return <PageStack pdfDoc={currentDocument} />;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue