Guide
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.
pip install openpyxl pillow
No Excel install is required — this runs unchanged on a server or CI runner.
Step 1: create a sample logo
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.
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.
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")
Step 3: create, size, and anchor the logo
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.
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")
Step 4: add a merged title beside the logo
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.
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.
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 / symptom | Cause | Fix |
|---|---|---|
ImportError when building Image(...) | Pillow not installed | pip install pillow |
| Logo overlaps the data rows | Images float and never push cells down | Reserve room with ws.row_dimensions[1].height and start data below |
FileNotFoundError on wb.save() | Source PNG deleted before the save | Keep the file on disk until after wb.save() runs |
| Logo looks stretched | width and height set independently | Scale both by one factor from the source size (Step 3) |
| Logo vanished after a later edit | File was rewritten with pandas.to_excel, which drops drawings | Make 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:
- Inserting Images and Logos into Excel — the concepts behind anchoring, resizing, and pandas-safe sequencing.
Related pages:
- Using openpyxl for Excel File Manipulation — loading, editing, and saving workbooks, the foundation this recipe stands on.
- Building Multi-Sheet Excel Dashboards — assemble branded, logo-topped sheets into a single dashboard workbook.