Turn PDF Report Pages into Images
A PDF is the right format for a report that will be printed or archived, but not for every place a report is seen. A chat message, an intranet page, a ticket or the body of an email shows an image straight away, while a PDF attachment needs a click and often a download. Turning the first page — or every page — of a report PDF into an image makes it visible where people already are: a summary page as a PNG in a team channel, a thumbnail of each monthly report on an index page, a picture of the key page pasted into a slide. This page renders PDF pages to images with pypdfium2, a Python binding for the PDF engine in Chrome that installs with pip and needs nothing else, and covers the decisions that matter: resolution, format, trimming the margins and thumbnails. It belongs to Exporting Excel Reports to PDF.
soffice --headless --convert-to pdf sales_report.xlsx > /dev/null 2>&1 && pdfinfo sales_report.pdf | grep -E "Pages|Page size"
Prerequisites
pip install pypdfium2 pillow
pypdfium2 ships the PDFium library inside its wheel, so it works the same on Windows, macOS and Linux without installing Poppler or Ghostscript. Pillow handles saving, trimming and resizing. The PDF here is a short A4 landscape sales report exported from Excel with LibreOffice: a summary table with a chart on the first page, then the detail rows.
Step 1 — Render each page at a chosen resolution
import pypdfium2 as pdfium
pdf = pdfium.PdfDocument("sales_report.pdf")
dpi = 150
for i, page in enumerate(pdf, start=1):
w_pt, h_pt = page.get_size()
image = page.render(scale=dpi / 72).to_pil()
image.save(f"sales_report_p{i}.png", optimize=True)
print(f"page {i}: {w_pt:.0f} x {h_pt:.0f} pt -> {image.size[0]} x {image.size[1]} px")
PDF pages are measured in points, 72 to the inch, so the scale passed to render is the target resolution divided by 72. At 150 DPI an A4 landscape page becomes about 1750 × 1240 pixels, which is sharp on a normal screen and readable when zoomed. The right resolution depends on where the image is going: 96 to 110 DPI is enough for a picture that will be shown at page width on screen, 150 suits images that people will zoom into, and 300 is for printing. Every doubling of the resolution makes four times as many pixels, so the file size and the time to render grow quickly — render at the resolution you need, not the highest you can.
Step 2 — Trim the white margins
from PIL import Image, ImageChops
def trim(image, pad=24):
background = Image.new(image.mode, image.size, (255, 255, 255))
box = ImageChops.difference(image, background).getbbox() # bounding box of non-white pixels
if not box:
return image
l, t, r, b = box
return image.crop((max(l - pad, 0), max(t - pad, 0), min(r + pad, image.width), min(b + pad, image.height)))
first = pdf[0].render(scale=150 / 72).to_pil()
trimmed = trim(first)
trimmed.save("sales_summary.png", optimize=True)
print("full page", first.size, "-> trimmed", trimmed.size)
A report page rendered as an image carries its print margins with it — a wide white border that wastes space in a chat window and makes the content smaller when the image is scaled to fit. Trimming crops the image to the area that actually contains ink, plus a small padding so the content does not touch the edge. ImageChops.difference against a plain white image leaves only the non-white pixels, and getbbox() returns the rectangle around them. For a summary page that only half fills the sheet, the trimmed image can be a fraction of the original height, and every pixel of it is content. Here the summary page shrinks from 1754 × 1241 pixels to 993 × 764 — less than 35% of the original area — because the table and chart occupy only the top-left part of the landscape sheet.
Step 3 — PNG or JPEG, and thumbnails
from pathlib import Path
trimmed.save("sales_summary.jpg", quality=85, optimize=True)
for f in ["sales_summary.png", "sales_summary.jpg"]:
print(f, f"{Path(f).stat().st_size / 1024:.0f} KB")
thumb = trimmed.copy()
thumb.thumbnail((400, 400)) # keeps the aspect ratio, never enlarges
thumb.save("sales_summary_thumb.png", optimize=True)
print("thumbnail", thumb.size, f"{Path('sales_summary_thumb.png').stat().st_size / 1024:.0f} KB")
small = trimmed.quantize(colors=64, method=Image.Quantize.MEDIANCUT) # palette PNG
small.save("sales_summary_64.png", optimize=True)
print("64-colour PNG", f"{Path('sales_summary_64.png').stat().st_size / 1024:.0f} KB")
For report pages — text, table lines and flat-coloured charts on white — PNG is usually the better format: it is lossless, so text stays crisp, and large areas of a single colour compress very well. JPEG is designed for photographs; on text it produces faint smudges around letters, and its files are often no smaller for this kind of content. On the summary page above, the JPEG at quality 85 saves only 2 KB over the PNG (85 KB against 87 KB), and it is the PNG whose text looks clean when zoomed. Use JPEG only when a page contains photographs or large gradients. Thumbnails are best made from the rendered image with Pillow's thumbnail, which keeps the proportions and uses a high-quality filter, rather than rendering the PDF at a very low resolution, where text breaks up into unreadable pixels.
The last lines show the most effective way to make a report image smaller. A rendered page uses only a handful of real colours — black text, a few chart colours, white — plus the grey shades of anti-aliased edges. Reducing it to a 64-colour palette keeps the text smooth while cutting the PNG to a fraction of its full-colour size, which matters when the image is going into an email or a chat message with a size limit. Check the result by eye the first time: a chart with gradients or many colours may need 128 or 256 colours to look right.
The command-line alternative
When Poppler is installed — it is on most Linux servers, and available for Windows and macOS — pdftoppm renders pages without any Python code. It is handy in shell scripts and for checking what a PDF looks like on a server with no screen:
pdftoppm -r 110 -png -f 1 -l 1 sales_report.pdf page && ls page*
-r sets the resolution, -f and -l the first and last page, and the output files are numbered automatically. The Python libraries pdf2image wrap this command; pypdfium2 does the same rendering in-process without needing Poppler, which makes it the simpler dependency when the script runs on several kinds of machine. PyMuPDF is another fast option with many extra features, but it is licensed under the AGPL, which matters if the code is distributed.
Where the images go
Once the summary page is an image, it can travel anywhere a picture can. Embedded in the body of a report email with a content ID, it shows the headline numbers before anyone opens the attachment — see Embed a Chart Image in the Body of a Report Email. Posted to a team chat with a webhook, it gives the channel the figures without a download. On an index page of past reports, a row of thumbnails, each linking to its PDF, makes it easy to find the right month by eye. In every case keep the PDF as the authoritative copy and the image as a preview: an image cannot be searched, its text cannot be copied, and readers with screen readers need the PDF or a text summary alongside it.
Common pitfalls
- Rendering at 72 DPI and scaling up. Text becomes blurry; render at the resolution you need.
- JPEG for text pages. Use PNG; JPEG smudges the edges of characters.
- Untrimmed margins. Crop to the content so the image is not mostly white.
- Thumbnails from low-resolution renders. Render normally, then shrink with Pillow.
- Images instead of the PDF. Share images as previews alongside the PDF, not instead of it.
Conclusion
To turn report PDF pages into images, open the PDF with pypdfium2, render each page with scale = dpi / 72 at a resolution suited to where the image will be seen, trim the white margins with Pillow, save as PNG for text and charts, and make thumbnails by shrinking the rendered image. pdftoppm does the same from the command line where Poppler is installed.
Frequently asked questions
How do I convert a PDF page to PNG in Python without Poppler?
Use pypdfium2: pdfium.PdfDocument(path)[0].render(scale=150 / 72).to_pil().save("page.png").
What DPI should I use when converting PDF to images? About 110 DPI for on-screen viewing, 150 if readers will zoom, and 300 for printing.
Is PNG or JPEG better for report pages? PNG, because it keeps text and lines sharp and compresses areas of flat colour well.
How do I remove the white border from a rendered page?
Compare the image with a white image using ImageChops.difference, take getbbox() and crop to that box with a small margin.
Related
- Up one level: Exporting Excel Reports to PDF — PDF output from Excel.
- Convert an Excel File to PDF with Python — producing the PDF.
- Export an Excel Chart as an Image with Python — just the chart.
- Sign a PDF Report Digitally with Python — sealing the PDF itself.
More on pdf export
- How-toFill PDF Forms from Excel Rows with Python
- How-toGenerate One PDF per Excel Row
- How-toAdd Bookmarks and a Cover Page to an Excel PDF Export
- How-toAdd Headers, Footers and Page Numbers to an Excel PDF
- How-toCheck a PDF Export for Cut-Off Columns
- How-toConvert an Excel File to PDF with Python