Skip to content

DOCX Rendering

The Pragmatic.Documents.Docx package renders a DocumentModel to a DOCX (Office Open XML / ISO/IEC 29500) byte stream. The implementation is pure managed — no native dependency, AOT-compatible.


public interface IDocxRenderer
{
void RenderTo(Stream output, DocumentModel model,
DocxResources? resources = null, DocxRenderOptions? options = null);
}
public sealed class DocxRenderer : IDocxRenderer
{
public byte[] Render(DocumentModel model,
DocxResources? resources = null, DocxRenderOptions? options = null);
public void RenderTo(Stream output, DocumentModel model,
DocxResources? resources = null, DocxRenderOptions? options = null);
}
  • Render materialises the full DOCX in memory.
  • RenderTo(Stream) streams into the target.
  • Both accept optional DocxResources (fonts, images) and DocxRenderOptions.

using Pragmatic.Documents.Model;
using Pragmatic.Documents.Docx;
var doc = new DocumentBuilder()
.Title("Quarterly report")
.Author("Alice")
.Page(p => p
.Heading("Q1 2026 Review", level: 1)
.Paragraph(new TextNode { Content = "Revenue grew 12% QoQ." })
.Table(t => t
.HeaderRow("Metric", "Q1", "Q4")
.Row("Revenue", "€1.2M", "€1.07M")
.Row("Headcount", "42", "38")))
.Build();
using var fs = File.Create("report.docx");
new DocxRenderer().RenderTo(fs, doc);

Open the file in Word, LibreOffice, or Google Docs.


The DOCX renderer accepts the same DocumentModel as the PDF renderer. Swap the output format by swapping the renderer:

DocumentModel doc = BuildReport();
using (var pdf = File.Create("report.pdf")) PdfRenderer.RenderTo(pdf, doc);
using (var docx = File.Create("report.docx")) new DocxRenderer().RenderTo(docx, doc);

Not every node maps identically — see the “Node mapping” section below.


NodeDOCX output
TextNode<w:r><w:t> run with style from NodeStyle
HeadingNodeHeading paragraph using the Heading1..Heading9 styles (bookmarked for TOC)
ParagraphNode<w:p> paragraph
TableNode<w:tbl> table with header row styling
ImageNodeEmbedded image (PNG/JPEG) with DrawingML
HyperlinkNodeExternal hyperlink run
TocNodeField-coded TOC (populated on open — see “Table of contents” below)
PageBreakNode<w:br w:type="page"/>
SpacerEmpty paragraph with height
HorizontalRuleParagraph with a bottom border
FieldNode.PageNumber / PageCount / DateField codes (PAGE, NUMPAGES, DATE)
BookmarkNode<w:bookmarkStart> / <w:bookmarkEnd> pair
FootnoteNode<w:footnoteReference> + footnote content
BarcodeNodeRendered as an image (the renderer rasterises barcodes so they survive copy/paste)

DOCX TOCs are field-coded: the TocNode emits a { TOC \o "1-3" \h \z \u } field with placeholder content that Word/LibreOffice refresh when the document is opened.

Enable explicit update on open via DocxRenderOptions:

var options = new DocxRenderOptions { UpdateFieldsOnOpen = true };
new DocxRenderer().RenderTo(fs, doc, options: options);

The renderer uses a heuristic to pre-populate the TOC text with estimated page numbers so the file looks right even in viewers that don’t auto-update fields. Word overwrites this when it refreshes.


var resources = new DocxResources();
resources.AddFont("Inter", File.ReadAllBytes("fonts/Inter-Regular.ttf"));
resources.AddImage("logo", File.ReadAllBytes("logo.png"));
new DocxRenderer().RenderTo(fs, doc, resources);

Fonts are embedded in the DOCX (you pay bytes in the output but the document renders consistently on any machine). Images are linked from the doc to embedded parts in the OOXML package.


Set a DocxTheme to customise heading colours, default fonts, and body text colour:

var options = new DocxRenderOptions
{
Theme = new DocxTheme
{
HeadingColor = "1F497D",
BodyFontFamily = "Calibri",
HeadingFontFamily = "Cambria",
}
};
new DocxRenderer().RenderTo(fs, doc, options: options);

Headers and footers are per-document (not per-page) in DOCX. If the DocumentModel has Header(...) / Footer(...) on a page, the renderer uses them for the whole document.

.Page(p => p
.Header(new TextNode { Content = "Acme Corp — Q1 2026" })
.Footer(
new TextNode { Content = "Page " },
new FieldNode { FieldType = FieldType.PageNumber },
new TextNode { Content = " of " },
new FieldNode { FieldType = FieldType.PageCount }))

  • No multi-column layout (single-column flow only).
  • No advanced drawing shapes (tables and images are supported; SmartArt / charts are not).
  • Custom paragraph styling is limited to NodeStyle properties — if you need complex styled paragraphs, use a designer template instead.
  • Track-changes and comments are out of scope.