Skip to content

Images

An image in the context is an Image, created from the template so that it can add its relationship to the right part:

from docx.shared import Mm

from docxtpl import Template

tpl = Template("letter.docx")
tpl.render({"logo": tpl.image("acme-logo.png", width=Mm(30))})
tpl.save("letter-acme.docx")

In the template, where the picture goes:

{{ logo }}

No prefix is needed. The image is placed where the tag was, in the middle of a sentence if that is where you typed it.

Sizing

Sizes are docx.shared lengths — Mm, Cm, Inches, Pt, Emu:

  • neither given: the image's native size, at its own resolution;
  • one given: the other scales to preserve the aspect ratio;
  • both given: exactly that, aspect ratio or not.
tpl.image("chart.png", width=Mm(160))          # -- height follows --
tpl.image("stamp.png", width=Mm(20), height=Mm(20))

Where the image comes from

Four shapes, all accepted wherever this documentation says a path:

tpl.image("acme-logo.png")                   # -- a path --
tpl.image(pathlib.Path("acme-logo.png"))     # -- or anything path-like --
tpl.image(response.content)                  # -- the bytes themselves --
tpl.image(buffer)                            # -- any stream holding them --

So an image that was generated rather than stored needs no temporary file:

import io

import matplotlib.pyplot as plt

buffer = io.BytesIO()
plt.savefig(buffer, format="png")
tpl.render({"chart": tpl.image(buffer, width=Mm(160))})

Note that buffer is passed as it is — there is no getvalue() and no seek(0). A stream is read once, in full, when the Image is created, and its position is put back exactly where it was found. That matters twice over: the stream is yours and may be something you are reading too, and the same Image rendered in every iteration of a loop would have nothing left to read on the second pass.

A path is the exception, and is opened at render time rather than at construction — so a missing file is reported when the document is built, as it always has been.

The same image used in twenty places is stored in the package once: the parts are matched by content, not by how many objects were created.

When it is not an image

Data that is not in a format the document model can identify raises ImageError, naming what was given:

ImageError: the 4096 bytes read from BytesIO is not in an image format this
package can read. PNG, JPEG, GIF, TIFF, BMP, SVG, WebP, WMF and EMF are
recognised; anything else has to be converted before it reaches the template.

It is a ValueError as well, so an except ValueError around a render keeps working.

Images in headers and footers

A w:drawing refers to its image by relationship id, and a relationship belongs to a part. An image rendered into a page header must be related from the header part; relating it from the document part gives a header Word draws as a red X, with nothing wrong-looking in the XML.

This is handled for you — the relationship is added to whichever part is being rendered — but it is the reason Image needs the template and cannot be built standalone.

Replacing an image the template already has

Some pictures cannot be reached by a tag at all: a logo placed in the header, a chart's backing workbook, an embedded spreadsheet. For those, the template keeps a placeholder and the caller swaps the bytes:

tpl.replace_picture("image1.png", "acme-logo.png")
tpl.replace_media("image2.png", banner_bytes)
tpl.replace_embedded("oleObject1.bin", workbook_stream)
tpl.replace_zip_member("word/media/image3.png", chart_bytes)

The replacement takes the same four shapes an Image does.

The name is the member's filename inside the package — open the .docx with any zip tool to find it. Replacements are recorded and applied when you save(), so they survive repeated renders and two saves produce the same bytes. Naming a member that is not there raises KeyError listing what the package holds, rather than silently shipping the placeholder.

reset_replacements() discards the pending ones.