Last month I had to turn a 40-page PDF into images. The reason was embarrassingly small: a form that only accepted JPG. Print to PDF had gotten me a document I could read but not upload, and every converter I found wanted the file on their server first.
So I built the conversion into a tool that already runs entirely in the browser. The happy path is about twenty lines of code. The rest of this post is the three things that broke after those twenty lines worked, all of which I only found by actually running it on real documents.
The twenty lines
pdf.js renders a page onto a canvas, the canvas becomes a blob, the blob becomes a file.
const pdf = await pdfjsLib.getDocument({ data: bytes }).promise;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({
canvasContext: canvas.getContext('2d'),
viewport
}).promise;
const blob = await new Promise(res =>
canvas.toBlob(res, 'image/jpeg', 0.92)
);
}
That produces a file. It also produces a file that is wrong in three ways I did not notice until I opened one.
Problem one: your JPEG has a black background
This is the one that surprised me most, because it fails silently and looks like a rendering bug.
A PDF page has no background colour. The page is a set of drawing operations floating on nothing. When pdf.js renders onto a canvas, the areas no content covers keep the canvas's initial state, which is fully transparent — rgba(0, 0, 0, 0), not white.
PNG can store that. JPEG cannot. JPEG has no alpha channel, so when you call toBlob with image/jpeg, the browser composites the transparent pixels against black. A white page with two paragraphs of text becomes a black page with two paragraphs of text.
The fix is one line before rendering:
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
await page.render({ canvasContext: ctx, viewport }).promise;
Worth noting: this is not a pdf.js defect. It is the correct behaviour for a renderer that does not invent a background the document never declared. But every tutorial I read skipped it, and so did I, until I opened the output.
Problem two: scale is not a zoom level, it is DPI
The natural reading of getViewport({ scale: 3 }) is "three times bigger". What it actually means is three times the PDF user-space unit, and one user-space unit is 1/72 inch. So:
| scale | A4 pixel size | effective DPI |
|---|---|---|
| 1 | 595 × 842 | 72 |
| 2 | 1190 × 1684 | 144 |
| 3 | 1785 × 2526 | 216 |
That matters because the number people care about is DPI, not scale. 72 renders fine on screen and looks blurry the moment it is printed or shown on a high-density display. 144 is the everyday setting. 216 is close to print quality and produces files several times larger for the same page.
My tool exposes these as buttons labelled Small, 72 DPI / Standard, 144 DPI / High, 216 DPI rather than 1x / 2x / 3x, precisely because "3x" tells you nothing about what you are going to get.
Problem three: canvas fails silently when it gets too big
This is the one that will bite anyone converting large-format documents rather than A4.
Browsers cap how large a canvas can be. The limits differ by engine and by platform, and — this is the annoying part — an oversized canvas usually does not throw. It returns a blank or partially drawn surface, and your code continues happily, producing empty JPEGs. iOS Safari is considerably stricter than desktop Chrome, so a document that renders correctly on your laptop can come back as solid white rectangles on a phone.
Two defences are worth having:
Check the dimensions before you allocate, and refuse the page with a real message instead of producing a blank image. A page 7152 × 10110 (A0 at scale 3) is well past what several browsers will give you.
Cap the total work. Mine is 100 pages per run. Not because 100 is a magic number, but because the alternative is a tool that accepts a 400-page catalogue and then dies halfway through with no explanation.
The memory arithmetic nobody does
A single A4 page at scale 3 allocates 1785 × 2526 × 4 bytes — about 17 MB of canvas backing store. Hold all 100 pages and you are at 1.7 GB before a single JPEG exists.
So pages have to be processed one at a time, and the canvas has to be released before the next one:
canvas.width = 0;
canvas.height = 0;
Then there is the archive. Collecting 100 JPEG blobs and handing them to a ZIP library means the whole output exists twice in memory at once. At an average 2 MB per page that is another 200 MB. On a desktop it survives. On a phone it is the difference between a finished download and a browser that closes the tab.
The honest answer is that batch size is not a product decision, it is a memory decision. If the limit feels arbitrary, it is — it is set by the smallest device I was willing to support.
What the output cannot do, and why I did not fix it
The output is a bitmap. There is no text layer inside it. Nobody can select the words, search them, copy them, or extract them, and a screen reader has nothing to read. If the PDF needs to stay searchable later, converting it to images is the wrong operation and no amount of engineering changes that — the text is being discarded on purpose, because discarding it is the point.
I also do not let you convert only pages 12 through 20. The tool converts the whole document. A range selector is easy to add and I have not added it, because the UI currently has one file input, one format toggle, one resolution toggle and one button, and every control I add has to be explained to someone who has never thought about DPI. That tradeoff is a choice, not an oversight, and it may change.
When not to use this
If the file is going to be printed, 216 DPI and a look at the first page before you commit to the batch.
If the file has to remain searchable, keep the PDF.
If it is a scan, converting it changes nothing — it already has no text layer, and you are paying DPI cost for an image you could have had.
If it is 300 pages, split it first, then convert the parts.
The tool
I built this as one of six small PDF utilities that all run locally — no upload, no account, no server to trust with the file. The rendering happens in the tab and the file never leaves it.
If you want to convert a PDF to JPG without uploading it, that is the page. The JPG/PNG toggle, the three DPI settings, and the ZIP download are all there, along with the text above about what the conversion costs you.
I am the person who wrote it, so treat the recommendation accordingly. The measurement that matters is your own: open the first output file before you run the batch.
Top comments (0)