If a pipeline of yours turns PDF pages into images and saves them as PNG because "it's lossless and text compresses well", lossless WebP deserves one test run before that default gets locked in. On one self-made sample rendered at 216 DPI, the text page came out at 161,212 bytes as lossless WebP and 838,404 bytes as truecolor PNG, so the PNG was 5.2 times the size for exactly the same pixels. The chart page went 37,390 against 127,051. Both WebP files were also smaller than a 256-colour palette PNG of the same page, and the palette version is the lossy one. The photo page broke the pattern badly, which is why what follows is a rule for text and chart pages and nothing wider.
What the sample was and what got measured
Scope first: a single 3-page A4 PDF, a single Apple silicon Mac, a single pass per format with nothing repeated. The first page packs text set at 10.5, 9, 7.5, 6 and 5 pt with a hairline 0.3 pt table; the second carries bar, line and pie charts drawn as vectors, with 6–7.5 pt labels; the third is mostly photograph, plus an enlarged detail and a line of 8 pt text. Everything except that photograph was built for the test; the photograph comes from Wikimedia Commons, a CC0 image by Bonnie Moreland. Poppler rasterised each page at 216 DPI, Pillow wrote every format from those identical pixels, and pngquant produced the palette versions.
| Format | Text page (bytes) | Chart page (bytes) | Photo page (bytes) |
|---|---|---|---|
| PNG truecolor, optimize | 838,404 | 127,051 | 2,431,138 |
| WebP lossless | 161,212 | 37,390 | 1,612,642 |
| PNG 256-colour palette (lossy) | 359,391 · 66.86 dB | 49,019 · 68.63 dB | 1,105,849 · 45.88 dB |
| WebP q75 | 287,552 | 49,492 | 147,988 |
| JPEG q92 | 796,608 | 233,599 | 550,440 |
| JPEG q75 | 535,729 | 178,232 | 302,318 |
Reading the text page column, lossless WebP came in at about 19% of the truecolor PNG, about 45% of the palette PNG and roughly a fifth of JPEG q92. Lossy WebP at q75 was 287,552 bytes, which is bigger than the lossless file while also changing pixels. The chart page tells the same story with smaller numbers: PNG was 3.4 times the lossless WebP, and the palette PNG (49,019 bytes) and WebP q75 (49,492) both stayed above 37,390. The palette rows carry PSNR because they are not lossless. 66.86 dB on the text page is a small error, but it is an error, so a 256-colour PNG is not a free lunch when you need the exact pixels back.
The photo page is where the rule stops. Lossless WebP there was 1,612,642 bytes, which is 2.9 times JPEG q92 at 550,440 and 5.3 times JPEG q75 at 302,318. A colour count on quarter-size copies of the three pages lines up with this split. The text page had about 447 colours with 81% white, the chart page about 2117 colours with 82% white, and the photo page about 26914 colours with only 49% white.
Why lossless WebP won is only a guess
This part is inference and none of it was tested. Going by the format descriptions, PNG filters each row against its neighbours and then runs DEFLATE over the result. Lossless WebP has more tools for this kind of image, such as predictors chosen per block, a small cache of recently used colours, and back-references that can point at pixels in earlier rows. A page that is mostly white with a few hundred stroke colours repeating everywhere looks like the case those tools were built for. Which of them did the work on this sample was never isolated.
Only Pillow's optimize flag was used for PNG; no dedicated PNG optimizer was tried, so the table compares lossless WebP with one PNG encoder rather than with PNG as a format.
Wiring it in without betting on a rule
The approach I'd take is to stop picking a format up front and let each page prove it. Encode the page both ways, keep the smaller file, and log which one won. The snippet below is an illustrative sketch and not the script that produced the table.
from io import BytesIO from PIL import Image def pick_lossless(page_img: Image.Image) -> tuple[str, bytes]: # sketch: try both lossless encoders, keep the smaller one candidates = {} for kind, params in (("png", {"optimize": True}), ("webp", {"lossless": True, "method": 6})): sink = BytesIO() page_img.save(sink, kind.upper(), **params) candidates[kind] = sink.getvalue() return min(candidates.items(), key=lambda item: len(item[1])) Photo-heavy pages need their own branch to a lossy format, and colour count is one obvious signal for spotting them. The other check before switching is whether every place your images end up accepts WebP, from email templates to upload forms and older desktop software. This test says nothing about that.
For one-off conversions outside a pipeline, ImgIng https://imging.ai/ is one desktop-browser option. It renders at 144, 216 or 300 DPI with 216 as the default and exports PNG, JPEG or WebP. During this test's network capture, no request carrying the file went out while it processed. Which WebP mode its export uses was not checked, so none of its files are in the table above. Whatever tool does the converting, zoom in on the tiniest lettering once it's done and confirm the small letters still look like the PDF.
To check this on your own documents, render one text page, one chart and one photo at the DPI you actually ship. Save each as PNG and as lossless WebP and compare the bytes, and the photo page will show you where the exception goes.


