Text Extraction & 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. |
A PDF is a page description, not a text document: it says "draw these glyphs at these coordinates with this font", and
it does not record paragraphs, lines or reading order. Reading text back out of a PDF is therefore a parsing problem. The
reader walks the content stream of each page, interprets the same operators a viewer would, and reassembles the glyphs
it meets into strings. This page shows how iText 9.8 does that, from the one-line PdfTextExtractor call to custom event
listeners that see every text chunk and image a page draws. Versions are stated as of 2026-10-09: iText Core 9.8.0 on
Java 21. All the examples were compiled and run against exactly that version.
The running scenario is the Bookshelf Billing invoice. Extraction is useful there in three places: a regression test that asserts the invoice the service just produced really contains the invoice number and the total; an intake job that reads supplier invoices received as PDFs; and a search over the archive of generated documents. How a PDF is laid out inside is explained in How a PDF File Works, and how the sample invoice is built is described in Tables and Document Lifecycle.
iText does extract text
Books written for iText 1 and 2, including the classic 2006 text iText in Action (consulted for the concept order of this
page and not reproduced), state that iText is a library for creating and manipulating PDF and does not extract text
(finding O13 of this section’s source analysis). That statement has been wrong for many years, as the Knowledge Base
article Parsing PDFs and the
PdfTextExtractor API
show. Text parsing arrived with
iText 5 and is a first-class feature of iText 7, 8 and 9: the com.itextpdf.kernel.pdf.canvas.parser package contains the
content-stream interpreter (PdfCanvasProcessor), the event data types (TextRenderInfo, ImageRenderInfo,
PathRenderInfo), the ready-made extraction strategies, and the filters that restrict extraction to a region or a
layer. The current approach is:
-
for plain text,
PdfTextExtractor.getTextFromPage(page); -
for a different ordering or for positions, an explicit strategy:
SimpleTextExtractionStrategy,LocationTextExtractionStrategyorRegexBasedLocationExtractionStrategy; -
for anything else (fonts, colours, images, positions per chunk), your own
IEventListenerattached to aPdfCanvasProcessor; -
for removing text from a document rather than reading it, the commercial-or-AGPL pdfSweep add-on, which is described in the last section of this page.
The extraction classes need only the iText Core kernel module. They are licensed like the rest of iText Core, under the AGPLv3 or under a commercial licence. The Maven coordinates, restated here so that this page is complete on its own, are:
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-core</artifactId>
<version>9.8.0</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>bouncy-castle-adapter</artifactId>
<version>9.8.0</version>
</dependency>
The Bouncy Castle adapter is only needed when the input may be encrypted or signed; plain extraction works without it. See iText Architecture & Modules for the module map.
Extracting the text of a page
PdfTextExtractor.getTextFromPage(PdfPage) returns the text of one page as a string. Pages are numbered from 1 and the
document is opened through a PdfReader, exactly as for any other read access. The examples below share a small helper,
InvoiceSamples.invoice(), that builds the sample invoice in memory so that every listing can be compiled and run as is.
package com.example.billing;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.layout.properties.UnitValue;
import java.io.ByteArrayOutputStream;
/** Builds the sample Bookshelf Billing invoice used by the extraction examples. */
final class InvoiceSamples {
private InvoiceSamples() {
}
static byte[] invoice() {
var out = new ByteArrayOutputStream();
try (var pdf = new PdfDocument(new PdfWriter(out));
var document = new Document(pdf)) {
document.add(new Paragraph("Bookshelf Billing").setFontSize(18));
document.add(new Paragraph("Invoice INV-2026-0042"));
document.add(new Paragraph("Customer: Example Reader, 1 Sample Street"));
var table = new Table(UnitValue.createPercentArray(new float[] {5, 1, 2, 2}))
.useAllAvailableWidth();
for (var h : new String[] {"Title", "Qty", "Unit", "Total"}) {
table.addHeaderCell(new Cell().add(new Paragraph(h)));
}
addLine(table, "Learning Java", "2", "30.00", "60.00");
addLine(table, "PDF Basics", "1", "25.50", "25.50");
document.add(table);
document.add(new Paragraph("Total due: 85.50 EUR").setTextAlignment(TextAlignment.RIGHT));
document.add(new Paragraph("Order references: ORD-1001 and ORD-1002"));
}
return out.toByteArray();
}
private static void addLine(Table table, String... values) {
for (var v : values) {
table.addCell(new Cell().add(new Paragraph(v)));
}
}
}
The extractor comes in two flavours, chosen by the second parameter of getTextFromPage:
-
LocationTextExtractionStrategyis the default. It collects every text chunk with its position and baseline, sorts the chunks into lines from top to bottom and left to right, and inserts a space between chunks that are apart by more than a space would be wide. It is the right choice for most documents, because the order in which a producer happened to draw the text is often not the reading order. The sorted result for tables is a line per row, with the cells separated by spaces. -
SimpleTextExtractionStrategyemits the text in the order the content stream draws it. It is faster and uses less memory, because nothing is kept for sorting, and it is the right choice for documents you generated yourself, where drawing order is reading order. It will scramble documents whose producer draws the text out of order (for instance a page footer drawn first) and it will miss a space between two chunks that were positioned with no space glyph between them.
For the sample invoice both strategies give the same text, because iText drew it top to bottom. The listing prints both and
compares the default with an explicitly created LocationTextExtractionStrategy:
package com.example.billing;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor;
import com.itextpdf.kernel.pdf.canvas.parser.listener.LocationTextExtractionStrategy;
import com.itextpdf.kernel.pdf.canvas.parser.listener.SimpleTextExtractionStrategy;
import java.io.ByteArrayInputStream;
import java.io.IOException;
public class ExtractText {
public static void main(String[] args) throws IOException {
byte[] pdfBytes = InvoiceSamples.invoice();
try (var pdf = new PdfDocument(new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
for (int i = 1; i <= pdf.getNumberOfPages(); i++) {
var page = pdf.getPage(i);
// Default strategy: LocationTextExtractionStrategy
String sorted = PdfTextExtractor.getTextFromPage(page);
// Content-stream order
String raw = PdfTextExtractor.getTextFromPage(page, new SimpleTextExtractionStrategy());
// Explicit location strategy
String located = PdfTextExtractor.getTextFromPage(page, new LocationTextExtractionStrategy());
System.out.println("--- page " + i + " (default) ---");
System.out.println(sorted);
System.out.println("--- page " + i + " (simple) ---");
System.out.println(raw);
System.out.println("same as location: " + sorted.equals(located));
}
}
}
}
Running it prints the eight lines of the invoice twice (for example Invoice INV-2026-0042, then Title Qty Unit Total
for the table header, then Total due: 85.50 EUR), followed by same as location: true. Each call creates a new
strategy and processes the page again; if you need several views of the same page, parse once with your own listener
(next sections) rather than calling getTextFromPage repeatedly. Do not share one strategy instance between pages or
between threads: the strategies accumulate state for the page they are processing. The documents themselves are read-only
when opened with a PdfReader only, so several threads may each open their own reader over the same bytes.
The listing derives from the iText knowledge base articles
Parsing PDFs and
Content parsing, extraction and redaction of text,
and from the API pages of
PdfTextExtractor
and
LocationTextExtractionStrategy.
The knowledge base tutorials were written for earlier iText 7 releases and some show methods that have since been renamed;
the listings on this page are the ones compiled against 9.8.0, and where they differ the compiled version is the reference.
Finding text and its position with a regular expression
Often you do not need all the text but the location of a pattern: where the invoice number is, so that a stamp can be
placed next to it, or which rectangle to redact. RegexBasedLocationExtractionStrategy takes a Java regular expression,
processes the page, and returns an IPdfTextLocation for every match, with the matched text and its bounding
Rectangle in default user space (points, origin at the bottom left of the page, see
How a PDF File Works). The strategy is a listener, so it is attached to
a PdfCanvasProcessor that interprets the page content. In iText 9 the location object does not know its page; the page
number is the one you passed to the processor, so the loop below keeps it itself.
package com.example.billing;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.listener.RegexBasedLocationExtractionStrategy;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Locale;
public class FindWithRegex {
public static void main(String[] args) throws IOException {
byte[] pdfBytes = InvoiceSamples.invoice();
try (var pdf = new PdfDocument(new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
for (int i = 1; i <= pdf.getNumberOfPages(); i++) {
var strategy = new RegexBasedLocationExtractionStrategy("(INV|ORD)-\\d{4}(-\\d{4})?");
new PdfCanvasProcessor(strategy).processPageContent(pdf.getPage(i));
for (var location : strategy.getResultantLocations()) {
var r = location.getRectangle();
System.out.printf(Locale.ROOT, "page %d: %s at x=%.1f y=%.1f w=%.1f h=%.1f%n",
i, location.getText(), r.getX(), r.getY(),
r.getWidth(), r.getHeight());
}
}
}
}
}
For the sample invoice the output lists INV-2026-0042 near the top of the page and the two ORD- references near the
foot of the table; each line shows the rectangle as x, y, w and h in points. The order of the results is not
guaranteed to be the reading order, so sort them if you need that. Two further details matter in practice. First, a match is
computed on the text that the strategy reassembled, which means a pattern cannot match across a line break unless the
regular expression allows for the newline. Second, the rectangle covers the glyph boxes of the match, which makes it a
good input for annotations and for pdfSweep redaction areas, but it is not pixel-exact for kerned or rotated text.
The listing derives from the API page of
RegexBasedLocationExtractionStrategy
and from the knowledge base article Parsing PDFs.
Extracting a region of the page
Structured documents such as invoices keep the data you want in a known place. Instead of extracting the whole page and
parsing it, restrict extraction to a rectangle. A FilteredEventListener wraps one or more delegate listeners and lets an
event through to a delegate only when the delegate’s filters accept it. TextRegionEventFilter accepts text chunks whose
baseline (the line segment from the start to the end of the chunk) intersects the rectangle. The attachEventListener method returns the delegate you gave it, so that you can
read the delegate’s result afterwards.
package com.example.billing;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.filter.TextRegionEventFilter;
import com.itextpdf.kernel.pdf.canvas.parser.listener.FilteredEventListener;
import com.itextpdf.kernel.pdf.canvas.parser.listener.ITextExtractionStrategy;
import com.itextpdf.kernel.pdf.canvas.parser.listener.LocationTextExtractionStrategy;
import java.io.ByteArrayInputStream;
import java.io.IOException;
public class ExtractRegion {
/** Extracts the text found inside a rectangle given in default user space units (points). */
static String textInRegion(PdfDocument pdf, int pageNumber, Rectangle region) {
var filter = new TextRegionEventFilter(region);
var listener = new FilteredEventListener();
ITextExtractionStrategy strategy = listener.attachEventListener(new LocationTextExtractionStrategy(), filter);
new PdfCanvasProcessor(listener).processPageContent(pdf.getPage(pageNumber));
return strategy.getResultantText();
}
public static void main(String[] args) throws IOException {
try (var pdf = new PdfDocument(new PdfReader(new ByteArrayInputStream(InvoiceSamples.invoice())))) {
var pageSize = pdf.getPage(1).getPageSize();
// Top tenth of the page: only the heading line of the sample invoice
var top = new Rectangle(0, pageSize.getHeight() * 0.9f, pageSize.getWidth(), pageSize.getHeight() * 0.1f);
System.out.println(textInRegion(pdf, 1, top));
}
}
}
The rectangle is in the same user-space coordinates as before, in points, and the page size is read from the page itself,
so the code also works for A4 and for Letter pages. Remember that a page may carry a /Rotate entry and a CropBox
that differ from the media box. The filter works in unrotated user space; if your input comes from a scanner or from
several producers, test it with a rotated page. The filter keeps a chunk when any part of its baseline is inside the
region, so a line whose glyphs reach into the rectangle but whose baseline lies just below it is dropped; choose a rectangle
with a margin that contains the baselines rather than one that cuts through a line of text.
The listing derives from the API pages of
FilteredEventListener
and
TextRegionEventFilter.
Custom listeners on PdfCanvasProcessor
Behind every strategy is the same mechanism. PdfCanvasProcessor interprets the content stream and, whenever something
observable happens, it creates an event object and passes it to an IEventListener. The listener declares which event
types it wants through getSupportedEvents() and receives them in eventOccurred(IEventData, EventType). The event types
of interest are:
-
EventType.RENDER_TEXT, withTextRenderInfo: the text of a chunk, its font, font size, colour, baseline and per-character boxes (getCharacterRenderInfos()); -
EventType.RENDER_IMAGE, withImageRenderInfo: an image XObject or inline image and the matrix it is drawn with; -
EventType.RENDER_PATH,CLIP_PATH_CHANGED,BEGIN_TEXTandEND_TEXT, for vector graphics, clipping and text objects.
Any of the ready-made strategies is just a listener that happens to implement the ITextExtractionStrategy interface, so
you can write the same kind of class for any question you want to ask of a page. The listener below answers "which fonts
does this document use, and how much text is set in each?", which is a common input to the question of why a PDF/A
validation failed or why the file is so large. It uses a Java 21 record for the accumulated usage, a pattern-matching
instanceof, and Map.merge.
package com.example.billing;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/** A custom listener that records, per font, how many text chunks and characters were drawn. */
public class FontReport implements IEventListener {
record Usage(int chunks, int characters) {
Usage plus(String text) {
return new Usage(chunks + 1, characters + text.length());
}
}
private final Map<String, Usage> usageByFont = new LinkedHashMap<>();
@Override
public void eventOccurred(IEventData data, EventType type) {
if (type == EventType.RENDER_TEXT && data instanceof TextRenderInfo info) {
var font = info.getFont().getFontProgram().getFontNames().getFontName();
usageByFont.merge(font, new Usage(1, info.getText().length()),
(old, ignored) -> old.plus(info.getText()));
}
}
@Override
public Set<EventType> getSupportedEvents() {
return Set.of(EventType.RENDER_TEXT);
}
public static void main(String[] args) throws IOException {
var report = new FontReport();
try (var pdf = new PdfDocument(new PdfReader(new ByteArrayInputStream(InvoiceSamples.invoice())))) {
var processor = new PdfCanvasProcessor(report);
for (int i = 1; i <= pdf.getNumberOfPages(); i++) {
processor.processPageContent(pdf.getPage(i));
}
}
report.usageByFont.forEach((font, usage) ->
System.out.println(String.format(Locale.ROOT, "%s: %d chunks, %d characters",
font, usage.chunks(), usage.characters())));
}
}
For the sample invoice it prints a single line, Helvetica, with the number of chunks and characters, since the document
uses the standard font that iText applies to a Paragraph by default. Run the same listener over a document that
embeds subset fonts and the names come back with a six-letter subset prefix and a plus sign (for example
ABCDEF+DejaVuSans). The fonts themselves are explained in Fonts & Unicode.
The PdfCanvasProcessor works on any content stream, not just on whole pages: it can be pointed at a form XObject, an
annotation appearance or a Type 3 glyph procedure, which is how tools that audit documents obtain, for example, the text
inside stamps. Invoke processPageContent(PdfPage) for a page or processContent(byte[], PdfResources) for a raw stream.
A processor is meant to be used for one document at a time; create a new one per document, and per thread.
The listing derives from the API page of
PdfCanvasProcessor
and from the knowledge base article Parsing PDFs.
Reading a value out of a generated invoice
The most common use in a service is a typed answer: given the bytes of an invoice, return the invoice number and the total.
The class below combines the pieces seen so far with a Java 21 record, Optional, precompiled patterns and a
ReaderProperties object. Passing the password through ReaderProperties is how iText opens a file that is protected
with a user password; it is read from configuration or the environment by the caller and never written into source code.
An encrypted input also requires the Bouncy Castle adapter on the classpath; encryption itself is the subject of a later
page of this section on encryption and permissions.
package com.example.billing;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.ReaderProperties;
import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.Optional;
import java.util.regex.Pattern;
public class InvoiceTotalReader {
record InvoiceSummary(String number, BigDecimal total) {
}
private static final Pattern NUMBER = Pattern.compile("Invoice (INV-\\d{4}-\\d{4})");
private static final Pattern TOTAL = Pattern.compile("Total due: (\\d+\\.\\d{2}) EUR");
static Optional<InvoiceSummary> read(InputStream in, byte[] ownerOrUserPassword) throws IOException {
// The password comes from configuration, never from source code; null means "not encrypted"
var properties = new ReaderProperties();
if (ownerOrUserPassword != null) {
properties.setPassword(ownerOrUserPassword);
}
try (var pdf = new PdfDocument(new PdfReader(in, properties))) {
var text = new StringBuilder();
for (int i = 1; i <= pdf.getNumberOfPages(); i++) {
text.append(PdfTextExtractor.getTextFromPage(pdf.getPage(i))).append('\n');
}
var number = NUMBER.matcher(text);
var total = TOTAL.matcher(text);
if (number.find() && total.find()) {
return Optional.of(new InvoiceSummary(number.group(1), new BigDecimal(total.group(1))));
}
return Optional.empty();
}
}
public static void main(String[] args) throws IOException {
System.out.println(read(new ByteArrayInputStream(InvoiceSamples.invoice()), null));
}
}
For the sample invoice it prints Optional[InvoiceSummary[number=INV-2026-0042, total=85.50]]. The same method can be used in
a test: generate the invoice, read the bytes back with this class, and assert the number and the total. Keep text assertions
narrow (a number, a total) rather than comparing a whole page of extracted text, because extraction output depends on
line-breaking and font metrics and changes when the layout does. Whole-document regression testing, with a visual
and structural comparison of the PDF, is covered in the page on testing and performance of this section. Money is
parsed with BigDecimal, never double.
The ReaderProperties behaviour is documented on the API page
ReaderProperties.
What extraction cannot do
Text extraction reports what the file says; it does not recognise anything. The following limits recur in practice.
-
Scanned documents. A page that holds only a raster image has no text operators at all. The extractor returns an empty string, and the way to detect this is exactly that: nothing came back. The listing below makes the check explicit (
looksScanned) and counts the images drawn on the page through aRENDER_IMAGElistener. To obtain text from such pages, an optical character recognition step is needed. The open-source-or-commercial pdfOCR add-on of iText runs Tesseract or an ONNX engine over the image and produces a searchable PDF, as described in its installation guide and on the pdfOCR product page; it is licensed under the AGPLv3 or under a commercial licence and is not part of iText Core. It is introduced together with the other add-ons in a later page of this section. -
Text drawn as outlines or images. Some producers convert text to vector paths, or print it into a bitmap. The file then contains shapes, not characters, and extraction returns nothing for them.
-
Missing or wrong Unicode mapping. A glyph is shown by a code in the font’s encoding. To turn the code back into a character, iText uses the font’s
ToUnicodemap or a known encoding. Fonts embedded without aToUnicodemap can yield garbage characters, question marks or private-use code points even though the page looks right. Documents generated by iText from Unicode fonts always carry the map (see Fonts & Unicode). -
Tables and columns. A PDF has no table. The location strategy turns each visual row into a line and separates cells by spaces, so a cell with a wrapped text spreads over several lines and merges with its neighbours' lines. For tables, use a region filter per column, or position-based logic in your own listener based on the
xcoordinate of each chunk. -
Right-to-left and complex scripts. The extractor returns the characters in the logical order that the file supplies, and ligatures or shaped glyphs depend on the producer’s
ToUnicodeorActualTextdata. Complex-script shaping on the creation side needs the commercial pdfCalligraph add-on, as explained in the fonts page. -
Hidden and layered content. Text in optional content groups that are switched off in the viewer is still extracted unless your listener checks the layer, and text behind an opaque shape is extracted though not visible. Do not use extraction to decide what a reader can see.
package com.example.billing;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ScanDetector {
/** True when no page of the document yields any non-blank text. */
static boolean looksScanned(InputStream in) throws IOException {
try (var pdf = new PdfDocument(new PdfReader(in))) {
for (int i = 1; i <= pdf.getNumberOfPages(); i++) {
if (!PdfTextExtractor.getTextFromPage(pdf.getPage(i)).isBlank()) {
return false;
}
}
return true;
}
}
public static void main(String[] args) throws IOException {
System.out.println("invoice looks scanned: " + looksScanned(new ByteArrayInputStream(InvoiceSamples.invoice())));
}
}
package com.example.billing;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.ImageRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/** A page that holds only a raster image has nothing to extract: it needs OCR. */
public class ScannedPage {
public static void main(String[] args) throws IOException {
var image = new BufferedImage(200, 100, BufferedImage.TYPE_INT_RGB);
var png = new ByteArrayOutputStream();
javax.imageio.ImageIO.write(image, "png", png);
var out = new ByteArrayOutputStream();
try (var pdf = new PdfDocument(new PdfWriter(out)); var doc = new Document(pdf)) {
doc.add(new Image(ImageDataFactory.create(png.toByteArray())));
}
byte[] scanned = out.toByteArray();
System.out.println("looks scanned: " + ScanDetector.looksScanned(new ByteArrayInputStream(scanned)));
var images = new AtomicInteger();
IEventListener counter = new IEventListener() {
@Override
public void eventOccurred(IEventData data, EventType type) {
if (data instanceof ImageRenderInfo) {
images.incrementAndGet();
}
}
@Override
public Set<EventType> getSupportedEvents() {
return Set.of(EventType.RENDER_IMAGE);
}
};
try (var pdf = new PdfDocument(new PdfReader(new ByteArrayInputStream(scanned)))) {
new PdfCanvasProcessor(counter).processPageContent(pdf.getPage(1));
}
System.out.println("images drawn on page 1: " + images.get());
}
}
The first listing returns false for the sample invoice; the second builds a PDF whose only content is a PNG image,
reports looks scanned: true, and counts one image drawn on page 1. In a real intake service, route documents for which
looksScanned is true to an OCR queue, and only the others to text parsing. The OCR output of pdfOCR is itself a PDF
with an invisible text layer, so after OCR the same extraction code works on the result.
The listings derive from the API pages of
PdfTextExtractor and
PdfCanvasProcessor.
Extraction is not redaction
Reading text and removing text are different operations, and the second is much harder than it looks. Drawing a black
rectangle over a number hides it from the eye but leaves the glyphs in the content stream, where the extractor in this page
finds them. Real redaction has to remove the text operators, any image pixels under the area, and any metadata or
annotations that repeat the content. In iText this is the job of the pdfSweep add-on (PdfCleaner and the pattern
strategies such as RegexBasedCleanupStrategy), which is licensed under the AGPLv3 or under a commercial licence and is
published as the cleanup artifact. It is described in the knowledge base article
Removing content with pdfSweep and on the
pdfSweep product page, and it is covered with the other add-ons in
a later page of this section. A good redaction test is to run the extraction code of this page over the result and assert
that the removed string is gone.
A related distinction applies to permissions. A PDF that forbids copying text through its encryption flags can still be
read by PdfTextExtractor: owner-password permissions are advisory for a reader that is willing to ignore them, a point
explored in the encryption page of this section.
Choosing an approach
| Need | Use | Notes |
|---|---|---|
All the text of a page, in reading order |
|
Uses |
Text of a document you generated, fast |
|
Content-stream order. No sorting, lower memory. |
Position of a pattern such as an invoice number |
|
Returns text and rectangle. Page number is yours to track. |
Only a part of the page |
|
User-space rectangle in points. Mind rotation and cropbox. |
Fonts, colours, per-character boxes, images, paths |
Own |
|
Text from scanned pages |
OCR first (pdfOCR, AGPLv3 or commercial) |
Then extract from the searchable result. |
Remove text |
pdfSweep (AGPLv3 or commercial) |
Never a rectangle drawn on top. |
References
-
iText Knowledge Base, Parsing PDFs
-
iText Knowledge Base, Content parsing, extraction and redaction of text
-
iText Knowledge Base, Removing content with pdfSweep
-
iText Knowledge Base, Installing iText pdfOCR for Java developers
-
iText Knowledge Base, Breaking changes for iText 9
-
iText API,
PdfTextExtractor -
iText API,
PdfCanvasProcessor -
iText API,
LocationTextExtractionStrategy -
iText API,
RegexBasedLocationExtractionStrategy -
iText API,
FilteredEventListener -
iText API,
TextRegionEventFilter -
iText API,
ReaderProperties -
Bruno Lowagie, iText in Action (Manning, 2006), consulted for concept order only; it predates iText 7 and says iText does not extract text