DocWright.Formats.Text 1.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package DocWright.Formats.Text --version 1.1.0
                    
NuGet\Install-Package DocWright.Formats.Text -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DocWright.Formats.Text" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DocWright.Formats.Text" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="DocWright.Formats.Text" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DocWright.Formats.Text --version 1.1.0
                    
#r "nuget: DocWright.Formats.Text, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DocWright.Formats.Text@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DocWright.Formats.Text&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=DocWright.Formats.Text&version=1.1.0
                    
Install as a Cake Tool

DocWright

A from-scratch .NET Word-document library. Near-term goal: DOCX → PDF visually indistinguishable from Word's own PDF export for the supported feature set, proven by automated visual diffs against real oracle output. Long-term goal: a DocIO-class library built on the same core.

The tree ships the whole pipeline today: DOCX and legacy DOC readers, a lossless DOCX writer, the mutable DOM with an editing API, style resolution and layout, PDF and raster renderers, mail merge, document compare, content controls and form fields, ECMA-376 decryption, and HTML/RTF/Markdown/plain-text import and export — on top of the frozen plugin seams, the reference-oracle pipeline and the three-metric visual diff harness. See ROADMAP.md for what is done and what is next, and COMPAT.md for the supported feature set.

Cross-app authoring guidance

For teams authoring DOCX content that must remain readable across Microsoft Word and LibreOffice, use the one-page standard here:

Solution map

Project Purpose
src/DocWright.Core Twips/EMU/Color primitives, fixed-point math, plugin seams (IDocumentReader/IDocumentWriter, ILayoutRenderer, ITextShaper), IConversionDiagnostics, exception taxonomy. Zero dependencies.
src/DocWright.Dom Mutable, lossless document object model: blocks, inlines, styles, numbering, theme, shapes and settings, plus the editing API, the DOM field engine, revisions, content controls and form fields, and the preservation islands that make round-trip lossless.
src/DocWright.Formats.Doc Legacy Word 97-2003 (.doc) reader over a reusable CFB parser.
src/DocWright.Formats.Docx Streaming DOCX reader and the lossless DOCX writer, including DrawingML/VML shape parsing and verbatim island re-emission. The only package that may depend on DocumentFormat.OpenXml.
src/DocWright.Formats.Html HTML import and export: a tolerant, dependency-free HTML5-shaped parser, a documented CSS subset, and a DOM-to-HTML writer with self-contained and linked output modes.
src/DocWright.Formats.Markdown Markdown import and export: a dependency-free CommonMark 0.31.2 block and inline parser with the GitHub-flavored table, strikethrough and task-list extensions, checked against the specification's own 652 examples.
src/DocWright.Formats.Rdl SSRS Report Definition Language export (.rdl), for RDL 2016/01 and 2010/01. Turns a document into a report template — page setup, Tablix tables, embedded images and merge fields bound as =Fields!X.Value dataset expressions — validated against both committed XSDs. Export only; there is no RDL reader.
src/DocWright.Formats.Rtf RTF 1.9 import and export: a dependency-free tokenizer and destination reader, single-byte code pages compiled in so decoding never depends on host configuration, and a deterministic DOM-to-RTF writer.
src/DocWright.Formats.Text Plain-text import and export: encoding detection, line-ending and paragraph-splitting policy on the way in, and a GetText-based writer with page-break and table-rendering options on the way out.
src/DocWright.Fonts Font discovery, OpenType/TTC parsing and metrics, kerning and cluster advances, glyph-outline decoding (glyf and CFF Type 2), CFF subsetting, and GlyphRunPlanner — the single place both renderers decide which glyphs a run draws.
src/DocWright.Layout StyleResolver + layout engine + shape geometry (guide evaluation, preset outlines, gradients, charts). The resolver is the one-way bridge DOM → immutable ResolvedDocument; layout never sees the DOM.
src/DocWright.Renderers.Pdf Single-pass PDF renderer with TrueType/CFF subsetting. Consumes the laid-out seam only.
src/DocWright.Renderers.Imaging Raster renderer: the same laid-out seam to PNG/JPEG pages. Dependency-free, integer-deterministic (byte-identical output across runs and TFMs).
src/DocWright.MailMerge Merge-field discovery, a data-source abstraction with no System.Data dependency, simple and nested-region merges, and conditional record flow over the DOM field engine.
src/DocWright.Compare Tree diff over two WordDocuments producing tracked changes (w:ins, w:del, w:rPrChange, w:pPrChange), with paragraph alignment, move detection, run-level LCS and bounded execution. Implements no seam — an ordinary DOM consumer.
src/DocWright.Cryptography ECMA-376 / MS-OFFCRYPTO: agile and standard encrypted-package detection and decryption, legacy RC4 and XOR obfuscation (read), document-protection password hashes, and a host-owned signing seam. BCL cryptography only.
src/DocWright.ComplexText Optional HarfBuzz-backed complex-text shaper behind ITextShaper. Opt-in: nothing in the core path requires it.
src/DocWright.Composition Fluent document composition: ComposedDocument.Create(...) authors a document from code and lowers it onto the DOM, so it renders through the ordinary pipeline to PDF, images or .docx. No second layout engine — the composer never breaks pages, which is what COMPAT.md's maps/approximates/refused matrix is built around.
src/DocWright.Formats.Pdf PDF import: object layer, cross-reference machinery, filters, content-stream interpreter, colour spaces, fonts, encryption, and a page rasterizer. The opposite direction from Renderers.Pdf, not its partner.
src/DocWright.Formats.Odt OpenDocument Text export, gated by LibreOffice opening every output. Export only; there is no ODT reader.
src/DocWright.Formats.Epub EPUB 3 export over the HTML core, gated by EPUBCheck. Export only.
src/DocWright.Ole The MS-CFB compound-file reader and OLE payload extraction, below Formats.Doc and Cryptography — a .doc, an encrypted package and an OLE payload are all compound files.
src/DocWright.Signatures OPC package signature verification and signing, and PDF signing. The one package that takes System.Security.Cryptography.Xml, because a hand-written C14N produces signatures that verify against nothing but themselves.
src/DocWright Facade: DocWrightConverter.Convert(input, pdf, options), ConvertToImages(input, imageOptions, options), ConvertPdfToImages, Load and Save, with format auto-detection via DocumentFormatRegistry.
tests/DocWright.Tests xunit suite, runs on net48, net8.0 and net10.0 — every runtime TFM the packages ship; public-API approval tests, zero-allocation gates, tr-TR culture leg, and the hostile-input fuzz layers.
tests/DocWright.Rendering.Tests net10.0-only visual harness tests. The only test project allowed native deps (PDFium via Docnet, ImageSharp).
bench/DocWright.Benchmarks BenchmarkDotNet hot-path benchmarks (net48 + net8.0 + net10.0); committed baselines in bench/results/.
tools/DocWright.VisualHarness The harness library: PDF rasterization, pixel/SSIM/glyph metrics, artifacts, thresholds, scoreboard. Never shipped.
tools/ConvertCompareApp Converts input/test.docx and diffs it against the Word reference, writing side-by-side, overlay, heatmap and report.json artifacts per page. The primary fidelity-triage loop.
tools/CorpusGen Regenerates the seed corpus (byte-stable output).
tools/ReferenceGen Oracle scripts + RefRaster (PDF → reference PNGs + manifest).
tools/Ratchet Nightly threshold-tightening proposal script.

All library projects multi-target net48;netstandard2.0;net8.0;net10.0; netstandard2.0 is the functional floor, and the net8+/net10 legs are trim/AOT-clean (IsAotCompatible).

Quick start

dotnet build DocWright.slnx -c Release            # all TFMs
dotnet test tests/DocWright.Tests -c Release      # unit tests (net48 + net10)
dotnet test tests/DocWright.Rendering.Tests -c Release   # visual harness
dotnet run --project bench/DocWright.Benchmarks -c Release -f net10.0 -- --filter '*'

Package smoke validation (Phase 10 release gate)

dotnet pack DocWright.slnx -c Release -o artifacts/nuget
powershell -NoProfile -ExecutionPolicy Bypass -File .\eng\Run-Phase10Smoke.ps1 `
  -RepositoryRoot (Get-Location).Path `
  -PackageDirectory (Resolve-Path .\artifacts\nuget).Path

Run-Phase10Smoke.ps1 validates:

  • net48 console consumer (PackageReference) conversion,
  • net10 ASP.NET Core minimal API conversion over HTTP,
  • net10 NativeAOT publish + conversion smoke (requires NativeAOT C++ toolchain prerequisites).

Net48 quickstarts

PackageReference (SDK-style):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="DocWright" Version="0.10.0" />
  </ItemGroup>
</Project>

packages.config (non-SDK):

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="DocWright" version="0.10.0" targetFramework="net48" />
</packages>

Net10 minimal API quickstart

using DocWright;

var app = WebApplication.CreateBuilder(args).Build();
app.MapPost("/convert", async (HttpRequest request, CancellationToken cancellationToken) =>
{
    await using var output = new MemoryStream();
    var converter = new DocWrightConverter();
    await converter.ConvertAsync(request.Body, output, options: null, cancellationToken);
    return Results.File(output.ToArray(), "application/pdf", "converted.pdf");
});
app.Run();

Compose a document from code

DocWright.Composition authors a document with a fluent, nested surface that reads like the page it produces. It lowers onto the DOM, so the result is a real Word document — every line of shaping, table, pagination and PDF/A work applies to it, and .SaveAsDocx() comes free.

using DocWright.Composition;

var invoice = ComposedDocument.Create(document =>
{
    document.Page(page =>
    {
        page.Size(PageSizes.A4).Margin(2, Unit.Centimetre);
        page.DefaultTextStyle(text => text.FontFamily("Calibri").FontSize(11));

        page.Header().Text("INVOICE 2026-0042").FontSize(20).Bold();

        page.Content().Column(column =>
        {
            column.Spacing(8);
            column.Item().Text("Bill to: Contoso Ltd.");
            column.Item().Table(table =>
            {
                table.Columns(c => { c.Relative(3); c.Relative(1); c.Relative(1); });
                table.Header(h =>
                {
                    h.Cell().Text("Item").Bold();
                    h.Cell().Text("Qty").Bold();
                    h.Cell().Text("Total").Bold();
                });

                foreach (var line in lines)
                {
                    table.Cell().Text(line.Name);
                    table.Cell().AlignRight().Text(line.Quantity);
                    table.Cell().AlignRight().Text(line.Total);
                }
            });
        });

        page.Footer().AlignCenter().Text(t =>
        {
            t.Span("Page "); t.CurrentPageNumber(); t.Span(" of "); t.TotalPages();
        });
    });
});

invoice.GeneratePdf("invoice.pdf");
invoice.GenerateImages(new ImageRenderOptions { Width = 1200 });
invoice.SaveAsDocx("invoice.docx");

A table's header row repeats on every page it crosses, and PAGE/NUMPAGES are substituted per page — both by the existing layout engine, not by anything the composer added.

The composer does not break pages; the layout engine does. Anything that would have to measure against the space left on a page — ShrinkToFit, dynamic components, infinite-height containers — is refused by design and absent at compile time rather than throwing. COMPAT.md carries the full maps / approximates / refused matrix, and ToWordDocument() is the escape hatch for anything the fluent surface cannot express.

Phase 10 end-to-end benchmark smoke matrix

Commands:

$env:DOCWRIGHT_BENCH_SMOKE='1'
dotnet run --project bench/DocWright.Benchmarks -c Release -f net48 -- --filter '*EndToEndConversionBenchmarks*'
dotnet run --project bench/DocWright.Benchmarks -c Release -f net8.0 -- --filter '*EndToEndConversionBenchmarks*'
dotnet run --project bench/DocWright.Benchmarks -c Release -f net10.0 -- --filter '*EndToEndConversionBenchmarks*'

Observed Dry means (single-iteration smoke profile):

Scenario net48 net8.0 net10.0
Small document 2.198 s 1.405 s 1.345 s
500-page document 3.041 s 2.413 s 2.491 s
Table-heavy document 1.745 s 1.005 s 1.021 s
Image-heavy document 2.001 s 1.043 s 1.101 s

Reports:

  • bench/results/phase10/net48/DocWright.Benchmarks.EndToEndConversionBenchmarks-report-github.md
  • bench/results/phase10/net8.0/DocWright.Benchmarks.EndToEndConversionBenchmarks-report-github.md
  • bench/results/phase10/net10.0/DocWright.Benchmarks.EndToEndConversionBenchmarks-report-github.md
  • bench/results/phase10/net48/shortrun-artifacts/results/DocWright.Benchmarks.EndToEndConversionBenchmarks-report-github.md
  • bench/results/phase10/net8.0/shortrun-artifacts/results/DocWright.Benchmarks.EndToEndConversionBenchmarks-report-github.md
  • bench/results/phase10/net10.0/shortrun-artifacts/results/DocWright.Benchmarks.EndToEndConversionBenchmarks-report-github.md

The reference oracle (the definition of "perfect")

References live under references/<category>/<doc>/: the oracle's document.pdf, its pages rasterized to page-NNN.png at 150 DPI via PDFium, and reference.json recording the oracle identity (word-<build> or libreoffice-<version>), export settings, DPI and content hashes. Reference PNGs/PDFs are stored in Git LFS.

Regeneration is deliberate and manual — never per-CI:

# Uses Word COM automation when Word is installed; LibreOffice otherwise.
powershell tools/ReferenceGen/Generate-References.ps1 [-Oracle word|libreoffice] [-Filter 'seed-01*']

Every comparison records which oracle produced its reference (oracleId in actuals and scoreboard). The CI workflow refresh-references.yml (manual trigger) does the same on a self-hosted Windows runner with Word.

The committed corpus references are Word 16.0 exports (word-16.0.16.0.20131), regenerated 2026-07-25. They were LibreOffice exports before that, and switching oracles moved every number — it retired the single largest apparent defect (a ~1% advance-width drift that turned out to be LibreOffice's) and exposed one the old oracle could not see (WQ-0005 row banding). Treat any fidelity measurement as meaningless without the oracleId it was taken against, and don't compare numbers across oracles.

Real-world documents enter the corpus through the intake script, which strips personal information via Word and assigns default thresholds:

powershell tools/ReferenceGen/Intake-Document.ps1 -Path C:\docs\contract.docx -Category 02-styles

The visual diff harness

Three metrics per page, candidate vs reference:

  1. Pixel diff % — after a 1-pixel tolerance dilation (a pixel only counts as different if it cannot be matched within the 3×3 neighborhood in both directions), over the union canvas. Catches missing/moved content while forgiving single-pixel rasterization jitter.
  2. SSIM — structural similarity over luma, uniform 8×8 sliding window; 1.0 = identical.
  3. Glyph-position diff — characters and boxes are extracted from both PDFs via PDFium and compared with per-axis twip tolerances. This catches sub-visual drift (a fraction of a pixel is many twips) and localizes it to specific glyphs.

On any threshold failure the harness writes, per page: a side-by-side PNG, a red diff heatmap over the reference, and a JSON report with the worst diff rectangles and the glyph mismatch list (under artifacts/visual/, or DOCWRIGHT_ARTIFACTS_DIR).

Two comparison gates

The harness is driven from tests/DocWright.Rendering.Tests in two distinct modes:

  1. Oracle self-check (CorpusTests) — compares each committed reference PDF against itself. It exercises the full production path (discovery, rasterization, glyph extraction, thresholds, scoreboard) and guards against rasterizer/reference drift; it is not a fidelity signal (candidate == reference). Actuals: artifacts/visual/corpus/.
  2. Candidate fidelity gate (DocWrightFidelityTests) — the real DOCX → PDF signal. For every reference it converts the source DOCX with DocWrightConverter and compares DocWright's own PDF against the oracle through the same three metrics. Actuals: artifacts/visual/candidate/candidate-actuals.json.

The candidate gate has two enforcement tiers:

  • Always enforced: a well-formed PDF is produced and its page count matches the oracle. Differing pagination can never be "visually identical", so it is a hard failure.
  • Enforced under DOCWRIGHT_ENFORCE_FIDELITY=1: the pixel/SSIM/glyph budgets below. Every document's real numbers are written to candidate-actuals.json and the test output either way, so a non-enforcing run still reports the current fidelity.

Both tiers are conditional on the font environment. DocWright must resolve the same families the oracle embedded (the seed corpus is Calibri / Calibri Light); a substituted family has different advance widths, which moves line breaks and therefore page breaks. So a run whose conversion reports DXP3004 — the converter's family-level substitution summary — prints its numbers and gates on nothing, naming the substituted families. Style-level simulations on a family that is installed (DXP3001 synthetic bold, for instance) are deterministic DocWright behavior, not an environment difference, and do not disarm the gate. The flag is set in CI, but the Linux harness job has no Calibri, so it reports; run the harness where the reference fonts exist to make it gate. candidate-font-substitutions.json next to the actuals records what a given run substituted.

Thresholds

Two files, on deliberately different scales:

  • tests/corpus/thresholds.json — the oracle self-check budget, and the floor the perturbed fixture must breach to prove the harness detects a 40tw shift. Actuals here are exactly 0, so it stays tight.
  • tests/corpus/fidelity-thresholds.json — the candidate fidelity budget: DocWright's measured gap against the oracle plus ~20% headroom. This is recorded debt, not a target. It has no $default: every corpus document carries its own reviewed, measured entry, and a document without one fails with instructions rather than silently inheriting a budget.

Both map a corpus doc id (01-plain-text/seed-01-...) to {maxPixelDiffPercent, minSsim, maxTextDxTwips, maxTextDyTwips}. Semantics: a page fails when pixelDiff% exceeds its max, SSIM falls below its min, or any glyph drifts beyond a per-axis twip tolerance.

Keeping them apart matters: DocWright's real gaps are orders of magnitude larger than the self-check's, so widening the shared file to fit them would silently retire both the rasterizer-drift guard and the perturbation detection test.

Ratchet policy

Thresholds only ever tighten. The nightly job (nightly-ratchet.yml) runs the harness and proposes tightened values wherever actuals beat thresholds by more than 20% (proposal = actual + 20% headroom), once per file — the self-check budget from artifacts/visual/corpus/actuals.json, the fidelity budget from artifacts/visual/candidate/candidate-actuals.json. Each proposal is an artifact; applying one is a reviewed commit. Loosening a threshold requires a human and a justification.

Scoreboard

scoreboard.json at the repo root is the long-term fidelity trend line: one appended entry per recorded commit with the corpus-wide fidelity score (mean over pages of ssim × (1 − pixelDiff)), per-document scores, and the oracles used. The corpus test appends an entry when DOCWRIGHT_UPDATE_SCOREBOARD=1 (with DOCWRIGHT_COMMIT=<sha>).

Corpus

Graded folders under tests/corpus/: 01-plain-text08-doc-binary. The three seed documents in 01-plain-text are generated by tools/CorpusGen (OpenXml) and are byte-stable: zip entries are sorted, timestamps pinned, relationship ids fixed — so the corpus is reproducible from source. tests/fixtures/perturbed/ holds a deliberately shifted variant of seed-01 (top margin +2pt) whose oracle export must fail the thresholds — the harness's canary.

Determinism rules (enforced from Phase 0)

  • All layout arithmetic is integer twips (1/20pt); font-unit conversion is funits × emSize / unitsPerEm with round-half-away-from-zero (FixedPointMath).
  • Floating point only in font parsing and at the renderer boundary.
  • InvariantCulture everywhere; CI runs the entire suite under tr-TR.
  • No timestamps in output unless caller-supplied (PdfMetadata.CreationDate).
  • Public API is approval-tested (tests/DocWright.Tests/PublicApi/*.approved.txt); evolution is additive-only within a major version. To re-approve after an intentional additive change: DOCWRIGHT_APPROVE_API=1 dotnet test -f net10.0.
  • Hot paths are covered by zero-allocation tests (net10) and BenchmarkDotNet baselines; later phases may not regress committed baselines by more than 5%.

CI

  • Windows job — builds every TFM, runs the net48 test leg.
  • Linux job — net10 tests with an 85% line-coverage gate on core projects, the same suite under tr-TR, the visual harness against committed references (LFS), and a benchmark smoke run.
  • refresh-references — manual, self-hosted Windows + Word; opens a PR with regenerated references.
  • nightly-ratchet — proposes threshold tightenings as an artifact.

Word quirks

When observed Word output contradicts ECMA-376/ISO-32000, Word wins — every such case is recorded in WORDQUIRKS.md with a repro document and the Word build.

For host-side compatibility extension guidance (registering external readers/fallbacks), see COMPAT.md.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 is compatible.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on DocWright.Formats.Text:

Package Downloads
DocWright

Convert, render and edit Word documents in .NET — no Microsoft Word, no Office interop, no native dependencies. Reads DOCX and legacy DOC, renders to PDF and PNG/JPEG, saves to DOCX, HTML, Markdown, RTF, text, ODT, EPUB and RDL, rasterizes existing PDFs, and edits documents through a mutable model with find/replace, placeholder filling, content controls and tracked changes. This is the main package: install it for the whole pipeline.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.1 155 8/22/2026
1.1.0 165 8/22/2026

See CHANGELOG.md for release notes.