PDF Text Layer Auditor is a Java CLI for diagnosing missing or suspicious native text layers before they disrupt search, copy/paste, indexing, accessibility workflows, or downstream text extraction.
A PDF can look correct in a viewer while containing no searchable text, broken Unicode mappings, replacement characters, or text that is too small to be useful. The auditor inspects these signals page by page and produces either a readable terminal report or versioned JSON for automated checks.
Use it to investigate a troublesome PDF, preflight documents before ingestion, or fail a CI job when a document needs attention. It does not run OCR, modify the source file, or claim accessibility conformance.
- pages with no native text glyphs
- glyphs without a usable Unicode mapping
- composite-font text whose Unicode is inferred without an explicit
/ToUnicodemap - parser recovery warnings emitted while PDFBox repairs malformed object structure
- Unicode replacement characters
- suspiciously tiny text below a configurable threshold (3 pt by default)
- fonts used on each page, including embedded and damaged status
The repository also provides a Docker-based GitHub Action that audits every added, modified, or renamed PDF in a pull request. It reads the changed-file list through the GitHub REST API, adds file annotations to the workflow check, writes a job summary, and creates one combined JSON report.
Install it from the GitHub Marketplace.
name: PDF text layer
on:
pull_request:
permissions:
contents: read
pull-requests: read
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Check out the pull request
uses: actions/checkout@v7
with:
lfs: true
- name: Audit changed PDFs
id: pdf-audit
uses: lMysticl/pdf-text-layer-auditor@v0.7.0
with:
token: ${{ github.token }}
- name: Upload the JSON report
if: always() && steps.pdf-audit.outputs.report_path != ''
uses: actions/upload-artifact@v7
with:
name: pdf-text-layer-audit
path: ${{ steps.pdf-audit.outputs.report_path }}The action intentionally supports pull_request only. It rejects
pull_request_target, where checking out and parsing untrusted pull-request
content can expose a more privileged token. The documented workflow grants
read-only access; annotations are emitted through GitHub workflow commands and
do not require checks: write.
Docker actions run on Linux runners. When a repository stores PDFs in Git LFS,
configure the checkout step with lfs: true; otherwise the action receives the
small LFS pointer file instead of the PDF.
Inputs:
| Input | Default | Meaning |
|---|---|---|
token |
required | Token used only to list pull-request files |
fail_on_findings |
true |
Fail when at least one page needs attention |
max_annotations |
20 |
Maximum annotations emitted by the step |
max_files |
50 |
Maximum changed PDFs audited in one pull request |
max_total_size_mib |
500 |
Maximum combined size of changed PDFs |
max_file_size_mib |
100 |
Per-file input-size limit |
max_pages |
1000 |
Per-file page-count limit |
tiny_text_threshold_pt |
3 |
Tiny-text threshold; 0 disables it |
report_path |
pdf-text-layer-audit.json |
Workspace-relative JSON report path |
Outputs:
| Output | Meaning |
|---|---|
files_checked |
PDFs audited successfully |
files_with_findings |
Audited PDFs containing findings |
files_failed |
PDFs that could not be audited |
report_path |
Workspace-relative path to the combined JSON report |
Deleted PDFs and non-PDF files are ignored. Before parsing begins, the action
rejects a workload above max_files or max_total_size_mib; both limits can be
raised explicitly for a controlled repository. GitHub exposes at most 3,000
files through the pull-request files endpoint, so the action also rejects
larger pull requests instead of silently auditing an incomplete list.
The combined report follows the versioned GitHub Action report schema v3. Each successful file entry embeds the auditor report schema v3. The v1 and v2 schemas remain available for consumers of releases through 0.6.x.
Download the prebuilt executable JAR from the latest release, or build it locally.
Requirements:
- Java 21 or newer
- Maven 3.9 or newer
Build the executable JAR:
mvn clean packageAudit a PDF:
java -jar target/pdf-text-layer-auditor.jar document.pdfEmit a machine-readable report:
java -jar target/pdf-text-layer-auditor.jar --json document.pdfJSON output includes the report summary, parse health, evidence-completeness
flags, per-page text surfaces, raw-versus-semantic Unicode mapping, reading
order, page classification, image/vector/annotation inventory, glyph geometry
and paint-state observations, font state, and findings. Schema v3 also records
display-oriented page geometry and bounded, text-free location boxes for
missing Unicode, replacement characters, tiny text, implicit composite
mapping, and RTL-profile findings. New reports use the strict
version 3 JSON Schema; the
version 1 and
version 2 schemas remain published for existing
consumers. A false completeness flag means that the corresponding surface
was not assessed and must not be interpreted as clean. Successful reports
always set extractionAllowed to true; a PDF that forbids extraction
produces exit code 2 and no report. The default output remains human-readable.
The tiny-text threshold defaults to 3 pt. Adjust it for a specific workflow, or use 0 to disable that finding:
java -jar target/pdf-text-layer-auditor.jar \
--tiny-text-threshold-pt 2.5 \
document.pdfCheck the installed build:
java -jar target/pdf-text-layer-auditor.jar --versionInspect only selected pages:
java -jar target/pdf-text-layer-auditor.jar --pages 1,3-5 document.pdfThe report keeps the document page numbers and distinguishes total pages from inspected pages.
spatialEvidence uses PDF points with a top-left origin after page rotation.
It keeps at most eight boxes per locatable finding on each page, reports the
uncapped totalLocationCount, and sets locationsTruncated when more locations
exist. Boxes describe the extraction location, not an exact vector outline of
the painted glyph. No extracted text or font name is stored in a location.
Findings such as reading-order divergence or a page-wide OCR classification
remain page-level until a defensible smaller region can be derived.
The auditor is script-neutral: it validates Unicode mappings rather than
assuming Latin text. Schema v3 reports observed scripts such as HAN,
ARABIC, HEBREW, DEVANAGARI, THAI, and HANGUL, together with RTL,
combining-mark, non-BMP, variation-selector, ZWJ, and bidi-control counts.
Font evidence includes subtype, encoding (Identity-H/Identity-V when
declared), vertical-writing mode, embedding/subsetting, /ToUnicode presence,
and raw unmapped glyph counts. A script appearing in this list proves that its
semantic Unicode was observed; it does not certify shaping or visual fidelity.
Example:
PDF Text Layer Audit
File: /path/to/document.pdf
Size: 0.02 MiB
Pages in document: 2
Pages inspected: 2
Encrypted: false
Tiny text threshold: 3.00 pt
Page 1: 154 glyphs, 154 Unicode characters, 2 fonts
Font: Helvetica | embedded=false | damaged=false | glyphs=120
Font: Times-Roman | embedded=false | damaged=false | glyphs=34
OK: no basic text-layer problems detected
Page 2: 0 glyphs, 0 Unicode characters, 0 fonts
WARN NO_TEXT_LAYER: No native text glyphs were found; the page may be blank or image-only.
Result: 1 of 2 pages need attention
Reports, help, and version information are written to standard output. Invalid arguments, audit failures, and output failures are written to standard error. In JSON mode, standard output contains only the JSON document.
Keep the streams separate when saving a report:
java -jar target/pdf-text-layer-auditor.jar --json document.pdf \
> report.json 2> audit-error.logExit code 1 still means that a valid report was written; one or more inspected pages need attention.
| Code | Meaning |
|---|---|
0 |
No basic text-layer problems detected |
1 |
One or more pages need attention |
2 |
Invalid arguments, audit failure, or output failure |
This makes the tool usable in CI without parsing its human-readable output.
The default audit rejects files larger than 100 MiB and documents with more than 1,000 pages. The GitHub Action additionally limits one pull request to 50 PDFs and 500 MiB of PDF input in total. Temporary PDF streams are buffered on disk instead of an unrestricted heap cache.
Each document is also bounded to 1,000,000 text glyphs, 5,000,000 semantic
Unicode characters, 10,000 fonts, 100,000 images, 1,000,000 painted vector
paths, 100,000 annotations, 100,000 annotation appearance streams, and 100,000
optional-content references, and 100,000 document-level surface entries. These
internal safety limits are deliberately not
CLI-tunable. Exceeding one produces exit code 2 and a stable
pdfTextLayerAuditorFailure=WORK_LIMIT_<TYPE> stderr marker; it does not mean
that the document's text layer was found invalid.
Controlled environments can override either limit:
java -jar target/pdf-text-layer-auditor.jar \
--max-file-size-mib 250 \
--max-pages 2500 \
document.pdfThese limits reduce accidental resource use but do not sandbox the PDF parser. Process untrusted files in an isolated environment with separate CPU, memory, disk, and time limits.
- This tool does not perform OCR.
- The
SPARSE_OCRheuristic means the union of painted images covers at least 75% of the page and the page exposes at most 32 Unicode characters; it is a review signal, not a claim that OCR is wrong. PARTIAL_OCRdivides the crop box into an 8 × 8 grid. When images cover at least 75% of the page but visible page-stream text overlaps less than 25% of the image cells, the page is sent to review even if it exceeds 32 characters. This catches localized headers and page numbers over an otherwise incomplete OCR layer; it is not a semantic OCR comparison.- Pages containing strong right-to-left characters emit
RTL_TEXT_REQUIRES_EXTRACTION_PROFILE. PDFBox applies bidi reordering during ordinary text extraction, and the correct downstream order depends on that extractor's contract; the auditor records the script facts and refuses to guess a universal order. - Paint mode, alpha, crop/clip origin, overlap, rotation, and vertical-font
counters are observations. They are not automatic failures because invisible
OCR and
/ActualTextlayers can be legitimate. - Geometry uses glyph origins rather than complete rendered glyph outlines, so it does not prove full or partial visual visibility.
- PDF text coordinates and Unicode mappings depend on the source file.
IMPLICIT_COMPOSITE_UNICODE_MAPPINGis a provenance warning, not proof that every extracted character is wrong. Without/ToUnicode, a viewer or parser may infer plausible text from a predefined CMap; semantic equivalence still needs an authored map, a trusted reference, or OCR comparison.- Normal, rollover, and down annotation appearance streams are checked separately from page text. Form values without a generated appearance stream cannot be inferred from the field value alone.
- AcroForm/XFA fields, signatures, embedded-file name trees, catalog-associated files, and PDF portfolios are inventoried at document level. Their presence is profile-dependent evidence: the auditor does not silently merge form values or attachment contents into page text.
- OCG/OCMD visibility is evaluated for View, Print, and Export. A malformed,
cyclic, or unsupported visibility expression makes
optionalContent=falsein the document completeness object instead of being guessed clean. - Password-protected PDFs are not supported.
- Documents that exceed an internal work budget stop without a partial report.
- Parser recovery is captured by PDFBox logger category and severity, not by localized message text. A recovered report is reviewable even when all page glyphs look clean.
- Results are diagnostics, not PDF/UA or accessibility certification.
mvn verifyBuild the same container used by the GitHub Action:
docker build -t pdf-text-layer-audit-action .The tests create small synthetic PDFs at runtime, so the repository does not need large binary fixtures. They include a deterministic Unicode and PDF font validation matrix covering representative scripts, combining and directional characters, non-BMP symbols, embedded Type 0 fonts, Type 1 and Type 3 fonts, and malformed mappings. The matrix is category-based: no finite test suite can enumerate every font file or every Unicode string.
mvn verify enforces code-coverage floors and writes the HTML report to
target/site/jacoco/index.html.
mvn verify also creates a CycloneDX 1.6 software bill of materials at target/bom.json. Tagged releases publish the SBOM beside the executable JAR and checksum. Release artifacts include signed build-provenance attestations that can be verified with:
gh attestation verify pdf-text-layer-auditor.jar --repo lMysticl/pdf-text-layer-auditorSee CONTRIBUTING.md before submitting a fixture, issue, or pull request. Report suspected vulnerabilities through the private process in SECURITY.md.
Licensed under the Apache License, Version 2.0.