PDF Text Extraction: Pdf.ExtractText
One method returns a PDF's text in four forms — plain text, Markdown, search matches and tables — and a quality report on every call that says how far the text can be trusted and which pages need OCR. Text is located by page and by label ("Invoice Number:").
bool ExtractText(string op, JsonObject jsonIn, JsonObject jsonOut)bool ExtractTextSb(string op, JsonObject jsonIn, StringBuilder sb, JsonObject jsonOut)
The operations
The op argument selects what comes back in jsonOut. Every operation also returns the quality object. Returns true on success; a page with no extractable text (a scanned image) is still success, and the quality report says why. Chilkat does not perform OCR.
| op | Returns in jsonOut | Use it for |
|---|---|---|
text | Plain text as flowing paragraphs, visual lines, or a layout-preserving grid (text, pages[]) | Indexing, display, field capture with label |
markdown | Markdown with headings, emphasis, lists, links, code, ruled tables, image placeholders and page markers (markdown, pages[]) | AI and search pipelines, conversion |
search | Matches for a string or regular expression, each with page and context (count, matches[]) | Finding and highlighting |
tables | Detected tables as rows and cells with spans (count, tables[]) | Data capture |
assess | Only the quality report, computed without producing text | Triage before extracting: is the text trustworthy, which pages need OCR |
Quick start
An empty jsonIn extracts every page in automatic reading order, without artifacts, with the standard clean-up.
Chilkat.Pdf pdf = new Chilkat.Pdf();
pdf.LoadFile("report.pdf");
Chilkat.JsonObject jsonIn = new Chilkat.JsonObject();
Chilkat.JsonObject jsonOut = new Chilkat.JsonObject();
if (!pdf.ExtractText("text", jsonIn, jsonOut)) {
Console.WriteLine(pdf.LastErrorText);
return;
}
string text = jsonOut.StringOf("text"); // all pages, joined
string rating = jsonOut.StringOf("quality.rating"); // good, fair, poor or none
jsonIn
{ }jsonOut
{
"text": "Quarterly Report\nRevenue grew 12% ...\f...",
"pages": [
{ "page": 1, "text": "Quarterly Report\nRevenue grew 12% ..." },
{ "page": 2, "text": "..." }
],
"quality": {
"rating": "good",
"tagged": true,
"structureUsable": true,
"pagesNeedingOcr": "",
"pages": [ ... ]
}
}Scope: pages and labels (all operations)
Scope is set by page and, optionally, by a label in the text.
| Member | Values (default in bold) | Description |
|---|---|---|
pages | all, first, last, odd, even, or a range list such as 1,3,5-9,12- | The pages to extract (1-based; 12- means to the end). |
label | string (none) | Restricts the operation to the text belonging to this label, such as Invoice Number: or Bill To. Matching ignores case, extra spaces and a trailing colon. |
labelValue | auto, right, below, section | Which text belongs to the label (see below). |
labelOccurrence | first, all, or a number | Which occurrences of the label on the selected pages are used, counted in reading order. |
labelRegex | false, true | Treat label as a regular expression. |
| labelValue | The text returned |
|---|---|
auto | The rest of the label's line if there is text there; otherwise the block directly below. |
right | The rest of the label's line. |
below | The block directly below the label, ending at the next gap or heading. |
section | Everything after the label up to the next heading of the same or higher level (the label is itself a heading). Most reliable on tagged PDFs. |
The label scope works with every operation: text returns the value, markdown returns it as Markdown, search looks only inside the labelled text, and tables returns only the tables inside it (the line-item table under "Items", for example).
Example: capture a field by its label
jsonIn
{ "label": "Invoice Number" }jsonOut
{
"text": "INV-2026-0412",
"pages": [ { "page": 1, "text": "INV-2026-0412" } ],
"labelMatches": [
{ "page": 1, "label": "Invoice Number:", "text": "INV-2026-0412" }
],
"quality": { "rating": "good", ... }
}Example: an address block under a label
jsonIn
{ "label": "Bill To", "labelValue": "below" }jsonOut
{
"text": "Acme Corp\n100 Main Street\nAustin, TX 78701",
"pages": [ { "page": 1, "text": "Acme Corp\n100 Main Street\nAustin, TX 78701" } ],
"labelMatches": [
{ "page": 1, "label": "Bill To", "text": "Acme Corp\n100 Main Street\nAustin, TX 78701" }
],
"quality": { ... }
}Example: every occurrence, on selected pages, by regular expression
jsonIn
{
"pages": "2-5",
"label": "(Phone|Tel)\\.?",
"labelRegex": true,
"labelValue": "right",
"labelOccurrence": "all"
}jsonOut
{
"text": "866 607 7339\n852 3160 9833\n44 (0) 1440 711 822",
"pages": [ { "page": 2, "text": "866 607 7339\n852 3160 9833\n44 (0) 1440 711 822" } ],
"labelMatches": [
{ "page": 2, "label": "Phone:", "text": "866 607 7339" },
{ "page": 2, "label": "Phone:", "text": "852 3160 9833" },
{ "page": 2, "label": "Phone:", "text": "44 (0) 1440 711 822" }
],
"quality": { ... }
}labelMatches is empty, text is empty, and the quality report carries the issue labelNotFound.Reading order (all operations)
| Member | Values (default in bold) | Description |
|---|---|---|
readingOrder | auto, structure, visual, content | auto follows the structure tree when the PDF is tagged and the tree is usable, otherwise reads in visual order. visual detects columns and reads top to bottom within each column. content is the order in which the text is drawn. |
columns | auto, none | none switches off column detection and reads the full page width line by line. |
jsonIn: a two-column newsletter, read across the page
{ "readingOrder": "visual", "columns": "none", "layout": "lines" }jsonOut
{
"text": "...",
"pages": [ { "page": 1, "text": "..." }, ... ],
"quality": { "rating": "good", ...,
"pages": [ { "page": 1, "readingOrderSource": "visual", "columns": 1, ... }, ... ] }
}jsonIn: trust the structure tree only
{ "readingOrder": "structure" }jsonOut
{
"text": "...",
"pages": [ ... ],
"quality": { "rating": "good", "tagged": true, "structureUsable": true, ...,
"pages": [ { "page": 1, "readingOrderSource": "structure", ... }, ... ] }
}Filters (all operations)
Each filter is include or exclude and decides whether a kind of text appears in the output.
| Member | Default | What it covers |
|---|---|---|
artifacts | exclude | Tagged artifacts, Acrobat and Chilkat watermarks and backgrounds, Acrobat headers and footers. |
repeatedHeadersFooters | exclude | Untagged text repeated at the same position on most pages (running heads, page numbers). |
invisibleText | auto | auto, include or exclude. auto includes invisible text only on pages that have no visible text, which is exactly an OCR layer over a scanned image. |
clippedText | exclude | Text hidden by a clipping path. |
offPageText | exclude | Text outside the crop box. |
obscuredText | exclude | Text drawn in the same color as a filled area directly beneath it (white on white). |
rotatedText | include | Text not aligned with the page, such as vertical margin labels. |
formFields | exclude | Form field values, placed at the field's position in reading order. |
annotations | exclude | Free-text annotations and comment text. |
jsonIn: everything, including hidden text (forensic review)
{
"artifacts": "include",
"repeatedHeadersFooters": "include",
"invisibleText": "include",
"clippedText": "include",
"offPageText": "include",
"obscuredText": "include",
"formFields": "include",
"annotations": "include"
}jsonOut
{
"text": "...",
"pages": [ ... ],
"quality": { "rating": "fair", ...,
"pages": [ { "page": 2, "textSource": "mixed",
"issues": [ { "code": "rotatedText", "message": "Rotated text was included" } ], ... } ] }
}jsonIn: a filled-in form, with its field values
{ "formFields": "include", "layout": "lines" }jsonOut
{
"text": "Name: Jane Doe\nAccount: 44-1029\nSignature: ...",
"pages": [ { "page": 1, "text": "Name: Jane Doe\nAccount: 44-1029\n..." } ],
"quality": { ... }
}Clean-up (all operations)
| Member | Values (default in bold) | Description |
|---|---|---|
expandLigatures | true, false | "fi" becomes "fi". |
dehyphenate | true, false | Rejoins words split by a line-end hyphen; keeps hyphens that belong to the word. |
actualText | true, false | Uses the PDF's ActualText replacements where present (common for symbols and decorative letters). |
bidi | true, false | Reorders right-to-left scripts into logical order. |
normalize | NFC, NFKC, none | Unicode normalization of the output. |
lineEnding | \n, \r\n | The line ending in the text (lf and crlf are accepted as well). |
jsonIn: raw text for a diff tool
{
"expandLigatures": false,
"dehyphenate": false,
"actualText": false,
"normalize": "none",
"lineEnding": "\r\n"
}jsonOut
{
"text": "The ligature \ufb01le and the hyphen-\r\nated word stay as drawn...",
"pages": [ ... ],
"quality": { ... }
}With lineEnding given as a JSON string, write it as "\r\n" in the JSON text (the JSON escape for a carriage return and a line feed), or simply "crlf".
op = text
| Member | Values (default in bold) | Description |
|---|---|---|
layout | flow, lines, grid | flow joins the lines of each paragraph; lines keeps each visual line on its own line; grid pads with spaces so that horizontal positions are preserved, like a fixed-pitch rendering of the page. |
pageSeparator | string (\f) | Inserted between pages in the joined text. |
Results
| Member | Description |
|---|---|
text | The text of all selected pages, joined with pageSeparator. |
pages[i].page, pages[i].text | Each page's text (1-based page number). |
labelMatches[i] | When label is set: page, label (the label as found) and text (its value). |
quality | The quality report. |
Example: the three layouts of the same invoice line
jsonIn
{ "pages": "1", "layout": "grid", "pageSeparator": "\n\n" }jsonOut.pages[0].text, per layout
flow: Widget, blue 2 $10.00 $20.00 Subtotal $20.00
lines: Widget, blue 2 $10.00 $20.00
Subtotal $20.00
grid: Widget, blue 2 $10.00 $20.00
Subtotal $20.00op = markdown
| Member | Values (default in bold) | Description |
|---|---|---|
headings | true, false | From the structure tree when tagged; otherwise inferred from font size and weight relative to the body text. |
maxHeadingLevel | 1–6 (3) | Deeper inferred levels are folded into the last one. |
emphasis | true, false | Bold and italic spans. |
lists | true, false | Bullets, numbering and hanging indents. |
links | true, false | From link annotations over the text. |
code | true, false | Monospaced runs become code spans; monospaced blocks become code blocks. |
tables | ruled, all, none | ruled converts tables with drawn cell borders. all is accepted for tables detected from column alignment alone, but this release detects only ruled tables. |
images | placeholder, none |  where an image sits in the flow. |
pageBreaks | comment, rule, none | <!-- page 3 --> or --- between pages. |
footnotes | inline, end, none | Small-print notes at the page foot, kept in place or gathered at the end. |
Results
markdown (all pages joined, with the page breaks chosen), pages[i].page / pages[i].markdown, labelMatches[i].markdown when label is set, and quality.
Example: Markdown for an AI pipeline, with a quality gate
pdf.ExtractText("markdown", jsonIn, jsonOut);
string md = jsonOut.StringOf("markdown");
if (jsonOut.StringOf("quality.rating") == "poor") {
// send for review, or OCR the pages listed in quality.pagesNeedingOcr
}
jsonIn
{
"pageBreaks": "rule",
"images": "none",
"footnotes": "end",
"maxHeadingLevel": 2
}jsonOut.markdown
# Quarterly Report
## Revenue
Revenue grew **12%** over the prior quarter, driven by
the *Widget* line.[^1]
| Product | Units | Revenue |
| --- | --- | --- |
| Widget, blue | 2 | $20.00 |
| Widget, red | 5 | $50.00 |
See [the appendix](https://example.com/appendix) for details.
---
## Outlook
- Expand the blue line
- Open the Austin office
[^1]: Excluding returns.Example: one section as Markdown
jsonIn
{ "label": "Terms and Conditions", "labelValue": "section" }jsonOut
{
"markdown": "## Terms and Conditions\n\nPayment is due within 30 days ...",
"pages": [ { "page": 4, "markdown": "## Terms and Conditions\n\n..." } ],
"labelMatches": [ { "page": 4, "label": "Terms and Conditions", "markdown": "## Terms and Conditions\n\n..." } ],
"quality": { ... }
}op = search
| Member | Values (default in bold) | Description |
|---|---|---|
query | string | Required: the string or regular expression to find. |
regex | false, true | Treat query as a regular expression. |
caseSensitive | false, true | |
wholeWord | false, true | Match whole words only. |
maxResults | number (all) | The maximum number of matches returned. |
context | number of characters (40) | Text returned before and after each match. |
Results
count, and matches[i] with page, text (the matched text), before and after (the context), plus quality.
Example: find an invoice total with a regular expression
jsonIn.UpdateString("query", @"Total\s+\$?[0-9,]+\.[0-9]{2}");
jsonIn.UpdateBool("regex", true);
pdf.ExtractText("search", jsonIn, jsonOut);
string total = jsonOut.StringOf("matches[0].text");
int page = jsonOut.IntOf("matches[0].page");
jsonIn
{
"query": "Total\\s+\\$?[0-9,]+\\.[0-9]{2}",
"regex": true,
"context": 20
}jsonOut
{
"count": 2,
"matches": [
{ "page": 1, "text": "Total $1,234.56",
"before": " $1,234.56\nSub", "after": "\nThank you for your" },
{ "page": 3, "text": "Total $98.00",
"before": "Shipping $8.00\n", "after": "\nPaid by card" }
],
"quality": { ... }
}Example: a plain word, whole words only, first 10 hits
jsonIn
{ "query": "warranty", "wholeWord": true, "maxResults": 10 }jsonOut
{
"count": 7,
"matches": [
{ "page": 2, "text": "warranty", "before": "This limited ", "after": " covers defects in" },
...
],
"quality": { ... }
}jsonIn: search only inside a labelled section
{ "label": "Exclusions", "labelValue": "section", "query": "water damage" }jsonOut
{
"count": 1,
"matches": [
{ "page": 5, "text": "water damage",
"before": "not covered: ", "after": ", misuse, or" }
],
"quality": { ... }
}op = tables
| Member | Values (default in bold) | Description |
|---|---|---|
detection | ruled, all | As for the markdown tables option; this release detects only ruled tables (drawn cell borders). |
minRows, minColumns | 2, 2 | Smaller grids are not reported as tables. |
Results
count, and tables[i] with page, detectedBy (ruled, aligned or structure), rows, columns, and cells[j] with row, col (counted from 0), rowSpan, colSpan and text.
jsonIn
{ "pages": "1", "minRows": 2, "minColumns": 3 }jsonOut
{
"count": 1,
"tables": [
{ "page": 1, "detectedBy": "ruled", "rows": 3, "columns": 3,
"cells": [
{ "row": 0, "col": 0, "rowSpan": 1, "colSpan": 1, "text": "Product" },
{ "row": 0, "col": 1, "rowSpan": 1, "colSpan": 1, "text": "Units" },
{ "row": 0, "col": 2, "rowSpan": 1, "colSpan": 1, "text": "Revenue" },
{ "row": 1, "col": 0, "rowSpan": 1, "colSpan": 1, "text": "Widget, blue" },
{ "row": 1, "col": 1, "rowSpan": 1, "colSpan": 1, "text": "2" },
{ "row": 1, "col": 2, "rowSpan": 1, "colSpan": 1, "text": "$20.00" },
{ "row": 2, "col": 0, "rowSpan": 1, "colSpan": 2, "text": "Total" },
{ "row": 2, "col": 2, "rowSpan": 1, "colSpan": 1, "text": "$20.00" }
] }
],
"quality": { ... }
}int n = jsonOut.SizeOfArray("tables[0].cells");
for (int i = 0; i < n; i++) {
jsonOut.I = i;
Console.WriteLine(jsonOut.IntOf("tables[0].cells[i].row") + "," + jsonOut.IntOf("tables[0].cells[i].col")
+ ": " + jsonOut.StringOf("tables[0].cells[i].text"));
}
op = assess
assess returns only the quality report. It takes the same scope, reading-order and filter options as the other operations (they decide what is assessed), never returns text, and is the one operation allowed on an encrypted PDF that forbids copying.
pdf.ExtractText("assess", jsonIn, jsonOut);
string ocrPages = jsonOut.StringOf("quality.pagesNeedingOcr"); // e.g. "4,9"
bool tagged = jsonOut.BoolOf("quality.tagged");
The quality report
Every operation returns a quality object that rates each page and the whole document and names the reasons, so an application can decide whether to trust the text, send pages to OCR, or flag the output for review.
"quality": {
"rating": "fair",
"tagged": true,
"structureUsable": false,
"pagesNeedingOcr": "4,9",
"pages": [
{ "page": 3, "rating": "fair",
"textSource": "text",
"unicodeCoverage": 0.97,
"unmappedFonts": [ "F12+Wingdings" ],
"suspiciousCharRatio": 0.004,
"readingOrderSource": "visual",
"columns": 2,
"structure": { "headings": "inferred", "tables": { "ruled": 1, "aligned": 0 } },
"issues": [
{ "code": "multiColumn", "message": "Multi-column layout; reading order inferred" },
{ "code": "missingUnicode", "message": "Some glyphs have no reliable Unicode mapping" } ] },
{ "page": 4, "rating": "none", "textSource": "imageOnly",
"unicodeCoverage": 0, "suspiciousCharRatio": 0,
"readingOrderSource": "visual", "columns": 0,
"structure": { "headings": "none", "tables": { "ruled": 0, "aligned": 0 } },
"issues": [ { "code": "imageOnly", "message": "No text; page is a scanned image" } ] }
]
}
| Member | Meaning |
|---|---|
rating | The document rating: the worst page rating among pages that have text. good: coverage at least 0.99 and no suspicious characters to speak of. fair: coverage 0.95–0.99, or structure inferred on a complex page (columns, unruled tables, rotated text). poor: coverage below 0.95 or suspicious characters above 1%; the text should not be trusted without review. none: nothing to extract. |
tagged, structureUsable | Whether the PDF has a structure tree, and whether it could be used for reading order and headings. |
pagesNeedingOcr | The image-only pages, such as 4,9 (empty when none). |
pages[i].textSource | text (real text), ocrLayer (invisible text over an image), imageOnly (needs OCR), mixed, empty. |
pages[i].unicodeCoverage | The share of glyphs with a reliable Unicode mapping, 0 to 1. |
pages[i].unmappedFonts | Fonts whose glyphs could not be mapped, so their text is likely wrong (present only when there are any). |
pages[i].suspiciousCharRatio | The share of replacement, private-use and control characters in the output. |
pages[i].readingOrderSource | structure, visual or content: how the order was decided. |
pages[i].columns | Columns detected. |
pages[i].structure | Whether headings came from the structure tree (structure), were inferred (inferred) or are absent (none), and how many ruled and aligned tables were found. |
pages[i].issues[j] | Machine-readable code with a message: imageOnly, ocrLayer, missingUnicode, suspiciousCharacters, multiColumn, unruledTable, structureTreeUnusable, rotatedText, obscuredTextSkipped, clippedTextSkipped, labelNotFound. |
ExtractTextSb: large documents
ExtractTextSb(op, jsonIn, sb, jsonOut) is the same call, except that the joined text or Markdown is written into the StringBuilder instead of the text / markdown member of jsonOut. Everything else (pages, labelMatches, search and table results, quality) is still returned in jsonOut.
Chilkat.StringBuilder sb = new Chilkat.StringBuilder();
if (pdf.ExtractTextSb("markdown", jsonIn, sb, jsonOut)) {
sb.WriteFile("report.md", "utf-8", false);
}
Permissions and signed PDFs
- Extraction is read-only: it never affects signatures, pending changes, or what
WriteFilewrites. - For an encrypted PDF, every operation except
assessrequires owner rights or the copy permission. Chilkat does not ignore owner restrictions; load the PDF with theOwnerPasswordproperty set to extract from a PDF that restricts copying. The failure reason is inLastErrorText. - An encrypted PDF that could not be opened (wrong password) refuses every operation except
assess. - Watermarks added by Chilkat or Acrobat are excluded by the
artifactsfilter, the same recognition that Watermarkdetectuses.
Option reference
Every member of jsonIn, grouped as above. All of them are optional except query for search.
{
"pages": "all", "label": "", "labelValue": "auto",
"labelOccurrence": "first", "labelRegex": false,
"readingOrder": "auto", "columns": "auto",
"artifacts": "exclude", "repeatedHeadersFooters": "exclude",
"invisibleText": "auto", "clippedText": "exclude",
"offPageText": "exclude", "obscuredText": "exclude",
"rotatedText": "include", "formFields": "exclude",
"annotations": "exclude",
"expandLigatures": true, "dehyphenate": true,
"actualText": true, "bidi": true,
"normalize": "NFC", "lineEnding": "\n",
"layout": "flow", "pageSeparator": "\f", // text
"headings": true, "maxHeadingLevel": 3, // markdown
"emphasis": true, "lists": true,
"links": true, "code": true,
"tables": "ruled", "images": "placeholder",
"pageBreaks": "comment", "footnotes": "inline",
"query": "", "regex": false, // search
"caseSensitive": false, "wholeWord": false,
"maxResults": 0, "context": 40,
"detection": "ruled", "minRows": 2, // tables
"minColumns": 2
}
Reference documentation: Pdf.ExtractText, Pdf.ExtractTextSb. Related: TextReplace, Watermark, Protection, Optimize, Pdf class overview.
New in Chilkat v11.7.0.