How-to
Formatting And Charting Excel Reports With PythonDeep dive

Export an Excel Chart as an Image with Python

Turn a chart inside an Excel workbook into a PNG for an email, a slide or a web page — with Excel itself through COM on Windows, with headless LibreOffice on any server plus a Pillow crop, or by redrawing the chart from the same data with matplotlib — and choose by fidelity, platform and resolution.

Charts live in workbooks, but people want them elsewhere: in the body of the weekly email, on a slide, in a dashboard page or a PDF summary. openpyxl and XlsxWriter only write a chart's definition — series, colours, axes — and leave the drawing to whatever application opens the file. To get a picture, something has to render it. This guide, part of Inserting Images and Logos into Excel, covers the three practical routes: let Excel render it through COM, let LibreOffice render it on a server, or redraw the same chart from the same data with matplotlib.

Choosing how to turn a workbook chart into an image On Windows with Excel installed, Chart.Export gives an exact rendering. On a server without Excel, headless LibreOffice renders the workbook to PNG or PDF, which is then cropped. When the image must look the same everywhere or be generated at scale, redraw the chart with matplotlib from the same data. Where does the export run, and how exact must it be? Windows with Excel Chart.Export exact rendering Linux server, no Excel headless LibreOffice PNG or PDF, then crop many images, full control redraw with matplotlib same data

Prerequisites

Bash
pip install openpyxl pillow

LibreOffice (soffice) for the server route, pdftoppm from poppler for high-resolution output, and pywin32 plus Excel for the COM route. A workbook with data on one sheet:

Python
import subprocess

from openpyxl import Workbook
from openpyxl.chart import BarChart, Reference
from PIL import Image, ImageChops

def build_chart(ws_data):
    chart = BarChart()
    chart.x_axis.delete = False  # Excel hides axes unless this is set
    chart.y_axis.delete = False
    chart.add_data(Reference(ws_data, min_col=2, min_row=1, max_row=5), titles_from_data=True)
    chart.set_categories(Reference(ws_data, min_col=1, min_row=2, max_row=5))
    chart.title = "Revenue"
    chart.width, chart.height = 16, 9          # centimetres
    return chart

Route 1 — Excel renders it (Windows)

With Excel installed, ask it for the picture. Chart.Export writes the chart exactly as it appears in the workbook, fonts and theme included:

Python
import win32com.client as win32

excel = win32.DispatchEx("Excel.Application")
excel.Visible = False
try:
    wb = excel.Workbooks.Open(r"C:\reports\sales.xlsx", ReadOnly=True)
    ws = wb.Worksheets("Summary")
    for i in range(1, ws.ChartObjects().Count + 1):
        chart_object = ws.ChartObjects(i)
        chart_object.Chart.Export(rf"C:\reports\chart_{i}.png")
    wb.Close(SaveChanges=False)
finally:
    excel.Quit()

Paths must be absolute, because Excel resolves them from its own working directory. The image size follows the chart object's size on the sheet; enlarge chart_object.Width and Height before exporting for a larger picture. Driving Excel through COM, including running it from scheduled tasks, is covered in Run Excel COM Automation from Task Scheduler.

Route 2 — LibreOffice renders it (any server)

LibreOffice has no "export this chart" command, but it can render the first sheet of a workbook to PNG or PDF. So build a small workbook whose first sheet holds nothing but the chart, anchored at A1, with its data on a second sheet:

Python
wb = Workbook()
chart_sheet = wb.active
chart_sheet.title = "ChartOnly"
data = wb.create_sheet("Data")
data.append(["Month", "Revenue"])
for row in [("Jan", 82), ("Feb", 91), ("Mar", 78), ("Apr", 104)]:
    data.append(row)
chart_sheet.add_chart(build_chart(data), "A1")
wb.save("chart_only.xlsx")

subprocess.run(["soffice", "--headless", "--convert-to", "png", "chart_only.xlsx"],
               check=True, capture_output=True, timeout=120)

The PNG is a whole page: the chart in the top-left corner and white space around it. Crop it to the chart with Pillow by finding the bounding box of everything that is not white:

Python
def crop_white(path, out, margin=4):
    picture = Image.open(path).convert("RGB")
    background = Image.new("RGB", picture.size, (255, 255, 255))
    left, top, right, bottom = ImageChops.difference(picture, background).getbbox()
    picture.crop((max(left - margin, 0), max(top - margin, 0), right + margin, bottom + margin)).save(out)
    return Image.open(out).size

print(crop_white("chart_only.png", "chart.png"))        # (614, 349) on our system

On our system the page rendered at screen resolution and the cropped chart came out at roughly 600 × 340 pixels — fine for an email, soft on a slide. A chart on a chartsheet, by contrast, rendered as a blank page, which is why the chart goes on an ordinary worksheet here.

Rendering a chart on a server with LibreOffice Build a workbook whose first sheet contains only the chart at A1, with its data on another sheet. Convert it with headless LibreOffice to PNG for a quick image or to PDF for high resolution, rasterise the PDF with pdftoppm, then crop the white page margins with Pillow. Chart-only sheet chart at A1 soffice --headless PNG or PDF pdftoppm -r 200 for sharp output Crop margins Pillow getbbox chart.png ready to use

Route 2b — Higher resolution through PDF

For slides and print, render to PDF and rasterise at the resolution you need with pdftoppm, then crop as before:

Python
subprocess.run(["soffice", "--headless", "--convert-to", "pdf", "chart_only.xlsx"],
               check=True, capture_output=True, timeout=120)
subprocess.run(["pdftoppm", "-f", "1", "-l", "1", "-r", "200", "-png", "chart_only.pdf", "page"],
               check=True, timeout=120)
print(crop_white("page-1.png", "chart_hd.png"))        # (1268, 717)

At 200 dots per inch the same chart comes out at about 1,250 × 700 pixels with crisp text, because the PDF keeps the chart as vector graphics until pdftoppm draws it. Set -r 300 for print; the rendering time grows with the resolution, but for a single chart it stays well under a second. The PDF itself is also a good deliverable when the chart goes into a document: it scales without losing quality.

Route 2c — Every chart in an existing workbook

Reports usually arrive with several charts already on a sheet. openpyxl reads existing charts back into ws._charts, so each one can be moved onto its own render page: load the workbook, take one chart off its sheet, anchor it at A1 of a new first sheet, save a copy and render that:

Python
from openpyxl import load_workbook

def export_charts(path, sheet_name, prefix="chart", size_cm=(16, 9)):
    count = len(load_workbook(path)[sheet_name]._charts)
    sizes = []
    for i in range(count):
        wb = load_workbook(path)
        chart = wb[sheet_name]._charts[i]
        wb[sheet_name]._charts = []                 # keep only the chart being exported
        chart.width, chart.height = size_cm         # reloaded charts use the default size
        page = wb.create_sheet("Render", 0)
        page.add_chart(chart, "A1")
        wb.active = 0
        wb.save(f"{prefix}_{i}.xlsx")
        subprocess.run(["soffice", "--headless", "--convert-to", "png", f"{prefix}_{i}.xlsx"],
                       check=True, capture_output=True, timeout=120)
        sizes.append(crop_white(f"{prefix}_{i}.png", f"{prefix}_{i}_cropped.png"))
    return sizes

print(export_charts("chart_only.xlsx", "ChartOnly"))     # [(614, 349)]

Loading a fresh copy for each chart keeps the exports independent. A reloaded chart comes back at openpyxl's default size rather than the size it had in the workbook, so the function sets the size explicitly — without that line the same chart rendered noticeably smaller in our test. Tried on a workbook with three styled charts — stacked columns with labels, a 100% stack and horizontal bars — each came out as its own image with colours, labels and legends intact. openpyxl models most chart features, but anything it does not understand is dropped when it reloads a chart, so compare the exports with the workbook once when adopting this for a new report.

Route 3 — Redraw it with matplotlib

When every image must look identical regardless of which office suite is installed, or when you produce hundreds of charts, skip the workbook renderer and draw the same chart from the same data:

Python
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

months, revenue = ["Jan", "Feb", "Mar", "Apr"], [82, 91, 78, 104]
fig, ax = plt.subplots(figsize=(6.4, 3.6), dpi=200)
ax.bar(months, revenue, color="#4338CA")
ax.set_title("Revenue")
ax.spines[["top", "right"]].set_visible(False)
fig.tight_layout()
fig.savefig("chart_mpl.png")
plt.close(fig)
print(Image.open("chart_mpl.png").size)            # (1280, 720)

The image is fast, reproducible and exactly as styled — but it is a second chart definition that must be kept in step with the one in the workbook. Put the data and styling in one function used for both when the two must match. Going the other way — putting a matplotlib chart into a workbook — is covered in Embed a Matplotlib Chart in an Excel Report.

Rendering the workbook chart or redrawing it Rendering the workbook's own chart with Excel or LibreOffice guarantees the image matches the workbook, but needs an office program and depends on its rendering. Redrawing with matplotlib needs only Python and gives full control and speed, but duplicates the chart definition. Render the workbook chart matches the workbook one chart definition needs Excel or LibreOffice renderer's fonts and styling Redraw with matplotlib only Python needed fast at scale, any resolution second definition to maintain identical on every machine

Common pitfalls

  • Charts on chartsheets. They rendered as blank pages through LibreOffice. Use a worksheet with the chart at A1.
  • Several charts on one sheet. The page render includes all of them. Build one chart-only workbook per image, or crop by position.
  • Rendering in parallel. Two soffice --headless processes sharing one user profile can block each other. Render files one after another, or give each worker its own profile with -env:UserInstallation=file:///tmp/lo_profile_N.
  • Leftover render files. Every export leaves a helper workbook and a full-page PNG behind. Write them to a temporary directory and delete it when the images are done.
  • Fonts on servers. LibreOffice substitutes fonts that are not installed, which changes text widths. Install the report's fonts on the render machine.

Conclusion

openpyxl only describes charts, so an image needs a renderer. On Windows with Excel, Chart.Export through COM gives an exact picture. On a server, put the chart alone at A1 of a workbook's first sheet, convert it with headless LibreOffice — to PNG for speed or to PDF plus pdftoppm for resolution — and crop the page margins with Pillow. When images must be identical everywhere or produced at scale, redraw the chart from the same data with matplotlib.

Frequently asked questions

Can openpyxl save a chart as an image? No. openpyxl writes the chart's definition into the workbook but never draws it. A program that renders charts — Excel, LibreOffice — has to produce the picture, or you redraw the chart from the data with a plotting library.

How do I export a chart to PNG with Excel installed? Open the workbook with win32com and call Export on the chart, for example ws.ChartObjects(1).Chart.Export with the full path of the PNG to write. Excel renders it exactly as it appears in the workbook.

How do I export a chart on a Linux server? Put the chart alone on the first sheet of a workbook, convert that workbook with headless LibreOffice to PNG or PDF, and crop the white page margins with Pillow. The PDF route with pdftoppm gives a higher resolution.

Why is the exported image blank? A chart on a chartsheet rendered as an empty page in LibreOffice in our test. Place the chart on an ordinary worksheet at A1 instead, with its data on another sheet.

More on charts & images

More on charts & images →