diff --git a/frontend/src/app/AppLayout.tsx b/frontend/src/app/AppLayout.tsx index 35b94c6..08afd8c 100644 --- a/frontend/src/app/AppLayout.tsx +++ b/frontend/src/app/AppLayout.tsx @@ -37,16 +37,12 @@ const AppLayout: React.FC = () => { }); if (['logged-out', 'authenticating', 'selecting-tenant'].includes(appStatus)) { - const shouldRememberLastLocation = appStatus !== 'logged-out'; + const redirectTarget = `${location.pathname}${location.search}${location.hash || ''}`; return ( ); } diff --git a/frontend/src/preview/PdfViewer.tsx b/frontend/src/preview/PdfViewer.tsx index 1df58c8..e143eee 100644 --- a/frontend/src/preview/PdfViewer.tsx +++ b/frontend/src/preview/PdfViewer.tsx @@ -39,6 +39,12 @@ interface PageDescriptor { scale: number; } +interface RenderQueueRequest { + pageNumber: number; + resume: () => void; + cancel: () => void; +} + const RERENDER_DELTA = 48; const getAvailableViewportSize = (viewportNode: Element, stackNode: Element) => { @@ -85,6 +91,50 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX. const wasmUrlRef = useRef(null); const pageNodeMapRef = useRef>(new Map()); const intersectionObserverRef = useRef(null); + const renderQueueRef = useRef([]); + const activeRendersRef = useRef(new Set()); + const maxConcurrentRendersRef = useRef(2); + + const requestQueueFlush = useCallback(() => { + const queue = renderQueueRef.current; + while (queue.length > 0 && activeRendersRef.current.size < maxConcurrentRendersRef.current) { + const next = queue.pop(); + if (!next) { + break; + } + if (activeRendersRef.current.has(next.pageNumber)) { + next.cancel(); + continue; + } + activeRendersRef.current.add(next.pageNumber); + next.resume(); + } + }, []); + + const enqueueRender = useCallback((request: RenderQueueRequest) => { + renderQueueRef.current.push(request); + requestQueueFlush(); + }, [requestQueueFlush]); + + const releaseRenderSlot = useCallback((pageNumber: number) => { + if (activeRendersRef.current.delete(pageNumber)) { + requestQueueFlush(); + } + }, [requestQueueFlush]); + + const cancelRenderRequest = useCallback((pageNumber: number) => { + const queue = renderQueueRef.current; + const index = queue.findIndex((entry) => entry.pageNumber === pageNumber); + if (index >= 0) { + const [entry] = queue.splice(index, 1); + entry.cancel(); + } + releaseRenderSlot(pageNumber); + }, [releaseRenderSlot]); + + const handleRenderFinished = useCallback((pageNumber: number) => { + releaseRenderSlot(pageNumber); + }, [releaseRenderSlot]); const focusTargetRef = useRef<{ ratioX: number; ratioY: number; @@ -428,11 +478,14 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX. viewMode={viewMode} shouldRender={shouldRenderPage(page.number)} registerPageNode={registerPageNode} + enqueueRender={enqueueRender} + cancelRenderRequest={cancelRenderRequest} + onRenderFinished={handleRenderFinished} /> )) - ), [handlePageClick, pages, pixelRatio, registerPageNode, shouldRenderPage, - viewportHeight, viewportWidth, viewMode]); + ), [cancelRenderRequest, enqueueRender, handlePageClick, handleRenderFinished, + pages, pixelRatio, registerPageNode, shouldRenderPage, viewportHeight, viewportWidth, viewMode]); return (
@@ -466,6 +519,9 @@ interface PdfPageCanvasProps { viewMode: ViewMode; shouldRender: boolean; registerPageNode: (pageNumber: number, node: HTMLElement | null) => void; + enqueueRender: (request: RenderQueueRequest) => void; + cancelRenderRequest: (pageNumber: number) => void; + onRenderFinished: (pageNumber: number) => void; } function PdfPageCanvas({ @@ -478,6 +534,9 @@ function PdfPageCanvas({ viewMode, shouldRender, registerPageNode, + enqueueRender, + cancelRenderRequest, + onRenderFinished, }: PdfPageCanvasProps): JSX.Element { const wrapperRef = useRef(null); const canvasRef = useRef(null); @@ -488,6 +547,18 @@ function PdfPageCanvas({ registerPageNode(descriptor.number, node); }, [descriptor.number, registerPageNode]); const [canvasMounted, setCanvasMounted] = useState(false); + const [allowRender, setAllowRender] = useState(false); + + const resetCanvas = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) { + return; + } + canvas.width = 0; + canvas.height = 0; + const context = canvas.getContext('2d'); + context?.clearRect(0, 0, context.canvas.width || 0, context.canvas.height || 0); + }, []); const renderPage = useCallback(async () => { if (renderTaskRef.current || hasRenderedRef.current) { @@ -496,51 +567,31 @@ function PdfPageCanvas({ const pdf = pdfRef.current; const canvas = canvasRef.current; if (!pdf || !canvas) { + onRenderFinished(descriptor.number); return; } try { const page = await pdf.getPage(descriptor.number); const viewport = page.getViewport({ scale: descriptor.scale }); const qualityScale = 1; - const supportsOffscreen = typeof OffscreenCanvas !== 'undefined'; - let renderContext: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null = null; - let offscreenCanvas: OffscreenCanvas | null = null; - - if (supportsOffscreen) { - offscreenCanvas = new OffscreenCanvas( - viewport.width * pixelRatio * qualityScale, - viewport.height * pixelRatio * qualityScale, - ); - renderContext = offscreenCanvas.getContext('2d'); - } else { - renderContext = canvas.getContext('2d'); - if (!renderContext) { - return; - } - canvas.width = viewport.width * pixelRatio; - canvas.height = viewport.height * pixelRatio; + const renderContext = canvas.getContext('2d'); + if (!renderContext) { + return; } - renderContext?.setTransform(pixelRatio * qualityScale, 0, 0, pixelRatio * qualityScale, 0, 0); + canvas.width = viewport.width * pixelRatio * qualityScale; + canvas.height = viewport.height * pixelRatio * qualityScale; + renderContext.setTransform(pixelRatio * qualityScale, 0, 0, pixelRatio * qualityScale, 0, 0); const renderTask = page.render({ - canvasContext: renderContext as CanvasRenderingContext2D, + canvasContext: renderContext, viewport, - canvas: supportsOffscreen ? undefined : canvas, + canvas, }); renderTaskRef.current = renderTask; await renderTask.promise; hasRenderedRef.current = true; - if (offscreenCanvas) { - const bitmap = offscreenCanvas.transferToImageBitmap(); - canvas.width = bitmap.width; - canvas.height = bitmap.height; - const destContext = canvas.getContext('2d'); - destContext?.drawImage(bitmap, 0, 0); - bitmap.close(); - } - page.cleanup(); const wrapper = wrapperRef.current; @@ -554,8 +605,9 @@ function PdfPageCanvas({ console.error('Failed to render PDF page', descriptor.number, error); } finally { renderTaskRef.current = null; + onRenderFinished(descriptor.number); } - }, [descriptor.number, descriptor.scale, pdfRef, pixelRatio]); + }, [descriptor.number, descriptor.scale, onRenderFinished, pdfRef, pixelRatio]); useEffect(() => { if (!canvasMounted) { @@ -579,10 +631,39 @@ function PdfPageCanvas({ setCanvasMounted(false); hasRenderedRef.current = false; renderTaskRef.current?.cancel(); - }, [descriptor.number]); + resetCanvas(); + }, [descriptor.number, resetCanvas]); useEffect(() => { - if (shouldRender) { + if (!shouldRender) { + setAllowRender(false); + cancelRenderRequest(descriptor.number); + return; + } + let cancelled = false; + const request: RenderQueueRequest = { + pageNumber: descriptor.number, + resume: () => { + if (cancelled) { + return; + } + setAllowRender(true); + }, + cancel: () => { + cancelled = true; + setAllowRender(false); + }, + }; + enqueueRender(request); + return () => { + cancelled = true; + setAllowRender(false); + cancelRenderRequest(descriptor.number); + }; + }, [cancelRenderRequest, descriptor.number, enqueueRender, shouldRender]); + + useEffect(() => { + if (allowRender) { setCanvasMounted((prev) => (prev ? prev : true)); return; } @@ -593,16 +674,17 @@ function PdfPageCanvas({ hasRenderedRef.current = false; renderTaskRef.current?.cancel(); wrapperRef.current?.querySelector('.textLayer')?.remove(); + resetCanvas(); return false; }); - }, [shouldRender]); + }, [allowRender, resetCanvas]); useEffect(() => { - if (!canvasMounted) { + if (!canvasMounted || !allowRender) { return; } void renderPage(); - }, [canvasMounted, renderPage]); + }, [allowRender, canvasMounted, renderPage]); const limitAxis: 'width' | 'height' = useMemo(() => { if (viewMode === 'fit-width') {