New in Chilkat v11.7.0

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.

opReturns in jsonOutUse it for
textPlain text as flowing paragraphs, visual lines, or a layout-preserving grid (text, pages[])Indexing, display, field capture with label
markdownMarkdown with headings, emphasis, lists, links, code, ruled tables, image placeholders and page markers (markdown, pages[])AI and search pipelines, conversion
searchMatches for a string or regular expression, each with page and context (count, matches[])Finding and highlighting
tablesDetected tables as rows and cells with spans (count, tables[])Data capture
assessOnly the quality report, computed without producing textTriage 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.

MemberValues (default in bold)Description
pagesall, 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).
labelstring (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.
labelValueauto, right, below, sectionWhich text belongs to the label (see below).
labelOccurrencefirst, all, or a numberWhich occurrences of the label on the selected pages are used, counted in reading order.
labelRegexfalse, trueTreat label as a regular expression.
labelValueThe text returned
autoThe rest of the label's line if there is text there; otherwise the block directly below.
rightThe rest of the label's line.
belowThe block directly below the label, ending at the next gap or heading.
sectionEverything 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": { ... }
}
A label that is not found is still success: labelMatches is empty, text is empty, and the quality report carries the issue labelNotFound.

Reading order (all operations)

MemberValues (default in bold)Description
readingOrderauto, structure, visual, contentauto 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.
columnsauto, nonenone 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.

MemberDefaultWhat it covers
artifactsexcludeTagged artifacts, Acrobat and Chilkat watermarks and backgrounds, Acrobat headers and footers.
repeatedHeadersFootersexcludeUntagged text repeated at the same position on most pages (running heads, page numbers).
invisibleTextautoauto, 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.
clippedTextexcludeText hidden by a clipping path.
offPageTextexcludeText outside the crop box.
obscuredTextexcludeText drawn in the same color as a filled area directly beneath it (white on white).
rotatedTextincludeText not aligned with the page, such as vertical margin labels.
formFieldsexcludeForm field values, placed at the field's position in reading order.
annotationsexcludeFree-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)

MemberValues (default in bold)Description
expandLigaturestrue, false"fi" becomes "fi".
dehyphenatetrue, falseRejoins words split by a line-end hyphen; keeps hyphens that belong to the word.
actualTexttrue, falseUses the PDF's ActualText replacements where present (common for symbols and decorative letters).
biditrue, falseReorders right-to-left scripts into logical order.
normalizeNFC, NFKC, noneUnicode normalization of the output.
lineEnding\n, \r\nThe 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

MemberValues (default in bold)Description
layoutflow, lines, gridflow 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.
pageSeparatorstring (\f)Inserted between pages in the joined text.

Results

MemberDescription
textThe text of all selected pages, joined with pageSeparator.
pages[i].page, pages[i].textEach page's text (1-based page number).
labelMatches[i]When label is set: page, label (the label as found) and text (its value).
qualityThe 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.00

op = markdown

MemberValues (default in bold)Description
headingstrue, falseFrom the structure tree when tagged; otherwise inferred from font size and weight relative to the body text.
maxHeadingLevel1–6 (3)Deeper inferred levels are folded into the last one.
emphasistrue, falseBold and italic spans.
liststrue, falseBullets, numbering and hanging indents.
linkstrue, falseFrom link annotations over the text.
codetrue, falseMonospaced runs become code spans; monospaced blocks become code blocks.
tablesruled, all, noneruled converts tables with drawn cell borders. all is accepted for tables detected from column alignment alone, but this release detects only ruled tables.
imagesplaceholder, none![image](page-3-image-2) where an image sits in the flow.
pageBreakscomment, rule, none<!-- page 3 --> or --- between pages.
footnotesinline, end, noneSmall-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 = tables

MemberValues (default in bold)Description
detectionruled, allAs for the markdown tables option; this release detects only ruled tables (drawn cell borders).
minRows, minColumns2, 2Smaller 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" } ] }
  ]
}
MemberMeaning
ratingThe 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, structureUsableWhether the PDF has a structure tree, and whether it could be used for reading order and headings.
pagesNeedingOcrThe image-only pages, such as 4,9 (empty when none).
pages[i].textSourcetext (real text), ocrLayer (invisible text over an image), imageOnly (needs OCR), mixed, empty.
pages[i].unicodeCoverageThe share of glyphs with a reliable Unicode mapping, 0 to 1.
pages[i].unmappedFontsFonts whose glyphs could not be mapped, so their text is likely wrong (present only when there are any).
pages[i].suspiciousCharRatioThe share of replacement, private-use and control characters in the output.
pages[i].readingOrderSourcestructure, visual or content: how the order was decided.
pages[i].columnsColumns detected.
pages[i].structureWhether 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 WriteFile writes.
  • For an encrypted PDF, every operation except assess requires owner rights or the copy permission. Chilkat does not ignore owner restrictions; load the PDF with the OwnerPassword property set to extract from a PDF that restricts copying. The failure reason is in LastErrorText.
  • 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 artifacts filter, the same recognition that Watermark detect uses.

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.