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 |
|
Artifact |
|
PDF library |
Apache PDFBox 3.0.8 ( |
Component name in |
|
Options class |
|
Pages policy class |
|
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 ( |
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 |
|---|---|---|---|
|
194, 161 |
0, 0 |
Two born-digital pages. |
|
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">. Becausecatalogue.pdfis 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 inaltName. -
The attachment
errata.pdfis an embedded document. With the defaultParseContext,AutoDetectParserparses it inline, so its text appears inside the container’s XHTML after an<div class="embedded">marker. With theRecursiveParserWrapperor/rmetait is a separate metadata object withtk:embedded-resource-type = ATTACHMENT,tk:embedded-resource-path = /errata.pdfandpdf:embedded-file-description. See Embedded Documents & Recursive Parsing. -
pdf:incremental-update-count = 1and twopdf:eof-offsetsvalues: PDFBox wrote the form field value as an incremental update when the fixture was built, so the file has two%%EOFmarkers. -
The
dcterms:createdandpdf:docinfo:createdvalues 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 |
|---|---|
|
Page count. |
|
PDF version from the header ( |
|
Raw values of the information dictionary ( |
|
Whether the document has an XMP metadata stream. |
|
One value per page; the input to the |
|
Document totals of glyphs without a Unicode mapping. A high value means the text layer is unreliable. |
|
How many pages tripped the OCR verdict, whether or not an engine ran. |
|
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). |
|
Whether the file is encrypted, and each permission flag ( |
|
Whether the PDF has an AcroForm, an XFA form or is a PDF portfolio. |
|
Annotation subtypes found ( |
|
Font facts. A damaged font often explains garbled text. |
|
How many times the file was updated in place, and where each revision ends. |
|
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.
Source: PDFParserConfig, Javadoc 3.3.2,
Configuring Tika, Apache Tika 3.3.2 and
TikaOCR, Apache Tika wiki (3.x-era page).
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 |
|---|---|---|
|
|
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. |
|
|
Let PDFBox estimate where spaces belong. Most PDFs do not draw explicit space glyphs, so turning this off usually glues words together. |
|
|
PDFBox character-width tolerance for inserting spaces. |
|
|
PDFBox space-width tolerance for inserting spaces. |
|
|
Line-height multiple that starts a new paragraph. |
|
|
Ignore space glyphs drawn in the content stream and rely on the spacing algorithm (PDFBOX-3774). |
|
|
Remove text drawn twice over the same place (fake bold). Can be slow on large pages. |
|
|
Detect and correct rotated text runs within a page. Costs an extra pass per page; it also overrides |
|
|
Record the font names used in metadata. |
|
see below |
Tagged PDF handling: |
Annotations, outline, forms and actions
| Key | Default | What it does |
|---|---|---|
|
|
Write annotation text (comments, form-field captions) into |
|
|
Write the outline as a |
|
|
Write AcroForm field names and values into |
|
|
If the PDF has an XFA form, write only the XFA data and skip the page text. |
|
|
Extract PDF actions; JavaScript becomes embedded documents. Useful for security triage. |
Images and page rendering
| Key | Default | What it does |
|---|---|---|
|
|
Extract the images in the content streams as embedded documents ( |
|
|
Emit each image (by object id) only once, even if many pages draw it, such as a logo. |
|
|
Record inline-image metadata without extracting the bytes. Faster than |
|
none |
Expert: a class implementing |
|
|
What PDFBox draws when it renders a page for OCR: |
|
see below |
The 4.1 pages block: text source, rendering, OCR budget and verdict, inference release, page images. It overlays the general |
|
|
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 |
|---|---|---|
|
|
Maximum number of pages to process; |
|
|
How much of the PDF PDFBox may hold in memory (512 MB) before it buffers to a temporary file. |
|
|
Collect a per-stream |
|
|
Whether to enforce the PDF’s extraction permissions: |
|
|
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 |
|---|---|---|
|
|
Record whether the file has incremental updates and how many ( |
|
|
Parse each earlier revision as an embedded document. Useful for forensics: text that was "deleted" in a later revision is still in the file. |
|
|
Maximum number of revisions to parse when |
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 |
|---|---|
|
Follow the tree on each page that passes the quality gate; write the other pages as the stripper would. |
|
Follow the tree on every page that has tagged text. |
|
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 |
|---|---|
|
Where does a page’s text come from? |
|
How is a page turned into an image (for OCR, for inference, for emitted images)? |
|
How many pages may be OCR’d, and when does |
|
What does the parser release to the inference bindings? (Experimental in 4.1.0.) |
|
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 |
|---|---|
|
The content stream only. Pages are never rendered for OCR. |
|
The content stream, plus OCR of each page whose extracted text is poor by the |
|
Both sources, on every page. |
|
The engine’s output only; the content stream is not read. Fails the parse when no engine covers the render type. |
|
No text from any source. Metadata, attachments, page renders, inference and emission still happen. |
render: how a page becomes an image
| Key | Default | Meaning |
|---|---|---|
|
|
Target resolution; 300 suits Tesseract. |
|
|
A box the image must fit: scaled down to fit, never enlarged. A thumbnail is a box, not a resolution. |
|
|
A page smaller than this at the target dpi is not rendered at all, with no warning. |
|
|
|
|
|
|
|
|
ImageIO quality: fidelity for JPEG, an inverted effort knob for PNG. |
|
|
Skip a page that would still be larger than this many pixels, with a warning and the page number in |
ocr: the recognizer’s budget and verdict
| Key | Default | Meaning |
|---|---|---|
|
|
Pages of one document that may be OCR’d, counted from the first. |
|
|
Under |
|
|
Under |
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 |
|---|---|---|
|
|
Emit each page’s render at the end of the page. |
|
|
Pages emitted, counted from the first; also bounded by |
|
|
Deepest embedded level whose pages are emitted: |
|
|
Emit only for documents embedded as one of these |
|
|
An overlay on |
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:
-
parse-context.pagesintika-config.json: the default for every rendering parser. -
pagesin a request’s parse-context (or a server preset). It replaces the configuration’s general block. -
pagesinside thepdf-parserentry underparsers: the PDF overlay, kept across requests. -
pagesinside a request’spdf-parserentry: 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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
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) |
|
No rendering cost, and no OCR engine needed. |
Mixed born-digital and scans (a typical intake service) |
|
OCR only the pages that need it. |
Scans with an unreliable embedded OCR layer |
|
Keeps both; the index sees the engine’s text even when the layer has enough characters to pass |
Image-only archives, or a layer you know is wrong |
|
One source, no duplicated text. Fails loudly if the engine is missing. |
Thumbnails, embeddings or page annotators only |
|
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-recognizerswhoseenabledisfalse; thenOCRdoes 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.textset toAUTO,OCRorEXTRACT_AND_OCR): the parser renders the whole page and passes the image to the recognizer. -
Inline images (
extractInlineImagesset totrue): each image in the content stream becomes an embedded document,image-parserparses 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.maxPagescaps the pages read at all;pages.ocr.maxPagescaps the pages OCR’d. -
pages.render.maxImagePixelsskips 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 |
|---|---|---|
|
Extract regardless of the permission flags. |
Text extracted. |
|
Refuse when extraction is forbidden, unless the extract-for-accessibility flag is set. |
Text extracted (the accessibility flag is set). |
|
Refuse whenever content extraction is forbidden. |
|
{
"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.
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 |
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 |
Affected artifacts and versions, as listed in the CVE records:
| Artifact | Affected | Safe |
|---|---|---|
|
1.13 to 3.2.1 |
3.2.2 and later, including all 4.x |
|
2.0.0 to 3.2.1 |
3.2.2 and later, including all 4.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:
-
Upgrade both
tika-coreand the parser modules to 3.2.2 or later (3.3.x, or 4.1.x). Importtika-bom(or usetika-parsers-standard-package) so every Tika artifact has the same version, and check the resolved versions withmvn dependency:treeorgradle dependencies. A staletika-corepulled in by another library (a search client, an ingestion framework) is exactly the case CVE-2025-66516 describes. -
Rebuild container images;
apache/tikaimages for 3.2.2 and later carry the fix. -
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.
-
Do not rely on
pdf:has-xfaas 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.
Source: CVE-2025-54988, CVE-2025-66516 and Apache Tika Security.
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 ( |
Cheap: proportional to the number and complexity of pages. |
Structure tree ( |
A second look at each page’s tree; small next to the text pass. |
|
Extra passes or sorting per page; turn them on for producers that need them, not globally. |
Inline images ( |
Decoding every image, plus the image parser and any OCR. |
Page renders (OCR, |
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. |
OCR |
Seconds per page; the dominant cost of any scanned corpus. |
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
PDFTextStripperis 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 |
|---|---|
Objects, content streams, fonts and incremental updates: why |
|
Extracting text with iText’s strategies when you need positions or regions. |
|
AcroForm versus XFA, fields, widgets and flattening. |
|
PDFBox, the library under |
|
User and owner passwords and the permission flags Tika reports. |
|
How tagged, accessible PDFs are produced: the structure trees |
|
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) |
|
2.x |
|
3.x |
Java 11, PDFBox 3. Same |
4.0 |
Java 17, JSON configuration ( |
4.1 |
The |
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
parserslist with onlypdf-parserloads only the PDF parser. Keepdefault-parser. -
Expecting OCR without an engine.
AUTOwith no engine silently behaves asEXTRACT. Watchpdf:ocr-page-countversus the text you got, or name an engine intext-recognizersso a missing engine fails at start-up. -
OCR’ing twice. Page OCR plus
extractInlineImagesreads every scanned page twice. -
A request that drops your settings. A request’s general
pagesblock replaces the configuration’s general block; keep deployment-wide values in thepdf-parseroverlay. -
Trusting
AUTOon scanner output. A scanner’s invisible OCR layer can pass the character thresholds and still be wrong; useEXTRACT_AND_OCRorOCRfor 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
maxImagePixelsand be skipped; look fortk: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,TextPolicyandAccessCheckModeAPIs, read from the 4.1.0 jars withjavap; -
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
accessCheckModevalues againstmembers-catalogue.pdf,ifXFAExtractOnlyXFAagainstloan-form-xfa.pdf,extractInlineImageswithmaxPagesagainst 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_OCRwith a real engine was not run; -
the cost figures for OCR (seconds per page) are general guidance, not measurements;
-
parseIncrementalUpdates,extractActions,detectAngles,suppressDuplicateOverlappingText,sortByPositionandthrowOnEncryptedPayloadwere 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.
References
-
Pages: rendering, OCR policy, emitted page images, Apache Tika 4.1.x
-
Configuration (including the text-recognizers list), Apache Tika 4.1.x
-
PDFParser,PDFParserConfig,PagesConfigandPasswordProvider, Javadoc 4.1.0 -
PDFParserConfig, Javadoc 3.3.2 and Configuring Tika, Apache Tika 3.3.2 -
TikaOCR, Apache Tika wiki (3.x-era)
-
Chris A. Mattmann and Jukka L. Zitting, Tika in Action, Manning, 2011, chapter 8 (parser families, including the PDFBox-based PDF parser of the 1.x line)