---
title: "Extract text from a PDF in C#"
url: https://recursiveai.org/onepdf/articles/extract-text-from-pdf-csharp/
description: "Extract text from PDF files in C# with OnePDF: every page in reading order, two-column pages, words with positions, uploads and passwords. MIT or Apache-2.0."
date: 2026-10-26
author: OnePDF team
---

# Extract text from a PDF in C#

Extract text from PDF files in C# with OnePDF: every page in reading order, two-column pages, words with positions, uploads and passwords. MIT or Apache-2.0.

October 26, 2026 · OnePDF team

To extract text from a PDF in C#, add the `RecursiveAI.OnePdf` package, open the file with `PdfDocument.Open` and call `ExtractText()`. You get each page’s text in reading order, so two-column pages come out one column after the other, plus every word with its position on the page. OnePDF is pure C# with no native binaries, licensed MIT OR Apache-2.0. This page covers whole documents, multi-column layouts, text from one area of a page, uploads in ASP.NET Core, passwords, and what to do about scans and tables. Every sample was compiled and run against OnePDF 1.11.0 on .NET 10.

## Before you start

*Terminal · NuGet*

```
dotnet new console -o PdfText
cd PdfText
dotnet add package RecursiveAI.OnePdf --version 1.11.0
```

OnePDF targets .NET 10 (`net10.0`). The samples use [IRS Form W-9](https://www.irs.gov/pub/irs-pdf/fw9.pdf) (Rev. March 2024), a six-page US Letter PDF with two-column instruction pages, so you can run them on the same file. Save it as `fw9.pdf` next to the program.

## Extract the text of every page

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
using OnePdf.Text;
 
using PdfDocument doc = PdfDocument.Open("fw9.pdf");
 
// The text of every page, in reading order. Pages are read in parallel.
IReadOnlyList<PdfPageText> pages = doc.ExtractText();
 
using StreamWriter output = File.CreateText("fw9.txt");
for (int i = 0; i < pages.Count; i++)
{
    output.WriteLine($"--- page {i + 1}");
    output.WriteLine(pages[i].Text);
}
 
Console.WriteLine($"{pages.Count} pages, {pages.Sum(p => p.Text.Length):N0} characters");
```

*Output · Console*

```
6 pages, 37,973 characters
```

`fw9.txt` starts with “Form W-9”, “(Rev. March 2024)” and the form’s title, each block separated by a blank line.

- `PdfDocument` holds the file open and reads it as needed, so dispose it. It opens a path, a `Stream` or a `ReadOnlyMemory<byte>`.
- `doc.ExtractText()` reads the pages in parallel, on up to four threads by default. Reading order comes from the structure tree when the file is tagged (the W-9 is) and from the geometry of the page otherwise.
- Ligatures are split into letters, Arabic and Hebrew presentation forms are folded to their base letters, and right-to-left text comes out in logical order.
- Whole-document extraction also flags running headers and footers by their repetition. Set `ExcludeHeadersAndFooters = true` in `PdfTextOptions` to leave them out. The [extract-text recipe](https://recursiveai.org/onepdf/cookbook/extract-text/) shows the options.

## Two-column pages: reading order or layout

Page 2 of the W-9 is set in two columns. A plain left-to-right sweep would mix them line by line. OnePDF has three plain-text modes for a page:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
using OnePdf.Text;
 
using PdfDocument doc = PdfDocument.Open("fw9.pdf");
 
// Page 2 of the W-9 is set in two columns. Page indexes are zero-based.
PdfPageText page = doc.Pages[1].ExtractText();
 
File.WriteAllText("page2-reading-order.txt", page.Text);                  // left column, then right column
File.WriteAllText("page2-layout.txt", page.GetText(PdfTextMode.Layout));  // the page's arrangement, like pdftotext -layout
File.WriteAllText("page2-content-order.txt", page.GetText(PdfTextMode.ContentOrder));
```

The reading-order file keeps the left column together. It starts like this, and the right column (“Foreign person. If you are a foreign person…”) follows on line 104:

*page2-reading-order.txt · Excerpt*

```
Form W-9 (Rev. 3-2024)
 
Page 2
 
must obtain your correct taxpayer identification number (TIN), which
may be your social security number (SSN), individual taxpayer
identification number (ITIN), adoption taxpayer identification number
(ATIN), or employer identification number (EIN), to report on an
information return the amount paid to you, or other amount reportable
on an information return. Examples of information returns include, but
are not limited to, the following.
 
• Form 1099-INT (interest earned or paid).
```

The layout file keeps the page’s arrangement on a monospace grid instead, like `pdftotext -layout`:

*page2-layout.txt · Excerpt*

```
Form W-9 (Rev. 3-2024)                                                                                                           Page 2
 
 
must obtain your correct taxpayer identification number (TIN), which  Foreign person. If you are a foreign person or the U.S. branch of a
may be your social security number (SSN), individual taxpayer         foreign bank that has elected to be treated as a U.S. person (under
identification number (ITIN), adoption taxpayer identification number Regulations section 1.1441-1(b)(2)(iv) or other applicable section for
(ATIN), or employer identification number (EIN), to report on an      chapter 3 or 4 purposes), do not use Form W-9. Instead, use the
```

- **`ReadingOrder`** (the default) is what you want for search, indexing and feeding text to other tools. Blocks are separated by a blank line, lines by a line feed and table cells by a tab.
- **`Layout`** suits fixed-format documents where position carries meaning, such as statements and forms.
- **`ContentOrder`** keeps the order in which the file draws its text, which is how a simple extractor reads.

`page.Text` is the text in the mode you asked for (reading order by default), and `page.GetText(mode)` gives another mode from the same page. How lines, columns and blocks are found, and every threshold involved, is in [text layout and reading order](https://recursiveai.org/onepdf/docs/text-layout/).

## Get words with their positions (text from one area)

Every word comes with its bounding box, so you can read the text inside a region of the page, the way you would pick a field out of a fixed form:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
using OnePdf.Text;
 
using PdfDocument doc = PdfDocument.Open("fw9.pdf");
PdfPageText text = doc.Pages[0].ExtractText();
 
// An area of the page in points (1/72 inch) with the origin at the lower left:
// the box in the top right corner of the W-9.
var area = new PdfRectangle(490, 710, 612, 760);
 
List<PdfTextWord> words = text.Words
    .Where(w => w.Bounds.Left >= area.Left && w.Bounds.Right <= area.Right
             && w.Bounds.Bottom >= area.Bottom && w.Bounds.Top <= area.Top)
    .ToList();
 
Console.WriteLine(string.Join(" ", words.Select(w => w.Text)));
foreach (PdfTextWord w in words.Take(3))
    Console.WriteLine($"{w.Text,-10} left={w.Bounds.Left:F1} bottom={w.Bounds.Bottom:F1} width={w.Bounds.Width:F1}");
```

*Output · Console*

```
Give form to the requester. Do not send to the IRS.
Give       left=496.6 bottom=738.0 width=19.0
form       left=518.1 bottom=738.0 width=20.2
to         left=540.8 bottom=738.0 width=8.7
```

- `Bounds` is in points (1/72 inch) with the origin at the lower left of the page and y going up, after the page’s rotation is applied. A US Letter page is 612 by 792 points.
- `text.Words` lists the page’s words in reading order. `text.Blocks`, each block’s `Lines` and each line’s `Words` give the same words grouped.
- To find the coordinates of an area, print the words of the page with their boxes once, as in [get words and their positions](https://recursiveai.org/onepdf/cookbook/word-positions/). That recipe also groups words into table rows and finds the value printed next to a label.
- For machine processing, the whole result (glyphs, words, lines and blocks) is available as versioned JSON: see [extraction JSON](https://recursiveai.org/onepdf/docs/extraction-json/).

## Read text from an uploaded PDF in ASP.NET Core

In a web app the PDF usually arrives as an upload. Open it from memory with `TryOpen`, which returns an error instead of throwing when the file isn’t a PDF it can read. Create the project with `dotnet new web` and add the package as above. Run it with `dotnet run`, then post a file to the address it prints:

*Program.cs · OnePDF 1.11.0 · ASP.NET Core 10*

```
using OnePdf;
using OnePdf.Text;
 
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
// POST a PDF as multipart/form-data (field "file"); get back the text of each page.
app.MapPost("/pdf/text", async (IFormFile file) =>
{
    using var buffer = new MemoryStream();
    await file.CopyToAsync(buffer);
 
    if (!PdfDocument.TryOpen(buffer.ToArray(), out PdfDocument? doc, out PdfException? error))
        return Results.BadRequest(error?.Message);
 
    using (doc)
    {
        IReadOnlyList<PdfPageText> pages = doc.ExtractText();
        return Results.Ok(pages.Select((p, i) => new
        {
            page = i + 1,
            hasText = !string.IsNullOrWhiteSpace(p.Text),   // false for a scan without a text layer
            text = p.Text,
        }));
    }
}).DisableAntiforgery();
 
app.Run();
```

*Terminal · curl*

```
# APP_URL is the address that dotnet run prints
curl -F file=@fw9.pdf "$APP_URL/pdf/text"
```

For the W-9 this returns six objects, from `{"page":1,"hasText":true,"text":"Form W-9\n\n(Rev. March 2024)…` to page 6. A file that isn’t a PDF gets a 400 response with the reason, here “The file has no usable cross-reference data, trailer or document catalog.” A PDF that is only a scanned image returns `"hasText":false`.

OnePDF is designed for files you didn’t write: it has no `unsafe` code, it never runs a document’s scripts or fetches anything, and limits on decoded size, nesting and object count are on by default and checked before the work they guard. You can tighten them with `PdfOpenOptions.Limits`. Wall-clock limits are yours to set, by passing a `CancellationToken` to `ExtractText`.

## Password-protected PDFs

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
using OnePdf.Text;
 
try
{
    using PdfDocument doc = PdfDocument.Open("statement.pdf", new PdfOpenOptions { Password = "open sesame" });
    Console.WriteLine($"{doc.Encryption?.Cipher}: {doc.Pages[0].ExtractText().Text}");
}
catch (PdfPasswordException)
{
    Console.WriteLine("Wrong or missing password.");
}
```

*Output · Console*

```
Aes256: Statement for October 2026. Balance due: $2,420.00
```

Either the user password or the owner password opens the file. OnePDF reads the standard security handler from RC4 40-bit to AES-256. With a wrong or missing password, `Open` throws `PdfPasswordException`. Files encrypted for specific recipients’ certificates (public-key encryption) aren’t supported. To remove the password and save an unencrypted copy, see [decrypt or encrypt a PDF](https://recursiveai.org/onepdf/cookbook/decrypt-pdf/).

## Scanned PDFs, OCR and tables

- **Scans:** OnePDF does no OCR. A scanned page without a text layer gives empty text, which is how the `hasText` check above works. Run those pages through an OCR tool first.
- **Scans that were already OCRed** carry an invisible text layer. OnePDF includes that text and flags it: `word.IsVisible` is `false`.
- **Tables:** there is no table-extraction API. When a region of the page looks like a table, reading order keeps its cells on their rows, separated by tabs, and the word boxes let you rebuild rows and columns yourself, as the [word-positions recipe](https://recursiveai.org/onepdf/cookbook/word-positions/) does for an invoice.

## The same job with PdfPig or iText

If you are comparing libraries, here is the same whole-document extraction with the two most common alternatives, written from their own documentation and run on the same W-9. PdfPig 0.1.16 (Apache-2.0), following its [README](https://github.com/UglyToad/PdfPig), which recommends its text extractors over `page.Text`:

*Program.cs · PdfPig 0.1.16*

```
using UglyToad.PdfPig;
using UglyToad.PdfPig.Content;
using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor;
 
using PdfDocument document = PdfDocument.Open("fw9.pdf");
foreach (Page page in document.GetPages())
{
    Console.WriteLine($"--- page {page.Number}");
    Console.WriteLine(ContentOrderTextExtractor.GetText(page));
}
```

iText 9.8.0 (AGPL or commercial), following the `LocationTextExtractionStrategy` example in its [knowledge base](https://kb.itextpdf.com/itext/how-to-read-text-from-a-specific-position):

*Program.cs · iText 9.8.0*

```
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas.Parser;
using iText.Kernel.Pdf.Canvas.Parser.Listener;
 
using PdfDocument pdf = new PdfDocument(new PdfReader("fw9.pdf"));
for (int page = 1; page <= pdf.GetNumberOfPages(); page++)
{
    ITextExtractionStrategy strategy = new LocationTextExtractionStrategy();
    Console.WriteLine($"--- page {page}");
    Console.WriteLine(PdfTextExtractor.GetTextFromPage(pdf.GetPage(page), strategy));
}
```

Both read every page. On page 2, iText’s location strategy joins the two columns line by line (“must obtain your correct taxpayer identification number (TIN), which Foreign person. If you are a foreign person…”), while PdfPig’s content-order extractor keeps the columns apart on this file, because the W-9 draws one column after the other. Content order only works when the producer draws in reading order; OnePDF’s reading order comes from the document’s tags or the page geometry instead.

On the project’s published [text-quality benchmark](https://github.com/recursive-ai-oss/onepdf-csharp/blob/v1.11.0/README.md#text-extraction-quality) (62 generated files with exact reference text), OnePDF’s character error on content was 0.3% and its reading-order error 1.2%, against 5.6% and 10.7% for PdfPig 0.1.16’s layout pipeline and 21.9% and 22.7% for its default `Page.Text`. On the [speed benchmark](https://github.com/recursive-ai-oss/onepdf-csharp/blob/v1.11.0/README.md#benchmarks), full text of a 300-page born-digital document took 211 ms against PdfPig’s 238 ms on one thread, and 68.9 ms against 123 ms at the default degree of parallelism. Both ran on an AMD Ryzen 9 7950X3D under Linux with .NET 10.0.13, and the times are BenchmarkDotNet means. Moving code across is covered in [migrating from PdfPig](https://recursiveai.org/onepdf/docs/migrating-from-pdfpig/). For licensing differences between these libraries, see [choosing a C# PDF library](https://recursiveai.org/onepdf/articles/csharp-pdf-library/).

## Where next

- [Getting started with OnePDF](https://recursiveai.org/onepdf/docs/getting-started/): install, read, edit, fill a form, sign, generate.
- [Extract text from a PDF](https://recursiveai.org/onepdf/cookbook/extract-text/) and [get words and their positions](https://recursiveai.org/onepdf/cookbook/word-positions/), the cookbook recipes.
- [Redact text](https://recursiveai.org/onepdf/cookbook/redact/) once you have found it, or [create a PDF in C#](https://recursiveai.org/onepdf/articles/create-pdf-csharp/).
- To turn pages into images instead of text, see [convert a PDF to an image in C#](https://recursiveai.org/oneimage/articles/pdf-to-image-csharp/).
