TX Text Control .NET for Windows Forms

Text Extraction

Topic

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.

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

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 ContentLineContains
PageThe number of the page that contains the line
TextThe text of the line
X, Y, Width, HeightThe position and size of the line on its page, in points
RectangleThe 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.
  • Find returns whole lines, so Regex.Match cuts 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. With false, a line must lie completely inside it; with true, 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. RectangleF is part of System.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