同步完整源码 - 2026-05-24

This commit is contained in:
xiaoxue 2026-05-24 23:41:32 +08:00
commit 755d34b16a
46 changed files with 2622 additions and 0 deletions

30
.github/workflows/build-badge.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: build
on:
schedule:
- cron: '5 0 * * *'
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.13", "3.12", "3.11", "3.10", "3.9"]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name : Build and run tests
run: |
mkdir $HOME/test_external_dir
cd $HOME/test_external_dir
cp $GITHUB_WORKSPACE/tests . -r
python -m venv venv
source venv/bin/activate
pip install $GITHUB_WORKSPACE pytest pytest-xdist
python -m pytest -n=auto

52
.github/workflows/python-publish.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: PyPI
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: deploy
url: https://pypi.org/p/hotpdf
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python3 -m pip install --upgrade setuptools wheel twine build
- name: Build and publish
env:
TWINE_USERNAME: ${{secrets.TWINE_USERNAME}}
TWINE_PASSWORD: ${{secrets.TWINE_PASSWORD}}
run: |
python -m build
twine upload dist/*
badge:
needs: deploy
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.13", "3.12", "3.11", "3.10", "3.9"]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name : Install from PyPI and run tests
run: |
sleep 2m
mkdir $HOME/test_external_dir
cd $HOME/test_external_dir
cp $GITHUB_WORKSPACE/tests/ . -r
python -m venv venv
source venv/bin/activate
pip install hotpdf pytest pytest-xdist
python -m pytest tests -n=auto

72
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,72 @@
name: Unit tests
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
linter:
name: Linters
runs-on: ubuntu-latest
env:
Python3_ROOT_DIR:
timeout-minutes: 2
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Cache python packages
uses: actions/cache@v4
id: cache
with:
path: ${{ env.Python3_ROOT_DIR }}/lib/python3.11/site-packages
key: dev-${{ hashFiles('**/pyproject.toml') }}-3.11
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -e '.[dev]'
- name: Lint with ruff
run: |
pip install --upgrade --force-reinstall ruff
python3 -m ruff check
- name: Run mypy
run: python -m mypy
tests:
needs: linter
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
env:
Python3_ROOT_DIR:
name: Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Cache python packages
uses: actions/cache@v4
id: cache
with:
path: ${{ env.Python3_ROOT_DIR }}/lib/python${{ matrix.python-version }}/site-packages
key: dev-${{ hashFiles('**/pyproject.toml') }}-${{ matrix.python-version }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pip install -e '.[dev]'
- name: Run tests
run:
python -m pytest --cov-fail-under=96 -n=auto --cov=hotpdf --cov-report term-missing
- name: Upload coverage to coveralls
if: github.event_name == 'push'
uses: coverallsapp/github-action@v2.2.3

171
.gitignore vendored Normal file
View File

@ -0,0 +1,171 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# ruff
.ruff_cache
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.vscode/
# MACOS
.DS_Store
# Project specific
main.py
memray-*

24
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,24 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: local
hooks:
- id: ruff-lint
name: ruff lint
entry: ruff check
language: system
pass_filenames: false
- id: ruff-format
name: ruff format
entry: ruff format
language: system
pass_filenames: false
- id: mypy
name: mypy
entry: mypy
language: system
pass_filenames: false

16
.readthedocs.yaml Normal file
View File

@ -0,0 +1,16 @@
version: 2
build:
os: "ubuntu-22.04"
tools:
python: "3.11"
sphinx:
configuration: docs/source/conf.py
python:
install:
- method: pip
path: .
extra_requirements:
- docs

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Prestatech GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

147
README.md Normal file
View File

@ -0,0 +1,147 @@
# hotpdf
[![Documentation Status](https://readthedocs.org/projects/hotpdf/badge/?version=latest)](https://hotpdf.readthedocs.io/en/latest/?badge=latest)
[![latest](https://github.com/weareprestatech/hotpdf/actions/workflows/python-publish.yml/badge.svg)](https://github.com/weareprestatech/hotpdf/actions/workflows/python-publish.yml)
[![build](https://github.com/weareprestatech/hotpdf/actions/workflows/build-badge.yml/badge.svg)](https://github.com/weareprestatech/hotpdf/actions/workflows/build-badge.yml)
[![Coverage Status](https://coveralls.io/repos/github/weareprestatech/hotpdf/badge.svg?branch=main)](https://coveralls.io/github/weareprestatech/hotpdf?branch=main)
[![Unit tests](https://github.com/weareprestatech/hotpdf/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/weareprestatech/hotpdf/actions/workflows/test.yml)
This project was started as an internal project @ [Prestatech](http://prestatech.com/) to parse PDF files in a fast and memory-efficient way to overcome the difficulties we were having while parsing big PDF files using libraries such as [pdfquery](https://github.com/jcushman/pdfquery) [[Comparison](https://imgur.com/a/5XuwEqq)].
hotpdf is a wrapper around [pdfminer.six](https://github.com/pdfminer/pdfminer.six) focusing on text extraction and text search operations on PDFs.
hotpdf can be used to find and extract text from PDFs.
Please [read the docs](https://hotpdf.readthedocs.io/en/latest/) to understand how the library can help you!
## Installation
The latest version of hotpdf can be installed directly from [PyPI](https://pypi.org/project/hotpdf/) with pip.
```bash
pip install hotpdf
```
## Local Setup
First, install the dependencies required by hotpdf
```bash
python3 -m pip install -e .
```
### Contributing
You should install the [pre-commit](https://github.com/weareprestatech/hotpdf/blob/main/.pre-commit-config.yaml) hooks with `pre-commit install`. This will run the linter, mypy, and ruff formatting before each commit.
Remember to run `pip install -e '.[dev]'` to install the extra dependencies for development.
For more examples of how to run the full test suite please refer to the [CI workflow](https://github.com/weareprestatech/hotpdf/blob/main/.github/workflows/test.yml).
We strive to keep the test coverage at 100% (but can't due to certain reasons - e.g., test file not available): if you want your contributions accepted please write tests for them :D
Some examples of running tests locally:
```bash
python3 -m pip install -e '.[dev]' # install extra deps for testing
python3 -m pytest -n=auto tests/ # run the test suite
# run tests with coverage
python3 -m pytest --cov-fail-under=96 -n=auto --cov=hotpdf --cov-report term-missing
```
### Documentation
We use [sphinx](https://www.sphinx-doc.org/en/master/) for generating our docs and host them on [readthedocs](https://readthedocs.org/)
Please update and add documentation if required, with your contributions.
Update the `.rst` files, rebuild them, and commit them along with your PRs.
```bash
cd docs
make clean
make html
```
This will generate the necessary documentation files. Once merged to `main` the docs will be updated automatically.
## Usage
**To view more detailed usage information, please [read the docs](https://hotpdf.readthedocs.io/en/latest/)**
Basic usage is as follows:
```python
from hotpdf import HotPdf
pdf_file_path = "test.pdf"
# Load pdf file into memory
hotpdf_document = HotPdf(pdf_file_path)
# Alternatively, you can also pass an opened PDF stream to be loaded
with open(pdf_file_path, "rb") as f:
hotpdf_document_2 = HotPdf(f)
# You can also merge multiple HotPdf objects to get one single HotPdf object
merged_hotpdf_object = HotPdf.merge_multiple(hotpdfs=[hotpdf1, hotpdf2])
# Get the number of pages
print(len(hotpdf_document.pages))
# Find text
text_occurences = hotpdf_document.find_text("foo")
# Find text and its full span
text_occurences_full_span = hotpdf_document.find_text("foo", take_span=True)
# Extract text in the region
text_in_bbox = hotpdf_document.extract_text(
x0=0,
y0=0,
x1=100,
y1=10,
page=0,
)
# Extract spans in the region
spans_in_bbox = hotpdf_document.extract_spans(
x0=0,
y0=0,
x1=100,
y1=10,
page=0,
)
# Extract spans text in the region
spans_text_in_bbox = hotpdf_document.extract_spans_text(
x0=0,
y0=0,
x1=100,
y1=10,
page=0,
)
# Extract full-page text
full_page_text = hotpdf_document.extract_page_text(page=0)
```
## Known Issues
1. (cid:x) characters in text - In some pdfs when extracted, some symbols like `€` might not be properly decoded, and instead be extracted as `(cid:128)`.
This is a problem with the `pdfminer.six` library. We have fixed it from our side on our [fork](https://github.com/weareprestatech/pdfminer.six), and you can install it using pip. Until we can merge it to pdfminer.six repo and it gets released, we recommend that you install our fork with the fixes manually.
```bash
pip install --no-cache-dir git+https://github.com/weareprestatech/pdfminer.six.git@20240222#egg=pdfminer-six
```
## License
This project is licensed under the terms of the MIT license.
---
with ❤️ from the team @ [Prestatech GmbH](https://prestatech.com/)

49
demo.py Normal file
View File

@ -0,0 +1,49 @@
from hotpdf import HotPdf
pdf_file_path = "test.pdf"
# Load pdf file into memory
hotpdf_document = HotPdf(pdf_file_path)
# Alternatively, you can also pass an opened pdf stream to be loaded
with open(pdf_file_path, "rb") as f:
hotpdf_document_2 = HotPdf(f)
# Get number of pages
print(len(hotpdf_document.pages))
# Find text
text_occurences = hotpdf_document.find_text("foo")
# Find text and its full span
text_occurences_full_span = hotpdf_document.find_text("foo", take_span=True)
# Extract text in region
text_in_bbox = hotpdf_document.extract_text(
x0=0,
y0=0,
x1=100,
y1=10,
page=0,
)
# Extract spans in region
spans_in_bbox = hotpdf_document.extract_spans(
x0=0,
y0=0,
x1=100,
y1=10,
page=0,
)
# Extract spans text in region
spans_text_in_bbox = hotpdf_document.extract_spans_text(
x0=0,
y0=0,
x1=100,
y1=10,
page=0,
)
# Extract full page text
full_page_text = hotpdf_document.extract_page_text(page=0)

1
docs/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

17
docs/source/api.rst Normal file
View File

@ -0,0 +1,17 @@
API
===
.. autosummary::
:toctree: generated
hotpdf.hotpdf.HotPdf
hotpdf.memory_map.MemoryMap
hotpdf.sparse_matrix.SparseMatrix
hotpdf.span_map.SpanMap
hotpdf.trie.TrieNode
hotpdf.trie.Trie
hotpdf.utils
hotpdf.processor
hotpdf.data.classes.HotCharacter
hotpdf.data.classes.Span
hotpdf.data.classes.ElementDimension

45
docs/source/conf.py Normal file
View File

@ -0,0 +1,45 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
import os
import sys
sys.path.insert(0, os.path.abspath("../.."))
project = "hotpdf"
copyright = "2024, Prestatech GmbH"
author = "Prestatech GmbH"
release = "0.1"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"sphinx.ext.duration",
"sphinx.ext.doctest",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.intersphinx",
"sphinx.ext.napoleon",
"sphinx.ext.autosectionlabel",
]
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"sphinx": ("https://www.sphinx-doc.org/en/master/", None),
}
intersphinx_disabled_domains = ["std"]
templates_path = ["_templates"]
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "bizstyle"

View File

@ -0,0 +1,30 @@
hotpdf.data.classes.ElementDimension
====================================
.. currentmodule:: hotpdf.data.classes
.. autoclass:: ElementDimension
.. automethod:: __init__
.. rubric:: Methods
.. autosummary::
~ElementDimension.__init__
.. rubric:: Attributes
.. autosummary::
~ElementDimension.span_id
~ElementDimension.x0
~ElementDimension.y0
~ElementDimension.x1
~ElementDimension.y1

View File

@ -0,0 +1,31 @@
hotpdf.data.classes.HotCharacter
================================
.. currentmodule:: hotpdf.data.classes
.. autoclass:: HotCharacter
.. automethod:: __init__
.. rubric:: Methods
.. autosummary::
~HotCharacter.__init__
.. rubric:: Attributes
.. autosummary::
~HotCharacter.is_anno
~HotCharacter.value
~HotCharacter.x
~HotCharacter.y
~HotCharacter.x_end
~HotCharacter.span_id

View File

@ -0,0 +1,29 @@
hotpdf.data.classes.Span
========================
.. currentmodule:: hotpdf.data.classes
.. autoclass:: Span
.. automethod:: __init__
.. rubric:: Methods
.. autosummary::
~Span.__init__
~Span.get_element_dimension
~Span.to_text
.. rubric:: Attributes
.. autosummary::
~Span.characters
~Span.span_id

View File

@ -0,0 +1,23 @@
hotpdf.hotpdf.HotPdf
====================
.. currentmodule:: hotpdf.hotpdf
.. autoclass:: HotPdf
.. automethod:: __init__
.. rubric:: Methods
.. autosummary::
~HotPdf.__init__
~HotPdf.extract_page_text
~HotPdf.extract_spans
~HotPdf.extract_spans_text
~HotPdf.extract_text
~HotPdf.find_text
~HotPdf.load
~HotPdf.merge_multiple

View File

@ -0,0 +1,20 @@
hotpdf.memory\_map.MemoryMap
============================
.. currentmodule:: hotpdf.memory_map
.. autoclass:: MemoryMap
.. automethod:: __init__
.. rubric:: Methods
.. autosummary::
~MemoryMap.__init__
~MemoryMap.build_memory_map
~MemoryMap.extract_text_from_bbox
~MemoryMap.find_text
~MemoryMap.load_memory_map

View File

@ -0,0 +1,16 @@
hotpdf.processor
================
.. automodule:: hotpdf.processor
.. rubric:: Functions
.. autosummary::
process

View File

@ -0,0 +1,19 @@
hotpdf.span\_map.SpanMap
========================
.. currentmodule:: hotpdf.span_map
.. autoclass:: SpanMap
.. automethod:: __init__
.. rubric:: Methods
.. autosummary::
~SpanMap.__init__
~SpanMap.get_span
~SpanMap.insert
~SpanMap.items

View File

@ -0,0 +1,18 @@
hotpdf.sparse\_matrix.SparseMatrix
==================================
.. currentmodule:: hotpdf.sparse_matrix
.. autoclass:: SparseMatrix
.. automethod:: __init__
.. rubric:: Methods
.. autosummary::
~SparseMatrix.__init__
~SparseMatrix.get
~SparseMatrix.insert

View File

@ -0,0 +1,18 @@
hotpdf.trie.Trie
================
.. currentmodule:: hotpdf.trie
.. autoclass:: Trie
.. automethod:: __init__
.. rubric:: Methods
.. autosummary::
~Trie.__init__
~Trie.insert
~Trie.search_all

View File

@ -0,0 +1,16 @@
hotpdf.trie.TrieNode
====================
.. currentmodule:: hotpdf.trie
.. autoclass:: TrieNode
.. automethod:: __init__
.. rubric:: Methods
.. autosummary::
~TrieNode.__init__

View File

@ -0,0 +1,20 @@
hotpdf.utils
============
.. automodule:: hotpdf.utils
.. rubric:: Functions
.. autosummary::
filter_adjacent_coords
find_neighbour_coord
get_element_dimension
intersect
to_text

191
docs/source/guide.rst Normal file
View File

@ -0,0 +1,191 @@
==================
User Guide
==================
This guide contains a couple of common use cases that your application might have. Since HotPdf returns data in its own structure, its readability may not be optimal.
To mitigate this issue, we are writing this guide to help you get started with using HotPdf.
Text Operations
------------------------------------------
Start by loading the file.
The PDF can be loaded either by passing the file path or the file stream.
.. code-block:: python
from hotpdf import HotPdf
pdf_file_path = "path to your pdf file"
# Loading from path
hotpdf_document = HotPdf(pdf_file_path)
# Loading opened file
with open(pdf_file_path, "rb") as f:
hotpdf_document_2 = HotPdf(f)
Alternatively, to load a file, you can also defer loading from the constructor and using `.load()` instead.
.. code-block:: python
from hotpdf import HotPdf
pdf_file_path = "path to your pdf file"
# Loading from path
hotpdf_document = HotPdf()
hotpdf_document.load(pdf_file_path)
# Loading opened file
hotpdf_document_2 = HotPdf()
with open(pdf_file_path, "rb") as f:
hotpdf_document_2.load(f)
You can also merge multiple HotPdf objects to get one single HotPdf object!
.. code-block:: python
merged_hotpdf_object = HotPdf.merge_multiple(hotpdfs=[
hotpdf_document,
hotpdf_document2,
])
The `HotPdf` object has many attributes that you can use to solve your problems. One of them is `pages`, representing each page of the PDF stored in data structures (trie & sparse matrix) to help with text operations.
Locked PDFs can be loaded passing the password as the password argument:
.. code-block:: python
hotpdf_document = HotPdf()
hotpdf_document = hotpdf_document.load(locked_pdf_file_path, password="your_password")
Number of Pages
~~~~~~~~~~~~~~~~~~
Begin by getting the number of loaded pages or the total pages in the PDF.
.. code-block:: python
len_pages: int = len(hotpdf_document.pages)
Each `page` in the `pages` list is an in-memory representation of a page of the loaded PDF. All operations are performed on a page.
Looking up Text
~~~~~~~~~~~~~~~~~~
To look up text, use the `find_text` function.
You can attempt to find the full span the text lies in by setting `take_span` to `True`.
.. code-block:: python
text_occurrences = hotpdf_document.find_text("foo")
This will return a `dict` of `list` of `list` of `HotCharacter`:
- The `dict` keys are the page numbers.
- The outer `list` is all the occurrences found on the page.
- The inner `list` contains character-wise all the words that were found.
The `HotCharacter` object contains the value and the coordinates of the character on the PDF.
To get the entire span of the found occurrence, you could reuse the implementation of `get_element_dimension` that is found under `hotpdf.utils`.
.. code-block:: python
from hotpdf.utils import get_element_dimension
# Getting the dimension of the first element that was found on page 0.
element_dimension = get_element_dimension(text_occurrences[0][0])
`element_dimension` will return an `ElementDimension` data object which has the `x0`, `y0`, `x1`, `y1` values. These coordinates represent the position the text was found in.
You can set `take_span` to `True` to find the whole span that it lies in. Usage remains the same.
.. code-block:: python
text_occurrences = hotpdf_document.find_text("foo", take_span=True)
# Getting the dimension of the first element that was found on page 0.
element_dimension = get_element_dimension(text_occurrences[0][0])
For example, if you are looking for a text like "hotpdf v23" but you know that the part "v23" is variable, you can simply search for "hotpdf v" or just "hotpdf".
This will return the spans of the text as well, so you could also find "hotpdf v24" just by searching for "hotpdf v" or "hotpdf".
**Please note:** The text children of a `Span` depend on the PDF producing software, so it could be unpredictable. Either way, if it works for you, then it works. Please test it!
Extracting Text
~~~~~~~~~~~~~~~~~~
If you know the coordinates of the text that you are going to extract, you can use the `extract_text` function.
This will extract the text that lay in the specified coordinates (`x0`, `y0`, `x1`, `y1`) of the specified `page`.
If you don't specify a `page` it will default to 0 (i.e., the first page).
.. code-block:: python
text = pdf.extract_text(x0=0, y0=0, x1=600, y1=500, page=0)
This will return a `str` in plain text format. Characters, if they are on different lines, will be separated by `\n`.
Extracting Spans Text
~~~~~~~~~~~~~~~~~~~~~~
If you want to extract text of all spans that lay or intersect the coordinates (`x0`, `y0`, `x1`, `y1`) that you specify on the `page` that you specify, you need to use the `extract_spans_text` function.
In case you want more granular view of the spans, use `extract_spans` instead.
.. code-block:: python
text_spans = pdf.extract_spans_text(x0=0, y0=0, x1=600, y1=500, page=0)
This will return a `list` of `str`.
The `list` contains text of spans that lay within the given coordinates.
Extracting Spans
~~~~~~~~~~~~~~~~~~
If you want to extract all spans that lay or intersect the coordinates (`x0`, `y0`, `x1`, `y1`) that you specify on the `page` that you specify, you need to use the `extract_spans` function.
.. code-block:: python
text_spans = pdf.extract_spans(x0=0, y0=0, x1=600, y1=500, page=0)
This will return a `list` of `Span`.
The `list` contains the spans that lay within the given coordinates. A `Span` is a collection of `HotCharacter`
To access a span, you can access it by index. For example, if you want to get the dimensions of the first span that was returned, you can do this:
.. code-block:: python
from hotpdf.utils import get_element_dimension
# Get the dimensions of the first span
first_span_dimensions = spans[0].get_element_dimension()
# Get the text of the first span
span_text = spans[0].to_text()
This will give you the dimension of the span.
If you want to extract the text, you can iterate over a span and get the `value` attribute of the `HotCharacter`.
.. code-block:: python
extracted_span = "".join([hc.value for hc in text_spans[0]])
Extracting Text of Page
~~~~~~~~~~~~~~~~~~~~~~~~~
In case you want to view the text of a specified page, you can use the `extract_page_text` function.
This will return you an `str` of the whole page of the PDF.
.. code-block:: python
page_text = pdf.extract_page_text(page=0)
---
We will keep adding more functions to help with various operations. In any case please feel free to open an issue on our github.

100
docs/source/index.rst Normal file
View File

@ -0,0 +1,100 @@
=========
hotpdf
=========
This project was started as an internal project @ `Prestatech <http://prestatech.com/>`_ to parse PDF files in a fast and memory efficient way to overcome the difficulties we were having while parsing big PDF files using libraries such as `pdfquery <https://github.com/jcushman/pdfquery>`_.
hotpdf can be used to find and extract text from PDFs.
Installation
------------
The latest version of `hotpdf` can be installed directly from `PYPI`_ with pip.
.. code-block:: bash
pip install hotpdf
.. _PYPI: https://pypi.org/project/hotpdf/
Local Setup
------------
First install the dependencies required by hotpdf
.. code-block:: bash
python3 -m pip install -e .
Contributing
------------
You should install the `pre-commit` hooks with ``pre-commit install``. This will run the linter, mypy, and a subset of the tests on every commit.
For more examples of how to run the full test suite, please refer to the `CI workflow <https://github.com/weareprestatech/hotpdf/blob/main/.github/workflows/test.yml>`_.
We strive to keep the test coverage at 100%: if you want your contributions accepted, please write tests for them :D
Some examples of running tests locally:
.. code-block:: bash
python3 -m pip install -e '.[testing]' # install extra deps for testing
python3 -m pytest -n=auto test/ # run the test suite
# run tests with coverage
python3 -m pytest --cov-fail-under=96 -n=auto --cov=hotpdf --cov-report term-missing
Known Issues
--------------
1. (cid:x) characters in text - Some pdfs when extracted, some symbols like `€` might not be properly decoded, and instead be extracted as `(cid:128)`.
This is a problem with the `pdfminer.six` library. We have fixed it from our side on our `fork <https://github.com/weareprestatech/pdfminer.six>`, and you can install it using pip. Until we are able to merge it to pdfminer.six repo and it gets released, we recommend that you install our fork with the fixes manually.
.. code-block:: bash
pip install --no-cache-dir git+https://github.com/weareprestatech/pdfminer.six.git@20240222#egg=pdfminer-six
Documentation
--------------
We use `sphinx <https://www.sphinx-doc.org/en/master/>`_ for generating our docs and host them on `readthedocs <https://readthedocs.org/>`_.
Please update and add documentation if required, with your contributions.
Update the `.rst` files, rebuild them, and commit them along with your PRs.
.. code-block:: bash
cd docs
make clean
make html
This will generate the necessary documentation files. Once merged to `main` the docs will be updated automatically.
Contents
--------------
To view detailed usage information, please navigate to `Usage` and `User Guide`
.. toctree::
:maxdepth: 3
:caption: Contents:
usage
guide
api
License
-------
This project is licensed under the terms of the MIT license.
---
with ❤️ from the team @ `Prestatech GmbH <https://prestatech.com/>`_

160
docs/source/usage.rst Normal file
View File

@ -0,0 +1,160 @@
=========
Usage
=========
HotPdf Class
------------------------------------------
.. autoclass:: hotpdf.HotPdf
.. autofunction:: hotpdf.HotPdf.__init__
The HotPdf class is the wrapper around your PDF that allows for searching text and extracting text on your PDFs.
.. code-block:: python
from hotpdf import HotPdf
pdf_file_path = "path to your pdf file"
# Load directly from Path
hotpdf_document = HotPdf(pdf_file_path)
# Load from file stream
with open(pdf_file_path, "rb") as f:
hotpdf_document_2 = HotPdf(f)
Alternatively you can defer loading, and use the `.load()` function instead. The outcome is the same, internally the constructor for `HotPdf` calls the `.load()` function
.. code-block:: python
from hotpdf import HotPdf
pdf_file_path = "path to your pdf file"
# path
hotpdf_document = HotPdf()
hotpdf_document = hotpdf_document.load(pdf_file_path)
# file stream
hotpdf_document_2 = HotPdf()
with open(pdf_file_path, "rb") as f:
hotpdf_document_2 = hotpdf_document_2.load(f)
.. autofunction:: hotpdf.HotPdf.load
You can also merge multiple HotPdf objects to get one single HotPdf object!
.. code-block:: python
merged_hotpdf_object = HotPdf.merge_multiple(hotpdfs=[
hotpdf_document,
hotpdf_document2,
])
.. autofunction:: hotpdf.HotPdf.merge_multiple
File Operations
------------------------------------------
Length
~~~~~~~~~~~~~~~~~~~
The number of pages in the PDF file can be determined by checking the `len` of `pages` property of the hotpdf object.
.. code-block:: python
num_pages = len(hotpdf_document.pages)
Search
------------------------------------------
find_text
~~~~~~~~~~~~~~~~~~~
To look for a string in the entire PDF File, you can use the `find_text` function.
You can also specify what pages you want to search in. By default it will look through the whole PDF.
To get the whole span where the string lies in, you can set `take_span` to True.
.. code-block:: python
text_occurences = hotpdf_document.find_text("foo")
text_occurences_with_span = hotpdf_document.find_text(
"foo",
take_span=True,
)
.. autofunction:: hotpdf.HotPdf.find_text
Extraction
------------------------------------------
extract_text
~~~~~~~~~~~~~~~~~~~
To extract string from specific positions in the PDF, you can use the `extract_text` function.
This will extract the string that lies within the positions that have been specified on the page that it's specified (default is Page 0).
.. code-block:: python
text_in_bbox = hotpdf_document.extract_text(
x0=0,
y0=0,
x1=100,
y1=10,
page=0,
)
.. autofunction:: hotpdf.HotPdf.extract_text
extract_spans
~~~~~~~~~~~~~~~~~~~
Instead of just the individual characters that lay within the bounds that you specify, if you want full words, or the complete spans that intersect within the specified bounds - you can use the `extract_spans` functions instead.
This will extract all the spans that intersect with the positions that have been specified on the page that it's specified (default is Page 0).
.. code-block:: python
spans_in_bbox = hotpdf_document.extract_spans(
x0=0,
y0=0,
x1=100,
y1=10,
page=0,
)
.. autofunction:: hotpdf.HotPdf.extract_spans
extract_spans_text
~~~~~~~~~~~~~~~~~~~
Instead of handling the spans structures yourself, if you are only interested in the text of the spans, you can use the `extract_spans_text` function instead.
The function is the same as `extract_spans`_ except it returns you a `list` of `str`.
.. code-block:: python
spans_text_in_bbox = hotpdf_document.extract_spans_text(
x0=0,
y0=0,
x1=100,
y1=10,
page=0,
)
.. autofunction:: hotpdf.HotPdf.extract_spans_text
extract_page_text
~~~~~~~~~~~~~~~~~~~
If you want to view the text of an entire page in plaintext `str` format, you can use the `extract_page_text` function.
The function accepts `page` as a parameter.
.. code-block:: python
page_text = hotpdf_document.extract_page_text(page=0,)
.. autofunction:: hotpdf.HotPdf.extract_page_text

4
hotpdf/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from hotpdf.hotpdf import HotPdf
__version__ = "0.1.0"
__all__ = ["HotPdf"]

0
hotpdf/data/__init__.py Normal file
View File

94
hotpdf/data/classes.py Normal file
View File

@ -0,0 +1,94 @@
from collections import defaultdict
from dataclasses import dataclass
from typing import Optional
from uuid import UUID
@dataclass
class HotCharacter:
"""A hot character is a character on a page with certain attributes.
Attributes:
value (str): value value of the character.
x (int): x position of the character - column number.
y (int): y position of the character - row number.
x_end (int): end x position of the character. x_end - x = width of character.
span_id (UUID): hash of parent span the character lies in.
is_anno (bool, Optional): specify if a character is an annotation (default: False).
"""
value: str
x: int
y: int
x_end: int
span_id: UUID
is_anno: Optional[bool] = False
@dataclass
class ElementDimension:
"""ElementDimension is the dimension of an element in hotpdf.
Attributes:
x0 (int): starting x position of the element (column).
y0 (int): starting y position of the element (row).
x1 (int): end x position of the element (column). x1 - x0 = width.
y1 (int): end y position of the element (row) y1 - y0 = height.
span_id (UUID, Optional): hash of parent span the element lies in.
"""
x0: int
y0: int
x1: int
y1: int
span_id: Optional[UUID] = None
@dataclass(init=True)
class Span:
"""A span is a group of characters that are close to each other.
Attributes:
characters (list[HotCharacter]): list of characters in the span.
x0 (int): starting x position of the span (column).
y0 (int): starting y position of the span (row).
x_end (int): end x position of the span (column). x_end - x0 = width.
span_id (UUID, Optional): hash of the span.
"""
characters: list[HotCharacter]
span_id: UUID
def to_text(self) -> str:
"""Convert the span to text.
Returns:
str: text representation of the span.
"""
return "".join(char.value for char in self.characters)
def get_element_dimension(self) -> ElementDimension:
"""Get the element dimension of the span.
Raises:
ValueError: if the span has no characters.
Returns:
ElementDimension: _description_
"""
if not self.characters:
raise ValueError("Span has no characters")
x0 = self.characters[0].x
y0 = self.characters[0].y
x1 = self.characters[-1].x_end
y1 = self.characters[-1].y
return ElementDimension(x0, y0, x1, y1, self.span_id)
# All occurences of HotCharacters in a page
# A list[HotCharacter] is the representation of a word split into "HotCharacters"
# A list[list[HotCharacter]] is a list of multiple list[HotCharacter] found on a page
PageResult = list[list[HotCharacter]]
# Complete PageResult with Page Number as the index
SearchResult = defaultdict[int, PageResult]

View File

View File

@ -0,0 +1,2 @@
class HotPdfIsNoneError(Exception):
pass

361
hotpdf/hotpdf.py Normal file
View File

@ -0,0 +1,361 @@
import math
import os
from collections import defaultdict
from io import IOBase
from pathlib import PurePath
from typing import Optional, Union
from hotpdf import processor
from hotpdf.exceptions.custom_exceptions import HotPdfIsNoneError
from hotpdf.memory_map import MemoryMap
from hotpdf.utils import filter_adjacent_coords, intersect
from .data.classes import ElementDimension, HotCharacter, PageResult, SearchResult, Span
class HotPdf:
def __init__(
self,
pdf_file: Union[PurePath, str, IOBase, None] = None,
password: str = "",
page_numbers: Optional[list[int]] = None,
extraction_tolerance: int = 4,
laparams: Optional[dict[str, Union[float, bool]]] = None,
include_annotation_spaces: bool = False,
preserve_pdfminer_coordinates: bool = False,
) -> None:
"""Initialize the HotPdf class.
Args:
pdf_file (PurePath | str | IOBytes): The path to the PDF file to be loaded, or a bytes object.
password (str, optional): Password to use to unlock the pdf
page_numbers (list[int], optional): Pages to be loaded into memory. (0-indexed).
If not provided, will load all pages (default).
extraction_tolerance (int, optional): Tolerance value used during text extraction
to adjust the bounding box for capturing text. Defaults to 4.
laparams (dict[str, Union[float, bool]], optional): Layout parameters for pdfminer.
include_annotation_spaces (bool, optional): Add annotation spaces to the memory map. Default: False
preserve_pdfminer_coordinates (bool, Optional): Preserve pdfminer y-coordinate values.
Default: False - use natural coords
Raises:
ValueError: If the page range is invalid.
FileNotFoundError: If the file is not found.
PermissionError: If the file is encrypted or the password is wrong.
RuntimeError: If an unknown error is generated by transfotmer.
"""
self.pages: list[MemoryMap] = []
self.extraction_tolerance: int = extraction_tolerance
if pdf_file:
self.load(
pdf_file,
password,
page_numbers,
laparams=laparams,
include_annotation_spaces=include_annotation_spaces,
preserve_pdfminer_coordinates=preserve_pdfminer_coordinates,
)
def __check_file_exists(self, pdf_file: str) -> None:
if not os.path.exists(pdf_file):
raise FileNotFoundError(f"File {pdf_file} not found")
def __check_coordinates(self, x0: int, y0: int, x1: int, y1: int) -> None:
if x0 < 0 or x1 < 0 or y0 < 0 or y1 < 0:
raise ValueError("Invalid coordinates")
def __check_page_number(self, page: int) -> None:
if page < 0 or page >= len(self.pages):
raise ValueError("Invalid page number")
def __check_page_numbers(self, pages: list[int]) -> None:
for page in pages:
self.__check_page_number(page)
def __check_page_range(self, page_numbers: list[int]) -> None:
if any(_page_num < 0 for _page_num in page_numbers):
raise ValueError("Invalid page range")
def __prechecks(self, pdf_file: Union[PurePath, str, IOBase], page_numbers: list[int]) -> None:
if type(pdf_file) is str:
self.__check_file_exists(pdf_file)
self.__check_page_range(page_numbers)
@staticmethod
def merge_multiple(
hotpdfs: list["HotPdf"],
) -> "HotPdf":
"""Merge multiple HotPdf objects and return a single HotPdf object consisting of all pages
Args:
hotpdfs (list[HotPdf]): List of HotPdf objects that will be combined
to form one single HotPdf object. All other params will be ignored in this case.
Raises:
HotPdfIsNoneError: If any of the HotPdf objects in the hotpdfs list is None
Returns:
HotPdf: Merged HotPdf object
"""
if any(_hotpdf is None for _hotpdf in hotpdfs):
raise HotPdfIsNoneError("HotPdf object cannot be None")
merged_hotpdf = HotPdf()
for _hotpdf in hotpdfs:
merged_hotpdf.pages.extend(_hotpdf.pages)
return merged_hotpdf
def load(
self,
pdf_file: Union[PurePath, str, IOBase],
password: str = "",
page_numbers: Optional[list[int]] = None,
laparams: Optional[dict[str, Union[float, bool]]] = None,
include_annotation_spaces: bool = False,
preserve_pdfminer_coordinates: bool = False,
) -> None:
"""Load a PDF file into memory.
Args:
pdf_file (str | Bytes): The path to the PDF file to be loaded, or a bytes object.
password (str, optional): Password to use to unlock the pdf
page_numbers (list[int], optional): Pages to be loaded into memory. (0-indexed).
If not provided, will load all pages (default).
laparams (dict[str, Union[float, bool]], optional): Layout parameters for pdfminer.
include_annotation_spaces (bool, optional): Add annotation spaces to the memory map.
preserve_pdfminer_coordinates (bool, Optional): Preserve pdfminer y-coordinate values.
Default: False - use natural coords
Raises:
Exception: If an unknown error is generated by pdfminer.
"""
page_numbers = page_numbers or []
self.__prechecks(pdf_file, page_numbers)
try:
self.pages = processor.process(
source=pdf_file,
password=password,
page_numbers=page_numbers,
laparams=laparams,
include_annotation_spaces=include_annotation_spaces,
preserve_pdfminer_coordinates=preserve_pdfminer_coordinates,
)
except Exception as e:
raise e
def __extract_full_text_span(
self,
hot_characters: list[HotCharacter],
page_num: int,
) -> Union[list[HotCharacter], None]:
"""Extract the full span of text that the given hot characters are a part of.
Args:
hot_characters (list[HotCharacter]): the list of hot characters to extract the span for.
page_num (int): the page number of the hot characters.
Returns:
Union[list[HotCharacter], None]: the full span of text that the hot characters are a part of.
"""
_span: Optional[Span] = None
if hot_characters[0].span_id:
_span = self.pages[page_num].span_map[hot_characters[0].span_id]
return _span.characters if _span else None
def find_text(
self,
query: str,
pages: Optional[list[int]] = None,
take_span: bool = False,
sort: bool = True,
case_sensitive: bool = True,
) -> SearchResult:
"""Find text within the loaded PDF pages.
Args:
query (str): The text to search for.
pages (list[int], optional): List of page numbers to search.
take_span (bool, optional): Take the full span of the text that it is a part of.
sort (bool, Optional): Return elements sorted by their positions.
case_sensitive (bool, optional): Whether the search should be case-sensitive. Defaults to True.
Raises:
ValueError: If the page number is invalid.
Returns:
SearchResult: A dictionary mapping page numbers to found text coordinates.
"""
pages = pages or []
self.__check_page_numbers(pages)
query_pages = (
{i: self.pages[i] for i in range(len(self.pages))} if len(pages) == 0 else {i: self.pages[i] for i in pages}
)
found_page_map = {}
normalized_query = query if case_sensitive else query.lower()
for page_num in query_pages:
found_page_map[page_num] = filter_adjacent_coords(
*query_pages[page_num].find_text(query, case_sensitive=case_sensitive)
)
final_found_page_map: SearchResult = defaultdict(PageResult)
for page_num in found_page_map:
hot_character_page_occurences: PageResult = found_page_map[page_num]
final_found_page_map[page_num] = []
for hot_characters in hot_character_page_occurences:
text = "".join(hc.value for hc in hot_characters)
comparable_text = text if case_sensitive else text.lower()
if normalized_query not in comparable_text:
continue
full_span_dimension_hot_characters: Union[list[HotCharacter], None] = (
self.__extract_full_text_span(
hot_characters=hot_characters,
page_num=page_num,
)
if take_span
else None
)
chars_to_append = (
full_span_dimension_hot_characters
if (take_span and full_span_dimension_hot_characters)
else hot_characters
)
if chars_to_append and sort:
chars_to_append = sorted(chars_to_append, key=lambda ch: (ch.y, ch.x))
final_found_page_map[page_num].append(chars_to_append)
if sort:
for _page in final_found_page_map:
final_found_page_map[_page] = sorted(
final_found_page_map[_page], key=lambda element: (element[0].y, element[0].x)
)
return final_found_page_map
def extract_spans(self, x0: int, y0: int, x1: int, y1: int, page: int = 0, sort: bool = True) -> list[Span]:
"""Extract spans that intersect with the given bounding box.
Args:
x0 (int): The left x-coordinate of the bounding box.
y0 (int): The bottom y-coordinate of the bounding box.
x1 (int): The right x-coordinate of the bounding box.
y1 (int): The top y-coordinate of the bounding box.
page (int, optional): The page number. Defaults to 0.
sort (bool, optional): Sort the spans by their coordinates. Defaults to True.
Raises:
ValueError: If the coordinates are invalid.
ValueError: If the page number is invalid.
Returns:
list[Span]: List of spans of hotcharacters that intersect with the given bounding box
"""
spans: list[Span] = []
self.__check_coordinates(x0, y0, x1, y1)
self.__check_page_number(page)
for _, span in self.pages[page].span_map.items():
if intersect(ElementDimension(x0, y0, x1, y1, None), span.get_element_dimension()):
span.characters = sorted(span.characters, key=lambda ch: (ch.y, ch.x))
spans.append(span)
if sort:
spans = sorted(
spans, key=lambda span: (span.get_element_dimension().y0, span.get_element_dimension().x0)
)
return spans
def extract_text(
self,
x0: int,
y0: int,
x1: int,
y1: int,
page: int = 0,
) -> str:
"""Extract text from a specified bounding box on a page.
Args:
x0 (int): The left x-coordinate of the bounding box.
y0 (int): The bottom y-coordinate of the bounding box.
x1 (int): The right x-coordinate of the bounding box.
y1 (int): The top y-coordinate of the bounding box.
page (int): The page number. Defaults to 0.
Raises:
ValueError: If the coordinates are invalid.
ValueError: If the page number is invalid.
Returns:
str: Extracted text within the bounding box.
"""
self.__check_coordinates(x0, y0, x1, y1)
self.__check_page_number(page)
page_to_search: MemoryMap = self.pages[page]
extracted_text = page_to_search.extract_text_from_bbox(
x0=math.floor(x0),
x1=math.ceil(x1 + self.extraction_tolerance),
y0=y0,
y1=y1,
)
return extracted_text
def extract_page_text(
self,
page: int,
) -> str:
"""Extract text from a specified page.
Args:
page (int): The page number.
Raises:
ValueError: If the page number is invalid.
Returns:
str: Extracted text from the page.
"""
self.__check_page_number(page)
page_to_search: MemoryMap = self.pages[page]
extracted_text = page_to_search.extract_text_from_bbox(
x0=0,
x1=page_to_search.width,
y0=0,
y1=page_to_search.height,
)
return extracted_text
def extract_spans_text(
self,
x0: int,
y0: int,
x1: int,
y1: int,
page: int = 0,
) -> str:
"""Extract text from spans that intersect with the given bounding box.
Args:
x0 (int): The left x-coordinate of the bounding box.
y0 (int): The bottom y-coordinate of the bounding box.
x1 (int): The right x-coordinate of the bounding box.
y1 (int): The top y-coordinate of the bounding box.
page (int, optional): The page number. Defaults to 0.
Raises:
ValueError: If the coordinates are invalid.
ValueError: If the page number is invalid.
Returns:
str: Extracted text that intersects with the bounding box.
"""
self.__check_coordinates(x0, y0, x1, y1)
self.__check_page_number(page)
spans: list[Span] = self.extract_spans(x0, y0, x1, y1, page)
extracted_text: list[str] = []
for span in spans:
extracted_text.append(span.to_text())
return "".join(extracted_text)

250
hotpdf/memory_map.py Normal file
View File

@ -0,0 +1,250 @@
import math
from collections import defaultdict
from collections.abc import Generator
from typing import Union
from uuid import UUID, uuid4
from pdfminer.layout import LTAnno, LTChar, LTComponent, LTFigure, LTPage, LTText, LTTextContainer, LTTextLine
from .data.classes import HotCharacter, PageResult
from .span_map import SpanMap
from .sparse_matrix import SparseMatrix
from .trie import Trie
class MemoryMap:
def __init__(self) -> None:
"""Initialize the MemoryMap. 2D Matrix representation of a PDF Page.
Args:
width (int): The width of a page.
height (int) The height of a page.
"""
self.text_trie = Trie()
self.span_map = SpanMap()
self.width: int = 0
self.height: int = 0
def build_memory_map(self) -> None:
"""Build the memory map based on width and height.
The memory map is a SparseMatrix representation of the PDF.
"""
self.memory_map = SparseMatrix()
def __reverse_page_objs(self, page_objs: list[LTComponent]) -> Generator[LTComponent, None, None]:
yield from reversed(page_objs)
def __extract_from_ltfigure(self, lt_figure_obj: LTFigure) -> Generator[Union[LTTextLine, LTChar], None, None]:
for element in lt_figure_obj:
if isinstance(element, (LTTextLine, LTChar)):
yield element
elif isinstance(element, LTTextContainer):
element_stack = self.__reverse_page_objs(list(element))
yield from (em for em in element_stack if isinstance(em, LTTextLine))
def __get_page_spans(self, page: LTPage) -> Generator[Union[LTTextLine, LTChar], None, None]:
element_stack = self.__reverse_page_objs(page._objs)
for obj in element_stack:
if isinstance(obj, LTTextLine):
yield obj
elif isinstance(obj, (LTTextContainer)):
element_stack = self.__reverse_page_objs(list(obj))
yield from (em for em in element_stack if isinstance(em, LTTextLine))
elif isinstance(obj, (LTFigure)):
yield from self.__extract_from_ltfigure(obj)
def __get_right_shifted_hot_character(self, hot_character: HotCharacter, shift: int) -> HotCharacter:
"""Shift a hotcharacter on the x-coordinate
Args:
hot_character (HotCharacter): HotCharacter object to be shifted
shift (int): shift offset on the x-coordinate
Returns:
HotCharacter: HotCharacter object with new coords
"""
hot_character.x += shift
hot_character.x_end += shift
return hot_character
def load_memory_map(
self,
page: LTPage,
include_annotation_spaces: bool = False,
preserve_pdfminer_coordinates: bool = False,
) -> None:
"""Load memory map data from an XML page.
Args:
page (str): LTPage Element returned by pdfminer
include_annotation_spaces (bool, optional): Add annotation spaces to the memory map.
preserve_pdfminer_coordinates(bool, optional): Preserve pdfminer y-coordinate values.
Returns:
None
"""
char_hot_characters: list[HotCharacter] = []
page_components: Generator[Union[LTTextLine, LTChar], None, None] = self.__get_page_spans(page)
line_shift: defaultdict[int, int] = defaultdict(int)
for component in page_components:
span_id = uuid4()
prev_char_inserted = False
if isinstance(component, LTChar):
x0 = round(component.x0)
x1 = round(component.x1)
y0 = round(component.y0) if preserve_pdfminer_coordinates else round(page.height - component.y0)
hot_character: HotCharacter = self.__get_hot_character_of(
value=component.get_text(),
x=x0,
y=y0,
x_end=x1,
span_id=span_id,
)
char_hot_characters.append(
hot_character,
)
continue
for character in component:
if (
include_annotation_spaces
and isinstance(character, LTAnno)
and (character._text == " ")
and prev_char_inserted
):
_elem_width: int = 1
space_char: HotCharacter = self.__get_hot_character_of(
value=" ", x=x0, y=y0, x_end=x0 + _elem_width, span_id=span_id, is_anno=True
)
char_hot_characters.append(
space_char,
)
prev_char_inserted = False
elif (
isinstance(character, (LTChar, LTText))
and (
hasattr(character, "x0")
and hasattr(character, "x1")
and hasattr(character, "y0")
and hasattr(character, "y1")
)
and not isinstance(character, LTAnno)
):
char_c = character.get_text()
x0 = round(character.x0)
x1 = round(character.x1)
y0 = round(component.y0) if preserve_pdfminer_coordinates else round(page.height - component.y0)
hot_character = self.__get_hot_character_of(
value=char_c,
x=x0,
y=y0,
x_end=x1,
span_id=span_id,
)
char_hot_characters.append(
hot_character,
)
prev_char_inserted = char_c != " "
# Insert into Trie and Span Maps
last_inserted_x_y: tuple[int, int] = (-1, -1)
for i in range(len(char_hot_characters)):
_current_character: HotCharacter = char_hot_characters[i]
# Determine if annotation spaces should be added
if include_annotation_spaces and i > 0 and i < len(char_hot_characters) - 1:
prev_char: HotCharacter = char_hot_characters[i - 1]
next_char: HotCharacter = char_hot_characters[i + 1]
# Anno and non-anno characters should not be overlapping
if _current_character.is_anno and next_char.x - prev_char.x <= 0:
continue
# Prevent characters from overlapping
if (last_inserted_x_y[0] > 0 and last_inserted_x_y[1] > 0) and (
_current_character.x == last_inserted_x_y[0] and _current_character.y == last_inserted_x_y[1]
):
line_shift[_current_character.y] += 1
last_inserted_x_y = (_current_character.x, _current_character.y)
_current_character = self.__get_right_shifted_hot_character(
_current_character, line_shift[_current_character.y]
)
self.__insert_hotcharacter_to_memory(_current_character)
self.width = math.ceil(page.width)
self.height = math.ceil(page.height)
def __insert_hotcharacter_to_memory(
self,
hot_character: HotCharacter,
) -> None:
"""Insert hotcharacter into memory map & trie"""
if hot_character.value == "":
return None
self.memory_map.insert(value=hot_character.value, row_idx=hot_character.y, column_idx=hot_character.x)
self.text_trie.insert(word=hot_character.value, hot_character=hot_character)
if hot_character.span_id:
self.span_map[hot_character.span_id] = hot_character
def __get_hot_character_of(
self,
value: str,
x: int,
y: int,
x_end: int,
span_id: UUID,
is_anno: bool = False,
) -> HotCharacter:
"""Insert element into memory map.
Args:
value (str): Value of the object to be inserted.
x (int): column index (x0-coordinate) of the element.
y (int): row index (y0-coordinate) of the element.
x_end (int): end column index (x1-coordinate) of element. x_end - x = width of element.
span_id (UUID): id of parent span.
Returns:
HotCharacter: HotCharacter object of the whitespace.
"""
return HotCharacter(
value=value,
x=x,
y=y,
x_end=x_end,
span_id=span_id,
is_anno=is_anno,
)
def extract_text_from_bbox(self, x0: int, x1: int, y0: int, y1: int) -> str:
"""Extract text within a specified bounding box.
Args:
x0 (int): Left x-coordinate of the bounding box.
x1 (int): Right x-coordinate of the bounding box.
y0 (int): Bottom y-coordinate of the bounding box.
y1 (int): Top y-coordinate of the bounding box.
Returns:
str: Extracted text within the bounding box.
"""
extracted_text: str = ""
for row in range(max(y0, 0), min(y1, self.memory_map.rows - 1) + 1):
row_text: str = ""
row_text = "".join(
self.memory_map.get(row_idx=row, column_idx=col)
for col in range(max(x0, 0), min(x1, self.memory_map.columns - 1) + 1)
)
if row_text:
extracted_text += row_text + "\n"
return extracted_text
def find_text(self, query: str, case_sensitive: bool = True) -> tuple[list[str], PageResult]:
"""Find text within the memory map.
Args:
query (str): The text to search for.
case_sensitive (bool): Whether the search should be case-sensitive. Defaults to True.
Returns:
list: List of found text coordinates.
"""
found_text = self.text_trie.search_all(query, case_sensitive=case_sensitive)
return found_text

73
hotpdf/processor.py Normal file
View File

@ -0,0 +1,73 @@
import logging
from io import IOBase
from pathlib import PurePath
from typing import Optional, Union
from pdfminer.high_level import extract_pages
from pdfminer.layout import LAParams
from hotpdf.memory_map import MemoryMap
logging.getLogger("pdfminer").setLevel(logging.ERROR)
def __make_custom_laparams_object(
laparams: Optional[dict[str, Union[float, bool]]] = None,
) -> Union[LAParams, None]:
if not laparams:
return None
laparams_obj: LAParams = LAParams()
for key in laparams:
if hasattr(laparams_obj, key):
laparams_obj.__setattr__(key, laparams[key])
return laparams_obj
def __supress_pdfminer_logs() -> None:
logging.getLogger("pdfminer").setLevel(logging.ERROR)
def __process(
source: Union[PurePath, str, IOBase],
password: str = "",
page_numbers: Optional[list[int]] = None,
laparams: Optional[dict[str, Union[float, bool]]] = None,
include_annotation_spaces: bool = False,
preserve_pdfminer_coordinates: bool = False,
) -> list[MemoryMap]:
pages: list[MemoryMap] = []
__supress_pdfminer_logs()
page_numbers = sorted(page_numbers) if page_numbers else []
laparams_obj = __make_custom_laparams_object(laparams)
hl_page_layouts = extract_pages(
source, password=password, page_numbers=page_numbers, caching=True, laparams=laparams_obj
)
for page_layout in hl_page_layouts:
parsed_page: MemoryMap = MemoryMap()
parsed_page.build_memory_map()
parsed_page.load_memory_map(
page=page_layout,
include_annotation_spaces=include_annotation_spaces,
preserve_pdfminer_coordinates=preserve_pdfminer_coordinates,
)
pages.append(parsed_page)
return pages
def process(
source: Union[PurePath, str, IOBase],
password: str = "",
page_numbers: Optional[list[int]] = None,
laparams: Optional[dict[str, Union[float, bool]]] = None,
include_annotation_spaces: bool = False,
preserve_pdfminer_coordinates: bool = False,
) -> list[MemoryMap]:
return __process(
source=source,
password=password,
page_numbers=page_numbers,
laparams=laparams,
include_annotation_spaces=include_annotation_spaces,
preserve_pdfminer_coordinates=preserve_pdfminer_coordinates,
)

45
hotpdf/span_map.py Normal file
View File

@ -0,0 +1,45 @@
from collections.abc import Iterable
from typing import Union
from uuid import UUID
from .data.classes import HotCharacter, Span
class SpanMap:
"""Hashmap to store spans and their child words for fast referencing
and character grouping.
Keys are span_ids and values are Span objects.
"""
def __init__(self) -> None:
self.span_map: dict[UUID, Span] = dict()
def __len__(self) -> int:
return len(self.span_map)
def __getitem__(self, span_id: UUID) -> Union[Span, None]:
return self.get_span(span_id)
def __setitem__(self, span_id: UUID, hot_character: HotCharacter) -> None:
self.insert(span_id, hot_character)
def items(self) -> Iterable[tuple[UUID, Span]]:
yield from self.span_map.items()
def insert(self, span_id: UUID, hot_character: HotCharacter) -> None:
span = self.span_map.get(span_id)
if not span:
span = Span(
characters=[],
span_id=span_id,
)
span.characters.append(hot_character)
self.span_map[span_id] = span
def get_span(self, span_id: UUID) -> Union[Span, None]:
span = self.span_map.get(span_id)
if not span:
return None
span.characters = sorted(span.characters, key=lambda ch: (ch.y, ch.x))
return span

60
hotpdf/sparse_matrix.py Normal file
View File

@ -0,0 +1,60 @@
from collections import defaultdict
from collections.abc import Iterator
from warnings import warn
class SparseMatrix:
"""2D representation of a PDF in plain text format.
Sparse matrix removes the need to keep in memory the blank spaces
that are present in a PDF. Thus reducing memory usage drastically.
"""
def __init__(self, rows: int = 0, columns: int = 0):
self.values: defaultdict[tuple[int, int], str] = defaultdict(str)
self.rows = rows
self.columns = columns
def __getitem__(self, key: tuple[int, int]) -> str:
row_idx, column_idx = key
self.__check_indices(row_idx, column_idx)
return self.values[(row_idx, column_idx)]
def __update_indices(self, row_idx: int, column_idx: int) -> None:
if row_idx > self.rows:
self.rows = row_idx + 1
if column_idx > self.columns:
self.columns = column_idx + 1
def __check_indices(self, row_idx: int, column_idx: int) -> None:
if row_idx < 0 or column_idx < 0:
raise IndexError("Specified index is out of range")
def __setitem__(self, key: tuple[int, int], value: str) -> None:
row_idx, column_idx = key
try:
self.__check_indices(row_idx, column_idx)
except IndexError as ie:
warn("Index Error. Skipping insertion into SparseMatrix", stacklevel=1)
warn(str(ie), stacklevel=1)
return
self.__update_indices(row_idx, column_idx)
if value:
self.values[(row_idx, column_idx)] = value
def __iter__(self) -> Iterator[tuple[tuple[int, int], str]]:
yield from self.values.items()
def insert(self, value: str, row_idx: int, column_idx: int) -> None:
self.__update_indices(row_idx, column_idx)
try:
self.__check_indices(row_idx, column_idx)
except IndexError:
warn("Index Error. Skipping insertion into SparseMatrix", stacklevel=1)
return
if value:
self.values[(row_idx, column_idx)] = value
def get(self, row_idx: int, column_idx: int) -> str:
self.__check_indices(row_idx, column_idx)
return self.values[(row_idx, column_idx)]

66
hotpdf/trie.py Normal file
View File

@ -0,0 +1,66 @@
from collections import defaultdict
from .data.classes import HotCharacter, PageResult
class TrieNode:
def __init__(self) -> None:
"""Initialize a TrieNode.
Args:
children (dict): Mapping of characters to TrieNode objects.
is_end_of_word (bool): Flag indicating the end of a word.
hot_characters (list[HotCharacter]): List of HotCharacter instances associated with the word.
"""
self.children: defaultdict[str, TrieNode] = defaultdict(TrieNode)
self.is_end_of_word: bool = False
self.hot_characters: list[HotCharacter] = []
class Trie:
def __init__(self) -> None:
"""Initialize a Trie."""
self.root = TrieNode()
def insert(self, word: str, hot_character: HotCharacter) -> None:
"""Insert a word and a HotCharacter into the Trie.
Args:
word (str): The word to insert.
hot_character (HotCharacter): The HotCharacter to insert.
"""
node: TrieNode = self.root
for char in word:
node = node.children[char]
node.is_end_of_word = True
node.hot_characters = node.hot_characters + [hot_character]
def search_all(self, text: str, case_sensitive: bool = True) -> tuple[list[str], PageResult]:
"""Search for words in the Trie that match a given text.
Args:
text (str): The text to search for.
case_sensitive (bool): Whether the search should be case-sensitive. Defaults to True.
Returns:
tuple: A tuple containing a list of the target characters found and corresponding PageResult.
"""
node: TrieNode = self.root
found: list[str] = []
hot_characters: PageResult = []
for char in text:
if case_sensitive:
if char in node.children and node.children[char].is_end_of_word:
found.append(char)
if node.children[char].hot_characters:
hot_characters.append(node.children[char].hot_characters)
else:
combined_hot_chars = []
for candidate in {char, char.lower(), char.upper()}:
if candidate in node.children and node.children[candidate].is_end_of_word:
combined_hot_chars.extend(node.children[candidate].hot_characters)
if combined_hot_chars:
found.append(char)
hot_characters.append(combined_hot_chars)
node = self.root
return found, hot_characters

119
hotpdf/utils.py Normal file
View File

@ -0,0 +1,119 @@
from typing import Union
from .data.classes import ElementDimension, HotCharacter, PageResult
def find_neighbour_coord(
reference_character: HotCharacter,
hot_characters: list[HotCharacter],
max_distance: int = 5,
span_tolerance: int = 5,
) -> Union[HotCharacter, None]:
"""Find a neighbouring coordinate within a specified maximum distance.
Args:
reference_character (HotCharacter): Previous char instance.
hot_characters (list[HotCharacter]): List of character instances to search through.
max_distance (int): Maximum distance between reference coordinate and target coordinate.
span_tolerance (int): Additional distance to consider if text lies in the same span.
Returns:
HotCharacter: The neighbouring HotCharacter if found, else None.
"""
for hot_character in hot_characters:
if hot_character == reference_character:
continue
if (
(
0
<= (hot_character.x - reference_character.x_end)
<= (
(max_distance + span_tolerance)
if hot_character.span_id == reference_character.span_id
and (hot_character.span_id and reference_character.span_id)
else max_distance
)
)
or (reference_character.span_id == hot_character.span_id and reference_character.x < hot_character.x)
and (hot_character.x <= reference_character.x_end)
) and reference_character.y == hot_character.y:
return hot_character
return None
def filter_adjacent_coords(text: list[str], page_hot_character_occurences: PageResult) -> PageResult:
"""Filter adjacent coordinates based on the given text.
Args:
text (str): The text to filter by.
page_hot_character_occurences (list): List of coordinates to filter by page
Returns:
PageResult: List of adjacent groups of HotCharacters on a page.
"""
if not page_hot_character_occurences:
return []
max_len = len(text)
adjacent_groups = []
anchor_hot_character_instances = page_hot_character_occurences[0]
for anchor_hot_character in anchor_hot_character_instances:
neighbours = [anchor_hot_character]
reference_hot_character = anchor_hot_character
for coords_j in page_hot_character_occurences[1:]:
neighbour_hot_character = find_neighbour_coord(
reference_character=reference_hot_character,
hot_characters=coords_j,
)
if neighbour_hot_character:
neighbours.append(neighbour_hot_character)
reference_hot_character = neighbour_hot_character
if len(neighbours) == max_len:
adjacent_groups.append(neighbours[:])
neighbours.clear()
neighbours = []
return adjacent_groups
def get_element_dimension(elem: list[HotCharacter]) -> ElementDimension:
"""Get the dimensions of an element based on its coordinates.
Args:
elem (list): List of coordinates representing an element.
Returns:
ElementDimension: ElementDimension object containing the dimensions (x0, x1, y0, y1, span_id).
"""
x0 = min(elem, key=lambda item: item.x).x
x1 = max(elem, key=lambda item: item.x_end).x_end
y0 = min(elem, key=lambda item: item.y).y
y1 = max(elem, key=lambda item: item.y).y
span = elem[0].span_id
return ElementDimension(x0=x0, x1=x1, y0=y0, y1=y1, span_id=span)
def intersect(bbox1: ElementDimension, bbox2: ElementDimension) -> bool:
"""Check if two bounding boxes intersect.
Args:
bbox1 (ElementDimension): Bounding box 1. (x0, y0, x1, y1)
bbox2 (ElementDimension): Bounding box 2. (x0, y0, x1, y1)
Returns:
bool: True if the bounding boxes intersect, else False.
"""
return not (bbox2.x0 > bbox1.x1 or bbox2.x1 < bbox1.x0 or bbox2.y0 > bbox1.y1 or bbox2.y1 < bbox1.y0)
def to_text(el: list[HotCharacter]) -> str:
"""Convert a list of HotCharacter instances to text.
Args:
el (list): List of HotCharacter instances.
Returns:
str: The text.
"""
return "".join(char.value for char in el)

100
pyproject.toml Normal file
View File

@ -0,0 +1,100 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta:__legacy__"
[project]
name="hotpdf"
version="0.5.3"
authors = [
{name = "Krishnasis Mandal", email = "krishnasis@hotmail.com"}]
maintainers = [
{name = "Alex Ptakhin" , email = "alex.ptakhin@prestatech.com"},
{name = "Izel Odabasi" , email = "izel.odabasi@prestatech.com"},
{name = "Mattia Callegari" , email = "callegari.mattia@protonmail.com"}]
description = "Fast PDF Data Extraction library"
readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.9"
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.9",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Text Processing :: General",
"Topic :: Text Processing :: Indexing",
"Topic :: Text Processing :: Linguistic",
"Topic :: Utilities",
"Operating System :: OS Independent",
]
keywords = [
"pdf",
"data extraction",
"text extraction",
"hotpdf",
"pdfminer",
"pdfquery"
]
dependencies = [
"pdfminer.six>=20231228",
]
[tool.setuptools]
include-package-data = false
[tool.setuptools.packages.find]
include = ["hotpdf*"]
exclude = ["docs*", "tests*"]
[project.optional-dependencies]
dev = ["pytest", "pytest-cov", "pytest-xdist", "pylint", "mypy", "typing-extensions", "pre-commit", "ruff"]
docs = ["pdfminer.six"]
[tool.ruff]
indent-width = 4
line-length = 120
select = [
# pycodestyle
"E", "W",
# Pyflakes
"F",
# pyupgrade
"UP",
# flake8-bugbear
"B",
# flake8-simplify
"SIM",
# isort
"I",
# MaccaBe
"C901",
# Pep8-naming
#"N", # TODO: enable
]
fix = true
fixable = ["ALL"]
exclude = ["docs"]
preview = true
[tool.ruff.mccabe]
max-complexity = 10
[tool.mypy]
warn_unused_configs = true
files = "hotpdf"
ignore_missing_imports = true
check_untyped_defs = true
explicit_package_bases = true
warn_unreachable = true
warn_redundant_casts = true
strict = true
[tool.pytest.ini_options]
log_cli=true
log_level="NOTSET"

0
tests/__init__.py Normal file
View File

57
tests/conftest.py Normal file
View File

@ -0,0 +1,57 @@
import pytest
@pytest.fixture()
def valid_file_name():
return "tests/resources/PDF.pdf"
@pytest.fixture()
def blank_file_name():
return "tests/resources/blank.pdf"
@pytest.fixture()
def multiple_pages_file_name():
return "tests/resources/20pages.pdf"
@pytest.fixture()
def non_existent_file_name():
return "non_existent_file.pdf"
@pytest.fixture()
def locked_file_name():
return "tests/resources/PDF_locked.pdf"
@pytest.fixture()
def invalid_file_name():
return "tests/resources/invalid_file.txt"
@pytest.fixture()
def bible_file_name():
return "tests/resources/bible.pdf"
@pytest.fixture()
def mock_hotpdf_bank_file_name():
return "tests/resources/hotpdf_bank.pdf"
@pytest.fixture()
def duplicate_span_file_name():
return "tests/resources/dup_span.pdf"
@pytest.fixture
def document_lt_figure_file_name():
# File sourced from: https://github.com/druskacik/ltfigure-pdfminer
return "tests/resources/doc_lt_figure.pdf"
@pytest.fixture()
def variations_simple_file_name():
return "tests/resources/test_variations_simple.pdf"

775
tests/resources/20pages.pdf Normal file
View File

@ -0,0 +1,775 @@
THE HOLY BIBLE
TRANSLATED FROM THE LATIN VULGATE
DILIGENTLY COMPARED WITH THE HEBREW, GREEK, AND
OTHER EDITIONS IN DIVERS LANGUAGES
DOUAY-RHEIMS VERSION
1609, 1582
2
Contents
I Old Testament 1
1 Book of Genesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2 Book of Exodus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
3 Book of Leviticus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
4 Book of Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
5 Book of Deuteronomy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
6 Book of Josue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
7 Book of Judges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
8 Book of Ruth . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
9 First Book of Samuel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
10 Second Book of Samuel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365
11 Third Book of Kings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
12 Fourth Book of Kings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 441
13 First Book of Paralipomenon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479
14 Second Book of Paralipomenon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 516
15 First Book of Esdras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 560
16 Book of Nehemias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 574
17 Book of Tobias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 593
18 Book of Judith . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 606
19 Book of Esther . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 623
20 Book of Job . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 639
21 Book of Psalms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 675
22 Book of Proverbs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 761
23 Ecclesiastes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 791
24 Book of Wisdom . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 806
25 Ecclesiasticus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 826
26 Prophecy of Isaias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 883
27 Prophecy of Jeremias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 947
28 Lamentations of Jeremias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1018
29 Prophecy of Baruch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1025
ii CONTENTS
30 Prophecy of Ezechiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1035
31 Prophecy of Daniel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1101
32 Prophecy of Osee . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1128
33 Prophecy of Joel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1138
34 Prophecy of Amos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1142
35 Prophecy of Abdias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1150
36 Prophecy of Jonas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1152
37 Prophecy of Micheas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1155
38 Prophecy of Nahum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1161
39 Prophecy of Habacuc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1164
40 Prophecy of Sophonias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1167
41 Prophecy of Aggeus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1170
42 Prophecy of Zacharias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1173
43 Prophecy of Malachias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1184
44 First Book of Machabees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1188
45 Second Book of Machabees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1229
II New Testament 1258
46 The Holy Gospel Of Jesus Christ, According to St. Matthew . . . . . . . . . . . . . 1260
47 The Holy Gospel of Jesus Christ, According to St. Mark . . . . . . . . . . . . . . . . 1302
48 The Holy Gospel of Jesus Christ, According to St. Luke . . . . . . . . . . . . . . . . 1329
49 The Holy Gospel of Jesus Christ, According to St. John . . . . . . . . . . . . . . . . 1374
50 The Acts of the Apostles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1408
51 The Epistle of St. Paul the Apostle to the Romans . . . . . . . . . . . . . . . . . . . 1451
52 The First Epistle of St. Paul to the Corinthians . . . . . . . . . . . . . . . . . . . . 1469
53 The Second Epistle of St. Paul to the Corinthians . . . . . . . . . . . . . . . . . . . 1486
54 The Epistle of St. Paul to the Galatians . . . . . . . . . . . . . . . . . . . . . . . . . 1498
55 The Epistle of St. Paul to the Ephesians . . . . . . . . . . . . . . . . . . . . . . . . . 1504
56 The Epistle of St. Paul to the Philippians . . . . . . . . . . . . . . . . . . . . . . . . 1510
57 The Epistle of St. Paul to the Colossians . . . . . . . . . . . . . . . . . . . . . . . . 1515
58 The First Epistle of St. Paul to the Thessalonians . . . . . . . . . . . . . . . . . . . 1519
59 The Second Epistle of St. Paul to the Thessalonians . . . . . . . . . . . . . . . . . . 1523
60 The First Epistle of St. Paul to Timothy . . . . . . . . . . . . . . . . . . . . . . . . . 1526
61 The Second Epistle of St. Paul to Timothy . . . . . . . . . . . . . . . . . . . . . . . 1531
62 The Epistle of St. Paul to Titus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1535
63 The Epistle of St. Paul to Philemon . . . . . . . . . . . . . . . . . . . . . . . . . . . 1538
64 The Epistle of St. Paul to the Hebrews . . . . . . . . . . . . . . . . . . . . . . . . . . 1540
65 The Catholic Epistle of St. James the Apostle . . . . . . . . . . . . . . . . . . . . . . 1553
66 The First Epistle of St. Peter the Apostle . . . . . . . . . . . . . . . . . . . . . . . . 1558
CONTENTS iii
67 The Second Epistle of St. Peter the Apostle . . . . . . . . . . . . . . . . . . . . . . . 1563
68 The First Epistle of St. John the Apostle . . . . . . . . . . . . . . . . . . . . . . . . 1567
69 The Second Epistle of St. John the Apostle . . . . . . . . . . . . . . . . . . . . . . . 1572
70 The Third Epistle of St. John the Apostle . . . . . . . . . . . . . . . . . . . . . . . . 1573
71 The Catholic Epistle of St. Jude the Apostle . . . . . . . . . . . . . . . . . . . . . . 1574
72 The Apocalypse of St. John the Apostle . . . . . . . . . . . . . . . . . . . . . . . . . 1576
iv CONTENTS
Part I
Old Testament
Book of Genesis
Chapter 1 12 And the earth brought forth the green
herb, and such as yieldeth seed according to its
In the beginning God created heaven, and earth. kind, and the tree that beareth fruit, having seed
2 And the earth was void and empty, and each one according to its kind. And God saw
that it was good.
darkness was upon the face of the deep; and the
spirit of God moved over the waters. 13 And the evening and the morning were the
third day.
3 And God said: Be light made. And light
was made. 14 And God said: Let there be lights made
in the firmament of heaven, to divide the day
4 And God saw the light that it was good; and and the night, and let them be for signs, and for
he divided the light from the darkness. seasons, and for days and years:
5 And he called the light Day, and the dark- 15 To shine in the firmament of heaven, and
ness Night; and there was evening and morning to give light upon the earth, and it was so done.
one day.
16 And God made two great lights: a greater
6 And God said: Let there be a firmament light to rule the day; and a lesser light to rule
made amidst the waters: and let it divide the the night: and The stars.
waters from the waters.
17 And he set them in the firmament of heaven
7 And god made a firmament, and divided to shine upon the earth.
the waters that were under the firmament, from
those that were above the firmament, and it was 18 And to rule the day and the night, and to
so. divide the light and the darkness. And God saw
that it was good.
8 And God called the firmament, Heaven; and
the evening and morning were the second day. 19 And the evening and morning were the
fourth day.
9 God also said; Let the waters that are under
the heaven, be gathered together into one place: 20 God also said: let the waters bring forth
and let the dry land appear. And it was so done. the creeping creature having life, and the fowl
that may fly over the earth under the firmament
10 And God called the dry land, Earth; and of heaven.
the gathering together of the waters, he called
Seas. And God saw that it was good. 21 And God created the great whales, and
every living and moving creature, which the
11 And he said: let the earth bring forth green waaters brought forth, according to their kinds,
herb, and such as may seed, and the fruit tree and every winged fowl according to its kind. And
yielding fruit after its kind, which may have seed God saw that it was good.
in itself upon the earth. And it was so done.
4 Book of Genesis
22 And he blessed them, saying: Increase and Chapter 2
multiply, and fill the waters of the sea: and let
the birds be multiplied upon the earth. So the heavens and the earth were finished, and
all the furniture of them.
23 And the evening and morning were the fifth
day. 2 And on the seventh day God ended his work
which he had made: and he rested on the seventh
24 And God said: Let the earth bring forth day from all his work which he had done.
the living creature in its kind, cattle and creeping
things, and beasts of the earth, according to their 3 And he blessed the seventh day, and sancti-
kinds. And it was so done. fied it: because in it he had rested from all his
work which God created and made.
25 And God made the beasts of the earth
according to their kinds, and cattle, and every 4 These are the generations of the heaven and
thing that creepeth on the earth after its kind. the earth, when they were created, in the day
And God saw that it was good. that the Lord God made the heaven and the
earth:
26 And he said: Let us make man to our image
and likeness: and let him have dominion over the 5 And every plant of the field before it sprung
fishes of the sea, and the fowls of the air, and the up in the earth, and every herb of the ground
beasts, and the whole earth, and every creeping before it grew: for the Lord God had not rained
creature that moveth upon the earth. upon the earth; and there was not a man to till
the earth.
27 And God created man to his own image:
to the image of God he created him: male and 6 But a spring rose out of the earth, watering
female he created them. all the surface of the earth.
28 And God blessed them, saying: Increase 7 And the Lord God formed man of the slime
and multiply, and fill the earth, and subdue it, of the earth: and breathed into his face the
and rule over the fishes of the sea, and the fowls breath of life, and man became a living soul.
of the air, and all living creatures that move
upon the earth. 8 And the Lord God had planted a paradise of
pleasure from the beginning: wherein he placed
29 And God said: Behold I have given you man whom he had formed.
every herb bearing seed upon the earth, and all
trees that have in themselves seed of their own 9 And the Lord God brought forth of the
kind, to be your meat: ground all manner of trees, fair to behold, and
pleasant to eat of: the tree of life also in the
30 And to all beasts of the earth, and to every midst of paradise: and the tree of knowledge of
fowl of the air, and to all that move upon the good and evil.
earth, and wherein there is life, that they may
have to feed upon. And it was so done. 10 And a river went out of the place of plea-
sure to water paradise, which from thence is di-
31 And God saw all the things that he had vided into four heads.
made, and they were very good. And the evening
and morning were the sixth day. 11 The name of the one is Phison: that is it
which compasseth all the land of Hevilath, where
gold groweth.
12 And the gold of that land is very good:
there is found bdellium, and the onyx stone.
Book of Genesis 5
13 And the name of the second river is Gehon: Chapter 3
the same is it that compasseth all the land of
Ethiopia. Now the serpent was more subtle tha any of the
beasts of the earth which the Lord God had
14 And the name of the third river is Tigris: made. And he said to the woman: Why hath
the same passeth along by the Assyrians. And God commanded you, that you should not eat
the fourth river is Euphrates. of every tree of paradise?
15 And the Lord God took man, and put him 2 And the woman answered him, saying: Of
into the paradise of pleasure, to dress it, and to the fruit of the trees that are in paradise we do
keep it. eat:
16 And he commanded him, saying: Of every 3 But of the fruit of the tree which is in the
tree of paradise thou shalt eat: midst of paradise, God hath commanded us that
we should not eat; and that we should not touch
17 But of the tree of knowledge of good and it, lest perhaps we die.
evil, thou shalt not eat. For in what day soever
thou shalt eat of it, thou shalt die the death. 4 And the serpent said to the woman: No, you
shall not die the death.
18 And the Lord God said: It is not good for
man to be alone: let us make him a help like 5 For God doth know that in what day soever
unto himself. you shall eat thereof, your eyes shall be opened:
and you shall be as Gods, knowing good and evil.
19 And the Lord God having formed out of
the ground all the beasts of the earth, and all 6 And the woman saw that the tree was good
the fowls of the air, brought them to Adam to see to eat, and fair to the eyes, and delightful to
what he would call them: for whatsoever Adam behold: and she took of the fruit thereof, and
called any living creature the same is its name. did eat, and gave to her husband, who did eat.
20 And Adam called all the beasts by their 7 And the eyes of them both were opened:
names, and all the fowls of the air, and all the and when they perceived themselves to be naked,
cattle of the field: but for Adam there was not they sewed together fig leaves, and made them-
found a helper like himself. selves aprons.
21 Then the Lord God cast a deep sleep upon 8 And when they heard the voice of the Lord
Adam: and when he was fast asleep, he took one God walking in paradise at the afternoon air,
of his ribs, and filled up flesh for it. Adam and his wife hid themselves from the face
of the Lord God, amidst the trees of paradise.
22 And the Lord God built the rib which he
took from Adam into a woman: and brought her 9 And the Lord God called Adam, and said to
to Adam. him: Where art thou?
23 And Adam said: This now is bone of my 10 And he said: I heard thy voice in paradise;
bones, and flesh of my flesh; she shall be called and I was afraid, because I was naked, and I hid
woman, because she was taken out of man. myself.
24 Wherefore a man shall leave father and 11 And he said to him: And who hath told
mother, and shall cleave to his wife: and they thee that thou wast naked, but that thou hast
shall be two in one flesh. eaten of the tree whereof I commanded thee that
thou shouldst not eat?
25 And they were both naked: to wit, Adam
and his wife: and were not ashamed.
6 Book of Genesis
12 And Adam said: The woman, whom thou 23 And the Lord God sent him out of the par-
gavest me to be my companion, gave me of the adise of pleasure, to till the earth from which he
tree, and I did eat. was taken.
13 And the Lord God said to the woman: Why 24 And he cast out Adam: and placed before
hast thou done this? And she answered: The the paradise of pleasure Cherubims, and a flam-
serpent deceived me, and I did eat. ing sword, turning every way, to keep the way of
the tree of life.
14 And the Lord God said to the serpent: Be-
cause thou hast done this thing, thou art cursed Chapter 4
among all cattle, and beasts of the earth: upon
thy breast shalt thou go, and earth shalt thou And Adam knew Eve his wife; who conceived
eat all the days of thy life. and brought forth Cain, saying: I have gotten a
man through God.
15 I will put enmities between thee and the
woman, and thy seed and her seed: she shall 2 And again she brought forth his brother
cursh thy head, and thou shalt lie in wait for her Abel. And Abel was a shepherd, and Cain a
heel. husbandman.
16 To the woman also he said: I will multi- 3 And it came to pass after many days, that
ply thy sorrows, and thy conceptions: in sorrow Cain offered, of the fruits of the earth, gifts to
shalt thou bring forth children, and thou shalt the Lord.
be under thy husbands power, and he shall have
dominion over thee. 4 Abel also offered of the firstlings of his flock,
and of their fat: and the Lord had respect to
17 And to Adam he said: Because thou hast Abel, and to his offerings.
hearkened to the voice of thy wife, and hast eaten
of the tree, whereof I commanded thee, that thou 5 But to Cain and his offerings he had no re-
shouldst not eat, cursed is the earth in thy work: spect: and Cain was exceeding angry, and his
with labour and toil shalt thou eat thereof all the countenance fell.
days of thy life.
6 And the Lord said to him: Why art thou
18 Thorns and thistles shall it bring forth to angry? and why is thy countenance fallen?
thee, and thou shalt eat the herbs of the earth.
7 If thou do well, shalt thou not receive? but
19 In the sweat of thy face shalt thou eat bread if ill, shall not sin forthwith be present at the
till thou return to the earth out of which thou door? but the lust thereof shall be under thee,
wast taken: for dust thou art, and into dust thou and thou shalt have dominion over it.
shalt return.
8 And Cain said to Abel his brother: Let us go
20 And Adam called the name of his wife Eve: forth abroad. And when they were in the field,
because she was the mother of all the living. Cain rose up against his brother Abel, and slew
him.
21 And the Lord God made for Adam and his
wife garments of skins, and clothed them. 9 And the Lord said to Cain: Where is thy
brother Abel? And he answered: I know not:
22 And he said: Behold Adam is become as am I my brothers keeper?
one of us, knowing good and evil: now therefore
lest perhaps he put forth his hand and take also 10 And he said to him: What hast thou done?
of the tree of life, and eat, and live for ever.
Book of Genesis 7
the voice of thy brothers blood crieth to me from brass and iron. And the sister of Tubalcain was
the earth. Noema.
11 Now therefore cursed shalt thou be upon 23 And Lamech said to his wives Ada and
the earth, which hath opened her mouth and re- Sella: Hear my voice, ye wives of Lamech, hear-
cieved the blood of thy brother at thy hand. ken to my speech: for I have slain a man to the
wounding of myself, and a stripling to my own
12 When thou shalt till it, it shall not yield bruising.
to thee its fruit: a fugitive and a vagabond shalt
thou be upon the earth. 24 Sevenfold vengeance shall be taken for
Cain: but for Lamech seventy times sevenfold.
13 And Cain said to the Lord: My iniquity is
greater than that I may deserve pardon. 25 Adam also knew his wife again: and she
brought forth a son, and called his name Seth,
14 Behold thou dost cast me out this day from saying: God hath given me another seed for
the face of the earth, and from thy face I shall be Abel, whom Cain slew.
hid, and I shall be a vagabond and a fugitive on
the earth: every one therefore that findeth me, 26 But to Seth also was born a son, whom
shall kill me. he called Enos: this man began to call upon the
name of the Lord.
15 And the Lord said to him: No, it shall
not so be: but whosoever shall kill Cain, shall Chapter 5
be punished sevenfold. And the Lord set a mark
upon Cain, that whosoever found him should not This is the book of the generation of Adam. In
kill him. the day that God created man, he made him to
the likeness of God.
16 And Cain went out from the face of the
Lord, and dwelt as a fugitive on the earth at the 2 He created them male and female; and
east side of Eden. blessed them: and called their name Adam, in
the day when they were created.
17 And Cain knew his wife, and she conceived,
and brought forth Henoch: and he built a city, 3 And Adam lived a hundred and thirty years,
and called the name thereof by the name of his and begot a son to his own image and likeness,
son Henoch. and called his name Seth.
18 And Henoch begot Irad, and Irad be- 4 And the days of Adam, after he begot Seth,
got Maviael, and Maviael begot Mathusael, and were eight hundred years: and he begot sons and
Mathusael begot Lamech, daughters.
19 Who took two wives: the name of the one 5 And all the time that Adam lived, came to
was Ada, and the name of the other Sella. nine hundred and thirty years, and he died.
20 And Ada brought forth Jabel: who was the 6 Seth also lived a hundred and five years, and
father of such as dwell in tents, and of herdsmen. begot Enos.
21 And his brothers name was Jubal: he was 7 And Seth lived after he begot Enos, eight
the father of them that play upon the harp and hundred and seven years, and begot sons and
the organs. daughters.
22 Sella also brought forth Tubalcain, who
was a hammerer and artificer in every work of
8 Book of Genesis
8 And all the days of Seth were nine hundred 26 And Mathlusala lived after he begot
and twelve years, and he died. Lamech, seven hundred and eighty-two years,
and begot sons and daughters.
9 And Enos lived ninety years, and begot
Cainan. 27 And all the days of Mathusala were nine
hundred and sixty-nine years, and he died.
10 After whose birth he lived eight hundred
and fifteen years, and begot sons and daughters. 28 And Lamech lived a hundred and eighty-
two years, and begot a son.
11 And all the days of Enos were nine hundred
and five years, and he died. 29 And he called his name Noe, saying:
This same shall comfort us from the works and
12 And Cainan lived seventy years, and begot labours of our hands on the earth, which the
Malaleel. Lord hath cursed.
13 And Cainan lived after he begot Malaleel, 30 And Lamech lived after he begot Noe, five
eight hundred and forty years, and begot sons hundred and ninety-five years, and begot sons
and daughters. and daughters.
14 And all the days of Cainan were nine hun- 31 And all the days of Lamech came to seven
dred and ten years, and he died. hundred and seventy-seven years, and he died.
And Noe, when he was five hundred years old,
15 And Malaleel lived sixty-five years and be- begot Sem, Cham, and Japheth.
got Jared.
Chapter 6
16 And Malaleel lived after he begot Jared,
eight hundred and thirty years, and begot sons And after that men began to be multiplied upon
and daughters. the earth, and daughters were born to them,
17 And all the days of Malaleel were eight 2 The sons of God seeing the daughters of
hundred and ninety-five years, and he died. men, that they were fair, took to themselves
wives of all which they chose.
18 And Jared lived a hundred and sixty-two
years, and begot Henoch. 3 And God said: My spirit shall not remain
in man for ever, because he is flesh, and his days
19 And Jared lived after he begot Henoch, shall be a hundred and twenty years.
eight hundred years, and begot sons and daugh-
ters. 4 Now giants were upon the earth in those
days. For after the sons of God went in to the
20 And all the days of Jared were nine hun- daughters of men, and they brought forth chil-
dred and sixty-two years, and he died. dren, these are the mighty men of old, men of
renown.
21 And Henoch lived sixty-five years, and be-
got Mathusala. 5 And God seeing that the wickedness of men
was great on the earth, and that all the thought
22 And Henoch walked with God: and lived of their heart was bent upon evil at all times,
after he begot Mathusala, three hundred years,
and begot sons and daughters. 6 It repented him that he had made man on
the earth. And being touched inwardly with sor-
23 And all the days of Henoch were three hun-
dred and sixty-five years.
24 And he walked with God, and was seen no
more: because God took him.
25 And Mathusala lived a hundred and eighty-
seven years, and begot Lamech.
Book of Genesis 9
row of heart, sons, and thy wife, and the wives of thy sons
7 He said: I will destroy man, whom I have with thee.
created, from the face of the earth, from man 19 And of every living creature of all flesh,
even to beasts, from the creeping thing even to thou shalt bring two of a sort into the ark, that
the fowls of the air, for it repenteth me that I they may live with thee: of the male sex, and
have made them. the female.
8 But Noe found grace before the Lord. 20 Of fowls according to their kind, and of
9 These are the generations of Noe: Noe was beasts in their kind, and of every thing that
a just and perfect man in his generations, he creepeth on the earth according to its kind: two
walked with God. of every sort shall go in with thee, that they may
10 And he begot three sons, Sem, Cham, and live.
Japheth.
11 And the earth was corrupted before God, 21 Thou shalt take unto thee of all food that
and was filled with iniquity. may be eaten, and thou shalt lay it up with thee:
12 And when God had seen that the earth and it shall be food for thee and them.
was corrupted (for all flesh had corrupted its way
upon the earth), 22 And Noe did all things which God com-
13 He said to Noe: The end of all flesh is manded him.
come before me, the earth is filled with iniquity
through them, and I will destroy them with the Chapter 7
earth.
14 Make thee an ark of timber planks: thou And the Lord said to him: Go in, thou and all
shalt make little rooms in the ark, and thou shalt thy house, into the ark: for thee I have seen just
pitch it within and without. before me in this generation.
15 And thus shalt thou make it. The length
of the ark shall be three hundred cubits: the 2 Of all clean beasts take seven and seven, the
breadth of it fifty cubits, and the height of it male and the female.
thirty cubits.
16 Thou shalt make a window in the ark, and 3 But of the beasts that are unclean two and
in a cubit shalt thou finish the top of it: and the two, the male and the female. Of the fowls also
door of the ark thou shalt set in the side: with of the air seven and seven, the male and the fe-
lower, middle chambers, and third stories shalt male: that seed may be saved upon the face of
thou make it. the whole earth.
17 Behold, I will bring the waters of a great
flood upon the earth, to destroy all flesh, wherein 4 For yet a while, and after seven days, I will
is the breath of life under heaven. All things that rain upon the earth forty days and forty nights:
are in the earth shall be consumed. and I will destroy every substance that I have
18 And I will establish my covenant with thee, made, from the face of the earth.
and thou shalt enter into the ark, thou and thy
5 And Noe did all things which the Lord had
commanded him.
6 And he was six hundred years old, when the
waters of the flood overflowed the earth.
7 And Noe went in and his sons, his wife and
the wives of his sons with him into the ark, be-
10 Book of Genesis
cause of the waters of the flood. 20 The water was fifteen cubits higher than
8 And of beasts clean and unclean, and of the mountains which it covered.
fowls, and of every thing that moveth upon the 21 And all flesh was destroyed that moved
earth, upon the earth, both of fowl and of cattle, and
of beasts, and of all creeping things that creep
9 Two and two went in to Noe into the ark, upon the earth: and all men.
male and female, as the Lord had commanded
Noe. 22 And all things wherein there is the breath
of life on the earth, died.
10 And after the seven days were passed, the
waters of the flood overflowed the earth. 23 And he destroyed all the substance that
was upon the earth, from man even to beast,
11 In the six hundredth year of the life of Noe, and the creeping things and fowls of the air: and
in the second month, in the seventeenth day of they were destroyed from the earth: and Noe
the month, all the fountains of the great deep only remained, and they that were with him in
were broken up, and the floodgates of heaven the ark.
were opened:
24 And the waters prevailed upon the earth a
12 And the rain fell upon the earth forty days hundred and fifty days.
and forty nights.
Chapter 8
13 In the selfsame day Noe, and Sem, and
Cham, and Japheth, his sons: his wife, and the And God remembered Noe, and all the living
three wives of his sons with them, went into the creatures, and all the cattle which were with him
ark. in the ark, and brought a wind upon the earth,
and the waters were abated:
14 They and every beast according to its kind,
and all the cattle in their kind, and every thing 2 The fountains also of the deep, and the
that moveth upon the earth, according to its floodgates of heaven, were shut up, and the rain
kind, and every fowl according to its kind, all from heaven was restrained.
birds, and all that fly,
3 And the waters returned from off the earth
15 Went in to Noe into the ark, two and two going and coming: and they began to be abated
of all flesh, wherein was the breath of life. after a hundred and fifty days.
16 And they that went in, went in male and 4 And the ark rested in the seventh month,
female of all flesh, as God had commanded him: the seven and twentieth day of the month, upon
and the Lord shut him in on the outside. the mountains of Armenia.
17 And the flood was forty days upon the 5 And the waters were going and decreasing
earth: and the waters increased, and lifted up until the tenth month: for in the tenth month,
the ark on high from the earth. the first day of the month, the tops of the moun-
tains appeared.
18 For they overflowed exceedingly: and filled
all on the face of the earth: and the ark was 6 And after that forty days were passed, Noe
carried upon the waters. opening the window of the ark, which he had
made, sent forth a raven:
19 And the waters prevailed beyond measure
upon the earth: and all the high mountains un-
der the whole heaven were covered.
Book of Genesis 11
7 Which went forth and did not return, till the 20 And Noe built an altar unto the Lord: and
waters were dried up upon the earth. taking of all cattle and fowls that were clean,
offered holocausts upon the altar.
8 He sent forth also a dove after him, to see if
the waters had now ceased upon the face of the 21 And the Lord smelled a sweet savour, and
earth. said: I will no more curse the earth for the sake of
man: for the imagination and thought of mans
9 But she not finding where her foot might heart are prone to evil from his youth: therefore
rest, returned to him into the ark: for the waters I will no more destroy every living soul as I have
were upon the whole earth: and he put forth his done.
hand, and caught her, and brought her into the
ark. 22 All the days of the earth, seedtime and har-
vest, cold and heat, summer and winter, night
10 And having waited yet seven other days, and day, shall not cease.
he again sent forth the dove out of the ark.
Chapter 9
11 And she came to him in the evening carry-
ing a bough of an olive tree, with green leaves, in And God blessed Noe and his sons. And he
her mouth. Noe therefore understood that the said to them: Increase, and multiply, and fill the
waters were ceased upon the earth. earth.
12 And he stayed yet other seven days: and 2 And let the fear and dread of you be upon
he sent forth the dove, which returned not any all the beasts of the earth, and upon all the fowls
more unto him. of the air, and all that move upon the earth: all
the fishes of the sea are delivered into your hand.
13 Therefore in the six hundredth and first
year, the first month, the first day of the month, 3 And every thing that moveth, and liveth
the waters were lessened upon the earth, and shall be meat for you: even as the green herbs
Noe opening the covering of the ark, looked, and have I delivered them all to you:
saw that the face of the earth was dried.
4 Saving that flesh with blood you shall not
14 In the second month, the seven and twen- eat.
tieth day of the month, the earth was dried.
5 For I will require the blood of your lives at
15 And God spoke to Noe, saying: the hand of every beast, and at the hand of man,
16 Go out of the ark, thou and thy wife, thy at the hand of every man, and of his brother, will
sons and the wives of thy sons with thee. I require the life of man.
17 All living things that are with thee of all
flesh, as well in fowls as in beasts, and all creep- 6 Whosoever shall shed mans blood, his blood
ing things that creep upon the earth, bring out shall be shed: for man was made to the image of
with thee, and go ye upon the earth: increase God.
and multiply upon it.
18 So Noe went out, he and his sons: his wife, 7 But increase you and multiply, and go upon
and the wives of his sons with him. the earth and fill it.
19 And all living things, and cattle, and creep-
ing things that creep upon the earth, according 8 Thus also said God to Noe, and to his sons
to their kinds went out of the ark. with him:
12 Book of Genesis
9 Behold I will establish my covenant with 22 Which when Cham the father of Chanaan
you, and with your seed after you: had seen, to wit, that his fathers nakedness was
uncovered, he told it to his two brethren without.
10 And with every living soul that is with you,
as well in all birds, as in cattle and beasts of the 23 But Sem and Japheth put a cloak upon
earth, that are come forth out of the ark, and in their shoulders, and going backward, covered the
all the beasts of the earth. nakedness of their father: and their faces were
turned away, and they saw not their fathers
11 I will establish my covenant with you, and nakedness.
all flesh shall be no more destroyed with the wa-
ters of a flood, neither shall there be from hence- 24 And Noe awaking from the wine, when he
forth a flood to waste the earth. had learned what his younger son had done to
him,
12 And God said: This is the sign of the
covenant which I give between me and you, and 25 He said: Cursed be Chanaan, a servant of
to every living soul that is with you, for perpet- servants shall he be unto his brethren.
ual generations.
26 And he said: Blessed be the Lord God of
13 I will set my bow in the clouds, and it shall Sem, be Chanaan his servant.
be the sign of a covenant between me and be-
tween the earth. 27 May God enlarge Japheth, and may he
dwell in the tents of Sem, and Chanaan be his
14 And when I shall cover the sky with clouds, servant.
my bow shall appear in the clouds:
28 And Noe lived after the flood three hun-
15 And I will remember my covenant with dred and fifty years.
you, and with every living soul that beareth
flesh: and there shall no more be waters of a 29 And all his days were in the whole nine
flood to destroy all flesh. hundred and fifty years: and he died.
16 And the bow shall be in the clouds, and I Chapter 10
shall see it, and shall remember the everlasting
covenant, that was made between God and every These are the generations of the sons of Noe:
living soul of all flesh which is upon the earth. Sem, Cham, and Japheth: and unto them sons
were born after the flood.
17 And God said to Noe: This shall be the
sign of the covenant, which I have established, 2 The sons of Japheth: Gomer, and Magog,
between me and all flesh upon the earth. and Madai, and Javan, and Thubal, and Mosoch,
and Thiras.
18 And the sons of Noe, who came out of the
ark, were Sem, Cham, and Japheth: and Cham 3 And the sons of Gomer: Ascenez and
is the father of Chanaan. Riphath and Thogorma.
19 These three are the sons of Noe: and from 4 And the sons of Javan: Elisa and Tharsis,
these was all mankind spread over the whole Cetthim and Dodanim.
earth.
5 By these were divided the islands of the Gen-
20 And Noe a husbandman began to till the tiles in their lands, every one according to his
ground, and planted a vineyard. tongue and their families in their nations.
21 And drinking of the wine was made drunk,
and was uncovered in his tent.
Book of Genesis 13
6 And the Sons of Cham: Chus, and Mesram, 22 The sons of Sem: Elam and Assur, and
and Phuth, and Chanaan. Arphaxad, and Lud, and Aram.
7 And the sons of Chus: Saba, and Hevila, 23 The sons of Aram: Us, and Hull, and
and Sabatha, and Regma, and Sabatacha. The Gether; and Mes.
sons of Regma: Saba, and Dadan.
24 But Arphaxad begot Sale, of whom was
8 Now Chus begot Nemrod: he began to be born Heber.
mighty on the earth.
25 And to Heber were born two sons: the
9 And he was a stout hunter before the Lord. name of the one was Phaleg, because in his days
Hence came a proverb: Even as Nemrod the was the earth divided: and his brothers name
stout hunter before the Lord. Jectan.
10 And the beginning of his kingdom was 26 Which Jectan begot Elmodad, and Saleph,
Babylon, and Arach, and Achad, and Chalanne and Asarmoth, Jare,
in the land of Sennaar.
27 And Aduram, and Uzal, and Decla,
11 Out of that land came forth Assur, and 28 And Ebal, and Abimael, Saba,
built Ninive, and the streets of the city, and 29 And Ophir, and Hevila, and Jobab. All
Chale. these were the sons of Jectan.
30 And their dwelling was from Messa as we
12 Resen also between Ninive and Chale: this go on as far as Sephar, a mountain in the east.
is the great city. 31 These are the children of Sem according
to their kindreds and tongues, and countries in
13 And Mesraim begot Ludim, and Anamim their nations.
and Laabim, Nephthuim. 32 These are the families of Noe, according
to their people and nations. By these were the
14 And Phetrusim, and Chasluim; of whom nations divided on the earth after the flood.
came forth the Philistines, and the Capthorim.
Chapter 11
15 And Chanaan begot Sidon his firstborn,
the Hethite, And the earth was of one tongue, and of the same
speech.
16 And the Jebusite, and the Amorrhite, and
the Gergesite. 2 And when they removed from the east, they
found a plain in the land of Sennaar, and dwelt
17 The Hevite and Aracite: the Sinite, in it.
18 And the Aradian, the Samarite, and the
Hamathite: and afterwards the families of the 3 And each one said to his neighbour: Come
Chanaanites were spread abroad. let us make brick, and bake them with fire. And
19 And the limits of Chanaan were from Sidon they had brick instead of stones, and slime in-
as one comes to Gerara even to Gaza, until thou stead of mortar:
enter Sodom and Gomorrha, and Adama, and
Seboim even to Lesa. 4 And they said: Come, let us make a city and
20 These are the children of Cham in their kin- a tower, the top whereof may reach to heaven;
dreds and tongues, and generations, and lands,
and nations.
21 Of Sem also the father of all the children of
Heber, the elder brother of Japheth, sons were
born.
14 Book of Genesis
and let us make our name famous before we be daughters.
scattered abroad into all lands. 18 Phaleg also lived thirty years, and begot
5 And the Lord came down to see the city Reu.
and the tower, which the children of Adam were 19 And Phaleg lived after he begot Reu, two
building.
hundred and nine years, and begot sons and
6 And he said: Behold, it is one people, and all daughters.
have one tongue: and they have begun to do this,
neither will they leave off from their designs, till 20 And Reu lived thirty-two years, and begot
they accomplish them in deed. Sarug.
7 Come ye, therefore, let us go down, and 21 And Reu lived after he begot Sarug, two
there confound their tongue, that they may not hundred and seven years, and begot sons and
understand one anothers speech. daughters.
8 And so the Lord scattered them from that 22 And Sarug lived thirty years, and begot
place into all lands, and they ceased to build the Nachor.
city.
23 And Sarug lived after he begot Nachor, two
9 And therefore the name thereof was called hundred years, and begot sons and daughters.
Babel, because there the language of the whole
earth was confounded: and from thence the Lord 24 And Nachor lived nine and twenty years,
scattered them abroad upon the face of all coun- and begot Thare.
tries.
25 And Nachor lived after he begot Thare, a
10 These are the generations of Sem: Sem was hundred and nineteen years, and begot sons and
a hundred years old when he begot Arphaxad, daughters.
two years after the flood.
26 And Thare lived seventy years, and begot
11 And Sem lived after he begot Arphaxad, Abram, and Nachor, and Aran.
five hundred years, and begot sons and daugh-
ters. 27 And these are the generations of Thare:
Thare begot Abram, Nachor, and Aran. And
12 And Arphaxad lived thirty-five years, and Aran begot Lot.
begot Sale.
28 And Aran died before Thare his father, in
13 And Arphaxad lived after he begot Sale, the land of his nativity in Ur of the Chaldees.
three hundred and three years, and begot sons
and daughters. 29 And Abram and Nachor married wives: the
name of Abrams wife was Sarai: and the name
14 Sale also lived thirty years, and begot of Nachors wife, Melcha, the daughter of Aran,
Heber. father of Melcha and father of Jescha.
15 And Sale lived after he begot Heber, four 30 And Sarai was barren, and had no children.
hundred and three years: and begot sons and 31 And Thare took Abram his son, and Lot
daughters. the son of Aran, his sons son, and Sarai his
daughter in law, the wife of Abram his son, and
16 And Heber lived thirty-four years, and be- brought them out of Ur of the Chaldees, to go
got Phaleg. into the land of Chanaan: and they came as far
as Haran, and dwelt there.
17 And Heber lived after he begot Phaleg, four 32 And the days of Thare were two hundred
hundred and thirty years: and begot sons and and five years, and he died in Haran.

27
tests/resources/PDF.pdf Normal file
View File

@ -0,0 +1,27 @@
PDF HOTPDF
THE BEST PDF PARSING LIBRARY TO EVER EXIST(DEBATABLE)
NAME EXPERIENCE
Hotpdf PARSER • PRESTATECH • 05/12/2023-PRESENT
Trying to parse pdfs without failing. <<<<!@#!@****
SKILLS
EMOJIS
I am good at parsing pdfs…….. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
Dots: ….. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
হ ােলা
DEGREE • DATE EARNED • SCHOOL
Testing some languages
VOLUNTEER EXPERIENCE OR LEADERSHIP
Net Payment: 5000,23
EMAIL TWITTER HANDLE TELEPHONE LINKEDIN URL

File diff suppressed because it is too large Load Diff