同步完整源码 - 2026-05-24
This commit is contained in:
commit
755d34b16a
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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-*
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
# hotpdf
|
||||
|
||||
[](https://hotpdf.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://github.com/weareprestatech/hotpdf/actions/workflows/python-publish.yml)
|
||||
[](https://github.com/weareprestatech/hotpdf/actions/workflows/build-badge.yml)
|
||||
[](https://coveralls.io/github/weareprestatech/hotpdf?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/)
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1 @@
|
|||
* text=auto
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
hotpdf.processor
|
||||
================
|
||||
|
||||
.. automodule:: hotpdf.processor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. rubric:: Functions
|
||||
|
||||
.. autosummary::
|
||||
|
||||
process
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
hotpdf.trie.Trie
|
||||
================
|
||||
|
||||
.. currentmodule:: hotpdf.trie
|
||||
|
||||
.. autoclass:: Trie
|
||||
|
||||
|
||||
.. automethod:: __init__
|
||||
|
||||
|
||||
.. rubric:: Methods
|
||||
|
||||
.. autosummary::
|
||||
|
||||
~Trie.__init__
|
||||
~Trie.insert
|
||||
~Trie.search_all
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
hotpdf.trie.TrieNode
|
||||
====================
|
||||
|
||||
.. currentmodule:: hotpdf.trie
|
||||
|
||||
.. autoclass:: TrieNode
|
||||
|
||||
|
||||
.. automethod:: __init__
|
||||
|
||||
|
||||
.. rubric:: Methods
|
||||
|
||||
.. autosummary::
|
||||
|
||||
~TrieNode.__init__
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
hotpdf.utils
|
||||
============
|
||||
|
||||
.. automodule:: hotpdf.utils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. rubric:: Functions
|
||||
|
||||
.. autosummary::
|
||||
|
||||
filter_adjacent_coords
|
||||
find_neighbour_coord
|
||||
get_element_dimension
|
||||
intersect
|
||||
to_text
|
||||
|
|
@ -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.
|
||||
|
|
@ -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/>`_
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from hotpdf.hotpdf import HotPdf
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__all__ = ["HotPdf"]
|
||||
|
|
@ -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]
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
class HotPdfIsNoneError(Exception):
|
||||
pass
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,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"
|
||||
|
|
@ -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 husband’s 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 brother’s 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 brother’s 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 brother’s 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 man’s
|
||||
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 man’s 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 father’s 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 father’s
|
||||
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 brother’s 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 another’s 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 Abram’s wife was Sarai: and the name
|
||||
14 Sale also lived thirty years, and begot of Nachor’s 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 son’s 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.
|
||||
|
||||
|
|
@ -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
Loading…
Reference in New Issue