Converts DOCX, PDF, XLSX, XLS, CSV and images into a single CommonMark .md file — without
truncation, at any input size.
Two properties define the library:
- Extension-aware strategy selection. The extension picks the converter, validated against
the file's magic bytes and container, so a DOCX named
report.pdfis either handled correctly or rejected loudly — never garbled. - Nothing is lost. The pipeline streams end to end (
IAsyncEnumerable<DocumentBlock>from converter to writer), so a 250,000-row CSV or a 5 MB PDF converts in bounded memory with every row, page and cell present in the output.
Images and scanned PDF pages are read with OCR.
Full design: docs/PRODUCT-SPEC.md.
dotnet add package Markdown.FileConverterusing Markdown.FileConverter.Abstractions;
using Markdown.FileConverter.Core;
var service = ConversionService.CreateDefault();
// File in, file out.
ConversionReport report = await service.ConvertFileAsync("statement.pdf", "statement.md");
// Or stream in, stream out.
using var source = await SourceDocument.FromStreamAsync(upload, "upload.docx");
await service.ConvertAsync(source, outputStream);With dependency injection:
services.AddMarkdownFileConverter(); // registers every built-in converter
services.AddSingleton<IOcrEngine, MyOcr>(); // or swap a pieceAdding a format takes one IDocumentConverter, a SourceFormat member, and a registration — no
change to the pipeline:
public sealed class RtfConverter : IDocumentConverter
{
public IReadOnlySet<SourceFormat> SupportedFormats { get; } = new HashSet<SourceFormat> { SourceFormat.Rtf };
public bool CanConvert(SourceDocument source) => true;
public async IAsyncEnumerable<DocumentBlock> ReadAsync(
SourceDocument source, ConversionOptions options, [EnumeratorCancellation] CancellationToken ct = default)
{
yield return new HeadingBlock(1, [new InlineRun("Title")]);
// ... yield blocks lazily; never buffer the document
}
}| Option | Default | Effect |
|---|---|---|
Flavor |
CommonMark |
Strict CommonMark. GitHub adds pipe tables, alerts and footnotes. |
DetectionPolicy |
Strict |
Sniff converts by content on mismatch, Trust believes the extension. |
Ocr |
Auto |
OCR images, and PDF pages with almost no extractable text. Always / Never override. |
CellValues |
Display |
Spreadsheet cells as shown in Excel. Raw emits stored values. |
ImageMode |
Reference |
Sidecar <name>.assets/ folder. Embed inlines data URIs; Omit keeps alt text only. |
IncludeFrontMatter |
true |
YAML provenance block (filename, SHA-256, format, size). |
MaxTableWidth |
50 |
Wider tables render as labelled records — a shape change, never a data loss. |
Output is deterministic: same input plus same options gives byte-identical markdown (the front-matter timestamp is opt-in for exactly this reason).
CommonMark has no table, alert or footnote syntax — those are GitHub extensions. In the default
flavor, tabular data is written as hard-broken rows (every cell intact, each row on its own line),
notes become blockquotes, and footnotes become a numbered ## Notes section. Set
Flavor = MarkdownFlavor.GitHub when the target renderer is GitHub and you want real pipe tables.
The default engine is Tesseract, which needs language data. Point at it with
ConversionOptions.TessDataPath, the TESSDATA_PREFIX environment variable, or a tessdata
folder beside the application. When OCR is unavailable, conversion still succeeds: the image is
referenced and a warning is written into the document rather than a silent blank.
Tesseract reads print, not handwriting. On the OCR fixtures it recovers every printed label of
a scanned form (62% mean confidence) but garbles the handwritten entries, and drops to 18-32% on
fully handwritten pages. The converter always states the confidence rather than passing weak text
off as content. For handwriting, implement IOcrEngine against a cloud reader (Azure AI Vision
Read, Google Vision, AWS Textract) and pass it to ConversionService.CreateDefault(engine) — no
other code changes.
Image decoding and preprocessing (EXIF orientation, greyscale, deskew, contrast stretch, upscale toward 300 DPI) run on Magick.NET, which is Apache-2.0.
dotnet test125 tests, including conversion of the production-compliant documents in
tests/Markdown.FileConverter.Tests/TestCase-Files. Each of those is verified against an
independent extraction of the same file (OpenXML for DOCX/XLSX, PdfPig for PDF): every character
of the source must appear in the markdown, in order. Generated fixtures cover 250,000-row CSVs and
100,000-row workbooks with exact row-count assertions and a memory budget.
This package is MIT. Its dependencies are not all the same licence, which matters if you redistribute:
| Dependency | Purpose | Licence |
|---|---|---|
| DocumentFormat.OpenXml | DOCX, XLSX | MIT |
| UglyToad.PdfPig | PDF text and positions | Apache-2.0 |
| CsvHelper | CSV parsing | MS-PL / Apache-2.0 |
| Magick.NET-Q8-AnyCPU | image decode and preprocessing | Apache-2.0 |
| Tesseract | OCR | Apache-2.0 |
| PDFtoImage / PDFium | PDF page rasterisation | MIT / BSD-3-Clause |
| NPOI | legacy .xls |
Apache-2.0 with an OSMF licence acceptance prompt |
NPOI asks consuming projects to accept its OSMF licence. That prompt does not flow through this
package, but review it if .xls support matters to you.
.xls(BIFF8) has no managed streaming reader, so it is read in full; large legacy workbooks are reported with a warning. Convert to.xlsxfor streaming.- PDF reading order for heavily designed layouts is best-effort; low-confidence regions are kept as text so content survives even when structure does not.
- Charts, SmartArt and pivot tables are noted, not rendered.