Text Extraction
Order numbers, totals and contract clauses often reach a desktop application inside PDF files. TX Text Control reads the text of a PDF in two ways: as a plain-text document in the TextControl, where the user reads and searches it, and as a list of text lines with their page and position, which your code searches with strings, regular expressions or areas of the page. The examples below extend a Windows Forms application with a TextControl named textControl1 and a ListBox named resultsListBox for the results.
Converting a PDF into an editable document with its layout is the subject of PDF Import and Its Limitations.
How Text Extraction Works
TX Text Control extracts all text it can identify in a PDF, adds missing spaces and puts the text blocks into their logical order, as What Happens When a PDF Is Loaded explains. A scanned page is an image: its text has to be recognized with OCR before it can be extracted.
| Way | Returns | Use it for |
|---|---|---|
Load with PDFImportSettings GenerateLines | The text of the PDF as a document, without images and paragraph formatting | Reading and searching the text in the editor, full-text indexes |
The Lines class of the TXTextControl.DocumentServer.PDF.Contents namespace | Each text line with its page number and position | Text per page, searches, values at a position |
Lines reads all text lines of a PDF file or byte array when you create it, without a TextControl. ContentLines returns them as a list of ContentLine objects, and each call of Find filters them, so create one Lines object per document and call Find as often as you need.
Property of ContentLine | Contains |
|---|---|
| Page | The number of the page that contains the line |
| Text | The text of the line |
| X, Y, Width, Height | The position and size of the line on its page, in points |
| Rectangle | The bounds of the line as a RectangleF |
The handlers below need these using directives in Form1.cs:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using TXTextControl;
using TXTextControl.DocumentServer.PDF.Contents;
Open the Text of a PDF in the Editor
With PDFImportSettings.GenerateLines, a PDF is loaded as text only: images and paragraph formatting are discarded, the setting best suited for searching PDF files. The following handler opens the Open dialog box with the filter for PDF files and loads the chosen file this way:
private void openPdfAsTextButton_Click(object sender, EventArgs e)
{
LoadSettings loadSettings = new LoadSettings()
{
// Text only: images and paragraph formatting are left out
PDFImportSettings = PDFImportSettings.GenerateLines
};
// Opens the Open dialog box with the filter for PDF documents
textControl1.Load(StreamType.AdobePDF, loadSettings);
}
The user now reads the text in the editor and searches it in the Find dialog box, which textControl1.Find() opens. Search and Replace shows how to search the text in code. A password-protected PDF needs its password in LoadSettings, as Passwords and Access Permissions shows.
Read the Text Page by Page
When a summary or a search result refers to a page, your code needs the text of that page. The following handler asks for a PDF file, groups its lines by page and lists them in reading order:
private string ChoosePdf()
{
using (OpenFileDialog openDialog = new OpenFileDialog() { Filter = "PDF documents (*.pdf)|*.pdf" })
{
return openDialog.ShowDialog(this) == DialogResult.OK ? openDialog.FileName : null;
}
}
private void showLinesButton_Click(object sender, EventArgs e)
{
string path = ChoosePdf();
if (path == null)
{
return;
}
Lines lines = new Lines(path);
resultsListBox.Items.Clear();
foreach (var page in lines.ContentLines.GroupBy(line => line.Page).OrderBy(group => group.Key))
{
resultsListBox.Items.Add($"--- Page {page.Key} ---");
// From top to bottom, then from left to right
foreach (ContentLine line in page.OrderBy(line => line.Y).ThenBy(line => line.X))
{
resultsListBox.Items.Add(line.Text);
}
}
}
The Lines constructor takes the path of a PDF file or the PDF as a byte array. The handler sorts the lines of each page by their Y and X coordinates, as the blog article Mining PDFs with Regex in C#: Practical Patterns, Tips, and Ideas does.
Search With Regular Expressions
Find with a regular expression returns every line that contains a match. The following handler finds invoice numbers such as INV-2025-001 and lists each one with its page:
private void findInvoiceNumbersButton_Click(object sender, EventArgs e)
{
string path = ChoosePdf();
if (path == null)
{
return;
}
// Invoice numbers such as INV-2025-001
string pattern = @"\bINV-\d{4}-\d{3}\b";
Lines lines = new Lines(path);
List<ContentLine> found = lines.Find(pattern, RegexOptions.IgnoreCase);
resultsListBox.Items.Clear();
foreach (ContentLine line in found)
{
Match match = Regex.Match(line.Text, pattern, RegexOptions.IgnoreCase);
resultsListBox.Items.Add($"Page {line.Page}: {match.Value}");
}
}
The code works as follows:
Find(string regex, RegexOptions options)takes a .NET regular expression and its options and returns the lines that match.Findreturns whole lines, soRegex.Matchcuts the matched value out of the text of each line.Find(string text)without options returns the lines that contain a text.
A value that wraps to the next line isn't found by an expression for a single line. The blog article of the previous section shows patterns for dates, amounts and clause numbers and reads the neighboring lines for context.
Read a Value From an Area of the Page
Documents of the same layout, such as invoices of one supplier, print a value at the same place. Find with a rectangle returns the lines in that area. The following handler takes the area of the invoice number from a sample invoice and reads the same area from every PDF in a folder the user chooses:
private void readInvoiceNumbersButton_Click(object sender, EventArgs e)
{
// Find the area of a known value in a sample document
Lines sample = new Lines("invoice-sample.pdf");
ContentLine known = sample.Find("INV-2025-001").FirstOrDefault(line => line.Page == 1);
if (known == null)
{
MessageBox.Show("The sample invoice doesn't contain the invoice number.");
return;
}
RectangleF area = known.Rectangle;
using (FolderBrowserDialog folderDialog = new FolderBrowserDialog())
{
if (folderDialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
resultsListBox.Items.Clear();
// Read the same area of other documents of the same layout
foreach (string file in Directory.GetFiles(folderDialog.SelectedPath, "*.pdf"))
{
Lines lines = new Lines(file);
// true: lines that intersect the area, not only lines completely inside it
ContentLine value = lines.Find(area, true).FirstOrDefault(line => line.Page == 1);
resultsListBox.Items.Add($"{Path.GetFileName(file)}: {value?.Text ?? "(no value)"}");
}
}
}
The code works as follows:
Find(RectangleF rectangle, bool partially)returns the lines in a rectangle. Withfalse, a line must lie completely inside it; withtrue, it's enough that the line intersects it, so a longer value is found as well.- The rectangle comes from a line of the sample, so the coordinates are those of the PDF files themselves rather than values measured by hand.
RectangleFis part ofSystem.Drawing. Find(PointF position, float radius, bool partially)returns the lines within a radius around a point.
The blog article Template-Based Text Extraction from PDF Documents in .NET C# reads several fields of flattened forms this way.
Get the Text and the Coordinates in One Load
To extract text without showing it, load the PDF into a ServerTextControl, the TX Text Control without a user interface. With LoadEmbeddedData in the import settings, LoadSettings.EmbeddedData also contains the text coordinates as XML, in which each line has its page number and position:
private void textAndCoordinatesButton_Click(object sender, EventArgs e)
{
using (ServerTextControl tx = new ServerTextControl())
{
// Initialize the new instance before using it
tx.Create();
LoadSettings loadSettings = new LoadSettings()
{
PDFImportSettings = PDFImportSettings.GenerateLines | PDFImportSettings.LoadEmbeddedData
};
tx.Load("invoice.pdf", StreamType.AdobePDF, loadSettings);
File.WriteAllText("invoice.txt", tx.Text);
if (loadSettings.EmbeddedData != null &&
loadSettings.EmbeddedData.TryGetValue(EmbeddedDataFormat.TextCoordinates, out object coordinates))
{
File.WriteAllText("coordinates.xml", coordinates.ToString());
}
}
}
ServerTextControl.Text returns the text of the document as one string. The keys of EmbeddedData are values of EmbeddedDataFormat: TextCoordinates, FormFields and MetaData. The dictionary is filled only when LoadEmbeddedData is set, and it contains the formats the PDF provides.
Next Steps
- PDF Import and Its Limitations: load a PDF as an editable document with its layout.
- Reading Form Fields and Values: read the values of PDF form fields.
- Document Metadata: read the title, author and keywords of a PDF.
- Attachments: extract the files embedded in a PDF.