Embed Images Inside Cells with xlsxwriter
A product catalogue with a photo column looks simple until someone sorts it by price. Pictures inserted the
traditional way float on a drawing layer above the grid, anchored to a cell but not part of it, so the rows
move and the photos stay where they were — every product now wears somebody else's picture. Current versions
of Excel can instead place a picture in a cell, as its value, and xlsxwriter writes that with
embed_image(). The picture then sorts, filters, copies and resizes with its row like any other value. This
guide, part of Inserting Images and Logos into Excel,
shows how to use it and when the floating kind is still the right choice.
Prerequisites
pip install xlsxwriter pillow pandas openpyxl
embed_image is available in recent xlsxwriter releases (it is present in the 3.2 series used here). Readers need a version of Excel that
supports pictures in cells, such as Excel for Microsoft 365; older versions show #VALUE! in those cells, which is covered in Step 5. The
snippet below draws two small placeholder product images with Pillow so the examples run without any assets:
from PIL import Image, ImageDraw
for name, colour in [("widget", "#0f9488"), ("gadget", "#5b5cf0")]:
picture = Image.new("RGB", (240, 240), "white")
ImageDraw.Draw(picture).ellipse((20, 20, 220, 220), fill=colour)
picture.save(f"{name}.png")
Step 1 — Embed an image in a cell
embed_image(row, col, source, options) takes zero-based row and column numbers (or an A1-style reference) and a filename. The image is
scaled to fit the cell while keeping its aspect ratio, so the cell's size decides how large it appears.
import xlsxwriter
workbook = xlsxwriter.Workbook("catalog.xlsx")
sheet = workbook.add_worksheet("Products")
header = workbook.add_format({"bold": True, "bottom": 1})
sheet.write_row(0, 0, ["SKU", "Name", "Price", "Photo"], header)
sheet.set_column("A:C", 14)
sheet.set_column("D:D", 12)
products = [("W-100", "Widget", 19.99, "widget.png"), ("G-200", "Gadget", 34.50, "gadget.png")]
for row, (sku, name, price, photo) in enumerate(products, start=1):
sheet.set_row(row, 60) # height in points; the picture fits inside
sheet.write_row(row, 0, [sku, name, price])
sheet.embed_image(row, 3, photo, {"description": f"{name} product photo"})
workbook.close()
Setting the row height before embedding is what makes the result look deliberate. A default-height row produces a thumbnail about the size of a
letter; 60 points gives a readable picture without making the table unwieldy. The description becomes the image's alternative text for screen
readers, which is worth filling in for any report that is shared widely.
Step 2 — Size rows and columns for the pictures
Because an embedded picture fits itself to its cell, sizing is entirely a question of column width and row height. Column width is measured in character units and row height in points, so a square cell needs a little arithmetic: at default font settings one character unit is roughly seven pixels and one point is four-thirds of a pixel.
def square_cell(sheet, row: int, col: int, pixels: int) -> None:
sheet.set_column(col, col, pixels / 7) # approximate character units
sheet.set_row(row, pixels * 0.75) # points
workbook = xlsxwriter.Workbook("square.xlsx")
sheet = workbook.add_worksheet()
for row in range(1, 3):
square_cell(sheet, row, 1, 90)
sheet.embed_image(1, 1, "widget.png")
sheet.embed_image(2, 1, "gadget.png")
workbook.close()
The picture keeps its proportions inside a non-square cell, leaving blank space on the longer side. For consistent results, pre-process source images to
the same aspect ratio before embedding — cropping product photos to squares is a two-line job with Pillow's ImageOps.fit. The general column and row
sizing rules are in Set Column Width and Row Height in openpyxl.
Step 3 — Embed images from bytes
Reports rarely have image files lying on disk. Photos come from a database, an object store or an HTTP API, and writing them to temporary files just to
embed them is wasteful. Pass a BytesIO object as image_data in the options, together with a filename argument that only serves to identify the
image type.
from io import BytesIO
def png_bytes(colour: str) -> BytesIO:
picture = Image.new("RGB", (240, 240), "white")
ImageDraw.Draw(picture).rectangle((30, 30, 210, 210), fill=colour)
buffer = BytesIO()
picture.save(buffer, format="PNG")
buffer.seek(0)
return buffer
workbook = xlsxwriter.Workbook("from_bytes.xlsx")
sheet = workbook.add_worksheet()
sheet.set_column("B:B", 12)
for row, colour in enumerate(["#f43f8f", "#b4740a", "#0f9488"], start=1):
sheet.set_row(row, 60)
sheet.write(row, 0, colour)
sheet.embed_image(row, 1, f"swatch{row}.png", {"image_data": png_bytes(colour)})
workbook.close()
xlsxwriter stores each distinct image once, even when it is embedded in many cells, so a status icon repeated down a column of thousands of rows adds
only one copy of the picture to the file. The same BytesIO approach works for matplotlib figures, as shown in
Embed a matplotlib Chart in an Excel Report.
Step 4 — Link an embedded image
Adding a url option makes the cell a hyperlink as well as a picture — useful for linking a thumbnail to a full-size photo or a product page. xlsxwriter
applies its default hyperlink format unless you pass a cell_format.
workbook = xlsxwriter.Workbook("linked.xlsx")
sheet = workbook.add_worksheet()
sheet.set_row(1, 60)
sheet.set_column("A:A", 12)
sheet.embed_image(1, 0, "widget.png", {
"url": "https://example.com/products/W-100",
"description": "Widget product photo, links to the product page",
})
workbook.close()
Hyperlinks in general, including links to other sheets and cells, are covered in Add Hyperlinks to Excel Cells with Python.
Step 5 — Plan for readers and tools that do not support it
A picture in a cell is stored as a rich value, a newer part of the file format. Three consequences follow, and each is worth deciding about before a report goes out.
- Older Excel versions show
#VALUE!in those cells, because xlsxwriter stores that error as the fallback value. If your readers include people on perpetual-licence Excel, use floating images instead. - pandas reads nothing for those cells: they come back as missing values, so an embedded-image column never pollutes data reads.
- openpyxl drops embedded pictures when it loads and re-saves the workbook. It sees
#VALUE!in the cell and does not keep the rich value parts, so any post-processing step must happen before xlsxwriter writes the file, not after.
import pandas as pd
from openpyxl import load_workbook
print(pd.read_excel("catalog.xlsx")) # Photo column is NaN
print(load_workbook("catalog.xlsx")["Products"]["D2"].value) # '#VALUE!'
Step 6 — Use floating images where they fit better
Floating images from insert_image remain the right tool for anything that does not belong to a row: a logo in the header area, a chart image beside a
table, a signature block. Their object_position option controls how they respond to row and column changes — 1 moves and sizes with cells, 2 moves
but does not size, and 3 neither moves nor sizes.
workbook = xlsxwriter.Workbook("mixed.xlsx")
sheet = workbook.add_worksheet()
sheet.set_row(0, 50)
sheet.insert_image("A1", "gadget.png", {"x_scale": 0.2, "y_scale": 0.2, "object_position": 3})
sheet.write("C1", "Quarterly product catalogue")
sheet.set_row(2, 60)
sheet.set_column("D:D", 12)
sheet.write_row(2, 0, ["W-100", "Widget", 19.99])
sheet.embed_image(2, 3, "widget.png")
workbook.close()
Mixing both in one sheet is normal: the logo floats and stays at the top, while product photos are embedded and follow their rows. For logo placement and scaling in detail, see Add a Logo Image to an Excel Report with openpyxl.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Tiny thumbnails | Default row height | set_row to 50–80 points before embedding |
#VALUE! instead of pictures | Reader's Excel does not support pictures in cells | Use insert_image, or require Microsoft 365 |
| Pictures disappear after a later step | Workbook re-saved with openpyxl | Do all processing before xlsxwriter closes the file |
| Photos mismatched after sorting | Images were floating, not embedded | Use embed_image for row-bound pictures |
| File much larger than expected | Full-resolution photos | Resize images to display size before embedding |
AttributeError: embed_image | Old xlsxwriter release | Upgrade xlsxwriter |
Performance and scale notes
File size is driven by the images themselves. A 4,000-pixel product photo displayed in a 90-pixel cell carries the full resolution into the workbook, so downscale to roughly twice the display size with Pillow first. Identical images are stored once, which keeps repeated icons cheap. Large catalogues with hundreds of distinct photos are still slow to open in Excel regardless of how they were written, so consider linking to a photo per row instead of embedding every image when the list runs into the thousands.
Conclusion
Use embed_image whenever a picture belongs to a row — product photos, status icons, per-row thumbnails — so it sorts, filters and copies with its data. Size
rows and columns first, feed images from BytesIO when they come from a database or API, add descriptions for accessibility, and downscale large photos. Keep
insert_image for logos and page furniture, and remember the compatibility rules: older Excel shows #VALUE!, pandas reads nothing, and openpyxl must never
re-save a workbook that contains embedded pictures.
Frequently asked questions
Can openpyxl embed images inside cells? No. openpyxl adds floating images only, and it drops pictures in cells when it re-saves a workbook that contains them. Use xlsxwriter for embedded images.
Does the image resize when I change the row height in Excel? Yes. An embedded picture is fitted to its cell, so making the row taller or the column wider enlarges it, keeping its aspect ratio.
Can I embed the same image in thousands of cells? Yes. xlsxwriter stores each distinct image once and references it from every cell, so repeated icons add very little to the file size.
What image formats are supported? PNG and JPEG are the safe choices for embedded pictures. Convert other formats with Pillow before embedding.
Related
- Up one level: Inserting Images and Logos into Excel — every way to put pictures into a workbook.
- Resize and Position Images in Excel with openpyxl — anchoring floating images precisely.
- Extract Images from an Excel File with Python — getting pictures back out, including embedded ones.
- Building Excel Reports with XlsxWriter — the library behind embed_image.
- Write a Formatted Excel Report with xlsxwriter — formats and layout around the pictures.
More on charts & images
- How-toBuild a Dashboard Sheet with Charts from Multiple Tabs
- How-toBuild a Rolling 12-Month Chart on an Excel Dashboard
- How-toKeep Charts and Images When Filling an Excel Template
- How-toAdd a Chart to an Excel File with xlsxwriter
- How-toAdd Sparklines to an Excel Report with xlsxwriter
- How-toBuild a Waterfall Chart in Excel with Python