opendataloader-project/opendataloader-pdf · error · InvalidPdfFileException

'{}' is not a valid PDF file (missing %PDF- header).

Error message

'{}' is not a valid PDF file (missing %PDF- header).

What it means

InvalidPdfFileException thrown by validatePdfMagicNumber before veraPDF is ever invoked: the first 1024 bytes of the file do not contain the ASCII %PDF- marker. This is the 'not a PDF at all' failure mode, distinct from error 81 (header present, body broken). Detected cheaply so the cost of a full veraPDF parse is avoided on obviously wrong input.

Source

Thrown at java/opendataloader-pdf-core/src/main/java/org/opendataloader/pdf/processors/DocumentProcessor.java:694

     *
     * <p>ISO 32000-1 §7.5.2 allows the {@code %PDF-} header to appear "near
     * the beginning" of the file rather than strictly at byte 0; real-world
     * PDFs sometimes have a leading UTF-8 BOM or whitespace. A 1024-byte
     * search window matches that tolerance while still rejecting any
     * JPG/PNG/HTML/empty file.
     *
     * @throws InvalidPdfFileException if the magic number is not present
     * @throws IOException if the file cannot be opened or read
     */
    private static void validatePdfMagicNumber(String pdfName) throws IOException {
        Path path = Path.of(pdfName);
        byte[] head;
        try (InputStream in = Files.newInputStream(path)) {
            head = in.readNBytes(1024);
        }
        byte[] marker = "%PDF-".getBytes(StandardCharsets.US_ASCII);
        if (indexOfBytes(head, marker) < 0) {
            throw new InvalidPdfFileException(
                "'" + displayName(pdfName) + "' is not a valid PDF file (missing %PDF- header).");
        }
    }

    /**
     * Verifies the JVM temporary directory is writable.
     *
     * <p>veraPDF streams anything larger than a small in-memory threshold
     * through a temporary file: the Standard 14 font metrics embedded in the
     * jar, embedded font programs, CMaps and decoded content streams all take
     * that path. A PDF stream has no size bound while memory does, so writing
     * to disk is by design and cannot be avoided by buffering.
     *
     * <p>When the temporary directory is not writable those reads fail deep
     * inside veraPDF, where the failure is logged at {@code FINE} and
     * swallowed. Processing then continues on missing data and surfaces as an
     * unrelated {@code NullPointerException}, or — worse — completes with
     * exit code 0 while silently dropping most of the text. Failing up front

View on GitHub (pinned to a7789b8e77)

Solutions

  1. Confirm the file is actually a PDF: check the first bytes (head -c 5 file.pdf should print %PDF-) or run file(1).
  2. Re-export or re-save the source document as PDF from the originating application.
  3. If accepting user uploads, validate the magic number on your side before calling processFile and reject early with your own message.
  4. Check the path is not pointing at a directory or a symlink to a non-PDF target.

Example fix

# before: file is a renamed image
$ mv scan.jpg report.pdf && odl-pdf report.pdf
-> InvalidPdfFileException: 'report.pdf' is not a valid PDF file (missing %PDF- header).
# after: export to real PDF
$ file report.pdf   # -> PDF document, version 1.4
$ odl-pdf report.pdf
Defensive patterns

Strategy: validation

Validate before calling

// Validate the magic number yourself before calling the library:
private static boolean looksLikePdf(Path p) throws IOException {
    try (InputStream in = Files.newInputStream(p)) {
        byte[] head = in.readNBytes(5);
        byte[] marker = "%PDF-".getBytes(StandardCharsets.US_ASCII);
        return Arrays.equals(head, marker);
    }
}

Type guard

static boolean isMissingHeader(IOException e) {
    return e instanceof InvalidPdfFileException
        && e.getMessage() != null
        && e.getMessage().contains("missing %PDF- header");
}

Try / catch

try {
    DocumentProcessor.processFile(pdfName, config);
} catch (InvalidPdfFileException e) {
    if (e.getMessage() != null && e.getMessage().contains("missing %PDF- header")) {
        rejectUpload("File is not a PDF (no %PDF- header).");
    } else {
        throw e; // corrupted-content variant (error 81)
    }
}

Prevention

When it happens

Trigger: Any public entry point (DocumentProcessor.processFile/extractContents/preprocessing/processFileWithResult, OpenDataLoaderPDF.processFile, AutoTagger.tag) is called with a path whose content lacks %PDF- in the first 1024 bytes — e.g. a JPEG/PNG/HTML/ZIP/text file, an empty file, or a PostScript file.

Common situations: A user renamed a .jpg/.png/.html to .pdf. The wrong file was passed (a .txt report instead of the PDF). An empty placeholder file. A path pointing at an OpenDocument or Office file exported to the wrong format.

Related errors


AI-assisted analysis of opendataloader-project/opendataloader-pdf@a7789b8e77 (2026-08-14). Data as JSON: /api/errors/3e1bf833221aa287. Report an issue: GitHub.