Three NuGet packages, one HttpClient call.

PdfPig or iText, the Open XML SDK, and a Tesseract wrapper each do one job well. None of the three talk to each other, and none of them do OCR without native binaries.

the-parser-zoo

.NET's document story means picking a library per format and living with each one's specific trade-off. On PDF, PdfPig is MIT-licensed and capable for text extraction but has no OCR of its own; iText is more full-featured but dual-licensed — free under AGPLv3 (which requires open-sourcing anything that links it, in most commercial contexts a non-starter) or a paid commercial license. The Open XML SDK reads .docx/.xlsx/.pptx directly against the OOXML schema, but its object model is low-level by design — pulling plain text out of a paragraph means walking Run and Text elements manually, and it has nothing at all for the legacy .doc/.xls/.ppt binary formats, which need a separate library entirely. OCR means a native Tesseract wrapper (commonly the Tesseract NuGet package, a wrapper over the C++ library) — which ships platform-specific native binaries (win-x64, linux-x64, osx-arm64, …) as part of your deployment, and picking the wrong runtime identifier for a target container is a common source of a DllNotFoundException that only shows up once deployed, not at build time.

librarycoversstops at
PdfPigPDF text extraction, MIT-licensedNo OCR of its own — scanned pages with no text layer return nothing
iTextFull-featured PDF reading and writingFree under AGPLv3 (viral copyleft) or a paid commercial license — a real decision for closed-source products
Open XML SDKDirect, schema-accurate .docx/.xlsx/.pptx accessLow-level object model (manual Run/Text traversal); no legacy .doc/.xls/.ppt support at all
Tesseract (NuGet wrapper)OCR via a native Tesseract binary per platformShips platform-specific native binaries; a wrong runtime identifier fails only after deployment

one-request

txtfetch replaces the three-library split with one call: POST a file or pass ?url= to https://api.txtfetch.com/v1/extract using System.Net.Http and System.Text.Json — both already in the BCL, so this needs zero NuGet packages. No AGPL-vs-commercial licensing decision for PDF, no manual Run/Text traversal for Office files, and no native Tesseract runtime identifier to get right for OCR. The response is the same { "status": "success", "extracted_text": "..." } across every format.

C#
using System.Net.Http.Headers;
using System.Text.Json;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TXTFETCH_KEY"));

using var content = new MultipartFormDataContent();
var fileBytes = await File.ReadAllBytesAsync("report.pdf");
content.Add(new ByteArrayContent(fileBytes), "file", "report.pdf");

var response = await client.PostAsync("https://api.txtfetch.com/v1/extract", content);
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;

Console.WriteLine(json.GetProperty("extracted_text").GetString());
{
  "status": "success",
  "extracted_text": "..."
}

from-a-url

Skip the download entirely — pass a url parameter and txtfetch fetches the document server-side:

C#
using System.Net.Http.Headers;
using System.Text.Json;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TXTFETCH_KEY"));

var url = "https://api.txtfetch.com/v1/extract?url=" + Uri.EscapeDataString("https://example.com/report.pdf");
var response = await client.PostAsync(url, null);
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;

Console.WriteLine(json.GetProperty("extracted_text").GetString());

errors

Every non-success response carries a stable error.code — match on that, not on error.message. See the full error reference for every code and HTTP status txtfetch can return.

C#
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TXTFETCH_KEY"));

using var content = new MultipartFormDataContent();
var fileBytes = await File.ReadAllBytesAsync("report.pdf");
content.Add(new ByteArrayContent(fileBytes), "file", "report.pdf");

var response = await client.PostAsync("https://api.txtfetch.com/v1/extract", content);
var body = await response.Content.ReadAsStringAsync();
var json = JsonDocument.Parse(body).RootElement;

if (response.IsSuccessStatusCode)
{
    Console.WriteLine(json.GetProperty("extracted_text").GetString());
}
else if (response.StatusCode == (HttpStatusCode)429)
{
    var code = json.GetProperty("error").GetProperty("code").GetString();
    var retryAfter = response.Headers.RetryAfter?.Delta?.TotalSeconds;
    Console.WriteLine($"back off: {code}, retry after {retryAfter}s");
}
else
{
    var error = json.GetProperty("error");
    Console.WriteLine($"extraction failed: {error.GetProperty("code").GetString()} — {error.GetProperty("message").GetString()}");
}

big-files-and-batches

Large uploads or slow documents are routed to an async job automatically — 202 plus a job_id to poll — and ?async=true forces that path for any request. Direct upload size ceilings by plan: Hobby 10 MB, Developer 50 MB, Scale 200 MB. Use ?url= for anything larger — server-side fetches aren't held to the upload ceiling. See async jobs & webhooks for the full lifecycle, including webhook delivery instead of polling.

C#
using System.Net.Http.Headers;
using System.Text.Json;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TXTFETCH_KEY"));

var submitUrl = "https://api.txtfetch.com/v1/extract?url="
    + Uri.EscapeDataString("https://example.com/report.pdf") + "&async=true";
var submit = await client.PostAsync(submitUrl, null);
var submitJson = JsonDocument.Parse(await submit.Content.ReadAsStringAsync()).RootElement;
var jobId = submitJson.GetProperty("job_id").GetString();

JsonElement result;
while (true)
{
    await Task.Delay(2000);
    var poll = await client.GetAsync($"https://api.txtfetch.com/v1/extract/{jobId}");
    result = JsonDocument.Parse(await poll.Content.ReadAsStringAsync()).RootElement;
    if (result.GetProperty("status").GetString() != "processing") break;
}

Console.WriteLine(result.GetProperty("extracted_text").GetString());

gotchas

  • iText's AGPLv3 free tier requires open-sourcing software that links it in most distribution models — many commercial .NET teams end up on the paid commercial license specifically to avoid that, which is a cost most PDF-reading tasks don't need to carry.
  • The Open XML SDK's object model mirrors the OOXML schema closely, which is powerful for editing but means even "give me the plain text" requires walking Paragraph → Run → Text nodes yourself — there's no ExtractText() convenience method.
  • Open XML SDK is OOXML-only: a legacy .doc or .xls (still common in older enterprise archives) needs an entirely separate library, since the SDK doesn't parse the OLE2 binary format at all.
  • The Tesseract NuGet wrapper's native binaries are runtime-identifier-specific (win-x64, linux-x64, linux-arm64, …) — a container built for the wrong RID compiles fine and then throws DllNotFoundException the first time OCR actually runs.

formats

faq

How do I extract text from a PDF in C# without iText's AGPL license?
POST the PDF (or pass ?url=) to https://api.txtfetch.com/v1/extract with System.Net.Http — no PDF library, and so no AGPLv3-vs-commercial-license decision to make at all. The response is { "status": "success", "extracted_text": "..." }.
Does the Open XML SDK have a simple way to get plain text from a .docx?
Not directly — its object model mirrors the OOXML schema, so plain text means walking Paragraph, Run, and Text elements yourself. txtfetch returns extracted_text directly for .docx (and .pptx, .xlsx, and the legacy .doc/.xls/.ppt formats the Open XML SDK doesn't read at all).
How do I OCR a scanned document in .NET without shipping native Tesseract binaries?
POST the scan to txtfetch — OCR runs server-side, so there's no Tesseract NuGet package, no per-runtime-identifier native binary, and no DllNotFoundException risk in your deployment.
Is there an official .NET SDK for txtfetch?
Not yet — but there's nothing to install in the meantime. It's a single HttpClient POST using System.Net.Http and System.Text.Json, both already part of the .NET base class library, shown above.

go-further

Stop parsing. Start shipping.

Create an account and get an API key in minutes — the free Hobby plan needs no card.

Get started →