Guide

Formatting And Charting Excel Reports With PythonDeep dive

Inserting Images and Logos into Excel

Embed logos and images into Excel with openpyxl: floating drawings anchored to cells, pixel resizing, header bands, and why pandas can't keep them.

A branded report needs a logo in the top-left corner, and a .xlsx file can hold one — but not the way you might expect. An image in Excel is not a cell value. It is a floating drawing that sits on top of the grid, anchored to a cell but living in its own layer. That single fact explains every quirk in this cluster: images do not push cells aside, they do not appear in ws["A1"].value, and they vanish the moment a tool that does not understand drawings rewrites the file. This page is the practical guide to getting a logo in, sized right, and kept there. It is part of Formatting and Charting Excel Reports with Python.

Everything below is plain openpyxl plus Pillow, and every block builds its own tiny PNG inline, so you can paste and run without supplying an image of your own.

Install openpyxl and Pillow

openpyxl writes the .xlsx drawing XML itself, but it leans on Pillow to read the image you hand it — measure its dimensions, validate the format, and convert anything that is not already a clean PNG. Without Pillow installed, openpyxl.drawing.image.Image("logo.png") raises ImportError the moment you construct it.

Bash
pip install openpyxl pillow

That is the whole toolchain. Neither library needs Excel installed, so these scripts run on a headless CI runner or a Linux server exactly as they run on your laptop.

Embed an image anchored to a cell

The core API is small: build an openpyxl.drawing.image.Image, then ws.add_image(img, "A1") to anchor its top-left corner at a cell. The image floats — anchoring to A1 pins where it starts, not which cells it occupies. It will happily overlap B1, C1, and the rows below if it is larger than that cell.

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

# Build a small placeholder logo so this runs end to end
PILImage.new("RGB", (160, 50), color="#4472C4").save("logo.png")

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

logo = XLImage("logo.png")
ws.add_image(logo, "A1")          # top-left corner anchored to A1

wb.save("with_logo.xlsx")
print("Embedded logo.png anchored at A1")

The image bytes are copied into the workbook, so with_logo.xlsx is self-contained — email it and the logo travels along. You do not keep a reference to logo.png inside the file; openpyxl has already absorbed it (with one timing caveat covered below).

Resize a logo in pixels

A raw logo is rarely the right size for a report header. Set img.width and img.height in pixels before adding the image. These are display dimensions on the sheet, independent of the source file's real resolution, so you control exactly how big the logo renders.

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

PILImage.new("RGB", (400, 120), color="#4472C4").save("big_logo.png")

wb = Workbook()
ws = wb.active

logo = XLImage("big_logo.png")
logo.width = 200      # pixels on the sheet
logo.height = 60      # pixels on the sheet
ws.add_image(logo, "A1")

wb.save("resized_logo.xlsx")
print(f"Logo rendered at {logo.width}x{logo.height}px")

There is no "lock aspect ratio" flag — setting width and height independently can stretch the image. To keep proportions, read the source size with Pillow and scale by a single factor: scale = 200 / src_w; logo.width, logo.height = int(src_w * scale), int(src_h * scale). Add a Logo Image to an Excel Report with openpyxl walks through that aspect-safe sizing step by step.

Build a header band with a logo and title

Because the logo floats and never pushes cells, you reserve space for it yourself with row height and place the title text in a merged cell beside it. Tall first row, merged title across a few columns, logo anchored at A1 — that is the standard report header.

Python
from openpyxl import Workbook
from openpyxl.drawing.image import Image as XLImage
from openpyxl.styles import Font, Alignment
from PIL import Image as PILImage

PILImage.new("RGB", (150, 45), color="#4472C4").save("brand.png")

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

# Reserve a header band: one tall row for the logo to sit in
ws.row_dimensions[1].height = 48

# Merged title to the right of the logo
ws.merge_cells("B1:E1")
title = ws["B1"]
title.value = "Weekly Sales Report"
title.font = Font(bold=True, size=16, color="1F3864")
title.alignment = Alignment(vertical="center")

logo = XLImage("brand.png")
logo.width, logo.height = 150, 45
ws.add_image(logo, "A1")

# Data starts cleanly below the band
ws.append([])  # spacer row 2
ws.append(["Region", "Revenue"])
ws.append(["North", 25640])

wb.save("header_band.xlsx")
print("Built a header band: logo at A1, title merged across B1:E1")

The 48-pixel row height gives the 45-pixel logo room without overlapping the data, and the merged B1:E1 keeps the title from colliding with the logo's float. Widen column A to the logo's pixel width (roughly width / 7 in Excel character units) if you want the logo fully contained rather than spilling toward B.

Add a logo to a pandas report

pandas.to_excel writes data and nothing else — it has no API to embed an image, because it only knows about cell values. The pattern is therefore two steps: pandas writes the data, then you re-open the file with openpyxl and add the logo. This is the same write-then-decorate flow the rest of this track uses, and it builds on Using openpyxl for Excel File Manipulation.

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

PILImage.new("RGB", (140, 44), color="#4472C4").save("co_logo.png")

# 1. pandas writes the raw table, starting a few rows down to leave a band
df = pd.DataFrame({"Region": ["North", "South"], "Revenue": [25640, 18890]})
df.to_excel("pandas_report.xlsx", sheet_name="Sales",
            index=False, startrow=3)

# 2. re-open with openpyxl and drop the logo into the empty band
wb = load_workbook("pandas_report.xlsx")
ws = wb["Sales"]
ws.row_dimensions[1].height = 40

logo = XLImage("co_logo.png")
logo.width, logo.height = 140, 44
ws.add_image(logo, "A1")

wb.save("pandas_report.xlsx")
print("pandas wrote the data; openpyxl added the logo")

startrow=3 reserves rows 1-3 for the header band so the logo never overlaps the table. The crucial ordering rule: do the openpyxl step last.

Why pandas erases your images

If you embed a logo and later write to that same file with pandas.to_excel, the logo disappears. pandas does not read or preserve drawings — when it writes a sheet it regenerates the file's XML from the DataFrame alone, dropping the image, the charts, and most styling along with it. The fix is sequencing.

SymptomCauseFix
Logo gone after a later pandas writeto_excel rebuilds the sheet from the DataFrame and discards drawingsMake the openpyxl image step the last write to the file
ImportError on Image(...)Pillow not installedpip install pillow
Logo overlaps the data tableImages float; they never push cells downReserve a band with row_dimensions[1].height and startrow
Image missing from saved fileSource PNG deleted before wb.save()Keep the file on disk until after the save call

The mental model: pandas owns data; openpyxl owns the decorated artifact. Once a workbook carries a logo, every subsequent edit must go through openpyxl, never back through pandas.

Frequently asked questions

Why isn't my image in ws["A1"].value? Because an Excel image is never a cell value. It is a floating drawing in a separate layer that is merely anchored to A1. ws["A1"].value reads the cell's data; the image lives in ws._images, and Excel renders it on top of the grid.

Do I really need Pillow? Yes. openpyxl uses Pillow to read the image's dimensions and validate its format. Constructing openpyxl.drawing.image.Image(...) without Pillow installed raises ImportError immediately, even for a plain PNG.

My logo overlaps the data — how do I push the table down? You cannot push cells with an image; it floats. Instead reserve space: increase ws.row_dimensions[1].height and write your data starting a few rows lower (startrow= in pandas, or just append below row 1 in openpyxl).

Are the width and height in pixels or Excel units?img.width and img.height are in pixels and control the on-sheet display size, independent of the source file's resolution. Column widths and row heights use Excel's own character/point units, which is why aligning them takes a conversion factor.

Will the logo survive if I reopen and resave with openpyxl? Yes. load_workbook reads existing drawings and wb.save() writes them back. Only tools that rebuild the sheet from scratch — chiefly pandas.to_excel — drop the image.

Conclusion

An Excel image is a floating drawing anchored to a cell, not a cell value — internalize that and the rest follows. Build an openpyxl.drawing.image.Image, size it in pixels, anchor it with ws.add_image, and reserve space with row height and a merged title since the float never moves cells. Because pandas cannot embed images and erases them on rewrite, always make openpyxl the last hand to touch the file.

Where to go next

Up to the parent pillar:

Go deeper here:

Sibling clusters: