You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When we want to render a page in web app, we call the page.toPixmap (this is done in a worker to not freeze UI). But the problem is, suppose the user clicks the Next button continuously, then we end up with sequence of calls to page.toPixmap. Once the user finally stopped clicking Next, we need to wait for all the intermediate pages to be rendered and discarded to get the output for current page.
pdf.js returns RenderTask which can be canceled when the rendering is no longer required.
Is there a way to do the same in MuPDF?
I thought of doing something like using microtasks on the worker side:
Divide the page into fixed blocks of a size, like say 64x64.
Use matrix argument of page.toPixmap to decide which block to be rendered.
If the request is cancelled in-between, then stop immediately.
Else continue for all the blocks.
Use an OffscreenCanvas to render the blocks together as the final image.
Does this sound good? Or does MuPDF offer a better way to do progressive rendering?
Hi @arun-mani-j I think MuPDF.js doesn't really attempt to manage the UI - especially in relation to interactions. Therefore it is up to the developer to try to balance the interactions of their web app.
Thanks for the reply @jamie-lemon. Yea that's why it is a little tricky. I think MuPDF need not manage UI; but just need a way to render a page step by step.
I can't think of how it can be solved on the app side either without launching multiple MuPDF.js workers for the same file, which does not sound efficient.
@jamie-lemon There are two separate concerns here.
Canceling an in-progress render. This requires the "cookie" API from C to flag to the task that it should stop. This requires shared memory. We don't support this.
Rendering a partial image. This we do support, but you have to assemble the lego blocks yourself. There are many different trade-offs involved, and the core library APIs don't lock you into one particular set of trade-offs like the high level PyMuPDF functions do.
Create a Pixmap for the slice you want to render, then create a DrawDevice to render to it, and pass the device to either Page.run or DisplayList.run.
Where a file is slow varies a lot. Sometimes it's parsing the vector graphic stream (for complicated CAD drawings), decoding images (high resolution photographic content), and sometimes it's rendering (complicated transparency blending stacks).
Rendering the page in multiple slices will not make the parsing/decoding steps any faster. In fact, it may make them slower because you need to decode the same content once for each slice. You can use the DisplayList as an intermediate step, so that you only need to parse the content once, and then replay from the display list (which is a faster in-memory representation) to render each slice.
Thank you so much @ccxvii. Yea the little slow-down in doing block by block is fine. Because the other alternative of frozen UI feels like a bad UX. I actually faced this when I was reading an encyclopedia at 3x zoom. May be the presence of a lot of pictures caused this.
I have used your example to make a full example in TypeScript that does the blocks algorithm with AbortSignal to cancel.
It works nicely on my side. I'm happy to make a PR if you think we can add this to examples in docs.
import{Document,Rect,Pixmap,ColorSpace,Matrix,DrawDevice,DisplayList,}from"mupdf";importfsfrom"node:fs";import{createCanvas,Canvas}from"canvas";import{scheduler}from"node:timers/promises";constBLOCK_SIZE=512;function*split(x0: number,y0: number,x1: number,y1: number,): Iterable<Rect>{console.log("splitting",x0,y0,x1,y1);for(letx=x0;x<x1;x+=BLOCK_SIZE){for(lety=y0;y<y1;y+=BLOCK_SIZE){yield[x,y,Math.min(x+BLOCK_SIZE,x1),Math.min(y+BLOCK_SIZE,y1),];}}}functionrenderBlock(display: DisplayList,transform: Matrix,bbox: Rect){console.log("rendering block",bbox);constpixmap=newPixmap(ColorSpace.DeviceRGB,bbox,true);pixmap.clear(255);constdevice=newDrawDevice(Matrix.identity,pixmap);display.run(device,transform);device.close();returnpixmap;}asyncfunctionrender(doc: Document,idx: number,scale: number,signal: AbortSignal,): Promise<Canvas>{constpage=doc.loadPage(idx);constdisplay=page.toDisplayList();consttransform=Matrix.scale(scale,scale);const[x0,y0,x1,y1]=Rect.transform(display.getBounds(),transform);constcanvas=createCanvas(x1-x0,y1-y0);constctx=canvas.getContext("2d");if(!ctx)thrownewError("unable to get 2D context");for(constblockofsplit(x0,y0,x1,y1)){if(signal.aborted)break;constpixmap=renderBlock(display,transform,block);constpixels=pixmap.getPixels();constblockWidth=block[2]-block[0];constblockHeight=block[3]-block[1];constcanvasX=block[0]-x0;constcanvasY=block[1]-y0;constimage=ctx.getImageData(canvasX,canvasY,blockWidth,blockHeight,);image.data.set(pixels);ctx.putImageData(image,canvasX,canvasY);pixmap.destroy();awaitscheduler.yield();}display.destroy();page.destroy();if(signal.aborted)thrownewError("rendering cancelled");returncanvas;}constdoc=Document.openDocument("sample.pdf");constcontroller=newAbortController();constid=setTimeout(()=>controller.abort(),5*1000);constcanvas=awaitrender(doc,0,20,controller.signal);clearTimeout(id);constdata=canvas.toBuffer("image/png");fs.writeFileSync("out.png",data);
Activity
arun-mani-j commentedon Jul 5, 2026
Any help regarding this, (sorry to ping) @jamie-lemon? :)
jamie-lemon commentedon Jul 6, 2026
Hi @arun-mani-j I think MuPDF.js doesn't really attempt to manage the UI - especially in relation to interactions. Therefore it is up to the developer to try to balance the interactions of their web app.
arun-mani-j commentedon Jul 6, 2026
Thanks for the reply @jamie-lemon. Yea that's why it is a little tricky. I think MuPDF need not manage UI; but just need a way to render a page step by step.
I can't think of how it can be solved on the app side either without launching multiple MuPDF.js workers for the same file, which does not sound efficient.
PyMuPDF offers a parameter called
clip- https://pymupdf.readthedocs.io/en/latest/page.html#Page.get_pixmap, which I think does what I'm asking. So if MuPDF.js offered the same, the issue would be solved. 😅jamie-lemon commentedon Jul 6, 2026
@artifex-tor What do you think? Could the
clipparameter be implemented?ccxvii commentedon Jul 7, 2026
@jamie-lemon There are two separate concerns here.
Canceling an in-progress render. This requires the "cookie" API from C to flag to the task that it should stop. This requires shared memory. We don't support this.
Rendering a partial image. This we do support, but you have to assemble the lego blocks yourself. There are many different trade-offs involved, and the core library APIs don't lock you into one particular set of trade-offs like the high level PyMuPDF functions do.
Create a Pixmap for the slice you want to render, then create a DrawDevice to render to it, and pass the device to either Page.run or DisplayList.run.
Where a file is slow varies a lot. Sometimes it's parsing the vector graphic stream (for complicated CAD drawings), decoding images (high resolution photographic content), and sometimes it's rendering (complicated transparency blending stacks).
Rendering the page in multiple slices will not make the parsing/decoding steps any faster. In fact, it may make them slower because you need to decode the same content once for each slice. You can use the DisplayList as an intermediate step, so that you only need to parse the content once, and then replay from the display list (which is a faster in-memory representation) to render each slice.
ccxvii commentedon Jul 7, 2026
Here is an example script of rendering to a display list and then two slices.
arun-mani-j commentedon Jul 7, 2026
Thank you so much @ccxvii. Yea the little slow-down in doing block by block is fine. Because the other alternative of frozen UI feels like a bad UX. I actually faced this when I was reading an encyclopedia at 3x zoom. May be the presence of a lot of pictures caused this.
I have used your example to make a full example in TypeScript that does the blocks algorithm with
AbortSignalto cancel.It works nicely on my side. I'm happy to make a PR if you think we can add this to examples in docs.