PDF Parsing

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-12. This page was written against Apache Tika 4.1.0 (the latest release on Maven Central on that date) with the 3.3.x differences noted, Java 21 and later for the examples (Tika 4.x needs Java 17), Spring Boot 4.x on Spring Framework 7 and the Docker image apache/tika:4.1.0. PDF is the format a document intake service sees most, and the one where "extract the text" hides the most decisions: whether the page has a text layer at all, whether to trust it, whether to follow the document’s structure tree, what to do with forms, comments, attachments and images, and what to do with a file that is encrypted or carries an XFA form. This page explains how PDFParser makes those decisions, every option that changes them, the 4.1 pages block that decides where each page’s text comes from, the OCR strategies for scanned pages, passwords and permissions, and the XFA vulnerabilities that every Tika deployment older than 3.2.2 still carries.

Related pages: Parser API & AutoDetectParser (the ParseContext that carries the options on this page), Content Handlers & Output Formats (the XHTML shown below), Metadata (the pdf: keys), Embedded Documents & Recursive Parsing (PDF attachments and inline images), Configuration (tika-config.json and TikaLoader), OCR with Tesseract (the OCR engine itself) and VLM Parsers & Inference (a VLM as the text recognizer).

All Java programs on this page that print output were compiled and run against Tika 4.1.0 (tika-core, tika-parsers-standard-package as a pom dependency and tika-serialization for TikaLoader) on Java 25, which resolves Apache PDFBox 3.0.8. The sample files were written for this page with PDFBox: a two-page, tagged catalogue.pdf (3,601 bytes) with bookmarks, a comment annotation, an AcroForm field and an attached errata.pdf; a two-page scanned-loan-form.pdf whose first page is a 150 dpi scan with no text layer and whose second page is a born-digital cover note; members-catalogue.pdf, the catalogue encrypted with AES-256 and a user password; and loan-form-xfa.pdf, a one-page PDF with an XFA form. No OCR engine was installed on the test machine, so the OCR output itself is not shown; everything up to the call to the engine (the per-page verdict, the error when no engine is configured, page rendering) was run. The section What was verified and what was not lists the rest.

PDFParser at a glance

org.apache.tika.parser.pdf.PDFParser is the parser AutoDetectParser picks for application/pdf. It lives in the tika-parser-pdf-module artifact, which the standard parser package pulls in, and it is a wrapper around Apache PDFBox: PDFBox loads the file, decrypts it, walks the pages and strips the text, and Tika turns what PDFBox finds into XHTML SAX events and Metadata.

Item Value in Tika 4.1.0

Class

org.apache.tika.parser.pdf.PDFParser, which implements Parser, RenderingParser and EnrichingParser

Artifact

org.apache.tika:tika-parser-pdf-module (in tika-parsers-standard-package)

PDF library

Apache PDFBox 3.0.8 (pdfbox, pdfbox-io, fontbox, plus jbig2-imageio for JBIG2 images)

Component name in tika-config.json

pdf-parser

Options class

org.apache.tika.parser.pdf.PDFParserConfig, with MarkedContentConfig for tagged PDFs

Pages policy class

org.apache.tika.parser.pages.PagesConfig (in tika-core), shared with the EMF and WMF parsers

Media type

application/pdf (PDFParser.MEDIA_TYPE)

From one PDF the parser can produce:

  • the text of each page, inside a <div class="page">, either from the page’s content stream (the text layer), from an OCR engine run on a rendered image of the page, or both;

  • the structure of a tagged PDF (headings, paragraphs, lists, tables, links), when the structure tree is good enough;

  • annotation text (comments, sticky notes), bookmark (outline) text, AcroForm field values and XFA form data;

  • embedded documents: file attachments, inline images (optional), page renders (optional), JavaScript actions (optional) and earlier incremental-update versions (optional);

  • metadata: the document information dictionary and XMP, plus Tika’s own pdf: facts (page count, characters per page, encryption, permissions, fonts, forms, incremental updates).

The parser does not OCR anything itself. When a page needs OCR it renders the page and hands the image to the text recognizer configured for the image type (Tesseract, Tess4J or a VLM). The 4.1 pages block, described below, decides when that happens.

Add the module on its own when you want PDF and nothing else, or use the standard package:

<dependency>
  <groupId>org.apache.tika</groupId>
  <artifactId>tika-parser-pdf-module</artifactId>
  <version>4.1.0</version>
</dependency>
dependencies {
    implementation("org.apache.tika:tika-parser-pdf-module:4.1.0")
}

Do not pin your own PDFBox version below the one Tika 4.1.0 was built with (3.0.8). PDFBox 2.x and 3.x have different APIs (PDDocument.load became Loader.loadPDF), and mixing them breaks PDFParser at run time. If another part of the application uses PDFBox, let Maven or Gradle converge on the newer 3.0.x release.

What a PDF gives Tika to read

A PDF page is a program: a content stream of drawing operators that place glyphs, lines and images. Text is extractable only when the page draws glyphs from a font that maps them back to Unicode. That leads to the three kinds of pages every PDF intake service meets:

Page kind What the content stream holds What Tika needs

Born-digital

Glyphs with a Unicode mapping (ToUnicode or a standard encoding)

Nothing more: the text layer is the text.

Scanned

One full-page image, no glyphs

OCR of a rendered page image.

Broken text layer

Glyphs without a Unicode mapping, or an invisible OCR layer of poor quality

OCR, or both sources.

Tika measures each page while it reads it. pdf:chars-per-page holds one value per page, and pdf:unmapped-unicode-chars-per-page counts the glyphs PDFBox could not map to Unicode. These two numbers are the input to the AUTO OCR verdict described below. For the Bookshelf files they are:

File pdf:chars-per-page pdf:unmapped-unicode-chars-per-page What it means

catalogue.pdf

194, 161

0, 0

Two born-digital pages.

scanned-loan-form.pdf

0, 100

0, 0

Page 1 is a scan; page 2 has a text layer.

For how PDF objects, content streams, fonts and incremental updates work at the byte level, read How a PDF File Works in the PDF Generation section; this page only uses what the parser makes of them.

A first parse of catalogue.pdf

The next program parses a PDF with the default configuration, or with a tika-config.json given as the second argument, and prints the XHTML <body> and the pdf:, dc: and xmpTPg: metadata. It is reused through the page with different configurations.

import java.nio.file.Path;
import java.util.Arrays;
import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.apache.tika.sax.ToXMLContentHandler;

/** Usage: PdfToXhtml <file.pdf> [tika-config.json] */
public class PdfToXhtml {

    public static void main(String[] args) throws Exception {
        TikaLoader loader = args.length > 1
                ? TikaLoader.load(Path.of(args[1]))
                : TikaLoader.loadDefault();
        Parser parser = loader.loadAutoDetectParser();
        ParseContext context = loader.loadParseContext();

        Metadata metadata = new Metadata();
        ToXMLContentHandler handler = new ToXMLContentHandler();
        try (TikaInputStream tis = TikaInputStream.get(Path.of(args[0]), metadata)) {
            parser.parse(tis, handler, metadata, context);
        }
        String xhtml = handler.toString();
        // Print the <body> only; the <head> repeats the metadata printed below.
        System.out.println(xhtml.substring(xhtml.indexOf("<body>"), xhtml.indexOf("</body>") + 7));

        String[] names = metadata.names();
        Arrays.sort(names);
        for (String name : names) {
            if (name.startsWith("pdf:") || name.startsWith("dc:") || name.startsWith("xmpTPg:")) {
                System.out.println(name + " = " + String.join(", ", metadata.getValues(name)));
            }
        }
    }
}
java PdfToXhtml catalogue.pdf

Output:

<body><div class="page"><h1>Autumn Catalogue 2026</h1>
<h2>New fiction</h2>
<p>The Silent Shelf by A. Moreno: a librarian finds a book that rewrites itself.</p>
<p>Paper Lanterns by K. Ito: twelve stories set in one Kyoto bookshop.</p>
<div class="artifact"><p>Bookshelf - page 1</p>
</div>
<div class="annotation"><div class="annotationContents">Check the price of Paper Lanterns before printing.</div>
</div>
</div>
<div class="page"><h1>Order form</h1>
<h2>Non-fiction</h2>
<p>Ink and Iron: a history of the printing press, 480 pages.</p>
<p>Fill in the member number below and return this page to the shop.</p>
<div class="artifact"><p>Bookshelf - page 2</p>
</div>
</div>
<ul>	<li>New fiction</li>
	<li>Non-fiction and order form</li>
</ul>
<div source="attachment" class="embedded" id="errata.pdf" />
<div class="page"><p />

<p>Errata: The Silent Shelf, 2nd printing, page 112.</p>

<p />

</div>

<div class="acroform"><ol>	<li altName="Member number">memberNumber: BK-20417</li>
</ol>
</div>
</body>
dc:creator = Bookshelf Editorial
dc:format = application/pdf; version=1.6
dc:language = en-GB
dc:title = Bookshelf Autumn Catalogue
pdf:annotation-subtypes = Text, Widget
pdf:annotation-types = null
pdf:chars-per-page = 194, 161
pdf:contains-damaged-font = false
pdf:contains-non-embedded-font = true
pdf:docinfo:created = 2026-09-01T07:00:00Z
pdf:docinfo:creator = Bookshelf Editorial
pdf:docinfo:creator-tool = Bookshelf intake fixtures
pdf:docinfo:title = Bookshelf Autumn Catalogue
pdf:encrypted = false
pdf:eof-offsets = 2081, 3601
pdf:has-acro-form-fields = true
pdf:has-collection = false
pdf:has-marked-content = true
pdf:has-xfa = false
pdf:has-xmp = false
pdf:incremental-update-count = 1
pdf:marked-content-pages-fallback = 0
pdf:marked-content-pages-tagged = 2
pdf:num-3d-annotations = 0
pdf:ocr-page-count = 0
pdf:overall-percentage-unmapped-unicode-chars = 0.0
pdf:pdf-version = 1.6
pdf:total-unmapped-unicode-chars = 0
pdf:unmapped-unicode-chars-per-page = 0, 0
xmpTPg:NPages = 2

Read the output from the top:

  • Each page is a <div class="page">. Because catalogue.pdf is tagged and its structure tree passed the quality gate (pdf:marked-content-pages-tagged = 2), the headings came out as <h1> and <h2>, not as plain paragraphs.

  • The running footer was marked as an artifact in the PDF, so Tika put it in <div class="artifact">. A search index can drop that division to keep page furniture out of the text.

  • The sticky-note comment is an annotation (extractAnnotationText), the outline is a <ul> after the pages (extractBookmarksText) and the AcroForm field is an <ol> in <div class="acroform"> (extractAcroFormContent), with the field’s alternate name in altName.

  • The attachment errata.pdf is an embedded document. With the default ParseContext, AutoDetectParser parses it inline, so its text appears inside the container’s XHTML after an <div class="embedded"> marker. With the RecursiveParserWrapper or /rmeta it is a separate metadata object with tk:embedded-resource-type = ATTACHMENT, tk:embedded-resource-path = /errata.pdf and pdf:embedded-file-description. See Embedded Documents & Recursive Parsing.

  • pdf:incremental-update-count = 1 and two pdf:eof-offsets values: PDFBox wrote the form field value as an incremental update when the fixture was built, so the file has two %%EOF markers.

  • The dcterms:created and pdf:docinfo:created values are in UTC; the fixture was written at 09:00 Madrid time.

The PDF metadata keys

The keys below are the ones a Bookshelf intake service reads; all of them appeared in the runs on this page. Key names follow the 4.x kebab-case convention; see Metadata for the 3.x names and the LegacyKeyMigrationFilter that restores them.

Key Meaning

xmpTPg:NPages

Page count.

pdf:pdf-version, dc:format

PDF version from the header (1.6) and the media type with that version.

pdf:docinfo:*

Raw values of the information dictionary (title, creator, creator-tool, producer, created, …​). The generic dc:title, dc:creator and dcterms:created are mapped from them and from XMP.

pdf:has-xmp

Whether the document has an XMP metadata stream.

pdf:chars-per-page, pdf:unmapped-unicode-chars-per-page

One value per page; the input to the AUTO OCR verdict.

pdf:total-unmapped-unicode-chars, pdf:overall-percentage-unmapped-unicode-chars

Document totals of glyphs without a Unicode mapping. A high value means the text layer is unreliable.

pdf:ocr-page-count

How many pages tripped the OCR verdict, whether or not an engine ran.

pdf:has-marked-content, pdf:marked-content-pages-tagged, pdf:marked-content-pages-fallback, pdf:marked-content-rejections

Whether the PDF is tagged and how many pages used the structure tree or fell back to the plain stripper (with the reason per page).

pdf:encrypted, access-permission:*

Whether the file is encrypted, and each permission flag (extract-content, extract-for-accessibility, can-print, can-modify, fill-in-form, …​).

pdf:has-acro-form-fields, pdf:has-xfa, pdf:has-collection

Whether the PDF has an AcroForm, an XFA form or is a PDF portfolio.

pdf:annotation-subtypes, pdf:num-3d-annotations

Annotation subtypes found (Text, Widget, Link, …​) and 3D annotations.

pdf:contains-non-embedded-font, pdf:contains-damaged-font

Font facts. A damaged font often explains garbled text.

pdf:incremental-update-count, pdf:eof-offsets

How many times the file was updated in place, and where each revision ends.

pdf:embedded-file-description, pdf:embedded-file-subtype

On an attachment: the file specification’s description and declared subtype.

Configuring PDFParser

PDFParserConfig holds every PDF option. There are three ways to set it, and they can be combined.

In tika-config.json

The pdf-parser entry in the parsers list sets options for the lifetime of the loaded parser. A parsers list loads only the parsers it names, so keep the default-parser entry, or the configuration becomes PDF-only:

{
  "parsers": [
    {
      "pdf-parser": {
        "markedContent": { "strategy": "NONE" },
        "extractBookmarksText": false,
        "extractAnnotationText": false
      }
    },
    { "default-parser": {} }
  ]
}
java PdfToXhtml catalogue.pdf tika-config.json

Output (the <body> only):

<body><div class="page"><p />
<p>Autumn Catalogue 2026
</p>
<p>New fiction
</p>
<p>The Silent Shelf by A. Moreno: a librarian finds a book that rewrites itself.
</p>
<p>Paper Lanterns by K. Ito: twelve stories set in one Kyoto bookshop.
</p>
<p>Bookshelf - page 1</p>
<p />
</div>
<div class="page"><p />
<p>Order form
</p>
<p>Non-fiction
</p>
<p>Ink and Iron: a history of the printing press, 480 pages.
</p>
<p>Fill in the member number below and return this page to the shop.
</p>
<p>Bookshelf - page 2</p>
<p />
</div>
<div source="attachment" class="embedded" id="errata.pdf" />
<div class="page"><p />

<p>Errata: The Silent Shelf, 2nd printing, page 112.</p>

<p />

</div>

<div class="acroform"><ol>	<li altName="Member number">memberNumber: BK-20417</li>
</ol>
</div>
</body>

With markedContent.strategy set to NONE the parser writes what the plain PDFBox text stripper writes, as Tika did before 4.1.0: no headings, and the footer is an ordinary paragraph again. The comment and the outline are gone. The AcroForm field is still there because extractAcroFormContent was not changed.

In code, per request

PDFParser also reads a PDFParserConfig from the ParseContext, and the pages policy from a PagesConfig in the context. Values set this way apply to one call only, which suits a service that changes options per upload:

import java.nio.file.Path;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.pages.PagesConfig;
import org.apache.tika.parser.pages.TextPolicy;
import org.apache.tika.parser.pdf.PDFParserConfig;
import org.apache.tika.sax.BodyContentHandler;

public class PdfIntake {

    public static void main(String[] args) throws Exception {
        AutoDetectParser parser = new AutoDetectParser();

        // Per-request PDF options: a typed object in the ParseContext.
        PDFParserConfig pdf = new PDFParserConfig();
        pdf.setExtractBookmarksText(false);   // the outline repeats the headings
        pdf.setExtractAnnotationText(true);   // keep reviewer comments
        pdf.setMaxPages(50);                  // a catalogue never needs more

        // Per-request pages policy: never OCR in this request.
        PagesConfig pages = new PagesConfig();
        pages.setText(TextPolicy.EXTRACT);

        ParseContext context = new ParseContext();
        context.set(PDFParserConfig.class, pdf);
        context.set(PagesConfig.class, pages);

        for (String file : new String[] {"files/catalogue.pdf", "files/scanned-loan-form.pdf"}) {
            Metadata metadata = new Metadata();
            BodyContentHandler handler = new BodyContentHandler(-1);
            try (TikaInputStream tis = TikaInputStream.get(Path.of(file), metadata)) {
                parser.parse(tis, handler, metadata, context);
            }
            System.out.println("== " + file);
            System.out.println(handler.toString().strip().replaceAll("\\s*\\n\\s*", "\n"));
            System.out.println("pdf:chars-per-page = " + String.join(", ", metadata.getValues("pdf:chars-per-page")));
            System.out.println("pdf:ocr-page-count = " + metadata.get("pdf:ocr-page-count"));
        }
    }
}

Output:

== files/catalogue.pdf
Autumn Catalogue 2026
New fiction
The Silent Shelf by A. Moreno: a librarian finds a book that rewrites itself.
Paper Lanterns by K. Ito: twelve stories set in one Kyoto bookshop.
Bookshelf - page 1
Check the price of Paper Lanterns before printing.
Order form
Non-fiction
Ink and Iron: a history of the printing press, 480 pages.
Fill in the member number below and return this page to the shop.
Bookshelf - page 2
Errata: The Silent Shelf, 2nd printing, page 112.
memberNumber: BK-20417
pdf:chars-per-page = 194, 161
pdf:ocr-page-count = 0
== files/scanned-loan-form.pdf
Cover note: request scanned at the front desk on 2026-10-02.
Staff: process within five working days.
pdf:chars-per-page = 0, 100
pdf:ocr-page-count = 0

The bookmarks are gone from the catalogue, and the scanned page of the loan form produced no text and no OCR verdict: with TextPolicy.EXTRACT the parser never considers OCR (pdf:ocr-page-count = 0). Compare that with the AUTO run further down.

A PDFParserConfig in the context replaces the parser’s configured one for that call, as a whole: every option you did not set falls back to its default, not to the value in tika-config.json. A test that loaded the NONE configuration above and added a context PDFParserConfig that only turned off extractAcroFormContent got headings, bookmarks and annotations back. Build a fresh object per request with every option you need, rather than sharing and mutating one instance across threads.

Through tika-server and Tika Pipes

tika-server and Tika Pipes read the same pdf-parser entry from the server’s tika-config.json, and both can receive a per-request parse-context that overrides it, for example a pages block or a pdf-parser overlay. How a request carries that JSON (headers, a multipart part, a FetchEmitTuple) is covered on the Tika Server (REST API) and Tika Pipes pages of this section. Per-request configuration is a capability that must be enabled on the server; keep it off for untrusted callers.

The 3.x tika-config.xml equivalent

In Tika 3.x the same options were XML <param> elements on the PDFParser entry, and the OCR settings were flat ocr* parameters on PDFParserConfig (ocrStrategy, ocrDPI, ocrImageType, ocrRenderingStrategy). There was no maxPages and no pages block. This block is for readers maintaining a 3.x deployment; it was not run for this page:

<!-- Tika 3.x only: tika-config.xml -->
<properties>
  <parsers>
    <parser class="org.apache.tika.parser.DefaultParser">
      <parser-exclude class="org.apache.tika.parser.pdf.PDFParser"/>
    </parser>
    <parser class="org.apache.tika.parser.pdf.PDFParser">
      <params>
        <param name="extractBookmarksText" type="bool">false</param>
        <param name="extractMarkedContent" type="bool">false</param>
        <param name="ocrStrategy" type="string">auto</param>
        <param name="ocrDPI" type="int">300</param>
      </params>
    </parser>
  </parsers>
</properties>

tika-app 4.x can convert such a file with its --convert-config-xml-to-json flag; check the result against the tables below, because the OCR parameters move into pages.

The PDFParserConfig options

The tables list every pdf-parser key in Tika 4.1.0 with its default. The JSON key is the bean property name: the key sortByPosition maps to setSortByPosition. The list was checked against the PDFParserConfig class in the 4.1.0 jar and the full configuration example in the 4.1.x documentation.

Text extraction

Key Default What it does

sortByPosition

false

Sort the text by x/y position instead of content-stream order. Helps PDFs whose producer drew text out of order; can interleave the columns of a multi-column page.

enableAutoSpace

true

Let PDFBox estimate where spaces belong. Most PDFs do not draw explicit space glyphs, so turning this off usually glues words together.

averageCharTolerance

0.3

PDFBox character-width tolerance for inserting spaces.

spacingTolerance

0.5

PDFBox space-width tolerance for inserting spaces.

dropThreshold

2.5

Line-height multiple that starts a new paragraph.

ignoreContentStreamSpaceGlyphs

false

Ignore space glyphs drawn in the content stream and rely on the spacing algorithm (PDFBOX-3774).

suppressDuplicateOverlappingText

false

Remove text drawn twice over the same place (fake bold). Can be slow on large pages.

detectAngles

false

Detect and correct rotated text runs within a page. Costs an extra pass per page; it also overrides markedContent.

extractFontNames

false

Record the font names used in metadata.

markedContent

see below

Tagged PDF handling: strategy (AUTO, TAGS, NONE), minCoverage (0.5), maxDanglingRatio (0.2). The 3.x boolean extractMarkedContent still loads: true is TAGS, false is NONE.

Annotations, outline, forms and actions

Key Default What it does

extractAnnotationText

true

Write annotation text (comments, form-field captions) into <div class="annotation">.

extractBookmarksText

true

Write the outline as a <ul> after the pages.

extractAcroFormContent

true

Write AcroForm field names and values into <div class="acroform">.

ifXFAExtractOnlyXFA

false

If the PDF has an XFA form, write only the XFA data and skip the page text.

extractActions

false

Extract PDF actions; JavaScript becomes embedded documents. Useful for security triage.

Images and page rendering

Key Default What it does

extractInlineImages

false

Extract the images in the content streams as embedded documents (tk:embedded-resource-type = INLINE). The image parser then parses them and, if a text recognizer is configured, OCRs them.

extractUniqueInlineImagesOnly

true

Emit each image (by object id) only once, even if many pages draw it, such as a logo.

extractInlineImageMetadataOnly

false

Record inline-image metadata without extracting the bytes. Faster than extractInlineImages.

imageGraphicsEngineFactoryClass

none

Expert: a class implementing ImageGraphicsEngineFactory that replaces the inline-image extractor.

renderingStrategy

ALL

What PDFBox draws when it renders a page for OCR: ALL, TEXT_ONLY, NO_TEXT, VECTOR_GRAPHICS_ONLY. NO_TEXT keeps the existing text layer out of the image, so OCR does not read it a second time.

pages

see below

The 4.1 pages block: text source, rendering, OCR budget and verdict, inference release, page images. It overlays the general parse-context.pages block, field by field.

setKCMS

false

Expert: set the Sun KCMS colour-management system property, which can speed up rendering on some JDKs.

Limits, memory and errors

Key Default What it does

maxPages

-1

Maximum number of pages to process; -1 means no limit. Not in 3.3.2. It also bounds OCR and page emission: a page that is not read is not rendered.

maxMainMemoryBytes

536870912

How much of the PDF PDFBox may hold in memory (512 MB) before it buffers to a temporary file.

catchIntermediateIOExceptions

true

Collect a per-stream IOException in metadata, keep parsing the rest of the document and rethrow after parsing.

accessCheckMode

DONT_CHECK

Whether to enforce the PDF’s extraction permissions: DONT_CHECK, ALLOW_EXTRACTION_FOR_ACCESSIBILITY, IGNORE_ACCESSIBILITY_ALLOWANCE. See Encrypted PDFs and passwords.

throwOnEncryptedPayload

false

Throw on an encrypted payload (a PDF 2.0 unencrypted wrapper around an encrypted document) instead of skipping it.

Incremental updates

Key Default What it does

extractIncrementalUpdateInfo

true

Record whether the file has incremental updates and how many (pdf:incremental-update-count, pdf:eof-offsets).

parseIncrementalUpdates

false

Parse each earlier revision as an embedded document. Useful for forensics: text that was "deleted" in a later revision is still in the file.

maxIncrementalUpdates

10

Maximum number of revisions to parse when parseIncrementalUpdates is on.

Tagged PDFs and marked content

A tagged PDF carries a structure tree that says which content is a paragraph, a heading, a list, a table, a link or a figure with alternative text, and which content is an artifact (running headers, footers, page numbers). Tagging is what PDF/UA requires for accessibility; PDF/A & PDF/UA with iText shows how such files are produced.

In 4.1.0 the parser can follow that tree. The words and the spacing still come from the same text stripper (with the same sortByPosition, enableAutoSpace and tolerance settings); the tree decides which XHTML elements the words land in and their order across blocks.

strategy Behaviour

AUTO (default)

Follow the tree on each page that passes the quality gate; write the other pages as the stripper would.

TAGS

Follow the tree on every page that has tagged text.

NONE

Ignore the tree: the output of Tika before 4.1.0.

The AUTO gate is per page. A page uses its tags only when the tree claims at least minCoverage of the page’s text (0.5), at most maxDanglingRatio of its leaves point at content the page never draws (0.2), some of its text is in a text-holding element (a paragraph, heading, cell or item), and the tree does not cut words into pieces (the documentation describes a 30% threshold for words that span three or more blocks, a pattern of some form generators). A PDF without a structure tree is always written as with NONE, and detectAngles always wins over the tree.

With tags, P becomes <p>, H1 becomes <h1>, L becomes <ul> or <ol>, Table, TR, TH and TD become a table, and Link becomes <a href> when its annotation has a URI. Other types become a <div> or <span> with the type as its class. A Figure or Formula writes its Alt text, so the output reads as a screen reader would read it. Text the tree does not reference goes into <div class="untagged"> and artifacts into <div class="artifact">, so nothing is lost and a consumer can drop headers and footers.

The result is reported in metadata: pdf:marked-content-pages-tagged and pdf:marked-content-pages-fallback count the pages, and pdf:marked-content-rejections gives the reason per page that fell back. Watch the fallback count when you ingest a new producer’s files: a high value means the tags were rejected and the page was written as plain text.

{
  "parsers": [
    {
      "pdf-parser": {
        "markedContent": { "strategy": "AUTO", "minCoverage": 0.8, "maxDanglingRatio": 0.1 }
      }
    },
    { "default-parser": {} }
  ]
}

The stricter thresholds above make AUTO trust the tree only on well-tagged pages. Setting minCoverage above 1 sends every page back to the stripper. The documentation says the defaults start loose "until a corpus says otherwise", so measure on your own files before you tighten them.

The pages block (4.1)

Tika 4.1 moved every "what to do with the pages" decision into one block, pages, shared by every parser that renders (the PDF parser and, in 4.1, the EMF and WMF parsers). It answers five questions:

Field Question

text

Where does a page’s text come from?

render

How is a page turned into an image (for OCR, for inference, for emitted images)?

ocr

How many pages may be OCR’d, and when does AUTO decide a page needs OCR?

inference

What does the parser release to the inference bindings? (Experimental in 4.1.0.)

emit

Are page images emitted as embedded documents?

The defaults, in full:

{
  "parse-context": {
    "pages": {
      "text": "AUTO",
      "render": { "dpi": 300, "imageType": "GRAY", "imageFormat": "PNG", "imageQuality": 0.5,
                  "maxWidth": -1, "maxHeight": -1, "minWidth": 2, "minHeight": 2,
                  "maxImagePixels": 100000000 },
      "ocr": { "maxPages": -1, "auto": { "totalCharsPerPage": 10, "unmappedUnicodeCharsPerPage": 10 } },
      "inference": ["TEXT"],
      "emit": { "enabled": false, "maxPages": -1, "maxDepth": -1, "resourceTypes": [], "render": {} }
    }
  }
}

text: where a page’s text comes from

Value (TextPolicy) Behaviour

EXTRACT

The content stream only. Pages are never rendered for OCR.

AUTO (default)

The content stream, plus OCR of each page whose extracted text is poor by the ocr.auto thresholds. The page’s text is replaced by the engine’s output when the engine is a text recognizer that actually wrote text, and kept otherwise (no engine, ocr.maxPages spent, engine timeout). AUTO with no engine behaves as EXTRACT.

EXTRACT_AND_OCR

Both sources, on every page.

OCR

The engine’s output only; the content stream is not read. Fails the parse when no engine covers the render type.

NONE

No text from any source. Metadata, attachments, page renders, inference and emission still happen.

render: how a page becomes an image

Key Default Meaning

dpi

300

Target resolution; 300 suits Tesseract.

maxWidth, maxHeight

-1

A box the image must fit: scaled down to fit, never enlarged. A thumbnail is a box, not a resolution.

minWidth, minHeight

2

A page smaller than this at the target dpi is not rendered at all, with no warning.

imageType

GRAY

GRAY or RGB.

imageFormat

PNG

PNG, JPEG or TIFF.

imageQuality

0.5

ImageIO quality: fidelity for JPEG, an inverted effort knob for PNG.

maxImagePixels

100000000

Skip a page that would still be larger than this many pixels, with a warning and the page number in tk:rendering:failed-page. -1 for no limit.

ocr: the recognizer’s budget and verdict

Key Default Meaning

maxPages

-1

Pages of one document that may be OCR’d, counted from the first.

auto.totalCharsPerPage

10

Under AUTO, a page with fewer extracted characters than this is OCR’d.

auto.unmappedUnicodeCharsPerPage

10

Under AUTO, glyphs without a Unicode mapping that send a page to OCR: a count when 1 or more, a fraction of the page’s characters when below 1.

inference and emit

inference is ["TEXT"] by default. Adding PAGES renders each page for a PAGES inference binding (an image embedding or a VLM that reads page images); it never turns OCR on. The 4.1.0 documentation marks this field experimental: its values may change in a minor release. See VLM Parsers & Inference.

emit turns page renders into embedded documents of type RENDERING:

Key Default Meaning

enabled

false

Emit each page’s render at the end of the page.

maxPages

-1

Pages emitted, counted from the first; also bounded by pdf-parser.maxPages.

maxDepth

-1

Deepest embedded level whose pages are emitted: 0 for the top-level document only.

resourceTypes

[]

Emit only for documents embedded as one of these tk:embedded-resource-type values.

render

{}

An overlay on render for the emitted images only, so OCR keeps 300 dpi grayscale while previews are small and in colour.

Where the block goes, and which one wins

The block is read at four layers, later ones winning. Layers 3 and 4 overlay the general block field by field; layer 2 replaces layer 1 entirely:

  1. parse-context.pages in tika-config.json: the default for every rendering parser.

  2. pages in a request’s parse-context (or a server preset). It replaces the configuration’s general block.

  3. pages inside the pdf-parser entry under parsers: the PDF overlay, kept across requests.

  4. pages inside a request’s pdf-parser entry: the same overlay per request.

The second rule is the surprise. A setting that a request must not lose (for example a lower dpi for every PDF) belongs in the parser’s overlay (layer 3), because a request that sends its own general pages block replaces layer 1 entirely. In Java, context.set(PagesConfig.class, overlay) is the request layer, as in the PdfIntake program above.

{
  "parsers": [
    { "default-parser": {} },
    { "pdf-parser": { "pages": { "render": { "dpi": 200 },
                                 "emit": { "render": { "imageType": "RGB" } } } } }
  ]
}

With this file a request that sends pages with text set to OCR OCRs every page at 200 dpi, because the PDF overlay still holds.

The 4.0 spellings

A 4.0 pdf-parser configuration still loads; its values land in pages, and a configuration dump writes pages only. Do not mix both spellings in one JSON object: the later key wins.

4.0 key 4.1 key

ocr.strategy (NO_OCR, AUTO, OCR_ONLY, OCR_AND_TEXT_EXTRACTION)

pages.text (EXTRACT, AUTO, OCR, EXTRACT_AND_OCR)

ocr.strategyAuto

pages.ocr.auto

ocr.maxPagesToOcr

pages.ocr.maxPages

ocr.dpi, imageType, imageFormat, imageQuality, maxImagePixels

pages.render.*

ocr.renderingStrategy

pdf-parser.renderingStrategy

imageStrategy set to RAW_IMAGES

extractInlineImages set to true

imageStrategy set to RENDER_PAGES_BEFORE_PARSE or RENDER_PAGES_AT_PAGE_END

pages.emit.enabled set to true (renders are emitted at page end either way)

The 3.x names (ocrStrategy, ocrDPI and the other flat ocr* setters) map to the same places. In 3.x the strategy values were the 4.0 ones: NO_OCR, AUTO, OCR_ONLY and OCR_AND_TEXT_EXTRACTION.

The per-page decision

This diagram puts text, ocr.auto, ocr.maxPages and the text recognizers together. It shows what happens to one page that is within pdf-parser.maxPages.

flowchart TD PAGE["Next page, within pdf-parser.maxPages"] --> POLICY{"pages.text"} POLICY -- "EXTRACT" --> EXT["Text from the content stream"] POLICY -- "NONE" --> NOTEXT["No text; renders, inference and emit still run"] POLICY -- "OCR" --> OCRONLY{"Engine for the render type?"} OCRONLY -- "no" --> FAIL["Parse fails: no OCR engine"] OCRONLY -- "yes" --> R1["Render page with pages.render"] --> T1["OCR text only"] POLICY -- "EXTRACT_AND_OCR" --> R2["Content stream text, then render and OCR"] --> T2["Both texts"] POLICY -- "AUTO" --> A1["Text from the content stream"] --> CHECK{"Too few chars or too many unmapped glyphs? (ocr.auto)"} CHECK -- "no" --> KEEP["Keep the extracted text"] CHECK -- "yes" --> COUNT["Counted in pdf:ocr-page-count"] --> BUDGET{"Engine configured and ocr.maxPages not spent?"} BUDGET -- "no" --> KEEP BUDGET -- "yes" --> R3["Render page and call the text recognizer"] --> WROTE{"Engine wrote text?"} WROTE -- "yes" --> REPLACE["Replace the page text with the OCR text"] WROTE -- "no, or timeout" --> KEEP

Two consequences follow from the diagram. First, pdf:ocr-page-count counts the verdict, not the OCR: it is 1 for the loan form even on a machine without Tesseract, which makes it a cheap way to find scanned PDFs in a corpus before you pay for OCR. Second, under AUTO a page with a reasonable text layer is never OCR’d, so a PDF with a poor invisible OCR layer from a scanner (enough characters, all mapped, mostly wrong) slips through; for such sources use EXTRACT_AND_OCR or OCR.

OCR strategies for scanned pages

Choosing the text policy

Corpus pages.text Why

Born-digital only (exports from word processors, reports)

EXTRACT

No rendering cost, and no OCR engine needed.

Mixed born-digital and scans (a typical intake service)

AUTO

OCR only the pages that need it.

Scans with an unreliable embedded OCR layer

EXTRACT_AND_OCR

Keeps both; the index sees the engine’s text even when the layer has enough characters to pass AUTO.

Image-only archives, or a layer you know is wrong

OCR

One source, no duplicated text. Fails loudly if the engine is missing.

Thumbnails, embeddings or page annotators only

NONE

Renders and inference without text cost.

The text recognizer

The PDF parser calls the engine named in the top-level text-recognizers list, or, with no list, the one Tika finds among the loaded parsers (it logs one line per engine at start-up). Configure Tesseract once under engines and name it:

{
  "engines": {
    "tesseract": { "tesseract-ocr-parser": { "language": "eng" } }
  },
  "text-recognizers": [
    { "engine": "tesseract" }
  ],
  "parsers": [
    { "default-parser": {} },
    { "pdf-parser": { "pages": { "ocr": { "maxPages": 20 } } } }
  ]
}

On the test machine, which has no Tesseract binary, this configuration failed at load time rather than at the first scan, which is the documented behaviour for a named engine that reports no media types:

org.apache.tika.exception.TikaConfigException: Text recognizer engine "tesseract" advertises no media types (a _mime-include list does not count). Is the engine unavailable (missing native binary, unreachable inference server) or configured to skip enrichment?

That is a good failure: a container that was built without Tesseract does not start and silently skip OCR. Install Tesseract and its language packs, or use the apache/tika:4.1.0-full image; see OCR with Tesseract for the engine options (language, page segmentation, timeouts, preprocessing) and the in-process Tess4J engine.

Two related switches:

  • An empty list, "text-recognizers": [], turns OCR off for the whole configuration.

  • A request can turn the recognizers off for itself with a parse-context entry text-recognizers whose enabled is false; then OCR does not fail for lack of an engine.

scanned-loan-form.pdf under AUTO

The next program parses the loan form with RecursiveParserWrapper, prints the per-page counts and the OCR verdict, and lists the embedded documents. Its configuration keeps AUTO and also emits a colour thumbnail of the first page, the kind of preview an intake UI shows next to the extracted text.

{
  "parsers": [
    { "default-parser": {} },
    {
      "pdf-parser": {
        "pages": {
          "text": "AUTO",
          "emit": {
            "enabled": true,
            "maxPages": 1,
            "render": { "maxWidth": 256, "maxHeight": 256, "imageType": "RGB" }
          }
        }
      }
    }
  ]
}
import java.nio.file.Path;
import java.util.List;
import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.RecursiveParserWrapper;
import org.apache.tika.sax.BasicContentHandlerFactory;
import org.apache.tika.sax.RecursiveParserWrapperHandler;

public class LoanFormPages {

    public static void main(String[] args) throws Exception {
        TikaLoader loader = TikaLoader.load(Path.of(args[0]));
        RecursiveParserWrapper wrapper = new RecursiveParserWrapper(loader.loadAutoDetectParser());
        ParseContext context = loader.loadParseContext();
        RecursiveParserWrapperHandler handler = new RecursiveParserWrapperHandler(
                new BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.TEXT, -1));

        Metadata metadata = new Metadata();
        try (TikaInputStream tis = TikaInputStream.get(Path.of("files/scanned-loan-form.pdf"), metadata)) {
            wrapper.parse(tis, handler, metadata, context);
        }

        List<Metadata> list = handler.getMetadataList();
        Metadata pdf = list.get(0);
        System.out.println("chars per page : " + String.join(", ", pdf.getValues("pdf:chars-per-page")));
        System.out.println("pages for OCR  : " + pdf.get("pdf:ocr-page-count"));
        System.out.println("text           : " + pdf.get("tk:content").strip().replaceAll("\\s*\\n\\s*", " | "));
        for (Metadata child : list.subList(1, list.size())) {
            System.out.println("embedded       : " + child.get("tk:embedded-resource-type")
                    + " " + child.get("Content-Type")
                    + " page " + child.get("tk:page:number")
                    + " " + child.get("tiff:ImageWidth") + "x" + child.get("tiff:ImageLength"));
        }
    }
}
java LoanFormPages pages-config.json

Output (no OCR engine installed):

chars per page : 0, 100
pages for OCR  : 1
text           : Inter-library loan form | Cover note: request scanned at the front desk on 2026-10-02. | Staff: process within five working days.
embedded       : RENDERING image/png page 1 181x256

The verdict is right: page 1 has 0 characters, below totalCharsPerPage (10), so AUTO sent it to OCR, and page 2 (100 characters) kept its text layer. With no engine the page keeps its (empty) extracted text and the parse succeeds; the first line of the text is the document title that the text handler writes from the XHTML <title>. With Tesseract configured, the page text would be the engine’s transcription of the scan (member number, title, library). The emitted render is a 181 x 256 RGB PNG: the A4 page scaled to fit the 256-pixel box, aspect preserved, while OCR would still render at 300 dpi grayscale. Its metadata also carries tk:page:number and rendering timings (tk:rendering:rendering-time-ms).

Switching the same file to OCR without an engine fails the parse, with an error that names the fix:

org.apache.tika.exception.TikaException: Unable to extract PDF content
Caused by: java.io.IOException: org.apache.tika.exception.TikaException: I regret that I couldn't find an OCR engine to handle image/png. Name one that covers it in "text-recognizers" (a configured list is authoritative), add one to the classpath when no list is configured, or set "pages": {"text": "EXTRACT"}.

Inline images versus page renders

There are two ways a scan can reach an OCR engine, and they are independent:

  • Page renders (pages.text set to AUTO, OCR or EXTRACT_AND_OCR): the parser renders the whole page and passes the image to the recognizer.

  • Inline images (extractInlineImages set to true): each image in the content stream becomes an embedded document, image-parser parses it, and the same recognizer enriches it.

With extractInlineImages on and maxPages set to 1, the loan form’s scan comes out as an embedded image-0.png (1240 x 1754 pixels, tk:embedded-resource-type = INLINE, tk:page:number = 1). A scanned page is one full-page image, so with both paths on it is OCR’d twice, and the text appears twice in the output. Pick one: page renders for scans, inline images when you need the pictures themselves (for example cover images in a catalogue). renderingStrategy set to NO_TEXT is a further refinement for EXTRACT_AND_OCR: the render omits the text layer, so OCR only reads what was not text already.

OCR cost

OCR is the most expensive thing a PDF parse can do. A 300 dpi grayscale A4 page is about 2480 x 3508 pixels (8.7 megapixels), and Tesseract typically takes seconds per page; a 300-page scanned book is minutes of CPU. Bound it:

  • pdf-parser.maxPages caps the pages read at all; pages.ocr.maxPages caps the pages OCR’d.

  • pages.render.maxImagePixels skips absurd pages (a poster at 300 dpi) with a warning instead of exhausting the heap.

  • Lower dpi (200) trades some accuracy for speed; do not go below what the engine needs for small print.

  • Run OCR in forked processes (Tika Pipes, tika-server) with timeouts, so one slow scan does not block or crash the application. The Robustness, Limits & Timeouts page of this section covers the forked-parse model.

Encrypted PDFs and passwords

A PDF can be encrypted with two passwords. The user password (also called the open password) is needed to read the file at all; the owner password lifts the permission restrictions. A file with only an owner password opens without any password, and PDFBox reads it; whether the text may be extracted is then a question of permissions, which Tika reports and, by default, ignores. How encryption and permissions are written is shown in Encryption & Permissions in the PDF Generation section.

Supplying the password

Tika asks a PasswordProvider in the ParseContext for the password. It receives the document’s Metadata, so it can pick the password by tk:resource-name or by a value the caller put into metadata. The Bookshelf members-catalogue.pdf is AES-256 encrypted with a user password, and its permissions forbid content extraction but allow extraction for accessibility:

import java.nio.file.Path;
import org.apache.tika.exception.EncryptedDocumentException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.PasswordProvider;
import org.apache.tika.sax.BodyContentHandler;

public class MembersCatalogue {

    static void parse(AutoDetectParser parser, ParseContext context, String label) throws Exception {
        Metadata metadata = new Metadata();
        BodyContentHandler handler = new BodyContentHandler(-1);
        try (TikaInputStream tis = TikaInputStream.get(Path.of("files/members-catalogue.pdf"), metadata)) {
            parser.parse(tis, handler, metadata, context);
            String text = handler.toString().strip();
            System.out.println(label + ": " + text.substring(0, text.indexOf('\n')));
        } catch (EncryptedDocumentException e) {
            System.out.println(label + ": " + e.getMessage() + " (cause: " + e.getCause().getMessage() + ")");
        }
        System.out.println("   pdf:encrypted = " + metadata.get("pdf:encrypted")
                + ", access-permission:extract-content = " + metadata.get("access-permission:extract-content"));
    }

    public static void main(String[] args) throws Exception {
        AutoDetectParser parser = new AutoDetectParser();

        // 1. No PasswordProvider in the context.
        parse(parser, new ParseContext(), "no password");

        // 2. A PasswordProvider that reads the secret from the environment.
        ParseContext context = new ParseContext();
        context.set(PasswordProvider.class, metadata -> System.getenv("BOOKSHELF_MEMBERS_PDF_PASSWORD"));
        parse(parser, context, "with password");
    }
}
# The value comes from your secret store; it is never written in source code.
export BOOKSHELF_MEMBERS_PDF_PASSWORD="$(your-secret-tool read bookshelf/members-pdf)"
java MembersCatalogue

Output:

no password: Unable to process: document is encrypted (cause: Cannot decrypt PDF, the password is incorrect)
   pdf:encrypted = true, access-permission:extract-content = null
with password: Autumn Catalogue 2026
   pdf:encrypted = true, access-permission:extract-content = false

Without a password PDFParser throws org.apache.tika.exception.EncryptedDocumentException, whose cause is PDFBox’s InvalidPasswordException; a wrong password fails the same way. Catch it as its own case: it is not a corrupt file, and a service should answer "password required" instead of "cannot parse". With the right password the text comes out, and the metadata records that the file is encrypted and what its permissions say.

The same PasswordProvider serves encrypted Office files, so one provider can cover the whole intake. The command line and tika-server have their own ways to pass a password (a CLI option and a request header); those are described on the Tika App (Command Line & GUI) and Tika Server (REST API) pages of this section. Treat a password as a credential: read it from the environment or a secret manager, keep it out of logs and out of Metadata that you emit or index.

Permissions and accessCheckMode

The members-catalogue.pdf permissions say "no content extraction". Tika ignores that by default (DONT_CHECK), because an indexer usually has a legitimate reason to read files it was given. When your policy is to honour the producer’s flags, set accessCheckMode:

accessCheckMode Behaviour members-catalogue.pdf, with the password

DONT_CHECK (default)

Extract regardless of the permission flags.

Text extracted.

ALLOW_EXTRACTION_FOR_ACCESSIBILITY

Refuse when extraction is forbidden, unless the extract-for-accessibility flag is set.

Text extracted (the accessibility flag is set).

IGNORE_ACCESSIBILITY_ALLOWANCE

Refuse whenever content extraction is forbidden.

AccessPermissionException: Content extraction is not allowed.

{
  "parsers": [
    { "pdf-parser": { "accessCheckMode": "IGNORE_ACCESSIBILITY_ALLOWANCE" } },
    { "default-parser": {} }
  ]
}

org.apache.tika.exception.AccessPermissionException is a TikaException; catch it next to EncryptedDocumentException and record the file as "restricted" rather than "failed". In 3.x this was configured with an AccessChecker object (setAccessChecker); 4.x replaced it with the enum (the 3.3.2 Javadoc shows the setter; this page did not check in which 4.x release the change happened).

throwOnEncryptedPayload covers a different case: a PDF 2.0 encrypted payload document, an unencrypted wrapper whose real content is an encrypted attachment. By default Tika skips the payload; set the flag to true to fail instead, so such files are not indexed as their cover page.

What Tika does not do

Tika does not guess or crack passwords, and it does not remove encryption. A file that needs a user password you do not have is a failed parse with an EncryptedDocumentException; report it as such.

XFA forms

PDF has two form technologies. AcroForm fields are widgets on the page, which Tika writes into <div class="acroform"> as shown for the catalogue. XFA (XML Forms Architecture) forms are XML templates and data inside the PDF, from which an XFA-capable viewer generates the pages; many viewers do not support XFA, and the PDF 2.0 standard deprecates it. The PDF Generation section explains both in Editable PDF Forms: Fundamentals, and Editable Forms with JasperReports & PDFBox shows how PDFBox fills AcroForm fields.

Tika reads the XFA data with its XFAExtractor and writes field names and values into <div class="xfa_content">. The Bookshelf loan-form-xfa.pdf has one page that says the viewer cannot show the form, and an XFA form with two fields:

java PdfToXhtml loan-form-xfa.pdf

Output (the <body> and pdf:has-xfa):

<body><div class="page"><p />
<p>Please wait... this form needs an XFA-capable viewer.</p>
<p />
</div>
<div class="xfa_content"><div class="xfa_form"><ol>	<li fieldName="memberNumber">memberNumber: BK-20417</li>
	<li fieldName="title">title: Ink and Iron</li>
</ol>
</div>
</div>
</body>
pdf:has-xfa = true

The page text is the fallback message every XFA form carries for viewers that cannot render it, and it is noise in a search index. With ifXFAExtractOnlyXFA set to true Tika writes only the XFA content:

{
  "parsers": [
    { "pdf-parser": { "ifXFAExtractOnlyXFA": true } },
    { "default-parser": {} }
  ]
}
<body><div class="xfa_content"><div class="xfa_form"><ol>	<li fieldName="memberNumber">memberNumber: BK-20417</li>
	<li fieldName="title">title: Ink and Iron</li>
</ol>
</div>
</div>
</body>

Use pdf:has-xfa to route XFA files: they are rarer than AcroForm files, often come from government or insurance workflows, and are the files the next section is about.

XFA and CVE-2025-54988 / CVE-2025-66516

Because XFA is XML, a PDF that carries XFA makes Tika run an XML parser on attacker-supplied bytes. That is the setting of an XML External Entity (XXE) attack: the XML declares an entity that points at a local file or an internal URL, and a parser that resolves it copies the file’s contents (or the response) into the output.

CVE Summary

CVE-2025-54988 (published 2025-08-20)

Critical XXE in tika-parser-pdf-module 1.13 through 3.2.1 (as worded in the CVE record; the separate module exists from 2.0.0, and 1.x shipped the parser in tika-parsers), through a crafted XFA file inside a PDF. An attacker may read sensitive data or make the server send requests to internal resources or third parties. The module is used by tika-parsers-standard-modules, tika-parsers-standard-package, tika-app, tika-grpc and tika-server-standard. Fixed in 3.2.2.

CVE-2025-66516 (published 2025-12-04)

The same vulnerability with a wider scope. The entry point is the PDF module, but the vulnerability and its fix were in tika-core: anyone who upgraded tika-parser-pdf-module without upgrading tika-core to 3.2.2 or later is still vulnerable. It also names the 1.x tika-parsers artifact, where PDFParser lived in 1.x.

Affected artifacts and versions, as listed in the CVE records:

Artifact Affected Safe

org.apache.tika:tika-core

1.13 to 3.2.1

3.2.2 and later, including all 4.x

org.apache.tika:tika-parser-pdf-module

2.0.0 to 3.2.1

3.2.2 and later, including all 4.x

org.apache.tika:tika-parsers (1.x)

1.13 to 1.28.5

None: 1.x reached end of life in 2022. Move to a supported line.

At the baseline date Tika’s own security page lists CVE-2025-54988 but not CVE-2025-66516; the MITRE record is the authoritative source for the second one.

What to do:

  1. Upgrade both tika-core and the parser modules to 3.2.2 or later (3.3.x, or 4.1.x). Import tika-bom (or use tika-parsers-standard-package) so every Tika artifact has the same version, and check the resolved versions with mvn dependency:tree or gradle dependencies. A stale tika-core pulled in by another library (a search client, an ingestion framework) is exactly the case CVE-2025-66516 describes.

  2. Rebuild container images; apache/tika images for 3.2.2 and later carry the fix.

  3. Parse untrusted PDFs in a forked process (Tika Pipes, tika-server) with no access to secrets or to internal networks it does not need. Isolation does not stop XXE, but it limits what an entity can reach.

  4. Do not rely on pdf:has-xfa as a guard: the key is set by the same parse that reads the XFA, so by the time you see it the XML has been parsed. Upgrading is the fix.

On Tika 4.1.0 the protection is easy to see. A PDF whose XFA declares an external entity pointing at a local file (xxe-probe.pdf, built for this page, with the entity pointing at a dummy file in the test directory) produced an empty <div class="xfa_content" />, did not include the file’s contents, and recorded the refusal as a warning:

pdf:has-xfa = true
tk:exception:warn = javax.xml.stream.XMLStreamException: ParseError at [row,col]:[1,359]
Message: The entity "leak" was referenced, but not declared.

The StAX factory that tika-core 4.1.0 builds in XMLReaderUtils turns off SUPPORT_DTD and IS_SUPPORTING_EXTERNAL_ENTITIES, so the document type declaration was ignored and the entity was never defined, let alone resolved. The broader XXE and upload-validation guidance is in the Security page of this section and the OWASP XXE Prevention Cheat Sheet.

Memory, performance and robustness

PDF parsing cost grows with what you ask for. In rough order of cost:

Work Cost and control

Text layer only (EXTRACT)

Cheap: proportional to the number and complexity of pages. maxPages caps it.

Structure tree (markedContent AUTO)

A second look at each page’s tree; small next to the text pass. NONE skips it.

sortByPosition, suppressDuplicateOverlappingText, detectAngles

Extra passes or sorting per page; turn them on for producers that need them, not globally.

Inline images (extractInlineImages)

Decoding every image, plus the image parser and any OCR. extractUniqueInlineImagesOnly avoids repeating logos.

Page renders (OCR, PAGES inference, emit)

An A4 page at 300 dpi is about 8.7 MB of pixels in grayscale and three times that in RGB and hundreds of milliseconds; the loan form’s thumbnail took about 350 ms to render on the test machine. maxImagePixels and maxPages cap it.

OCR

Seconds per page; the dominant cost of any scanned corpus. ocr.maxPages caps it.

Memory: PDFBox keeps up to maxMainMemoryBytes (512 MB by default) of the file in memory before it buffers to a temporary file. TikaInputStream spools a non-file stream to a temporary file anyway, so for large PDFs pass a Path or a File and keep enough temporary disk space. Lower maxMainMemoryBytes in a small container.

Robustness: PDF is the format with the longest history of parser bugs, infinite loops and out-of-memory errors on malformed files, in PDFBox and in every other library. catchIntermediateIOExceptions keeps a damaged stream from losing the rest of the document (the error is recorded in metadata and rethrown at the end), but nothing in-process protects the JVM from a pathological file. For untrusted uploads parse in a forked JVM with a timeout and a heap limit; the Tika Pipes, Robustness, Limits & Timeouts and Security pages of this section describe how.

What Tika does not give you from a PDF

  • Layout and tables. For an untagged PDF, Tika writes paragraphs in content-stream (or position) order. It does not rebuild columns, reading order or tables. A tagged PDF with good tags is the exception (tables come out as tables). For layout-aware parsing see Document Parsing in the RAG section.

  • Form filling or editing. Tika reads AcroForm and XFA values; it never writes a PDF. Use PDFBox or iText, as the PDF Generation section shows.

  • Signature validation. Tika does not validate digital signatures. See Signature Validation & Long-Term Validity.

  • Text-extraction precision. For coordinates of each glyph, custom extraction strategies or regions of a page, use the PDF library directly: Text Extraction & Parsing shows the iText approach, and PDFBox’s PDFTextStripper is what Tika uses underneath.

PDF internals in the PDF Generation section

This page treats PDF as an input. The PDF Generation section treats it as an output and explains the internals that explain Tika’s behaviour:

Page Why it helps when parsing

How a PDF File Works

Objects, content streams, fonts and incremental updates: why pdf:eof-offsets has two values, and why text extraction depends on fonts.

Text Extraction & Parsing

Extracting text with iText’s strategies when you need positions or regions.

Editable PDF Forms: Fundamentals

AcroForm versus XFA, fields, widgets and flattening.

Editable Forms with JasperReports & PDFBox

PDFBox, the library under PDFParser, used to fill forms.

Encryption & Permissions

User and owner passwords and the permission flags Tika reports.

PDF/A & PDF/UA with iText

How tagged, accessible PDFs are produced: the structure trees markedContent reads.

PDF Standards & Conformance

PDF versions, PDF/A, PDF/UA and why XFA is deprecated.

The Office Files section covers the Office side of the same intake in Text Extraction & Apache Tika.

Changes from 1.x to 4.x

Line PDF parsing

1.x (Tika in Action)

PDFParser in the single tika-parsers artifact, on PDFBox 1.x and later 2.x. Options were setters on PDFParser and later PDFParserConfig, configured from a properties file. The book’s chapter on parser families describes this generation. 1.x is end of life and affected by CVE-2025-54988 and CVE-2025-66516.

2.x

tika-parser-pdf-module split out; PDFBox 2.0.x. PDFParserConfig with ocrStrategy (NO_OCR, OCR_ONLY, OCR_AND_TEXT_EXTRACTION, AUTO), XML <params> in tika-config.xml, typed metadata keys (PDF.*).

3.x

Java 11, PDFBox 3. Same ocr* options and extractMarkedContent boolean; AccessChecker object for permissions. The XFA fix arrived in 3.2.2.

4.0

Java 17, JSON configuration (pdf-parser), kebab-case pdf: keys, a nested ocr block and imageStrategy. By 4.1.0 PDFParserConfig also has maxPages and the accessCheckMode enum, neither of which is in 3.3.2; this page did not check whether each arrived in 4.0 or 4.1.

4.1

The pages block (text, render, ocr, inference, emit) replaces the ocr block and imageStrategy (old spellings still load); markedContent strategy with an AUTO quality gate (structure-aware XHTML from tagged PDFs); the top-level text-recognizers list that names the OCR engine.

The 2.x and 1.x rows summarise the history and the 3.3.2 Javadoc; they were not re-run for this page. The What’s Changed & Migration page of this section collects the full 3.x to 4.x changes.

Pitfalls

  • A PDF-only parsers list. A parsers list with only pdf-parser loads only the PDF parser. Keep default-parser.

  • Expecting OCR without an engine. AUTO with no engine silently behaves as EXTRACT. Watch pdf:ocr-page-count versus the text you got, or name an engine in text-recognizers so a missing engine fails at start-up.

  • OCR’ing twice. Page OCR plus extractInlineImages reads every scanned page twice.

  • A request that drops your settings. A request’s general pages block replaces the configuration’s general block; keep deployment-wide values in the pdf-parser overlay.

  • Trusting AUTO on scanner output. A scanner’s invisible OCR layer can pass the character thresholds and still be wrong; use EXTRACT_AND_OCR or OCR for such sources.

  • Multi-column text with sortByPosition. It can interleave columns. Test on the producer’s files first.

  • Headers and footers in the index. For tagged PDFs, drop <div class="artifact">; for untagged PDFs Tika cannot tell furniture from body text.

  • Passwords in logs. Read them from the environment or a secret store, and do not copy them into Metadata.

  • An old tika-core. Upgrading only the PDF module does not fix CVE-2025-54988 (that is CVE-2025-66516). Align every Tika artifact with the BOM.

  • Huge pages. A large-format page at 300 dpi can exceed maxImagePixels and be skipped; look for tk:rendering:failed-page.

What was verified and what was not

Verified on 2026-10-12 against Tika 4.1.0 on Java 25 (PDFBox 3.0.8 resolved by Maven):

  • every program on this page, with its output as shown (lightly trimmed only where stated);

  • the PDFParserConfig, MarkedContentConfig, PagesConfig, RenderSettings, TextPolicy and AccessCheckMode APIs, read from the 4.1.0 jars with javap;

  • the JSON keys and defaults, from the 4.1.x PDF parser and pages documentation, and by loading each configuration on this page with TikaLoader;

  • the three accessCheckMode values against members-catalogue.pdf, ifXFAExtractOnlyXFA against loan-form-xfa.pdf, extractInlineImages with maxPages against the loan form, and the XXE probe;

  • the CVE descriptions, dates and affected versions, from the CVE records.

Not verified:

  • OCR output: no Tesseract or VLM engine was installed, so no transcription is shown and EXTRACT_AND_OCR with a real engine was not run;

  • the cost figures for OCR (seconds per page) are general guidance, not measurements;

  • parseIncrementalUpdates, extractActions, detectAngles, suppressDuplicateOverlappingText, sortByPosition and throwOnEncryptedPayload were not exercised on purpose-built files;

  • the 3.x XML block and the 1.x and 2.x history were not run;

  • per-request configuration through tika-server and Pipes is described from the documentation only.