A .NET library that converts Microsoft Word DOCX documents or HTML content into PNG images, PDF, semantic HTML, or Markdown.
Either input can produce any of the four outputs, and a document is parsed once no matter how many formats it is exported to.
Try it live in the browser — a Blazor WebAssembly app that converts a DOCX to PNG, PDF, Markdown or plain text client-side, built on Morph.ImageSharp and Morph.Pdf.
This project participates in the Open Source Maintenance Fee. The source code is freely available under the terms of the license. To support sustainable maintenance, use of the project's official binary releases in revenue-generating activities and all government agencies requires adherence to the Open Source Maintenance Fee EULA. The fee is paid by sponsoring Papyrine.
This project uses SponsorCheck to surface a build-time reminder in consuming projects that are not yet sponsoring.
- .NET 10.0 or later
- Cross-platform support: Windows, macOS, Linux
Which package is needed depends on the output format, not the input — DOCX and HTML are both handled by every package:
| Output | Package | Notes |
|---|---|---|
| HTML | Morph |
No rendering backend required |
| Markdown | Morph |
No rendering backend required |
Morph.Pdf |
Vector text via PdfSharp | |
| PNG | Morph.Skia or Morph.ImageSharp |
Pick a rendering backend |
Morph.Skia, Morph.ImageSharp and Morph.Pdf all depend on Morph, so any of them also brings the HTML and Markdown exporters.
- Font families and sizes
- Bold, italic, underline, strikethrough
- Text colors and highlighting
- All caps, small caps
- Superscript, subscript
- Character spacing
- Text alignment (left, right, center, justified)
- Indentation (first-line, hanging, left, right)
- Spacing (before, after, line spacing)
- Contextual spacing
- Paragraph borders
- Multiple sections with different margins/orientation
- Page breaks (manual and automatic)
- Section breaks (continuous, next page, odd/even)
- Headers and footers
- Page numbering
- Line numbering
- Complex table structures with merged cells
- Cell borders and shading
- Table styles
- Nested tables
- Column widths
- Bullet lists
- Numbered lists
- Multi-level lists with various numbering styles
- Custom list formatting
- Embedded images (JPEG, PNG)
- Shapes (rectangles, circles, etc.)
- Drawing objects
- SVG content
- Ink/handwriting annotations
- Theme support (colors, fonts)
- Compatibility modes (Word 2007 and later)
- Font resolution — name-table-driven, deterministic across platforms
- Hyphenation
- HTML content via AltChunk
The HTML, Markdown and PDF exporters run off the same parsed model as the PNG renderers above — the DOCX parser for a .docx source, the HTML parser for an HTML source — so the same content carries across, each within the limits of its format:
- HTML — semantic tags (
<strong>,<em>,<u>,<h1>–<h6>,<table>,<ul>/<ol>) over one embedded stylesheet, with inline overrides only where a run or paragraph deviates from the document defaults. Theme colours (includingthemeShade/themeTint), per-run fonts (with generic fallbacks) and sizes, the page background, paragraph spacing / indentation / alignment / borders, and table cell widths / shading / borders / vertical alignment are preserved; background shapes, gradients and accent panels are emitted as inline SVG behind the text. - Markdown — CommonMark with GFM pipe tables; adjacent runs are coalesced so emphasis stays well-formed and headings stay clean.
- PDF — vector text via PdfSharp, paginated to match the source page layout.
Export galleries — a visual index of every scenario's exporter output (PDF pages beside the Word reference render):
- HTML
- Markdown
- PDF — pages rendered by Verify.PDFium
Morph supports two rendering backends:
| Backend | Package (DOCX + HTML) | Pros |
|---|---|---|
| SkiaSharp | Morph.Skia |
Mature, includes SVG support |
| ImageSharp | Morph.ImageSharp |
Fully managed (no native dependencies) |
Rendering comparison gallery — every scenario rendered by both backends side by side against the Microsoft Word reference image, with per-page error metrics.
The examples below use the SkiaSharp backend. To use ImageSharp instead, replace SkiaDocumentConverter with ImageSharpDocumentConverter.
var converter = new SkiaDocumentConverter();
var result = converter.ConvertToImages(
"document.docx",
"output-folder");
Console.WriteLine($"Generated {result.PageCount} pages");
foreach (var path in result.ImagePaths)
{
Console.WriteLine($"Created: {path}");
}var converter = new SkiaDocumentConverter();
var imageData = converter.ConvertToImageData("document.docx");
foreach (var pngBytes in imageData)
{
// Use the PNG byte array as needed
}var converter = new SkiaDocumentConverter();
using var stream = File.OpenRead("document.docx");
// From stream to files
var result = converter.ConvertToImages(stream, "output-folder");
// Or from stream to memory
var imageData = converter.ConvertToImageData(stream);var converter = new SkiaDocumentConverter();
var options = new ImageExportOptions
{
Dpi = 300,
FontWidthScale = 1.08
};
var result = converter.ConvertToImages(
"document.docx",
"output-folder",
options);Morph also serializes a DOCX directly to semantic HTML, Markdown, or a vector-text PDF — no backend choice needed for HTML / Markdown. Per-format options classes (HtmlExportOptions, MarkdownExportOptions, PdfExportOptions) carry the knobs relevant to each output.
The HTML output renders background shapes, gradients and accent panels from the source as inline SVG behind the text, so coloured backgrounds and decorative artwork survive the conversion.
var html = DocumentConverter.ConvertToHtml("document.docx");
File.WriteAllText("document.html", html);var markdown = DocumentConverter.ConvertToMarkdown("document.docx");
File.WriteAllText("document.md", markdown);var outputPath = "document.pdf";
PdfDocumentConverter.ConvertToPdf("document.docx", outputPath);For multi-format export, WordDocument parses the source a single time and supports calling as many ExportToXxx methods as needed. The PDF extension method comes from Morph.Pdf; HTML and Markdown are built in.
// Parse once with WordDocument, then export to as many formats as you like — the source
// .docx is only opened and parsed a single time.
var document = new WordDocument("document.docx");
File.WriteAllText("document.html", document.ExportToHtml());
File.WriteAllText("document.md", document.ExportToMarkdown());
document.ExportToPdf("document.pdf"); // extension method from Morph.PdfBy default Morph inlines images as base64 data URIs. Pass an ImageHandler to write images to disk, upload them to a CDN, or reference them however suits the calling pipeline.
// Write images to a media folder and reference them relatively, instead of base64-inlining.
Directory.CreateDirectory("media");
var html = DocumentConverter.ConvertToHtml(
"document.docx",
new()
{
ImageHandler = image =>
{
var extension = image.ContentType switch
{
"image/svg+xml" => "svg",
"image/jpeg" => "jpg",
_ => "png"
};
var path = $"media/image-{image.Index}.{extension}";
File.WriteAllBytes(path, image.Data);
return path;
}
});Some source features can't always be represented in the chosen output (ink strokes in HTML, foreground vector shapes in PDF, etc.). Pass an OnWarning callback to discover what was dropped.
// Discover features in the source that couldn't be fully represented in the output —
// unsupported elements (ink strokes, vector shapes), missing fonts, etc.
var warnings = new List<ExportWarning>();
var html = DocumentConverter.ConvertToHtml(
"document.docx",
new()
{
OnWarning = warnings.Add
});
foreach (var warning in warnings)
{
Console.WriteLine($"[{warning.Kind}] {warning.Message}");
}Render only specific pages — useful for previews / thumbnails.
// Render only the first three pages of the document.
var firstThreePages = PdfDocumentConverter.ConvertToPdf(
"document.docx",
new()
{
Pages = new(Start: 1, End: 3)
});
File.WriteAllBytes("document-preview.pdf", firstThreePages);To use ImageSharp instead, replace SkiaHtmlConverter with ImageSharpHtmlConverter. HTML parsing is asynchronous (AngleSharp), so these APIs return a Task.
var converter = new SkiaHtmlConverter();
var result = await converter.ConvertToImages(
"<h1>Hello</h1><p>World</p>",
"output-folder");
Console.WriteLine($"Generated {result.PageCount} pages");
foreach (var path in result.ImagePaths)
{
Console.WriteLine($"Created: {path}");
}var converter = new SkiaHtmlConverter();
var imageData = await converter.ConvertToImageData("<h1>Hello</h1><p>World</p>");
foreach (var pngBytes in imageData)
{
// Use the PNG byte array as needed
}The same exporters are available for an HTML source: HtmlConverter.ConvertToHtml normalizes the input into the same semantic HTML the DOCX path emits, HtmlConverter.ConvertToMarkdown converts it to Markdown, and PdfHtmlConverter.ConvertToPdf (from Morph.Pdf) paginates it into a vector-text PDF.
var markdown = await HtmlConverter.ConvertToMarkdown("<h1>Hello</h1><p>World</p>");
File.WriteAllText("page.md", markdown);var pdf = await PdfHtmlConverter.ConvertToPdf("<h1>Hello</h1><p>World</p>");
File.WriteAllBytes("page.pdf", pdf);HtmlDocument is the HTML-source counterpart to WordDocument — construct it with LoadAsync, then export as many times as needed off the single parse.
// Parse once with HtmlDocument, then export to as many formats as you like — the
// source HTML is only parsed a single time.
var document = await HtmlDocument.LoadAsync("<h1>Hello</h1><p>World</p>");
File.WriteAllText("page.html", document.ExportToHtml());
File.WriteAllText("page.md", document.ExportToMarkdown());
document.ExportToPdf("page.pdf"); // extension method from Morph.PdfA DOCX authored in Word carries parts that hold no rendering information. DocumentCleaner removes them. Applied to this repository's own 328-document test corpus it recovered 9.5 MB, 14% of the corpus on disk.
The biggest single contributor is the Explorer preview picture Word writes when "Save Thumbnails" is on: 46 documents carried one, totalling 7.1 MB, and in the worst case a 3.85 MB card template was 91% preview picture, leaving 350 KB once stripped.
DocumentParts |
Package location | What it is |
|---|---|---|
Thumbnail |
docProps/thumbnail.* |
The preview picture Explorer shows. Usually the largest part in a template. |
Glossary |
word/glossary/ |
Building blocks and Quick Parts, used only by Word's insert UI. |
CustomXml |
customXml/ |
Custom XML data islands and their properties. |
RevisionAuthors |
word/people.xml |
Display names for tracked-change author IDs. |
None of these are reachable from word/document.xml content, so removing them cannot change what any of Morph's exporters produce. Parts that survive are copied across verbatim — only the package relationships and [Content_Types].xml are rewritten, and only to drop entries that would otherwise dangle.
// Strips every part that carries no rendering information. Returns what was
// actually removed, or DocumentParts.None if there was nothing to strip — in
// which case the file is left byte-for-byte untouched.
var removed = DocumentCleaner.Remove("document.docx");
Console.WriteLine($"Removed: {removed}");DocumentParts is a [Flags] enum, so a subset can be selected, and Find reports what a package holds without touching it:
// Drop only the Explorer preview picture, keeping building blocks and custom XML.
DocumentCleaner.Remove("document.docx", DocumentParts.Thumbnail);
// Or report what a package is carrying without modifying it.
var present = DocumentCleaner.Find("document.docx");
if (present.HasFlag(DocumentParts.Thumbnail))
{
Console.WriteLine("This document has a preview picture");
}
// Stream overloads write the cleaned package to a destination of your choosing.
using var source = File.OpenRead("document.docx");
using var target = File.Create("document-clean.docx");
DocumentCleaner.Remove(source, target, DocumentParts.Thumbnail | DocumentParts.Glossary);One caveat on CustomXml: a content control can carry a w:dataBinding into a data island. The bound value is also cached inline in word/document.xml — which is what Morph, and Word until it refreshes, actually reads — but if the island and the cache have drifted apart, removing the island changes what Word eventually shows.
| Option | Type | Default | Description |
|---|---|---|---|
Dpi |
int | 150 | Image resolution in dots per inch |
Font-related options (FontDirectory, FontFallback, DefaultFont, FontWidthScale, DeterministicRendering) are documented in docs/fonts.md.
Impossible Star designed by Rflor from The Noun Project.