pub struct Docx { /* private fields */ }Expand description
A Word document opened as a template.
from_bytes opens a .docx file, the replace_*
methods fill in its placeholders, and to_bytes or
write_to give the result. Replacements apply to the
main text and to every header, footer, footnote and endnote; see
replace_text for how placeholders are matched.
Writing doesn’t consume the document, and a Docx can be cloned, so a
template can be opened once and each output filled from a clone of it.
Clones share the original file and copy only the parsed parts.
use ciclostile::Docx;
let template = Docx::from_bytes(std::fs::read("letter.docx")?)?;
for name in ["Ada", "Grace"] {
let mut letter = template.clone();
letter.replace_text([("{{name}}", name)]);
std::fs::write(format!("letter-{name}.docx"), letter.to_bytes())?;
}Implementations§
Source§impl Docx
impl Docx
Sourcepub fn from_bytes(bytes: impl Into<Bytes>) -> Result<Self, InvalidDocx>
pub fn from_bytes(bytes: impl Into<Bytes>) -> Result<Self, InvalidDocx>
Open a .docx file.
bytes can be anything that converts into Bytes without a
copy, such as the Vec<u8> that std::fs::read returns or a
&'static [u8]. Entries that are copied to the output are not
copied in memory: the output shares them with bytes.
Only Transitional Office Open XML is supported, the format Word and
LibreOffice write, with XML parts in UTF-8: Strict Office Open XML
documents, encrypted (password-protected) documents and legacy
.doc files are rejected. Templates (.dotx, .dotm) and
macro-enabled documents (.docm) open too, and keep their content
type once filled, so save them with the same kind of extension: Word
refuses to open a .dotx saved as .docx. To produce a .docx, start
from a .docx.
Only the parts the replacements need are decompressed and parsed: the content types, the relationships, the main document part and its headers, footers, footnotes and endnotes, the document settings, plus the comments and the glossary document, if any, to collect the ids of their drawings. Other entries, such as images, are copied without ever being decompressed.
§Limits
The parts read may decompress to 64 MiB in total and hold 4 million nodes (elements, runs of text, comments) together. Their elements may nest 512 levels deep, under at most 128 namespace declarations, with element and attribute names of at most 256 bytes. Input beyond these limits is rejected, so opening a document takes at most about 650 MB of memory, whatever the input.
§Errors
Returns InvalidDocx::Zip when bytes is not a ZIP archive that
can be read and written back, or when a part that must be parsed
can’t be decompressed (including when the parts read add up to more
than 64 MiB); InvalidDocx::MissingPart when [Content_Types].xml,
_rels/.rels, the main document part or a story part it refers to
is missing; InvalidDocx::Xml when one of the parts read is
malformed, or when they hold more than 4,000,000 nodes together;
and InvalidDocx::NotWordprocessingMl when the file is not a
Transitional WordprocessingML document, such as a spreadsheet, a
presentation or a Strict OOXML document.
Sourcepub fn replace_text<I, K, V>(&mut self, replacements: I) -> usize
pub fn replace_text<I, K, V>(&mut self, replacements: I) -> usize
Replace every occurrence of each key of replacements with its
value, and return the number of replacements made.
The keys are placeholders: plain text, such as {{name}}, matched
exactly and case-sensitively in the text of each paragraph, as Word
displays it. Word often stores text typed in one go as several runs -
after a formatting change, a spelling check or an edit made on
another day - so {{name}} may be stored as {{, name and }}
with markup in between: it still matches. A placeholder never spans
two paragraphs, nor anything in a paragraph that isn’t text: one
interrupted by a tab, a line break, a picture, an equation, a
footnote reference or a field is left as it is. Text deleted with
tracked changes isn’t matched.
The main text is searched, including its tables, text boxes and content controls, and so are headers, footers, footnotes and endnotes. Other parts, such as comments, are left alone. Word stores each text box twice, as DrawingML and as VML for older readers, so a placeholder in a text box counts twice.
All pairs are applied in a single pass over the text as it was
before the call: text inserted for a key is never searched again, for
that key or another one, so replacing {{a}} with {{b}} and
{{b}} with x in the same call turns {{a}} into {{b}}. Where
two keys start at the same place, the longer one wins. Empty keys are
ignored, and a key given more than once only uses its first value.
A value takes the formatting of the first character of the
placeholder it replaces. In values, line breaks (\n, \r\n or
\r) become line breaks in the paragraph and tabs become tab
characters. Characters that XML can’t represent (most control
characters) are dropped.
Each occurrence gets its own copy of its value, and the template decides how many occurrences there are, so a small template can make a large document. With untrusted templates, bound their size or the number of placeholders they hold.
Pairs can come from any collection of string-like pairs, such as an
array of tuples, a Vec<(String, String)> or a map. A
&Vec<(String, String)> yields references to tuples instead: pass
pairs.iter().map(|(k, v)| (k, v)), or the Vec itself.
use std::collections::HashMap;
use ciclostile::Docx;
let mut docx = Docx::from_bytes(std::fs::read("template.docx")?)?;
let values = HashMap::from([
("{{name}}".to_owned(), "Ada Lovelace".to_owned()),
("{{address}}".to_owned(), "12 St James's Square\nLondon".to_owned()),
]);
let replaced = docx.replace_text(&values);
println!("{replaced} placeholders replaced");Sourcepub fn replace_with_image(&mut self, placeholder: &str, image: &Image) -> usize
pub fn replace_with_image(&mut self, placeholder: &str, image: &Image) -> usize
Replace every occurrence of placeholder with image, and return
the number of replacements made.
The placeholder is matched like a key of
replace_text. The image takes its place
inline, as part of the line of text, at the size set on image. Its
file is added to the document once per call, however many
placeholders it replaces. An empty placeholder matches nothing.
Each occurrence gets a drawing of its own, so a small template can
make a large document: 200,000 image placeholders in a 2.4 KB .docx
take 830 MB of memory to fill. With untrusted templates, bound their
size or the number of placeholders they hold. An occurrence nested
so deep, or under so many namespace declarations, that its drawing
would exceed the limits of from_bytes is left
as it is, and not counted.
When no placeholder is found, the document is left unchanged.
Sourcepub fn replace_with_table(
&mut self,
placeholder: &str,
table: &Table,
) -> Result<usize, InvalidPlaceholder>
pub fn replace_with_table( &mut self, placeholder: &str, table: &Table, ) -> Result<usize, InvalidPlaceholder>
Replace every paragraph that holds only placeholder with table,
and return the number of paragraphs replaced.
The placeholder is matched like a key of
replace_text, and must be the only content
of its paragraph: the two are compared with the whitespace around
each trimmed, non-breaking spaces included. The table takes the
paragraph’s place, and keeps its bookmarks and comment anchors.
Empty paragraphs are kept or added where Word requires them: after
the table when the placeholder’s paragraph ended a section or isn’t
followed by another paragraph (a table cell, for one, must end with
a paragraph), and before it when it would follow another table
(adjacent tables would merge). An empty placeholder matches nothing.
Each paragraph replaced gets a copy of the whole table (see
Table for how many cells it has), so a small template can make a
large document. With untrusted templates, bound their size or the
number of placeholders they hold. A paragraph nested so deep, or
under so many namespace declarations, that the table would exceed
the limits of from_bytes is left as it is, and
not counted.
§Errors
Returns InvalidPlaceholder when placeholder occurs in a
paragraph that holds anything else. The document is then left
unchanged: no paragraph is replaced, not even those that qualify.
Sourcepub fn update_fields_on_open(&mut self)
pub fn update_fields_on_open(&mut self)
Make Word offer to update the document’s fields when it opens the file, so that tables of contents and page references show the pages of the filled document.
Nothing else needs to be laid out again: a .docx file stores no pages. Word and LibreOffice paginate a document when they open it, so longer text simply pushes what follows onto the next pages. Only the cached results of fields depend on the layout: a .docx file stores the result each field had when it was last saved, so a table of contents or a page reference keeps the page numbers saved in the template, even where the replacements moved text to other pages, and Word only refreshes them when the fields are updated (F9).
This sets w:updateFields in the document settings (ECMA-376 Part 1,
§17.15.1.90), creating the settings part if the document has none.
Word then asks the reader, when opening the file, whether to update
the fields: the reader has to accept the prompt, and Word then
updates every field with its own layout. LibreOffice ignores the
setting: there, update the fields with Tools > Update > Update All.
Calling it again changes nothing.
use ciclostile::Docx;
let mut docx = Docx::from_bytes(std::fs::read("report.docx")?)?;
docx.replace_text([("{{findings}}", "A long text that moves the conclusion.")]);
docx.update_fields_on_open();
std::fs::write("filled.docx", docx.to_bytes())?;Sourcepub fn write_to<W: Write + ?Sized>(&self, writer: &mut W) -> Result<()>
pub fn write_to<W: Write + ?Sized>(&self, writer: &mut W) -> Result<()>
Write the document as a .docx file to writer.
Parts that no replacement modified are never re-serialized: their compressed entries are copied byte for byte, and uncompressed entries keep identical content but come out compressed. Modified parts keep the exact bytes of all the XML the replacements didn’t touch: attribute order, namespace prefixes, whitespace and comments included. Writing doesn’t change the document, which can be written again or modified further.
writer receives many small writes: wrap a file or a socket in a
BufWriter, and flush it once write_to
returns, so that an error from it isn’t lost when it is dropped.
use std::{
fs::File,
io::{BufWriter, Write},
};
let mut out = BufWriter::new(File::create("output.docx")?);
docx.write_to(&mut out)?;
out.flush()?;§Errors
Returns the io::Error of writer when writing to it fails;
writer may then hold part of the archive.