Guide

Formatting And Charting Excel Reports With PythonDeep dive

Add a Logo Image to an Excel Report with openpyxl

Step-by-step openpyxl guide to add a logo to an Excel report: create the image, anchor it at A1, size it to a header band, add a merged title, and save.

You have a report and you want your logo in the top-left corner. This page is the copy-paste recipe: create the image, anchor it at A1, size it to fit a header band, set a merged title next to it, and save — with every pitfall that bites people on the way. It is the hands-on companion to Inserting Images and Logos into Excel, which covers the concepts; here we just build the thing.

Prerequisites

You need two libraries. openpyxl writes the workbook and its drawing XML; Pillow is what openpyxl uses to read and measure the image, so the logo step fails without it.

Bash
pip install openpyxl pillow

No Excel install is required — this runs unchanged on a server or CI runner.

So this guide runs end to end with nothing to download, generate a small placeholder PNG with Pillow. In your real report, skip this and point at your actual logo file.

Python
from PIL import Image as PILImage

PILImage.new("RGB", (180, 56), color="#4472C4").save("logo.png")
print("Wrote logo.png (180x56)")

Step 2: open a workbook and reserve a header band

Start a workbook (or load_workbook("report.xlsx") for an existing one) and make the first row tall enough to hold the logo. Remember that the image will float over the grid — it does not push rows down — so reserving height by hand is what keeps it from sitting on top of your data.

Python
from openpyxl import Workbook

wb = Workbook()
ws = wb.active
ws.title = "Report"

ws.row_dimensions[1].height = 50   # header band tall enough for the logo
print("Reserved a 50px header band on row 1")

Build an openpyxl.drawing.image.Image, scale it to fit the band without distorting it, and anchor its top-left corner at A1 with ws.add_image. To preserve the aspect ratio, read the source size with Pillow and scale by one factor instead of setting width and height blindly.

Python
from openpyxl.drawing.image import Image as XLImage
from PIL import Image as PILImage

# Measure the source so we scale proportionally
with PILImage.open("logo.png") as src:
    src_w, src_h = src.size

target_h = 44                       # pixels, fits inside the 50px row
scale = target_h / src_h
logo = XLImage("logo.png")
logo.height = target_h
logo.width = int(src_w * scale)     # same factor keeps proportions

ws.add_image(logo, "A1")            # anchor top-left at A1
print(f"Logo sized to {logo.width}x{logo.height}px, anchored at A1")

Put the report title to the right of the logo by merging a few cells in row 1 and styling the anchor cell. Merging gives the title room and centers it vertically against the tall band.

Python
from openpyxl.styles import Font, Alignment

ws.merge_cells("B1:F1")
title = ws["B1"]
title.value = "Q2 Sales Report"
title.font = Font(bold=True, size=16, color="1F3864")
title.alignment = Alignment(horizontal="left", vertical="center")
print("Merged B1:F1 and set the title")

Step 5: add data and save

Write the table starting below the band so nothing overlaps, then save while the PNG still exists on disk — openpyxl reads the image bytes at save time, not when you construct the Image.

Python
ws.append([])                       # spacer row 2
ws.append(["Region", "Revenue"])    # header row 3
for region, revenue in [("North", 25640), ("South", 18890), ("East", 31200)]:
    ws.append([region, revenue])

wb.save("branded_report.xlsx")
print("Saved branded_report.xlsx with logo and title")

Run steps 1-5 in one script and you get branded_report.xlsx: a self-contained workbook with the logo anchored at A1, a title beside it, and the table below — emailable, with the logo embedded inside the file.

Common pitfalls

Error / symptomCauseFix
ImportError when building Image(...)Pillow not installedpip install pillow
Logo overlaps the data rowsImages float and never push cells downReserve room with ws.row_dimensions[1].height and start data below
FileNotFoundError on wb.save()Source PNG deleted before the saveKeep the file on disk until after wb.save() runs
Logo looks stretchedwidth and height set independentlyScale both by one factor from the source size (Step 3)
Logo vanished after a later editFile was rewritten with pandas.to_excel, which drops drawingsMake openpyxl the last writer; never resave with pandas afterward

The deletion trap is worth stressing: openpyxl does not slurp the image when you call Image("logo.png") — it reads the bytes during wb.save(). If you generate a temp PNG and os.remove it before saving, the save fails or the image is missing. Delete temp files only after the workbook is written.

A quick note on the floating model: anchoring at A1 sets where the logo starts, not which cells it covers. A logo wider than column A spills over B and C, which is fine for a header band but means widening column A alone will not "contain" it. Reserve space with row height and column width together if you need a tidy boxed logo.

Frequently asked questions

Do I have to install Pillow just for one logo? Yes. openpyxl delegates all image reading to Pillow, so even a single PNG requires it. The Image(...) constructor raises ImportError without it.

Why anchor at A1 instead of putting the image "in" the cell? Excel has no concept of an image inside a cell. Images are floating drawings anchored to a corner cell. Anchoring at A1 places the logo's top-left there; the image then floats over whatever cells it spans.

How do I keep the logo from looking squashed? Do not set width and height to arbitrary numbers. Read the source dimensions with Pillow and multiply both by the same scale factor (Step 3). Setting one to a value and the other independently is what distorts it.

Can I do this on an existing report instead of a new workbook? Yes. Replace Workbook() in Step 2 with load_workbook("report.xlsx") and pick the sheet you want. openpyxl preserves the existing data and styles while you add the logo.

The logo disappeared after I regenerated the file — why? Something rewrote the workbook with pandas.to_excel, which rebuilds the sheet from the DataFrame and discards images, charts, and most styling. Add the logo as the last step and never round-trip the file back through pandas.

Conclusion

Adding a logo is five steps: make (or point at) the PNG, reserve a header band with row height, build and proportionally size the Image, anchor it at A1 with a merged title beside it, and save while the source file still exists. Keep openpyxl as the last tool to write the file and your logo stays put. Pillow is non-negotiable, and the float model — images sit over cells, never inside them — explains every layout quirk you will hit.

Where to go next

Up to the parent cluster:

Related pages: