spring-projects / spring-projects/spring-ai
OutOfMemoryError in ForkPDFLayoutTextStripper on parsing offset whitespace
Nobody has claimed this yet.
- 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.
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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