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, LocationTextExtractionStrategy or RegexBasedLocationExtractionStrategy;

  • for anything else (fonts, colours, images, positions per chunk), your own IEventListener attached to a PdfCanvasProcessor;

  • 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:

  • LocationTextExtractionStrategy is 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.

  • SimpleTextExtractionStrategy emits 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, with TextRenderInfo: the text of a chunk, its font, font size, colour, baseline and per-character boxes (getCharacterRenderInfos());

  • EventType.RENDER_IMAGE, with ImageRenderInfo: an image XObject or inline image and the matrix it is drawn with;

  • EventType.RENDER_PATH, CLIP_PATH_CHANGED, BEGIN_TEXT and END_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 a RENDER_IMAGE listener. 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 ToUnicode map or a known encoding. Fonts embedded without a ToUnicode map 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 x coordinate 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 ToUnicode or ActualText data. 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

PdfTextExtractor.getTextFromPage(page)

Uses LocationTextExtractionStrategy. Sorted by position.

Text of a document you generated, fast

SimpleTextExtractionStrategy

Content-stream order. No sorting, lower memory.

Position of a pattern such as an invoice number

RegexBasedLocationExtractionStrategy

Returns text and rectangle. Page number is yours to track.

Only a part of the page

FilteredEventListener with TextRegionEventFilter

User-space rectangle in points. Mind rotation and cropbox.

Fonts, colours, per-character boxes, images, paths

Own IEventListener on PdfCanvasProcessor

RENDER_TEXT, RENDER_IMAGE, RENDER_PATH and others.

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