From 1266c04b6d2a56cd9b8fc658e9598721e4f66fd1 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Tue, 19 Sep 2023 15:34:57 +0200 Subject: [PATCH] Add PDF Document Readers - Add a set of PDF readers fro per-page (PagePdfDocumentReader) and per-paragraph (ParagraphPdfDocumentReader) readers. - Use a PDFLayoutTextStripper fork and PDFLayoutTextStripperByArea extension to preserve the structure of the extracted document. - PdfDocumentReaderConfig and PageExtractedTextFormatter in standalone classes. - Craeate a new document-readers top level model and the pdf-reader under. --- document-readers/pdf-reader/README.md | 53 ++ document-readers/pdf-reader/pom.xml | 57 +++ .../ai/reader/pdf/PagePdfDocumentReader.java | 165 ++++++ .../pdf/ParagraphPdfDocumentReader.java | 241 +++++++++ .../ai/reader/pdf/PdfTestUtils.java | 62 +++ .../reader/pdf/config/ParagraphManager.java | 199 ++++++++ .../pdf/config/PdfDocumentReaderConfig.java | 133 +++++ .../pdf/layout/ForkPDFLayoutTextStripper.java | 473 ++++++++++++++++++ .../layout/PDFLayoutTextStripperByArea.java | 154 ++++++ .../layout/PageExtractedTextFormatter.java | 186 +++++++ .../pdf/PagePdfDocumentReaderTests.java | 60 +++ .../pdf-reader/src/test/resources/sample1.pdf | Bin 0 -> 88226 bytes .../pdf-reader/src/test/resources/sample2.pdf | Bin 0 -> 54836 bytes pom.xml | 1 + spring-ai-core/pom.xml | 1 - .../ai/reader/TextReaderTests.java | 1 - .../src/test/resources/text_source.txt | 2 +- 17 files changed, 1785 insertions(+), 3 deletions(-) create mode 100644 document-readers/pdf-reader/README.md create mode 100644 document-readers/pdf-reader/pom.xml create mode 100644 document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/PagePdfDocumentReader.java create mode 100644 document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/ParagraphPdfDocumentReader.java create mode 100644 document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/PdfTestUtils.java create mode 100644 document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/config/ParagraphManager.java create mode 100644 document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/config/PdfDocumentReaderConfig.java create mode 100644 document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/ForkPDFLayoutTextStripper.java create mode 100644 document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/PDFLayoutTextStripperByArea.java create mode 100644 document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/PageExtractedTextFormatter.java create mode 100644 document-readers/pdf-reader/src/test/java/org/springframework/ai/reader/pdf/PagePdfDocumentReaderTests.java create mode 100644 document-readers/pdf-reader/src/test/resources/sample1.pdf create mode 100644 document-readers/pdf-reader/src/test/resources/sample2.pdf diff --git a/document-readers/pdf-reader/README.md b/document-readers/pdf-reader/README.md new file mode 100644 index 000000000..208cd81e7 --- /dev/null +++ b/document-readers/pdf-reader/README.md @@ -0,0 +1,53 @@ +# How to configure PDF Reader + + +## PagePdfDocumentReader + +``` java + PagePdfDocumentReader pdfReader = new PagePdfDocumentReader( + "file:document-readers/pdf-reader/src/test/resources/sample.pdf", + PdfDocumentReaderConfig.builder() + .withPageTopMargin(0) + .withPageBottomMargin(0) + .withPageExtractedTextFormatter(PageExtractedTextFormatter.builder() + .withNumberOfTopTextLinesToDelete(0) + .withNumberOfBottomTextLinesToDelete(3) + .withNumberOfTopPagesToSkipBeforeDelete(0) + .build()) + .withPagesPerDocument(1) + .build()); + + var documents = pdfReader.get(); + + PdfTestUtils.writeToFile("document-readers/pdf-reader/target/sample.txt", documents, false); +``` + +```java + public static void main(String[] args) throws IOException { + + ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader( + "file:document-readers/pdf-reader/src/test/resources/sample2.pdf", + PdfDocumentReaderConfig.builder() + // .withPageBottomMargin(15) + // .withReversedParagraphPosition(true) + // .withTextLeftAlignment(true) + .build()); + // ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader( + // "file:document-readers/pdf-reader/src/test/resources/spring-framework.pdf", + // PdfDocumentReaderConfig.builder() + // .withPageBottomMargin(15) + // .withReversedParagraphPosition(true) + // // .withTextLeftAlignment(true) + // .build()); + + // PdfDocumentReader pdfReader = new + // PdfDocumentReader("file:document-readers/pdf-reader/src/test/resources/uber-k-10.pdf", + // PdfDocumentReaderConfig.builder().withPageTopMargin(80).withPageBottomMargin(60).build()); + + var documents = pdfReader.get(); + + writeToFile("document-readers/pdf-reader/target/sample2.txt", documents, true); + System.out.println(documents.size()); + + } +``` \ No newline at end of file diff --git a/document-readers/pdf-reader/pom.xml b/document-readers/pdf-reader/pom.xml new file mode 100644 index 000000000..08debeeb1 --- /dev/null +++ b/document-readers/pdf-reader/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + org.springframework.experimental.ai + spring-ai + 0.7.0-SNAPSHOT + ../../pom.xml + + spring-ai-pdf-document-reader + jar + Spring AI Document Reader - PDF + Spring AI PDF document reader + https://github.com/spring-projects-experimental/spring-ai + + + https://github.com/spring-projects-experimental/spring-ai + git://github.com/spring-projects-experimental/spring-ai.git + git@github.com:spring-projects-experimental/spring-ai.git + + + + + org.springframework.experimental.ai + spring-ai-core + ${parent.version} + + + + org.apache.pdfbox + pdfbox + 3.0.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + + org.testcontainers + junit-jupiter + test + + + + + diff --git a/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/PagePdfDocumentReader.java b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/PagePdfDocumentReader.java new file mode 100644 index 000000000..7163e2834 --- /dev/null +++ b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/PagePdfDocumentReader.java @@ -0,0 +1,165 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.reader.pdf; + +import java.awt.Rectangle; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.pdfbox.pdfparser.PDFParser; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; + +import org.springframework.ai.document.Document; +import org.springframework.ai.document.DocumentReader; +import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig; +import org.springframework.ai.reader.pdf.layout.PDFLayoutTextStripperByArea; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Groups the parsed PDF pages into {@link Document}s. You can group one or more pages + * into a single output document. Use {@link PdfDocumentReaderConfig} for customization + * options. The default configuration is: - pagesPerDocument = 1 - pageTopMargin = 0 - + * pageBottomMargin = 0 + * + * @author Christian Tzolov + */ +public class PagePdfDocumentReader implements DocumentReader { + + private static final String PDF_PAGE_REGION = "pdfPageRegion"; + + public static final String METADATA_START_PAGE_NUMBER = "page_number"; + + public static final String METADATA_END_PAGE_NUMBER = "end_page_number"; + + public static final String METADATA_FILE_NAME = "file_name"; + + private final PDDocument document; + + private PdfDocumentReaderConfig config; + + private File resourceFileName; + + public PagePdfDocumentReader(String resourceUrl) { + this(new DefaultResourceLoader().getResource(resourceUrl)); + } + + public PagePdfDocumentReader(Resource pdfResource) { + this(pdfResource, PdfDocumentReaderConfig.defaultConfig()); + } + + public PagePdfDocumentReader(String resourceUrl, PdfDocumentReaderConfig config) { + this(new DefaultResourceLoader().getResource(resourceUrl), config); + } + + public PagePdfDocumentReader(Resource pdfResource, PdfDocumentReaderConfig config) { + + try { + + PDFParser pdfParser = new PDFParser( + new org.apache.pdfbox.io.RandomAccessReadBuffer(pdfResource.getInputStream())); + this.document = pdfParser.parse(); + + this.resourceFileName = pdfResource.getFile(); + this.config = config; + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public List get() { + + List readDocuments = new ArrayList<>(); + try { + var pdfTextStripper = new PDFLayoutTextStripperByArea(); + + int pageNumber = 0; + int pagesPerDocument = 0; + int startPageNumber = pageNumber; + + List pageTextGroupList = new ArrayList<>(); + + for (PDPage page : this.document.getDocumentCatalog().getPages()) { + + pagesPerDocument++; + + if (this.config.pagesPerDocument != PdfDocumentReaderConfig.ALL_PAGES + && pagesPerDocument >= this.config.pagesPerDocument) { + pagesPerDocument = 0; + + var aggregatedPageTextGroup = pageTextGroupList.stream().collect(Collectors.joining()); + if (StringUtils.hasText(aggregatedPageTextGroup)) { + readDocuments.add(toDocument(aggregatedPageTextGroup, startPageNumber, pageNumber)); + } + pageTextGroupList.clear(); + + startPageNumber = pageNumber + 1; + } + int x0 = (int) page.getMediaBox().getLowerLeftX(); + int xW = (int) page.getMediaBox().getWidth(); + + int y0 = (int) page.getMediaBox().getLowerLeftY() + this.config.pageTopMargin; + int yW = (int) page.getMediaBox().getHeight() + - (this.config.pageTopMargin + this.config.pageBottomMargin); + + pdfTextStripper.addRegion(PDF_PAGE_REGION, new Rectangle(x0, y0, xW, yW)); + pdfTextStripper.extractRegions(page); + var pageText = pdfTextStripper.getTextForRegion(PDF_PAGE_REGION); + + if (StringUtils.hasText(pageText)) { + + pageText = this.config.pageExtractedTextFormatter.format(pageText, pageNumber); + + pageTextGroupList.add(pageText); + } + pageNumber++; + pdfTextStripper.removeRegion(PDF_PAGE_REGION); + } + if (!CollectionUtils.isEmpty(pageTextGroupList)) { + readDocuments.add(toDocument(pageTextGroupList.stream().collect(Collectors.joining()), startPageNumber, + pageNumber)); + } + + return readDocuments; + + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + private Document toDocument(String docText, int startPageNumber, int endPageNumber) { + + Document doc = new Document(docText); + doc.getMetadata().put(METADATA_START_PAGE_NUMBER, startPageNumber); + if (startPageNumber != endPageNumber) { + doc.getMetadata().put(METADATA_END_PAGE_NUMBER, endPageNumber); + } + doc.getMetadata().put(METADATA_FILE_NAME, this.resourceFileName); + + return doc; + } + +} diff --git a/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/ParagraphPdfDocumentReader.java b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/ParagraphPdfDocumentReader.java new file mode 100644 index 000000000..6c7385474 --- /dev/null +++ b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/ParagraphPdfDocumentReader.java @@ -0,0 +1,241 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.reader.pdf; + +import java.awt.Rectangle; +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.pdfbox.pdfparser.PDFParser; +import org.apache.pdfbox.pdmodel.PDDocument; + +import org.springframework.ai.document.Document; +import org.springframework.ai.document.DocumentReader; +import org.springframework.ai.reader.pdf.config.ParagraphManager; +import org.springframework.ai.reader.pdf.config.ParagraphManager.Paragraph; +import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig; +import org.springframework.ai.reader.pdf.layout.PDFLayoutTextStripperByArea; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Uses the PDF catalog (e.g. TOC) information to split the input PDF into text paragraphs + * and output a single {@link Document} per paragraph. + * + * This class provides methods for reading and processing PDF documents. It uses the + * Apache PDFBox library for parsing PDF content and converting it into text paragraphs. + * The paragraphs are grouped into {@link Document} objects. + * + * @author Christian Tzolov + */ +public class ParagraphPdfDocumentReader implements DocumentReader { + + // Constants for metadata keys + private static final String METADATA_START_PAGE = "page_number"; + + private static final String METADATA_END_PAGE = "end_page_number"; + + private static final String METADATA_TITLE = "title"; + + private static final String METADATA_LEVEL = "level"; + + private static final String METADATA_FILE_NAME = "file_name"; + + private final ParagraphManager paragraphTextExtractor; + + private final PDDocument document; + + private PdfDocumentReaderConfig config; + + private File resourceFileName; + + /** + * Constructs a ParagraphPdfDocumentReader using a resource URL. + * @param resourceUrl The URL of the PDF resource. + */ + public ParagraphPdfDocumentReader(String resourceUrl) { + this(new DefaultResourceLoader().getResource(resourceUrl)); + } + + /** + * Constructs a ParagraphPdfDocumentReader using a resource. + * @param pdfResource The PDF resource. + */ + public ParagraphPdfDocumentReader(Resource pdfResource) { + this(pdfResource, PdfDocumentReaderConfig.defaultConfig()); + } + + /** + * Constructs a ParagraphPdfDocumentReader using a resource URL and a configuration. + * @param resourceUrl The URL of the PDF resource. + * @param config The configuration for PDF document processing. + */ + public ParagraphPdfDocumentReader(String resourceUrl, PdfDocumentReaderConfig config) { + this(new DefaultResourceLoader().getResource(resourceUrl), config); + } + + /** + * Constructs a ParagraphPdfDocumentReader using a resource and a configuration. + * @param pdfResource The PDF resource. + * @param config The configuration for PDF document processing. + */ + public ParagraphPdfDocumentReader(Resource pdfResource, PdfDocumentReaderConfig config) { + + try { + PDFParser pdfParser = new PDFParser( + new org.apache.pdfbox.io.RandomAccessReadBuffer(pdfResource.getInputStream())); + this.document = pdfParser.parse(); + + this.config = config; + + this.paragraphTextExtractor = new ParagraphManager(this.document); + + this.resourceFileName = pdfResource.getFile(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Reads and processes the PDF document to extract paragraphs. + * @return A list of {@link Document} objects representing paragraphs. + */ + @Override + public List get() { + + var paragraphs = this.paragraphTextExtractor.flatten(); + + List documents = new ArrayList<>(paragraphs.size()); + + if (!CollectionUtils.isEmpty(paragraphs)) { + Iterator itr = paragraphs.iterator(); + + var current = itr.next(); + + if (!itr.hasNext()) { + documents.add(toDocument(current, current)); + } + else { + while (itr.hasNext()) { + var next = itr.next(); + Document document = toDocument(current, next); + if (document != null && StringUtils.hasText(document.getContent())) { + documents.add(toDocument(current, next)); + } + current = next; + } + } + } + + return documents; + } + + private Document toDocument(Paragraph from, Paragraph to) { + + String docText = this.getTextBetweenParagraphs(from, to); + + if (!StringUtils.hasText(docText)) { + return null; + } + + Document document = new Document(docText); + document.getMetadata().put(METADATA_TITLE, from.title()); + document.getMetadata().put(METADATA_START_PAGE, from.startPageNumber()); + document.getMetadata().put(METADATA_END_PAGE, to.startPageNumber()); + document.getMetadata().put(METADATA_LEVEL, from.level()); + document.getMetadata().put(METADATA_FILE_NAME, this.resourceFileName); + + return document; + } + + public String getTextBetweenParagraphs(Paragraph fromParagraph, Paragraph toParagraph) { + + // Page started from index 0, while PDFBOx getPage return them from index 1. + int startPage = fromParagraph.startPageNumber() - 1; + int endPage = toParagraph.startPageNumber() - 1; + + try { + + StringBuilder sb = new StringBuilder(); + + var pdfTextStripper = new PDFLayoutTextStripperByArea(); + pdfTextStripper.setSortByPosition(true); + + for (int pageNumber = startPage; pageNumber <= endPage; pageNumber++) { + + var page = this.document.getPage(pageNumber); + + int fromPosition = fromParagraph.position(); + int toPosition = toParagraph.position(); + + if (this.config.reversedParagraphPosition) { + fromPosition = (int) (page.getMediaBox().getHeight() - fromPosition); + toPosition = (int) (page.getMediaBox().getHeight() - toPosition); + } + + int x0 = (int) page.getMediaBox().getLowerLeftX(); + int xW = (int) page.getMediaBox().getWidth(); + + int y0 = (int) page.getMediaBox().getLowerLeftY(); + int yW = (int) page.getMediaBox().getHeight(); + + if (pageNumber == startPage) { + y0 = fromPosition; + yW = (int) page.getMediaBox().getHeight() - y0; + } + if (pageNumber == endPage) { + yW = toPosition - y0; + } + + if ((y0 + yW) == (int) page.getMediaBox().getHeight()) { + yW = yW - this.config.pageBottomMargin; + } + + if (y0 == 0) { + y0 = y0 + this.config.pageTopMargin; + yW = yW - this.config.pageTopMargin; + } + + pdfTextStripper.addRegion("pdfPageRegion", new Rectangle(x0, y0, xW, yW)); + pdfTextStripper.extractRegions(page); + var text = pdfTextStripper.getTextForRegion("pdfPageRegion"); + if (StringUtils.hasText(text)) { + sb.append(text); + } + pdfTextStripper.removeRegion("pdfPageRegion"); + + } + + String text = sb.toString(); + + if (StringUtils.hasText(text)) { + text = this.config.pageExtractedTextFormatter.format(text, startPage); + } + + return text; + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/PdfTestUtils.java b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/PdfTestUtils.java new file mode 100644 index 000000000..e4a9c569a --- /dev/null +++ b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/PdfTestUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.reader.pdf; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; + +import org.springframework.ai.document.Document; + +/** + * Utility class for working with PDF documents in tests. + * + * This utility class provides methods for writing extracted PDF documents to a file for + * testing purposes. It can write documents with or without document markers. + * + * @author Christian Tzolov + */ +public class PdfTestUtils { + + private PdfTestUtils() { + } + + /** + * Writes extracted PDF documents to a file. + * @param fileName The name of the file to write the documents to. + * @param docs The list of {@link Document} objects to write. + * @param withDocumentMarkers Whether to include document markers in the output. + * @throws IOException If an I/O error occurs while writing to the file. + */ + public static void writeToFile(String fileName, List docs, boolean withDocumentMarkers) + throws IOException { + try (var writer = new FileWriter(fileName, false)) { + + int i = 0; + for (Document doc : docs) { + if (withDocumentMarkers) { + writer.write(String.format("%n### Doc: %s, pages:[%s,%s]\n", i, + doc.getMetadata().get(PagePdfDocumentReader.METADATA_START_PAGE_NUMBER), + doc.getMetadata().get(PagePdfDocumentReader.METADATA_END_PAGE_NUMBER))); + } + writer.write(doc.getContent()); + i++; + } + } + } + +} diff --git a/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/config/ParagraphManager.java b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/config/ParagraphManager.java new file mode 100644 index 000000000..1ccf87466 --- /dev/null +++ b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/config/ParagraphManager.java @@ -0,0 +1,199 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.reader.pdf.config; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageTree; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode; + +import org.springframework.util.CollectionUtils; + +/** + * The ParagraphManager class is responsible for managing the paragraphs and hierarchy of + * a PDF document. It can process bookmarks and generate a structured tree of paragraphs, + * representing the table of contents (TOC) of the PDF document. + * + * @author Christian Tzolov + */ +public class ParagraphManager { + + /** + * Represents a document paragraph metadata and hierarchy. + * + * @param parent Parent paragraph that will contain a children paragraphs. + * @param title Paragraph title as it appears in the PDF document. + * @param level The TOC deepness level for this paragraph. The root is at level 0. + * @param startPageNumber The page number in the PDF where this paragraph begins. + * @param endPageNumber The page number in the PDF where this paragraph ends. + * @param children Sub-paragraphs for this paragraph. + */ + public record Paragraph(Paragraph parent, String title, int level, int startPageNumber, int endPageNumber, + int position, List children) { + + public Paragraph(Paragraph parent, String title, int level, int startPageNumber, int endPageNumber, + int position) { + this(parent, title, level, startPageNumber, endPageNumber, position, new ArrayList<>()); + } + + @Override + public String toString() { + String indent = (level < 0) ? "" : new String(new char[level * 2]).replace('\0', ' '); + + return indent + " " + level + ") " + title + " [" + startPageNumber + "," + endPageNumber + "], children = " + + children.size() + ", pos = " + position; + } + + } + + /** + * Root of the paragraphs tree. + */ + private final Paragraph rootParagraph; + + private final PDDocument document; + + public ParagraphManager(PDDocument document) { + try { + + this.document = document; + + this.rootParagraph = this.generateParagraphs( + new Paragraph(null, "root", -1, 1, this.document.getNumberOfPages(), 0), + this.document.getDocumentCatalog().getDocumentOutline(), 0); + + printParagraph(rootParagraph, System.out); + } + catch (Exception e) { + throw new RuntimeException(e); + } + + } + + public List flatten() { + List paragraphs = new ArrayList<>(); + for (var child : this.rootParagraph.children()) { + flatten(child, paragraphs); + } + return paragraphs; + } + + private void flatten(Paragraph current, List paragraphs) { + paragraphs.add(current); + for (var child : current.children()) { + flatten(child, paragraphs); + } + } + + private void printParagraph(Paragraph paragraph, PrintStream printStream) { + printStream.println(paragraph); + for (Paragraph childParagraph : paragraph.children()) { + printParagraph(childParagraph, printStream); + } + } + + /** + * For given {@link PDOutlineNode} bookmark convert all sibling {@link PDOutlineItem} + * items into {@link Paragraph} instances under the parentParagraph. For each + * {@link PDOutlineItem} item, recursively call + * {@link ParagraphManager#generateParagraphs} to process its children items. + * @param parentParagraph Root paragraph that the bookmark sibling items should be + * added to. + * @param bookmark TOC paragraphs to process. + * @param level Current TOC deepness level. + * @return Returns a tree of {@link Paragraph}s that represent the PDF document TOC. + * @throws IOException + */ + protected Paragraph generateParagraphs(Paragraph parentParagraph, PDOutlineNode bookmark, Integer level) + throws IOException { + + PDOutlineItem current = bookmark.getFirstChild(); + + while (current != null) { + + int pageNumber = getPageNumber(current); + var nextSiblingNumber = getPageNumber(current.getNextSibling()); + if (nextSiblingNumber < 0) { + nextSiblingNumber = getPageNumber(current.getLastChild()); + } + + var paragraphPosition = (current.getDestination() instanceof PDPageXYZDestination) + ? ((PDPageXYZDestination) current.getDestination()).getTop() : 0; + + var currentParagraph = new Paragraph(parentParagraph, current.getTitle(), level, pageNumber, + nextSiblingNumber, paragraphPosition); + + parentParagraph.children().add(currentParagraph); + + // Recursive call to go the current paragraph's children paragraphs. + // E.g. go one level deeper. + this.generateParagraphs(currentParagraph, current, level + 1); + + current = current.getNextSibling(); + } + return parentParagraph; + } + + private int getPageNumber(PDOutlineItem current) throws IOException { + if (current == null) { + return -1; + } + PDPage currentPage = current.findDestinationPage(this.document); + PDPageTree pages = this.document.getDocumentCatalog().getPages(); + for (int i = 0; i < pages.getCount(); i++) { + var page = pages.get(i); + if (page.equals(currentPage)) { + return i + 1; + } + } + return -1; + } + + public List getParagraphsByLevel(Paragraph paragraph, int level, boolean interLevelText) { + + List resultList = new ArrayList<>(); + + if (paragraph.level() < level) { + if (!CollectionUtils.isEmpty(paragraph.children())) { + + if (interLevelText) { + var interLevelParagraph = new Paragraph(paragraph.parent(), paragraph.title(), paragraph.level(), + paragraph.startPageNumber(), paragraph.children().get(0).startPageNumber(), + paragraph.position()); + resultList.add(interLevelParagraph); + } + + for (Paragraph child : paragraph.children()) { + resultList.addAll(getParagraphsByLevel(child, level, interLevelText)); + } + } + } + else if (paragraph.level() == level) { + resultList.add(paragraph); + } + + return resultList; + } + +} diff --git a/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/config/PdfDocumentReaderConfig.java b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/config/PdfDocumentReaderConfig.java new file mode 100644 index 000000000..2253ffe5c --- /dev/null +++ b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/config/PdfDocumentReaderConfig.java @@ -0,0 +1,133 @@ +package org.springframework.ai.reader.pdf.config; + +import org.springframework.ai.reader.pdf.PagePdfDocumentReader; +import org.springframework.ai.reader.pdf.ParagraphPdfDocumentReader; +import org.springframework.ai.reader.pdf.layout.PageExtractedTextFormatter; +import org.springframework.util.Assert; + +/** + * Common configuration builder for the {@link PagePdfDocumentReader} and the + * {@link ParagraphPdfDocumentReader}. + * + * @author Christian Tzolov + */ +public class PdfDocumentReaderConfig { + + public static final int ALL_PAGES = 0; + + public final boolean reversedParagraphPosition; + + public final int pagesPerDocument; + + public final int pageTopMargin; + + public final int pageBottomMargin; + + public final PageExtractedTextFormatter pageExtractedTextFormatter; + + /** + * Start building a new configuration. + * @return The entry point for creating a new configuration. + */ + public static PdfDocumentReaderConfig.Builder builder() { + + return new Builder(); + } + + /** + * {@return the default config} + */ + public static PdfDocumentReaderConfig defaultConfig() { + return builder().build(); + } + + private PdfDocumentReaderConfig(PdfDocumentReaderConfig.Builder builder) { + this.pagesPerDocument = builder.pagesPerDocument; + this.pageBottomMargin = builder.pageBottomMargin; + this.pageTopMargin = builder.pageTopMargin; + this.pageExtractedTextFormatter = builder.pageExtractedTextFormatter; + this.reversedParagraphPosition = builder.reversedParagraphPosition; + } + + public static class Builder { + + private int pagesPerDocument = 1; + + private int pageTopMargin = 0; + + private int pageBottomMargin = 0; + + private PageExtractedTextFormatter pageExtractedTextFormatter = PageExtractedTextFormatter.defaults(); + + private boolean reversedParagraphPosition = false; + + private Builder() { + } + + /** + * Formatter of the extracted text. + * @param pageExtractedTextFormatter Instance of the PageExtractedTextFormatter. + * @return this builder + */ + public PdfDocumentReaderConfig.Builder withPageExtractedTextFormatter( + PageExtractedTextFormatter pageExtractedTextFormatter) { + Assert.notNull(pagesPerDocument >= 0, "PageExtractedTextFormatter must not be null."); + this.pageExtractedTextFormatter = pageExtractedTextFormatter; + return this; + } + + /** + * How many pages to put in a single Document instance. 0 stands for all pages. + * Defaults to 0. + * @param pagesPerDocument Number of page's content to group in single Document. + * @return this builder + */ + public PdfDocumentReaderConfig.Builder withPagesPerDocument(int pagesPerDocument) { + Assert.isTrue(pagesPerDocument >= 0, "Page count must be a positive value."); + this.pagesPerDocument = pagesPerDocument; + return this; + } + + /** + * Configures the Pdf reader page top margin. Defaults to 0. + * @param topMargin page top margin to use + * @return this builder + */ + public PdfDocumentReaderConfig.Builder withPageTopMargin(int topMargin) { + Assert.isTrue(topMargin >= 0, "Page margins must be a positive value."); + this.pageTopMargin = topMargin; + return this; + } + + /** + * Configures the Pdf reader page bottom margin. Defaults to 0. + * @param bottomMargin page top margin to use + * @return this builder + */ + public PdfDocumentReaderConfig.Builder withPageBottomMargin(int bottomMargin) { + Assert.isTrue(bottomMargin >= 0, "Page margins must be a positive value."); + this.pageBottomMargin = bottomMargin; + return this; + } + + /** + * Configures the Pdf reader reverse paragraph position. Defaults to false. + * @param reversedParagraphPosition to reverse or not the paragraph position + * withing a page. + * @return this builder + */ + public Builder withReversedParagraphPosition(boolean reversedParagraphPosition) { + this.reversedParagraphPosition = reversedParagraphPosition; + return this; + } + + /** + * {@return the immutable configuration} + */ + public PdfDocumentReaderConfig build() { + return new PdfDocumentReaderConfig(this); + } + + } + +} \ No newline at end of file diff --git a/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/ForkPDFLayoutTextStripper.java b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/ForkPDFLayoutTextStripper.java new file mode 100644 index 000000000..43119c61b --- /dev/null +++ b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/ForkPDFLayoutTextStripper.java @@ -0,0 +1,473 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +package org.springframework.ai.reader.pdf.layout; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.text.PDFTextStripper; +import org.apache.pdfbox.text.TextPosition; +import org.apache.pdfbox.text.TextPositionComparator; + +/** + * This class extends PDFTextStripper to provide custom text extraction and formatting + * capabilities for PDF pages. It includes features like processing text lines, sorting + * text positions, and managing line breaks. + * + * @author Jonathan Link + * + */ +public class ForkPDFLayoutTextStripper extends PDFTextStripper { + + public static final boolean DEBUG = false; + + public static final int OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT = 4; + + private double currentPageWidth; + + private TextPosition previousTextPosition; + + private List textLineList; + + /** + * Constructor + */ + public ForkPDFLayoutTextStripper() throws IOException { + super(); + this.previousTextPosition = null; + this.textLineList = new ArrayList(); + } + + /** + * @param page page to parse + */ + @Override + public void processPage(PDPage page) throws IOException { + PDRectangle pageRectangle = page.getMediaBox(); + if (pageRectangle != null) { + this.setCurrentPageWidth(pageRectangle.getWidth() * 1.4); + super.processPage(page); + this.previousTextPosition = null; + this.textLineList = new ArrayList(); + } + } + + @Override + protected void writePage() throws IOException { + List> charactersByArticle = super.getCharactersByArticle(); + for (int i = 0; i < charactersByArticle.size(); i++) { + List textList = charactersByArticle.get(i); + try { + this.sortTextPositionList(textList); + } + catch (java.lang.IllegalArgumentException e) { + System.err.println(e); + } + this.iterateThroughTextList(textList.iterator()); + } + this.writeToOutputStream(this.getTextLineList()); + } + + private void writeToOutputStream(final List textLineList) throws IOException { + for (TextLine textLine : textLineList) { + char[] line = textLine.getLine().toCharArray(); + super.getOutput().write(line); + super.getOutput().write('\n'); + super.getOutput().flush(); + } + } + + /* + * In order to get rid of the warning: TextPositionComparator class should implement + * Comparator instead of Comparator + */ + @SuppressWarnings("unchecked") + private void sortTextPositionList(final List textList) { + TextPositionComparator comparator = new TextPositionComparator(); + Collections.sort(textList, comparator); + } + + private void writeLine(final List textPositionList) { + if (textPositionList.size() > 0) { + TextLine textLine = this.addNewLine(); + boolean firstCharacterOfLineFound = false; + for (TextPosition textPosition : textPositionList) { + CharacterFactory characterFactory = new CharacterFactory(firstCharacterOfLineFound); + Character character = characterFactory.createCharacterFromTextPosition(textPosition, + this.getPreviousTextPosition()); + textLine.writeCharacterAtIndex(character); + this.setPreviousTextPosition(textPosition); + firstCharacterOfLineFound = true; + } + } + else { + this.addNewLine(); // white line + } + } + + private void iterateThroughTextList(Iterator textIterator) { + List textPositionList = new ArrayList(); + + while (textIterator.hasNext()) { + TextPosition textPosition = (TextPosition) textIterator.next(); + int numberOfNewLines = this.getNumberOfNewLinesFromPreviousTextPosition(textPosition); + if (numberOfNewLines == 0) { + textPositionList.add(textPosition); + } + else { + this.writeTextPositionList(textPositionList); + this.createNewEmptyNewLines(numberOfNewLines); + textPositionList.add(textPosition); + } + this.setPreviousTextPosition(textPosition); + } + if (!textPositionList.isEmpty()) { + this.writeTextPositionList(textPositionList); + } + } + + private void writeTextPositionList(final List textPositionList) { + this.writeLine(textPositionList); + textPositionList.clear(); + } + + private void createNewEmptyNewLines(int numberOfNewLines) { + for (int i = 0; i < numberOfNewLines - 1; ++i) { + this.addNewLine(); + } + } + + private int getNumberOfNewLinesFromPreviousTextPosition(final TextPosition textPosition) { + TextPosition previousTextPosition = this.getPreviousTextPosition(); + if (previousTextPosition == null) { + return 1; + } + + float textYPosition = Math.round(textPosition.getY()); + float previousTextYPosition = Math.round(previousTextPosition.getY()); + + if (textYPosition > previousTextYPosition && (textYPosition - previousTextYPosition > 5.5)) { + double height = textPosition.getHeight(); + int numberOfLines = (int) (Math.floor(textYPosition - previousTextYPosition) / height); + numberOfLines = Math.max(1, numberOfLines - 1); // exclude current new line + if (DEBUG) + System.out.println(height + " " + numberOfLines); + return numberOfLines; + } + else { + return 0; + } + } + + private TextLine addNewLine() { + TextLine textLine = new TextLine(this.getCurrentPageWidth()); + textLineList.add(textLine); + return textLine; + } + + private TextPosition getPreviousTextPosition() { + return this.previousTextPosition; + } + + private void setPreviousTextPosition(final TextPosition setPreviousTextPosition) { + this.previousTextPosition = setPreviousTextPosition; + } + + private int getCurrentPageWidth() { + return (int) Math.round(this.currentPageWidth); + } + + private void setCurrentPageWidth(double currentPageWidth) { + this.currentPageWidth = currentPageWidth; + } + + private List getTextLineList() { + return this.textLineList; + } + +} + +class TextLine { + + private static final char SPACE_CHARACTER = ' '; + + private int lineLength; + + private String line; + + private int lastIndex; + + public TextLine(int lineLength) { + this.line = ""; + this.lineLength = lineLength / ForkPDFLayoutTextStripper.OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT; + this.completeLineWithSpaces(); + } + + public void writeCharacterAtIndex(final Character character) { + character.setIndex(this.computeIndexForCharacter(character)); + int index = character.getIndex(); + char characterValue = character.getCharacterValue(); + if (this.indexIsInBounds(index) && this.line.charAt(index) == SPACE_CHARACTER) { + this.line = this.line.substring(0, index) + characterValue + + this.line.substring(index + 1, this.getLineLength()); + } + } + + public int getLineLength() { + return this.lineLength; + } + + public String getLine() { + return line; + } + + private int computeIndexForCharacter(final Character character) { + int index = character.getIndex(); + boolean isCharacterPartOfPreviousWord = character.isCharacterPartOfPreviousWord(); + boolean isCharacterAtTheBeginningOfNewLine = character.isCharacterAtTheBeginningOfNewLine(); + boolean isCharacterCloseToPreviousWord = character.isCharacterCloseToPreviousWord(); + + if (!this.indexIsInBounds(index)) { + return -1; + } + else { + if (isCharacterPartOfPreviousWord && !isCharacterAtTheBeginningOfNewLine) { + index = this.findMinimumIndexWithSpaceCharacterFromIndex(index); + } + else if (isCharacterCloseToPreviousWord) { + if (this.line.charAt(index) != SPACE_CHARACTER) { + index = index + 1; + } + else { + index = this.findMinimumIndexWithSpaceCharacterFromIndex(index) + 1; + } + } + index = this.getNextValidIndex(index, isCharacterPartOfPreviousWord); + return index; + } + } + + private boolean isSpaceCharacterAtIndex(int index) { + return this.line.charAt(index) != SPACE_CHARACTER; + } + + private boolean isNewIndexGreaterThanLastIndex(int index) { + int lastIndex = this.getLastIndex(); + return (index > lastIndex); + } + + private int getNextValidIndex(int index, boolean isCharacterPartOfPreviousWord) { + int nextValidIndex = index; + int lastIndex = this.getLastIndex(); + if (!this.isNewIndexGreaterThanLastIndex(index)) { + nextValidIndex = lastIndex + 1; + } + if (!isCharacterPartOfPreviousWord && this.isSpaceCharacterAtIndex(index - 1)) { + nextValidIndex = nextValidIndex + 1; + } + this.setLastIndex(nextValidIndex); + return nextValidIndex; + } + + private int findMinimumIndexWithSpaceCharacterFromIndex(int index) { + int newIndex = index; + while (newIndex >= 0 && this.line.charAt(newIndex) == SPACE_CHARACTER) { + newIndex = newIndex - 1; + } + return newIndex + 1; + } + + private boolean indexIsInBounds(int index) { + return (index >= 0 && index < this.lineLength); + } + + private void completeLineWithSpaces() { + for (int i = 0; i < this.getLineLength(); ++i) { + line += SPACE_CHARACTER; + } + } + + private int getLastIndex() { + return this.lastIndex; + } + + private void setLastIndex(int lastIndex) { + this.lastIndex = lastIndex; + } + +} + +class Character { + + private char characterValue; + + private int index; + + private boolean isCharacterPartOfPreviousWord; + + private boolean isFirstCharacterOfAWord; + + private boolean isCharacterAtTheBeginningOfNewLine; + + private boolean isCharacterCloseToPreviousWord; + + public Character(char characterValue, int index, boolean isCharacterPartOfPreviousWord, + boolean isFirstCharacterOfAWord, boolean isCharacterAtTheBeginningOfNewLine, + boolean isCharacterPartOfASentence) { + this.characterValue = characterValue; + this.index = index; + this.isCharacterPartOfPreviousWord = isCharacterPartOfPreviousWord; + this.isFirstCharacterOfAWord = isFirstCharacterOfAWord; + this.isCharacterAtTheBeginningOfNewLine = isCharacterAtTheBeginningOfNewLine; + this.isCharacterCloseToPreviousWord = isCharacterPartOfASentence; + if (ForkPDFLayoutTextStripper.DEBUG) + System.out.println(this.toString()); + } + + public char getCharacterValue() { + return this.characterValue; + } + + public int getIndex() { + return this.index; + } + + public void setIndex(int index) { + this.index = index; + } + + public boolean isCharacterPartOfPreviousWord() { + return this.isCharacterPartOfPreviousWord; + } + + public boolean isFirstCharacterOfAWord() { + return this.isFirstCharacterOfAWord; + } + + public boolean isCharacterAtTheBeginningOfNewLine() { + return this.isCharacterAtTheBeginningOfNewLine; + } + + public boolean isCharacterCloseToPreviousWord() { + return this.isCharacterCloseToPreviousWord; + } + + public String toString() { + String toString = ""; + toString += index; + toString += " "; + toString += characterValue; + toString += " isCharacterPartOfPreviousWord=" + isCharacterPartOfPreviousWord; + toString += " isFirstCharacterOfAWord=" + isFirstCharacterOfAWord; + toString += " isCharacterAtTheBeginningOfNewLine=" + isCharacterAtTheBeginningOfNewLine; + toString += " isCharacterPartOfASentence=" + isCharacterCloseToPreviousWord; + toString += " isCharacterCloseToPreviousWord=" + isCharacterCloseToPreviousWord; + return toString; + } + +} + +class CharacterFactory { + + private TextPosition previousTextPosition; + + private boolean firstCharacterOfLineFound; + + private boolean isCharacterPartOfPreviousWord; + + private boolean isFirstCharacterOfAWord; + + private boolean isCharacterAtTheBeginningOfNewLine; + + private boolean isCharacterCloseToPreviousWord; + + public CharacterFactory(boolean firstCharacterOfLineFound) { + this.firstCharacterOfLineFound = firstCharacterOfLineFound; + } + + public Character createCharacterFromTextPosition(final TextPosition textPosition, + final TextPosition previousTextPosition) { + this.setPreviousTextPosition(previousTextPosition); + this.isCharacterPartOfPreviousWord = this.isCharacterPartOfPreviousWord(textPosition); + this.isFirstCharacterOfAWord = this.isFirstCharacterOfAWord(textPosition); + this.isCharacterAtTheBeginningOfNewLine = this.isCharacterAtTheBeginningOfNewLine(textPosition); + this.isCharacterCloseToPreviousWord = this.isCharacterCloseToPreviousWord(textPosition); + char character = this.getCharacterFromTextPosition(textPosition); + int index = (int) textPosition.getX() / ForkPDFLayoutTextStripper.OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT; + return new Character(character, index, isCharacterPartOfPreviousWord, isFirstCharacterOfAWord, + isCharacterAtTheBeginningOfNewLine, isCharacterCloseToPreviousWord); + } + + private boolean isCharacterAtTheBeginningOfNewLine(final TextPosition textPosition) { + if (!firstCharacterOfLineFound) { + return true; + } + TextPosition previousTextPosition = this.getPreviousTextPosition(); + float previousTextYPosition = previousTextPosition.getY(); + return (Math.round(textPosition.getY()) < Math.round(previousTextYPosition)); + } + + private boolean isFirstCharacterOfAWord(final TextPosition textPosition) { + if (!firstCharacterOfLineFound) { + return true; + } + double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(previousTextPosition, textPosition); + return (numberOfSpaces > 1) || this.isCharacterAtTheBeginningOfNewLine(textPosition); + } + + private boolean isCharacterCloseToPreviousWord(final TextPosition textPosition) { + if (!firstCharacterOfLineFound) { + return false; + } + double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(previousTextPosition, textPosition); + return (numberOfSpaces > 1 && numberOfSpaces <= ForkPDFLayoutTextStripper.OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT); + } + + private boolean isCharacterPartOfPreviousWord(final TextPosition textPosition) { + TextPosition previousTextPosition = this.getPreviousTextPosition(); + if (previousTextPosition.getUnicode().equals(" ")) { + return false; + } + double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(previousTextPosition, textPosition); + return (numberOfSpaces <= 1); + } + + private double numberOfSpacesBetweenTwoCharacters(final TextPosition textPosition1, + final TextPosition textPosition2) { + double previousTextXPosition = textPosition1.getX(); + double previousTextWidth = textPosition1.getWidth(); + double previousTextEndXPosition = (previousTextXPosition + previousTextWidth); + double numberOfSpaces = Math.abs(Math.round(textPosition2.getX() - previousTextEndXPosition)); + return numberOfSpaces; + } + + private char getCharacterFromTextPosition(final TextPosition textPosition) { + String string = textPosition.getUnicode(); + char character = string.charAt(0); + return character; + } + + private TextPosition getPreviousTextPosition() { + return this.previousTextPosition; + } + + private void setPreviousTextPosition(final TextPosition previousTextPosition) { + this.previousTextPosition = previousTextPosition; + } + +} \ No newline at end of file diff --git a/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/PDFLayoutTextStripperByArea.java b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/PDFLayoutTextStripperByArea.java new file mode 100644 index 000000000..54183b0e0 --- /dev/null +++ b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/PDFLayoutTextStripperByArea.java @@ -0,0 +1,154 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.reader.pdf.layout; + +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.text.TextPosition; + +/** + * Re-implement the PDFLayoutTextStripperByArea on top of the PDFLayoutTextStripper + * instead the original PDFTextStripper. + * + * This class allows cropping pages (e.g., removing headers, footers, and between-page + * empty spaces) while extracting layout text, preserving the PDF's internal text + * formatting. + * + * @author Christian Tzolov + */ +public class PDFLayoutTextStripperByArea extends ForkPDFLayoutTextStripper { + + private final List regions = new ArrayList(); + + private final Map regionArea = new HashMap(); + + private final Map>> regionCharacterList = new HashMap>>(); + + private final Map regionText = new HashMap(); + + /** + * Constructor. + * @throws IOException If there is an error loading properties. + */ + public PDFLayoutTextStripperByArea() throws IOException { + super.setShouldSeparateByBeads(false); + } + + /** + * This method does nothing in this derived class, because beads and regions are + * incompatible. Beads are ignored when stripping by area. + * @param aShouldSeparateByBeads The new grouping of beads. + */ + @Override + public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) { + } + + /** + * Add a new region to group text by. + * @param regionName The name of the region. + * @param rect The rectangle area to retrieve the text from. The y-coordinates are + * java coordinates (y == 0 is top), not PDF coordinates (y == 0 is bottom). + */ + public void addRegion(String regionName, Rectangle2D rect) { + regions.add(regionName); + regionArea.put(regionName, rect); + } + + /** + * Delete a region to group text by. If the region does not exist, this method does + * nothing. + * @param regionName The name of the region to delete. + */ + public void removeRegion(String regionName) { + regions.remove(regionName); + regionArea.remove(regionName); + } + + /** + * Get the list of regions that have been setup. + * @return A list of java.lang.String objects to identify the region names. + */ + public List getRegions() { + return regions; + } + + /** + * Get the text for the region, this should be called after extractRegions(). + * @param regionName The name of the region to get the text from. + * @return The text that was identified in that region. + */ + public String getTextForRegion(String regionName) { + StringWriter text = regionText.get(regionName); + return text.toString(); + } + + /** + * Process the page to extract the region text. + * @param page The page to extract the regions from. + * @throws IOException If there is an error while extracting text. + */ + public void extractRegions(PDPage page) throws IOException { + for (String regionName : regions) { + setStartPage(getCurrentPageNo()); + setEndPage(getCurrentPageNo()); + // reset the stored text for the region so this class can be reused. + ArrayList> regionCharactersByArticle = new ArrayList>(); + regionCharactersByArticle.add(new ArrayList()); + regionCharacterList.put(regionName, regionCharactersByArticle); + regionText.put(regionName, new StringWriter()); + } + + if (page.hasContents()) { + processPage(page); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void processTextPosition(TextPosition text) { + for (Map.Entry regionAreaEntry : regionArea.entrySet()) { + Rectangle2D rect = regionAreaEntry.getValue(); + if (rect.contains(text.getX(), text.getY())) { + charactersByArticle = regionCharacterList.get(regionAreaEntry.getKey()); + super.processTextPosition(text); + } + } + } + + /** + * This will print the processed page text to the output stream. + * @throws IOException If there is an error writing the text. + */ + @Override + protected void writePage() throws IOException { + for (String region : regionArea.keySet()) { + charactersByArticle = regionCharacterList.get(region); + output = regionText.get(region); + super.writePage(); + } + } + +} diff --git a/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/PageExtractedTextFormatter.java b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/PageExtractedTextFormatter.java new file mode 100644 index 000000000..3a35c1d8d --- /dev/null +++ b/document-readers/pdf-reader/src/main/java/org/springframework/ai/reader/pdf/layout/PageExtractedTextFormatter.java @@ -0,0 +1,186 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.reader.pdf.layout; + +import org.springframework.util.StringUtils; + +/** + * Provides text formatting options for extracted PDF page text, including left alignment + * and the ability to trim and delete lines from the top and bottom of the text. + * + * This class allows customization of text formatting applied to extracted PDF page text. + * It can align the text to the left, remove specified lines from the top and bottom of + * the text, and trim adjacent blank lines. + * + * @author Christian Tzolov + */ +public class PageExtractedTextFormatter { + + private boolean leftAlignment; + + private int numberOfTopPagesToSkipBeforeDelete; + + private int numberOfTopTextLinesToDelete; + + private int numberOfBottomTextLinesToDelete; + + private PageExtractedTextFormatter(Builder builder) { + this.leftAlignment = builder.leftAlignment; + this.numberOfBottomTextLinesToDelete = builder.numberOfBottomTextLinesToDelete; + this.numberOfTopPagesToSkipBeforeDelete = builder.numberOfTopPagesToSkipBeforeDelete; + this.numberOfTopTextLinesToDelete = builder.numberOfTopTextLinesToDelete; + } + + public static Builder builder() { + return new Builder(); + } + + public static PageExtractedTextFormatter defaults() { + return new Builder().build(); + } + + public String format(String pageText, int pageNumber) { + + var text = trimAdjacentBlankLines(pageText); + + if (pageNumber >= this.numberOfTopPagesToSkipBeforeDelete) { + text = deleteTopTextLines(text, this.numberOfTopTextLinesToDelete); + text = deleteBottomTextLines(text, this.numberOfBottomTextLinesToDelete); + } + + if (this.leftAlignment) { + text = alignToLeft(text); + } + + return text; + } + + public static class Builder { + + private boolean leftAlignment = false; + + private int numberOfTopPagesToSkipBeforeDelete = 0; + + private int numberOfTopTextLinesToDelete = 0; + + private int numberOfBottomTextLinesToDelete = 0; + + /** + * Align the document text to the left. Defaults to false. + * @param leftAlignment Flag to align the text to the left. + * @return this builder + */ + public Builder withLeftAlignment(boolean leftAlignment) { + this.leftAlignment = leftAlignment; + return this; + } + + /** + * Withdraw the top N pages from the text top/bottom line deletion. Defaults to 0. + * @param numberOfTopPagesToSkipBeforeDelete Number of pages to skip from + * top/bottom line deletion policy. + * @return this builder + */ + public Builder withNumberOfTopPagesToSkipBeforeDelete(int numberOfTopPagesToSkipBeforeDelete) { + this.numberOfTopPagesToSkipBeforeDelete = numberOfTopPagesToSkipBeforeDelete; + return this; + } + + /** + * Remove the top N lines from the page text. Defaults to 0. + * @param numberOfTopTextLinesToDelete Number of top text lines to delete. + * @return this builder + */ + public Builder withNumberOfTopTextLinesToDelete(int numberOfTopTextLinesToDelete) { + this.numberOfTopTextLinesToDelete = numberOfTopTextLinesToDelete; + return this; + } + + /** + * Remove the bottom N lines from the page text. Defaults to 0. + * @param numberOfBottomTextLinesToDelete Number of bottom text lines to delete. + * @return this builder + */ + public Builder withNumberOfBottomTextLinesToDelete(int numberOfBottomTextLinesToDelete) { + this.numberOfBottomTextLinesToDelete = numberOfBottomTextLinesToDelete; + return this; + } + + public PageExtractedTextFormatter build() { + return new PageExtractedTextFormatter(this); + } + + } + + /** + * Replaces multiple, adjacent blank lines into a single blank line. + * @param pageText text to adjust the blank lines for. + * @return Returns the same text but with blank lines trimmed. + */ + public static String trimAdjacentBlankLines(String pageText) { + + return pageText.replaceAll("(?m)(^ *\n)", "\n").replaceAll("(?m)^$([\r\n]+?)(^$[\r\n]+?^)+", "$1"); + + } + + /** + * @param pageText text to align. + * @return Returns the same text but aligned to the left side. + */ + public static String alignToLeft(String pageText) { + return pageText.replaceAll("(?m)(^ *| +(?= |$))", "").replaceAll("(?m)^$( ?)(^$[\r\n]+?^)+", "$1"); + } + + /** + * Removes the specified number of lines from the bottom part of the text. + * @param pageText Text to remove lines from. + * @param numberOfLines Number of lines to remove. + * @return Returns the text striped from last lines. + */ + public static String deleteBottomTextLines(String pageText, int numberOfLines) { + if (!StringUtils.hasText(pageText)) { + return pageText; + } + + int lineCount = 0; + int truncateIndex = pageText.length(); + int nextTruncateIndex = truncateIndex; + while (lineCount < numberOfLines && nextTruncateIndex >= 0) { + nextTruncateIndex = pageText.lastIndexOf(System.lineSeparator(), truncateIndex - 1); + truncateIndex = nextTruncateIndex < 0 ? truncateIndex : nextTruncateIndex; + lineCount++; + } + return pageText.substring(0, truncateIndex); + } + + public static String deleteTopTextLines(String pageText, int numberOfLines) { + if (!StringUtils.hasText(pageText)) { + return pageText; + } + int lineCount = 0; + + int truncateIndex = 0; + int nextTruncateIndex = truncateIndex; + while (lineCount < numberOfLines && nextTruncateIndex >= 0) { + nextTruncateIndex = pageText.indexOf(System.lineSeparator(), truncateIndex + 1); + truncateIndex = nextTruncateIndex < 0 ? truncateIndex : nextTruncateIndex; + lineCount++; + } + return pageText.substring(truncateIndex, pageText.length()); + } + +} diff --git a/document-readers/pdf-reader/src/test/java/org/springframework/ai/reader/pdf/PagePdfDocumentReaderTests.java b/document-readers/pdf-reader/src/test/java/org/springframework/ai/reader/pdf/PagePdfDocumentReaderTests.java new file mode 100644 index 000000000..e02b05ef7 --- /dev/null +++ b/document-readers/pdf-reader/src/test/java/org/springframework/ai/reader/pdf/PagePdfDocumentReaderTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.reader.pdf; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig; +import org.springframework.ai.reader.pdf.layout.PageExtractedTextFormatter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Tzolov + */ +public class PagePdfDocumentReaderTests { + + @Test + public void test1() { + + PagePdfDocumentReader pdfReader = new PagePdfDocumentReader("classpath:/sample1.pdf", + PdfDocumentReaderConfig.builder() + .withPageTopMargin(0) + .withPageBottomMargin(0) + .withPageExtractedTextFormatter(PageExtractedTextFormatter.builder() + .withNumberOfTopTextLinesToDelete(0) + .withNumberOfBottomTextLinesToDelete(3) + .withNumberOfTopPagesToSkipBeforeDelete(0) + .build()) + .withPagesPerDocument(1) + .build()); + + List docs = pdfReader.get(); + + assertThat(docs).hasSize(4); + + String allText = docs.stream().map(d -> d.getContent()).collect(Collectors.joining("\n")); + + assertThat(allText).doesNotContain( + List.of("Page 1 of 4", "Page 2 of 4", "Page 3 of 4", "Page 4 of 4", "PDF Bookmark Sample")); + } + +} diff --git a/document-readers/pdf-reader/src/test/resources/sample1.pdf b/document-readers/pdf-reader/src/test/resources/sample1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8efd05c3dfb7e359ac27a95af9dd517ca4e88394 GIT binary patch literal 88226 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 OPF8qw@(=Rj@c#!7$iJ2V literal 0 HcmV?d00001 diff --git a/document-readers/pdf-reader/src/test/resources/sample2.pdf b/document-readers/pdf-reader/src/test/resources/sample2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..99d31cef1efdbb90fba2231a19974f4f74229be0 GIT binary patch literal 54836 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/spring-ai-milvus-store vector-stores/spring-ai-neo4j-store embedding-clients/spring-ai-postgresml-embedding-client + document-readers/pdf-reader diff --git a/spring-ai-core/pom.xml b/spring-ai-core/pom.xml index 1cc19e60e..9f209e4dc 100644 --- a/spring-ai-core/pom.xml +++ b/spring-ai-core/pom.xml @@ -24,7 +24,6 @@ - org.antlr diff --git a/spring-ai-core/src/test/java/org/springframework/ai/reader/TextReaderTests.java b/spring-ai-core/src/test/java/org/springframework/ai/reader/TextReaderTests.java index 988762c63..c7187861f 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/reader/TextReaderTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/reader/TextReaderTests.java @@ -21,7 +21,6 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.ai.document.Document; -import org.springframework.ai.reader.TextReader; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; diff --git a/spring-ai-core/src/test/resources/text_source.txt b/spring-ai-core/src/test/resources/text_source.txt index 9a661ffb5..5f777418d 100644 --- a/spring-ai-core/src/test/resources/text_source.txt +++ b/spring-ai-core/src/test/resources/text_source.txt @@ -4121,4 +4121,4 @@ target bean, bypassing the proxy. Hence, it would be inconsistent to apply the interceptors to the init method, because doing so would couple the lifecycle of the target bean to its proxy or interceptors and leave strange semantics when your code interacts directly with the raw target - bean. + bean. \ No newline at end of file