Phase 3 implementation. Coordinate system implemented. Initial text box tool implemented.
This commit is contained in:
parent
c159ad4f37
commit
4cb038ec78
34 changed files with 6676 additions and 130 deletions
88
frontend/src/features/editor/pages/PageRenderer.tsx
Normal file
88
frontend/src/features/editor/pages/PageRenderer.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
|
||||
import { AnnotationLayer } from '../canvas/AnnotationLayer';
|
||||
|
||||
interface PageRendererProps {
|
||||
pdfDoc: PDFDocumentProxy;
|
||||
pageNumber: number;
|
||||
scale: number;
|
||||
dpr: number;
|
||||
}
|
||||
|
||||
export function PageRenderer({ pdfDoc, pageNumber, scale, dpr }: PageRendererProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [page, setPage] = useState<PDFPageProxy | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
pdfDoc.getPage(pageNumber).then(p => {
|
||||
if (active) setPage(p);
|
||||
});
|
||||
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();
|
||||
};
|
||||
}, [page, scale, dpr]);
|
||||
|
||||
if (!page) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white shadow-sm flex items-center justify-center text-neutral-400 border border-neutral-200"
|
||||
style={{ width: 612 * scale, height: 792 * scale }}
|
||||
>
|
||||
Loading page {pageNumber}...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const baseViewport = page ? page.getViewport({ scale: 1, rotation: 0 }) : null;
|
||||
|
||||
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}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
frontend/src/features/editor/pages/PageStack.tsx
Normal file
32
frontend/src/features/editor/pages/PageStack.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||
import { PageRenderer } from './PageRenderer';
|
||||
import { useEditorStore } from '../store';
|
||||
|
||||
interface PageStackProps {
|
||||
pdfDoc: PDFDocumentProxy;
|
||||
}
|
||||
|
||||
export function PageStack({ pdfDoc }: PageStackProps) {
|
||||
const numPages = pdfDoc.numPages;
|
||||
const pages = Array.from({ length: numPages }, (_, i) => i + 1);
|
||||
|
||||
const zoom = useEditorStore(state => state.zoom);
|
||||
|
||||
// For now, render all pages vertically. Virtualization comes in Phase 7.
|
||||
const scale = zoom * 2.0;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-8 py-8 w-full max-w-full">
|
||||
{pages.map(pageNum => (
|
||||
<PageRenderer
|
||||
key={pageNum}
|
||||
pdfDoc={pdfDoc}
|
||||
pageNumber={pageNum}
|
||||
scale={scale}
|
||||
dpr={dpr}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
frontend/src/features/editor/pages/PdfDocument.tsx
Normal file
53
frontend/src/features/editor/pages/PdfDocument.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PageStack } from './PageStack';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
export function PdfDocument({ documentId }: { documentId: string }) {
|
||||
const [pdfDoc, setPdfDoc] = useState<pdfjsLib.PDFDocumentProxy | null>(null);
|
||||
const [error, setError] = useState<string | 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);
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
loadingTask.destroy();
|
||||
};
|
||||
}, [documentId]);
|
||||
|
||||
if (error) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
if (!pdfDoc) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return <PageStack pdfDoc={pdfDoc} />;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue