TX Text Control .NET for WPF

Text Extraction

Topic

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.

WayReturnsUse it for
Load with PDFImportSettings GenerateLinesThe text of the PDF as a document, without images and paragraph formattingReading and searching the text in the editor, full-text indexes
The Lines class of the TXTextControl.DocumentServer.PDF.Contents namespaceEach text line with its page number and positionText 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.

Property of ContentLineContains
PageThe page number of the line
TextThe text of the line
X, Y, Width, HeightPosition and size of the line on the page, in points
RectangleThe bounds of the line as a System.Drawing.RectangleF

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.Match takes 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: with false only lines completely inside it, with true also 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. OpenFolderDialog is 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