How to Extract Tables from PDFs and Other Documents in C#
Learn how table extraction differs from plain OCR and how to turn tables from PDFs, Office files, emails, and images into structured C# objects.
Join the DZone community and get the full member experience.
Join For FreeBusiness documents rarely keep their most useful information in convenient database records or JSON objects. Invoices hold line items in tables, financial reports organize figures by period, inspection forms group findings by category, and emailed statements often arrive as attachments or message files.
Before our applications can validate, compare, search, or store that information, they have to somehow recover the relationships between rows, columns, headers, and values.
At first glance, this may look like a routine text-extraction problem. If we can read the words on the page, surely we can rebuild the table... right? In practice, unfortunately, recognizing the text is only the first layer. We also need to determine which values belong to the same row, where columns begin and end, which headers describe which cells, and whether the document contains multiple distinct tables. Again, tables represent relationships, and if those relationships are mishandled, the data becomes useless.
In this article, we’ll look at why table extraction becomes difficult across different document formats. We’ll then implement an API-based extraction workflow in C# and process the structured table data it returns.
Why Table Extraction Is More Than OCR
Optical character recognition (OCR) has been around forever, and by today's technological standards, it answers a relatively narrow question: which characters appear in an image, and where are they located? Table extraction has to answer a more contextual question: how are those characters related?
Consider a scanned invoice with the headers Description, Quantity, Unit Price, and Total. An OCR engine might correctly identify all four headers and every value beneath them, but a stream of recognized text isn’t enough for downstream automation. We still need to establish that 3, $12.00, and $36.00 belong to the same line item, and that $36.00 represents the total rather than the unit price.
Visible borders can certainly help, but we can’t count on them; many modern tables use whitespace, shading, or alignment instead of grid lines. Cells may span multiple apparent columns, descriptions may run into character constraints and wrap onto additional lines, and a table may continue onto the next page with repeated or missing headers.
Poor document image quality has also been the bane of OCR solutions for decades; it introduces another unwelcome layer of ambiguity. A recognition error can change both an individual data value and the entire interpretation of its surrounding structure.
All of this is to say that a document can contain perfectly readable text while still producing a poor table. Reliable table extraction requires both character recognition and layout analysis.
Different Formats Hide Tables in Different Ways
The word "document" itself covers several very different internal structures.
A DOCX file, for example, is a package of XML parts that can contain explicit table elements. An XLSX workbook stores cells, values, formulas, and worksheet relationships in another Office Open XML structure. PPTX files can combine true table objects with independently positioned text boxes that only look like tables to a human reader.
PDF files are significantly more complicated. A PDF may describe text by drawing individual characters at specific page coordinates on the page without preserving any semantic concept of a row, column, or table for extraction services to key in on. Two values that appear next to each other on the page may or may not be related within the underlying PDF structure; context is required to know for sure.
Images push us fully into visual interpretation. For example, JPG, PNG, and WEBP inputs don’t contain native text or table objects, so OCR and layout analysis are required for automated data extraction.
Email containers such as EML and MSG introduce another variation. We might not think about these file types quite as frequently as the others mentioned in this section, but they introduce an interesting challenge worth considering in the pursuit of solving this problem for a wide range of inputs. These formats allow for a useful table to appear in the message body, a rendered representation of that body, or an attached document.
As a result, a broadly focused table-extraction system can’t simply run the same parser against every extension. It first needs to identify and decode the format, obtain a useful page or layout representation, recognize text when necessary, and map the discovered structures into a consistent table model.
Once every input produces the same hierarchy (tables containing rows, rows containing cells, and cells containing headers and values), the rest of our application doesn't need format-specific extraction logic anymore.
Building the Workflow With Open-Source .NET Libraries
The .NET ecosystem gives us several useful open-source building blocks for structured data extraction, but the right library depends heavily on the source document.
For Office documents, the Open XML SDK is your first stop: it can inspect the native structures inside DOCX, XLSX, and PPTX files directly. Format-specific libraries such as ClosedXML can provide a more approachable layer for working with Excel worksheets. These options are a strong fit when our tables exist as real Office table or cell structures, which is fairly often.
PDF documents often require a different path. A library such as PdfPig (more than 28 million downloads on GitHub) can extract text and positional information from text-based PDFs, but our application may still need custom logic to group those positioned words into rows and columns. If the PDF contains scanned pages, we first need to render those pages into images and send them through an OCR engine such as Tesseract (another widely used & loved package).
If reading from email files is a must, a bit more routing work is required. MimeKit can parse MIME-based messages such as EML files, while MSG files may require a separate Outlook message parser. After parsing the message, we still need to inspect the body and each relevant attachment independently.
Each of these libraries can, of course, make sense within its own lane. If our application receives one predictable document type with a stable layout, an open-source implementation may give us all the control we need.
The complexity rears its head when we want one production workflow to accept PDFs, Office files, email containers, and images all at once. In that case, we have to detect formats, route documents to the correct parser, render pages when native extraction fails, decide when OCR is necessary, and reconcile several different output structures. That's a lot.
We also own the quality heuristics for that workflow. That includes the borderless-table detection challenge in addition to merged cells, rotated pages, repeated headers, image preprocessing, wrapped text, and validation thresholds.
In other words, multi-format table extraction is better understood as a document-processing system than a single library call.
Using a Normalized Table-Extraction API
If maintaining separate extraction paths for each format feels like too much, we can send the input document to a dedicated table-extraction service that performs the format handling, recognition, and table analysis through a consistent API.
We'll walk through one example that uses AI to consistently identify table structures in DOCX, PDF, XLSX, PPTX, EML, MSG, JPG, PNG, and WEBP input. The response JSON organizes the extracted content into tables, rows, and cells.
We’ll access the endpoint through its generated .NET Core SDK. To begin, we’ll install version 1.0.0:
dotnet add package Cloudmersive.APIClient.NETCore.DocumentAI --version 1.0.0
Once the package is installed, we can import the API, client, and model namespaces required for the request:
using System;
using System.Diagnostics;
using Cloudmersive.APIClient.NETCore.DocumentAI.Api;
using Cloudmersive.APIClient.NETCore.DocumentAI.Client;
using Cloudmersive.APIClient.NETCore.DocumentAI.Model;
The snippets provided below mirror the supplied SDK code directly; as code examples, they assume we’ll adapt placeholders and surrounding application details as needed.
Configuring the API Client
First, we’ll add our API key under the Apikey authorization name in the default configuration:
Configuration.Default.AddApiKey("Apikey", "YOUR_API_KEY");
Creating the Extraction Client
Next, we’ll create a new ExtractApi instance:
var apiInstance = new ExtractApi();
Loading the Input Document
We’ll assign a value to the optional recognition-mode parameter:
var recognitionMode = "Advanced";
Advanced is the default recognition mode and provides the highest accuracy with slower processing, while Normal provides faster processing with lower accuracy for low-quality images.
Next, we’ll open our input document as a FileStream:
var inputFile = new System.IO.FileStream("C:\\temp\\inputfile", System.IO.FileMode.Open);
Executing the Table-Extraction Request
With recognitionMode and inputFile ready, we’ll pass them into ExtractTables and write the returned ExtractTablesResponse object to the debug output:
try
{
// Extract Tables of Data from a Document using AI
ExtractTablesResponse result = apiInstance.ExtractTables(recognitionMode, inputFile);
Debug.WriteLine(result);
}
catch (Exception e)
{
Debug.Print("Exception when calling ExtractApi.ExtractTables: " + e.Message );
}
Understanding the Response Structure
A successful response should follow this general structure:
{
"Successful": true,
"TableResults": [
{
"Title": "Invoice Line Items",
"Rows": [
{
"Cells": [
{
"CellHeader": "Description",
"CellValue": "Replacement filter"
},
{
"CellHeader": "Quantity",
"CellValue": "3"
},
{
"CellHeader": "Unit Price",
"CellValue": "$12.00"
},
{
"CellHeader": "Total",
"CellValue": "$36.00"
}
]
}
]
}
]
}
Successful tells us whether the extraction operation completed. TableResults is a collection because one document may contain multiple distinct tables; the endpoint naturally distinguishes between each and returns their results separately.
Ultimately, this response is a pretty straightforward JSON mapping. Every table can include a Title followed by Rows. Every row contains a collection of Cells, and each cell provides an inferred CellHeader and extracted CellValue.
Note that a structurally valid response does not guarantee every property will contain a value. A document may contain a table with no visible title, for example, so our workflow shouldn’t rely on Title as a required identifier.
We should also definitely expect real documents to contain blank cells, inconsistent headers, and values that require additional parsing before validation or storage.
Reading the Returned Tables in C#
if (result.Successful == true &&
result.TableResults != null)
{
foreach (var table in result.TableResults)
{
foreach (var row in table.Rows)
{
foreach (var cell in row.Cells)
{
Console.WriteLine(
$"{cell.CellHeader}: {cell.CellValue}"
);
}
}
}
}
The simple nested loops I've included here get each header and value while preserving the table structure, leaving us free to map rows into dictionaries, database entities, CSV records, or custom models such as InvoiceLineItem.
Note that we should probably avoid aggressive type casting; identifiers may need leading zeroes preserved, while currency and date values may require locale-aware parsing. Extraction may structure the data, but schema validation remains our application’s responsibility.
The Full Implementation
Here's a fully assembled example implementation including everything we just outlined above:
using System;
using System.Diagnostics;
using Cloudmersive.APIClient.NETCore.DocumentAI.Api;
using Cloudmersive.APIClient.NETCore.DocumentAI.Client;
using Cloudmersive.APIClient.NETCore.DocumentAI.Model;
namespace Example
{
public class ExtractTablesExample
{
public static void Main()
{
Configuration.Default.AddApiKey(
"Apikey",
"YOUR_API_KEY"
);
var apiInstance = new ExtractApi();
var recognitionMode = "Advanced";
using (
var inputFile = new System.IO.FileStream(
"C:\\temp\\inputfile",
System.IO.FileMode.Open
)
)
{
try
{
ExtractTablesResponse result =
apiInstance.ExtractTables(
recognitionMode,
inputFile
);
Debug.WriteLine(result);
if (result != null &&
result.Successful == true &&
result.TableResults != null)
{
foreach (var table in result.TableResults)
{
if (table == null ||
table.Rows == null)
{
continue;
}
foreach (var row in table.Rows)
{
if (row == null ||
row.Cells == null)
{
continue;
}
foreach (var cell in row.Cells)
{
if (cell == null)
{
continue;
}
Console.WriteLine(
$"{cell.CellHeader}: " +
$"{cell.CellValue}"
);
}
}
}
}
}
catch (Exception e)
{
Debug.Print(
"Exception when calling " +
"ExtractApi.ExtractTables: " +
e.Message
);
}
}
}
}
}
Adding Production Guardrails
Whether we use the API-based approach demonstrated above or assemble an open-source extraction system, a production pipeline still needs some guardrails around it. The exact implementation will differ, but the underlying goals are mostly the same: we want to control things like resource use & preserve traceability, and very importantly, we want to prevent questionable extraction results from quietly entering downstream systems.
First, we should validate each file and enforce practical document and page limits before processing begins. With the API approach, page counts directly affect consumption. In an open-source system, those same long documents can consume substantial memory, CPU time, OCR capacity, and worker availability. Both implementations benefit from clear limits and a plan for handling unusually large documents.
We should also try to retain enough context to audit each result. For the API workflow, that record might include the source document identifier, recognition mode, response status, table index, etc. An open-source workflow might additionally record which parser, OCR engine, preprocessing steps, model version, and fallback path were used. These details make extraction problems much easier to reproduce and diagnose later.
Most importantly, we need to define what successful means at the application level. An API response with Successful set to true indicates that the extraction operation completed. Likewise, an open-source parser returning rows without throwing an exception only tells us that its processing path completed. Neither outcome proves that the expected table was found or that every extracted value is correct.
If the extracted data affects payments, compliance decisions, inventory, or customer records, human review remains sensible for incomplete or internally inconsistent results. Automation should reduce the amount of manual work required, not remove the opportunity to catch a result that doesn’t make sense.
Conclusion
In this article, we separated table extraction from plain text recognition and saw why supporting extraction from PDFs, Office documents, email containers, and images can require several different processing paths.
Open-source .NET libraries give us plenty of capable building blocks when our formats and layouts are controlled. A broad intake workflow, however, also needs document routing, OCR, layout analysis, output normalization, and ongoing quality logic, all of which can be burdensome to implement in a production environment.
We then installed a Document AI .NET SDK and took a look at structuring a request to handle table extraction automatically.
With sufficient validation incorporated around those results, the same pattern we just demonstrated can support invoice processing, reporting, database imports, reconciliation workflows, and other systems that need structured document data rather than another block of extracted text.
Opinions expressed by DZone contributors are their own.
Comments