From beb75b2150fb06a937d53105f3c0ba1d2bfce13b Mon Sep 17 00:00:00 2001 From: Philippe PRADOS Date: Sat, 8 Feb 2025 03:31:12 +0100 Subject: [PATCH] community[minor]: 05 - Refactoring PyPDFium2 parser (#29625) This is one part of a larger Pull Request (PR) that is too large to be submitted all at once. This specific part focuses on updating the PyPDFium2 parser. For more details, see https://github.com/langchain-ai/langchain/pull/28970. --- .../document_loaders/pypdfium2.ipynb | 1028 ++++++++++++++++- .../document_loaders/parsers/pdf.py | 220 +++- .../document_loaders/pdf.py | 103 +- .../parsers/test_pdf_parsers.py | 14 +- .../document_loaders/test_pdf.py | 19 +- .../parsers/test_pdf_parsers.py | 13 +- 6 files changed, 1276 insertions(+), 121 deletions(-) diff --git a/docs/docs/integrations/document_loaders/pypdfium2.ipynb b/docs/docs/integrations/document_loaders/pypdfium2.ipynb index 24740de99ac11..afcee7e750bb3 100644 --- a/docs/docs/integrations/document_loaders/pypdfium2.ipynb +++ b/docs/docs/integrations/document_loaders/pypdfium2.ipynb @@ -6,46 +6,53 @@ "source": [ "# PyPDFium2Loader\n", "\n", + "This notebook provides a quick overview for getting started with `PyPDF` [document loader](https://python.langchain.com/docs/concepts/document_loaders). For detailed documentation of all DocumentLoader features and configurations head to the [API reference](https://python.langchain.com/api_reference/community/document_loaders/langchain_community.document_loaders.pdf.PyPDFium2Loader.html).\n", "\n", - "This notebook provides a quick overview for getting started with PyPDFium2 [document loader](https://python.langchain.com/docs/concepts/document_loaders). For detailed documentation of all __ModuleName__Loader features and configurations head to the [API reference](https://python.langchain.com/api_reference/community/document_loaders/langchain_community.document_loaders.pdf.PyPDFium2Loader.html).\n", + " \n", "\n", "## Overview\n", "### Integration details\n", "\n", "| Class | Package | Local | Serializable | JS support|\n", "| :--- | :--- | :---: | :---: | :---: |\n", - "| [PyPDFium2Loader](https://python.langchain.com/api_reference/community/document_loaders/langchain_community.document_loaders.pdf.PyPDFium2Loader.html) | [langchain_community](https://python.langchain.com/api_reference/community/index.html) | ✅ | ❌ | ❌ | \n", + "| [PyPDFLoader](https://python.langchain.com/api_reference/community/document_loaders/langchain_community.document_loaders.pdf.PyPDFLoader.html) | [langchain_community](https://python.langchain.com/api_reference/community/index.html) | ✅ | ❌ | ❌ | \n", + " \n", + "--------- \n", + "\n", "### Loader features\n", - "| Source | Document Lazy Loading | Native Async Support\n", - "| :---: | :---: | :---: | \n", - "| PyPDFium2Loader | ✅ | ❌ | \n", "\n", - "## Setup\n", + "| Source | Document Lazy Loading | Native Async Support | Extract Images | Extract Tables |\n", + "|:-----------:| :---: | :---: | :---: |:---: |\n", + "| PyPDFLoader | ✅ | ❌ | ✅ | ❌ |\n", "\n", + " \n", "\n", - "To access PyPDFium2 document loader you'll need to install the `langchain-community` integration package.\n", + "## Setup\n", "\n", "### Credentials\n", "\n", - "No credentials are needed." + "No credentials are required to use `PyPDFLoader`." ] }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "If you want to get automated best in-class tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" - ] + "source": "If you want to get automated best in-class tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:15.370257Z", + "start_time": "2025-02-06T07:04:15.367300Z" + } + }, "source": [ "# os.environ[\"LANGSMITH_API_KEY\"] = getpass.getpass(\"Enter your LangSmith API key: \")\n", "# os.environ[\"LANGSMITH_TRACING\"] = \"true\"" - ] + ], + "outputs": [], + "execution_count": 1 }, { "cell_type": "markdown", @@ -53,17 +60,28 @@ "source": [ "### Installation\n", "\n", - "Install **langchain_community**." + "Install **langchain_community** and **pypdf**." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install -qU langchain_community" - ] + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:18.630037Z", + "start_time": "2025-02-06T07:04:15.634391Z" + } + }, + "source": "%pip install -qU langchain_community pypdfium2", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "execution_count": 2 }, { "cell_type": "markdown", @@ -76,15 +94,20 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:19.594910Z", + "start_time": "2025-02-06T07:04:18.671508Z" + } + }, "source": [ "from langchain_community.document_loaders import PyPDFium2Loader\n", "\n", "file_path = \"./example_data/layout-parser-paper.pdf\"\n", "loader = PyPDFium2Loader(file_path)" - ] + ], + "outputs": [], + "execution_count": 3 }, { "cell_type": "markdown", @@ -95,13 +118,21 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:19.717964Z", + "start_time": "2025-02-06T07:04:19.607741Z" + } + }, + "source": [ + "docs = loader.load()\n", + "docs[0]" + ], "outputs": [ { "data": { "text/plain": [ - "Document(metadata={'source': './example_data/layout-parser-paper.pdf', 'page': 0}, page_content='LayoutParser: A Unified Toolkit for Deep\\r\\nLearning Based Document Image Analysis\\r\\nZejiang Shen\\r\\n1\\r\\n(), Ruochen Zhang\\r\\n2\\r\\n, Melissa Dell\\r\\n3\\r\\n, Benjamin Charles Germain\\r\\nLee\\r\\n4\\r\\n, Jacob Carlson\\r\\n3\\r\\n, and Weining Li\\r\\n5\\r\\n1 Allen Institute for AI\\r\\nshannons@allenai.org 2 Brown University\\r\\nruochen zhang@brown.edu 3 Harvard University\\r\\n{melissadell,jacob carlson}@fas.harvard.edu\\r\\n4 University of Washington\\r\\nbcgl@cs.washington.edu 5 University of Waterloo\\r\\nw422li@uwaterloo.ca\\r\\nAbstract. Recent advances in document image analysis (DIA) have been\\r\\nprimarily driven by the application of neural networks. Ideally, research\\r\\noutcomes could be easily deployed in production and extended for further\\r\\ninvestigation. However, various factors like loosely organized codebases\\r\\nand sophisticated model configurations complicate the easy reuse of im\\x02portant innovations by a wide audience. Though there have been on-going\\r\\nefforts to improve reusability and simplify deep learning (DL) model\\r\\ndevelopment in disciplines like natural language processing and computer\\r\\nvision, none of them are optimized for challenges in the domain of DIA.\\r\\nThis represents a major gap in the existing toolkit, as DIA is central to\\r\\nacademic research across a wide range of disciplines in the social sciences\\r\\nand humanities. This paper introduces LayoutParser, an open-source\\r\\nlibrary for streamlining the usage of DL in DIA research and applica\\x02tions. The core LayoutParser library comes with a set of simple and\\r\\nintuitive interfaces for applying and customizing DL models for layout de\\x02tection, character recognition, and many other document processing tasks.\\r\\nTo promote extensibility, LayoutParser also incorporates a community\\r\\nplatform for sharing both pre-trained models and full document digiti\\x02zation pipelines. We demonstrate that LayoutParser is helpful for both\\r\\nlightweight and large-scale digitization pipelines in real-word use cases.\\r\\nThe library is publicly available at https://layout-parser.github.io.\\r\\nKeywords: Document Image Analysis· Deep Learning· Layout Analysis\\r\\n· Character Recognition· Open Source library· Toolkit.\\r\\n1 Introduction\\r\\nDeep Learning(DL)-based approaches are the state-of-the-art for a wide range of\\r\\ndocument image analysis (DIA) tasks including document image classification [11,\\r\\narXiv:2103.15348v2 [cs.CV] 21 Jun 2021\\n')" + "Document(metadata={'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'pdfTeX-1.40.21', 'creationdate': '2021-06-22T01:27:10+00:00', 'moddate': '2021-06-22T01:27:10+00:00', 'source': './example_data/layout-parser-paper.pdf', 'total_pages': 16, 'page': 0}, page_content='LayoutParser: A Unified Toolkit for Deep\\nLearning Based Document Image Analysis\\nZejiang Shen\\n1\\n(), Ruochen Zhang\\n2\\n, Melissa Dell\\n3\\n, Benjamin Charles Germain\\nLee\\n4\\n, Jacob Carlson\\n3\\n, and Weining Li\\n5\\n1 Allen Institute for AI\\nshannons@allenai.org 2 Brown University\\nruochen zhang@brown.edu 3 Harvard University\\n{melissadell,jacob carlson\\n}@fas.harvard.edu\\n4 University of Washington\\nbcgl@cs.washington.edu 5 University of Waterloo\\nw422li@uwaterloo.ca\\nAbstract. Recent advances in document image analysis (DIA) have been\\nprimarily driven by the application of neural networks. Ideally, research\\noutcomes could be easily deployed in production and extended for further\\ninvestigation. However, various factors like loosely organized codebases\\nand sophisticated model configurations complicate the easy reuse of im\\x02portant innovations by a wide audience. Though there have been on-going\\nefforts to improve reusability and simplify deep learning (DL) model\\ndevelopment in disciplines like natural language processing and computer\\nvision, none of them are optimized for challenges in the domain of DIA.\\nThis represents a major gap in the existing toolkit, as DIA is central to\\nacademic research across a wide range of disciplines in the social sciences\\nand humanities. This paper introduces LayoutParser, an open-source\\nlibrary for streamlining the usage of DL in DIA research and applica\\x02tions. The core LayoutParser library comes with a set of simple and\\nintuitive interfaces for applying and customizing DL models for layout de\\x02tection, character recognition, and many other document processing tasks.\\nTo promote extensibility, LayoutParser also incorporates a community\\nplatform for sharing both pre-trained models and full document digiti\\x02zation pipelines. We demonstrate that LayoutParser is helpful for both\\nlightweight and large-scale digitization pipelines in real-word use cases.\\nThe library is publicly available at https://layout-parser.github.io.\\nKeywords: Document Image Analysis· Deep Learning· Layout Analysis\\n· Character Recognition· Open Source library· Toolkit.\\n1 Introduction\\nDeep Learning(DL)-based approaches are the state-of-the-art for a wide range of\\ndocument image analysis (DIA) tasks including document image classification [11,\\narXiv:2103.15348v2 [cs.CV] 21 Jun 2021\\n')" ] }, "execution_count": 4, @@ -109,49 +140,952 @@ "output_type": "execute_result" } ], + "execution_count": 4 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:19.784617Z", + "start_time": "2025-02-06T07:04:19.782020Z" + } + }, + "source": [ + "import pprint\n", + "\n", + "pprint.pp(docs[0].metadata)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'title': '',\n", + " 'author': '',\n", + " 'subject': '',\n", + " 'keywords': '',\n", + " 'creator': 'LaTeX with hyperref',\n", + " 'producer': 'pdfTeX-1.40.21',\n", + " 'creationdate': '2021-06-22T01:27:10+00:00',\n", + " 'moddate': '2021-06-22T01:27:10+00:00',\n", + " 'source': './example_data/layout-parser-paper.pdf',\n", + " 'total_pages': 16,\n", + " 'page': 0}\n" + ] + } + ], + "execution_count": 5 + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "docs = loader.load()\n", - "docs[0]" + "## Lazy Load\n" ] }, { "cell_type": "code", - "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:22.359295Z", + "start_time": "2025-02-06T07:04:22.143306Z" + } + }, + "source": [ + "pages = []\n", + "for doc in loader.lazy_load():\n", + " pages.append(doc)\n", + " if len(pages) >= 10:\n", + " # do some paged operation, e.g.\n", + " # index.upsert(page)\n", + "\n", + " pages = []\n", + "len(pages)" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 6 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:23.200681Z", + "start_time": "2025-02-06T07:04:23.189169Z" + } + }, + "source": [ + "print(pages[0].page_content[:100])\n", + "pprint.pp(pages[0].metadata)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LayoutParser: A Unified Toolkit for DL-Based DIA 11\n", + "focuses on precision, efficiency, and robustness\n", + "{'title': '',\n", + " 'author': '',\n", + " 'subject': '',\n", + " 'keywords': '',\n", + " 'creator': 'LaTeX with hyperref',\n", + " 'producer': 'pdfTeX-1.40.21',\n", + " 'creationdate': '2021-06-22T01:27:10+00:00',\n", + " 'moddate': '2021-06-22T01:27:10+00:00',\n", + " 'source': './example_data/layout-parser-paper.pdf',\n", + " 'total_pages': 16,\n", + " 'page': 10}\n" + ] + } + ], + "execution_count": 7 + }, + { + "cell_type": "markdown", "metadata": {}, + "source": [ + "The metadata attribute contains at least the following keys:\n", + "- source\n", + "- page (if in mode *page*)\n", + "- total_page\n", + "- creationdate\n", + "- creator\n", + "- producer\n", + "\n", + "Additional metadata are specific to each parser.\n", + "These pieces of information can be helpful (to categorize your PDFs for example)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Splitting mode & custom pages delimiter" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When loading the PDF file you can split it in two different ways:\n", + "- By page\n", + "- As a single text flow\n", + "\n", + "By default PyPDFLoader will split the PDF as a single text flow." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### Extract the PDF by page. Each page is extracted as a langchain Document object:" + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:27.102894Z", + "start_time": "2025-02-06T07:04:26.941787Z" + } + }, + "source": [ + "loader = PyPDFium2Loader(\n", + " \"./example_data/layout-parser-paper.pdf\",\n", + " mode=\"page\",\n", + ")\n", + "docs = loader.load()\n", + "print(len(docs))\n", + "pprint.pp(docs[0].metadata)" + ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'source': './example_data/layout-parser-paper.pdf', 'page': 0}\n" + "16\n", + "{'title': '',\n", + " 'author': '',\n", + " 'subject': '',\n", + " 'keywords': '',\n", + " 'creator': 'LaTeX with hyperref',\n", + " 'producer': 'pdfTeX-1.40.21',\n", + " 'creationdate': '2021-06-22T01:27:10+00:00',\n", + " 'moddate': '2021-06-22T01:27:10+00:00',\n", + " 'source': './example_data/layout-parser-paper.pdf',\n", + " 'total_pages': 16,\n", + " 'page': 0}\n" ] } ], + "execution_count": 8 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "In this mode the pdf is split by pages and the resulting Documents metadata contains the page number. But in some cases we could want to process the pdf as a single text flow (so we don't cut some paragraphs in half). In this case you can use the *single* mode :" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### Extract the whole PDF as a single langchain Document object:" + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:29.714085Z", + "start_time": "2025-02-06T07:04:29.646263Z" + } + }, + "source": [ + "loader = PyPDFium2Loader(\n", + " \"./example_data/layout-parser-paper.pdf\",\n", + " mode=\"single\",\n", + ")\n", + "docs = loader.load()\n", + "print(len(docs))\n", + "pprint.pp(docs[0].metadata)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1\n", + "{'title': '',\n", + " 'author': '',\n", + " 'subject': '',\n", + " 'keywords': '',\n", + " 'creator': 'LaTeX with hyperref',\n", + " 'producer': 'pdfTeX-1.40.21',\n", + " 'creationdate': '2021-06-22T01:27:10+00:00',\n", + " 'moddate': '2021-06-22T01:27:10+00:00',\n", + " 'source': './example_data/layout-parser-paper.pdf',\n", + " 'total_pages': 16}\n" + ] + } + ], + "execution_count": 9 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "Logically, in this mode, the ‘page_number’ metadata disappears. Here's how to clearly identify where pages end in the text flow :" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### Add a custom *pages_delimiter* to identify where are ends of pages in *single* mode:" + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:33.462591Z", + "start_time": "2025-02-06T07:04:33.299846Z" + } + }, "source": [ - "print(docs[0].metadata)" + "loader = PyPDFium2Loader(\n", + " \"./example_data/layout-parser-paper.pdf\",\n", + " mode=\"single\",\n", + " pages_delimiter=\"\\n-------THIS IS A CUSTOM END OF PAGE-------\\n\",\n", + ")\n", + "docs = loader.load()\n", + "print(docs[0].page_content[:5780])" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LayoutParser: A Unified Toolkit for Deep\n", + "Learning Based Document Image Analysis\n", + "Zejiang Shen\n", + "1\n", + "(), Ruochen Zhang\n", + "2\n", + ", Melissa Dell\n", + "3\n", + ", Benjamin Charles Germain\n", + "Lee\n", + "4\n", + ", Jacob Carlson\n", + "3\n", + ", and Weining Li\n", + "5\n", + "1 Allen Institute for AI\n", + "shannons@allenai.org 2 Brown University\n", + "ruochen zhang@brown.edu 3 Harvard University\n", + "{melissadell,jacob carlson\n", + "}@fas.harvard.edu\n", + "4 University of Washington\n", + "bcgl@cs.washington.edu 5 University of Waterloo\n", + "w422li@uwaterloo.ca\n", + "Abstract. Recent advances in document image analysis (DIA) have been\n", + "primarily driven by the application of neural networks. Ideally, research\n", + "outcomes could be easily deployed in production and extended for further\n", + "investigation. However, various factors like loosely organized codebases\n", + "and sophisticated model configurations complicate the easy reuse of im\u0002portant innovations by a wide audience. Though there have been on-going\n", + "efforts to improve reusability and simplify deep learning (DL) model\n", + "development in disciplines like natural language processing and computer\n", + "vision, none of them are optimized for challenges in the domain of DIA.\n", + "This represents a major gap in the existing toolkit, as DIA is central to\n", + "academic research across a wide range of disciplines in the social sciences\n", + "and humanities. This paper introduces LayoutParser, an open-source\n", + "library for streamlining the usage of DL in DIA research and applica\u0002tions. The core LayoutParser library comes with a set of simple and\n", + "intuitive interfaces for applying and customizing DL models for layout de\u0002tection, character recognition, and many other document processing tasks.\n", + "To promote extensibility, LayoutParser also incorporates a community\n", + "platform for sharing both pre-trained models and full document digiti\u0002zation pipelines. We demonstrate that LayoutParser is helpful for both\n", + "lightweight and large-scale digitization pipelines in real-word use cases.\n", + "The library is publicly available at https://layout-parser.github.io.\n", + "Keywords: Document Image Analysis· Deep Learning· Layout Analysis\n", + "· Character Recognition· Open Source library· Toolkit.\n", + "1 Introduction\n", + "Deep Learning(DL)-based approaches are the state-of-the-art for a wide range of\n", + "document image analysis (DIA) tasks including document image classification [11,\n", + "arXiv:2103.15348v2 [cs.CV] 21 Jun 2021\n", + "-------THIS IS A CUSTOM END OF PAGE-------\n", + "2 Z. Shen et al.\n", + "37], layout detection [38, 22], table detection [26], and scene text detection [4].\n", + "A generalized learning-based framework dramatically reduces the need for the\n", + "manual specification of complicated rules, which is the status quo with traditional\n", + "methods. DL has the potential to transform DIA pipelines and benefit a broad\n", + "spectrum of large-scale document digitization projects.\n", + "However, there are several practical difficulties for taking advantages of re\u0002cent advances in DL-based methods: 1) DL models are notoriously convoluted\n", + "for reuse and extension. Existing models are developed using distinct frame\u0002works like TensorFlow [1] or PyTorch [24], and the high-level parameters can\n", + "be obfuscated by implementation details [8]. It can be a time-consuming and\n", + "frustrating experience to debug, reproduce, and adapt existing models for DIA,\n", + "and many researchers who would benefit the most from using these methods lack\n", + "the technical background to implement them from scratch. 2) Document images\n", + "contain diverse and disparate patterns across domains, and customized training\n", + "is often required to achieve a desirable detection accuracy. Currently there is no\n", + "full-fledged infrastructure for easily curating the target document image datasets\n", + "and fine-tuning or re-training the models. 3) DIA usually requires a sequence of\n", + "models and other processing to obtain the final outputs. Often research teams use\n", + "DL models and then perform further document analyses in separate processes,\n", + "and these pipelines are not documented in any central location (and often not\n", + "documented at all). This makes it difficult for research teams to learn about how\n", + "full pipelines are implemented and leads them to invest significant resources in\n", + "reinventing the DIA wheel.\n", + "LayoutParser provides a unified toolkit to support DL-based document image\n", + "analysis and processing. To address the aforementioned challenges, LayoutParser\n", + "is built with the following components:\n", + "1. An off-the-shelf toolkit for applying DL models for layout detection, character\n", + "recognition, and other DIA tasks (Section 3)\n", + "2. A rich repository of pre-trained neural network models (Model Zoo) that\n", + "underlies the off-the-shelf usage\n", + "3. Comprehensive tools for efficient document image data annotation and model\n", + "tuning to support different levels of customization\n", + "4. A DL model hub and community platform for the easy sharing, distribu\u0002tion, and discussion of DIA models and pipelines, to promote reusability,\n", + "reproducibility, and extensibility (Section 4)\n", + "The library implements simple and intuitive Python APIs without sacrificing\n", + "generalizability and versatility, and can be easily installed via pip. Its convenient\n", + "functions for handling document image data can be seamlessly integrated with\n", + "existing DIA pipelines. With detailed documentations and carefully curated\n", + "tutorials, we hope this tool will benefit a variety of end-users, and will lead to\n", + "advances in applications in both industry and academic research.\n", + "LayoutParser is well aligned with recent efforts for improving DL model\n", + "reusability in other disciplines like natural language processing [8, 34] and com\u0002puter vision [35], but with a focus on unique challenges in DIA. We show\n", + "LayoutParser can be applied in sophisticated and large-scale digitization projects\n", + "-------THIS IS A CUSTOM END OF PAGE-------\n", + "LayoutParser: A Unified Toolkit for DL-Based DIA 3\n", + "that require precision, efficiency, and robustness, as well as \n" + ] + } + ], + "execution_count": 10 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "This could simply be \\n, or \\f to clearly indicate a page change, or \\ for seamless injection in a Markdown viewer without a visual effect." + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Extract images from the PDF" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can extract images from your PDFs with a choice of three different solutions:\n", + "- rapidOCR (lightweight Optical Character Recognition tool)\n", + "- Tesseract (OCR tool with high precision)\n", + "- Multimodal language model\n", + "\n", + "You can tune these functions to choose the output format of the extracted images among *html*, *markdown* or *text*\n", + "\n", + "The result is inserted between the last and the second-to-last paragraphs of text of the page." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### Extract images from the PDF with rapidOCR:" + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:04:39.419623Z", + "start_time": "2025-02-06T07:04:37.250297Z" + } + }, + "source": [ + "%pip install -qU rapidocr-onnxruntime" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "execution_count": 11 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:05:02.902374Z", + "start_time": "2025-02-06T07:04:39.500569Z" + } + }, + "source": [ + "from langchain_community.document_loaders.parsers import RapidOCRBlobParser\n", + "\n", + "loader = PyPDFium2Loader(\n", + " \"./example_data/layout-parser-paper.pdf\",\n", + " mode=\"page\",\n", + " images_inner_format=\"markdown-img\",\n", + " images_parser=RapidOCRBlobParser(),\n", + ")\n", + "docs = loader.load()\n", + "\n", + "print(docs[5].page_content)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6 Z. Shen et al.\n", + "Fig. 2: The relationship between the three types of layout data structures.\n", + "Coordinate supports three kinds of variation; TextBlock consists of the co\u0002ordinate information and extra features like block text, types, and reading orders;\n", + "a Layout object is a list of all possible layout elements, including other Layout\n", + "objects. They all support the same set of transformation and operation APIs for\n", + "maximum flexibility.\n", + "Shown in Table 1, LayoutParser currently hosts 9 pre-trained models trained\n", + "on 5 different datasets. Description of the training dataset is provided alongside\n", + "with the trained models such that users can quickly identify the most suitable\n", + "models for their tasks. Additionally, when such a model is not readily available,\n", + "LayoutParser also supports training customized layout models and community\n", + "sharing of the models (detailed in Section 3.5).\n", + "3.2 Layout Data Structures\n", + "A critical feature of LayoutParser is the implementation of a series of data\n", + "structures and operations that can be used to efficiently process and manipulate\n", + "the layout elements. In document image analysis pipelines, various post-processing\n", + "on the layout analysis model outputs is usually required to obtain the final\n", + "outputs. Traditionally, this requires exporting DL model outputs and then loading\n", + "the results into other pipelines. All model outputs from LayoutParser will be\n", + "stored in carefully engineered data types optimized for further processing, which\n", + "makes it possible to build an end-to-end document digitization pipeline within\n", + "LayoutParser. There are three key components in the data structure, namely\n", + "the Coordinate system, the TextBlock, and the Layout. They provide different\n", + "levels of abstraction for the layout data, and a set of APIs are supported for\n", + "transformations or operations on these classes.\n", + "\n", + "\n", + "\n", + "![Coordinate\n", + "(x1, y1)\n", + "(X1, y1)\n", + "(x2,y2)\n", + "APIS\n", + "x-interval\n", + "tart\n", + "end\n", + "Quadrilateral\n", + "operation\n", + "Rectangle\n", + "y-interval\n", + "ena\n", + "(x2, y2)\n", + "(x4, y4)\n", + "(x3, y3)\n", + "and\n", + "textblock\n", + "Coordinate\n", + "transformation\n", + "+\n", + "Block\n", + "Block\n", + "Reading\n", + "Extra features\n", + "Text\n", + "Type\n", + "Order\n", + "coordinatel\n", + "textblockl\n", + "layout\n", + " same\n", + "textblock2\n", + "layoutl\n", + "The\n", + "A list of the layout elements](#)\n", + "\n" + ] + } + ], + "execution_count": 12 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "Be careful, RapidOCR is designed to work with Chinese and English, not other languages." + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### Extract images from the PDF with Tesseract:" + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:05:07.656993Z", + "start_time": "2025-02-06T07:05:05.890565Z" + } + }, + "source": [ + "%pip install -qU pytesseract" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "execution_count": 13 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:05:16.677336Z", + "start_time": "2025-02-06T07:05:07.724790Z" + } + }, + "source": [ + "from langchain_community.document_loaders.parsers import TesseractBlobParser\n", + "\n", + "loader = PyPDFium2Loader(\n", + " \"./example_data/layout-parser-paper.pdf\",\n", + " mode=\"page\",\n", + " images_inner_format=\"html-img\",\n", + " images_parser=TesseractBlobParser(),\n", + ")\n", + "docs = loader.load()\n", + "print(docs[5].page_content)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6 Z. Shen et al.\n", + "Fig. 2: The relationship between the three types of layout data structures.\n", + "Coordinate supports three kinds of variation; TextBlock consists of the co\u0002ordinate information and extra features like block text, types, and reading orders;\n", + "a Layout object is a list of all possible layout elements, including other Layout\n", + "objects. They all support the same set of transformation and operation APIs for\n", + "maximum flexibility.\n", + "Shown in Table 1, LayoutParser currently hosts 9 pre-trained models trained\n", + "on 5 different datasets. Description of the training dataset is provided alongside\n", + "with the trained models such that users can quickly identify the most suitable\n", + "models for their tasks. Additionally, when such a model is not readily available,\n", + "LayoutParser also supports training customized layout models and community\n", + "sharing of the models (detailed in Section 3.5).\n", + "3.2 Layout Data Structures\n", + "A critical feature of LayoutParser is the implementation of a series of data\n", + "structures and operations that can be used to efficiently process and manipulate\n", + "the layout elements. In document image analysis pipelines, various post-processing\n", + "on the layout analysis model outputs is usually required to obtain the final\n", + "outputs. Traditionally, this requires exporting DL model outputs and then loading\n", + "the results into other pipelines. All model outputs from LayoutParser will be\n", + "stored in carefully engineered data types optimized for further processing, which\n", + "makes it possible to build an end-to-end document digitization pipeline within\n", + "LayoutParser. There are three key components in the data structure, namely\n", + "the Coordinate system, the TextBlock, and the Layout. They provide different\n", + "levels of abstraction for the layout data, and a set of APIs are supported for\n", + "transformations or operations on these classes.\n", + "\n", + "\n", + "\n", + "\"Coordinate\n",\n", + "\n" + ] + } + ], + "execution_count": 14 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### Extract images from the PDF with multimodal model:" + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:05:57.591688Z", + "start_time": "2025-02-06T07:05:54.591989Z" + } + }, + "source": [ + "%pip install -qU langchain_openai" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "execution_count": 15 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:05:58.280055Z", + "start_time": "2025-02-06T07:05:58.180689Z" + } + }, + "source": [ + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv()" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 16 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:05:59.170560Z", + "start_time": "2025-02-06T07:05:59.167117Z" + } + }, + "source": [ + "from getpass import getpass\n", + "\n", + "if not os.environ.get(\"OPENAI_API_KEY\"):\n", + " os.environ[\"OPENAI_API_KEY\"] = getpass(\"OpenAI API key =\")" + ], + "outputs": [], + "execution_count": 17 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:07:05.416416Z", + "start_time": "2025-02-06T07:06:00.694853Z" + } + }, + "source": [ + "from langchain_community.document_loaders.parsers import LLMImageBlobParser\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "loader = PyPDFium2Loader(\n", + " \"./example_data/layout-parser-paper.pdf\",\n", + " mode=\"page\",\n", + " images_inner_format=\"markdown-img\",\n", + " images_parser=LLMImageBlobParser(model=ChatOpenAI(model=\"gpt-4o\", max_tokens=1024)),\n", + ")\n", + "docs = loader.load()\n", + "print(docs[5].page_content)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6 Z. Shen et al.\n", + "Fig. 2: The relationship between the three types of layout data structures.\n", + "Coordinate supports three kinds of variation; TextBlock consists of the co\u0002ordinate information and extra features like block text, types, and reading orders;\n", + "a Layout object is a list of all possible layout elements, including other Layout\n", + "objects. They all support the same set of transformation and operation APIs for\n", + "maximum flexibility.\n", + "Shown in Table 1, LayoutParser currently hosts 9 pre-trained models trained\n", + "on 5 different datasets. Description of the training dataset is provided alongside\n", + "with the trained models such that users can quickly identify the most suitable\n", + "models for their tasks. Additionally, when such a model is not readily available,\n", + "LayoutParser also supports training customized layout models and community\n", + "sharing of the models (detailed in Section 3.5).\n", + "3.2 Layout Data Structures\n", + "A critical feature of LayoutParser is the implementation of a series of data\n", + "structures and operations that can be used to efficiently process and manipulate\n", + "the layout elements. In document image analysis pipelines, various post-processing\n", + "on the layout analysis model outputs is usually required to obtain the final\n", + "outputs. Traditionally, this requires exporting DL model outputs and then loading\n", + "the results into other pipelines. All model outputs from LayoutParser will be\n", + "stored in carefully engineered data types optimized for further processing, which\n", + "makes it possible to build an end-to-end document digitization pipeline within\n", + "LayoutParser. There are three key components in the data structure, namely\n", + "the Coordinate system, the TextBlock, and the Layout. They provide different\n", + "levels of abstraction for the layout data, and a set of APIs are supported for\n", + "transformations or operations on these classes.\n", + "\n", + "\n", + "\n", + "![**Image Summary**: Diagram showing a data structure for layout elements including coordinates (intervals, rectangles, quadrilaterals) and text blocks with extra features (block text, type, reading order). It illustrates a hierarchy from coordinates to text blocks and a list of layout elements.\n", + "\n", + "**Extracted Text**:\n", + "```\n", + "Coordinate \n", + "Coordinate\n", + "\n", + "x-interval\n", + "x1, y1\n", + "(x1, y1)\n", + "y-interval\n", + "(x2, y2)\n", + "(x2, y2)\n", + "\n", + "Rectangle\n", + "Rectangle\n", + "\n", + "Quadrilateral\n", + "\n", + "textblock\n", + "\n", + "Coordinate\n", + "Coordinate\n", + "\n", + "+ +\n", + "Extra features\n", + "Extra features\n", + "\n", + "Block Block Reading ...\n", + "Block Text\n", + "Block Type\n", + "Reading Order\n", + "...\n", + "\n", + "layout\n", + "\n", + "[coordinate1, textblock1, textblock2, layout1\\\\]\n", + "...\n", + "\n", + "[\\\\]\n", + "A list of the layout elements\n", + "A list of the layout elements\n", + "\n", + "The same transformation and operation APIs\n", + "The same transformation\n", + "and operation APIs\n", + "```](#)\n", + "\n" + ] + } + ], + "execution_count": 18 + }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Lazy Load" + "## Working with Files\n", + "\n", + "Many document loaders involve parsing files. The difference between such loaders usually stems from how the file is parsed, rather than how the file is loaded. For example, you can use `open` to read the binary content of either a PDF or a markdown file, but you need different parsing logic to convert that binary data into text.\n", + "\n", + "As a result, it can be helpful to decouple the parsing logic from the loading logic, which makes it easier to re-use a given parser regardless of how the data was loaded.\n", + "You can use this strategy to analyze different files, with the same parsing parameters." ] }, { "cell_type": "code", - "execution_count": 6, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-06T07:07:08.394894Z", + "start_time": "2025-02-06T07:07:08.164047Z" + } + }, + "source": [ + "from langchain_community.document_loaders import FileSystemBlobLoader\n", + "from langchain_community.document_loaders.generic import GenericLoader\n", + "from langchain_community.document_loaders.parsers import PyPDFium2Parser\n", + "\n", + "loader = GenericLoader(\n", + " blob_loader=FileSystemBlobLoader(\n", + " path=\"./example_data/\",\n", + " glob=\"*.pdf\",\n", + " ),\n", + " blob_parser=PyPDFium2Parser(),\n", + ")\n", + "docs = loader.load()\n", + "print(docs[0].page_content)\n", + "pprint.pp(docs[0].metadata)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LayoutParser: A Unified Toolkit for Deep\n", + "Learning Based Document Image Analysis\n", + "Zejiang Shen\n", + "1\n", + "(), Ruochen Zhang\n", + "2\n", + ", Melissa Dell\n", + "3\n", + ", Benjamin Charles Germain\n", + "Lee\n", + "4\n", + ", Jacob Carlson\n", + "3\n", + ", and Weining Li\n", + "5\n", + "1 Allen Institute for AI\n", + "shannons@allenai.org 2 Brown University\n", + "ruochen zhang@brown.edu 3 Harvard University\n", + "{melissadell,jacob carlson\n", + "}@fas.harvard.edu\n", + "4 University of Washington\n", + "bcgl@cs.washington.edu 5 University of Waterloo\n", + "w422li@uwaterloo.ca\n", + "Abstract. Recent advances in document image analysis (DIA) have been\n", + "primarily driven by the application of neural networks. Ideally, research\n", + "outcomes could be easily deployed in production and extended for further\n", + "investigation. However, various factors like loosely organized codebases\n", + "and sophisticated model configurations complicate the easy reuse of im\u0002portant innovations by a wide audience. Though there have been on-going\n", + "efforts to improve reusability and simplify deep learning (DL) model\n", + "development in disciplines like natural language processing and computer\n", + "vision, none of them are optimized for challenges in the domain of DIA.\n", + "This represents a major gap in the existing toolkit, as DIA is central to\n", + "academic research across a wide range of disciplines in the social sciences\n", + "and humanities. This paper introduces LayoutParser, an open-source\n", + "library for streamlining the usage of DL in DIA research and applica\u0002tions. The core LayoutParser library comes with a set of simple and\n", + "intuitive interfaces for applying and customizing DL models for layout de\u0002tection, character recognition, and many other document processing tasks.\n", + "To promote extensibility, LayoutParser also incorporates a community\n", + "platform for sharing both pre-trained models and full document digiti\u0002zation pipelines. We demonstrate that LayoutParser is helpful for both\n", + "lightweight and large-scale digitization pipelines in real-word use cases.\n", + "The library is publicly available at https://layout-parser.github.io.\n", + "Keywords: Document Image Analysis· Deep Learning· Layout Analysis\n", + "· Character Recognition· Open Source library· Toolkit.\n", + "1 Introduction\n", + "Deep Learning(DL)-based approaches are the state-of-the-art for a wide range of\n", + "document image analysis (DIA) tasks including document image classification [11,\n", + "arXiv:2103.15348v2 [cs.CV] 21 Jun 2021\n", + "\n", + "{'title': '',\n", + " 'author': '',\n", + " 'subject': '',\n", + " 'keywords': '',\n", + " 'creator': 'LaTeX with hyperref',\n", + " 'producer': 'pdfTeX-1.40.21',\n", + " 'creationdate': '2021-06-22T01:27:10+00:00',\n", + " 'moddate': '2021-06-22T01:27:10+00:00',\n", + " 'source': 'example_data/layout-parser-paper.pdf',\n", + " 'total_pages': 16,\n", + " 'page': 0}\n" + ] + } + ], + "execution_count": 19 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "It is possible to work with files from cloud storage." + }, + { + "cell_type": "code", + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "page = []\n", - "for doc in loader.lazy_load():\n", - " page.append(doc)\n", - " if len(page) >= 10:\n", - " # do some paged operation, e.g.\n", - " # index.upsert(page)\n", + "from langchain_community.document_loaders import CloudBlobLoader\n", + "from langchain_community.document_loaders.generic import GenericLoader\n", "\n", - " page = []" + "loader = GenericLoader(\n", + " blob_loader=CloudBlobLoader(\n", + " url=\"s3://mybucket\", # Supports s3://, az://, gs://, file:// schemes.\n", + " glob=\"*.pdf\",\n", + " ),\n", + " blob_parser=PyPDFium2Parser(),\n", + ")\n", + "docs = loader.load()\n", + "print(docs[0].page_content)\n", + "pprint.pp(docs[0].metadata)" ] }, { @@ -160,13 +1094,13 @@ "source": [ "## API reference\n", "\n", - "For detailed documentation of all PyPDFium2Loader features and configurations head to the API reference: https://python.langchain.com/api_reference/community/document_loaders/langchain_community.document_loaders.pdf.PyPDFium2Loader.html" + "For detailed documentation of all `PyPDFium2Loader` features and configurations head to the API reference: https://python.langchain.com/api_reference/community/document_loaders/langchain_community.document_loaders.pdf.PyPDFium2Loader.html" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -180,9 +1114,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.10.1" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/libs/community/langchain_community/document_loaders/parsers/pdf.py b/libs/community/langchain_community/document_loaders/parsers/pdf.py index 319aa48b1f579..782edddad44af 100644 --- a/libs/community/langchain_community/document_loaders/parsers/pdf.py +++ b/libs/community/langchain_community/document_loaders/parsers/pdf.py @@ -1158,50 +1158,216 @@ def _extract_tables_from_page(self, page: pymupdf.Page) -> str: class PyPDFium2Parser(BaseBlobParser): - """Parse `PDF` with `PyPDFium2`.""" + """Parse a blob from a PDF using `PyPDFium2` library. - def __init__(self, extract_images: bool = False) -> None: - """Initialize the parser.""" + This class provides methods to parse a blob from a PDF document, supporting various + configurations such as handling password-protected PDFs, extracting images, and + defining extraction mode. + It integrates the 'PyPDFium2' library for PDF processing and offers synchronous + blob parsing. + + Examples: + Setup: + + .. code-block:: bash + + pip install -U langchain-community pypdfium2 + + Load a blob from a PDF file: + + .. code-block:: python + + from langchain_core.documents.base import Blob + + blob = Blob.from_path("./example_data/layout-parser-paper.pdf") + + Instantiate the parser: + + .. code-block:: python + + from langchain_community.document_loaders.parsers import PyPDFium2Parser + + parser = PyPDFium2Parser( + # password=None, + mode="page", + pages_delimiter="\n\f", + # extract_images = True, + # images_to_text = convert_images_to_text_with_tesseract(), + ) + + Lazily parse the blob: + + .. code-block:: python + + docs = [] + docs_lazy = parser.lazy_parse(blob) + + for doc in docs_lazy: + docs.append(doc) + print(docs[0].page_content[:100]) + print(docs[0].metadata) + """ + + # PyPDFium2 is not thread safe. + # See https://pypdfium2.readthedocs.io/en/stable/python_api.html#thread-incompatibility + _lock = threading.Lock() + + def __init__( + self, + extract_images: bool = False, + *, + password: Optional[str] = None, + mode: Literal["single", "page"] = "page", + pages_delimiter: str = _DEFAULT_PAGES_DELIMITER, + images_parser: Optional[BaseImageBlobParser] = None, + images_inner_format: Literal["text", "markdown-img", "html-img"] = "text", + ) -> None: + """Initialize a parser based on PyPDFium2. + + Args: + password: Optional password for opening encrypted PDFs. + mode: The extraction mode, either "single" for the entire document or "page" + for page-wise extraction. + pages_delimiter: A string delimiter to separate pages in single-mode + extraction. + extract_images: Whether to extract images from the PDF. + images_parser: Optional image blob parser. + images_inner_format: The format for the parsed output. + - "text" = return the content as is + - "markdown-img" = wrap the content into an image markdown link, w/ link + pointing to (`![body)(#)`] + - "html-img" = wrap the content as the `alt` text of an tag and link to + (`{body}`) + extraction_mode: “plain” for legacy functionality, “layout” for experimental + layout mode functionality + extraction_kwargs: Optional additional parameters for the extraction + process. + + Returns: + This method does not directly return data. Use the `parse` or `lazy_parse` + methods to retrieve parsed documents with content and metadata. + + Raises: + ValueError: If the mode is not "single" or "page". + """ + super().__init__() + if mode not in ["single", "page"]: + raise ValueError("mode must be single or page") + self.extract_images = extract_images + if extract_images and not images_parser: + images_parser = RapidOCRBlobParser() + self.images_parser = images_parser + self.images_inner_format = images_inner_format + self.password = password + self.mode = mode + self.pages_delimiter = pages_delimiter + + def lazy_parse(self, blob: Blob) -> Iterator[Document]: # type: ignore[valid-type] + """ + Lazily parse the blob. + Insert image, if possible, between two paragraphs. + In this way, a paragraph can be continued on the next page. + + Args: + blob: The blob to parse. + + Raises: + ImportError: If the `pypdf` package is not found. + + Yield: + An iterator over the parsed documents. + """ try: - import pypdfium2 # noqa:F401 + import pypdfium2 except ImportError: raise ImportError( "pypdfium2 package not found, please install it with" " `pip install pypdfium2`" ) - self.extract_images = extract_images - - def lazy_parse(self, blob: Blob) -> Iterator[Document]: # type: ignore[valid-type] - """Lazily parse the blob.""" - import pypdfium2 # pypdfium2 is really finicky with respect to closing things, # if done incorrectly creates seg faults. - with blob.as_bytes_io() as file_path: # type: ignore[attr-defined] - pdf_reader = pypdfium2.PdfDocument(file_path, autoclose=True) - try: - for page_number, page in enumerate(pdf_reader): - text_page = page.get_textpage() - content = text_page.get_text_range() - text_page.close() - content += "\n" + self._extract_images_from_page(page) - page.close() - metadata = {"source": blob.source, "page": page_number} # type: ignore[attr-defined] - yield Document(page_content=content, metadata=metadata) - finally: - pdf_reader.close() + with PyPDFium2Parser._lock: + with blob.as_bytes_io() as file_path: # type: ignore[attr-defined] + pdf_reader = None + try: + pdf_reader = pypdfium2.PdfDocument( + file_path, password=self.password, autoclose=True + ) + full_content = [] + + doc_metadata = _purge_metadata(pdf_reader.get_metadata_dict()) + doc_metadata["source"] = blob.source + doc_metadata["total_pages"] = len(pdf_reader) + + for page_number, page in enumerate(pdf_reader): + text_page = page.get_textpage() + text_from_page = "\n".join( + text_page.get_text_range().splitlines() + ) # Replace \r\n + text_page.close() + image_from_page = self._extract_images_from_page(page) + all_text = _merge_text_and_extras( + [image_from_page], text_from_page + ).strip() + page.close() + + if self.mode == "page": + # For legacy compatibility, add the last '\n' + if not all_text.endswith("\n"): + all_text += "\n" + yield Document( + page_content=all_text, + metadata=_validate_metadata( + { + **doc_metadata, + "page": page_number, + } + ), + ) + else: + full_content.append(all_text) + + if self.mode == "single": + yield Document( + page_content=self.pages_delimiter.join(full_content), + metadata=_validate_metadata(doc_metadata), + ) + finally: + if pdf_reader: + pdf_reader.close() def _extract_images_from_page(self, page: pypdfium2._helpers.page.PdfPage) -> str: - """Extract images from page and get the text with RapidOCR.""" - if not self.extract_images: + """Extract images from a PDF page and get the text using images_to_text. + + Args: + page: The page object from which to extract images. + + Returns: + str: The extracted text from the images on the page. + """ + if not self.images_parser: return "" import pypdfium2.raw as pdfium_c images = list(page.get_objects(filter=(pdfium_c.FPDF_PAGEOBJ_IMAGE,))) - - images = list(map(lambda x: x.get_bitmap().to_numpy(), images)) - return extract_from_images_with_rapidocr(images) + if not images: + return "" + str_images = [] + for image in images: + image_bytes = io.BytesIO() + np_image = image.get_bitmap().to_numpy() + if np_image.size < 3: + continue + numpy.save(image_bytes, image.get_bitmap().to_numpy()) + blob = Blob.from_data(image_bytes.getvalue(), mime_type="application/x-npy") + text_from_image = next(self.images_parser.lazy_parse(blob)).page_content + str_images.append( + _format_inner_image(blob, text_from_image, self.images_inner_format) + ) + image.close() + return _FORMAT_IMAGE_STR.format(image_text=_JOIN_IMAGES.join(str_images)) class PDFPlumberParser(BaseBlobParser): diff --git a/libs/community/langchain_community/document_loaders/pdf.py b/libs/community/langchain_community/document_loaders/pdf.py index 3cf0d54ed9b85..b9e57b19ff1d6 100644 --- a/libs/community/langchain_community/document_loaders/pdf.py +++ b/libs/community/langchain_community/document_loaders/pdf.py @@ -308,25 +308,116 @@ def lazy_load( class PyPDFium2Loader(BasePDFLoader): - """Load `PDF` using `pypdfium2` and chunks at character level.""" + """Load and parse a PDF file using the `pypdfium2` library. + + This class provides methods to load and parse PDF documents, supporting various + configurations such as handling password-protected files, extracting images, and + defining extraction mode. + It integrates the `pypdfium2` library for PDF processing and offers both + synchronous and asynchronous document loading. + + Examples: + Setup: + + .. code-block:: bash + + pip install -U langchain-community pypdfium2 + + Instantiate the loader: + + .. code-block:: python + + from langchain_community.document_loaders import PyPDFium2Loader + + loader = PyPDFium2Loader( + file_path = "./example_data/layout-parser-paper.pdf", + # headers = None + # password = None, + mode = "single", + pages_delimiter = "\n\f", + # extract_images = True, + # images_to_text = convert_images_to_text_with_tesseract(), + ) + + Lazy load documents: + + .. code-block:: python + + docs = [] + docs_lazy = loader.lazy_load() + + for doc in docs_lazy: + docs.append(doc) + print(docs[0].page_content[:100]) + print(docs[0].metadata) + + Load documents asynchronously: + + .. code-block:: python + + docs = await loader.aload() + print(docs[0].page_content[:100]) + print(docs[0].metadata) + """ def __init__( self, file_path: Union[str, PurePath], *, - headers: Optional[dict] = None, + mode: Literal["single", "page"] = "page", + pages_delimiter: str = _DEFAULT_PAGES_DELIMITER, + password: Optional[str] = None, extract_images: bool = False, + images_parser: Optional[BaseImageBlobParser] = None, + images_inner_format: Literal["text", "markdown-img", "html-img"] = "text", + headers: Optional[dict] = None, ): - """Initialize with a file path.""" + """Initialize with a file path. + + Args: + file_path: The path to the PDF file to be loaded. + headers: Optional headers to use for GET request to download a file from a + web path. + password: Optional password for opening encrypted PDFs. + mode: The extraction mode, either "single" for the entire document or "page" + for page-wise extraction. + pages_delimiter: A string delimiter to separate pages in single-mode + extraction. + extract_images: Whether to extract images from the PDF. + images_parser: Optional image blob parser. + images_inner_format: The format for the parsed output. + - "text" = return the content as is + - "markdown-img" = wrap the content into an image markdown link, w/ link + pointing to (`![body)(#)`] + - "html-img" = wrap the content as the `alt` text of an tag and link to + (`{body}`) + + Returns: + This class does not directly return data. Use the `load`, `lazy_load` or + `aload` methods to retrieve parsed documents with content and metadata. + """ super().__init__(file_path, headers=headers) - self.parser = PyPDFium2Parser(extract_images=extract_images) + self.parser = PyPDFium2Parser( + mode=mode, + password=password, + extract_images=extract_images, + images_parser=images_parser, + images_inner_format=images_inner_format, + pages_delimiter=pages_delimiter, + ) def lazy_load( self, ) -> Iterator[Document]: - """Lazy load given path as pages.""" + """ + Lazy load given path as pages. + Insert image, if possible, between two paragraphs. + In this way, a paragraph can be continued on the next page. + """ if self.web_path: - blob = Blob.from_data(open(self.file_path, "rb").read(), path=self.web_path) # type: ignore[attr-defined] + blob = Blob.from_data( # type: ignore[attr-defined] + open(self.file_path, "rb").read(), path=self.web_path + ) else: blob = Blob.from_path(self.file_path) # type: ignore[attr-defined] yield from self.parser.parse(blob) diff --git a/libs/community/tests/integration_tests/document_loaders/parsers/test_pdf_parsers.py b/libs/community/tests/integration_tests/document_loaders/parsers/test_pdf_parsers.py index fa3cc0d8d4b64..1137dd79f2e78 100644 --- a/libs/community/tests/integration_tests/document_loaders/parsers/test_pdf_parsers.py +++ b/libs/community/tests/integration_tests/document_loaders/parsers/test_pdf_parsers.py @@ -12,7 +12,6 @@ from langchain_community.document_loaders.parsers import ( BaseImageBlobParser, PDFPlumberParser, - PyPDFium2Parser, ) if TYPE_CHECKING: @@ -96,12 +95,6 @@ def _assert_with_duplicate_parser(parser: BaseBlobParser, dedupe: bool = False) assert "11000000 SSeerriieess" == docs[0].page_content.split("\n")[0] -def test_pypdfium2_parser() -> None: - """Test PyPDFium2 parser.""" - # Does not follow defaults to split by page. - _assert_with_parser(PyPDFium2Parser()) - - def test_pdfplumber_parser() -> None: """Test PDFPlumber parser.""" _assert_with_parser(PDFPlumberParser()) @@ -109,11 +102,6 @@ def test_pdfplumber_parser() -> None: _assert_with_duplicate_parser(PDFPlumberParser(dedupe=True), dedupe=True) -def test_extract_images_text_from_pdf_pypdfium2parser() -> None: - """Test extract image from pdf and recognize text with rapid ocr - PyPDFium2Parser""" # noqa: E501 - _assert_with_parser(PyPDFium2Parser(extract_images=True)) - - class EmptyImageBlobParser(BaseImageBlobParser): def _analyze_image(self, img: "Image") -> str: return "Hello world" @@ -128,6 +116,7 @@ def _analyze_image(self, img: "Image") -> str: [ ("PDFMinerParser", {}), ("PyMuPDFParser", {}), + ("PyPDFium2Parser", {}), ("PyPDFParser", {"extraction_mode": "plain"}), ("PyPDFParser", {"extraction_mode": "layout"}), ], @@ -157,6 +146,7 @@ def test_mode_and_extract_images_variations( [ ("PDFMinerParser", {}), ("PyMuPDFParser", {}), + ("PyPDFium2Parser", {}), ("PyPDFParser", {"extraction_mode": "plain"}), ("PyPDFParser", {"extraction_mode": "layout"}), ], diff --git a/libs/community/tests/integration_tests/document_loaders/test_pdf.py b/libs/community/tests/integration_tests/document_loaders/test_pdf.py index 88dd5169d5f5e..891308a0441e9 100644 --- a/libs/community/tests/integration_tests/document_loaders/test_pdf.py +++ b/libs/community/tests/integration_tests/document_loaders/test_pdf.py @@ -9,7 +9,6 @@ AmazonTextractPDFLoader, MathpixPDFLoader, PDFMinerPDFasHTMLLoader, - PyPDFium2Loader, UnstructuredPDFLoader, ) @@ -56,21 +55,6 @@ def test_pdfminer_pdf_as_html_loader() -> None: assert len(docs) == 1 -def test_pypdfium2_loader() -> None: - """Test PyPDFium2Loader.""" - file_path = Path(__file__).parent.parent / "examples/hello.pdf" - loader = PyPDFium2Loader(file_path) - docs = loader.load() - - assert len(docs) == 1 - - file_path = Path(__file__).parent.parent / "examples/layout-parser-paper.pdf" - loader = PyPDFium2Loader(file_path) - - docs = loader.load() - assert len(docs) == 16 - - @pytest.mark.skipif( not os.environ.get("MATHPIX_API_KEY"), reason="Mathpix API key not found" ) @@ -184,6 +168,7 @@ def test_amazontextract_loader_failures() -> None: [ ("PDFMinerLoader", {}), ("PyMuPDFLoader", {}), + ("PyPDFium2Loader", {}), ("PyPDFLoader", {}), ], ) @@ -206,8 +191,6 @@ def test_standard_parameters( images_parser=None, images_inner_format="text", password=None, - extract_tables=None, - extract_tables_settings=None, ) docs = loader.load() assert len(docs) == 16 diff --git a/libs/community/tests/unit_tests/document_loaders/parsers/test_pdf_parsers.py b/libs/community/tests/unit_tests/document_loaders/parsers/test_pdf_parsers.py index ed1323bcd47a8..e89a8d854dfad 100644 --- a/libs/community/tests/unit_tests/document_loaders/parsers/test_pdf_parsers.py +++ b/libs/community/tests/unit_tests/document_loaders/parsers/test_pdf_parsers.py @@ -9,10 +9,7 @@ import langchain_community.document_loaders.parsers as pdf_parsers from langchain_community.document_loaders.base import BaseBlobParser from langchain_community.document_loaders.blob_loaders import Blob -from langchain_community.document_loaders.parsers.pdf import ( - PyPDFium2Parser, - _merge_text_and_extras, -) +from langchain_community.document_loaders.parsers.pdf import _merge_text_and_extras _THIS_DIR = Path(__file__).parents[3] @@ -74,19 +71,13 @@ def _assert_with_parser(parser: BaseBlobParser, *, splits_by_page: bool = True) assert int(metadata["page"]) == 0 -@pytest.mark.requires("pypdfium2") -def test_pypdfium2_parser() -> None: - """Test PyPDFium2 parser.""" - # Does not follow defaults to split by page. - _assert_with_parser(PyPDFium2Parser()) - - @pytest.mark.parametrize( "parser_factory,require,params", [ ("PDFMinerParser", "pdfminer", {"splits_by_page": False}), ("PyMuPDFParser", "pymupdf", {}), ("PyPDFParser", "pypdf", {}), + ("PyPDFium2Parser", "pypdfium2", {}), ], ) def test_parsers(