spring-projects / spring-projects/spring-ai

OutOfMemoryError in ForkPDFLayoutTextStripper on parsing offset whitespace

Open
#5,829 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
Dominant language
Java
Stars
9.5k
Forks
2.9k
Avg merge
1d 7h
Merged PRs (30d)
6

Description

Bug description
ForkPDFLayoutTextStripper has this code:
https://github.com/spring-projects/spring-ai/blob/061d9a15313e32379c0bfe683df0d2e7e7ea96dc/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/ForkPDFLayoutTextStripper.java#L169-L177

When parsing a document where there are two consecutive TextPositions offset along the y-axis by more than 5.5 points and representing whitespace characters (such that height == 0.0), there is a division by zero. As the resultant Double.POSITIVE_INFINITY is cast to an int, numberOfLines ends up being set to 2147483646. As each new line creates a char[] buffer based on the number of characters that can fit in each line, this can easily end up being more than 400 Gi of memory.

Environment
spring-ai 1.1.4 (currently 1.1.x), Corretto OpenJDK 21

Steps to reproduce
This issue was encountered in the wild with the (open access) PDF found at: https://www.clinical-lung-cancer.com/article/S1525-7304(22)00115-2/fulltext (direct link).

When saved, you can just run

new PagePdfDocumentReader("<path_to_pdf>/PIIS1525730422001152.pdf").get()

and it'll either consume ~416 Gi of memory, or more likely, fail with OutOfMemoryError.

Image Image

The quirk about this document specifically is that on the authors section of the research paper:

  • there is a space set on the superscript [1] level, right after the first author's name
  • there is a space at the baseline level, before the next author's name
  • the different in y-positions exceeds 5.5 points.

Expected behavior
Parsing consecutive whitespace text that is offset along the Y-axis should not create an exorbitant amount of character buffers, and there should never be a division by zero type of error when determining the number of newlines to add.

Minimal Complete Reproducible example

Here is a (mostly Gemini-vibecoded) test example I came up with to avoid copyright burdens:

package org.springframework.ai.reader.pdf;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.util.Matrix;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.PathResource;

import java.io.IOException;
import java.nio.file.Path;

public class PdfDocumentReaderWithOffsetWhitespaceTest {
    @TempDir(cleanup = CleanupMode.ON_SUCCESS)
    Path workingDir;

    @Test
    public void readFromBuggyPdf() throws IOException {
        Path buggyPaperPath = workingDir.resolve("buggy-paper.pdf");
        generateBuggyPdf(buggyPaperPath);

        PagePdfDocumentReader pagePdfDocumentReader = new PagePdfDocumentReader(new PathResource(buggyPaperPath));
        Assertions.assertThatCode(pagePdfDocumentReader::get)
                .doesNotThrowAnyException();
    }

    public static void generateBuggyPdf(Path path) throws IOException {
        try (PDDocument doc = new PDDocument()) {
            PDPage page = new PDPage();
            doc.addPage(page);

            PDType1Font mainFont = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
            PDType1Font boldFont = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD);
            PDType1Font italicFont = new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE);

            try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
                // --- 1. Title ---
                contents.beginText();
                contents.setFont(boldFont, 22);
                contents.newLineAtOffset(50, 750);
                contents.showText("A Study on AI-Driven PDF Parsing");
                contents.endText();

                // --- 2. Author One + Superscript 1 ---
                contents.beginText();
                contents.setFont(mainFont, 12);
                contents.newLineAtOffset(50, 720);
                contents.showText("John Doe");

                contents.setTextMatrix(Matrix.getTranslateInstance(110, 728));
                contents.showText("1");

                // --- 3. THE TRIGGER: The Vertical Space Jump ---
                // FIRST SPACE: In the high Y position, as part of the superscript
                contents.setFont(mainFont, 0); // Force height to 0.0
                contents.showText(" ");
                contents.endText();

                // SECOND SPACE: In the baseline Y position
                contents.beginText();
                contents.setFont(mainFont, 0);
                contents.setTextMatrix(Matrix.getTranslateInstance(120, 740));
                contents.showText(" ");
                contents.endText();

                // --- 4. Author Two + Superscript 2 ---
                contents.beginText();
                contents.setFont(mainFont, 12);
                contents.setTextMatrix(Matrix.getTranslateInstance(135, 720));
                contents.showText("Jane Smith");

                contents.setTextMatrix(Matrix.getTranslateInstance(200, 728));
                contents.showText("2");
                contents.endText();

                contents.beginText();
                contents.setFont(mainFont, 0);
                contents.showText(" ");
                contents.endText();

                // --- 5. Affiliations Section ---
                // John's Office
                contents.beginText();
                contents.setFont(italicFont, 9);
                contents.newLineAtOffset(50, 150);
                contents.showText("1. Department of Large Integers, Spring AI University");
                contents.endText();

                // Jane's Office
                contents.beginText();
                contents.setFont(italicFont, 9);
                contents.newLineAtOffset(50, 135);
                contents.showText("2. Division of Divide-by-Zero, Overflow Institute");
                contents.endText();

                contents.beginText();
                contents.setFont(mainFont, 8);
                contents.newLineAtOffset(50, 110);
                contents.showText("Correspondence: j.smith@overflow.edu");
                contents.endText();

                // --- Footer ---
                contents.beginText();
                contents.setFont(mainFont, 8);
                contents.newLineAtOffset(240, 30);
                contents.showText("Document generated for Spring AI Bug Report");
                contents.endText();
            }

            doc.save(path.toFile());
        }
    }
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/ForkPDFLayoutTextStripper.java around lines 169-177, then run the supplied PdfDocumentReaderWithOffsetWhitespaceTest reproduction. Done means the generated PDF can be read without an OutOfMemoryError or division-by-zero-derived buffer allocation.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.