Table of Contents

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.