Word documents
DocxEditor is the whole DOCX surface: fill a template somebody made in Word, build a document from scratch when there is no template, read the text back out, and export it somewhere that is not Word.
Filling a template
The common case. Somebody in the business produces invoice.docx in Word with {{customer}} typed
where the name goes, and your job is to put the name there.
byte[] template = await HtmlToDocxConverter.ConvertAsync("<p>Customer: {{customer}}</p>");
byte[] filled = DocxEditor.ReplaceText(template, new Dictionary<string, string>
{
["{{customer}}"] = "Contoso Ltd",
});
That looks too simple to need a library, and it would be, except for one thing:
Word splits placeholders across runs. {{customer}} is frequently three or four separate
<w:t> elements in the XML — because someone corrected a typo in the middle of it, or a spell
checker touched it, or it was pasted. A find-and-replace over the document XML finds nothing and
reports success, which is the single most common way hand-rolled Word templating fails. ReplaceText
splices the runs back together before matching, which is most of what it is for.
ReplaceText also reaches into headers and footers, so a customer name in a letterhead is
replaced too.
One row per record
A table where the row count depends on your data — invoice lines, a roster, a statement. Put a
single template row in the document and let FillRows clone it.
byte[] withRows = DocxEditor.FillRows(invoiceTemplate, "item", new[]
{
new Dictionary<string, string> { ["Desc"] = "Widget", ["Qty"] = "2", ["Total"] = "19.98" },
new Dictionary<string, string> { ["Desc"] = "Gadget", ["Qty"] = "5", ["Total"] = "45.00" },
new Dictionary<string, string> { ["Desc"] = "Doohickey", ["Qty"] = "1", ["Total"] = "7.50" },
});
byte[] invoice = DocxEditor.ReplaceText(withRows, new Dictionary<string, string>
{
["{{customer}}"] = "Contoso Ltd",
});
Each generated row keeps the template row's formatting: borders, shading, fonts, column widths. That is the reason to do this rather than build the table yourself.
Important
Expand rows first, then fill scalars. FillRows clones the template row, so any scalar
placeholder already inside it gets duplicated into every generated line. The sample follows this
order even where it does not strictly matter, because the safe order is the one worth having in
your fingers.
The collection name in the placeholder ({{item.Desc}}) matches the collection argument
("item"). Anything the row does not consume is left alone.
Images
ReplaceImage swaps a text placeholder for actual image bytes.
byte[] branded = DocxEditor.ReplaceImage(letterhead, "{{logo}}", logo, widthPoints: 96);
// The same call, a different format. Nothing here tells DocToolkit which it is.
branded = DocxEditor.ReplaceImage(branded, "{{stamp}}", stamp, widthPoints: 24);
Sizes are in points. Give one dimension and the other scales to keep the aspect ratio; give neither and the image's own header decides, read at 96 DPI.
The format is decided by the bytes, never by a filename. PNG and JPEG are read by completely
different code paths — a PNG states its dimensions at a fixed offset in the IHDR chunk, while a
JPEG hides them in a Start-Of-Frame segment that has to be found by walking the marker chain. A
file called logo.png that actually holds JPEG bytes is read as the JPEG it is, because the
alternative is a blank frame in Word and no error anywhere.
For images referenced by URL rather than handed over as bytes, see Images the HTML points at.
When there is no template
Sometimes the document's shape comes from your data, not from a file somebody made. Describe it as a sequence of DocxBlock values and skip the round trip through HTML.
byte[] report = DocxEditor.Create(
new[]
{
DocxBlock.Heading("Quarterly report", 1),
DocxBlock.Paragraph("Revenue by region, in thousands."),
DocxBlock.Table(
new[] { "Region", "Q1", "Q2" },
new[]
{
new object?[] { "EMEA", 1200, 1310 },
new object?[] { "APAC", 980, 1040 },
}),
},
PageSetup.A4.WithMargins(54));
DocxBlock has four factories — Heading, Paragraph, Table and Image — which is
deliberately not a document model. It covers reports and statements. Anything that needs real
layout control wants a template, where a person with Word can do the layout.
Create takes an optional PageSetup, same as the HTML converters, and defaults to A4.
Note
Generated documents have no headers or footers. DocxEditor.Create and HtmlToDocxConverter
produce a body. ReplaceText does reach into the headers and footers of a document you supply
— the limit is on generating them, not on editing them.
Reading text back out
ExtractText returns the document's text as a string, which is what you want for search
indexing, diffing, or asserting in a test that the fill actually worked.
string text = DocxEditor.ExtractText(docx);
string withChrome = DocxEditor.ExtractText(docx, includeHeadersAndFooters: true);
The default excludes headers and footers, because a letterhead repeated on every page is noise in
an index. Pass true when you want the whole thing.
Exporting it somewhere else
The same document, as a PDF, as HTML for a web page, or as Markdown for a record you can diff.
string html = DocxToHtmlConverter.Convert(invoice);
string markdown = DocxToMarkdownConverter.Convert(invoice);
As HTML : 795 chars, has a <table>: True
As Markdown : 147 chars, first line "# Invoice for Contoso Ltd"
And DocxToPdfConverter for the PDF:
byte[] pdf = DocxToPdfConverter.Convert(invoice);
DocxToPdfConverter.ConvertFile("invoice.docx", "invoice.pdf");
Two things to know before you wire these into something:
The HTML is a full document, not a fragment. DocxToHtmlConverter.Convert emits
<html><head>…<body>. There is no fragment mode — producing one would mean re-serialising the
renderer's output. If you are embedding the result in a page, extract the body with an HTML parser
rather than a regular expression. Both text converters embed images as data: URIs, so what you
get is self-contained with no asset files to host.
PDF fonts depend on the machine doing the conversion. Where a system font is available it is embedded; in a slim container with no fonts installed, nothing is embedded and the PDF falls back to the base-14 standard fonts. Both are valid and both render, but they are not byte-identical. See Fonts before you compare PDF hashes across environments.