Text Extraction
Which invoice number does this PDF contain, and on which page is the termination clause? Answering such questions takes the text of the PDF. TX Text Control .NET for WPF gets it in two forms: as a plain-text document in the WPF TextControl, where users read and search it, and as a list of text lines with page numbers and coordinates, which your code filters by text, regular expression or area. The examples extend a window with a TextControl named textControl1 and a ListBox named resultsListBox that shows the results.
To turn a PDF into an editable document that keeps its layout, see PDF Import and Its Limitations instead.
How Text Extraction Works
TX Text Control extracts the text it can identify through the character mapping of the PDF, inserts missing spaces and sorts the text blocks into their logical order, as What Happens When a PDF Is Loaded explains; scanned pages need OCR first.
| 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 |
A Lines object reads every text line of a PDF file or byte array in its constructor and needs no TextControl. ContentLines is the list of ContentLine objects, and Find returns a filtered part of it. Read a document once and call Find on the same object as often as needed.
Prepare the Window
Add the list box to the DockPanel of MainWindow.xaml, before the TextControl element, so that it docks at the right of the editor:
<ListBox x:Name="resultsListBox" DockPanel.Dock="Right" Width="300" />
The handlers below go into MainWindow.xaml.cs and need these using directives. System.Drawing provides the RectangleF structure the coordinates use, and Microsoft.Win32 the file dialogs of WPF:
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.Win32;
using TXTextControl;
using TXTextControl.DocumentServer.PDF.Contents;
Open the Text of a PDF in the Editor
PDFImportSettings.GenerateLines loads a PDF as text only and discards images and paragraph formatting, the setting best suited for searching PDF files. The following handler shows the Open dialog with the PDF filter and loads the chosen file as text:
private void openPdfAsTextButton_Click(object sender, RoutedEventArgs e)
{
LoadSettings loadSettings = new LoadSettings()
{
// Text only: images and paragraph formatting are left out
PDFImportSettings = PDFImportSettings.GenerateLines
};
// Opens the Open dialog with the filter for PDF documents
textControl1.Load(StreamType.AdobePDF, loadSettings);
}
Users read the text in the editor and search it with the Find dialog, which textControl1.Find() opens. Your code gets the complete text as a string from Text, and Search and Replace searches it in code. A protected PDF needs its password in LoadSettings; see Passwords and Access Permissions.
Read the Text Page by Page
A search hit or a summary often refers to a page. The following handlers let the user pick a PDF file and fill the list box with its lines, page by page and in reading order:
private string ChoosePdf()
{
OpenFileDialog openDialog = new OpenFileDialog() { Filter = "PDF documents (*.pdf)|*.pdf" };
return openDialog.ShowDialog(this) == true ? openDialog.FileName : null;
}
private void showLinesButton_Click(object sender, RoutedEventArgs 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 accepts a file path or a byte array. Sorting the lines of a page by Y and then by X gives the reading order, the approach of the blog article Mining PDFs with Regex in C#: Practical Patterns, Tips, and Ideas. The WPF OpenFileDialog returns a bool?, which is true when the user chose a file.
Search With Regular Expressions
Given a regular expression, Find returns each line with a match. The following handler lists every invoice number of the form INV-2025-001 together with its page:
private void findInvoiceNumbersButton_Click(object sender, RoutedEventArgs 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)applies a .NET regular expression with its options to every line.- The result contains whole lines, and
Regex.Matchtakes the matched value out of each one. Find(string text)returns the lines that contain a plain text.
A single-line expression misses a value that wraps to the next line. The blog article above has patterns for dates, amounts and clause numbers and looks at neighboring lines for context.
Read a Value From an Area of the Page
In documents of one layout, for example the invoices of one supplier, a value sits at the same place on every page. Find with a rectangle returns what is printed there. The following handler takes the area of the invoice number from a sample invoice and reads that area in each PDF of a folder:
private void readInvoiceNumbersButton_Click(object sender, RoutedEventArgs 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(this, "The sample invoice doesn't contain the invoice number.");
return;
}
RectangleF area = known.Rectangle;
OpenFolderDialog folderDialog = new OpenFolderDialog();
if (folderDialog.ShowDialog(this) != true)
{
return;
}
resultsListBox.Items.Clear();
// Read the same area of other documents of the same layout
foreach (string file in Directory.GetFiles(folderDialog.FolderName, "*.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($"{System.IO.Path.GetFileName(file)}: {value?.Text ?? "(no value)"}");
}
}
The code works as follows:
Find(RectangleF rectangle, bool partially)returns the lines in a rectangle: withfalseonly lines completely inside it, withtruealso lines that intersect it, so a longer value is found too.- Taking the rectangle from a line of the sample uses the coordinates of the PDF files themselves, not values measured by hand.
Find(PointF position, float radius, bool partially)returns the lines around a point.OpenFolderDialogis part of WPF since .NET 8.
Several fields of flattened forms are read this way in the blog article Template-Based Text Extraction from PDF Documents in .NET C#.
Get the Text and the Coordinates in One Load
Text that the user doesn't need to see can be extracted with a ServerTextControl, the TX Text Control without a user interface, which leaves the document in the window untouched. Adding LoadEmbeddedData to the import settings fills LoadSettings.EmbeddedData with the text coordinates as XML, one element per line with its page and position:
private void textAndCoordinatesButton_Click(object sender, RoutedEventArgs 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 is the text of the loaded document as one string. EmbeddedData is keyed by EmbeddedDataFormat, with the values TextCoordinates, FormFields and MetaData; it is filled only with LoadEmbeddedData and only with 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: get the values of PDF form fields.
- Document Metadata: read title, author and keywords of a PDF.
- Attachments: extract embedded files from a PDF.