Templates, styles across documents, embedded files and macros¶
Word templates¶
A .dotx or .dotm template holds exactly the same markup as a document. It differs
only in the content type of its main part, which is what tells Word to start a new
document from it rather than open it for editing.
Templates open like anything else:
save() keeps whichever it already is, so a template
opened and saved is still a template. as_template overrides that, which is how you
generate a document from a template — or turn a document into one:
document.save("report.docx", as_template=False) # a document from a template
document.save("house-style.dotx", as_template=True) # a template from a document
Macro-enabled input stays macro-enabled either way, so a .dotm saved with
as_template=False is a .docm.
Note
This sets the content type. It does not choose the file extension for you — pass a name whose extension matches, or Word will complain about the mismatch.
Copying a style between documents¶
Applying a style by name fails with KeyError: no style with name 'Callout' whenever the
target document's style part lacks it, which is routine when content is assembled from
several sources. copy_style_from() brings
one across:
template = Document("house-style.dotx")
report = Document()
callout = template.styles["Callout"]
report.styles.copy_style_from(callout)
report.add_paragraph("Mind the gap", style="Callout")
The dependency closure is the point. A style is not a self-contained object:
w:basedOn names the style it inherits from, w:next the style for the following
paragraph, and w:link the paired character or paragraph style. Copying one w:style
element by hand gives a style whose basedOn target is missing, which then renders as if
it inherited from Normal. Those are followed and copied too, unless you say otherwise:
A list style references numbering.xml, so include_numbering (on by default) copies
the w:num and w:abstractNum behind it and rewrites the reference to the new id.
Name collisions¶
report.styles.copy_style_from(callout, name="House Callout")
report.styles.copy_style_from(callout, on_collision="overwrite")
on_collision decides what happens when the target already has a style of that name:
| Value | Behaviour |
|---|---|
"skip" (default) |
leave the existing style alone and return it |
"overwrite" |
replace its definition |
"rename" |
copy under a free name — "Callout 2", and so on |
"raise" |
raise ValueError |
Warning
Theme fonts are not carried over. A style referencing w:asciiTheme resolves
against this document's theme part, so a copied style can legitimately look
different in its new home.
Importing a template's styles¶
copy_style_from() moves one style. Two operations built on it move whole sets, which is
what a house template usually calls for:
report = Document()
result = report.styles.import_from("house-style.dotx")
# -> {"Callout": "added", "Heading 1": "skipped", ...}
Styles.import_from() accepts a path, a stream or
an already-open Document — a .dotx opens without special handling, since a template
holds the same main part as a document. It returns a report keyed by UI name, saying what
it did with each: "added", "replaced" or "skipped".
report.styles.import_from("house-style.dotx", ["Callout", "Sidebar"])
report.styles.import_from("house-style.dotx", overwrite=True)
report.styles.import_from("house-style.dotx", include_latent=True)
Without overwrite, a name the destination already defines is skipped and reported as
such — the existing definition wins, which is what "import these styles into my document"
almost always means. include_latent also brings the source's w:latentStyles
exceptions across, and is off by default because that changes which of Word's built-ins
appear in the destination's style gallery.
The other direction pulls styles out of a document into a template of their own:
added = report.styles.extract("house.dotx", as_template=True)
added = report.styles.extract("headings.docx", ["Heading 1", "Heading 2"])
xml = report.styles.extract_xml(["Heading 1"]) # -> just the styles.xml bytes
Styles.extract() writes an otherwise empty document
carrying the named styles and their dependency closure, and returns the names it added.
Naming nothing extracts every style the document defines. The extract starts from a
template whose own unused styles have been pruned, so what comes out is the styles you
named plus the handful the closure keeps alive — Normal, Default Paragraph Font and the
other defaults — rather than those plus the 168 the bundled template ships.
as_template=True writes a .dotx — the same content with the template content type, so
Word treats it as a template rather than a document.
Embedded OLE objects¶
An OLE object is a whole file carried inside the document and shown as an icon or a
preview image that opens the original application on double-click. This is a different
thing from an altChunk: an altChunk is dissolved into the document when Word opens the
file, whereas an embedded object stays a distinct file forever.
The read side matters on its own. A document with attachments embedded in it previously gave no way to discover that they exist, let alone get them out:
from pathlib import Path
for obj in document.embedded_objects:
print(obj.prog_id, obj.content_type, obj.filename)
if obj.blob is not None:
Path(obj.filename or "attachment").write_bytes(obj.blob)
Document.embedded_objects covers the body and
Run.embedded_objects one run. Each
EmbeddedObject offers:
prog_id |
the application Word launches, e.g. "Excel.Sheet.12" |
blob |
the bytes of the embedded file, or None |
content_type |
the content type of the embedded part |
filename |
the basename of the part it landed in, e.g. "oleObject1.bin" |
is_linked |
True when the object links to an external file instead of embedding it, in which case there are no bytes in the package |
shows_icon |
True when Word shows an icon rather than a preview |
image |
the icon or preview image Word displays |
embedded_part |
the package part itself |
Note
OOXML does not record the original file name of an embedded object. filename is the
name of the part it was stored in, which is what a caller extracting it has to work
with.
Writing one takes an icon, and the icon is required:
run = document.add_paragraph().add_run()
run.add_embedded_object(
"budget.xlsx",
icon="excel-icon.png",
prog_id="Excel.Sheet.12",
)
Word cannot render the embedded file itself, so without an image there is nothing to draw
where the object sits. prog_id defaults to "Package", the generic value Word uses for a
file it has no better name for — an object whose ProgID names no installed application is
one Word displays but cannot open, so pass the right one when you know it. width and
height default to the icon's own size.
The visual is VML, not DrawingML, because that is what Word writes for an OLE object.
Macros¶
A macro-enabled document keeps its VBA project as a single opaque blob,
word/vbaProject.bin:
document = Document("macros.docm")
document.has_macros # -> True
len(document.vba_project) # -> the project bytes
Reading, transplanting and stripping one are all expressible:
# -- move a project into a generated document --
generated = Document()
generated.vba_project = Document("macros.docm").vba_project
generated.save("generated.docm")
# -- strip the macros out of a document you received --
received = Document("received.docm")
del received.vba_project # -- or: received.vba_project = None
received.save("safe.docx")
received.remove_vba_project() # -> how many were removed, 0 or 1
Assigning or removing a project switches the main part's content type with it, which is the part that is easy to get wrong by hand: Word ignores macros in a document that does not claim to be macro-enabled, and warns about macros in a document that claims to be macro-enabled but is not. A template switches to the macro-enabled template type rather than the document one.
Note
The blob is not parsed. The project is an OLE compound file with compressed module streams inside it; reading the source of a macro is a separate matter and is not supported.
Embedding another document¶
An altChunk embeds a whole file — HTML, RTF, another .docx — and lets Word import it
on open:
document = Document()
document.add_alt_chunk(b"<h1>Report</h1><p>Generated.</p>", content_type="text/html")
chunk may be bytes, a path, or a file-like object open for binary read. Note that a
str is read as a path, not as content — pass content as bytes, as above.
content_type must be right, because Word picks its importer from it:
| Content type | Format |
|---|---|
text/html |
HTML |
text/plain |
plain text |
application/rtf |
RTF |
application/vnd.openxmlformats-officedocument.wordprocessingml.document |
.docx |
with open("appendix.docx", "rb") as f:
document.add_alt_chunk(
f,
content_type=(
"application/vnd.openxmlformats-officedocument"
".wordprocessingml.document"
),
)
Warning
Word performs the import when it opens the document, so the embedded content is
not visible to this library. Its paragraphs and tables do not appear in
Document.paragraphs,
Document.tables or
iter_inner_content(), and nothing here
can style it. It is a handover to Word, not a merge.
Readers other than Word may ignore altChunk entirely.
Document.alt_chunks lists what has been embedded.