OCR Redaction in Java: Tess4J and Apache PDFBox
Java OCR redaction uses Tess4J and Apache PDFBox together. Tess4J is a Java JNA wrapper for the Tesseract OCR engine native binary, while Apache PDFBox renders PDF pages to BufferedImage objects that Tesseract can process.1 Redaction in PDFBox works by drawing a filled black rectangle in the content stream over the target region, then appending a blank text object at the same position to replace the underlying text in the PDF structure.
This combination represents the most common enterprise Java OCR redaction pattern as of 2026. Tess4J handles character extraction with bounding box data, PDFBox handles PDF rendering and modification, and the integration layer correlates OCR bounding boxes to PDF coordinate space for precise rectangle placement.
Setting up Tess4J for OCR in Java
Add the tess4j dependency to your Maven or Gradle build file. Tess4J requires the Tesseract native binary and language data files installed on the system or provided as a bundled resource. Set the tessdata path using instance.setDatapath() pointing to your tessdata directory containing eng.traineddata. On Linux servers, install the Tesseract native package through your distribution's package manager and point the tessdata path at /usr/share/tesseract-ocr/4.00/tessdata or the equivalent location for your distribution. Windows deployments typically bundle the Tesseract installer alongside the application and reference the tessdata subdirectory within the installation path. Containerized deployments require special attention because the Tesseract native binary and all language data files must be installed inside the Docker image at build time rather than mounted at runtime, which means your Dockerfile needs explicit apt-get or apk install commands for the tesseract-ocr package and any additional language packs.
Word-level bounding box extraction
Call instance.doOCR(image) for raw text or instance.getWords(image, RIL.WORD) to retrieve a list of Word objects, each containing its bounding rectangle in pixel coordinates.2 Because the getWords() call returns pixel positions relative to the top-left of the image, the bounding boxes map directly to PDF page coordinates after scaling by the render DPI ratio. Consequently, a 300 DPI render requires multiplying pixel values by 72/300 to convert to PDF points. For ad hoc redaction without writing Java code, CapyToolkit's browser-based OCR Redactor provides the same Tesseract engine with a visual interface. The bounding box coordinates returned at the WORD level correspond to individual tokens, which means multi-word identifiers like names and addresses require merging adjacent word rectangles before drawing a single redaction region.
When processing documents with mixed font sizes or dense tabular layouts, the WORD level bounding boxes may fragment single logical tokens across multiple Word objects if Tesseract's segmentation splits a word at an unexpected point. Check each Word object's confidence field and filter out results below 80 for high-stakes redaction where missing a single character is unacceptable. Additionally, the pixel-to-point scaling factor of 0.24 at 300 DPI assumes the PDF page dimensions exactly match the rendered image; if PDFBox's PDFRenderer applies any page-level scaling or rotation, the bounding box coordinates must be adjusted by the same transformation matrix before drawing redaction rectangles.
Rendering PDF pages with PDFBox for OCR input
Create a PDFRenderer from your PDDocument instance and call renderer.renderImageWithDPI(pageIndex, 300, ImageType.RGB) to produce a BufferedImage at 300 DPI.3 Feed this image to Tess4J. After OCR, draw redaction rectangles in the PDPageContentStream by calling cs.setNonStrokingColor(Color.BLACK) and cs.fillRect(x, y, width, height), where coordinates are in PDF points.4 Building on this, scale pixel bounding boxes from the 300 DPI render to PDF points by multiplying pixel values by 72 / 300 (0.24). Note that PDFBox uses a bottom-left coordinate origin, while images use a top-left origin; invert the y-axis by computing pdfY = pageHeight - pixelY * (72/300) - rectHeight. Rendering at 300 DPI rather than the default 72 DPI ensures that Tesseract receives enough pixel detail for accurate character recognition on standard printed text.
When to use a server-side Java implementation vs the browser tool
A Java server-side implementation makes sense when your redaction workflow is automated, when document volumes are high (hundreds or thousands per day), or when redaction must integrate into an existing Java enterprise pipeline such as a content management system or document management platform. The browser-based OCR Redactor is better suited for one-off and ad hoc redaction by non-technical users who need to process a single document without writing any code. Furthermore, Java implementations can leverage GPU-accelerated Tesseract builds for higher throughput on large batches, while the browser WASM implementation is single-threaded. Choose the approach that matches your volume and deployment context.
Exception handling and resource management in Tess4J processing
Tess4J wraps the native Tesseract library through JNA, and native resource lifecycle management differs from standard Java object cleanup. The Tesseract instance does not implement AutoCloseable, so it cannot be used in a try-with-resources block. Configure your instance once with setDatapath and the language parameter, then reuse the same instance across all documents in a batch rather than constructing a new instance per file.
Handling TesseractException in production pipelines
Tesseract throws TesseractException, a checked exception, when the native library fails to process an image, which can happen due to corrupted file data, unsupported image formats, or missing language data files. Wrap each doOCR call in a try-catch that logs the exception with the source file path, marks the file as failed in your processing record, and continues to the next file without terminating the batch. For pipelines that run overnight or on a schedule, silent failures are more dangerous than noisy exceptions: always log failed documents to a dedicated error file with enough detail to identify and reprocess them manually.
Choosing PageIteratorLevel for different redaction target granularities
Tess4J exposes Tesseract's PageIteratorLevel constants through the ResultIterator API.5 The level you select determines the granularity of bounding boxes returned during iteration. RIL_WORD returns one bounding box per token, RIL_TEXTLINE returns one per line, and RIL_BLOCK returns one per detected paragraph block. For workflows targeting individual tokens such as names, numbers, and dates, RIL_WORD is the correct level because it gives you the precise bounding rectangle for each redaction target without including surrounding whitespace or adjacent tokens that should remain visible.
When to use RIL_TEXTLINE for regional redaction
Some workflows need to redact an entire section of a document rather than individual tokens: a Notes field in a form, a full address block, or an entire paragraph of physician notes. In these cases, iterating at RIL_TEXTLINE and filtering by y-coordinate range produces more reliable coverage than matching individual words. Compare each line's bounding box to a predefined region of interest and compute a merged bounding rectangle over all lines within that region. This approach handles multi-line sections reliably regardless of how many tokens Tesseract detected within the covered area. For section-level redaction where the content spans a known vertical band on the page, RIL_TEXTLINE iteration with coordinate filtering is more robust than word-level matching because it does not depend on Tesseract correctly segmenting every individual token within the target region.
When to use this
Use a Java implementation when building an automated document processing pipeline in a Java enterprise environment, when you need programmatic batch processing of hundreds of documents, or when your existing stack already uses PDFBox for PDF manipulation.
Notes
Maven dependencies:
Core pattern: PDFBox PDFRenderer renders each page to BufferedImage at 300 DPI. Tesseract instance.getWords(image, pageIteratorLevel) returns ListPDF points (72 points/inch at 300 DPI: multiply pixel value by 72/300 = 0.24). Draw filled black rectangle in PDPageContentStream at the scaled coordinates. Note y-axis inversion: pdfY = pageHeight - pixelY * (72/300) - rectHeight.
Examples
Tess4J Maven dependency
net.sourceforge.tess4j:tess4j:5.11.0
PDFBox Maven dependency
org.apache.pdfbox:pdfbox:3.0.2
Render page to image
PDFRenderer renderer = new PDFRenderer(doc); BufferedImage img = renderer.renderImageWithDPI(pageNum, 300, ImageType.RGB);
Scale pixel to PDF points
float pdfX = pixelX * (72f / 300); float pdfY = pageHeight - pixelY * (72f / 300) - rectHeight;
Try in the tool
What to look for
- Render DPI 300 DPI for the PDFBox to Tesseract handoff
- Pixel-to-point scale factor 72/300 = 0.24
- Confidence filter for high-stakes redaction discard Word results below 80
- Tess4J / PDFBox versions tess4j 5.11.0, pdfbox 3.0.2
PDFBox uses a bottom-left coordinate origin while the rendered image uses top-left, so the y-axis must be inverted before drawing redaction rectangles.
Verify with the Offline OCR & Document Redactor tool.
Try it in the tool ↑- 1.
Tess4J, "Java JNA wrapper for Tesseract OCR API," github.com, accessed June 2026. https://github.com/nguyenq/tess4j
- 2.
Apache PDFBox, "PDFRenderer," pdfbox.apache.org, accessed June 2026. https://pdfbox.apache.org/docs/3.0.2/javadoc/org/apache/pdfbox/rendering/PDFRenderer.html
- 3.
Apache PDFBox, "PDPageContentStream," pdfbox.apache.org, accessed June 2026. https://pdfbox.apache.org/docs/3.0.2/javadoc/org/apache/pdfbox/pdmodel/PDPageContentStream.html
- 4.
Tesseract OCR, "PageIteratorLevel Enum Reference," tesseract-ocr.github.io, accessed June 2026. https://tesseract-ocr.github.io/tessapi/5.x/group___page_iterator_level.html
- 5.
Tesseract OCR, "PageIteratorLevel API," tesseract-ocr.github.io, accessed June 2026. https://tesseract-ocr.github.io/tessdoc/PageIteratorLevel.html
By default, Tess4J uses the system Tesseract binary. You can also bundle the Tesseract native libraries with your application using the tess4j-standalone or by including the JNA native binaries in your project resources. For containerized deployments, install Tesseract in the Docker image and set the system library path accordingly.
Tesseract returns pixel coordinates with the origin at the top-left of the image. PDFBox uses PDF coordinate space with the origin at the bottom-left of the page. To convert: pdfX = pixelX * (72 / dpi); pdfY = pageHeightInPoints - (pixelY * (72 / dpi)) - (boxHeightInPixels * (72 / dpi)). At 300 DPI, the scaling factor is 72/300 = 0.24.
Yes. Iterate over PDDocument.getNumberOfPages(), render each page to BufferedImage, run OCR, draw redaction rectangles, and save the modified PDDocument at the end. PDFBox modifies the document in memory; you save once after processing all pages. Use try-with-resources to ensure PDDocument is closed after saving.
Drawing a filled black rectangle in PDFBox covers the visual layer but does not remove the underlying text content stream. To remove the text layer, you must parse and rewrite the page content stream, removing the text drawing operations in the target region. This is complex with PDFBox. An alternative is to render the entire PDF to images using PDFRenderer and reassemble as an image-only PDF, which removes all text layers at the cost of searchability. CapyToolkit achieves the same result more simply by exporting raster PNGs with no text layer whatsoever.
Tess4J 5.x is designed for Tesseract 5.x binaries. Ensure your system Tesseract binary and the Tess4J library major version match. Tess4J 5.11.0 on Maven Central is the current stable release as of mid-2026 and works with Tesseract 5.3.x system installations on Linux, Windows, and macOS.
OCR Redaction in Python: pytesseract and PyMuPDF
Python's pytesseract and PyMuPDF form the standard OCR redaction pair. pytesseract provides a Python wrapper around the Tesseract binary and returns bounding box data via image_to_data(), which includes word-level bounding rectangles in pixel coordinates1. PyMuPDF's add_redact_annot() and apply_redactions() API marks regions for redaction and then removes the underlying content from the PDF structure permanently.
PyMuPDF 1.24 and later integrates Tesseract directly via MuPDF's built-in OCR binding, removing the need for a separate pytesseract installation for pure-PDF workflows2. For image-based workflows, pytesseract on top of Pillow remains the more flexible option, as it returns structured bounding box data that you can use to calculate page coordinates for the redaction annotation.
Extracting bounding boxes with pytesseract
Call pytesseract.image_to_data(image, output_type=Output.DATAFRAME) on a PIL Image object loaded from your document scan. The returned DataFrame contains a row per recognized word with columns for left, top, width, height, and text. Filter rows where conf > 0 to exclude non-text elements. The confidence threshold of 60 is a practical starting point for clean 300 DPI scans, though handwritten documents or degraded scans may require lowering it to 50 to capture all legitimate word detections without introducing excessive noise from misrecognized character fragments3.
Converting pixel coordinates to PDF points
Each row's left and top values are pixel coordinates from the top-left of the image. Convert them to PDF coordinates by multiplying by 72 divided by the scan DPI. Because PyMuPDF uses a coordinate system where (0, 0) is the top-left of the page in PDF points, no y-axis inversion is needed in PyMuPDF 1.18 and later when using the MuPDF coordinate convention. Consequently, the conversion is straightforward: x0 = left * 72/dpi; y0 = top * 72/dpi. For one-off redaction tasks without scripting, CapyToolkit provides the same Tesseract engine through a browser interface. When working with scanned documents that contain multi-column layouts, be aware that pytesseract returns bounding boxes in image order rather than reading order, so you may need to sort the resulting DataFrame by coordinates before mapping redactions to the correct page regions4.
When the source image DPI differs from the PDF page DPI, which is common when processing scanned PDFs where each page may have been scanned at a different resolution, the scaling factor must be calculated per page using the actual image DPI rather than assuming a constant. Use pdf2image's dpi parameter explicitly rather than relying on defaults, and verify the resulting point coordinates by spot-checking a few known text positions against the PyMuPDF page dimensions before applying redactions across the full document.
Applying redactions with PyMuPDF
Open the PDF with fitz.open() and iterate over pages. For each page, call page.add_redact_annot(fitz.Rect(x0, y0, x1, y1)) for each region to redact, where coordinates are in PDF points (72 per inch). After all annotations are placed on a page, call doc.apply_redactions() with flags=fitz.PDF_REDACT_IMAGE_NONE or flags=fitz.PDF_REDACT_IMAGE_PIXELS depending on whether you also want to redact image regions. The apply_redactions() call irrevocably removes the underlying content from the PDF structure, not just covering it visually. Building on this, saving the document with doc.save(output_path, garbage=4, deflate=True) also compresses and removes orphaned objects from the PDF, reducing file size and eliminating residual content5. Always save to a new file path rather than overwriting the original, because the redaction cannot be reversed after apply_redactions() completes.
When to use Python vs the browser tool
Python scripting is appropriate when building batch document processing pipelines, when redaction must be triggered by events in a data workflow, or when integrating OCR redaction into a CI/CD pipeline for compliance checking. The OCR Redactor browser tool handles ad hoc single-document workflows without any code or environment setup. Consequently, both serve different user types: data engineers and automation architects use the Python approach, while legal assistants and compliance officers benefit from the browser tool. For hybrid environments where some documents require automation and others require human review, both approaches can coexist using the same underlying Tesseract OCR engine. The Python path gives you programmatic control over every decision; the browser tool gives you immediate visual feedback on every rectangle you draw.
Preprocessing scanned images with Pillow for better OCR accuracy
Raw scanner output is not always optimal for Tesseract. Low-contrast documents such as faded receipts and lightly printed forms, and skewed pages placed at an angle on the platen, both reduce recognition accuracy. Pillow's ImageOps, ImageFilter, and ImageEnhance modules provide the preprocessing operations that produce the largest accuracy gains before passing an image to pytesseract.
Common preprocessing operations and their effects
Convert to grayscale first to remove color channel noise that Tesseract does not use for text detection. Apply a sharpening filter to improve edge contrast on blurred letterforms from fast-scan mode. Use ImageOps.autocontrast to stretch the histogram on washed-out or underexposed scans. For skewed pages, call pytesseract.image_to_osd to detect the rotation angle and rotate the image to align text horizontally before running recognition. These four operations handle the majority of scan quality issues and each runs in under a second per page on a mid-range CPU.
Processing multi-page scanned PDFs with pdf2image and pytesseract
pdf2image converts PDF pages to PIL Image objects using the poppler library6. The convert_from_path function returns a list of PIL Images, one per page, at the DPI you specify. For scanned document archives stored as PDFs, this function eliminates the need to pre-rasterize files and lets you pass pages directly to pytesseract.
Iterating pages and collecting bounding boxes per page
Call convert_from_path with your target DPI and iterate over the resulting page list. For each page, call pytesseract.image_to_data with the DICT output type and filter the returned dictionary by the tokens you need to redact. Each token entry includes pixel coordinates for its left edge, top edge, width, and height at the DPI you requested. Store bounding boxes with their page index so you can apply redaction rectangles to the correct page image before reassembling the document into a final output file.
When to use this
Use a Python implementation when automating document redaction as part of a data pipeline, when your environment already uses Tesseract and Python, or when you need fine-grained control over which text patterns to redact programmatically.
Notes
Install: pip install pytesseract pymupdf Pillow
pytesseract.image_to_data(img, output_type=Output.DATAFRAME) returns a DataFrame with columns: level, page_num, block_num, par_num, line_num, word_num, left, top, width, height, conf, text.
PyMuPDF redaction: page.add_redact_annot(fitz.Rect(x0, y0, x1, y1)) marks the region; doc.apply_redactions() removes content from PDF structure irrevocably. Scale pixels to PDF points: pts = pixels * 72 / dpi.
Examples
Install dependencies
pip install pytesseract pymupdf Pillow
Get bounding boxes
df = pytesseract.image_to_data(img, output_type=Output.DATAFRAME); words = df[df.conf > 0][["left","top","width","height","text"]]
Scale pixels to points
x0 = row.left * 72 / dpi; y0 = row.top * 72 / dpi; x1 = x0 + row.width * 72 / dpi; y1 = y0 + row.height * 72 / dpi
Apply redaction
page.add_redact_annot(fitz.Rect(x0, y0, x1, y1)); doc.apply_redactions()
Try in the tool
What to look for
- Confidence threshold, clean 300 DPI scans conf > 60
- Confidence threshold, degraded scans lower to 50
- Pixel-to-point scale pts = pixels * 72 / dpi
- Save flags after redaction garbage=4, deflate=True
add_redact_annot() plus apply_redactions() removes content from the PDF structure permanently; save to a new path since the operation cannot be reversed.
Verify with the Offline OCR & Document Redactor tool.
Try it in the tool ↑- 1.
pytesseract, "A Python wrapper for Google Tesseract," pypi.org, accessed June 2026. https://pypi.org/project/pytesseract/
- 2.
Artifex Software, "PyMuPDF 1.24.11 Changelog," github.com, October 2024. https://github.com/pymupdf/PyMuPDF/blob/1.24.11/changes.txt
- 3.
Tesseract OCR, "Improving Quality," tesseract-ocr.github.io, accessed June 2026. https://tesseract-ocr.github.io/tessdoc/ImproveQuality.html
- 4.
"How to preserve document structure in Tesseract," stackoverflow.com, accessed June 2026. https://stackoverflow.com/questions/22609778/how-to-preserve-document-structure-in-tesseract
- 5.
JorjMcKie, "Pdf size tripled after applying redactions," github.com, accessed June 2026. https://github.com/pymupdf/PyMuPDF/discussions/2458
- 6.
Belval, "pdf2image: A python module that wraps the pdftoppm utility," pypi.org, accessed June 2026. https://pypi.org/project/pdf2image/
Drawing a black rectangle in PyMuPDF adds a visual overlay but leaves the underlying text in the PDF content stream, recoverable by text extraction. add_redact_annot() followed by apply_redactions() removes the content from the PDF structure permanently. CapyToolkit offers the same Tesseract engine through a browser interface for one-off redaction tasks without scripting, with no risk of accidentally leaving content recoverable. The redaction is irrevocable after saving; always work on a copy of the original.
For PDFs, yes. PyMuPDF 1.24 includes MuPDF's built-in Tesseract integration. For JPEG/PNG image inputs without a PDF wrapper, pytesseract on Pillow images remains the standard approach because it returns structured bounding box data directly from the image.
Filter the pytesseract DataFrame where the text column matches your target string using df[df["text"].str.contains(pattern)]. This gives you the bounding box rows for all occurrences of that pattern in the image. Convert each row's pixel coordinates to PDF points and call page.add_redact_annot() for each occurrence.
Yes. Filter the pytesseract DataFrame with regex on the text column: df[df["text"].str.match(r"\d{3}-\d{2}-\d{4}")] finds SSN-formatted strings. Combine with surrounding-word context by joining adjacent rows for more complex patterns. Apply redaction annotations to all matching rows.
Yes, with the appropriate flags. The default call redacts text content in the region. Pass flags=fitz.PDF_REDACT_IMAGE_PIXELS to also modify rasterized image data in the redacted region. This handles pages where the text content is rendered as part of an embedded image rather than as PDF text objects.
OCR Redaction in Node.js: Tesseract.js Worker Thread Approach
Node.js OCR redaction runs Tesseract.js in a Worker thread. Tesseract.js 6.x supports Node.js natively, running the same Tesseract WASM binary that powers browser-based OCR tools in a worker_threads Worker to avoid blocking the main thread. The createWorker() function initializes the engine, loadLanguage() and initialize() load the language model, and recognize() returns a data object containing blocks, paragraphs, lines, words, and symbols, each with bounding box coordinates.1
The words array in the recognize result is the primary source for redaction: each word object contains bbox with x0, y0, x1, y1 in pixels. Using a canvas library such as node-canvas, draw filled black rectangles at those pixel coordinates on a copy of the source image to produce the redacted output.
Running Tesseract.js in a Node.js Worker thread
Tesseract.js 6.x runs OCR in a Worker thread automatically when called from Node.js. Call createWorker("eng") to initialize the worker. The recognize() method accepts a file path, Buffer, or base64 string and returns the full OCR result including all layout hierarchy levels. The Worker thread model means your main event loop stays free to handle other requests during OCR processing.
Each recognition result includes the full page layout hierarchy from blocks down to individual symbols, giving you the flexibility to extract bounding boxes at whichever granularity your redaction workflow requires. For a redaction pipeline, the words array is the most practical level because each entry carries the recognized text, a confidence score, and a pixel bounding box that maps directly to the source image coordinates.
Word-level results and confidence filtering
Accessing result.data.words gives the array of recognized words with confidence scores and pixel bounding boxes. Filter words by confidence > 60 to exclude uncertain detections before building your redaction list.2 Because recognition runs in a Worker, the main Node.js thread remains responsive during processing, which matters for API servers handling multiple concurrent redaction requests. Consequently, a single initialized worker instance can handle sequential requests efficiently.
For browser-based redaction without server infrastructure, CapyToolkit uses the same Tesseract.js engine directly in the user's browser. The confidence threshold of 60 is a practical starting point for printed text at 300 DPI, though handwritten documents or degraded scans may require lowering it to 50 to capture all legitimate word detections. For long-running server applications, monitor the memory usage of each worker because the WASM binary allocates a fixed heap that grows with image size and is not released between recognition calls on the same worker instance.
Drawing redactions with node-canvas
Load the source image using node-canvas's loadImage() function, create a Canvas of the same dimensions, draw the original image onto it, then draw filled black rectangles over the target bounding boxes. For each word to redact, call ctx.fillStyle = "#000000" and ctx.fillRect(bbox.x0, bbox.y0, bbox.x1 - bbox.x0, bbox.y1 - bbox.y0). After all rectangles are drawn, export with canvas.toBuffer("image/png") to produce the redacted PNG.3 Building on this, the same canvas object can be exported as JPEG using canvas.toBuffer("image/jpeg", { quality: 0.95 }) if file size matters more than lossless fidelity. The canvas approach produces a flat raster image with no text layer, ensuring the redacted content cannot be recovered through text extraction or copy-paste operations on the output file.
When to use Node.js vs the browser tool
A Node.js implementation is appropriate when OCR redaction must be part of a server-side API, when building a microservice that accepts uploaded documents and returns redacted versions, or when integrating redaction into an existing Express, Fastify, or NestJS application. Yet serving user-uploaded sensitive documents through a Node.js API reintroduces the upload risk that the browser tool eliminates by design. For privacy-sensitive workflows, the browser approach keeps documents on the user's device. Use the Node.js approach when documents are already server-side (such as documents generated by your application) rather than user-uploaded originals. The browser tool also requires zero deployment, zero configuration, and zero dependency management, which makes it the faster choice for one-off redaction tasks.
Building a folder-watch pipeline for automated OCR redaction of new scans
A folder-watch pipeline monitors an input directory and processes new scan files automatically as they arrive, without requiring manual triggering. The chokidar npm package provides a cross-platform file watcher with debouncing and ready-event detection built in. Install it with npm install chokidar and attach your processing function to the add event to respond to each new file.
Configuring chokidar to wait for complete file writes
The awaitWriteFinish option prevents the add event from firing before the scanner finishes writing the file to disk, which matters for large TIFF or PNG files that take several seconds to flush completely. Set a stabilityThreshold of 500 milliseconds and a pollInterval of 100 milliseconds as a reliable starting configuration for most USB and network scanners.4 When the add event fires after the stabilityThreshold elapses without further file modification, the file is complete and safe to pass to a Tesseract.js worker for recognition and redaction.
Worker reuse and memory management for concurrent Node.js redaction requests
Tesseract.js workers are expensive to initialize because loading the WASM binary and language pack takes several seconds per worker instance.5 For server-side processing, initialize a small worker pool at startup and reuse workers across requests rather than creating and destroying one per request. A pool of three workers handles most concurrent processing loads without excessive memory consumption.
Round-robin dispatch and pool cleanup
Assign incoming requests to workers by cycling through the pool using a modulo counter. Never call terminate between requests on a worker you intend to reuse; call terminate only during server shutdown to release the native WASM memory. Each recognize call on a reused worker retains the loaded language model in WASM memory, which is the primary benefit of pooling: the expensive load step runs once per worker at startup rather than once per document. For high-volume batch processing, increase pool size by one worker per additional CPU core available to the Node.js process.
When to use this
Use a Node.js implementation when building a server-side API that accepts and returns documents, when integrating OCR redaction into an existing JavaScript backend, or when deploying a redaction microservice within a trusted internal network where documents are generated server-side.
Notes
npm install tesseract.js canvas const { createWorker } = require('tesseract.js'); const worker = await createWorker('eng', 1, { logger: m => console.log(m) }); const { data } = await worker.recognize(imagePath); // data.words: [{ text, bbox: { x0, y0, x1, y1 }, confidence }, ...] // Draw redactions: use canvas to fill black rectangles at target bbox positions
Examples
Install packages
npm install tesseract.js canvas
Create worker and recognize
const worker = await createWorker('eng'); const { data } = await worker.recognize(imagePath); Filter confident words
const words = data.words.filter(w => w.confidence > 60);
Draw redaction rectangle
ctx.fillStyle = '#000000'; ctx.fillRect(bbox.x0, bbox.y0, bbox.x1-bbox.x0, bbox.y1-bbox.y0);
Try in the tool
What to look for
- Confidence filter confidence > 60 for printed text at 300 DPI
- WASM binary heap on init about 20 MB
- Per-image memory during recognition another 5-10 MB for a 300 DPI A4 image
- Worker pool size 3 workers handles most concurrent loads
- chokidar stability settings stabilityThreshold 500 ms, pollInterval 100 ms
Reuse initialized workers across requests; call terminate() only at server shutdown, since re-initializing a worker reloads the WASM binary and language pack.
Verify with the Offline OCR & Document Redactor tool.
Try it in the tool ↑- 1.
naptha/tesseract.js, "API," github.com, accessed June 2026. https://github.com/naptha/tesseract.js/blob/master/docs/api.md
- 2.
Jay Padimala, "How can I make tesseract.js return null or an empty string instead of noisy OCR output?," stackoverflow.com, February 2026. https://stackoverflow.com/questions/79886933/how-can-i-make-tesseract-js-return-null-or-an-empty-string-instead-of-noisy-ocr
- 3.
Automattic/node-canvas, "node-canvas," npmjs.com, accessed June 2026. https://www.npmjs.com/package/canvas
- 4.
paulmillr/chokidar, "chokidar," npmjs.com, accessed June 2026. https://www.npmjs.com/package/chokidar
- 5.
naptha/tesseract.js, "Performance," github.com, accessed June 2026. https://github.com/naptha/tesseract.js/blob/master/docs/performance.md
No. Tesseract.js bundles the Tesseract engine as a WebAssembly binary that runs in Node.js via the V8 WASM runtime. No system-level Tesseract installation is required. The language data files download from a CDN on first use or can be bundled with your application for offline deployments. CapyToolkit uses the same Tesseract.js engine in the browser for its OCR Redactor tool.
Download the language data files (eng.traineddata) from the tesseract.js-data npm package or the Tesseract GitHub releases. Pass a custom langPath option to createWorker(): createWorker("eng", 1, { langPath: "/app/tessdata" }). This eliminates the network fetch during worker initialization.
The Tesseract WASM binary requires approximately 20 MB of heap memory on initialization. Processing a 300 DPI A4 image adds another 5 to 10 MB during recognition. For concurrent requests on a server, limit the number of simultaneously active workers proportional to available memory. A single worker can handle sequential requests by reusing the initialized instance.
Yes. Sharp can composite SVG overlays or flatten multiple image layers. To draw redaction rectangles using Sharp, create an SVG string with rect elements for each redaction region and use sharp.composite([{ input: Buffer.from(svgString), blend: "over" }]) to overlay the rectangles onto the original image. Sharp is significantly faster than node-canvas for image I/O.
The WebAssembly version of Tesseract.js does not use GPU acceleration. For GPU-accelerated OCR in Node.js, consider using the native Tesseract binary through a child_process call or using a different OCR engine with GPU support such as PaddleOCR. For most document redaction workloads, the WASM version provides adequate throughput.
Tesseract.js in the Browser: How WASM OCR Works
Tesseract.js runs the same OCR engine this tool uses in-browser. When you drop an image into the OCR Redactor and Tesseract begins extracting text, the engine executing is Tesseract.js compiled to WebAssembly. Understanding how this architecture works helps you decide whether to use the ready-made OCR Redactor for your use case or implement your own Tesseract.js integration for a custom workflow.
WebAssembly (WASM) allows C++ code to run in the browser at near-native speed without plugins. Tesseract's C++ source compiles to a .wasm binary that the browser downloads once and caches. A Web Worker runs the WASM binary in a background thread, keeping the UI thread responsive during recognition.1 This architecture is identical in all Tesseract.js-based browser tools, including this one.
How WASM OCR works in the browser
Tesseract.js loads a WebWorker that downloads and executes the Tesseract WASM binary alongside the English language training data. The browser fetches both files once and caches them via the Cache API; subsequent page loads use the cached copies without re-downloading. The WASM binary is approximately 4.5 MB and the English language data is approximately 3 MB (LSTM-only, the default since Tesseract.js v5), totaling around 7.5 MB on first load.2
Worker thread isolation and UI responsiveness
The Worker receives the image data via postMessage, processes it entirely in the worker thread, and posts the result back to the main thread. Because everything runs in the Worker, the main thread remains free to update the UI during recognition. Consequently, you can display a progress indicator or keep the UI interactive while OCR runs, which is the behavior visible in the OCR Redactor. The worker thread model also means that closing the browser tab or navigating away from the page terminates the worker and releases all WASM memory automatically.3
When processing large images or running multiple recognitions in sequence, the WASM heap inside the worker can grow significantly. Calling worker.terminate() after a batch of recognitions and creating a fresh worker for the next batch forces the browser to reclaim the WASM memory, preventing the tab from accumulating hundreds of megabytes over a long session. For applications that process a steady stream of user uploads, this periodic worker recycling pattern keeps memory usage stable without impacting perceived performance.
Integrating Tesseract.js into a custom browser tool
Initialize the worker once per page session to avoid re-loading the WASM binary on every call. Store the worker instance in a module-level variable. Call worker.recognize(imageSource) where imageSource can be an HTMLImageElement, HTMLCanvasElement, File, or URL string. The result data object contains a full page layout hierarchy. For a custom redaction tool, consume data.words for word-level bounding boxes, or data.lines for line-level. Draw HTML canvas rectangles at the returned pixel coordinates to implement the redaction layer. Building on this, saving the canvas using canvas.toBlob() with type "image/png" produces the redacted image as a browser download without any server upload.4 The entire pipeline from image input to redacted output stays within the browser, which means no document data ever touches a network connection.
When to build custom vs use the ready-made tool
Building a custom Tesseract.js integration makes sense when you need specific UI workflows not supported by the OCR Redactor, when you need to integrate OCR output into a larger browser application (such as a form auto-fill tool or a document search index), or when you need to customize the recognition language or character whitelist. The OCR Redactor is the faster choice for standard redaction workflows: drop an image, draw rectangles, export PNG. A custom implementation requires several hundred lines of canvas management, worker lifecycle handling, and export code to replicate that behavior. Yet for specialized workflows, the Tesseract.js API is well-documented and the same engine guarantees feature parity.
Loading multiple language packs for multilingual document processing
Tesseract.js supports multiple languages in a single recognize call by passing an array of language codes to createWorker.5 Loading English, German, and French together lets you process documents in any of those languages without reinitializing the worker between documents. Each language pack downloads from a CDN or from a local path you configure when the worker initializes, adding a few seconds to startup for each additional language.
Hosting language packs locally for offline and privacy-sensitive applications
To load language packs from your own server rather than the default CDN, pass a langPath option to createWorker pointing to the directory where you host the traineddata files. The traineddata files for each language are available from the tessdata repository. Hosting them locally eliminates the CDN dependency and enables operation in environments where outbound internet access is restricted, which matters for applications that process confidential documents where even metadata-free requests to an external server are unacceptable.
Progress events, confidence scores, and worker lifecycle in Tesseract.js
Tesseract.js 6.x emits progress events during recognition through a logger callback you pass to createWorker.1 The logger receives an object with a status field describing the current pipeline stage and a progress field that runs from 0 to 1. Use these events to update a progress indicator in your UI or to log timing data during development to identify which pipeline stage consumes the most time for your typical document size.
Reading confidence scores and using them to flag uncertain tokens
The words array in the recognize result includes a confidence value per word, ranging from 0 to 100.6 Tokens with confidence below 60 may be misread, meaning the OCR text does not match the actual printed character. A misread token will not match the identifier you intend to redact even if your target string is correct. Filter low-confidence tokens from the region you are redacting and visually inspect the image to confirm the correct word is covered before finalizing the export. Call terminate only when the component or server shuts down; never between recognize calls in a processing loop.
When to use this
Build a custom Tesseract.js integration when your product requires OCR as part of a larger browser feature, when you need multi-language support beyond English, or when you need to connect OCR output to custom application logic not covered by the OCR Redactor.
Notes
npm install tesseract.js (or CDN: https://cdn.jsdelivr.net/npm/tesseract.js@6/dist/tesseract.min.js) const { createWorker } = Tesseract; const worker = await createWorker('eng'); const { data } = await worker.recognize(imageElement); // or canvas, URL, file // data.words[n].bbox = { x0, y0, x1, y1 } (pixel coords) // data.words[n].text = 'recognized word'
Examples
Load from CDN
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@6/dist/tesseract.min.js"></script>
Initialize worker
const worker = await Tesseract.createWorker('eng'); Run recognition
const { data } = await worker.recognize(document.getElementById("img")); Access word bounding boxes
data.words.forEach(w => console.log(w.text, w.bbox));
Try in the tool
What to look for
- WASM binary size about 4.5 MB
- English language data (LSTM-only) about 3 MB
- Total first-load download around 7.5 MB, cached after that
- Confidence scale 0 to 100 per word; below 60 may be misread
Call worker.terminate() only between batches or at shutdown, never between individual recognize() calls in a processing loop.
Verify with the Offline OCR & Document Redactor tool.
Try it in the tool ↑- 1.
naptha/tesseract.js, "API," github.com, accessed June 2026. https://github.com/naptha/tesseract.js/blob/master/docs/api.md
- 2.
naptha/tesseract.js, "Performance," github.com, accessed June 2026. https://github.com/naptha/tesseract.js/blob/master/docs/performance.md
- 3.
Mozilla, "Worker: terminate() method," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate
- 4.
Mozilla, "HTMLCanvasElement: toBlob() method," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
- 5.
naptha/tesseract.js, "tesseract.js," npmjs.com, accessed June 2026. https://www.npmjs.com/package/tesseract.js
- 6.
Jay Padimala, "How can I make tesseract.js return null or an empty string instead of noisy OCR output?," stackoverflow.com, February 2026. https://stackoverflow.com/questions/79886933/how-can-i-make-tesseract-js-return-null-or-an-empty-string-instead-of-noisy-ocr
No. After the initial download of the WASM binary and language data, Tesseract.js runs entirely offline in the browser. All recognition computation happens in the Web Worker on the client machine. No image data is sent to any server.
The WASM binary is approximately 4.5 MB and the English language training data is approximately 3 MB (LSTM-only, the default since Tesseract.js v5), totaling around 7.5 MB on first load. Both are cached by the browser after the first download. All subsequent page loads use the cached files. Fast trained data files are also available at around 2 MB for reduced accuracy but faster load.
Tesseract.js accepts any image source that can be drawn on an HTML canvas: HTMLImageElement, HTMLCanvasElement, ImageData, File, Blob, URL string, or base64 data URI. Formats supported depend on what the browser's image decoder handles, which includes JPEG, PNG, WebP, and GIF.
Yes. Create multiple workers with createWorker() and call recognize() on each simultaneously. Workers run in parallel in separate browser threads. On a 4-core machine, 3 to 4 workers run at near-full parallel throughput. The browser limits total Web Worker threads based on hardware concurrency; navigator.hardwareConcurrency gives the count.
CapyToolkit's OCR Redactor is a complete application built on Tesseract.js, providing the drop zone, canvas redaction UI, text panel, and export functions. Building directly with Tesseract.js gives you full control over every aspect of the workflow but requires implementing those UI layers yourself. For one-off redaction tasks, the OCR Redactor saves significant development time.
OCR Redaction in C#/.NET: Tesseract NuGet and Image Processing
C# OCR redaction uses the Tesseract NuGet package and System.Drawing. The charlesw/tesseract NuGet package (Tesseract for .NET) wraps the Tesseract native binaries and provides a managed API for OCR with bounding box output.1 System.Drawing.Graphics handles image manipulation, including drawing filled black rectangles over target regions on a Bitmap copy of the source image.
The workflow mirrors the Python and Java approaches: load the document image, run Tesseract to extract words with bounding rectangles, identify target words by content or position, draw filled rectangles over them on a Bitmap, and save the resulting Bitmap as a PNG. The Tesseract NuGet package ships the native binaries for Windows x64 and x86; Linux and macOS deployments require the Tesseract native library installed separately.1
Setting up the Tesseract NuGet package
Add the Tesseract NuGet package via the Package Manager Console: Install-Package Tesseract. The package includes native DLLs for Windows x64 and x86 in the build output directory. Create a tessdata directory in your project root and copy the eng.traineddata file there from the tessdata repository on GitHub. The NuGet package bundles the Tesseract native binaries for Windows, but Linux and macOS deployments require installing the Tesseract system library separately through the platform package manager and configuring the interop library path so the managed wrapper can locate the native libtesseract shared object.
Engine initialization and IDisposable resource management
Set the Copy to Output Directory property on the tessdata folder contents to Copy if newer. Initialize the engine with new TesseractEngine(tessDataPath, "eng", EngineMode.Default). Call engine.Process(pixImage) to run OCR on a Pix image object loaded with Pix.LoadFromFile() or Pix.LoadFromMemory(). Consequently, the engine, Pix image, and result page objects are all IDisposable and should be wrapped in using statements. For teams that need a managed redaction UI without building one from scratch, CapyToolkit provides a browser-based alternative using the same Tesseract engine. A single TesseractEngine instance is not thread-safe2, so for any multi-threaded scenario such as an ASP.NET web application processing concurrent requests, you must either use a lock around a shared instance or create a new engine per thread using the ThreadLocal pattern described in the batch processing section.
In long-running services such as a Windows Service or ASP.NET Core application, the TesseractEngine holds native memory that is not reclaimed by the .NET garbage collector until Dispose is called. If you initialize a single engine at startup and reuse it for the application lifetime, ensure you hook into the application shutdown lifecycle to call engine.Dispose(). For scenarios where the tessdata path might change or the engine needs to be reinitialized with a different language, dispose the current instance before constructing a new one to avoid leaking native handles.
Extracting bounding boxes and drawing redactions
After calling engine.Process(pix), use the result page's GetIterator() method to iterate over recognized elements at PageIteratorLevel.Word. For each word, call iter.TryGetBoundingBox(PageIteratorLevel.Word, out Rect rect) to get the pixel coordinates. Create a Bitmap copy of the source image using new Bitmap(originalImage). Obtain a Graphics object with Graphics.FromImage(bitmap) and call graphics.FillRectangle(Brushes.Black, rect.X1, rect.Y1, rect.Width, rect.Height) for each target word. Building on this, System.Drawing.Color.Black produces a solid black fill that completely obscures the underlying pixels. Save the result with bitmap.Save(outputPath, ImageFormat.Png) for a lossless output. The TryGetBoundingBox call returns false when no bounding box is available for the current element, so always check the return value before accessing the rect to avoid processing empty regions.
Deployment considerations on Linux and macOS
The Tesseract NuGet package ships Windows native binaries by default. Linux and macOS deployments require installing the Tesseract system library separately (apt install libtesseract-dev on Debian/Ubuntu, brew install tesseract on macOS) and configuring the interop library path in your .NET application. Docker deployments benefit from using a base image with Tesseract pre-installed. Furthermore, .NET 6 and later cross-platform deployments may also use Emgu.CV.OCR as an alternative managed wrapper that bundles native libraries for all target platforms in a single NuGet package, avoiding the separate system library installation step. On Linux, verify that the Tesseract binary is discoverable by setting the TESSDATA_PREFIX environment variable to point at your tessdata directory before initializing the engine.
Using SkiaSharp as a cross-platform alternative to System.Drawing
System.Drawing.Common is Windows-only in .NET 6 and later, and Microsoft explicitly removed non-Windows support. The library throws a PlatformNotSupportedException on Linux and macOS3, making it unsuitable for cross-platform redaction pipelines. SkiaSharp is the recommended replacement: install the SkiaSharp NuGet package and, for Linux deployments, add SkiaSharp.NativeAssets.Linux to supply the platform-specific native binaries. SkiaSharp uses the same Skia graphics library that powers Chrome and Android4, which means its rendering behavior is consistent across operating systems and its performance is well-optimized for bitmap manipulation operations like drawing filled rectangles.
Drawing redaction rectangles with SkiaSharp
Decode the source image into an SKBitmap, create an SKCanvas backed by that bitmap, and configure an SKPaint with a solid black fill. Call DrawRect with the bounding coordinates from Tesseract to paint the redaction rectangle directly onto the bitmap. Encode the modified bitmap to PNG bytes and write them to the output path. This pattern works identically on Linux, macOS, and Windows without conditional compilation or platform guards. For Alpine-based Docker images, use SkiaSharp.NativeAssets.Linux.NoDependencies instead to avoid libc version conflicts with the minimal base image. The SKCanvas.DrawRect method accepts an SKRect structure, so you need to convert the Tesseract pixel coordinates to the appropriate coordinate space before drawing, accounting for any DPI scaling between the source image and the bitmap dimensions.
Parallel batch processing with TesseractEngine in .NET
TesseractEngine is not thread-safe and must not be shared across threads. For parallel batch processing, the recommended pattern is ThreadLocal with TesseractEngine: each thread creates its own engine instance on first use, eliminating locking contention while avoiding the per-file initialization overhead of constructing a new engine for every document. This pattern is particularly effective for batch redaction workflows where hundreds or thousands of document pages need the same rectangle coordinates applied to each page in sequence.
Configuring Parallel.ForEach for batch document processing
Pass your file list to Parallel.ForEach with a localInit delegate that constructs a TesseractEngine and a localFinally delegate that disposes it when the thread's work is complete. Inside the body delegate, retrieve the thread-local engine and call Process on the loaded Pix object. Set MaxDegreeOfParallelism to the number of CPU cores minus one to keep the system responsive during large batch runs. The ThreadLocal trackAllValues option enables a final disposal pass that cleans up every engine instance created during the parallel run. For production workloads, consider also wrapping the engine construction in a retry handler because the native Tesseract library can occasionally fail to initialize on the first attempt due to transient memory pressure5, and a single retry typically resolves the issue without requiring the entire batch to abort.
When to use this
Use a C# implementation when building document processing functionality in a .NET application, when your organization's technology stack is primarily .NET and C#, or when integrating OCR redaction into an existing ASP.NET service.
Notes
NuGet: Install-Package Tesseract -Version 5.2.0 Tesseract package ships native tessdata/; set dataPath to the directory containing eng.traineddata. using Tesseract; var engine = new TesseractEngine(@"./tessdata", "eng", EngineMode.Default); var page = engine.Process(Pix.LoadFromFile(imagePath)); var iter = page.GetIterator(); iter.Begin(); do { if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out var rect)) // rect.X1, rect.Y1, rect.X2, rect.Y2 in pixels } while (iter.Next(PageIteratorLevel.Word));
Examples
Install NuGet package
Install-Package Tesseract -Version 5.2.0
Initialize engine
var engine = new TesseractEngine(@"./tessdata", "eng", EngineMode.Default);
Process image
var page = engine.Process(Pix.LoadFromFile(imagePath));
Draw redaction
graphics.FillRectangle(Brushes.Black, rect.X1, rect.Y1, rect.Width, rect.Height);
Try in the tool
What to look for
- NuGet package version Tesseract 5.2.0
- Thread safety TesseractEngine is not thread-safe; use one instance per thread
- CLI process-spawn overhead 50-200 ms per document versus a reused engine instance
- Recommended parallelism cap CPU core count minus one
System.Drawing.Common is Windows-only on .NET 6+; use SkiaSharp for cross-platform redaction rendering.
Verify with the Offline OCR & Document Redactor tool.
Try it in the tool ↑- 1.
charlesw/tesseract, "charlesw/tesseract," github.com, accessed June 2026. https://github.com/charlesw/tesseract
- 2.
iText, "Tesseract4LibOcrEngine Class Reference," api.itextpdf.com, accessed June 2026. https://api.itextpdf.com/pdfocr/dotnet/2.0.0/classi_text_1_1_pdfocr_1_1_tesseract4_1_1_tesseract4_lib_ocr_engine.html
- 3.
Microsoft, "Breaking change: System.Drawing.Common only supported on Windows," learn.microsoft.com, November 2022. https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/system-drawing-common-windows-only
- 4.
Mono, "SkiaSharp," mono.github.io, accessed June 2026. https://mono.github.io/SkiaSharp/
- 5.
tesseract-ocr/tesseract, "System.AccessViolationException: Attempted to read or write protected memory," github.com, accessed June 2026. https://github.com/tesseract-ocr/tesseract/issues/4399
Yes. The charlesw/tesseract package targets .NET Standard 2.0, which is compatible with .NET 6, 7, 8, and 9. The package version 5.x targets Tesseract 5.x native binaries. CapyToolkit offers a browser-based redaction tool powered by the same Tesseract engine with no deployment footprint, for teams evaluating whether to build a custom pipeline or use a ready-made solution. Ensure the native DLLs in the build output match the .NET runtime platform (x64 or x86).
System.Drawing.Common requires the libgdiplus library on Linux. Install it with apt install libgdiplus. Alternatively, use the SkiaSharp library as a cross-platform replacement for image manipulation. SkiaSharp.SKCanvas provides equivalent FillRect() functionality without the libgdiplus dependency.
Set the path relative to the application's execution directory using Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tessdata"). Include the tessdata folder in your publish profile by marking the folder contents as Copy to Output Directory. For Azure App Service deployments, confirm the deployment includes the tessdata directory in the wwwroot or App_Data folder.
Yes. Blazor WASM runs in the browser and can load external JavaScript including Tesseract.js. Use JS interop to call Tesseract.js from C# Blazor components. The OCR results can be passed back to C# for further processing. This approach keeps the OCR in the browser, matching the offline privacy model of the OCR Redactor.
The NuGet package calls the same native Tesseract binary via P/Invoke. Performance is equivalent to the CLI for single-file processing. For bulk processing, the NuGet approach avoids process spawn overhead by reusing the initialized engine instance across multiple documents. The CLI spawns a new process per invocation, which adds 50 to 200 ms of startup cost per document.
PyMuPDF PDF Redaction: Permanent Content Removal with apply_redactions()
PyMuPDF redacts PDFs by removing content, not hiding it. The two-step API mirrors how PDF viewers implement their own redaction interfaces: add_redact_annot() places a redaction annotation marking a region for removal, and apply_redactions() processes all pending annotations by permanently removing the underlying text, image, and vector content from the PDF structure.1 The result is a PDF where the redacted regions contain no recoverable data.
This is the critical distinction from drawing a colored rectangle. A rectangle drawn in PDF space sits on top of content but leaves the content in the page's content stream. A redaction annotation followed by apply_redactions() removes the content itself. Saving the resulting document with garbage collection (doc.save(path, garbage=4)) also removes orphaned objects from the cross-reference table.2
How add_redact_annot and apply_redactions work
Calling page.add_redact_annot(fitz.Rect(x0, y0, x1, y1)) creates a redaction annotation object in the PDF's annotation layer. The annotation marks the rectangular region for removal but does not yet modify the page content. This two-step design is intentional: it allows you to accumulate multiple redaction annotations across different regions of the page before committing to the actual content removal, which is safer than removing content incrementally because you can review all marked regions before applying the irreversible change.1
Content removal vs visual overlay
Calling page.apply_redactions() processes every pending redaction annotation on the page by removing all PDF content that falls within the marked regions: text draw operations, image data, and vector graphics. Building on this, the function also fills the redacted region with a specified fill color (default black) and optionally removes any image data in the region by passing the images flag. The annotation itself is consumed and no longer appears after apply_redactions() completes. For a managed redaction experience without writing Python code, CapyToolkit provides the same permanent content removal through its browser-based raster export. The key distinction between add_redact_annot followed by apply_redactions versus simply drawing a black rectangle is that the former removes the content from the PDF content stream entirely, while the latter only adds a visual overlay that any PDF parser can bypass by reading the underlying text objects directly.
When processing PDFs that contain transparency groups or layered content, the apply_redactions() call may leave faint visual artifacts at region boundaries if the fill color does not match the page background. Set the fill parameter explicitly to (0, 0, 0) for black fill on standard documents, or match the page's background color by sampling a non-text pixel outside the redaction region. For documents with complex layer stacking, test the output visually after the first redaction pass before committing to a full batch.
Handling text search and automatic region detection
PyMuPDF's page.search_for(text) method returns a list of fitz.Rect objects covering every occurrence of the search string on the page in PDF coordinates. Combining this with add_redact_annot() enables text-search-based redaction: search for the target string, add a redaction annotation for each returned rectangle, and call apply_redactions(). Consequently, you can automate redaction of specific strings such as names, account numbers, or patterns without manually specifying coordinates. For regex-based search, use page.get_text("words") to retrieve all words with coordinates, filter with Python's re module, and add annotations for matching words. The search_for method also accepts quads parameters for non-rectangular text regions, which handles rotated or curved text that a simple rectangle would not cover completely.
Saving securely with garbage collection
After applying redactions, save the document with doc.save(output_path, garbage=4, deflate=True). The garbage=4 parameter instructs PyMuPDF to remove all unreferenced objects from the PDF cross-reference table, including any objects that were cut from the content stream by apply_redactions() but not yet purged from the file structure.2 Without garbage collection, remnants of the removed content may persist in the PDF file bytes even though they are no longer referenced. The deflate=True parameter compresses object streams, reducing file size. Yet the most important parameter is garbage; always include it when saving redacted documents to ensure complete content removal. Think of garbage collection as the step that actually deletes the data from the file, while apply_redactions only marks it for removal within the page content stream.
Verifying redaction completeness with post-save text extraction
After saving a redacted PDF with PyMuPDF, reopen the saved file and extract all text to verify that no target strings remain. Concatenate the text from every page using get_text and search the result for each identifier you intended to redact. If any target appears in the extracted text, the corresponding add_redact_annot call did not place the annotation correctly, or apply_redactions did not cover the full span of that token.
Building a post-save assertion into your pipeline
Create a list of every target string before processing the document and run a presence check against the extracted text after saving. For each target string that still appears, log the document path, the target value, and the page number where it was found. Use those logs to reprocess the affected document with corrected bounding coordinates. This assertion runs in under a second per page and catches the two most common failure modes: an annotation placed on the wrong page, and a bounding box that does not fully overlap the target text span.
Clearing document metadata and XMP data alongside content redaction
PDF files carry metadata in two locations: the document information dictionary holding Author, Title, Subject, Creator, Producer, and CreationDate fields, and the XMP metadata stream embedded in the PDF catalog.3 Both locations can contain identifying information. A document scanned and OCR-processed by a named employee may carry that employee's name in the Author field or in the XMP creator element.
Clearing both metadata locations before saving
Call set_metadata with an empty dictionary to zero out all information dictionary fields. Then call del_xml_metadata to remove the XMP stream entirely, which prevents tools such as ExifTool from reading residual creator or author data from the catalog.4 Add both calls before every save operation in a redaction pipeline that produces documents for external distribution. Combining these two steps with the garbage=4 and deflate=True save parameters produces a PDF with no residual content in either the visible text layer or the file metadata.5
When to use this
Use PyMuPDF for permanent PDF redaction when your documents are searchable PDFs with a text layer (not scanned images), when you need to automate redaction by text search, or when you need to verify that redacted content is removed from the PDF structure and not just hidden visually.
Notes
pip install pymupdf import fitz # PyMuPDF doc = fitz.open("input.pdf") for page in doc: # Mark regions page.add_redact_annot(fitz.Rect(x0, y0, x1, y1)) # Apply all redactions on this page page.apply_redactions(images=fitz.PDF_REDACT_IMAGE_PIXELS) doc.save("redacted.pdf", garbage=4, deflate=True)
Examples
Install PyMuPDF
pip install pymupdf
Search and redact by text
rects = page.search_for("SSN"); [page.add_redact_annot(r) for r in rects]; page.apply_redactions() Apply with image redaction
page.apply_redactions(images=fitz.PDF_REDACT_IMAGE_PIXELS)
Save with garbage collection
doc.save("redacted.pdf", garbage=4, deflate=True) Try in the tool
What this page covers
- add_redact_annot() marks a rectangular region for removal without yet modifying page content
- apply_redactions() permanently removes text, image, and vector content within marked regions
- search_for() returns every occurrence of a text string on a page as coordinates, for automated redaction
- Metadata clearing set_metadata({}) plus del_xml_metadata() removes residual Author/Creator fields and the XMP stream
Verify with the Offline OCR & Document Redactor tool.
Try it in the tool ↑- 1.
PyMuPDF, "Page," pymupdf.readthedocs.io, accessed June 2026. https://pymupdf.readthedocs.io/en/latest/page.html
- 2.
PyMuPDF, "Document," pymupdf.readthedocs.io, accessed June 2026. https://pymupdf.readthedocs.io/en/latest/document.html
- 3.
"Extensible Metadata Platform," en.wikipedia.org, accessed June 2026. https://en.wikipedia.org/wiki/Extensible_Metadata_Platform
- 4.
PyMuPDF, "Using setMetadata() and setToC()," github.com, accessed June 2026. https://github.com/pymupdf/PyMuPDF/wiki/Using-setMetadata()-and-setToC()
- 5.
PyMuPDF, "PyMuPDF," github.com, accessed June 2026. https://github.com/pymupdf/PyMuPDF
apply_redactions() removes the content from the PDF page structure and the garbage=4 save removes orphaned objects. After saving with garbage=4, the redacted content should not be recoverable through standard PDF parsing. CapyToolkit offers the same permanent content removal through its browser-based raster export, which eliminates the text layer entirely without requiring code. For maximum assurance, verify by running a text extraction on the saved file using page.get_text() and confirming the redacted strings do not appear.
Drawing a rectangle in PyMuPDF (page.draw_rect()) adds a visual element to the page but leaves the underlying PDF content intact. The text below the drawn rectangle is still present in the content stream and is extractable. add_redact_annot followed by apply_redactions() removes the content itself. These are fundamentally different operations: one is cosmetic and one is destructive.
No. apply_redactions() permanently removes content. Always work on a copy of the original document. Save the original in a secure location before applying redactions to a working copy. There is no undo mechanism in PyMuPDF or in the PDF specification for applied redaction annotations.
For scanned PDFs with no text layer, apply_redactions() operates on the embedded image data using images=fitz.PDF_REDACT_IMAGE_PIXELS. The image pixels in the redacted region are overwritten. Alternatively, render each page to a Pixmap, draw black rectangles using the Pixmap draw methods, and reassemble the modified Pixmaps into a new PDF. This approach handles image-only PDFs without any text layer dependency.
apply_redactions() operates on page content, not document metadata. Document metadata such as author, title, and subject fields remain in the PDF metadata structure. To clear metadata, use doc.set_metadata({}) before saving. Also check doc.get_xml_metadata() for XMP metadata, which is a separate XML-based metadata layer in PDF files.
ocrmypdf and PyMuPDF: Full OCR-Then-Redact Pipeline for Scanned PDFs
ocrmypdf adds a searchable text layer to any scanned PDF. Before you can apply text-search-based redaction to a scanned document, the document needs a text layer. ocrmypdf runs Tesseract on each page of a scanned PDF and embeds an invisible text layer at the correct positions, producing a "sandwich" PDF: visible scanned image on top, searchable text beneath.1
Once the scanned PDF has a text layer, PyMuPDF's page.search_for() and add_redact_annot() can locate target text by content and mark the corresponding regions for redaction. This two-stage pipeline (ocrmypdf then PyMuPDF) handles the full scanned document redaction workflow programmatically, which is the server-side counterpart to the browser-based OCR Redactor workflow.
Stage 1: Adding an OCR text layer with ocrmypdf
Run ocrmypdf scanned.pdf searchable.pdf to add a Tesseract-generated text layer to each page. The output-type flag set to pdfa produces a PDF/A-2b archival output with embedded text.2 For speed on large documents, add the jobs flag with value 4 to use 4 CPU cores for parallel page processing.
Handling hybrid PDFs and verifying the text layer
The --skip-text flag skips pages that already have a text layer, preventing double-OCR on hybrid PDFs with some searchable and some image-only pages.2 After this stage, open searchable.pdf in any PDF viewer and confirm that text selection works: if you can select and copy text from the pages, the OCR text layer is present. For interactive redaction without building a pipeline, CapyToolkit's browser-based OCR Redactor uses the same Tesseract engine with a visual interface. A useful diagnostic check after running ocrmypdf is to examine the sidecar output file, which contains the raw OCR transcript without the image overlay, allowing you to verify that the text extraction succeeded before proceeding to the redaction stage.
When the input archive contains a mix of scanned pages and born-digital pages with existing text layers, the skip-text flag prevents redundant OCR but does not validate that the existing text layer is accurate. After the first pass, run a spot-check by extracting text from several pages that were skipped and compare against the visible content to confirm the pre-existing layer has no systematic encoding errors or missing glyphs that would cause silent redaction failures downstream.
Stage 2: Redacting the OCR-enhanced PDF with PyMuPDF
Open the searchable PDF with fitz.open(). For each page, call page.search_for(target_string)3 to find all occurrences of the target text. The search is case-insensitive by default.4 Each occurrence returns a fitz.Rect in PDF coordinate space. Call page.add_redact_annot(rect) for each returned rectangle, then page.apply_redactions() after all targets are marked for that page. Consequently, multiple search terms can be processed in a single page pass before calling apply_redactions(). This is more efficient than applying redactions after each individual search. Save with doc.save(output_path, garbage=4) after processing all pages. After saving, reopen the output file and run a text extraction pass to verify that none of the target strings survived the redaction step.
When to use this pipeline vs the browser tool
This pipeline is appropriate for batch processing large archives of scanned documents where human review of each page is not practical, for integrating OCR and redaction into a document management system, or for building an automated compliance workflow that redacts standard fields from known form types. The browser OCR Redactor handles one document at a time with human oversight of each redaction. Furthermore, the pipeline approach enables programmatic verification: after saving the redacted PDF, run a text extraction pass and assert that none of the target strings appear in the extracted text, providing automated redaction completeness confirmation that the browser tool does not include.
Assessing OCR text layer quality before committing to automated redaction
OCRmyPDF's text layer quality depends on source scan quality. Before running an automated redaction pipeline on a large archive, evaluate a representative sample of documents first. The sidecar option produces a plain text file alongside the searchable PDF in a single pass, giving you the raw OCR transcript without opening the PDF in a viewer.
Quality indicators to check in the sidecar text
Look for these specific failure patterns in the sidecar: digits substituted for letters in names (such as Joh0 instead of John), which usually indicates the source scan is below 200 DPI and needs rescanning; line breaks splitting multi-word identifiers across lines, which requires a post-OCR normalization step that joins hyphenated continuations before string matching; and missing spaces between adjacent fields in dense form layouts, which requires matching on stripped token sequences rather than the raw string. Addressing these patterns before running batch redaction prevents silent misses on the target identifiers that would otherwise pass through the pipeline undetected.
Parallel processing and batch automation in the ocrmypdf plus PyMuPDF pipeline
OCRmyPDF supports a jobs flag that parallelizes page-level processing within a single document. For batch processing across multiple documents, run multiple OCRmyPDF processes in parallel using Python's concurrent.futures.ProcessPoolExecutor.5 Each document gets its own isolated process, so a corrupted page in one file does not affect other running processes or cause the batch to abort.
Wiring the executor to the full redaction pipeline
Wrap the complete per-document workflow, from OCRmyPDF invocation through PyMuPDF redaction and post-save verification, in a single function and pass a list of input file paths to the executor's map method. Set max_workers to no more than the CPU core count minus one to keep the host system responsive during long batch runs. Each worker runs a complete OCR and redaction cycle per document, and the intermediate OCR file should be removed after redaction completes to avoid accumulating temporary storage over large batches.
When to use this
Use this pipeline when building automated redaction of large scanned document archives, when integrating OCR redaction into a document management system, or when you need programmatic verification that specific strings were removed from the final output.
Notes
pip install ocrmypdf pymupdf # Stage 1: Add OCR text layer (output-type pdfa for archival PDF/A) ocrmypdf scanned.pdf searchable.pdf
# Stage 2: Redact by text search using PyMuPDF import fitz doc = fitz.open("searchable.pdf") for page in doc: for target in ["John Smith", "SSN", "123-45-6789"]: for rect in page.search_for(target): page.add_redact_annot(rect) page.apply_redactions() doc.save("redacted.pdf", garbage=4, deflate=True)
Examples
Install CLI tools
pip install ocrmypdf pymupdf
Add OCR layer with parallelism
ocrmypdf --jobs 4 --output-type pdfa scanned.pdf searchable.pdf
Search and redact multiple terms
targets = ["SSN", "DOB", name]; [page.add_redact_annot(r) for t in targets for r in page.search_for(t)]; page.apply_redactions()
Verify redaction completeness
assert all(t not in page.get_text() for t in targets for page in fitz.open(redacted_path))
Try in the tool
What to look for
- Parallel page processing --jobs 4 for a 4-core machine
- Scan quality red flag below 200 DPI, digits often substitute for letters in names
- Batch process cap max_workers set to CPU core count minus one
The --skip-text flag avoids double-OCR on hybrid PDFs, but does not validate that an existing text layer is actually accurate.
Verify with the Offline OCR & Document Redactor tool.
Try it in the tool ↑- 1.
OCRmyPDF, "Introduction," ocrmypdf.readthedocs.io, accessed June 2026. https://ocrmypdf.readthedocs.io/en/latest/introduction.html
- 2.
OCRmyPDF, "Advanced features," ocrmypdf.readthedocs.io, accessed June 2026. https://ocrmypdf.readthedocs.io/en/latest/advanced.html
- 3.
PyMuPDF, "Page," pymupdf.readthedocs.io, accessed June 2026. https://pymupdf.readthedocs.io/en/latest/page.html
- 4.
PyMuPDF, "Tutorial," pymupdf.readthedocs.io, accessed June 2026. https://pymupdf.readthedocs.io/en/latest/tutorial.html
- 5.
OCRmyPDF, "OCRmyPDF," github.com, accessed June 2026. https://github.com/ocrmypdf/OCRmyPDF
No. ocrmypdf produces a "sandwich" PDF: the original scanned image is preserved as the visible layer, and an invisible OCR text layer is added beneath it. The document looks identical to the original when viewed, but becomes searchable and copyable. The original image quality is unaffected.
Yes. Use a shell loop or Python's pathlib to iterate over files: run ocrmypdf on each file separately. Alternatively, write a Python script using subprocess.run(["ocrmypdf", str(input), str(output)]) for each file in a directory. ocrmypdf handles multi-page PDFs natively in a single invocation.
Use the --skip-text flag to skip pages that already have a searchable text layer. Without this flag, ocrmypdf applies OCR to all pages, potentially double-processing hybrid PDFs. The --redo-ocr flag forces re-OCR on all pages including those with existing text layers, useful when replacing a low-quality existing OCR layer.
Use a named entity recognition (NER) library such as spaCy on the text extracted by PyMuPDF. Extract all page text with page.get_text(), run spaCy NER to identify PERSON entities, then search_for() each identified name. This automates identification of names without a pre-built target list, at the cost of NER accuracy and processing time.
Yes. ocrmypdf uses Tesseract to generate the text layer and embeds it in standard PDF text object format. PyMuPDF's page.search_for() searches the same text layer that any PDF reader would find. The text positions match the visible image coordinates because ocrmypdf aligns the OCR text layer to the image pixels precisely. CapyToolkit uses the same Tesseract engine in its browser-based tool for users who prefer a visual interface over a CLI pipeline.