());
+ 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') {