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.
| library | covers | stops at |
|---|---|---|
PdfPig | PDF text extraction, MIT-licensed | No OCR of its own — scanned pages with no text layer return nothing |
iText | Full-featured PDF reading and writing | Free under AGPLv3 (viral copyleft) or a paid commercial license — a real decision for closed-source products |
Open XML SDK | Direct, schema-accurate .docx/.xlsx/.pptx access | Low-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 platform | Ships 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.
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:
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.
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.
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
other-languages
Stop parsing. Start shipping.
Create an account and get an API key in minutes — the free Hobby plan needs no card.
Get started →