From fe68328ef2dc957417e5899b110ce64ef4cb2588 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 8 Oct 2018 01:10:48 +0530 Subject: [PATCH 01/89] Move opencv-python to extra_requires (#134) --- README.md | 4 ++-- camelot/__version__.py | 2 +- docs/user/install.rst | 8 ++++---- requirements-dev.txt | 5 ----- requirements.txt | 8 -------- setup.py | 39 +++++++++++++++++++++++++++------------ 6 files changed, 34 insertions(+), 32 deletions(-) delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt diff --git a/README.md b/README.md index 8e86772..184d979 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ See [comparison with other PDF table extraction libraries and tools](https://git After [installing the dependencies](https://camelot-py.readthedocs.io/en/latest/user/install.html) ([tk](https://packages.ubuntu.com/trusty/python-tk) and [ghostscript](https://www.ghostscript.com/)), you can simply use pip to install Camelot:
-$ pip install camelot-py
+$ pip install camelot-py[all]
 
### Alternatively @@ -74,7 +74,7 @@ and install Camelot using pip:
 $ cd camelot
-$ pip install .
+$ pip install ".[all]"
 
**Note:** Use a [virtualenv](https://virtualenv.pypa.io/en/stable/) if you don't want to affect your global Python installation. diff --git a/camelot/__version__.py b/camelot/__version__.py index 855e8c8..a6f488e 100644 --- a/camelot/__version__.py +++ b/camelot/__version__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -VERSION = (0, 2, 1) +VERSION = (0, 2, 2) __title__ = 'camelot-py' __description__ = 'PDF Table Extraction for Humans.' diff --git a/docs/user/install.rst b/docs/user/install.rst index 99c73cf..0d8318b 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -76,12 +76,12 @@ Or for Windows 32-bit:: If you have ghostscript, you should see the ghostscript version and copyright information. -$ pip install camelot-py ------------------------- +$ pip install camelot-py[all] +----------------------------- After installing the dependencies, you can simply use pip to install Camelot:: - $ pip install camelot-py + $ pip install camelot-py[all] Get the source code ------------------- @@ -97,4 +97,4 @@ Alternatively, you can install from the source by: :: $ cd camelot - $ pip install . \ No newline at end of file + $ pip install ".[all]" \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 57c1892..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,5 +0,0 @@ -codecov==2.0.15 -pytest==3.8.0 -pytest-cov==2.6.0 -pytest-runner==4.2 -Sphinx==1.7.9 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 20bd683..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -click==6.7 -matplotlib==2.2.3 -numpy==1.15.2 -opencv-python==3.4.2.17 -openpyxl==2.5.8 -pandas==0.23.4 -pdfminer.six==20170720 -PyPDF2==1.26.0 diff --git a/setup.py b/setup.py index 02a8199..23edc7d 100644 --- a/setup.py +++ b/setup.py @@ -13,17 +13,31 @@ with open('README.md', 'r') as f: readme = f.read() +requires = [ + 'click==6.7', + 'matplotlib==2.2.3', + 'numpy==1.15.2', + 'openpyxl==2.5.8', + 'pandas==0.23.4', + 'pdfminer.six==20170720', + 'PyPDF2==1.26.0' +] + +all_requires = [ + 'opencv-python==3.4.2.17' +] + +dev_requires = [ + 'codecov==2.0.15', + 'pytest==3.8.0', + 'pytest-cov==2.6.0', + 'pytest-runner==4.2', + 'Sphinx==1.7.9' +] +dev_requires = dev_requires + all_requires + + def setup_package(): - reqs = [] - with open('requirements.txt', 'r') as f: - for line in f: - reqs.append(line.strip()) - - dev_reqs = [] - with open('requirements-dev.txt', 'r') as f: - for line in f: - dev_reqs.append(line.strip()) - metadata = dict(name=about['__title__'], version=about['__version__'], description=about['__description__'], @@ -34,9 +48,10 @@ def setup_package(): author_email=about['__author_email__'], license=about['__license__'], packages=find_packages(exclude=('tests',)), - install_requires=reqs, + install_requires=requires, extras_require={ - 'dev': dev_reqs + 'all': all_requires, + 'dev': dev_requires }, entry_points={ 'console_scripts': [ From 1a358f603acc37600474a5943d21b9c4a1e041ff Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 8 Oct 2018 01:12:16 +0530 Subject: [PATCH 02/89] Update MANIFEST.in Update HISTORY.md --- HISTORY.md | 9 ++++++++- MANIFEST.in | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 3967a58..a642196 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,13 @@ Release History =============== +0.2.2 (2018-10-08) +----------------- + +**Bugfixes** + +* Move opencv-python to extra\_requires. [#134](https://github.com/socialcopsdev/camelot/pull/134) by Vinayak Mehta. + 0.2.1 (2018-10-05) ------------------ @@ -51,4 +58,4 @@ Release History 0.1.0 (2018-09-24) ------------------ -* Birth! \ No newline at end of file +* Birth! diff --git a/MANIFEST.in b/MANIFEST.in index babd072..19d196d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include MANIFEST.in README.md HISTORY.md LICENSE requirements.txt requirements-dev.txt setup.py setup.cfg \ No newline at end of file +include MANIFEST.in README.md HISTORY.md LICENSE setup.py setup.cfg From 296be21d9dc1e8e2101d3ec9cc74abd2fdadbedd Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 8 Oct 2018 01:44:20 +0530 Subject: [PATCH 03/89] Update requirement versions --- setup.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/setup.py b/setup.py index 23edc7d..9c35289 100644 --- a/setup.py +++ b/setup.py @@ -14,25 +14,25 @@ with open('README.md', 'r') as f: requires = [ - 'click==6.7', - 'matplotlib==2.2.3', - 'numpy==1.15.2', - 'openpyxl==2.5.8', - 'pandas==0.23.4', - 'pdfminer.six==20170720', - 'PyPDF2==1.26.0' + 'click>=6.7', + 'matplotlib>=2.2.3', + 'numpy>=1.15.2', + 'openpyxl>=2.5.8', + 'pandas>=0.23.4', + 'pdfminer.six>=20170720', + 'PyPDF2>=1.26.0' ] all_requires = [ - 'opencv-python==3.4.2.17' + 'opencv-python>=3.4.2.17' ] dev_requires = [ - 'codecov==2.0.15', - 'pytest==3.8.0', - 'pytest-cov==2.6.0', - 'pytest-runner==4.2', - 'Sphinx==1.7.9' + 'codecov>=2.0.15', + 'pytest>=3.8.0', + 'pytest-cov>=2.6.0', + 'pytest-runner>=4.2', + 'Sphinx>=1.7.9' ] dev_requires = dev_requires + all_requires @@ -76,5 +76,5 @@ def setup_package(): setup(**metadata) -if __name__ == '__main__': +if __name__ >= '__main__': setup_package() From 45e7f7570e913c1f23cefef45720eaf09142e32f Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 8 Oct 2018 03:54:21 +0530 Subject: [PATCH 04/89] Bump version --- HISTORY.md | 7 ++++++- camelot/__version__.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index a642196..ff386d5 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,8 +1,13 @@ Release History =============== +0.2.3 (2018-10-08) +------------------ + +* Remove hard dependencies on requirements versions. + 0.2.2 (2018-10-08) ------------------ +------------------ **Bugfixes** diff --git a/camelot/__version__.py b/camelot/__version__.py index a6f488e..89f8c08 100644 --- a/camelot/__version__.py +++ b/camelot/__version__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -VERSION = (0, 2, 2) +VERSION = (0, 2, 3) __title__ = 'camelot-py' __description__ = 'PDF Table Extraction for Humans.' From 898646b73bc999cc68a850c1e29c595c4e650323 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 9 Oct 2018 20:22:07 +0530 Subject: [PATCH 05/89] Add conda installation instructions --- HISTORY.md | 6 ++++++ README.md | 18 +++++++++++++----- docs/user/install.rst | 39 +++++++++++++++++++++++++++------------ setup.py | 2 +- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index ff386d5..44f1b5f 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,12 @@ Release History =============== +master +------ + +* Update installation instructions for conda. +* Downgrade numpy version from 1.15.2 to 1.13.3. + 0.2.3 (2018-10-08) ------------------ diff --git a/README.md b/README.md index 184d979..38b1e94 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,25 @@ See [comparison with other PDF table extraction libraries and tools](https://git ## Installation -After [installing the dependencies](https://camelot-py.readthedocs.io/en/latest/user/install.html) ([tk](https://packages.ubuntu.com/trusty/python-tk) and [ghostscript](https://www.ghostscript.com/)), you can simply use pip to install Camelot: +### Using conda + +The easiest way to install Camelot is to install it with [conda](https://conda.io/docs/), which is the package manager that the [Anaconda](http://docs.continuum.io/anaconda/) distribution is built upon. + +
+$ conda install -c camelot-dev camelot-py
+
+ +### Using pip + +After [installing the dependencies](https://camelot-py.readthedocs.io/en/latest/user/install.html#using-pip) ([tk](https://packages.ubuntu.com/trusty/python-tk) and [ghostscript](https://www.ghostscript.com/)), you can simply use pip to install Camelot:
 $ pip install camelot-py[all]
 
-### Alternatively +### From the source code -After [installing the dependencies](https://camelot-py.readthedocs.io/en/latest/user/install.html), clone the repo using: +After [installing the dependencies](https://camelot-py.readthedocs.io/en/latest/user/install.html#using-pip), clone the repo using:
 $ git clone https://www.github.com/socialcopsdev/camelot
@@ -77,8 +87,6 @@ $ cd camelot
 $ pip install ".[all]"
 
-**Note:** Use a [virtualenv](https://virtualenv.pypa.io/en/stable/) if you don't want to affect your global Python installation. - ## Documentation Great documentation is available at [http://camelot-py.readthedocs.io/](http://camelot-py.readthedocs.io/). diff --git a/docs/user/install.rst b/docs/user/install.rst index 0d8318b..7bfd97c 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -3,14 +3,30 @@ Installation of Camelot ======================= -This part of the documentation covers how to install Camelot. First, you'll need to install the dependencies, which include `Tkinter`_ and `ghostscript`_. +This part of the documentation covers how to install Camelot. + +Using conda +----------- + +The easiest way to install Camelot is to install it with `conda`_, which is the package manager that the `Anaconda`_ distribution is built upon. +:: + + $ conda install -c camelot-dev camelot-py + +.. note:: Camelot is available for Python 2.7, 3.5 and 3.6 on Linux, macOS and Windows. For Windows, you will need to install ghostscript which you can get from their `downloads page`_. + +.. _conda: https://conda.io/docs/ +.. _Anaconda: http://docs.continuum.io/anaconda/ +.. _downloads page: https://www.ghostscript.com/download/gsdnld.html + +Using pip +--------- + +First, you'll need to install the dependencies, which include `Tkinter`_ and `ghostscript`_. .. _Tkinter: https://wiki.python.org/moin/TkInter .. _ghostscript: https://www.ghostscript.com -Install the dependencies ------------------------- - These can be installed using your system's package manager. You can run one of the following, based on your OS. For Ubuntu @@ -76,17 +92,14 @@ Or for Windows 32-bit:: If you have ghostscript, you should see the ghostscript version and copyright information. -$ pip install camelot-py[all] ------------------------------ - -After installing the dependencies, you can simply use pip to install Camelot:: +Finally, you can use pip to install Camelot:: $ pip install camelot-py[all] -Get the source code -------------------- +From the source code +-------------------- -Alternatively, you can install from the source by: +After `installing the dependencies`_, you can install from the source by: 1. Cloning the GitHub repository. :: @@ -97,4 +110,6 @@ Alternatively, you can install from the source by: :: $ cd camelot - $ pip install ".[all]" \ No newline at end of file + $ pip install ".[all]" + +.. _installing the dependencies: https://camelot-py.readthedocs.io/en/latest/user/install.html#using-pip \ No newline at end of file diff --git a/setup.py b/setup.py index 9c35289..4d0095f 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ with open('README.md', 'r') as f: requires = [ 'click>=6.7', 'matplotlib>=2.2.3', - 'numpy>=1.15.2', + 'numpy>=1.13.3', 'openpyxl>=2.5.8', 'pandas>=0.23.4', 'pdfminer.six>=20170720', From 750f955f9ccf062ce4c5e765f1407eb3b5867446 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 9 Oct 2018 21:21:50 +0530 Subject: [PATCH 06/89] Add requirements.txt for rtd --- README.md | 8 ++++---- docs/user/install.rst | 2 +- requirements.txt | 7 +++++++ 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100755 requirements.txt diff --git a/README.md b/README.md index 38b1e94..d583dd3 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ | 2032_2 | 0.17 | 57.8 | 21.7% | 0.3% | 2.7% | 1.2% | | 4171_1 | 0.07 | 173.9 | 58.1% | 1.6% | 2.1% | 0.5% | -There's a [command-line interface](https://camelot-py.readthedocs.io/en/latest/user/cli.html) too! +There's a [command-line interface](https://camelot-py.readthedocs.io/en/master/user/cli.html) too! **Note:** Camelot only works with text-based PDFs and not scanned documents. (As Tabula [explains](https://github.com/tabulapdf/tabula#why-tabula), "If you can click and drag to select text in your table in a PDF viewer, then your PDF is text-based".) @@ -66,7 +66,7 @@ $ conda install -c camelot-dev camelot-py ### Using pip -After [installing the dependencies](https://camelot-py.readthedocs.io/en/latest/user/install.html#using-pip) ([tk](https://packages.ubuntu.com/trusty/python-tk) and [ghostscript](https://www.ghostscript.com/)), you can simply use pip to install Camelot: +After [installing the dependencies](https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip) ([tk](https://packages.ubuntu.com/trusty/python-tk) and [ghostscript](https://www.ghostscript.com/)), you can simply use pip to install Camelot:
 $ pip install camelot-py[all]
@@ -74,7 +74,7 @@ $ pip install camelot-py[all]
 
 ### From the source code
 
-After [installing the dependencies](https://camelot-py.readthedocs.io/en/latest/user/install.html#using-pip), clone the repo using:
+After [installing the dependencies](https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip), clone the repo using:
 
 
 $ git clone https://www.github.com/socialcopsdev/camelot
@@ -93,7 +93,7 @@ Great documentation is available at [http://camelot-py.readthedocs.io/](http://c
 
 ## Development
 
-The [Contributor's Guide](https://camelot-py.readthedocs.io/en/latest/dev/contributing.html) has detailed information about contributing code, documentation, tests and more. We've included some basic information in this README.
+The [Contributor's Guide](https://camelot-py.readthedocs.io/en/master/dev/contributing.html) has detailed information about contributing code, documentation, tests and more. We've included some basic information in this README.
 
 ### Source code
 
diff --git a/docs/user/install.rst b/docs/user/install.rst
index 7bfd97c..ac2cf1f 100644
--- a/docs/user/install.rst
+++ b/docs/user/install.rst
@@ -112,4 +112,4 @@ After `installing the dependencies`_, you can install from the source by:
     $ cd camelot
     $ pip install ".[all]"
 
-.. _installing the dependencies: https://camelot-py.readthedocs.io/en/latest/user/install.html#using-pip
\ No newline at end of file
+.. _installing the dependencies: https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100755
index 0000000..fdbb1c9
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,7 @@
+click>=6.7
+matplotlib>=2.2.3
+numpy>=1.13.3
+openpyxl>=2.5.8
+pandas>=0.23.4
+pdfminer.six>=20170720
+PyPDF2>=1.26.0
\ No newline at end of file

From d628e9b5df9933989b095d554dd7961b8fbead01 Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Tue, 9 Oct 2018 21:23:10 +0530
Subject: [PATCH 07/89] Update requirements.txt

---
 requirements.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/requirements.txt b/requirements.txt
index fdbb1c9..1f67677 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,7 @@
 click>=6.7
 matplotlib>=2.2.3
 numpy>=1.13.3
+opencv-python>=3.4.2.17
 openpyxl>=2.5.8
 pandas>=0.23.4
 pdfminer.six>=20170720

From c33bf9c1685f400e8f07901ab068afc42225633d Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Tue, 9 Oct 2018 21:33:51 +0530
Subject: [PATCH 08/89] Add docs badge

---
 README.md      | 3 ++-
 docs/index.rst | 4 ++++
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index d583dd3..e027437 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,8 @@
 
 # Camelot: PDF Table Extraction for Humans
 
-[![Build Status](https://travis-ci.org/socialcopsdev/camelot.svg?branch=master)](https://travis-ci.org/socialcopsdev/camelot) [![codecov.io](https://codecov.io/github/socialcopsdev/camelot/badge.svg?branch=master&service=github)](https://codecov.io/github/socialcopsdev/camelot?branch=master)
+[![Build Status](https://travis-ci.org/socialcopsdev/camelot.svg?branch=master)](https://travis-ci.org/socialcopsdev/camelot) [![Documentation Status](https://readthedocs.org/projects/camelot-py/badge/?version=master)](https://camelot-py.readthedocs.io/en/master/)
+ [![codecov.io](https://codecov.io/github/socialcopsdev/camelot/badge.svg?branch=master&service=github)](https://codecov.io/github/socialcopsdev/camelot?branch=master)
  [![image](https://img.shields.io/pypi/v/camelot-py.svg)](https://pypi.org/project/camelot-py/) [![image](https://img.shields.io/pypi/l/camelot-py.svg)](https://pypi.org/project/camelot-py/) [![image](https://img.shields.io/pypi/pyversions/camelot-py.svg)](https://pypi.org/project/camelot-py/)
 
 **Camelot** is a Python library that makes it easy for *anyone* to extract tables from PDF files!
diff --git a/docs/index.rst b/docs/index.rst
index b93451e..2999359 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -11,6 +11,10 @@ Release v\ |version|. (:ref:`Installation `)
 .. image:: https://travis-ci.org/socialcopsdev/camelot.svg?branch=master
     :target: https://travis-ci.org/socialcopsdev/camelot
 
+.. image:: https://readthedocs.org/projects/camelot-py/badge/?version=master
+    :target: https://camelot-py.readthedocs.io/en/master/
+    :alt: Documentation Status
+
 .. image:: https://codecov.io/github/socialcopsdev/camelot/badge.svg?branch=master&service=github
     :target: https://codecov.io/github/socialcopsdev/camelot?branch=master
 

From ac2d40aa444c50a4b56833c8d7e539ff65cf5d19 Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Wed, 10 Oct 2018 00:35:16 +0530
Subject: [PATCH 09/89] Update HISTORY.md

Update HISTORY.md again
---
 HISTORY.md | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/HISTORY.md b/HISTORY.md
index 44f1b5f..d21f31c 100755
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -4,8 +4,13 @@ Release History
 master
 ------
 
-* Update installation instructions for conda.
 * Downgrade numpy version from 1.15.2 to 1.13.3.
+* Add requirements.txt for readthedocs.
+
+**Documentation**
+
+* Add "Using conda" section to installation instructions.
+* Add readthedocs badge.
 
 0.2.3 (2018-10-08)
 ------------------
@@ -69,4 +74,4 @@ master
 0.1.0 (2018-09-24)
 ------------------
 
-* Birth!
+* Rebirth!

From 8d389078320fddc9c17b42f2afc278de5da91fcc Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Thu, 11 Oct 2018 12:57:52 +0530
Subject: [PATCH 10/89] Update conda installation instructions

Update conda installation instructions again
---
 README.md             | 8 ++++++++
 docs/user/install.rst | 8 +++++++-
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index e027437..c064c21 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,14 @@ See [comparison with other PDF table extraction libraries and tools](https://git
 
 The easiest way to install Camelot is to install it with [conda](https://conda.io/docs/), which is the package manager that the [Anaconda](http://docs.continuum.io/anaconda/) distribution is built upon.
 
+First, let's add the [conda-forge](https://conda-forge.org/) channel to conda's config:
+
+
+$ conda config --add channels conda-forge
+
+ +Now, you can simply use conda to install Camelot: +
 $ conda install -c camelot-dev camelot-py
 
diff --git a/docs/user/install.rst b/docs/user/install.rst index ac2cf1f..4d011ca 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -9,7 +9,12 @@ Using conda ----------- The easiest way to install Camelot is to install it with `conda`_, which is the package manager that the `Anaconda`_ distribution is built upon. -:: + +First, let's add the `conda-forge`_ channel to conda's config:: + + $ conda config --add channels conda-forge + +Now, you can simply use conda to install Camelot:: $ conda install -c camelot-dev camelot-py @@ -18,6 +23,7 @@ The easiest way to install Camelot is to install it with `conda`_, which is the .. _conda: https://conda.io/docs/ .. _Anaconda: http://docs.continuum.io/anaconda/ .. _downloads page: https://www.ghostscript.com/download/gsdnld.html +.. _conda-forge: https://conda-forge.org/ Using pip --------- From 1ba0cfc7bc7721be7ef43b601f90abf8fab6e14e Mon Sep 17 00:00:00 2001 From: Vaibhav Mule Date: Thu, 11 Oct 2018 23:36:36 +0530 Subject: [PATCH 11/89] [MRG + 1] Run codecov only once (#132) * Run codecov only once * Update .travis.yml * Update .travis.yml * Add os based install to Makefile * Add requests like .travis.yml and Makefile * Add 'sudo: required' to .travis.yml * Add before_install * Make separate command --- .travis.yml | 46 +++++++++++++++++++++++++++++----------------- Makefile | 28 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 Makefile diff --git a/.travis.yml b/.travis.yml index 91f3087..f93c5f4 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,32 @@ -sudo: false +sudo: true language: python cache: pip -python: - - "2.7" - - "3.5" - - "3.6" -matrix: - include: - - python: 3.7 - dist: xenial - sudo: true -before_install: - - sudo apt-get install python-tk python3-tk ghostscript +addons: + apt: + update: true install: - - pip install ".[dev]" -script: - - pytest --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot tests -after_success: - - codecov --verbose \ No newline at end of file + - make install +jobs: + include: + - stage: test + script: + - make test + python: '2.7' + - stage: test + script: + - make test + python: '3.5' + - stage: test + script: + - make test + python: '3.6' + - stage: test + script: + - make test + python: '3.7' + dist: xenial + - stage: coverage + python: 3.6 + script: + - make test + - codecov --verbose diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a4bea7d --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +.PHONY: docs +INSTALL := +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Linux) + INSTALL := @sudo apt install python-tk python3-tk ghostscript +else ifeq ($(UNAME_S),Darwin) + INSTALL := @brew install tcl-tk ghostscript +else + INSTALL := @echo "Please install tk and ghostscript" +endif + +install: + $(INSTALL) + pip install --upgrade pip + pip install ".[dev]" + +test: + pytest --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot tests + +docs: + cd docs && make html + @echo "\033[95m\n\nBuild successful! View the docs homepage at docs/_build/html/index.html.\n\033[0m" + +publish: + pip install twine + python setup.py sdist + twine upload dist/* + rm -fr build dist .egg camelot_py.egg-info \ No newline at end of file From 9e6474e5a6d1da2f936c6950e832bd4671662ee1 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 11 Oct 2018 23:51:05 +0530 Subject: [PATCH 12/89] Update HISTORY.md --- .travis.yml | 2 +- HISTORY.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f93c5f4..7426bb0 100755 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ jobs: python: '3.7' dist: xenial - stage: coverage - python: 3.6 + python: '3.6' script: - make test - codecov --verbose diff --git a/HISTORY.md b/HISTORY.md index d21f31c..16526f3 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,6 +7,10 @@ master * Downgrade numpy version from 1.15.2 to 1.13.3. * Add requirements.txt for readthedocs. +**Improvements** + +* Add Makefile and make codecov run only once. [#132](https://github.com/socialcopsdev/camelot/pull/132) by [Vaibhav Mule](https://github.com/vaibhavmule). + **Documentation** * Add "Using conda" section to installation instructions. From 9362175a82eaa94526e8a335fee9d3991e07d97f Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 12 Oct 2018 16:46:09 +0530 Subject: [PATCH 13/89] Update advanced.rst --- docs/user/advanced.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index fd65005..e697949 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -149,7 +149,7 @@ Finally, let's plot all line intersections present on the table's PDF page. Specify table areas ------------------- -Since :ref:`Stream ` treats the whole page as a table, `for now`_, it's useful to specify table boundaries in cases such as `these <../_static/pdf/table_areas.pdf>`__. You can :ref:`plot the text ` on this page and note the left-top and right-bottom coordinates of the table. +Since :ref:`Stream ` treats the whole page as a table, `for now`_, it's useful to specify table boundaries in cases such as `these <../_static/pdf/table_areas.pdf>`__. You can :ref:`plot the text ` on this page and note the top left and bottom right coordinates of the table. Table areas that you want Camelot to analyze can be passed as a list of comma-separated strings to :meth:`read_pdf() `, using the ``table_areas`` keyword argument. @@ -416,4 +416,4 @@ We don't need anything else. Now, let's pass ``copy_text=['v']`` to copy text in "3","Odisha","Kalahandi","iii. Food Poisoning","42","0","02/01/14","03/01/14","Under control","..." "4","West Bengal","West Medinipur","iv. Acute Diarrhoeal Disease","145","0","04/01/14","05/01/14","Under control","..." "4","West Bengal","Birbhum","v. Food Poisoning","199","0","31/12/13","31/12/13","Under control","..." - "4","West Bengal","Howrah","vi. Viral Hepatitis A &E","85","0","26/12/13","27/12/13","Under surveillance","..." \ No newline at end of file + "4","West Bengal","Howrah","vi. Viral Hepatitis A &E","85","0","26/12/13","27/12/13","Under surveillance","..." From 297888b18c5f4983d6e38dc4ee876f279f5e4655 Mon Sep 17 00:00:00 2001 From: Krishna Sumanth Date: Fri, 12 Oct 2018 20:22:02 +0530 Subject: [PATCH 14/89] Update conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index c649055..b7f0c0a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -63,7 +63,7 @@ master_doc = 'index' # General information about the project. project = u'Camelot' -copyright = u'2018, Peeply Private Ltd (Singapore)' +copyright = u'2018, SocialCops' author = u'Vinayak Mehta' # The version info for the project you're documenting, acts as replacement for From 970f90643561f0851f47158a0303c3a5db5c5514 Mon Sep 17 00:00:00 2001 From: Krishna Sumanth Date: Fri, 12 Oct 2018 21:37:53 +0530 Subject: [PATCH 15/89] Update conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index b7f0c0a..c4190a2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -63,7 +63,7 @@ master_doc = 'index' # General information about the project. project = u'Camelot' -copyright = u'2018, SocialCops' +copyright = u'2018, SocialCops' author = u'Vinayak Mehta' # The version info for the project you're documenting, acts as replacement for From 7a3b76cb767219f205713e2fc0994db1c1a868c3 Mon Sep 17 00:00:00 2001 From: Krishna Sumanth Date: Fri, 12 Oct 2018 21:39:38 +0530 Subject: [PATCH 16/89] Update conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index c4190a2..4c333bf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -63,7 +63,7 @@ master_doc = 'index' # General information about the project. project = u'Camelot' -copyright = u'2018, SocialCops' +copyright = u'2018, SocialCops' author = u'Vinayak Mehta' # The version info for the project you're documenting, acts as replacement for From 5645ef5b62a096fb8e3c3ef4752f9570f8df2e66 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 15 Oct 2018 04:31:54 +0530 Subject: [PATCH 17/89] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4d0095f..e727706 100644 --- a/setup.py +++ b/setup.py @@ -76,5 +76,5 @@ def setup_package(): setup(**metadata) -if __name__ >= '__main__': +if __name__ == '__main__': setup_package() From 7baea06bca09e01aa99b0c08e1180bca6907513a Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Fri, 19 Oct 2018 10:49:06 +0000 Subject: [PATCH 18/89] Add .editorconfig (#151) --- .editorconfig | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b0d1ce9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*.py] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +end_of_line = lf From 1d064adc3e292173f3dd0fe0d4e44ead19d2961b Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 19 Oct 2018 16:23:15 +0530 Subject: [PATCH 19/89] Update .editorconfig and HISTORY.md --- .editorconfig | 6 ++++-- HISTORY.md | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index b0d1ce9..942148d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,10 @@ root = true +[*] +end_of_line = lf +insert_final_newline = true + [*.py] charset = utf-8 indent_style = space indent_size = 4 -insert_final_newline = true -end_of_line = lf diff --git a/HISTORY.md b/HISTORY.md index 16526f3..9bee6df 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,6 +10,7 @@ master **Improvements** * Add Makefile and make codecov run only once. [#132](https://github.com/socialcopsdev/camelot/pull/132) by [Vaibhav Mule](https://github.com/vaibhavmule). +* Add .editorconfig. [#151](https://github.com/socialcopsdev/camelot/pull/151) by [KOLANICH](https://github.com/KOLANICH). **Documentation** From 3def4a5aea1c9309b6d98614a12aa3f788271e64 Mon Sep 17 00:00:00 2001 From: Jonathan Lloyd Date: Fri, 19 Oct 2018 12:25:00 +0100 Subject: [PATCH 20/89] [MRG + 1] Add suppress_warnings flag (#155) * Add suppress_warnings flag * Add --quiet flag to cli (to suppress warnings) * Remove TODO and update comment --- camelot/cli.py | 9 +++++++-- camelot/io.py | 20 ++++++++++++++------ tests/test_cli.py | 14 ++++++++++++++ tests/test_common.py | 2 +- tests/test_errors.py | 19 ++++++++++++++----- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/camelot/cli.py b/camelot/cli.py index 6a7a08b..e400204 100644 --- a/camelot/cli.py +++ b/camelot/cli.py @@ -38,6 +38,7 @@ pass_config = click.make_pass_decorator(Config) ' font size. Useful to detect super/subscripts.') @click.option('-M', '--margins', nargs=3, default=(1.0, 0.5, 0.1), help='PDFMiner char_margin, line_margin and word_margin.') +@click.option('-q', '--quiet', is_flag=True, help='Suppress warnings.') @click.pass_context def cli(ctx, *args, **kwargs): """Camelot: PDF Table Extraction for Humans""" @@ -89,6 +90,7 @@ def lattice(c, *args, **kwargs): output = conf.pop('output') f = conf.pop('format') compress = conf.pop('zip') + suppress_warnings = conf.pop('quiet') plot_type = kwargs.pop('plot_type') filepath = kwargs.pop('filepath') kwargs.update(conf) @@ -99,7 +101,8 @@ def lattice(c, *args, **kwargs): kwargs['copy_text'] = None if not copy_text else copy_text kwargs['shift_text'] = list(kwargs['shift_text']) - tables = read_pdf(filepath, pages=pages, flavor='lattice', **kwargs) + tables = read_pdf(filepath, pages=pages, flavor='lattice', + suppress_warnings=suppress_warnings, **kwargs) click.echo('Found {} tables'.format(tables.n)) if plot_type is not None: for table in tables: @@ -134,6 +137,7 @@ def stream(c, *args, **kwargs): output = conf.pop('output') f = conf.pop('format') compress = conf.pop('zip') + suppress_warnings = conf.pop('quiet') plot_type = kwargs.pop('plot_type') filepath = kwargs.pop('filepath') kwargs.update(conf) @@ -143,7 +147,8 @@ def stream(c, *args, **kwargs): columns = list(kwargs['columns']) kwargs['columns'] = None if not columns else columns - tables = read_pdf(filepath, pages=pages, flavor='stream', **kwargs) + tables = read_pdf(filepath, pages=pages, flavor='stream', + suppress_warnings=suppress_warnings, **kwargs) click.echo('Found {} tables'.format(tables.n)) if plot_type is not None: for table in tables: diff --git a/camelot/io.py b/camelot/io.py index 90adc96..5cdb542 100644 --- a/camelot/io.py +++ b/camelot/io.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- +import warnings from .handlers import PDFHandler from .utils import validate_input, remove_extra -def read_pdf(filepath, pages='1', flavor='lattice', **kwargs): +def read_pdf(filepath, pages='1', flavor='lattice', suppress_warnings=False, + **kwargs): """Read PDF and return extracted tables. Note: kwargs annotated with ^ can only be used with flavor='stream' @@ -20,6 +22,8 @@ def read_pdf(filepath, pages='1', flavor='lattice', **kwargs): flavor : str (default: 'lattice') The parsing method to use ('lattice' or 'stream'). Lattice is used by default. + suppress_warnings : bool, optional (default: False) + Prevent warnings from being emitted by Camelot. table_area : list, optional (default: None) List of table area strings of the form x1,y1,x2,y2 where (x1, y1) -> left-top and (x2, y2) -> right-bottom @@ -85,8 +89,12 @@ def read_pdf(filepath, pages='1', flavor='lattice', **kwargs): raise NotImplementedError("Unknown flavor specified." " Use either 'lattice' or 'stream'") - validate_input(kwargs, flavor=flavor) - p = PDFHandler(filepath, pages) - kwargs = remove_extra(kwargs, flavor=flavor) - tables = p.parse(flavor=flavor, **kwargs) - return tables + with warnings.catch_warnings(): + if suppress_warnings: + warnings.simplefilter("ignore") + + validate_input(kwargs, flavor=flavor) + p = PDFHandler(filepath, pages) + kwargs = remove_extra(kwargs, flavor=flavor) + tables = p.parse(flavor=flavor, **kwargs) + return tables diff --git a/tests/test_cli.py b/tests/test_cli.py index b89eab3..4797eae 100755 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -77,3 +77,17 @@ def test_cli_output_format(): result = runner.invoke(cli, ['--zip', '--format', 'csv', '--output', outfile.format('csv'), 'stream', infile]) assert result.exit_code == 0 + +def test_cli_quiet_flag(): + with TemporaryDirectory() as tempdir: + infile = os.path.join(testdir, 'blank.pdf') + outfile = os.path.join(tempdir, 'blank.csv') + runner = CliRunner() + + result = runner.invoke(cli, ['--format', 'csv', '--output', outfile, + 'stream', infile]) + assert 'No tables found on page-1' in result.output + + result = runner.invoke(cli, ['--quiet', '--format', 'csv', + '--output', outfile, 'stream', infile]) + assert 'No tables found on page-1' not in result.output diff --git a/tests/test_common.py b/tests/test_common.py index e872fb8..2e7c24e 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -139,7 +139,7 @@ def test_lattice_shift_text(): tables = camelot.read_pdf(filename, line_size_scaling=40, shift_text=['r', 'b']) assert df_rb.equals(tables[0].df) - + def test_repr(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) diff --git a/tests/test_errors.py b/tests/test_errors.py index a6ac35a..86e9e5c 100755 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -43,11 +43,20 @@ def test_stream_equal_length(): def test_no_tables_found(): filename = os.path.join(testdir, 'blank.pdf') - # TODO: use pytest.warns with warnings.catch_warnings(): warnings.simplefilter('error') - try: + with pytest.raises(UserWarning) as e: tables = camelot.read_pdf(filename) - except Exception as e: - assert type(e).__name__ == 'UserWarning' - assert str(e) == 'No tables found on page-1' + assert str(e.value) == 'No tables found on page-1' + + +def test_no_tables_found_warnings_supressed(): + filename = os.path.join(testdir, 'blank.pdf') + with warnings.catch_warnings(): + # the test should fail if any warning is thrown + warnings.simplefilter('error') + try: + tables = camelot.read_pdf(filename, suppress_warnings=True) + except Warning as e: + warning_text = str(e) + pytest.fail('Unexpected warning: {}'.format(warning_text)) From 2022a8abc9f3a477c7a8373973a6b5f886b5fff8 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 19 Oct 2018 17:00:20 +0530 Subject: [PATCH 21/89] Update HISTORY.md --- HISTORY.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 9bee6df..9a0fce5 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -9,7 +9,9 @@ master **Improvements** -* Add Makefile and make codecov run only once. [#132](https://github.com/socialcopsdev/camelot/pull/132) by [Vaibhav Mule](https://github.com/vaibhavmule). +* [#139](https://github.com/socialcopsdev/camelot/issues/139) Add suppress_warnings flag. [#155](https://github.com/socialcopsdev/camelot/pull/155) by [Jonathan Lloyd](https://github.com/jonathanlloyd). + * Warnings raised by Camelot can now be suppressed by passing `suppress_warnings=True` to `read_pdf` or `--quiet` to the command-line interface. +* [#114](https://github.com/socialcopsdev/camelot/issues/114) Add Makefile and make codecov run only once. [#132](https://github.com/socialcopsdev/camelot/pull/132) by [Vaibhav Mule](https://github.com/vaibhavmule). * Add .editorconfig. [#151](https://github.com/socialcopsdev/camelot/pull/151) by [KOLANICH](https://github.com/KOLANICH). **Documentation** From 2a60d1fd54a56486d615747a4124f3ccc86725bd Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 22 Oct 2018 21:52:49 +0530 Subject: [PATCH 22/89] Update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c064c21..5ad0426 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ **Camelot** is a Python library that makes it easy for *anyone* to extract tables from PDF files! +**Note:** You can also check out [Excalibur](https://github.com/camelot-dev/excalibur), which is a web interface for Camelot! + --- **Here's how you can extract tables from PDF files.** Check out the PDF used in this example [here](https://github.com/socialcopsdev/camelot/blob/master/docs/_static/pdf/foo.pdf). From 9c6ec496520bb702de5bc845b546d42bf38a3d7c Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 22 Oct 2018 21:53:38 +0530 Subject: [PATCH 23/89] Update index.rst --- docs/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 2999359..023f8c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,6 +29,8 @@ Release v\ |version|. (:ref:`Installation `) **Camelot** is a Python library that makes it easy for *anyone* to extract tables from PDF files! +.. note:: You can also check out [Excalibur](https://github.com/camelot-dev/excalibur), which is a web interface for Camelot! + ---- **Here's how you can extract tables from PDF files.** Check out the PDF used in this example `here`_. From c5c85a2dc83446dd1f3c0948f108f8ac6822547e Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 22 Oct 2018 21:58:51 +0530 Subject: [PATCH 24/89] Fix index.rst --- docs/index.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 023f8c0..52350c8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,7 +29,9 @@ Release v\ |version|. (:ref:`Installation `) **Camelot** is a Python library that makes it easy for *anyone* to extract tables from PDF files! -.. note:: You can also check out [Excalibur](https://github.com/camelot-dev/excalibur), which is a web interface for Camelot! +.. note:: You can also check out `Excalibur`_, which is a web interface for Camelot! + +.. _Excalibur: https://github.com/camelot-dev/excalibur ---- From 72481bc1b566ee02d40841bcd3161a85a4988db1 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 23 Oct 2018 04:00:17 +0530 Subject: [PATCH 25/89] Replace table_areas with table_area --- docs/user/advanced.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index e697949..b012c6d 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -151,13 +151,13 @@ Specify table areas Since :ref:`Stream ` treats the whole page as a table, `for now`_, it's useful to specify table boundaries in cases such as `these <../_static/pdf/table_areas.pdf>`__. You can :ref:`plot the text ` on this page and note the top left and bottom right coordinates of the table. -Table areas that you want Camelot to analyze can be passed as a list of comma-separated strings to :meth:`read_pdf() `, using the ``table_areas`` keyword argument. +Table areas that you want Camelot to analyze can be passed as a list of comma-separated strings to :meth:`read_pdf() `, using the ``table_area`` keyword argument. .. _for now: https://github.com/socialcopsdev/camelot/issues/102 :: - >>> tables = camelot.read_pdf('table_areas.pdf', flavor='stream', table_areas=['316,499,566,337']) + >>> tables = camelot.read_pdf('table_areas.pdf', flavor='stream', table_area=['316,499,566,337']) >>> tables[0].df .. csv-table:: @@ -172,7 +172,7 @@ You can pass the column separators as a list of comma-separated strings to :meth In case you passed a single column separators string list, and no table area is specified, the separators will be applied to the whole page. When a list of table areas is specified and you need to specify column separators as well, **the length of both lists should be equal**. Each table area will be mapped to each column separators' string using their indices. -For example, if you have specified two table areas, ``table_areas=['12,23,43,54', '20,33,55,67']``, and only want to specify column separators for the first table, you can pass an empty string for the second table in the column separators' list like this, ``columns=['10,120,200,400', '']``. +For example, if you have specified two table areas, ``table_area=['12,23,43,54', '20,33,55,67']``, and only want to specify column separators for the first table, you can pass an empty string for the second table in the column separators' list like this, ``columns=['10,120,200,400', '']``. Let's get back to the *x* coordinates we got from :ref:`plotting text ` that exists on this `PDF <../_static/pdf/column_separators.pdf>`__, and get the table out! From 60c12707452de557acd3e42f7ea59ef9e5954c6c Mon Sep 17 00:00:00 2001 From: Jonathan Lloyd Date: Tue, 23 Oct 2018 00:24:57 +0100 Subject: [PATCH 26/89] Fix typo in test name (#160) test_no_tables_found_warnings_supressed -> test_no_tables_found_warnings_suppressed --- tests/test_errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_errors.py b/tests/test_errors.py index 86e9e5c..2d0a813 100755 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -50,7 +50,7 @@ def test_no_tables_found(): assert str(e.value) == 'No tables found on page-1' -def test_no_tables_found_warnings_supressed(): +def test_no_tables_found_warnings_suppressed(): filename = os.path.join(testdir, 'blank.pdf') with warnings.catch_warnings(): # the test should fail if any warning is thrown From 61963aabb655f482414910152260c92a815e05ee Mon Sep 17 00:00:00 2001 From: Parth P Panchal Date: Tue, 23 Oct 2018 15:01:20 +0530 Subject: [PATCH 27/89] [MRG + 1] Add __main__ (#159) * Renames camelot.cli to camelot.__main__ Closes #154 * Keep __main__ and cli separate * Monkey patch click HelpFormatter --- camelot/__init__.py | 10 +++ camelot/__main__.py | 16 ++++ tests/test_cli.py | 186 ++++++++++++++++++++++---------------------- 3 files changed, 119 insertions(+), 93 deletions(-) create mode 100755 camelot/__main__.py diff --git a/camelot/__init__.py b/camelot/__init__.py index d8ff6a5..364cd72 100644 --- a/camelot/__init__.py +++ b/camelot/__init__.py @@ -2,10 +2,20 @@ import logging +from click import HelpFormatter + from .__version__ import __version__ from .io import read_pdf +def _write_usage(self, prog, args='', prefix='Usage: '): + return self._write_usage('camelot', args, prefix=prefix) + + +# monkey patch click.HelpFormatter +HelpFormatter._write_usage = HelpFormatter.write_usage +HelpFormatter.write_usage = _write_usage + # set up logging logger = logging.getLogger('camelot') diff --git a/camelot/__main__.py b/camelot/__main__.py new file mode 100755 index 0000000..c945051 --- /dev/null +++ b/camelot/__main__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + + +__all__ = ('main',) + + +def main(): + from camelot.cli import cli + + cli() + + +if __name__ == "__main__": + main() diff --git a/tests/test_cli.py b/tests/test_cli.py index 4797eae..3f51f8f 100755 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,93 +1,93 @@ -# -*- coding: utf-8 -*- - -import os - -from click.testing import CliRunner - -from camelot.cli import cli -from camelot.utils import TemporaryDirectory - - -testdir = os.path.dirname(os.path.abspath(__file__)) -testdir = os.path.join(testdir, 'files') - - -def test_cli_lattice(): - with TemporaryDirectory() as tempdir: - infile = os.path.join(testdir, 'foo.pdf') - outfile = os.path.join(tempdir, 'foo.csv') - runner = CliRunner() - result = runner.invoke(cli, ['--format', 'csv', '--output', outfile, - 'lattice', infile]) - assert result.exit_code == 0 - assert result.output == 'Found 1 tables\n' - - result = runner.invoke(cli, ['--format', 'csv', - 'lattice', infile]) - output_error = 'Error: Please specify output file path using --output' - assert output_error in result.output - - result = runner.invoke(cli, ['--output', outfile, - 'lattice', infile]) - format_error = 'Please specify output file format using --format' - assert format_error in result.output - - -def test_cli_stream(): - with TemporaryDirectory() as tempdir: - infile = os.path.join(testdir, 'budget.pdf') - outfile = os.path.join(tempdir, 'budget.csv') - runner = CliRunner() - result = runner.invoke(cli, ['--format', 'csv', '--output', outfile, - 'stream', infile]) - assert result.exit_code == 0 - assert result.output == 'Found 1 tables\n' - - result = runner.invoke(cli, ['--format', 'csv', 'stream', infile]) - output_error = 'Error: Please specify output file path using --output' - assert output_error in result.output - - result = runner.invoke(cli, ['--output', outfile, 'stream', infile]) - format_error = 'Please specify output file format using --format' - assert format_error in result.output - - -def test_cli_output_format(): - with TemporaryDirectory() as tempdir: - infile = os.path.join(testdir, 'health.pdf') - outfile = os.path.join(tempdir, 'health.{}') - runner = CliRunner() - - # json - result = runner.invoke(cli, ['--format', 'json', '--output', outfile.format('json'), - 'stream', infile]) - assert result.exit_code == 0 - - # excel - result = runner.invoke(cli, ['--format', 'excel', '--output', outfile.format('xlsx'), - 'stream', infile]) - assert result.exit_code == 0 - - # html - result = runner.invoke(cli, ['--format', 'html', '--output', outfile.format('html'), - 'stream', infile]) - assert result.exit_code == 0 - - # zip - result = runner.invoke(cli, ['--zip', '--format', 'csv', '--output', outfile.format('csv'), - 'stream', infile]) - assert result.exit_code == 0 - -def test_cli_quiet_flag(): - with TemporaryDirectory() as tempdir: - infile = os.path.join(testdir, 'blank.pdf') - outfile = os.path.join(tempdir, 'blank.csv') - runner = CliRunner() - - result = runner.invoke(cli, ['--format', 'csv', '--output', outfile, - 'stream', infile]) - assert 'No tables found on page-1' in result.output - - result = runner.invoke(cli, ['--quiet', '--format', 'csv', - '--output', outfile, 'stream', infile]) - assert 'No tables found on page-1' not in result.output +# -*- coding: utf-8 -*- + +import os + +from click.testing import CliRunner + +from camelot.cli import cli +from camelot.utils import TemporaryDirectory + + +testdir = os.path.dirname(os.path.abspath(__file__)) +testdir = os.path.join(testdir, 'files') + + +def test_cli_lattice(): + with TemporaryDirectory() as tempdir: + infile = os.path.join(testdir, 'foo.pdf') + outfile = os.path.join(tempdir, 'foo.csv') + runner = CliRunner() + result = runner.invoke(cli, ['--format', 'csv', '--output', outfile, + 'lattice', infile]) + assert result.exit_code == 0 + assert result.output == 'Found 1 tables\n' + + result = runner.invoke(cli, ['--format', 'csv', + 'lattice', infile]) + output_error = 'Error: Please specify output file path using --output' + assert output_error in result.output + + result = runner.invoke(cli, ['--output', outfile, + 'lattice', infile]) + format_error = 'Please specify output file format using --format' + assert format_error in result.output + + +def test_cli_stream(): + with TemporaryDirectory() as tempdir: + infile = os.path.join(testdir, 'budget.pdf') + outfile = os.path.join(tempdir, 'budget.csv') + runner = CliRunner() + result = runner.invoke(cli, ['--format', 'csv', '--output', outfile, + 'stream', infile]) + assert result.exit_code == 0 + assert result.output == 'Found 1 tables\n' + + result = runner.invoke(cli, ['--format', 'csv', 'stream', infile]) + output_error = 'Error: Please specify output file path using --output' + assert output_error in result.output + + result = runner.invoke(cli, ['--output', outfile, 'stream', infile]) + format_error = 'Please specify output file format using --format' + assert format_error in result.output + + +def test_cli_output_format(): + with TemporaryDirectory() as tempdir: + infile = os.path.join(testdir, 'health.pdf') + outfile = os.path.join(tempdir, 'health.{}') + runner = CliRunner() + + # json + result = runner.invoke(cli, ['--format', 'json', '--output', outfile.format('json'), + 'stream', infile]) + assert result.exit_code == 0 + + # excel + result = runner.invoke(cli, ['--format', 'excel', '--output', outfile.format('xlsx'), + 'stream', infile]) + assert result.exit_code == 0 + + # html + result = runner.invoke(cli, ['--format', 'html', '--output', outfile.format('html'), + 'stream', infile]) + assert result.exit_code == 0 + + # zip + result = runner.invoke(cli, ['--zip', '--format', 'csv', '--output', outfile.format('csv'), + 'stream', infile]) + assert result.exit_code == 0 + +def test_cli_quiet_flag(): + with TemporaryDirectory() as tempdir: + infile = os.path.join(testdir, 'blank.pdf') + outfile = os.path.join(tempdir, 'blank.csv') + runner = CliRunner() + + result = runner.invoke(cli, ['--format', 'csv', '--output', outfile, + 'stream', infile]) + assert 'No tables found on page-1' in result.output + + result = runner.invoke(cli, ['--quiet', '--format', 'csv', + '--output', outfile, 'stream', infile]) + assert 'No tables found on page-1' not in result.output From f734af3a0bf8e0731c54ed784ece461f24132e01 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 23 Oct 2018 15:04:54 +0530 Subject: [PATCH 28/89] Update HISTORY.md --- HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.md b/HISTORY.md index 9a0fce5..a4be364 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,6 +11,7 @@ master * [#139](https://github.com/socialcopsdev/camelot/issues/139) Add suppress_warnings flag. [#155](https://github.com/socialcopsdev/camelot/pull/155) by [Jonathan Lloyd](https://github.com/jonathanlloyd). * Warnings raised by Camelot can now be suppressed by passing `suppress_warnings=True` to `read_pdf` or `--quiet` to the command-line interface. +* [#154](https://github.com/socialcopsdev/camelot/issues/154) The CLI can now be run using `python -m`. Try `python -m camelot --help`. [#159](https://github.com/socialcopsdev/camelot/pull/159) by [Parth P Panchal](https://github.com/pqrth). * [#114](https://github.com/socialcopsdev/camelot/issues/114) Add Makefile and make codecov run only once. [#132](https://github.com/socialcopsdev/camelot/pull/132) by [Vaibhav Mule](https://github.com/vaibhavmule). * Add .editorconfig. [#151](https://github.com/socialcopsdev/camelot/pull/151) by [KOLANICH](https://github.com/KOLANICH). From a78ef7f841c74745a8161cc457924631c240f86e Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 23 Oct 2018 21:12:43 +0530 Subject: [PATCH 29/89] [MRG] Use find_executable for gs and raise error if not found (#166) * Use find_executable for gs and raise error if not found * Remove unused variable * Add test * Use pytest monkeypatch --- camelot/parsers/lattice.py | 50 ++++++++++++++++++++++++++------------ tests/test_errors.py | 15 ++++++++++++ 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index e3c0a6d..c2340ec 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -174,15 +174,39 @@ class Lattice(BaseParser): return t def _generate_image(self): - # TODO: hacky, get rid of ghostscript #96 - def get_platform(): + # TODO: get rid of ghostscript #96 + def get_executable(): import platform + from distutils.spawn import find_executable - info = { - 'system': platform.system().lower(), - 'machine': platform.machine().lower() - } - return info + class GhostscriptNotFound(Exception): pass + + gs = None + system = platform.system().lower() + try: + if system == 'windows': + if find_executable('gswin32c.exe'): + gs = 'gswin32c.exe' + elif find_executable('gswin64c.exe'): + gs = 'gswin64c.exe' + else: + raise ValueError + else: + if find_executable('gs'): + gs = 'gs' + elif find_executable('gsc'): + gs = 'gsc' + else: + raise ValueError + if 'ghostscript' not in subprocess.check_output( + [gs, '-version']).decode('utf-8').lower(): + raise ValueError + except ValueError: + raise GhostscriptNotFound( + 'Please make sure that Ghostscript is installed' + ' and available on the PATH environment variable') + + return gs self.imagename = ''.join([self.rootname, '.png']) gs_call = [ @@ -193,15 +217,9 @@ class Lattice(BaseParser): '-r600', self.filename ] - info = get_platform() - if info['system'] == 'windows': - bit = info['machine'][-2:] - gs_call.insert(0, 'gswin{}c.exe'.format(bit)) - else: - if 'ghostscript' in subprocess.check_output(['gs', '-version']).decode('utf-8').lower(): - gs_call.insert(0, 'gs') - else: - gs_call.insert(0, "gsc") + gs = get_executable() + gs_call.insert(0, gs) + subprocess.call( gs_call, stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT) diff --git a/tests/test_errors.py b/tests/test_errors.py index 2d0a813..095e5d5 100755 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -60,3 +60,18 @@ def test_no_tables_found_warnings_suppressed(): except Warning as e: warning_text = str(e) pytest.fail('Unexpected warning: {}'.format(warning_text)) + + +def test_ghostscript_not_found(monkeypatch): + import distutils + + def _find_executable_patch(arg): + return '' + + monkeypatch.setattr(distutils.spawn, 'find_executable', _find_executable_patch) + + message = ('Please make sure that Ghostscript is installed and available' + ' on the PATH environment variable') + filename = os.path.join(testdir, 'foo.pdf') + with pytest.raises(Exception, message=message): + tables = camelot.read_pdf(filename) From 8205e0e9ab3e78226d930e86e4563174c5c75b80 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 23 Oct 2018 21:16:18 +0530 Subject: [PATCH 30/89] Update HISTORY.md --- HISTORY.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index a4be364..d0174cf 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -15,6 +15,10 @@ master * [#114](https://github.com/socialcopsdev/camelot/issues/114) Add Makefile and make codecov run only once. [#132](https://github.com/socialcopsdev/camelot/pull/132) by [Vaibhav Mule](https://github.com/vaibhavmule). * Add .editorconfig. [#151](https://github.com/socialcopsdev/camelot/pull/151) by [KOLANICH](https://github.com/KOLANICH). +**Bugfixes** + +* Raise error if the ghostscript executable is not on the PATH variable. [#166](https://github.com/socialcopsdev/camelot/pull/166) by Vinayak Mehta. + **Documentation** * Add "Using conda" section to installation instructions. From 32df09ad1c434af871a70a070a045ce1f14c2b47 Mon Sep 17 00:00:00 2001 From: Parth P Panchal Date: Wed, 24 Oct 2018 23:06:53 +0530 Subject: [PATCH 31/89] Renames the keyword `table_area` to `table_areas` (#171) `table_areas` sounds more apt since it is a list and there can be multiple table areas on a page. Closes #165 --- camelot/cli.py | 12 ++++++------ camelot/io.py | 2 +- camelot/parsers/lattice.py | 10 +++++----- camelot/parsers/stream.py | 16 ++++++++-------- docs/user/advanced.rst | 6 +++--- tests/data.py | 4 ++-- tests/test_common.py | 12 ++++++------ tests/test_errors.py | 4 ++-- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/camelot/cli.py b/camelot/cli.py index e400204..8385450 100644 --- a/camelot/cli.py +++ b/camelot/cli.py @@ -48,7 +48,7 @@ def cli(ctx, *args, **kwargs): @cli.command('lattice') -@click.option('-T', '--table_area', default=[], multiple=True, +@click.option('-T', '--table_areas', default=[], multiple=True, help='Table areas to process. Example: x1,y1,x2,y2' ' where x1, y1 -> left-top and x2, y2 -> right-bottom.') @click.option('-back', '--process_background', is_flag=True, @@ -95,8 +95,8 @@ def lattice(c, *args, **kwargs): filepath = kwargs.pop('filepath') kwargs.update(conf) - table_area = list(kwargs['table_area']) - kwargs['table_area'] = None if not table_area else table_area + table_areas = list(kwargs['table_areas']) + kwargs['table_areas'] = None if not table_areas else table_areas copy_text = list(kwargs['copy_text']) kwargs['copy_text'] = None if not copy_text else copy_text kwargs['shift_text'] = list(kwargs['shift_text']) @@ -116,7 +116,7 @@ def lattice(c, *args, **kwargs): @cli.command('stream') -@click.option('-T', '--table_area', default=[], multiple=True, +@click.option('-T', '--table_areas', default=[], multiple=True, help='Table areas to process. Example: x1,y1,x2,y2' ' where x1, y1 -> left-top and x2, y2 -> right-bottom.') @click.option('-C', '--columns', default=[], multiple=True, @@ -142,8 +142,8 @@ def stream(c, *args, **kwargs): filepath = kwargs.pop('filepath') kwargs.update(conf) - table_area = list(kwargs['table_area']) - kwargs['table_area'] = None if not table_area else table_area + table_areas = list(kwargs['table_areas']) + kwargs['table_areas'] = None if not table_areas else table_areas columns = list(kwargs['columns']) kwargs['columns'] = None if not columns else columns diff --git a/camelot/io.py b/camelot/io.py index 5cdb542..06a31a9 100644 --- a/camelot/io.py +++ b/camelot/io.py @@ -24,7 +24,7 @@ def read_pdf(filepath, pages='1', flavor='lattice', suppress_warnings=False, Lattice is used by default. suppress_warnings : bool, optional (default: False) Prevent warnings from being emitted by Camelot. - table_area : list, optional (default: None) + table_areas : list, optional (default: None) List of table area strings of the form x1,y1,x2,y2 where (x1, y1) -> left-top and (x2, y2) -> right-bottom in PDF coordinate space. diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index c2340ec..7b7c411 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -28,7 +28,7 @@ class Lattice(BaseParser): Parameters ---------- - table_area : list, optional (default: None) + table_areas : list, optional (default: None) List of table area strings of the form x1,y1,x2,y2 where (x1, y1) -> left-top and (x2, y2) -> right-bottom in PDF coordinate space. @@ -76,12 +76,12 @@ class Lattice(BaseParser): For more information, refer `PDFMiner docs `_. """ - def __init__(self, table_area=None, process_background=False, + def __init__(self, table_areas=None, process_background=False, line_size_scaling=15, copy_text=None, shift_text=['l', 't'], split_text=False, flag_size=False, line_close_tol=2, joint_close_tol=2, threshold_blocksize=15, threshold_constant=-2, iterations=0, margins=(1.0, 0.5, 0.1), **kwargs): - self.table_area = table_area + self.table_areas = table_areas self.process_background = process_background self.line_size_scaling = line_size_scaling self.copy_text = copy_text @@ -244,9 +244,9 @@ class Lattice(BaseParser): self.threshold, direction='horizontal', line_size_scaling=self.line_size_scaling, iterations=self.iterations) - if self.table_area is not None: + if self.table_areas is not None: areas = [] - for area in self.table_area: + for area in self.table_areas: x1, y1, x2, y2 = area.split(",") x1 = float(x1) y1 = float(y1) diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 2792d82..6aee966 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -26,7 +26,7 @@ class Stream(BaseParser): Parameters ---------- - table_area : list, optional (default: None) + table_areas : list, optional (default: None) List of table area strings of the form x1,y1,x2,y2 where (x1, y1) -> left-top and (x2, y2) -> right-bottom in PDF coordinate space. @@ -50,10 +50,10 @@ class Stream(BaseParser): For more information, refer `PDFMiner docs `_. """ - def __init__(self, table_area=None, columns=None, split_text=False, + def __init__(self, table_areas=None, columns=None, split_text=False, flag_size=False, row_close_tol=2, col_close_tol=0, margins=(1.0, 0.5, 0.1), **kwargs): - self.table_area = table_area + self.table_areas = table_areas self.columns = columns self._validate_columns() self.split_text = split_text @@ -241,15 +241,15 @@ class Stream(BaseParser): return cols def _validate_columns(self): - if self.table_area is not None and self.columns is not None: - if len(self.table_area) != len(self.columns): - raise ValueError("Length of table_area and columns" + if self.table_areas is not None and self.columns is not None: + if len(self.table_areas) != len(self.columns): + raise ValueError("Length of table_areas and columns" " should be equal") def _generate_table_bbox(self): - if self.table_area is not None: + if self.table_areas is not None: table_bbox = {} - for area in self.table_area: + for area in self.table_areas: x1, y1, x2, y2 = area.split(",") x1 = float(x1) y1 = float(y1) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index b012c6d..e697949 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -151,13 +151,13 @@ Specify table areas Since :ref:`Stream ` treats the whole page as a table, `for now`_, it's useful to specify table boundaries in cases such as `these <../_static/pdf/table_areas.pdf>`__. You can :ref:`plot the text ` on this page and note the top left and bottom right coordinates of the table. -Table areas that you want Camelot to analyze can be passed as a list of comma-separated strings to :meth:`read_pdf() `, using the ``table_area`` keyword argument. +Table areas that you want Camelot to analyze can be passed as a list of comma-separated strings to :meth:`read_pdf() `, using the ``table_areas`` keyword argument. .. _for now: https://github.com/socialcopsdev/camelot/issues/102 :: - >>> tables = camelot.read_pdf('table_areas.pdf', flavor='stream', table_area=['316,499,566,337']) + >>> tables = camelot.read_pdf('table_areas.pdf', flavor='stream', table_areas=['316,499,566,337']) >>> tables[0].df .. csv-table:: @@ -172,7 +172,7 @@ You can pass the column separators as a list of comma-separated strings to :meth In case you passed a single column separators string list, and no table area is specified, the separators will be applied to the whole page. When a list of table areas is specified and you need to specify column separators as well, **the length of both lists should be equal**. Each table area will be mapped to each column separators' string using their indices. -For example, if you have specified two table areas, ``table_area=['12,23,43,54', '20,33,55,67']``, and only want to specify column separators for the first table, you can pass an empty string for the second table in the column separators' list like this, ``columns=['10,120,200,400', '']``. +For example, if you have specified two table areas, ``table_areas=['12,23,43,54', '20,33,55,67']``, and only want to specify column separators for the first table, you can pass an empty string for the second table in the column separators' list like this, ``columns=['10,120,200,400', '']``. Let's get back to the *x* coordinates we got from :ref:`plotting text ` that exists on this `PDF <../_static/pdf/column_separators.pdf>`__, and get the table out! diff --git a/tests/data.py b/tests/data.py index 3a04d24..00e070a 100755 --- a/tests/data.py +++ b/tests/data.py @@ -81,7 +81,7 @@ data_stream_table_rotated = [ ["", "", "", "", "", "", "", "", "54", "", "", "", "", "", "", "", "", ""] ] -data_stream_table_area = [ +data_stream_table_areas = [ ["", "One Withholding"], ["Payroll Period", "Allowance"], ["Weekly", "$71.15"], @@ -261,7 +261,7 @@ data_lattice_table_rotated = [ ["Pooled", "38742", "53618", "60601", "86898", "4459", "21918", "27041", "14312", "18519"] ] -data_lattice_table_area = [ +data_lattice_table_areas = [ ["", "", "", "", "", "", "", "", ""], ["State", "n", "Literacy Status", "", "", "", "", "", ""], ["", "", "Illiterate", "Read & Write", "1-4 std.", "5-8 std.", "9-12 std.", "College", ""], diff --git a/tests/test_common.py b/tests/test_common.py index 2e7c24e..1f599fd 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -45,11 +45,11 @@ def test_stream_table_rotated(): assert df.equals(tables[0].df) -def test_stream_table_area(): - df = pd.DataFrame(data_stream_table_area) +def test_stream_table_areas(): + df = pd.DataFrame(data_stream_table_areas) filename = os.path.join(testdir, "tabula/us-007.pdf") - tables = camelot.read_pdf(filename, flavor="stream", table_area=["320,500,573,335"]) + tables = camelot.read_pdf(filename, flavor="stream", table_areas=["320,500,573,335"]) assert df.equals(tables[0].df) @@ -100,11 +100,11 @@ def test_lattice_table_rotated(): assert df.equals(tables[0].df) -def test_lattice_table_area(): - df = pd.DataFrame(data_lattice_table_area) +def test_lattice_table_areas(): + df = pd.DataFrame(data_lattice_table_areas) filename = os.path.join(testdir, "twotables_2.pdf") - tables = camelot.read_pdf(filename, table_area=["80,693,535,448"]) + tables = camelot.read_pdf(filename, table_areas=["80,693,535,448"]) assert df.equals(tables[0].df) diff --git a/tests/test_errors.py b/tests/test_errors.py index 095e5d5..89db31d 100755 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -34,11 +34,11 @@ def test_unsupported_format(): def test_stream_equal_length(): - message = ("Length of table_area and columns" + message = ("Length of table_areas and columns" " should be equal") with pytest.raises(ValueError, message=message): tables = camelot.read_pdf(filename, flavor='stream', - table_area=['10,20,30,40'], columns=['10,20,30,40', '10,20,30,40']) + table_areas=['10,20,30,40'], columns=['10,20,30,40', '10,20,30,40']) def test_no_tables_found(): From 39cf65ffef9c2c8feb57ed5ab95370d454304fcd Mon Sep 17 00:00:00 2001 From: Vinicius Mesel Date: Wed, 24 Oct 2018 15:23:54 -0300 Subject: [PATCH 32/89] [MRG + 1] Convert filename to lowercase to check for extension (#169) * Creates a new variable that stores a lowercase version of the filename * Remove variable --- camelot/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camelot/handlers.py b/camelot/handlers.py index d50f313..6820cc7 100644 --- a/camelot/handlers.py +++ b/camelot/handlers.py @@ -26,7 +26,7 @@ class PDFHandler(object): """ def __init__(self, filename, pages='1'): self.filename = filename - if not self.filename.endswith('.pdf'): + if not filename.lower().endswith('.pdf'): raise NotImplementedError("File format not supported") self.pages = self._get_pages(self.filename, pages) From 2830ed941808c8b514c5be74db1d840b45b26660 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 25 Oct 2018 00:07:16 +0530 Subject: [PATCH 33/89] Update HISTORY.md --- HISTORY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index d0174cf..da5988c 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -14,10 +14,12 @@ master * [#154](https://github.com/socialcopsdev/camelot/issues/154) The CLI can now be run using `python -m`. Try `python -m camelot --help`. [#159](https://github.com/socialcopsdev/camelot/pull/159) by [Parth P Panchal](https://github.com/pqrth). * [#114](https://github.com/socialcopsdev/camelot/issues/114) Add Makefile and make codecov run only once. [#132](https://github.com/socialcopsdev/camelot/pull/132) by [Vaibhav Mule](https://github.com/vaibhavmule). * Add .editorconfig. [#151](https://github.com/socialcopsdev/camelot/pull/151) by [KOLANICH](https://github.com/KOLANICH). +* [#165](https://github.com/socialcopsdev/camelot/issues/165) Rename `table_area` to `table_areas`. [#171](https://github.com/socialcopsdev/camelot/pull/171) by [Parth P Panchal](https://github.com/pqrth). **Bugfixes** * Raise error if the ghostscript executable is not on the PATH variable. [#166](https://github.com/socialcopsdev/camelot/pull/166) by Vinayak Mehta. +* Convert filename to lowercase to check for PDF extension. [#169](https://github.com/socialcopsdev/camelot/pull/169) by [Vinicius Mesel](https://github.com/vmesel). **Documentation** From 43663134844767416464dacf217393991ee1473b Mon Sep 17 00:00:00 2001 From: gison93 <43569657+gison93@users.noreply.github.com> Date: Sun, 28 Oct 2018 10:11:04 +0100 Subject: [PATCH 34/89] Clarify example for argument pages in read_pdf (#177) --- camelot/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camelot/io.py b/camelot/io.py index 06a31a9..643a2b1 100644 --- a/camelot/io.py +++ b/camelot/io.py @@ -18,7 +18,7 @@ def read_pdf(filepath, pages='1', flavor='lattice', suppress_warnings=False, Path to PDF file. pages : str, optional (default: '1') Comma-separated page numbers. - Example: 1,3,4 or 1,4-end. + Example: '1,3,4' or '1,4-end'. flavor : str (default: 'lattice') The parsing method to use ('lattice' or 'stream'). Lattice is used by default. From 429640feeab449a4481f00bc48eeb92619129796 Mon Sep 17 00:00:00 2001 From: rbares Date: Sun, 28 Oct 2018 16:31:10 +0000 Subject: [PATCH 35/89] [MRG + 1] Add basic support for encrypted PDF files (#180) * [MRG] Add basic support for encrypted PDF files Update API and CLI to accept ASCII passwords to decrypt PDFs encrypted by algorithm code 1 or 2 (limited by support from PyPDF2). Update documentation and unit tests accordingly. Example document health_protected.pdf generated as follows: qpdf --encrypt userpass ownerpass 128 -- health.pdf health_protected.pdf Issue #162 * Support encrypted PDF files in python3 Issue #162 * Address review comments Explicitly check passwords for None rather than falsey. Correct read_pdf documentation for Owner/User password. Issue #162 * Correct API documentation changes for consistency Issue #162 * Move error tests from test_common to test_errors Issue #162 * Add qpdf example * Remove password is not None check * Fix merge conflict * Fix pages example --- camelot/cli.py | 1 + camelot/handlers.py | 19 +++++++++++---- camelot/io.py | 8 ++++--- docs/user/cli.rst | 40 ++++++++++++++++--------------- docs/user/quickstart.rst | 26 ++++++++++++++++++-- tests/files/health_protected.pdf | Bin 0 -> 84023 bytes tests/test_cli.py | 26 +++++++++++++++++++- tests/test_common.py | 11 +++++++++ tests/test_errors.py | 14 +++++++++++ 9 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 tests/files/health_protected.pdf diff --git a/camelot/cli.py b/camelot/cli.py index 8385450..e30b204 100644 --- a/camelot/cli.py +++ b/camelot/cli.py @@ -27,6 +27,7 @@ pass_config = click.make_pass_decorator(Config) @click.version_option(version=__version__) @click.option('-p', '--pages', default='1', help='Comma-separated page numbers.' ' Example: 1,3,4 or 1,4-end.') +@click.option('-pw', '--password', help='Password for decryption.') @click.option('-o', '--output', help='Output file path.') @click.option('-f', '--format', type=click.Choice(['csv', 'json', 'excel', 'html']), diff --git a/camelot/handlers.py b/camelot/handlers.py index 6820cc7..b6dc65c 100644 --- a/camelot/handlers.py +++ b/camelot/handlers.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import sys from PyPDF2 import PdfFileReader, PdfFileWriter @@ -21,14 +22,22 @@ class PDFHandler(object): Path to PDF file. pages : str, optional (default: '1') Comma-separated page numbers. - Example: 1,3,4 or 1,4-end. + Example: '1,3,4' or '1,4-end'. + password : str, optional (default: None) + Password for decryption. """ - def __init__(self, filename, pages='1'): + def __init__(self, filename, pages='1', password=None): self.filename = filename if not filename.lower().endswith('.pdf'): raise NotImplementedError("File format not supported") self.pages = self._get_pages(self.filename, pages) + if password is None: + self.password = '' + else: + self.password = password + if sys.version_info[0] < 3: + self.password = self.password.encode('ascii') def _get_pages(self, filename, pages): """Converts pages string to list of ints. @@ -52,6 +61,8 @@ class PDFHandler(object): page_numbers.append({'start': 1, 'end': 1}) else: infile = PdfFileReader(open(filename, 'rb'), strict=False) + if infile.isEncrypted: + infile.decrypt(self.password) if pages == 'all': page_numbers.append({'start': 1, 'end': infile.getNumPages()}) else: @@ -84,7 +95,7 @@ class PDFHandler(object): with open(filename, 'rb') as fileobj: infile = PdfFileReader(fileobj, strict=False) if infile.isEncrypted: - infile.decrypt('') + infile.decrypt(self.password) fpath = os.path.join(temp, 'page-{0}.pdf'.format(page)) froot, fext = os.path.splitext(fpath) p = infile.getPage(page - 1) @@ -103,7 +114,7 @@ class PDFHandler(object): os.rename(fpath, fpath_new) infile = PdfFileReader(open(fpath_new, 'rb'), strict=False) if infile.isEncrypted: - infile.decrypt('') + infile.decrypt(self.password) outfile = PdfFileWriter() p = infile.getPage(0) if rotation == 'anticlockwise': diff --git a/camelot/io.py b/camelot/io.py index 643a2b1..3766a7b 100644 --- a/camelot/io.py +++ b/camelot/io.py @@ -5,8 +5,8 @@ from .handlers import PDFHandler from .utils import validate_input, remove_extra -def read_pdf(filepath, pages='1', flavor='lattice', suppress_warnings=False, - **kwargs): +def read_pdf(filepath, pages='1', password=None, flavor='lattice', + suppress_warnings=False, **kwargs): """Read PDF and return extracted tables. Note: kwargs annotated with ^ can only be used with flavor='stream' @@ -19,6 +19,8 @@ def read_pdf(filepath, pages='1', flavor='lattice', suppress_warnings=False, pages : str, optional (default: '1') Comma-separated page numbers. Example: '1,3,4' or '1,4-end'. + password : str, optional (default: None) + Password for decryption. flavor : str (default: 'lattice') The parsing method to use ('lattice' or 'stream'). Lattice is used by default. @@ -94,7 +96,7 @@ def read_pdf(filepath, pages='1', flavor='lattice', suppress_warnings=False, warnings.simplefilter("ignore") validate_input(kwargs, flavor=flavor) - p = PDFHandler(filepath, pages) + p = PDFHandler(filepath, pages=pages, password=password) kwargs = remove_extra(kwargs, flavor=flavor) tables = p.parse(flavor=flavor, **kwargs) return tables diff --git a/docs/user/cli.rst b/docs/user/cli.rst index f96ceae..0dd677c 100644 --- a/docs/user/cli.rst +++ b/docs/user/cli.rst @@ -9,26 +9,28 @@ You can print the help for the interface by typing ``camelot --help`` in your fa :: - Usage: camelot [OPTIONS] COMMAND [ARGS]... +Usage: camelot [OPTIONS] COMMAND [ARGS]... Camelot: PDF Table Extraction for Humans - Options: - --version Show the version and exit. - -p, --pages TEXT Comma-separated page numbers. Example: 1,3,4 - or 1,4-end. - -o, --output TEXT Output file path. - -f, --format [csv|json|excel|html] - Output file format. - -z, --zip Create ZIP archive. - -split, --split_text Split text that spans across multiple cells. - -flag, --flag_size Flag text based on font size. Useful to - detect super/subscripts. - -M, --margins ... - PDFMiner char_margin, line_margin and - word_margin. - --help Show this message and exit. +Options: + --version Show the version and exit. + -p, --pages TEXT Comma-separated page numbers. Example: 1,3,4 + or 1,4-end. + -pw, --password TEXT Password for decryption. + -o, --output TEXT Output file path. + -f, --format [csv|json|excel|html] + Output file format. + -z, --zip Create ZIP archive. + -split, --split_text Split text that spans across multiple cells. + -flag, --flag_size Flag text based on font size. Useful to + detect super/subscripts. + -M, --margins ... + PDFMiner char_margin, line_margin and + word_margin. + -q, --quiet Suppress warnings. + --help Show this message and exit. - Commands: - lattice Use lines between text to parse the table. - stream Use spaces between text to parse the table. \ No newline at end of file +Commands: + lattice Use lines between text to parse the table. + stream Use spaces between text to parse the table. diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index f7c2863..5fb5bc0 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -87,6 +87,28 @@ By default, Camelot only uses the first page of the PDF to extract tables. To sp The ``pages`` keyword argument accepts pages as comma-separated string of page numbers. You can also specify page ranges — for example, ``pages=1,4-10,20-30`` or ``pages=1,4-10,20-end``. ------------------------- +Reading encrypted PDFs +---------------------- -Ready for more? Check out the :ref:`advanced ` section. \ No newline at end of file +To extract tables from encrypted PDF files you must provide a password when calling :meth:`read_pdf() `. + +:: + + >>> tables = camelot.read_pdf('foo.pdf', password='userpass') + >>> tables + + +Currently Camelot only supports PDFs encrypted with ASCII passwords and algorithm `code 1 or 2`_. An exception is thrown if the PDF cannot be read. This may be due to no password being provided, an incorrect password, or an unsupported encryption algorithm. + +Further encryption support may be added in future, however in the meantime if your PDF files are using unsupported encryption algorithms you are advised to remove encryption before calling :meth:`read_pdf() `. This can been successfully achieved with third-party tools such as `QPDF`_. + +:: + + $ qpdf --password= --decrypt input.pdf output.pdf + +.. _code 1 or 2: https://github.com/mstamy2/PyPDF2/issues/378 +.. _QPDF: https://www.github.com/qpdf/qpdf + +---- + +Ready for more? Check out the :ref:`advanced ` section. diff --git a/tests/files/health_protected.pdf b/tests/files/health_protected.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c5ce0801990a90c58106d5c9cb411bea0df7e693 GIT binary patch literal 84023 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

d4(N?LH%E*(}-b+S37vLh_qRBjXJ_zNGA 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 i3izM1{Pgje$}X Date: Sun, 28 Oct 2018 22:09:42 +0530 Subject: [PATCH 36/89] Update HISTORY.md Update HISTORY.md Update HISTORY.md again --- HISTORY.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index da5988c..6ab4c92 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,16 +4,13 @@ Release History master ------ -* Downgrade numpy version from 1.15.2 to 1.13.3. -* Add requirements.txt for readthedocs. - **Improvements** -* [#139](https://github.com/socialcopsdev/camelot/issues/139) Add suppress_warnings flag. [#155](https://github.com/socialcopsdev/camelot/pull/155) by [Jonathan Lloyd](https://github.com/jonathanlloyd). +* [#162](https://github.com/socialcopsdev/camelot/issues/162) Add password keyword argument. [#180](https://github.com/socialcopsdev/camelot/pull/180) by [rbares](https://github.com/rbares). + * An encrypted PDF can now be decrypted by passing `password=''` to `read_pdf` or `--password ` to the command-line interface. (Limited encryption algorithm support from PyPDF2.) +* [#139](https://github.com/socialcopsdev/camelot/issues/139) Add suppress_warnings keyword argument. [#155](https://github.com/socialcopsdev/camelot/pull/155) by [Jonathan Lloyd](https://github.com/jonathanlloyd). * Warnings raised by Camelot can now be suppressed by passing `suppress_warnings=True` to `read_pdf` or `--quiet` to the command-line interface. * [#154](https://github.com/socialcopsdev/camelot/issues/154) The CLI can now be run using `python -m`. Try `python -m camelot --help`. [#159](https://github.com/socialcopsdev/camelot/pull/159) by [Parth P Panchal](https://github.com/pqrth). -* [#114](https://github.com/socialcopsdev/camelot/issues/114) Add Makefile and make codecov run only once. [#132](https://github.com/socialcopsdev/camelot/pull/132) by [Vaibhav Mule](https://github.com/vaibhavmule). -* Add .editorconfig. [#151](https://github.com/socialcopsdev/camelot/pull/151) by [KOLANICH](https://github.com/KOLANICH). * [#165](https://github.com/socialcopsdev/camelot/issues/165) Rename `table_area` to `table_areas`. [#171](https://github.com/socialcopsdev/camelot/pull/171) by [Parth P Panchal](https://github.com/pqrth). **Bugfixes** @@ -21,6 +18,13 @@ master * Raise error if the ghostscript executable is not on the PATH variable. [#166](https://github.com/socialcopsdev/camelot/pull/166) by Vinayak Mehta. * Convert filename to lowercase to check for PDF extension. [#169](https://github.com/socialcopsdev/camelot/pull/169) by [Vinicius Mesel](https://github.com/vmesel). +**Files** + +* [#114](https://github.com/socialcopsdev/camelot/issues/114) Add Makefile and make codecov run only once. [#132](https://github.com/socialcopsdev/camelot/pull/132) by [Vaibhav Mule](https://github.com/vaibhavmule). +* Add .editorconfig. [#151](https://github.com/socialcopsdev/camelot/pull/151) by [KOLANICH](https://github.com/KOLANICH). +* Downgrade numpy version from 1.15.2 to 1.13.3. +* Add requirements.txt for readthedocs. + **Documentation** * Add "Using conda" section to installation instructions. From f73062c1c4c431bfeb323263dce95c503d50e91c Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 28 Oct 2018 22:33:54 +0530 Subject: [PATCH 37/89] Bump version Update HISTORY.md --- HISTORY.md | 3 +++ camelot/__version__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 6ab4c92..6c1bc42 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,9 @@ Release History master ------ +0.3.0 (2018-10-28) +------------------ + **Improvements** * [#162](https://github.com/socialcopsdev/camelot/issues/162) Add password keyword argument. [#180](https://github.com/socialcopsdev/camelot/pull/180) by [rbares](https://github.com/rbares). diff --git a/camelot/__version__.py b/camelot/__version__.py index 89f8c08..646976f 100644 --- a/camelot/__version__.py +++ b/camelot/__version__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -VERSION = (0, 2, 3) +VERSION = (0, 3, 0) __title__ = 'camelot-py' __description__ = 'PDF Table Extraction for Humans.' From 220d6ad29cfd290a0f16696e8c450d08d76e02e7 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 29 Oct 2018 01:03:36 +0530 Subject: [PATCH 38/89] Fix cli doc --- docs/user/cli.rst | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/user/cli.rst b/docs/user/cli.rst index 0dd677c..384b985 100644 --- a/docs/user/cli.rst +++ b/docs/user/cli.rst @@ -9,28 +9,28 @@ You can print the help for the interface by typing ``camelot --help`` in your fa :: -Usage: camelot [OPTIONS] COMMAND [ARGS]... + Usage: camelot [OPTIONS] COMMAND [ARGS]... - Camelot: PDF Table Extraction for Humans + Camelot: PDF Table Extraction for Humans -Options: - --version Show the version and exit. - -p, --pages TEXT Comma-separated page numbers. Example: 1,3,4 - or 1,4-end. - -pw, --password TEXT Password for decryption. - -o, --output TEXT Output file path. - -f, --format [csv|json|excel|html] - Output file format. - -z, --zip Create ZIP archive. - -split, --split_text Split text that spans across multiple cells. - -flag, --flag_size Flag text based on font size. Useful to - detect super/subscripts. - -M, --margins ... - PDFMiner char_margin, line_margin and - word_margin. - -q, --quiet Suppress warnings. - --help Show this message and exit. + Options: + --version Show the version and exit. + -p, --pages TEXT Comma-separated page numbers. Example: 1,3,4 + or 1,4-end. + -pw, --password TEXT Password for decryption. + -o, --output TEXT Output file path. + -f, --format [csv|json|excel|html] + Output file format. + -z, --zip Create ZIP archive. + -split, --split_text Split text that spans across multiple cells. + -flag, --flag_size Flag text based on font size. Useful to + detect super/subscripts. + -M, --margins ... + PDFMiner char_margin, line_margin and + word_margin. + -q, --quiet Suppress warnings. + --help Show this message and exit. -Commands: - lattice Use lines between text to parse the table. - stream Use spaces between text to parse the table. + Commands: + lattice Use lines between text to parse the table. + stream Use spaces between text to parse the table. From e8af4c2c1c6a894fdf48905e6aee0f775540f295 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 30 Oct 2018 23:36:31 +0530 Subject: [PATCH 39/89] Update conda install instructions --- README.md | 10 +--------- docs/user/install.rst | 9 ++------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5ad0426..6f3edaa 100644 --- a/README.md +++ b/README.md @@ -63,16 +63,8 @@ See [comparison with other PDF table extraction libraries and tools](https://git The easiest way to install Camelot is to install it with [conda](https://conda.io/docs/), which is the package manager that the [Anaconda](http://docs.continuum.io/anaconda/) distribution is built upon. -First, let's add the [conda-forge](https://conda-forge.org/) channel to conda's config: -

-$ conda config --add channels conda-forge
-
- -Now, you can simply use conda to install Camelot: - -
-$ conda install -c camelot-dev camelot-py
+$ conda install -c conda-forge camelot-py
 
### Using pip diff --git a/docs/user/install.rst b/docs/user/install.rst index 4d011ca..8d1762c 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -9,14 +9,9 @@ Using conda ----------- The easiest way to install Camelot is to install it with `conda`_, which is the package manager that the `Anaconda`_ distribution is built upon. +:: -First, let's add the `conda-forge`_ channel to conda's config:: - - $ conda config --add channels conda-forge - -Now, you can simply use conda to install Camelot:: - - $ conda install -c camelot-dev camelot-py + $ conda install -c conda-forge camelot-py .. note:: Camelot is available for Python 2.7, 3.5 and 3.6 on Linux, macOS and Windows. For Windows, you will need to install ghostscript which you can get from their `downloads page`_. From 29f22ad1a69ded04ec5199c8d86d55c7687f6ab6 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 30 Oct 2018 23:48:56 +0530 Subject: [PATCH 40/89] Update conda definition --- README.md | 2 +- docs/user/install.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6f3edaa..6a6128f 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ See [comparison with other PDF table extraction libraries and tools](https://git ### Using conda -The easiest way to install Camelot is to install it with [conda](https://conda.io/docs/), which is the package manager that the [Anaconda](http://docs.continuum.io/anaconda/) distribution is built upon. +The easiest way to install Camelot is to install it with [conda](https://conda.io/docs/), which is a package manager and environment management system for the [Anaconda](http://docs.continuum.io/anaconda/) distribution.
 $ conda install -c conda-forge camelot-py
diff --git a/docs/user/install.rst b/docs/user/install.rst
index 8d1762c..2c71d39 100644
--- a/docs/user/install.rst
+++ b/docs/user/install.rst
@@ -8,7 +8,7 @@ This part of the documentation covers how to install Camelot.
 Using conda
 -----------
 
-The easiest way to install Camelot is to install it with `conda`_, which is the package manager that the `Anaconda`_ distribution is built upon.
+The easiest way to install Camelot is to install it with `conda`_, which is a package manager and environment management system for the `Anaconda`_ distribution.
 ::
 
     $ conda install -c conda-forge camelot-py

From 79db6e3d1b2607e257ea4536a726ef78c1148ded Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Wed, 31 Oct 2018 17:33:40 +0530
Subject: [PATCH 41/89] Add gitter badge

---
 README.md      | 2 +-
 docs/index.rst | 3 +++
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 6a6128f..132cd78 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 [![Build Status](https://travis-ci.org/socialcopsdev/camelot.svg?branch=master)](https://travis-ci.org/socialcopsdev/camelot) [![Documentation Status](https://readthedocs.org/projects/camelot-py/badge/?version=master)](https://camelot-py.readthedocs.io/en/master/)
  [![codecov.io](https://codecov.io/github/socialcopsdev/camelot/badge.svg?branch=master&service=github)](https://codecov.io/github/socialcopsdev/camelot?branch=master)
- [![image](https://img.shields.io/pypi/v/camelot-py.svg)](https://pypi.org/project/camelot-py/) [![image](https://img.shields.io/pypi/l/camelot-py.svg)](https://pypi.org/project/camelot-py/) [![image](https://img.shields.io/pypi/pyversions/camelot-py.svg)](https://pypi.org/project/camelot-py/)
+ [![image](https://img.shields.io/pypi/v/camelot-py.svg)](https://pypi.org/project/camelot-py/) [![image](https://img.shields.io/pypi/l/camelot-py.svg)](https://pypi.org/project/camelot-py/) [![image](https://img.shields.io/pypi/pyversions/camelot-py.svg)](https://pypi.org/project/camelot-py/) [![Gitter chat](https://badges.gitter.im/camelot-dev/Lobby.png)](https://gitter.im/camelot-dev/Lobby)
 
 **Camelot** is a Python library that makes it easy for *anyone* to extract tables from PDF files!
 
diff --git a/docs/index.rst b/docs/index.rst
index 52350c8..2d69510 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -27,6 +27,9 @@ Release v\ |version|. (:ref:`Installation `)
 .. image:: https://img.shields.io/pypi/pyversions/camelot-py.svg
     :target: https://pypi.org/project/camelot-py/
 
+.. image:: https://badges.gitter.im/camelot-dev/Lobby.png
+    :target: https://gitter.im/camelot-dev/Lobby
+
 **Camelot** is a Python library that makes it easy for *anyone* to extract tables from PDF files!
 
 .. note:: You can also check out `Excalibur`_, which is a web interface for Camelot!

From c0e923516448889fd6e2899f577042e5d08336fe Mon Sep 17 00:00:00 2001
From: Suyash Behera 
Date: Fri, 2 Nov 2018 20:57:02 +0530
Subject: [PATCH 42/89] [MRG + 1] Create a new figure and test each plot type
 #127 (#179)

* [MRG] Create a new figure and test each plot type #127

 - move `plot()` to `plotting.py` as `plot_pdf()`
 - modify plotting functions to return matplotlib figures
 - add `test_plotting.py` and baseline images
 - import `plot_pdf()` in `__init__`
 - update `cli.py` to use `plot_pdf()`
 - update advanced usage docs to reflect changes

* Change matplotlib backend for image comparison tests

* Update plotting and tests
 - use matplotlib rectangle instead of `cv2.rectangle` in
`plot_contour()`
 - set matplotlib backend in `tests/__init__`
 - update contour plot baseline image
 - update `test_plotting` with more checks

* Update plot tests and config
 - remove unnecessary asserts
 - update setup.cfg and makefile with `--mpl`

* Add  to

* Add tolerance

* remove text from baseline plots
update plot tests with `remove_text`

* Change method name, update docs and add pep8

* Update docs
---
 Makefile                                      |   4 +-
 camelot/__init__.py                           |   1 +
 camelot/cli.py                                |  12 +-
 camelot/core.py                               |  29 ----
 camelot/handlers.py                           |   3 -
 camelot/plotting.py                           | 125 ++++++++++++++----
 docs/user/advanced.rst                        |  29 ++--
 setup.cfg                                     |   4 +-
 setup.py                                      |   3 +-
 tests/__init__.py                             |   2 +
 .../baseline_plots/test_contour_plot.png      | Bin 0 -> 34094 bytes
 .../files/baseline_plots/test_joint_plot.png  | Bin 0 -> 35743 bytes
 tests/files/baseline_plots/test_line_plot.png | Bin 0 -> 6808 bytes
 .../files/baseline_plots/test_table_plot.png  | Bin 0 -> 8440 bytes
 tests/files/baseline_plots/test_text_plot.png | Bin 0 -> 8992 bytes
 tests/test_plotting.py                        |  51 +++++++
 16 files changed, 186 insertions(+), 77 deletions(-)
 create mode 100644 tests/files/baseline_plots/test_contour_plot.png
 create mode 100644 tests/files/baseline_plots/test_joint_plot.png
 create mode 100644 tests/files/baseline_plots/test_line_plot.png
 create mode 100644 tests/files/baseline_plots/test_table_plot.png
 create mode 100644 tests/files/baseline_plots/test_text_plot.png
 create mode 100644 tests/test_plotting.py

diff --git a/Makefile b/Makefile
index a4bea7d..d0b54b0 100644
--- a/Makefile
+++ b/Makefile
@@ -15,7 +15,7 @@ install:
 	pip install ".[dev]"
 
 test:
-	pytest --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot tests
+	pytest --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot --mpl tests
 
 docs:
 	cd docs && make html
@@ -25,4 +25,4 @@ publish:
 	pip install twine
 	python setup.py sdist
 	twine upload dist/*
-	rm -fr build dist .egg camelot_py.egg-info
\ No newline at end of file
+	rm -fr build dist .egg camelot_py.egg-info
diff --git a/camelot/__init__.py b/camelot/__init__.py
index 364cd72..d8a41b9 100644
--- a/camelot/__init__.py
+++ b/camelot/__init__.py
@@ -6,6 +6,7 @@ from click import HelpFormatter
 
 from .__version__ import __version__
 from .io import read_pdf
+from .plotting import plot
 
 
 def _write_usage(self, prog, args='', prefix='Usage: '):
diff --git a/camelot/cli.py b/camelot/cli.py
index e30b204..8d67df7 100644
--- a/camelot/cli.py
+++ b/camelot/cli.py
@@ -3,9 +3,11 @@
 import logging
 
 import click
+import matplotlib.pyplot as plt
 
 from . import __version__
 from .io import read_pdf
+from .plotting import plot
 
 
 logger = logging.getLogger('camelot')
@@ -81,7 +83,7 @@ def cli(ctx, *args, **kwargs):
               help='Number of times for erosion/dilation will be applied.')
 @click.option('-plot', '--plot_type',
               type=click.Choice(['text', 'table', 'contour', 'joint', 'line']),
-              help='Plot geometry found on PDF page, for debugging.')
+              help='Plot elements found on PDF page for visual debugging.')
 @click.argument('filepath', type=click.Path(exists=True))
 @pass_config
 def lattice(c, *args, **kwargs):
@@ -107,7 +109,8 @@ def lattice(c, *args, **kwargs):
     click.echo('Found {} tables'.format(tables.n))
     if plot_type is not None:
         for table in tables:
-            table.plot(plot_type)
+            plot(table, plot_type=plot_type)
+            plt.show()
     else:
         if output is None:
             raise click.UsageError('Please specify output file path using --output')
@@ -128,7 +131,7 @@ def lattice(c, *args, **kwargs):
               ' used to combine text horizontally, to generate columns.')
 @click.option('-plot', '--plot_type',
               type=click.Choice(['text', 'table']),
-              help='Plot geometry found on PDF page for debugging.')
+              help='Plot elements found on PDF page for visual debugging.')
 @click.argument('filepath', type=click.Path(exists=True))
 @pass_config
 def stream(c, *args, **kwargs):
@@ -153,7 +156,8 @@ def stream(c, *args, **kwargs):
     click.echo('Found {} tables'.format(tables.n))
     if plot_type is not None:
         for table in tables:
-            table.plot(plot_type)
+            plot(table, plot_type=plot_type)
+            plt.show()
     else:
         if output is None:
             raise click.UsageError('Please specify output file path using --output')
diff --git a/camelot/core.py b/camelot/core.py
index d6eb3d7..45b316b 100644
--- a/camelot/core.py
+++ b/camelot/core.py
@@ -7,8 +7,6 @@ import tempfile
 import numpy as np
 import pandas as pd
 
-from .plotting import *
-
 
 class Cell(object):
     """Defines a cell in a table with coordinates relative to a
@@ -321,33 +319,6 @@ class Table(object):
                     cell.hspan = True
         return self
 
-    def plot(self, geometry_type):
-        """Plot geometry found on PDF page based on geometry_type
-        specified, useful for debugging and playing with different
-        parameters to get the best output.
-
-        Parameters
-        ----------
-        geometry_type : str
-            The geometry type for which a plot should be generated.
-            Can be 'text', 'table', 'contour', 'joint', 'line'
-
-        """
-        if self.flavor == 'stream' and geometry_type in ['contour', 'joint', 'line']:
-            raise NotImplementedError("{} cannot be plotted with flavor='stream'".format(
-                                       geometry_type))
-
-        if geometry_type == 'text':
-            plot_text(self._text)
-        elif geometry_type == 'table':
-            plot_table(self)
-        elif geometry_type == 'contour':
-            plot_contour(self._image)
-        elif geometry_type == 'joint':
-            plot_joint(self._image)
-        elif geometry_type == 'line':
-            plot_line(self._segments)
-
     def to_csv(self, path, **kwargs):
         """Writes Table to a comma-separated values (csv) file.
 
diff --git a/camelot/handlers.py b/camelot/handlers.py
index b6dc65c..47070a1 100644
--- a/camelot/handlers.py
+++ b/camelot/handlers.py
@@ -141,9 +141,6 @@ class PDFHandler(object):
         -------
         tables : camelot.core.TableList
             List of tables found in PDF.
-        geometry : camelot.core.GeometryList
-            List of geometry objects (contours, lines, joints) found
-            in PDF.
 
         """
         tables = []
diff --git a/camelot/plotting.py b/camelot/plotting.py
index bef06f2..73d5b37 100644
--- a/camelot/plotting.py
+++ b/camelot/plotting.py
@@ -1,15 +1,59 @@
-import cv2
+# -*- coding: utf-8 -*-
+
 import matplotlib.pyplot as plt
 import matplotlib.patches as patches
 
 
+def plot(table, plot_type='text', filepath=None):
+    """Plot elements found on PDF page based on plot_type
+    specified, useful for debugging and playing with different
+    parameters to get the best output.
+
+    Parameters
+    ----------
+    table: Table
+        A Camelot Table.
+    plot_type : str, optional (default: 'text')
+        {'text', 'table', 'contour', 'joint', 'line'}
+        The element type for which a plot should be generated.
+    filepath: str, optional (default: None)
+        Absolute path for saving the generated plot.
+
+    Returns
+    -------
+    fig : matplotlib.fig.Figure
+
+    """
+    if table.flavor == 'stream' and plot_type in ['contour', 'joint', 'line']:
+        raise NotImplementedError("{} cannot be plotted with flavor='stream'".format(
+                                    plot_type))
+    if plot_type == 'text':
+        fig = plot_text(table._text)
+    elif plot_type == 'table':
+        fig = plot_table(table)
+    elif plot_type == 'contour':
+        fig = plot_contour(table._image)
+    elif plot_type == 'joint':
+        fig = plot_joint(table._image)
+    elif plot_type == 'line':
+        fig = plot_line(table._segments)
+    if filepath:
+        plt.savefig(filepath)
+    return fig
+
+
 def plot_text(text):
-    """Generates a plot for all text present on the PDF page.
+    """Generates a plot for all text elements present
+    on the PDF page.
 
     Parameters
     ----------
     text : list
 
+    Returns
+    -------
+    fig : matplotlib.fig.Figure
+
     """
     fig = plt.figure()
     ax = fig.add_subplot(111, aspect='equal')
@@ -26,83 +70,116 @@ def plot_text(text):
         )
     ax.set_xlim(min(xs) - 10, max(xs) + 10)
     ax.set_ylim(min(ys) - 10, max(ys) + 10)
-    plt.show()
+    return fig
 
 
 def plot_table(table):
-    """Generates a plot for the table.
+    """Generates a plot for the detected tables
+    on the PDF page.
 
     Parameters
     ----------
     table : camelot.core.Table
 
+    Returns
+    -------
+    fig : matplotlib.fig.Figure
+
     """
+    fig = plt.figure()
+    ax = fig.add_subplot(111, aspect='equal')
     for row in table.cells:
         for cell in row:
             if cell.left:
-                plt.plot([cell.lb[0], cell.lt[0]],
+                ax.plot([cell.lb[0], cell.lt[0]],
                          [cell.lb[1], cell.lt[1]])
             if cell.right:
-                plt.plot([cell.rb[0], cell.rt[0]],
+                ax.plot([cell.rb[0], cell.rt[0]],
                          [cell.rb[1], cell.rt[1]])
             if cell.top:
-                plt.plot([cell.lt[0], cell.rt[0]],
+                ax.plot([cell.lt[0], cell.rt[0]],
                          [cell.lt[1], cell.rt[1]])
             if cell.bottom:
-                plt.plot([cell.lb[0], cell.rb[0]],
+                ax.plot([cell.lb[0], cell.rb[0]],
                          [cell.lb[1], cell.rb[1]])
-    plt.show()
+    return fig
 
 
 def plot_contour(image):
-    """Generates a plot for all table boundaries present on the
-    PDF page.
+    """Generates a plot for all table boundaries present
+    on the PDF page.
 
     Parameters
     ----------
     image : tuple
 
+    Returns
+    -------
+    fig : matplotlib.fig.Figure
+
     """
     img, table_bbox = image
+    fig = plt.figure()
+    ax = fig.add_subplot(111, aspect='equal')
     for t in table_bbox.keys():
-        cv2.rectangle(img, (t[0], t[1]),
-                      (t[2], t[3]), (255, 0, 0), 20)
-    plt.imshow(img)
-    plt.show()
+        ax.add_patch(
+            patches.Rectangle(
+                (t[0], t[1]),
+                t[2] - t[0],
+                t[3] - t[1],
+                fill=None,
+                edgecolor='red'
+            )
+        )
+    ax.imshow(img)
+    return fig
 
 
 def plot_joint(image):
-    """Generates a plot for all line intersections present on the
-    PDF page.
+    """Generates a plot for all line intersections present
+    on the PDF page.
 
     Parameters
     ----------
     image : tuple
 
+    Returns
+    -------
+    fig : matplotlib.fig.Figure
+
     """
     img, table_bbox = image
+    fig = plt.figure()
+    ax = fig.add_subplot(111, aspect='equal')
     x_coord = []
     y_coord = []
     for k in table_bbox.keys():
         for coord in table_bbox[k]:
             x_coord.append(coord[0])
             y_coord.append(coord[1])
-    plt.plot(x_coord, y_coord, 'ro')
-    plt.imshow(img)
-    plt.show()
+    ax.plot(x_coord, y_coord, 'ro')
+    ax.imshow(img)
+    return fig
 
 
 def plot_line(segments):
-    """Generates a plot for all line segments present on the PDF page.
+    """Generates a plot for all line segments present
+    on the PDF page.
 
     Parameters
     ----------
     segments : tuple
 
+    Returns
+    -------
+    fig : matplotlib.fig.Figure
+
     """
+    fig = plt.figure()
+    ax = fig.add_subplot(111, aspect='equal')
     vertical, horizontal = segments
     for v in vertical:
-        plt.plot([v[0], v[2]], [v[1], v[3]])
+        ax.plot([v[0], v[2]], [v[1], v[3]])
     for h in horizontal:
-        plt.plot([h[0], h[2]], [h[1], h[3]])
-    plt.show()
+        ax.plot([h[0], h[2]], [h[1], h[3]])
+    return fig
diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst
index e697949..7d6b349 100644
--- a/docs/user/advanced.rst
+++ b/docs/user/advanced.rst
@@ -27,12 +27,12 @@ To process background lines, you can pass ``process_background=True``.
 .. csv-table::
   :file: ../_static/csv/background_lines.csv
 
-Plot geometry
--------------
+Visual debugging
+----------------
 
-You can use a :class:`table ` object's :meth:`plot() ` method to plot various geometries that were detected by Camelot while processing the PDF page. This can help you select table areas, column separators and debug bad table outputs, by tweaking different configuration parameters.
+You can use the :meth:`plot() ` method to generate a `matplotlib `_ plot of various elements that were detected on the PDF page while processing it. This can help you select table areas, column separators and debug bad table outputs, by tweaking different configuration parameters.
 
-The following geometries are available for plotting. You can pass them to the :meth:`plot() ` method, which will then generate a `matplotlib `_ plot for the passed geometry.
+You can specify the type of element you want to plot using the ``plot_type`` keyword argument. The generated plot can be saved to a file by passing a ``filename`` keyword argument. The following plot types are supported:
 
 - 'text'
 - 'table'
@@ -40,9 +40,9 @@ The following geometries are available for plotting. You can pass them to the :m
 - 'line'
 - 'joint'
 
-.. note:: The last three geometries can only be used with :ref:`Lattice `, i.e. when ``flavor='lattice'``.
+.. note:: The last three plot types can only be used with :ref:`Lattice `, i.e. when ``flavor='lattice'``.
 
-Let's generate a plot for each geometry using this `PDF <../_static/pdf/foo.pdf>`__ as an example. First, let's get all the tables out.
+Let's generate a plot for each type using this `PDF <../_static/pdf/foo.pdf>`__ as an example. First, let's get all the tables out.
 
 ::
 
@@ -59,7 +59,8 @@ Let's plot all the text present on the table's PDF page.
 
 ::
 
-    >>> tables[0].plot('text')
+    >>> camelot.plot(tables[0], plot_type='text')
+    >>> plt.show()
 
 .. figure:: ../_static/png/geometry_text.png
     :height: 674
@@ -77,11 +78,12 @@ This, as we shall later see, is very helpful with :ref:`Stream ` for not
 table
 ^^^^^
 
-Let's plot the table (to see if it was detected correctly or not). This geometry type, along with contour, line and joint is useful for debugging and improving the extraction output, in case the table wasn't detected correctly. (More on that later.)
+Let's plot the table (to see if it was detected correctly or not). This plot type, along with contour, line and joint is useful for debugging and improving the extraction output, in case the table wasn't detected correctly. (More on that later.)
 
 ::
 
-    >>> tables[0].plot('table')
+    >>> camelot.plot(tables[0], plot_type='table')
+    >>> plt.show()
 
 .. figure:: ../_static/png/geometry_table.png
     :height: 674
@@ -101,7 +103,8 @@ Now, let's plot all table boundaries present on the table's PDF page.
 
 ::
 
-    >>> tables[0].plot('contour')
+    >>> camelot.plot(tables[0], plot_type='contour')
+    >>> plt.show()
 
 .. figure:: ../_static/png/geometry_contour.png
     :height: 674
@@ -119,7 +122,8 @@ Cool, let's plot all line segments present on the table's PDF page.
 
 ::
 
-    >>> tables[0].plot('line')
+    >>> camelot.plot(tables[0], plot_type='line')
+    >>> plt.show()
 
 .. figure:: ../_static/png/geometry_line.png
     :height: 674
@@ -137,7 +141,8 @@ Finally, let's plot all line intersections present on the table's PDF page.
 
 ::
 
-    >>> tables[0].plot('joint')
+    >>> camelot.plot(tables[0], plot_type='joint')
+    >>> plt.show()
 
 .. figure:: ../_static/png/geometry_joint.png
     :height: 674
diff --git a/setup.cfg b/setup.cfg
index 1b48058..1a59858 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,5 +2,5 @@
 test=pytest
 
 [tool:pytest]
-addopts = --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot tests
-python_files = tests/test_*.py
\ No newline at end of file
+addopts = --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot --mpl tests
+python_files = tests/test_*.py
diff --git a/setup.py b/setup.py
index e727706..8a1bcf6 100644
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,8 @@ dev_requires = [
     'pytest>=3.8.0',
     'pytest-cov>=2.6.0',
     'pytest-runner>=4.2',
-    'Sphinx>=1.7.9'
+    'Sphinx>=1.7.9',
+    'pytest-mpl>=0.10'
 ]
 dev_requires = dev_requires + all_requires
 
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29..a946ff7 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,2 @@
+import matplotlib
+matplotlib.use('agg')
diff --git a/tests/files/baseline_plots/test_contour_plot.png b/tests/files/baseline_plots/test_contour_plot.png
new file mode 100644
index 0000000000000000000000000000000000000000..57b39620e37c4fe51e1396af240658aa73955494
GIT binary patch
literal 34094
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�aY!gywdD3bdpKO(l<4#_x
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
CtK;JU

literal 0
HcmV?d00001

diff --git a/tests/files/baseline_plots/test_joint_plot.png b/tests/files/baseline_plots/test_joint_plot.png
new file mode 100644
index 0000000000000000000000000000000000000000..934aa74d906668165a184f247e7d5edda48aba83
GIT binary patch
literal 35743
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�aY!gywdD3bdpKO(l<4#_x
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
B`A`4=

literal 0
HcmV?d00001

diff --git a/tests/files/baseline_plots/test_table_plot.png b/tests/files/baseline_plots/test_table_plot.png
new file mode 100644
index 0000000000000000000000000000000000000000..d60f69f3e88878eb196022b97f7ed81ea1d67e95
GIT binary patch
literal 8440
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

literal 0
HcmV?d00001

diff --git a/tests/files/baseline_plots/test_text_plot.png b/tests/files/baseline_plots/test_text_plot.png
new file mode 100644
index 0000000000000000000000000000000000000000..8cc382525d84c90dda0fda3503a15c01c51c7e12
GIT binary patch
literal 8992
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
RFF*{mZL^&f&4PI9e*gqul|29e

literal 0
HcmV?d00001

diff --git a/tests/test_plotting.py b/tests/test_plotting.py
new file mode 100644
index 0000000..e01cac6
--- /dev/null
+++ b/tests/test_plotting.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+
+import os
+
+import pytest
+
+import camelot
+
+
+testdir = os.path.dirname(os.path.abspath(__file__))
+testdir = os.path.join(testdir, "files")
+
+
+@pytest.mark.mpl_image_compare(
+    baseline_dir="files/baseline_plots", remove_text=True)
+def test_text_plot():
+    filename = os.path.join(testdir, "foo.pdf")
+    tables = camelot.read_pdf(filename)
+    return camelot.plot(tables[0], plot_type='text')
+
+
+@pytest.mark.mpl_image_compare(
+    baseline_dir="files/baseline_plots", remove_text=True)
+def test_table_plot():
+    filename = os.path.join(testdir, "foo.pdf")
+    tables = camelot.read_pdf(filename)
+    return camelot.plot(tables[0], plot_type='table')
+
+
+@pytest.mark.mpl_image_compare(
+    baseline_dir="files/baseline_plots", remove_text=True)
+def test_contour_plot():
+    filename = os.path.join(testdir, "foo.pdf")
+    tables = camelot.read_pdf(filename)
+    return camelot.plot(tables[0], plot_type='contour')
+
+
+@pytest.mark.mpl_image_compare(
+    baseline_dir="files/baseline_plots", remove_text=True)
+def test_line_plot():
+    filename = os.path.join(testdir, "foo.pdf")
+    tables = camelot.read_pdf(filename)
+    return camelot.plot(tables[0], plot_type='line')
+
+
+@pytest.mark.mpl_image_compare(
+    baseline_dir="files/baseline_plots", remove_text=True)
+def test_joint_plot():
+    filename = os.path.join(testdir, "foo.pdf")
+    tables = camelot.read_pdf(filename)
+    return camelot.plot(tables[0], plot_type='joint')

From db3f8c689765bddf725f319e173c10402d5f312d Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Fri, 2 Nov 2018 23:16:03 +0530
Subject: [PATCH 43/89] [MRG] Make matplotlib optional (#190)

* Rename png files

* Convert plot to PlotMethods class and update docs

* Update test

* Update setup.py and docs

* Refactor PlotMethods

* Make matplotlib optional

* Raise ImportError in cli
---
 README.md                                     |   4 +-
 camelot/__init__.py                           |   5 +-
 camelot/cli.py                                |  49 +--
 camelot/plotting.py                           | 304 +++++++++---------
 ...{geometry_contour.png => plot_contour.png} | Bin
 .../{geometry_joint.png => plot_joint.png}    | Bin
 .../png/{geometry_line.png => plot_line.png}  | Bin
 .../{geometry_table.png => plot_table.png}    | Bin
 .../png/{geometry_text.png => plot_text.png}  | Bin
 docs/user/advanced.rst                        |  54 ++--
 docs/user/how-it-works.rst                    |  10 +-
 docs/user/install.rst                         |   6 +-
 setup.py                                      |  17 +-
 ...test_table_plot.png => test_grid_plot.png} | Bin
 tests/test_plotting.py                        |  12 +-
 15 files changed, 236 insertions(+), 225 deletions(-)
 rename docs/_static/png/{geometry_contour.png => plot_contour.png} (100%)
 rename docs/_static/png/{geometry_joint.png => plot_joint.png} (100%)
 rename docs/_static/png/{geometry_line.png => plot_line.png} (100%)
 rename docs/_static/png/{geometry_table.png => plot_table.png} (100%)
 rename docs/_static/png/{geometry_text.png => plot_text.png} (100%)
 rename tests/files/baseline_plots/{test_table_plot.png => test_grid_plot.png} (100%)

diff --git a/README.md b/README.md
index 132cd78..93b7215 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@ $ conda install -c conda-forge camelot-py
 After [installing the dependencies](https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip) ([tk](https://packages.ubuntu.com/trusty/python-tk) and [ghostscript](https://www.ghostscript.com/)), you can simply use pip to install Camelot:
 
 
-$ pip install camelot-py[all]
+$ pip install camelot-py[cv]
 
### From the source code @@ -87,7 +87,7 @@ and install Camelot using pip:
 $ cd camelot
-$ pip install ".[all]"
+$ pip install ".[cv]"
 
## Documentation diff --git a/camelot/__init__.py b/camelot/__init__.py index d8a41b9..68815f2 100644 --- a/camelot/__init__.py +++ b/camelot/__init__.py @@ -6,7 +6,7 @@ from click import HelpFormatter from .__version__ import __version__ from .io import read_pdf -from .plotting import plot +from .plotting import PlotMethods def _write_usage(self, prog, args='', prefix='Usage: '): @@ -26,3 +26,6 @@ handler = logging.StreamHandler() handler.setFormatter(formatter) logger.addHandler(handler) + +# instantiate plot method +plot = PlotMethods() diff --git a/camelot/cli.py b/camelot/cli.py index 8d67df7..eaae955 100644 --- a/camelot/cli.py +++ b/camelot/cli.py @@ -3,11 +3,14 @@ import logging import click -import matplotlib.pyplot as plt +try: + import matplotlib.pyplot as plt +except ImportError: + _HAS_MPL = False +else: + _HAS_MPL = True -from . import __version__ -from .io import read_pdf -from .plotting import plot +from . import __version__, read_pdf, plot logger = logging.getLogger('camelot') @@ -82,7 +85,7 @@ def cli(ctx, *args, **kwargs): @click.option('-I', '--iterations', default=0, help='Number of times for erosion/dilation will be applied.') @click.option('-plot', '--plot_type', - type=click.Choice(['text', 'table', 'contour', 'joint', 'line']), + type=click.Choice(['text', 'grid', 'contour', 'joint', 'line']), help='Plot elements found on PDF page for visual debugging.') @click.argument('filepath', type=click.Path(exists=True)) @pass_config @@ -104,18 +107,23 @@ def lattice(c, *args, **kwargs): kwargs['copy_text'] = None if not copy_text else copy_text kwargs['shift_text'] = list(kwargs['shift_text']) - tables = read_pdf(filepath, pages=pages, flavor='lattice', - suppress_warnings=suppress_warnings, **kwargs) - click.echo('Found {} tables'.format(tables.n)) if plot_type is not None: - for table in tables: - plot(table, plot_type=plot_type) - plt.show() + if not _HAS_MPL: + raise ImportError('matplotlib is required for plotting.') else: if output is None: raise click.UsageError('Please specify output file path using --output') if f is None: raise click.UsageError('Please specify output file format using --format') + + tables = read_pdf(filepath, pages=pages, flavor='lattice', + suppress_warnings=suppress_warnings, **kwargs) + click.echo('Found {} tables'.format(tables.n)) + if plot_type is not None: + for table in tables: + plot(table, kind=plot_type) + plt.show() + else: tables.export(output, f=f, compress=compress) @@ -130,7 +138,7 @@ def lattice(c, *args, **kwargs): @click.option('-c', '--col_close_tol', default=0, help='Tolerance parameter' ' used to combine text horizontally, to generate columns.') @click.option('-plot', '--plot_type', - type=click.Choice(['text', 'table']), + type=click.Choice(['text', 'grid']), help='Plot elements found on PDF page for visual debugging.') @click.argument('filepath', type=click.Path(exists=True)) @pass_config @@ -151,16 +159,21 @@ def stream(c, *args, **kwargs): columns = list(kwargs['columns']) kwargs['columns'] = None if not columns else columns - tables = read_pdf(filepath, pages=pages, flavor='stream', - suppress_warnings=suppress_warnings, **kwargs) - click.echo('Found {} tables'.format(tables.n)) if plot_type is not None: - for table in tables: - plot(table, plot_type=plot_type) - plt.show() + if not _HAS_MPL: + raise ImportError('matplotlib is required for plotting.') else: if output is None: raise click.UsageError('Please specify output file path using --output') if f is None: raise click.UsageError('Please specify output file format using --format') + + tables = read_pdf(filepath, pages=pages, flavor='stream', + suppress_warnings=suppress_warnings, **kwargs) + click.echo('Found {} tables'.format(tables.n)) + if plot_type is not None: + for table in tables: + plot(table, kind=plot_type) + plt.show() + else: tables.export(output, f=f, compress=compress) diff --git a/camelot/plotting.py b/camelot/plotting.py index 73d5b37..3b91cee 100644 --- a/camelot/plotting.py +++ b/camelot/plotting.py @@ -1,185 +1,179 @@ # -*- coding: utf-8 -*- -import matplotlib.pyplot as plt -import matplotlib.patches as patches +try: + import matplotlib.pyplot as plt + import matplotlib.patches as patches +except ImportError: + _HAS_MPL = False +else: + _HAS_MPL = True -def plot(table, plot_type='text', filepath=None): - """Plot elements found on PDF page based on plot_type - specified, useful for debugging and playing with different - parameters to get the best output. +class PlotMethods(object): + def __call__(self, table, kind='text', filename=None): + """Plot elements found on PDF page based on kind + specified, useful for debugging and playing with different + parameters to get the best output. - Parameters - ---------- - table: Table - A Camelot Table. - plot_type : str, optional (default: 'text') - {'text', 'table', 'contour', 'joint', 'line'} - The element type for which a plot should be generated. - filepath: str, optional (default: None) - Absolute path for saving the generated plot. + Parameters + ---------- + table: camelot.core.Table + A Camelot Table. + kind : str, optional (default: 'text') + {'text', 'grid', 'contour', 'joint', 'line'} + The element type for which a plot should be generated. + filepath: str, optional (default: None) + Absolute path for saving the generated plot. - Returns - ------- - fig : matplotlib.fig.Figure + Returns + ------- + fig : matplotlib.fig.Figure - """ - if table.flavor == 'stream' and plot_type in ['contour', 'joint', 'line']: - raise NotImplementedError("{} cannot be plotted with flavor='stream'".format( - plot_type)) - if plot_type == 'text': - fig = plot_text(table._text) - elif plot_type == 'table': - fig = plot_table(table) - elif plot_type == 'contour': - fig = plot_contour(table._image) - elif plot_type == 'joint': - fig = plot_joint(table._image) - elif plot_type == 'line': - fig = plot_line(table._segments) - if filepath: - plt.savefig(filepath) - return fig + """ + if not _HAS_MPL: + raise ImportError('matplotlib is required for plotting.') + if table.flavor == 'stream' and kind in ['contour', 'joint', 'line']: + raise NotImplementedError("Stream flavor does not support kind='{}'".format( + kind)) -def plot_text(text): - """Generates a plot for all text elements present - on the PDF page. + plot_method = getattr(self, kind) + return plot_method(table) - Parameters - ---------- - text : list + def text(self, table): + """Generates a plot for all text elements present + on the PDF page. - Returns - ------- - fig : matplotlib.fig.Figure + Parameters + ---------- + table : camelot.core.Table - """ - fig = plt.figure() - ax = fig.add_subplot(111, aspect='equal') - xs, ys = [], [] - for t in text: - xs.extend([t[0], t[2]]) - ys.extend([t[1], t[3]]) - ax.add_patch( - patches.Rectangle( - (t[0], t[1]), - t[2] - t[0], - t[3] - t[1] + Returns + ------- + fig : matplotlib.fig.Figure + + """ + fig = plt.figure() + ax = fig.add_subplot(111, aspect='equal') + xs, ys = [], [] + for t in table._text: + xs.extend([t[0], t[2]]) + ys.extend([t[1], t[3]]) + ax.add_patch( + patches.Rectangle( + (t[0], t[1]), + t[2] - t[0], + t[3] - t[1] + ) ) - ) - ax.set_xlim(min(xs) - 10, max(xs) + 10) - ax.set_ylim(min(ys) - 10, max(ys) + 10) - return fig + ax.set_xlim(min(xs) - 10, max(xs) + 10) + ax.set_ylim(min(ys) - 10, max(ys) + 10) + return fig + def grid(self, table): + """Generates a plot for the detected table grids + on the PDF page. -def plot_table(table): - """Generates a plot for the detected tables - on the PDF page. + Parameters + ---------- + table : camelot.core.Table - Parameters - ---------- - table : camelot.core.Table + Returns + ------- + fig : matplotlib.fig.Figure - Returns - ------- - fig : matplotlib.fig.Figure + """ + fig = plt.figure() + ax = fig.add_subplot(111, aspect='equal') + for row in table.cells: + for cell in row: + if cell.left: + ax.plot([cell.lb[0], cell.lt[0]], + [cell.lb[1], cell.lt[1]]) + if cell.right: + ax.plot([cell.rb[0], cell.rt[0]], + [cell.rb[1], cell.rt[1]]) + if cell.top: + ax.plot([cell.lt[0], cell.rt[0]], + [cell.lt[1], cell.rt[1]]) + if cell.bottom: + ax.plot([cell.lb[0], cell.rb[0]], + [cell.lb[1], cell.rb[1]]) + return fig - """ - fig = plt.figure() - ax = fig.add_subplot(111, aspect='equal') - for row in table.cells: - for cell in row: - if cell.left: - ax.plot([cell.lb[0], cell.lt[0]], - [cell.lb[1], cell.lt[1]]) - if cell.right: - ax.plot([cell.rb[0], cell.rt[0]], - [cell.rb[1], cell.rt[1]]) - if cell.top: - ax.plot([cell.lt[0], cell.rt[0]], - [cell.lt[1], cell.rt[1]]) - if cell.bottom: - ax.plot([cell.lb[0], cell.rb[0]], - [cell.lb[1], cell.rb[1]]) - return fig + def contour(self, table): + """Generates a plot for all table boundaries present + on the PDF page. + Parameters + ---------- + table : camelot.core.Table -def plot_contour(image): - """Generates a plot for all table boundaries present - on the PDF page. + Returns + ------- + fig : matplotlib.fig.Figure - Parameters - ---------- - image : tuple - - Returns - ------- - fig : matplotlib.fig.Figure - - """ - img, table_bbox = image - fig = plt.figure() - ax = fig.add_subplot(111, aspect='equal') - for t in table_bbox.keys(): - ax.add_patch( - patches.Rectangle( - (t[0], t[1]), - t[2] - t[0], - t[3] - t[1], - fill=None, - edgecolor='red' + """ + img, table_bbox = table._image + fig = plt.figure() + ax = fig.add_subplot(111, aspect='equal') + for t in table_bbox.keys(): + ax.add_patch( + patches.Rectangle( + (t[0], t[1]), + t[2] - t[0], + t[3] - t[1], + fill=None, + edgecolor='red' + ) ) - ) - ax.imshow(img) - return fig + ax.imshow(img) + return fig + def joint(self, table): + """Generates a plot for all line intersections present + on the PDF page. -def plot_joint(image): - """Generates a plot for all line intersections present - on the PDF page. + Parameters + ---------- + table : camelot.core.Table - Parameters - ---------- - image : tuple + Returns + ------- + fig : matplotlib.fig.Figure - Returns - ------- - fig : matplotlib.fig.Figure + """ + img, table_bbox = table._image + fig = plt.figure() + ax = fig.add_subplot(111, aspect='equal') + x_coord = [] + y_coord = [] + for k in table_bbox.keys(): + for coord in table_bbox[k]: + x_coord.append(coord[0]) + y_coord.append(coord[1]) + ax.plot(x_coord, y_coord, 'ro') + ax.imshow(img) + return fig - """ - img, table_bbox = image - fig = plt.figure() - ax = fig.add_subplot(111, aspect='equal') - x_coord = [] - y_coord = [] - for k in table_bbox.keys(): - for coord in table_bbox[k]: - x_coord.append(coord[0]) - y_coord.append(coord[1]) - ax.plot(x_coord, y_coord, 'ro') - ax.imshow(img) - return fig + def line(self, table): + """Generates a plot for all line segments present + on the PDF page. + Parameters + ---------- + table : camelot.core.Table -def plot_line(segments): - """Generates a plot for all line segments present - on the PDF page. + Returns + ------- + fig : matplotlib.fig.Figure - Parameters - ---------- - segments : tuple - - Returns - ------- - fig : matplotlib.fig.Figure - - """ - fig = plt.figure() - ax = fig.add_subplot(111, aspect='equal') - vertical, horizontal = segments - for v in vertical: - ax.plot([v[0], v[2]], [v[1], v[3]]) - for h in horizontal: - ax.plot([h[0], h[2]], [h[1], h[3]]) - return fig + """ + fig = plt.figure() + ax = fig.add_subplot(111, aspect='equal') + vertical, horizontal = table._segments + for v in vertical: + ax.plot([v[0], v[2]], [v[1], v[3]]) + for h in horizontal: + ax.plot([h[0], h[2]], [h[1], h[3]]) + return fig diff --git a/docs/_static/png/geometry_contour.png b/docs/_static/png/plot_contour.png similarity index 100% rename from docs/_static/png/geometry_contour.png rename to docs/_static/png/plot_contour.png diff --git a/docs/_static/png/geometry_joint.png b/docs/_static/png/plot_joint.png similarity index 100% rename from docs/_static/png/geometry_joint.png rename to docs/_static/png/plot_joint.png diff --git a/docs/_static/png/geometry_line.png b/docs/_static/png/plot_line.png similarity index 100% rename from docs/_static/png/geometry_line.png rename to docs/_static/png/plot_line.png diff --git a/docs/_static/png/geometry_table.png b/docs/_static/png/plot_table.png similarity index 100% rename from docs/_static/png/geometry_table.png rename to docs/_static/png/plot_table.png diff --git a/docs/_static/png/geometry_text.png b/docs/_static/png/plot_text.png similarity index 100% rename from docs/_static/png/geometry_text.png rename to docs/_static/png/plot_text.png diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 7d6b349..4d5c4de 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -30,12 +30,14 @@ To process background lines, you can pass ``process_background=True``. Visual debugging ---------------- -You can use the :meth:`plot() ` method to generate a `matplotlib `_ plot of various elements that were detected on the PDF page while processing it. This can help you select table areas, column separators and debug bad table outputs, by tweaking different configuration parameters. +.. note:: Visual debugging using ``plot()`` requires `matplotlib `_ which is an optional dependency. You can install it using ``$ pip install camelot-py[plot]``. -You can specify the type of element you want to plot using the ``plot_type`` keyword argument. The generated plot can be saved to a file by passing a ``filename`` keyword argument. The following plot types are supported: +You can use the :class:`plot() ` method to generate a `matplotlib `_ plot of various elements that were detected on the PDF page while processing it. This can help you select table areas, column separators and debug bad table outputs, by tweaking different configuration parameters. + +You can specify the type of element you want to plot using the ``kind`` keyword argument. The generated plot can be saved to a file by passing a ``filename`` keyword argument. The following plot types are supported: - 'text' -- 'table' +- 'grid' - 'contour' - 'line' - 'joint' @@ -50,8 +52,6 @@ Let's generate a plot for each type using this `PDF <../_static/pdf/foo.pdf>`__ >>> tables -.. _geometry_text: - text ^^^^ @@ -59,10 +59,10 @@ Let's plot all the text present on the table's PDF page. :: - >>> camelot.plot(tables[0], plot_type='text') + >>> camelot.plot(tables[0], kind='text') >>> plt.show() -.. figure:: ../_static/png/geometry_text.png +.. figure:: ../_static/png/plot_text.png :height: 674 :width: 1366 :scale: 50% @@ -73,8 +73,6 @@ This, as we shall later see, is very helpful with :ref:`Stream ` for not .. note:: The *x-y* coordinates shown above change as you move your mouse cursor on the image, which can help you note coordinates. -.. _geometry_table: - table ^^^^^ @@ -82,10 +80,10 @@ Let's plot the table (to see if it was detected correctly or not). This plot typ :: - >>> camelot.plot(tables[0], plot_type='table') + >>> camelot.plot(tables[0], kind='table') >>> plt.show() -.. figure:: ../_static/png/geometry_table.png +.. figure:: ../_static/png/plot_table.png :height: 674 :width: 1366 :scale: 50% @@ -94,8 +92,6 @@ Let's plot the table (to see if it was detected correctly or not). This plot typ The table is perfect! -.. _geometry_contour: - contour ^^^^^^^ @@ -103,18 +99,16 @@ Now, let's plot all table boundaries present on the table's PDF page. :: - >>> camelot.plot(tables[0], plot_type='contour') + >>> camelot.plot(tables[0], kind='contour') >>> plt.show() -.. figure:: ../_static/png/geometry_contour.png +.. figure:: ../_static/png/plot_contour.png :height: 674 :width: 1366 :scale: 50% :alt: A plot of all contours on a PDF page :align: left -.. _geometry_line: - line ^^^^ @@ -122,18 +116,16 @@ Cool, let's plot all line segments present on the table's PDF page. :: - >>> camelot.plot(tables[0], plot_type='line') + >>> camelot.plot(tables[0], kind='line') >>> plt.show() -.. figure:: ../_static/png/geometry_line.png +.. figure:: ../_static/png/plot_line.png :height: 674 :width: 1366 :scale: 50% :alt: A plot of all lines on a PDF page :align: left -.. _geometry_joint: - joint ^^^^^ @@ -141,10 +133,10 @@ Finally, let's plot all line intersections present on the table's PDF page. :: - >>> camelot.plot(tables[0], plot_type='joint') + >>> camelot.plot(tables[0], kind='joint') >>> plt.show() -.. figure:: ../_static/png/geometry_joint.png +.. figure:: ../_static/png/plot_joint.png :height: 674 :width: 1366 :scale: 50% @@ -154,7 +146,7 @@ Finally, let's plot all line intersections present on the table's PDF page. Specify table areas ------------------- -Since :ref:`Stream ` treats the whole page as a table, `for now`_, it's useful to specify table boundaries in cases such as `these <../_static/pdf/table_areas.pdf>`__. You can :ref:`plot the text ` on this page and note the top left and bottom right coordinates of the table. +Since :ref:`Stream ` treats the whole page as a table, `for now`_, it's useful to specify table boundaries in cases such as `these <../_static/pdf/table_areas.pdf>`__. You can plot the text on this page and note the top left and bottom right coordinates of the table. Table areas that you want Camelot to analyze can be passed as a list of comma-separated strings to :meth:`read_pdf() `, using the ``table_areas`` keyword argument. @@ -171,7 +163,7 @@ Table areas that you want Camelot to analyze can be passed as a list of comma-se Specify column separators ------------------------- -In cases like `these <../_static/pdf/column_separators.pdf>`__, where the text is very close to each other, it is possible that Camelot may guess the column separators' coordinates incorrectly. To correct this, you can explicitly specify the *x* coordinate for each column separator by :ref:`plotting the text ` on the page. +In cases like `these <../_static/pdf/column_separators.pdf>`__, where the text is very close to each other, it is possible that Camelot may guess the column separators' coordinates incorrectly. To correct this, you can explicitly specify the *x* coordinate for each column separator by plotting the text on the page. You can pass the column separators as a list of comma-separated strings to :meth:`read_pdf() `, using the ``columns`` keyword argument. @@ -179,7 +171,7 @@ In case you passed a single column separators string list, and no table area is For example, if you have specified two table areas, ``table_areas=['12,23,43,54', '20,33,55,67']``, and only want to specify column separators for the first table, you can pass an empty string for the second table in the column separators' list like this, ``columns=['10,120,200,400', '']``. -Let's get back to the *x* coordinates we got from :ref:`plotting text ` that exists on this `PDF <../_static/pdf/column_separators.pdf>`__, and get the table out! +Let's get back to the *x* coordinates we got from plotting the text that exists on this `PDF <../_static/pdf/column_separators.pdf>`__, and get the table out! :: @@ -287,23 +279,25 @@ Here's a `PDF <../_static/pdf/short_lines.pdf>`__ where small lines separating t :alt: A PDF table with short lines :align: left -Let's :ref:`plot the table ` for this PDF. +Let's plot the table for this PDF. :: >>> tables = camelot.read_pdf('short_lines.pdf') - >>> tables[0].plot('table') + >>> camelot.plot(tables[0], kind='table') + >>> plt.show() .. figure:: ../_static/png/short_lines_1.png :alt: A plot of the PDF table with short lines :align: left -Clearly, the smaller lines separating the headers, couldn't be detected. Let's try with ``line_size_scaling=40``, and `plot the table `_ again. +Clearly, the smaller lines separating the headers, couldn't be detected. Let's try with ``line_size_scaling=40``, and plot the table again. :: >>> tables = camelot.read_pdf('short_lines.pdf', line_size_scaling=40) - >>> tables[0].plot('table') + >>> camelot.plot(tables[0], kind='table') + >>> plt.show() .. figure:: ../_static/png/short_lines_2.png :alt: An improved plot of the PDF table with short lines diff --git a/docs/user/how-it-works.rst b/docs/user/how-it-works.rst index 385b393..0783c60 100644 --- a/docs/user/how-it-works.rst +++ b/docs/user/how-it-works.rst @@ -39,7 +39,7 @@ Let's see how Lattice processes the second page of `this PDF`_, step-by-step. 1. Line segments are detected. -.. image:: ../_static/png/geometry_line.png +.. image:: ../_static/png/plot_line.png :height: 674 :width: 1366 :scale: 50% @@ -49,7 +49,7 @@ Let's see how Lattice processes the second page of `this PDF`_, step-by-step. .. _and: https://en.wikipedia.org/wiki/Logical_conjunction -.. image:: ../_static/png/geometry_joint.png +.. image:: ../_static/png/plot_joint.png :height: 674 :width: 1366 :scale: 50% @@ -59,7 +59,7 @@ Let's see how Lattice processes the second page of `this PDF`_, step-by-step. .. _or: https://en.wikipedia.org/wiki/Logical_disjunction -.. image:: ../_static/png/geometry_contour.png +.. image:: ../_static/png/plot_contour.png :height: 674 :width: 1366 :scale: 50% @@ -75,10 +75,10 @@ Let's see how Lattice processes the second page of `this PDF`_, step-by-step. 5. Spanning cells are detected using the line segments and line intersections. -.. image:: ../_static/png/geometry_table.png +.. image:: ../_static/png/plot_table.png :height: 674 :width: 1366 :scale: 50% :align: left -6. Finally, the words found on the page are assigned to the table's cells based on their *x* and *y* coordinates. \ No newline at end of file +6. Finally, the words found on the page are assigned to the table's cells based on their *x* and *y* coordinates. diff --git a/docs/user/install.rst b/docs/user/install.rst index 2c71d39..e28e546 100644 --- a/docs/user/install.rst +++ b/docs/user/install.rst @@ -95,7 +95,7 @@ If you have ghostscript, you should see the ghostscript version and copyright in Finally, you can use pip to install Camelot:: - $ pip install camelot-py[all] + $ pip install camelot-py[cv] From the source code -------------------- @@ -111,6 +111,6 @@ After `installing the dependencies`_, you can install from the source by: :: $ cd camelot - $ pip install ".[all]" + $ pip install ".[cv]" -.. _installing the dependencies: https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip \ No newline at end of file +.. _installing the dependencies: https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip diff --git a/setup.py b/setup.py index 8a1bcf6..417b460 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ with open('README.md', 'r') as f: requires = [ 'click>=6.7', - 'matplotlib>=2.2.3', 'numpy>=1.13.3', 'openpyxl>=2.5.8', 'pandas>=0.23.4', @@ -23,18 +22,24 @@ requires = [ 'PyPDF2>=1.26.0' ] -all_requires = [ +cv_requires = [ 'opencv-python>=3.4.2.17' ] +plot_requires = [ + 'matplotlib>=2.2.3', +] + dev_requires = [ 'codecov>=2.0.15', 'pytest>=3.8.0', 'pytest-cov>=2.6.0', + 'pytest-mpl>=0.10', 'pytest-runner>=4.2', - 'Sphinx>=1.7.9', - 'pytest-mpl>=0.10' + 'Sphinx>=1.7.9' ] + +all_requires = cv_requires + plot_requires dev_requires = dev_requires + all_requires @@ -52,7 +57,9 @@ def setup_package(): install_requires=requires, extras_require={ 'all': all_requires, - 'dev': dev_requires + 'cv': cv_requires, + 'dev': dev_requires, + 'plot': plot_requires }, entry_points={ 'console_scripts': [ diff --git a/tests/files/baseline_plots/test_table_plot.png b/tests/files/baseline_plots/test_grid_plot.png similarity index 100% rename from tests/files/baseline_plots/test_table_plot.png rename to tests/files/baseline_plots/test_grid_plot.png diff --git a/tests/test_plotting.py b/tests/test_plotting.py index e01cac6..eeea81a 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -16,15 +16,15 @@ testdir = os.path.join(testdir, "files") def test_text_plot(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) - return camelot.plot(tables[0], plot_type='text') + return camelot.plot(tables[0], kind='text') @pytest.mark.mpl_image_compare( baseline_dir="files/baseline_plots", remove_text=True) -def test_table_plot(): +def test_grid_plot(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) - return camelot.plot(tables[0], plot_type='table') + return camelot.plot(tables[0], kind='grid') @pytest.mark.mpl_image_compare( @@ -32,7 +32,7 @@ def test_table_plot(): def test_contour_plot(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) - return camelot.plot(tables[0], plot_type='contour') + return camelot.plot(tables[0], kind='contour') @pytest.mark.mpl_image_compare( @@ -40,7 +40,7 @@ def test_contour_plot(): def test_line_plot(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) - return camelot.plot(tables[0], plot_type='line') + return camelot.plot(tables[0], kind='line') @pytest.mark.mpl_image_compare( @@ -48,4 +48,4 @@ def test_line_plot(): def test_joint_plot(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) - return camelot.plot(tables[0], plot_type='joint') + return camelot.plot(tables[0], kind='joint') From 36006cadc5d0155d45db7a969921149cdaad2319 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 2 Nov 2018 23:25:07 +0530 Subject: [PATCH 44/89] Bump version and update HISTORY.md --- HISTORY.md | 9 +++++++++ camelot/__version__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 6c1bc42..b277008 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,15 @@ Release History master ------ +0.3.1 (2018-11-02) +------------------ + +**Improvements** + +* Matplotlib is now an optional requirement. [#190](https://github.com/socialcopsdev/camelot/pull/190) by Vinayak Mehta. + * You can install it using `$ pip install camelot-py[plot]`. +* [#127](https://github.com/socialcopsdev/camelot/issues/127) Add tests for plotting. Coverage is now at 87%! [#179](https://github.com/socialcopsdev/camelot/pull/179) by [Suyash Behera](https://github.com/Suyash458). + 0.3.0 (2018-10-28) ------------------ diff --git a/camelot/__version__.py b/camelot/__version__.py index 646976f..84b6f7e 100644 --- a/camelot/__version__.py +++ b/camelot/__version__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -VERSION = (0, 3, 0) +VERSION = (0, 3, 1) __title__ = 'camelot-py' __description__ = 'PDF Table Extraction for Humans.' From a60ce38d4d9f69ebd6d41e54c8131332a04df028 Mon Sep 17 00:00:00 2001 From: Palash Chatterjee Date: Sat, 3 Nov 2018 01:06:44 +0530 Subject: [PATCH 45/89] [MRG + 1] Fix the order of coordinates in docs (#191) --- docs/user/advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 4d5c4de..92dcc1e 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -169,7 +169,7 @@ You can pass the column separators as a list of comma-separated strings to :meth In case you passed a single column separators string list, and no table area is specified, the separators will be applied to the whole page. When a list of table areas is specified and you need to specify column separators as well, **the length of both lists should be equal**. Each table area will be mapped to each column separators' string using their indices. -For example, if you have specified two table areas, ``table_areas=['12,23,43,54', '20,33,55,67']``, and only want to specify column separators for the first table, you can pass an empty string for the second table in the column separators' list like this, ``columns=['10,120,200,400', '']``. +For example, if you have specified two table areas, ``table_areas=['12,54,43,23', '20,67,55,33']``, and only want to specify column separators for the first table, you can pass an empty string for the second table in the column separators' list like this, ``columns=['10,120,200,400', '']``. Let's get back to the *x* coordinates we got from plotting the text that exists on this `PDF <../_static/pdf/column_separators.pdf>`__, and get the table out! From defaead6790f5737c348bc1106e6797cf480cee5 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Nov 2018 01:33:41 +0530 Subject: [PATCH 46/89] Add table bbox attribute (#193) --- camelot/parsers/lattice.py | 1 + camelot/parsers/stream.py | 1 + 2 files changed, 2 insertions(+) diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index 7b7c411..22c77e8 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -362,6 +362,7 @@ class Lattice(BaseParser): self.table_bbox.keys(), key=lambda x: x[1], reverse=True)): cols, rows, v_s, h_s = self._generate_columns_and_rows(table_idx, tk) table = self._generate_table(table_idx, cols, rows, v_s=v_s, h_s=h_s) + table._bbox = tk _tables.append(table) return _tables diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 6aee966..709f01d 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -361,6 +361,7 @@ class Stream(BaseParser): self.table_bbox.keys(), key=lambda x: x[1], reverse=True)): cols, rows = self._generate_columns_and_rows(table_idx, tk) table = self._generate_table(table_idx, cols, rows) + table._bbox = tk _tables.append(table) return _tables From b310f16dba440cadc8f66c6487875ac9b1ad6bc4 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Nov 2018 01:37:27 +0530 Subject: [PATCH 47/89] Bump version and update HISTORY.md --- HISTORY.md | 8 ++++++++ camelot/__version__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index b277008..1c74e4d 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,14 @@ Release History master ------ +0.3.2 (2018-11-04) +------------------ + +**Improvements** + +* [#186](https://github.com/socialcopsdev/camelot/issues/186) Add `_bbox` attribute to table. [#193](https://github.com/socialcopsdev/camelot/pull/193) by Vinayak Mehta. + * You can use `table._bbox` to get coordinates of the detected table. + 0.3.1 (2018-11-02) ------------------ diff --git a/camelot/__version__.py b/camelot/__version__.py index 84b6f7e..22adbc4 100644 --- a/camelot/__version__.py +++ b/camelot/__version__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -VERSION = (0, 3, 1) +VERSION = (0, 3, 2) __title__ = 'camelot-py' __description__ = 'PDF Table Extraction for Humans.' From cd3aa38f7e253ed1289443f78972594876963972 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 6 Nov 2018 19:18:45 +0530 Subject: [PATCH 48/89] Change table to grid (#196) --- docs/user/advanced.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 92dcc1e..e26d22c 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -80,7 +80,7 @@ Let's plot the table (to see if it was detected correctly or not). This plot typ :: - >>> camelot.plot(tables[0], kind='table') + >>> camelot.plot(tables[0], kind='grid') >>> plt.show() .. figure:: ../_static/png/plot_table.png @@ -284,7 +284,7 @@ Let's plot the table for this PDF. :: >>> tables = camelot.read_pdf('short_lines.pdf') - >>> camelot.plot(tables[0], kind='table') + >>> camelot.plot(tables[0], kind='grid') >>> plt.show() .. figure:: ../_static/png/short_lines_1.png @@ -296,7 +296,7 @@ Clearly, the smaller lines separating the headers, couldn't be detected. Let's t :: >>> tables = camelot.read_pdf('short_lines.pdf', line_size_scaling=40) - >>> camelot.plot(tables[0], kind='table') + >>> camelot.plot(tables[0], kind='grid') >>> plt.show() .. figure:: ../_static/png/short_lines_2.png From 123227aa8c11238ae94c72ba0506f2d476d0ac57 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 22 Nov 2018 05:31:02 +0530 Subject: [PATCH 49/89] Add TextEdge and TextEdges helper classes --- camelot/__version__.py | 11 ++++- camelot/core.py | 93 +++++++++++++++++++++++++++++++++++++++ camelot/parsers/stream.py | 35 +++++++++++++-- 3 files changed, 134 insertions(+), 5 deletions(-) diff --git a/camelot/__version__.py b/camelot/__version__.py index 22adbc4..f19ff5e 100644 --- a/camelot/__version__.py +++ b/camelot/__version__.py @@ -1,11 +1,18 @@ # -*- coding: utf-8 -*- -VERSION = (0, 3, 2) +VERSION = (0, 4, 0) +PHASE = 'alpha' # alpha, beta or rc +PHASE_VERSION = '1' __title__ = 'camelot-py' __description__ = 'PDF Table Extraction for Humans.' __url__ = 'http://camelot-py.readthedocs.io/' -__version__ = '.'.join(map(str, VERSION)) +if PHASE: + __version__ = '{}-{}'.format('.'.join(map(str, VERSION)), PHASE) + if PHASE_VERSION: + __version__ = '{}.{}'.format(__version__, PHASE_VERSION) +else: + __version__ = '.'.join(map(str, VERSION)) __author__ = 'Vinayak Mehta' __author_email__ = 'vmehta94@gmail.com' __license__ = 'MIT License' diff --git a/camelot/core.py b/camelot/core.py index 45b316b..66d1c28 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -3,11 +3,104 @@ import os import zipfile import tempfile +from itertools import chain import numpy as np import pandas as pd +class TextEdge(object): + def __init__(self, x, y0, y1, align='left'): + self.x = x + self.y0 = y0 + self.y1 = y1 + self.align = align + self.intersections = 0 + self.is_valid = False + + def __repr__(self): + return ''.format( + round(self.x, 2), round(self.y0, 2), round(self.y1, 2), self.align, self.is_valid) + + def update_coords(self, x, y0): + self.x = (self.intersections * self.x + x) / float(self.intersections + 1) + self.y0 = y0 + self.intersections += 1 + # a textedge is valid if it extends uninterrupted over required_elements + if self.intersections > 4: + self.is_valid = True + + +class TextEdges(object): + def __init__(self): + self._textedges = {'left': [], 'middle': [], 'right': []} + + @staticmethod + def get_x_coord(textline, align): + x_left = textline.x0 + x_right = textline.x1 + x_middle = x_left + (x_right - x_left) / 2.0 + x_coord = {'left': x_left, 'middle': x_middle, 'right': x_right} + return x_coord[align] + + def add_textedge(self, textline, align): + x = self.get_x_coord(textline, align) + y0 = textline.y0 + y1 = textline.y1 + te = TextEdge(x, y0, y1, align=align) + self._textedges[align].append(te) + + def find_textedge(self, x_coord, align): + for i, te in enumerate(self._textedges[align]): + if np.isclose(te.x, x_coord): + return i + return None + + def update_textedges(self, textline): + for align in ['left', 'middle', 'right']: + x_coord = self.get_x_coord(textline, align) + idx = self.find_textedge(x_coord, align) + if idx is None: + print('adding') + self.add_textedge(textline, align) + else: + print('updating') + self._textedges[align][idx].update_coords(x_coord, textline.y0) + + def generate_textedges(self, textlines): + textlines_flat = list(chain.from_iterable(textlines)) + for tl in textlines_flat: + if len(tl.get_text().strip()) > 1: # TODO: hacky + self.update_textedges(tl) + + # # debug + # import matplotlib.pyplot as plt + + # fig = plt.figure() + # ax = fig.add_subplot(111, aspect='equal') + # for te in self._textedges['left']: + # if te.is_valid: + # ax.plot([te.x, te.x], [te.y0, te.y1]) + # plt.show() + + # fig = plt.figure() + # ax = fig.add_subplot(111, aspect='equal') + # for te in self._textedges['middle']: + # if te.is_valid: + # ax.plot([te.x, te.x], [te.y0, te.y1]) + # plt.show() + + # fig = plt.figure() + # ax = fig.add_subplot(111, aspect='equal') + # for te in self._textedges['right']: + # if te.is_valid: + # ax.plot([te.x, te.x], [te.y0, te.y1]) + # plt.show() + + def generate_tableareas(self): + return {} + + class Cell(object): """Defines a cell in a table with coordinates relative to a left-bottom origin. (PDF coordinate space) diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 709f01d..55ef7ca 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -9,7 +9,7 @@ import numpy as np import pandas as pd from .base import BaseParser -from ..core import Table +from ..core import TextEdges, Table from ..utils import (text_in_bbox, get_table_index, compute_accuracy, compute_whitespace) @@ -116,7 +116,7 @@ class Stream(BaseParser): row_y = t.y0 temp.append(t) rows.append(sorted(temp, key=lambda t: t.x0)) - __ = rows.pop(0) # hacky + __ = rows.pop(0) # TODO: hacky return rows @staticmethod @@ -246,6 +246,34 @@ class Stream(BaseParser): raise ValueError("Length of table_areas and columns" " should be equal") + def _nurminen_table_detection(self, textlines): + # an general heuristic implementation of the table detection + # algorithm described by Anssi Nurminen's master's thesis: + # https://dspace.cc.tut.fi/dpub/bitstream/handle/123456789/21520/Nurminen.pdf?sequence=3 + + # minimum number of textlines to be considered a textedge + REQUIRED_ELEMENTS_FOR_TEXTEDGE = 4 + # padding added to table area's lt and rb + TABLE_AREA_PADDING = 10 + + # TODO: add support for arabic text #141 + # sort textlines in reading order + textlines.sort(key=lambda x: (-x.y0, x.x0)) + # group textlines into rows + text_grouped = self._group_rows( + self.horizontal_text, row_close_tol=self.row_close_tol) + textedges = TextEdges() + # generate left, middle and right textedges + textedges.generate_textedges(text_grouped) + # select relevant edges + # generate table areas using relevant edges and horizontal text + table_bbox = textedges.generate_tableareas() + # treat whole page as table if not table areas found + if not len(table_bbox): + table_bbox = {(0, 0, self.pdf_width, self.pdf_height): None} + + return table_bbox + def _generate_table_bbox(self): if self.table_areas is not None: table_bbox = {} @@ -257,7 +285,8 @@ class Stream(BaseParser): y2 = float(y2) table_bbox[(x1, y2, x2, y1)] = None else: - table_bbox = {(0, 0, self.pdf_width, self.pdf_height): None} + # find tables based on nurminen's detection algorithm + table_bbox = self._nurminen_table_detection(self.horizontal_text) self.table_bbox = table_bbox def _generate_columns_and_rows(self, table_idx, tk): From 378408a271309d7a4034d08d21ffb0b81557adc2 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 22 Nov 2018 05:42:10 +0530 Subject: [PATCH 50/89] Remove debug statements --- camelot/core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/camelot/core.py b/camelot/core.py index 66d1c28..f50f77b 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -61,10 +61,8 @@ class TextEdges(object): x_coord = self.get_x_coord(textline, align) idx = self.find_textedge(x_coord, align) if idx is None: - print('adding') self.add_textedge(textline, align) else: - print('updating') self._textedges[align][idx].update_coords(x_coord, textline.y0) def generate_textedges(self, textlines): From a587ea3782a84b348fc455d3f8f0a3371f1b77e0 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 22 Nov 2018 18:24:31 +0530 Subject: [PATCH 51/89] Add get_relevant textedges method --- camelot/core.py | 77 +++++++++++++++++++++------------------ camelot/parsers/stream.py | 12 ++---- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/camelot/core.py b/camelot/core.py index f50f77b..9a9882d 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -4,11 +4,20 @@ import os import zipfile import tempfile from itertools import chain +from operator import itemgetter import numpy as np import pandas as pd +# minimum number of textlines to be considered a textedge +TEXTEDGE_REQUIRED_ELEMENTS = 4 +# y coordinate tolerance for extending text edge +TEXTEDGE_EXTEND_TOLERANCE = 50 +# padding added to table area's lt and rb +TABLE_AREA_PADDING = 10 + + class TextEdge(object): def __init__(self, x, y0, y1, align='left'): self.x = x @@ -23,12 +32,13 @@ class TextEdge(object): round(self.x, 2), round(self.y0, 2), round(self.y1, 2), self.align, self.is_valid) def update_coords(self, x, y0): - self.x = (self.intersections * self.x + x) / float(self.intersections + 1) - self.y0 = y0 - self.intersections += 1 - # a textedge is valid if it extends uninterrupted over required_elements - if self.intersections > 4: - self.is_valid = True + if np.isclose(self.y0, y0, atol=TEXTEDGE_EXTEND_TOLERANCE): + self.x = (self.intersections * self.x + x) / float(self.intersections + 1) + self.y0 = y0 + self.intersections += 1 + # a textedge is valid if it extends uninterrupted over required_elements + if self.intersections > TEXTEDGE_REQUIRED_ELEMENTS: + self.is_valid = True class TextEdges(object): @@ -43,59 +53,56 @@ class TextEdges(object): x_coord = {'left': x_left, 'middle': x_middle, 'right': x_right} return x_coord[align] - def add_textedge(self, textline, align): + def find(self, x_coord, align): + for i, te in enumerate(self._textedges[align]): + if np.isclose(te.x, x_coord): + return i + return None + + def add(self, textline, align): x = self.get_x_coord(textline, align) y0 = textline.y0 y1 = textline.y1 te = TextEdge(x, y0, y1, align=align) self._textedges[align].append(te) - def find_textedge(self, x_coord, align): - for i, te in enumerate(self._textedges[align]): - if np.isclose(te.x, x_coord): - return i - return None - - def update_textedges(self, textline): - for align in ['left', 'middle', 'right']: + def update(self, textline): + for align in ['left', 'right', 'middle']: x_coord = self.get_x_coord(textline, align) - idx = self.find_textedge(x_coord, align) + idx = self.find(x_coord, align) if idx is None: - self.add_textedge(textline, align) + self.add(textline, align) else: self._textedges[align][idx].update_coords(x_coord, textline.y0) - def generate_textedges(self, textlines): + def generate(self, textlines): textlines_flat = list(chain.from_iterable(textlines)) for tl in textlines_flat: if len(tl.get_text().strip()) > 1: # TODO: hacky - self.update_textedges(tl) + self.update(tl) + def get_relevant(self): + intersections_sum = { + 'left': sum(te.intersections for te in self._textedges['left']), + 'right': sum(te.intersections for te in self._textedges['right']), + 'middle': sum(te.intersections for te in self._textedges['middle']) + } + + # TODO: naive + relevant_align = max(intersections_sum.items(), key=itemgetter(1))[0] + return self._textedges[relevant_align] + + def get_table_areas(self, relevant_textedges): # # debug # import matplotlib.pyplot as plt # fig = plt.figure() # ax = fig.add_subplot(111, aspect='equal') - # for te in self._textedges['left']: + # for te in relevant_textedges: # if te.is_valid: # ax.plot([te.x, te.x], [te.y0, te.y1]) # plt.show() - # fig = plt.figure() - # ax = fig.add_subplot(111, aspect='equal') - # for te in self._textedges['middle']: - # if te.is_valid: - # ax.plot([te.x, te.x], [te.y0, te.y1]) - # plt.show() - - # fig = plt.figure() - # ax = fig.add_subplot(111, aspect='equal') - # for te in self._textedges['right']: - # if te.is_valid: - # ax.plot([te.x, te.x], [te.y0, te.y1]) - # plt.show() - - def generate_tableareas(self): return {} diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 55ef7ca..982b5f6 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -251,11 +251,6 @@ class Stream(BaseParser): # algorithm described by Anssi Nurminen's master's thesis: # https://dspace.cc.tut.fi/dpub/bitstream/handle/123456789/21520/Nurminen.pdf?sequence=3 - # minimum number of textlines to be considered a textedge - REQUIRED_ELEMENTS_FOR_TEXTEDGE = 4 - # padding added to table area's lt and rb - TABLE_AREA_PADDING = 10 - # TODO: add support for arabic text #141 # sort textlines in reading order textlines.sort(key=lambda x: (-x.y0, x.x0)) @@ -264,10 +259,11 @@ class Stream(BaseParser): self.horizontal_text, row_close_tol=self.row_close_tol) textedges = TextEdges() # generate left, middle and right textedges - textedges.generate_textedges(text_grouped) + textedges.generate(text_grouped) # select relevant edges - # generate table areas using relevant edges and horizontal text - table_bbox = textedges.generate_tableareas() + relevant_textedges = textedges.get_relevant() + # guess table areas using relevant edges + table_bbox = textedges.get_table_areas(relevant_textedges) # treat whole page as table if not table areas found if not len(table_bbox): table_bbox = {(0, 0, self.pdf_width, self.pdf_height): None} From 4e2aee18c33fceafe77379af95c9b486d604d3e2 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 22 Nov 2018 19:48:51 +0530 Subject: [PATCH 52/89] Add get_table_areas textedges method --- camelot/core.py | 72 ++++++++++++++++++++++++++++++++------- camelot/parsers/stream.py | 3 +- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/camelot/core.py b/camelot/core.py index 9a9882d..c8051dc 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -83,27 +83,73 @@ class TextEdges(object): def get_relevant(self): intersections_sum = { - 'left': sum(te.intersections for te in self._textedges['left']), - 'right': sum(te.intersections for te in self._textedges['right']), - 'middle': sum(te.intersections for te in self._textedges['middle']) + 'left': sum(te.intersections for te in self._textedges['left'] if te.is_valid), + 'right': sum(te.intersections for te in self._textedges['right'] if te.is_valid), + 'middle': sum(te.intersections for te in self._textedges['middle'] if te.is_valid) } # TODO: naive + # get the vertical textedges that intersect maximum number of + # times with horizontal text rows relevant_align = max(intersections_sum.items(), key=itemgetter(1))[0] return self._textedges[relevant_align] - def get_table_areas(self, relevant_textedges): - # # debug - # import matplotlib.pyplot as plt + def get_table_areas(self, textlines, relevant_textedges): + def pad(area): + x0 = area[0] - TABLE_AREA_PADDING + y0 = area[1] - TABLE_AREA_PADDING + x1 = area[2] + TABLE_AREA_PADDING + y1 = area[3] + TABLE_AREA_PADDING + return (x0, y0, x1, y1) - # fig = plt.figure() - # ax = fig.add_subplot(111, aspect='equal') - # for te in relevant_textedges: - # if te.is_valid: - # ax.plot([te.x, te.x], [te.y0, te.y1]) - # plt.show() + # sort relevant textedges in reading order + relevant_textedges.sort(key=lambda te: (-te.y0, te.x)) - return {} + table_areas = {} + for te in relevant_textedges: + if te.is_valid: + if not table_areas: + table_areas[(te.x, te.y0, te.x, te.y1)] = None + else: + found = None + for area in table_areas: + # check for overlap + if te.y1 >= area[1] and te.y0 <= area[3]: + found = area + break + if found is None: + table_areas[(te.x, te.y0, te.x, te.y1)] = None + else: + table_areas.pop(found) + updated_area = ( + found[0], min(te.y0, found[1]), max(found[2], te.x), max(found[3], te.y1)) + table_areas[updated_area] = None + + # extend table areas based on textlines that overlap + # vertically. it's possible that these textlines were + # eliminated during textedges generation since numbers and + # sentences/chars are often aligned differently. + # drawback: table areas that have paragraphs to their left + # will include the paragraphs too. + for tl in textlines: + for area in table_areas: + found = None + # check for overlap + if tl.y0 >= area[1] and tl.y1 <= area[3]: + found = area + break + if found is not None: + table_areas.pop(found) + updated_area = ( + min(tl.x0, found[0]), min(tl.y0, found[1]), max(found[2], tl.x1), max(found[3], tl.y1)) + table_areas[updated_area] = None + + # add some padding to table areas + table_areas_padded = {} + for area in table_areas: + table_areas_padded[pad(area)] = None + + return table_areas_padded class Cell(object): diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 982b5f6..2aa5fc4 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -250,6 +250,7 @@ class Stream(BaseParser): # an general heuristic implementation of the table detection # algorithm described by Anssi Nurminen's master's thesis: # https://dspace.cc.tut.fi/dpub/bitstream/handle/123456789/21520/Nurminen.pdf?sequence=3 + # assumes that tables vertically separated by some distance # TODO: add support for arabic text #141 # sort textlines in reading order @@ -263,7 +264,7 @@ class Stream(BaseParser): # select relevant edges relevant_textedges = textedges.get_relevant() # guess table areas using relevant edges - table_bbox = textedges.get_table_areas(relevant_textedges) + table_bbox = textedges.get_table_areas(textlines, relevant_textedges) # treat whole page as table if not table areas found if not len(table_bbox): table_bbox = {(0, 0, self.pdf_width, self.pdf_height): None} From 529914eb6f3bf387dfc64a96dd99197cbd6065d1 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 22 Nov 2018 19:50:59 +0530 Subject: [PATCH 53/89] Update comment --- camelot/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camelot/core.py b/camelot/core.py index c8051dc..276840c 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -129,7 +129,7 @@ class TextEdges(object): # vertically. it's possible that these textlines were # eliminated during textedges generation since numbers and # sentences/chars are often aligned differently. - # drawback: table areas that have paragraphs to their left + # drawback: table areas that have paragraphs to their sides # will include the paragraphs too. for tl in textlines: for area in table_areas: From bcde67fe179e57a04a58aa824ee0a514885792b2 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 22 Nov 2018 19:56:16 +0530 Subject: [PATCH 54/89] Add constant to include table headers --- camelot/core.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/camelot/core.py b/camelot/core.py index 276840c..2173774 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -99,7 +99,9 @@ class TextEdges(object): x0 = area[0] - TABLE_AREA_PADDING y0 = area[1] - TABLE_AREA_PADDING x1 = area[2] + TABLE_AREA_PADDING - y1 = area[3] + TABLE_AREA_PADDING + # TODO: deal in percentages instead of absolutes + # add a constant to include table headers + y1 = area[3] + TABLE_AREA_PADDING + 10 return (x0, y0, x1, y1) # sort relevant textedges in reading order @@ -149,6 +151,41 @@ class TextEdges(object): for area in table_areas: table_areas_padded[pad(area)] = None + # debug + import matplotlib.pyplot as plt + import matplotlib.patches as patches + + fig = plt.figure() + ax = fig.add_subplot(111, aspect='equal') + xs, ys = [], [] + for t in textlines: + xs.extend([t.x0, t.x1]) + ys.extend([t.y0, t.y1]) + ax.add_patch( + patches.Rectangle( + (t.x0, t.y0), + t.x1 - t.x0, + t.y1 - t.y0, + color='blue' + ) + ) + for area in table_areas_padded: + xs.extend([area[0], area[2]]) + ys.extend([area[1], area[3]]) + ax.add_patch( + patches.Rectangle( + (area[0], area[1]), + area[2] - area[0], + area[3] - area[1], + fill=False, + color='red' + ) + ) + + ax.set_xlim(min(xs) - 10, max(xs) + 10) + ax.set_ylim(min(ys) - 10, max(ys) + 10) + plt.show() + return table_areas_padded From 9b5782f9ba58eb52955ebc949aecfa56b78fed14 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 22 Nov 2018 20:05:30 +0530 Subject: [PATCH 55/89] Fix indent --- camelot/core.py | 45 +++++---------------------------------------- 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/camelot/core.py b/camelot/core.py index 2173774..e9e6e0f 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -140,52 +140,17 @@ class TextEdges(object): if tl.y0 >= area[1] and tl.y1 <= area[3]: found = area break - if found is not None: - table_areas.pop(found) - updated_area = ( - min(tl.x0, found[0]), min(tl.y0, found[1]), max(found[2], tl.x1), max(found[3], tl.y1)) - table_areas[updated_area] = None + if found is not None: + table_areas.pop(found) + updated_area = ( + min(tl.x0, found[0]), min(tl.y0, found[1]), max(found[2], tl.x1), max(found[3], tl.y1)) + table_areas[updated_area] = None # add some padding to table areas table_areas_padded = {} for area in table_areas: table_areas_padded[pad(area)] = None - # debug - import matplotlib.pyplot as plt - import matplotlib.patches as patches - - fig = plt.figure() - ax = fig.add_subplot(111, aspect='equal') - xs, ys = [], [] - for t in textlines: - xs.extend([t.x0, t.x1]) - ys.extend([t.y0, t.y1]) - ax.add_patch( - patches.Rectangle( - (t.x0, t.y0), - t.x1 - t.x0, - t.y1 - t.y0, - color='blue' - ) - ) - for area in table_areas_padded: - xs.extend([area[0], area[2]]) - ys.extend([area[1], area[3]]) - ax.add_patch( - patches.Rectangle( - (area[0], area[1]), - area[2] - area[0], - area[3] - area[1], - fill=False, - color='red' - ) - ) - - ax.set_xlim(min(xs) - 10, max(xs) + 10) - ax.set_ylim(min(ys) - 10, max(ys) + 10) - plt.show() - return table_areas_padded From 9b67b271e48a896ce5822b2421d600abf781cf02 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 23 Nov 2018 02:44:55 +0530 Subject: [PATCH 56/89] Add atol and fix variable declaration --- camelot/core.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/camelot/core.py b/camelot/core.py index e9e6e0f..44aff2b 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -55,7 +55,7 @@ class TextEdges(object): def find(self, x_coord, align): for i, te in enumerate(self._textedges[align]): - if np.isclose(te.x, x_coord): + if np.isclose(te.x, x_coord, atol=0.5): return i return None @@ -134,17 +134,17 @@ class TextEdges(object): # drawback: table areas that have paragraphs to their sides # will include the paragraphs too. for tl in textlines: + found = None for area in table_areas: - found = None # check for overlap if tl.y0 >= area[1] and tl.y1 <= area[3]: found = area break - if found is not None: - table_areas.pop(found) - updated_area = ( - min(tl.x0, found[0]), min(tl.y0, found[1]), max(found[2], tl.x1), max(found[3], tl.y1)) - table_areas[updated_area] = None + if found is not None: + table_areas.pop(found) + updated_area = ( + min(tl.x0, found[0]), min(tl.y0, found[1]), max(found[2], tl.x1), max(found[3], tl.y1)) + table_areas[updated_area] = None # add some padding to table areas table_areas_padded = {} From a1e1fd781d7cdf39707f825a2851ff376e9ff5dd Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 23 Nov 2018 02:51:22 +0530 Subject: [PATCH 57/89] Fix comments --- camelot/core.py | 17 ++++++++++------- camelot/parsers/stream.py | 8 ++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/camelot/core.py b/camelot/core.py index 44aff2b..e0687d2 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -10,10 +10,12 @@ import numpy as np import pandas as pd -# minimum number of textlines to be considered a textedge +# minimum number of vertical textline intersections for a textedge +# to be considered valid TEXTEDGE_REQUIRED_ELEMENTS = 4 -# y coordinate tolerance for extending text edge +# y coordinate tolerance for extending textedge TEXTEDGE_EXTEND_TOLERANCE = 50 +# TODO: deal in percentages instead of absolutes # padding added to table area's lt and rb TABLE_AREA_PADDING = 10 @@ -36,7 +38,8 @@ class TextEdge(object): self.x = (self.intersections * self.x + x) / float(self.intersections + 1) self.y0 = y0 self.intersections += 1 - # a textedge is valid if it extends uninterrupted over required_elements + # a textedge is valid only if it extends uninterrupted + # over a required number of textlines if self.intersections > TEXTEDGE_REQUIRED_ELEMENTS: self.is_valid = True @@ -89,8 +92,8 @@ class TextEdges(object): } # TODO: naive - # get the vertical textedges that intersect maximum number of - # times with horizontal text rows + # get vertical textedges that intersect maximum number of + # times with horizontal textlines relevant_align = max(intersections_sum.items(), key=itemgetter(1))[0] return self._textedges[relevant_align] @@ -130,8 +133,8 @@ class TextEdges(object): # extend table areas based on textlines that overlap # vertically. it's possible that these textlines were # eliminated during textedges generation since numbers and - # sentences/chars are often aligned differently. - # drawback: table areas that have paragraphs to their sides + # chars/words/sentences are often aligned differently. + # drawback: table areas that have paragraphs on their sides # will include the paragraphs too. for tl in textlines: found = None diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 2aa5fc4..8f86dbd 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -247,10 +247,10 @@ class Stream(BaseParser): " should be equal") def _nurminen_table_detection(self, textlines): - # an general heuristic implementation of the table detection + # a general heuristic implementation of the table detection # algorithm described by Anssi Nurminen's master's thesis: # https://dspace.cc.tut.fi/dpub/bitstream/handle/123456789/21520/Nurminen.pdf?sequence=3 - # assumes that tables vertically separated by some distance + # assumes that tables are situated relatively apart vertically # TODO: add support for arabic text #141 # sort textlines in reading order @@ -263,9 +263,9 @@ class Stream(BaseParser): textedges.generate(text_grouped) # select relevant edges relevant_textedges = textedges.get_relevant() - # guess table areas using relevant edges + # guess table areas using textlines and relevant edges table_bbox = textedges.get_table_areas(textlines, relevant_textedges) - # treat whole page as table if not table areas found + # treat whole page as table area if no table areas found if not len(table_bbox): table_bbox = {(0, 0, self.pdf_width, self.pdf_height): None} From 0251422e339b568512c236f47e35c03515c06191 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 23 Nov 2018 03:27:23 +0530 Subject: [PATCH 58/89] Add fix to include table headers --- camelot/core.py | 18 +++++++++--------- camelot/parsers/stream.py | 5 +---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/camelot/core.py b/camelot/core.py index e0687d2..cc0b5a3 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -15,8 +15,7 @@ import pandas as pd TEXTEDGE_REQUIRED_ELEMENTS = 4 # y coordinate tolerance for extending textedge TEXTEDGE_EXTEND_TOLERANCE = 50 -# TODO: deal in percentages instead of absolutes -# padding added to table area's lt and rb +# padding added to table area on the left, right and bottom TABLE_AREA_PADDING = 10 @@ -79,8 +78,7 @@ class TextEdges(object): self._textedges[align][idx].update_coords(x_coord, textline.y0) def generate(self, textlines): - textlines_flat = list(chain.from_iterable(textlines)) - for tl in textlines_flat: + for tl in textlines: if len(tl.get_text().strip()) > 1: # TODO: hacky self.update(tl) @@ -98,13 +96,12 @@ class TextEdges(object): return self._textedges[relevant_align] def get_table_areas(self, textlines, relevant_textedges): - def pad(area): + def pad(area, average_row_height): x0 = area[0] - TABLE_AREA_PADDING y0 = area[1] - TABLE_AREA_PADDING x1 = area[2] + TABLE_AREA_PADDING - # TODO: deal in percentages instead of absolutes - # add a constant to include table headers - y1 = area[3] + TABLE_AREA_PADDING + 10 + # add a constant since table headers can be relatively up + y1 = area[3] + average_row_height * 5 return (x0, y0, x1, y1) # sort relevant textedges in reading order @@ -136,7 +133,9 @@ class TextEdges(object): # chars/words/sentences are often aligned differently. # drawback: table areas that have paragraphs on their sides # will include the paragraphs too. + sum_textline_height = 0 for tl in textlines: + sum_textline_height += tl.y1 - tl.y0 found = None for area in table_areas: # check for overlap @@ -148,11 +147,12 @@ class TextEdges(object): updated_area = ( min(tl.x0, found[0]), min(tl.y0, found[1]), max(found[2], tl.x1), max(found[3], tl.y1)) table_areas[updated_area] = None + average_textline_height = sum_textline_height / float(len(textlines)) # add some padding to table areas table_areas_padded = {} for area in table_areas: - table_areas_padded[pad(area)] = None + table_areas_padded[pad(area, average_textline_height)] = None return table_areas_padded diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 8f86dbd..79073ac 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -255,12 +255,9 @@ class Stream(BaseParser): # TODO: add support for arabic text #141 # sort textlines in reading order textlines.sort(key=lambda x: (-x.y0, x.x0)) - # group textlines into rows - text_grouped = self._group_rows( - self.horizontal_text, row_close_tol=self.row_close_tol) textedges = TextEdges() # generate left, middle and right textedges - textedges.generate(text_grouped) + textedges.generate(textlines) # select relevant edges relevant_textedges = textedges.get_relevant() # guess table areas using textlines and relevant edges From bf894116d2a7d28424fe0799307c36be3b4beb63 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 23 Nov 2018 04:25:04 +0530 Subject: [PATCH 59/89] Update test data --- tests/data.py | 85 ++++++++++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 49 deletions(-) diff --git a/tests/data.py b/tests/data.py index 00e070a..c75a588 100755 --- a/tests/data.py +++ b/tests/data.py @@ -33,52 +33,45 @@ data_stream = [ ["Nagaland", "2,368,724", "204,329", "226,400", "0", "2,799,453", "783,054", "3,582,507"], ["Odisha", "14,317,179", "2,552,292", "1,107,250", "0", "17,976,721", "451,438", "18,428,159"], ["Puducherry", "4,191,757", "52,249", "192,400", "0", "4,436,406", "2,173", "4,438,579"], - ["Punjab", "19,775,485", "2,208,343", "2,470,882", "0", "24,454,710", "1,436,522", "25,891,232"], - ["", "Health Sector Financing by Centre and States/UTs in India [2009-10 to 2012-13](Revised) P a g e |23", "", "", "", "", "", ""] + ["Punjab", "19,775,485", "2,208,343", "2,470,882", "0", "24,454,710", "1,436,522", "25,891,232"] ] data_stream_table_rotated = [ - ["", "", "Table 21 Current use of contraception by background characteristics\u2014Continued", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], - ["", "", "", "", "", "", "Modern method", "", "", "", "", "", "", "Traditional method", "", "", "", ""], - ["", "", "", "Any", "", "", "", "", "", "", "Other", "Any", "", "", "", "Not", "", "Number"], - ["", "", "Any", "modern", "Female", "Male", "", "", "", "Condom/", "modern", "traditional", "", "With-", "Folk", "currently", "", "of"], - ["", "Background characteristic", "method", "method", "sterilization", "sterilization", "Pill", "IUD", "Injectables", "Nirodh", "method", "method", "Rhythm", "drawal", "method", "using", "Total", "women"], - ["", "Caste/tribe", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], - ["", "Scheduled caste", "74.8", "55.8", "42.9", "0.9", "9.7", "0.0", "0.2", "2.2", "0.0", "19.0", "11.2", "7.4", "0.4", "25.2", "100.0", "1,363"], - ["", "Scheduled tribe", "59.3", "39.0", "26.8", "0.6", "6.4", "0.6", "1.2", "3.5", "0.0", "20.3", "10.4", "5.8", "4.1", "40.7", "100.0", "256"], - ["", "Other backward class", "71.4", "51.1", "34.9", "0.0", "8.6", "1.4", "0.0", "6.2", "0.0", "20.4", "12.6", "7.8", "0.0", "28.6", "100.0", "211"], - ["", "Other", "71.1", "48.8", "28.2", "0.8", "13.3", "0.9", "0.3", "5.2", "0.1", "22.3", "12.9", "9.1", "0.3", "28.9", "100.0", "3,319"], - ["", "Wealth index", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], - ["", "Lowest", "64.5", "48.6", "34.3", "0.5", "10.5", "0.6", "0.7", "2.0", "0.0", "15.9", "9.9", "4.6", "1.4", "35.5", "100.0", "1,258"], - ["", "Second", "68.5", "50.4", "36.2", "1.1", "11.4", "0.5", "0.1", "1.1", "0.0", "18.1", "11.2", "6.7", "0.2", "31.5", "100.0", "1,317"], - ["", "Middle", "75.5", "52.8", "33.6", "0.6", "14.2", "0.4", "0.5", "3.4", "0.1", "22.7", "13.4", "8.9", "0.4", "24.5", "100.0", "1,018"], - ["", "Fourth", "73.9", "52.3", "32.0", "0.5", "12.5", "0.6", "0.2", "6.3", "0.2", "21.6", "11.5", "9.9", "0.2", "26.1", "100.0", "908"], - ["", "Highest", "78.3", "44.4", "19.5", "1.0", "9.7", "1.4", "0.0", "12.7", "0.0", "33.8", "18.2", "15.6", "0.0", "21.7", "100.0", "733"], - ["", "Number of living children", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], - ["", "No children", "25.1", "7.6", "0.3", "0.5", "2.0", "0.0", + ["Table 21 Current use of contraception by background characteristics\u2014Continued", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + ["", "", "", "", "", "Modern method", "", "", "", "", "", "", "Traditional method", "", "", "", ""], + ["", "", "Any", "", "", "", "", "", "", "Other", "Any", "", "", "", "Not", "", "Number"], + ["", "Any", "modern", "Female", "Male", "", "", "", "Condom/", "modern", "traditional", "", "With-", "Folk", "currently", "", "of"], + ["Background characteristic", "method", "method", "sterilization", "sterilization", "Pill", "IUD", "Injectables", "Nirodh", "method", "method", "Rhythm", "drawal", "method", "using", "Total", "women"], + ["Caste/tribe", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + ["Scheduled caste", "74.8", "55.8", "42.9", "0.9", "9.7", "0.0", "0.2", "2.2", "0.0", "19.0", "11.2", "7.4", "0.4", "25.2", "100.0", "1,363"], + ["Scheduled tribe", "59.3", "39.0", "26.8", "0.6", "6.4", "0.6", "1.2", "3.5", "0.0", "20.3", "10.4", "5.8", "4.1", "40.7", "100.0", "256"], + ["Other backward class", "71.4", "51.1", "34.9", "0.0", "8.6", "1.4", "0.0", "6.2", "0.0", "20.4", "12.6", "7.8", "0.0", "28.6", "100.0", "211"], + ["Other", "71.1", "48.8", "28.2", "0.8", "13.3", "0.9", "0.3", "5.2", "0.1", "22.3", "12.9", "9.1", "0.3", "28.9", "100.0", "3,319"], + ["Wealth index", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + ["Lowest", "64.5", "48.6", "34.3", "0.5", "10.5", "0.6", "0.7", "2.0", "0.0", "15.9", "9.9", "4.6", "1.4", "35.5", "100.0", "1,258"], + ["Second", "68.5", "50.4", "36.2", "1.1", "11.4", "0.5", "0.1", "1.1", "0.0", "18.1", "11.2", "6.7", "0.2", "31.5", "100.0", "1,317"], + ["Middle", "75.5", "52.8", "33.6", "0.6", "14.2", "0.4", "0.5", "3.4", "0.1", "22.7", "13.4", "8.9", "0.4", "24.5", "100.0", "1,018"], + ["Fourth", "73.9", "52.3", "32.0", "0.5", "12.5", "0.6", "0.2", "6.3", "0.2", "21.6", "11.5", "9.9", "0.2", "26.1", "100.0", "908"], + ["Highest", "78.3", "44.4", "19.5", "1.0", "9.7", "1.4", "0.0", "12.7", "0.0", "33.8", "18.2", "15.6", "0.0", "21.7", "100.0", "733"], + ["Number of living children", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + ["No children", "25.1", "7.6", "0.3", "0.5", "2.0", "0.0", "0.0", "4.8", "0.0", "17.5", "9.0", "8.5", "0.0", "74.9", "100.0", "563"], - ["", "1 child", "66.5", "32.1", "3.7", "0.7", "20.1", "0.7", "0.1", "6.9", "0.0", "34.3", "18.9", "15.2", "0.3", "33.5", "100.0", "1,190"], - ["\x18\x18", "1 son", "66.8", "33.2", "4.1", "0.7", "21.1", "0.5", "0.3", "6.6", "0.0", "33.5", "21.2", "12.3", "0.0", "33.2", "100.0", "672"], - ["", "No sons", "66.1", "30.7", "3.1", "0.6", "18.8", "0.8", "0.0", "7.3", "0.0", "35.4", "15.8", "19.0", "0.6", "33.9", "100.0", "517"], - ["", "2 children", "81.6", "60.5", "41.8", "0.9", "11.6", "0.8", "0.3", "4.8", "0.2", "21.1", "12.2", "8.3", "0.6", "18.4", "100.0", "1,576"], - ["", "1 or more sons", "83.7", "64.2", "46.4", "0.9", "10.8", "0.8", "0.4", "4.8", "0.1", "19.5", "11.1", "7.6", "0.7", "16.3", "100.0", "1,268"], - ["", "No sons", "73.2", "45.5", "23.2", "1.0", "15.1", "0.9", "0.0", "4.8", "0.5", "27.7", "16.8", "11.0", "0.0", "26.8", "100.0", "308"], - ["", "3 children", "83.9", "71.2", "57.7", "0.8", "9.8", "0.6", "0.5", "1.8", "0.0", "12.7", "8.7", "3.3", "0.8", "16.1", "100.0", "961"], - ["", "1 or more sons", "85.0", "73.2", "60.3", "0.9", "9.4", "0.5", "0.5", "1.6", "0.0", "11.8", "8.1", "3.0", "0.7", "15.0", "100.0", "860"], - ["", "No sons", "74.7", "53.8", "35.3", "0.0", "13.7", "1.6", "0.0", "3.2", "0.0", "20.9", "13.4", "6.1", "1.5", "25.3", "100.0", "101"], - ["", "4+ children", "74.3", "58.1", "45.1", "0.6", "8.7", "0.6", "0.7", "2.4", "0.0", "16.1", "9.9", "5.4", "0.8", "25.7", "100.0", "944"], - ["", "1 or more sons", "73.9", "58.2", "46.0", "0.7", "8.3", "0.7", "0.7", "1.9", "0.0", "15.7", "9.4", "5.5", "0.8", "26.1", "100.0", "901"], - ["", "No sons", "(82.1)", "(57.3)", "(25.6)", "(0.0)", "(17.8)", "(0.0)", "(0.0)", "(13.9)", "(0.0)", "(24.8)", "(21.3)", "(3.5)", "(0.0)", "(17.9)", "100.0", "43"], - ["", "Total", "71.2", "49.9", "32.2", + ["1 child", "66.5", "32.1", "3.7", "0.7", "20.1", "0.7", "0.1", "6.9", "0.0", "34.3", "18.9", "15.2", "0.3", "33.5", "100.0", "1,190"], + ["1 son", "66.8", "33.2", "4.1", "0.7", "21.1", "0.5", "0.3", "6.6", "0.0", "33.5", "21.2", "12.3", "0.0", "33.2", "100.0", "672"], + ["No sons", "66.1", "30.7", "3.1", "0.6", "18.8", "0.8", "0.0", "7.3", "0.0", "35.4", "15.8", "19.0", "0.6", "33.9", "100.0", "517"], + ["2 children", "81.6", "60.5", "41.8", "0.9", "11.6", "0.8", "0.3", "4.8", "0.2", "21.1", "12.2", "8.3", "0.6", "18.4", "100.0", "1,576"], + ["1 or more sons", "83.7", "64.2", "46.4", "0.9", "10.8", "0.8", "0.4", "4.8", "0.1", "19.5", "11.1", "7.6", "0.7", "16.3", "100.0", "1,268"], + ["No sons", "73.2", "45.5", "23.2", "1.0", "15.1", "0.9", "0.0", "4.8", "0.5", "27.7", "16.8", "11.0", "0.0", "26.8", "100.0", "308"], + ["3 children", "83.9", "71.2", "57.7", "0.8", "9.8", "0.6", "0.5", "1.8", "0.0", "12.7", "8.7", "3.3", "0.8", "16.1", "100.0", "961"], + ["1 or more sons", "85.0", "73.2", "60.3", "0.9", "9.4", "0.5", "0.5", "1.6", "0.0", "11.8", "8.1", "3.0", "0.7", "15.0", "100.0", "860"], + ["No sons", "74.7", "53.8", "35.3", "0.0", "13.7", "1.6", "0.0", "3.2", "0.0", "20.9", "13.4", "6.1", "1.5", "25.3", "100.0", "101"], + ["4+ children", "74.3", "58.1", "45.1", "0.6", "8.7", "0.6", "0.7", "2.4", "0.0", "16.1", "9.9", "5.4", "0.8", "25.7", "100.0", "944"], + ["1 or more sons", "73.9", "58.2", "46.0", "0.7", "8.3", "0.7", "0.7", "1.9", "0.0", "15.7", "9.4", "5.5", "0.8", "26.1", "100.0", "901"], + ["No sons", "(82.1)", "(57.3)", "(25.6)", "(0.0)", "(17.8)", "(0.0)", "(0.0)", "(13.9)", "(0.0)", "(24.8)", "(21.3)", "(3.5)", "(0.0)", "(17.9)", "100.0", "43"], + ["Total", "71.2", "49.9", "32.2", "0.7", "11.7", "0.6", "0.3", "4.3", "0.1", "21.3", "12.3", "8.4", "0.5", "28.8", "100.0", "5,234"], - ["", "NFHS-2 (1998-99)", "66.6", "47.3", "32.0", "1.8", "9.2", "1.4", "na", "2.9", "na", "na", "8.7", "9.8", "na", "33.4", "100.0", "4,116"], - ["", "NFHS-1 (1992-93)", "57.7", "37.6", "26.5", "4.3", "3.6", "1.3", "0.1", "1.9", "na", "na", "11.3", "8.3", "na", "42.3", "100.0", "3,970"], - ["", "", "Note: If more than one method is used, only the most effective method is considered in this tabulation. Total includes women for whom caste/tribe was not known or is missing, who are", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], - ["", "not shown separately.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], - ["", "na = Not available", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], - ["", "", "ns = Not shown; see table 2b, footnote 1", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], - ["", "( ) Based on 25-49 unweighted cases.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], - ["", "", "", "", "", "", "", "", "54", "", "", "", "", "", "", "", "", ""] + ["NFHS-2 (1998-99)", "66.6", "47.3", "32.0", "1.8", "9.2", "1.4", "na", "2.9", "na", "na", "8.7", "9.8", "na", "33.4", "100.0", "4,116"], + ["NFHS-1 (1992-93)", "57.7", "37.6", "26.5", "4.3", "3.6", "1.3", "0.1", "1.9", "na", "na", "11.3", "8.3", "na", "42.3", "100.0", "3,970"] ] data_stream_table_areas = [ @@ -187,14 +180,10 @@ data_stream_split_text = [ ["", "", "", "", "1522 WEST LINDSEY", "", "", "", "", ""], ["632575", "BAW", "BASHU LEGENDS", "HYH HE CHUANG LLC", "STREET", "NORMAN", "OK", "73069", "-", "2014/07/21"], ["", "", "", "DEEP FORK HOLDINGS", "", "", "", "", "", ""], - ["543149", "BAW", "BEDLAM BAR-B-Q", "LLC", "610 NORTHEAST 50TH", "OKLAHOMA CITY", "OK", "73105", "(405) 528-7427", "2015/02/23"], - ["", "", "", "", "Page 1 of 151", "", "", "", "", ""] + ["543149", "BAW", "BEDLAM BAR-B-Q", "LLC", "610 NORTHEAST 50TH", "OKLAHOMA CITY", "OK", "73105", "(405) 528-7427", "2015/02/23"] ] data_stream_flag_size = [ - ["", "TABLE 125: STATE-WISE COMPOSITION OF OUTSTANDING LIABILITIES - 1997 (Contd.)", "", "", "", "", "", "", "", "", ""], - ["", "", "", "", "(As at end-March)", "", "", "", "", "", ""], - ["", "", "", "", "", "", "", "", "", "", "(` Billion)"], ["States", "Total", "Market", "NSSF", "WMA", "Loans", "Loans", "Loans", "Loans", "Loans", "Loans"], ["", "Internal", "Loans", "", "from", "from", "from", "from", "from", "from SBI", "from"], ["", "Debt", "", "", "RBI", "Banks", "LIC", "GIC", "NABARD", "& Other", "NCDC"], @@ -230,9 +219,7 @@ data_stream_flag_size = [ ["Uttar Pradesh", "80.62", "74.89", "-", "4.34", "1.34", "0.6", "-", "-0.21", "0.18", "0.03"], ["West Bengal", "34.23", "32.19", "-", "-", "2.04", "0.77", "-", "0.06", "-", "0.51"], ["NCT Delhi", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"], - ["ALL STATES", "513.38", "436.02", "-", "25.57", "51.06", "14.18", "-", "8.21", "11.83", "11.08"], - ["2 Includes `2.45 crore outstanding under “Market Loan Suspense”.", "", "", "", "", "", "", "", "", "", ""], - ["", "", "", "", "445", "", "", "", "", "", ""] + ["ALL STATES", "513.38", "436.02", "-", "25.57", "51.06", "14.18", "-", "8.21", "11.83", "11.08"] ] data_lattice = [ From 1f71513004e61a838035a39b31d223e9df478992 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 23 Nov 2018 19:28:55 +0530 Subject: [PATCH 60/89] Fix no table found warning and add tests for two tables --- Makefile | 2 +- camelot/parsers/stream.py | 15 ++++- setup.cfg | 2 +- tests/data.py | 125 ++++++++++++++++++++++++++++++++++++++ tests/test_common.py | 22 +++++++ 5 files changed, 162 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index d0b54b0..383c801 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ install: pip install ".[dev]" test: - pytest --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot --mpl tests + pytest --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot --mpl docs: cd docs && make html diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 79073ac..178e005 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -309,10 +309,21 @@ class Stream(BaseParser): cols.append(text_x_max) cols = [(cols[i], cols[i + 1]) for i in range(0, len(cols) - 1)] else: + # calculate mode of the list of number of elements in + # each row to guess the number of columns ncols = max(set(elements), key=elements.count) if ncols == 1: - warnings.warn("No tables found on {}".format( - os.path.basename(self.rootname))) + # if mode is 1, the page usually contains not tables + # but there can be cases where the list can be skewed, + # try to remove all 1s from list in this case and + # see if the list contains elements, if yes, then use + # the mode after removing 1s + elements = list(filter(lambda x: x != 1, elements)) + if len(elements): + ncols = max(set(elements), key=elements.count) + else: + warnings.warn("No tables found in table area {}".format( + table_idx + 1)) cols = [(t.x0, t.x1) for r in rows_grouped if len(r) == ncols for t in r] cols = self._merge_columns(sorted(cols), col_close_tol=self.col_close_tol) inner_text = [] diff --git a/setup.cfg b/setup.cfg index 1a59858..2c56c09 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,5 +2,5 @@ test=pytest [tool:pytest] -addopts = --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot --mpl tests +addopts = --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot --mpl python_files = tests/test_*.py diff --git a/tests/data.py b/tests/data.py index c75a588..4cc6f89 100755 --- a/tests/data.py +++ b/tests/data.py @@ -74,6 +74,99 @@ data_stream_table_rotated = [ ["NFHS-1 (1992-93)", "57.7", "37.6", "26.5", "4.3", "3.6", "1.3", "0.1", "1.9", "na", "na", "11.3", "8.3", "na", "42.3", "100.0", "3,970"] ] +data_stream_two_tables_1 = [ + ["[In thousands (11,062.6 represents 11,062,600) For year ending December 31. Based on Uniform Crime Reporting (UCR)", "", "", "", "", "", "", "", "", ""], + ["Program. Represents arrests reported (not charged) by 12,910 agencies with a total population of 247,526,916 as estimated", "", "", "", "", "", "", "", "", ""], + ["by the FBI. Some persons may be arrested more than once during a year, therefore, the data in this table, in some cases,", "", "", "", "", "", "", "", "", ""], + ["could represent multiple arrests of the same person. See text, this section and source]", "", "", "", "", "", "", "", "", ""], + ["", "", "Total", "", "", "Male", "", "", "Female", ""], + ["Offense charged", "", "Under 18", "18 years", "", "Under 18", "18 years", "", "Under 18", "18 years"], + ["", "Total", "years", "and over", "Total", "years", "and over", "Total", "years", "and over"], + ["Total . . . . . . . . . . . . . . . . . . . . . . . . .", "11,062 .6", "1,540 .0", "9,522 .6", "8,263 .3", "1,071 .6", "7,191 .7", "2,799 .2", "468 .3", "2,330 .9"], + ["Violent crime . . . . . . . . . . . . . . . . . .", "467 .9", "69 .1", "398 .8", "380 .2", "56 .5", "323 .7", "87 .7", "12 .6", "75 .2"], + ["Murder and nonnegligent", "", "", "", "", "", "", "", "", ""], + ["manslaughter . . . . . . . .. .. .. .. ..", "10.0", "0.9", "9.1", "9.0", "0.9", "8.1", "1.1", "–", "1.0"], + ["Forcible rape . . . . . . . .. .. .. .. .. .", "17.5", "2.6", "14.9", "17.2", "2.5", "14.7", "–", "–", "–"], + ["Robbery . . . .. .. . .. . ... . ... . ...", "102.1", "25.5", "76.6", "90.0", "22.9", "67.1", "12.1", "2.5", "9.5"], + ["Aggravated assault . . . . . . . .. .. ..", "338.4", "40.1", "298.3", "264.0", "30.2", "233.8", "74.4", "9.9", "64.5"], + ["Property crime . . . . . . . . . . . . . . . . .", "1,396 .4", "338 .7", "1,057 .7", "875 .9", "210 .8", "665 .1", "608 .2", "127 .9", "392 .6"], + ["Burglary . .. . . . . .. ... .... .... ..", "240.9", "60.3", "180.6", "205.0", "53.4", "151.7", "35.9", "6.9", "29.0"], + ["Larceny-theft . . . . . . . .. .. .. .. .. .", "1,080.1", "258.1", "822.0", "608.8", "140.5", "468.3", "471.3", "117.6", "353.6"], + ["Motor vehicle theft . . . . .. .. . .... .", "65.6", "16.0", "49.6", "53.9", "13.3", "40.7", "11.7", "2.7", "8.9"], + ["Arson .. . . . .. . ... .... .... .... .", "9.8", "4.3", "5.5", "8.1", "3.7", "4.4", "1.7", "0.6", "1.1"], + ["Other assaults .. . . . . .. . ... . ... ..", "1,061.3", "175.3", "886.1", "785.4", "115.4", "670.0", "276.0", "59.9", "216.1"], + ["Forgery and counterfeiting .. . . . . . ..", "68.9", "1.7", "67.2", "42.9", "1.2", "41.7", "26.0", "0.5", "25.5"], + ["Fraud .... .. . . .. ... .... .... ....", "173.7", "5.1", "168.5", "98.4", "3.3", "95.0", "75.3", "1.8", "73.5"], + ["Embezzlement . . .. . . . .. . ... . ....", "14.6", "–", "14.1", "7.2", "–", "6.9", "7.4", "–", "7.2"], + ["Stolen property 1 . . . . . . .. . .. .. ...", "84.3", "15.1", "69.2", "66.7", "12.2", "54.5", "17.6", "2.8", "14.7"], + ["Vandalism . . . . . . . .. .. .. .. .. ....", "217.4", "72.7", "144.7", "178.1", "62.8", "115.3", "39.3", "9.9", "29.4"], + ["Weapons; carrying, possessing, etc. .", "132.9", "27.1", "105.8", "122.1", "24.3", "97.8", "10.8", "2.8", "8.0"], + ["Prostitution and commercialized vice", + "56.9", "1.1", "55.8", "17.3", "–", "17.1", "39.6", "0.8", "38.7"], + ["Sex offenses 2 . . . . .. . . . .. .. .. . ..", "61.5", "10.7", "50.7", "56.1", "9.6", "46.5", "5.4", "1.1", "4.3"], + ["Drug abuse violations . . . . . . . .. ...", "1,333.0", "136.6", "1,196.4", "1,084.3", "115.2", "969.1", "248.7", "21.4", "227.3"], + ["Gambling .. . . . . .. ... . ... . ... ...", "8.2", "1.4", "6.8", "7.2", "1.4", "5.9", "0.9", "–", "0.9"], + ["Offenses against the family and", "", "", "", "", "", "", "", "", ""], + ["children . . . .. . . .. .. .. .. .. .. . ..", "92.4", "3.7", "88.7", "68.9", "2.4", "66.6", "23.4", "1.3", "22.1"], + ["Driving under the influence . . . . . .. .", "1,158.5", "109.2", "1,147.5", "895.8", "8.2", "887.6", "262.7", "2.7", "260.0"], + ["Liquor laws . . . . . . . .. .. .. .. .. .. .", "48.2", "90.2", "368.0", "326.8", "55.4", "271.4", + "131.4", "34.7", "96.6"], + ["Drunkenness . . .. . . . .. . ... . ... ..", "488.1", "11.4", "476.8", "406.8", "8.5", "398.3", "81.3", "2.9", "78.4"], + ["Disorderly conduct . .. . . . . . .. .. .. .", "529.5", "136.1", "393.3", "387.1", "90.8", "296.2", "142.4", "45.3", "97.1"], + ["Vagrancy . . . .. . . . ... .... .... ...", "26.6", "2.2", "24.4", "20.9", "1.6", "19.3", "5.7", "0.6", "5.1"], + ["All other offenses (except traffic) . . ..", "306.1", "263.4", "2,800.8", "2,337.1", "194.2", "2,142.9", "727.0", "69.2", "657.9"], + ["Suspicion . . . .. . . .. .. .. .. .. .. . ..", "1.6", "–", "1.4", "1.2", "–", "1.0", "–", "–", "–"], + ["Curfew and loitering law violations ..", "91.0", "91.0", "(X)", "63.1", "63.1", "(X)", "28.0", "28.0", "(X)"], + ["Runaways . . . . . . . .. .. .. .. .. ....", "75.8", "75.8", "(X)", "34.0", "34.0", "(X)", "41.8", "41.8", "(X)"], + ["", "– Represents zero. X Not applicable. 1 Buying, receiving, possessing stolen property. 2 Except forcible rape and prostitution.", "", "", "", "", "", "", "", ""], + ["", "Source: U.S. Department of Justice, Federal Bureau of Investigation, Uniform Crime Reports, Arrests Master Files.", "", "", "", "", "", "", "", ""] +] + +data_stream_two_tables_2 = [ + ["", "Source: U.S. Department of Justice, Federal Bureau of Investigation, Uniform Crime Reports, Arrests Master Files.", "", "", "", ""], + ["Table 325. Arrests by Race: 2009", "", "", "", "", ""], + ["[Based on Uniform Crime Reporting (UCR) Program. Represents arrests reported (not charged) by 12,371 agencies", "", "", "", "", ""], + ["with a total population of 239,839,971 as estimated by the FBI. See headnote, Table 324]", "", "", "", "", ""], + ["", "", "", "", "American", ""], + ["Offense charged", "", "", "", + "Indian/Alaskan", "Asian Pacific"], + ["", "Total", "White", "Black", "Native", "Islander"], + ["Total . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .", "10,690,561", "7,389,208", "3,027,153", "150,544", "123,656"], + ["Violent crime . . . . . . . . . . . . . . . . . . . . . . . . . . . .", "456,965", "268,346", "177,766", "5,608", "5,245"], + ["Murder and nonnegligent manslaughter . .. ... .", "9,739", "4,741", "4,801", "100", "97"], + ["Forcible rape . . . . . . . .. .. .. .. .... .. ...... .", "16,362", "10,644", "5,319", "169", "230"], + ["Robbery . . . . .. . . . ... . ... . .... .... .... . . .", "100,496", "43,039", "55,742", "726", "989"], + ["Aggravated assault . . . . . . . .. .. ...... .. ....", "330,368", "209,922", "111,904", "4,613", "3,929"], + ["Property crime . . . . . . . . . . . . . . . . . . . . . . . . . . .", "1,364,409", "922,139", "406,382", "17,599", "18,289"], + ["Burglary . . .. . . . .. . .... .... .... .... ... . . .", "234,551", "155,994", "74,419", "2,021", "2,117"], + ["Larceny-theft . . . . . . . .. .. .. .. .... .. ...... .", "1,056,473", "719,983", "306,625", "14,646", "15,219"], + ["Motor vehicle theft . . . . . .. ... . ... ..... ... ..", "63,919", "39,077", "23,184", "817", "841"], + ["Arson .. . . .. .. .. ... .... .... .... .... . . . . .", "9,466", "7,085", "2,154", "115", "112"], + ["Other assaults .. . . . . . ... . ... . ... ..... ... ..", "1,032,502", "672,865", "332,435", "15,127", "12,075"], + ["Forgery and counterfeiting .. . . . . . ... ..... .. ..", "67,054", "44,730", "21,251", "345", "728"], + ["Fraud ... . . . . .. .. .. .. .. .. .. .. .. .... . . . . . .", "161,233", "108,032", "50,367", "1,315", "1,519"], + ["Embezzlement . . . .. . . . ... . ... . .... ... .....", "13,960", "9,208", "4,429", "75", "248"], + ["Stolen property; buying, receiving, possessing .. .", "82,714", "51,953", "29,357", "662", "742"], + ["Vandalism . . . . . . . .. .. .. .. .. .. .... .. ..... .", "212,173", "157,723", "48,746", "3,352", "2,352"], + ["Weapons—carrying, possessing, etc. .. .. ... .. .", "130,503", "74,942", "53,441", "951", "1,169"], + ["Prostitution and commercialized vice . ... .. .. ..", "56,560", "31,699", "23,021", "427", "1,413"], + ["Sex offenses 1 . . . . . . . .. .. .. .. .... .. ...... .", "60,175", "44,240", "14,347", "715", "873"], + ["Drug abuse violations . . . . . . . .. . ..... .. .....", "1,301,629", "845,974", "437,623", "8,588", "9,444"], + ["Gambling . . . . .. . . . ... . ... . .. ... . ...... .. .", "8,046", "2,290", "5,518", "27", "211"], + ["Offenses against the family and children ... .. .. .", "87,232", "58,068", "26,850", "1,690", "624"], + ["Driving under the influence . . . . . . .. ... ...... .", "1,105,401", "954,444", "121,594", "14,903", "14,460"], + ["Liquor laws . . . . . . . .. .. .. .. .. . ..... .. .....", "444,087", "373,189", "50,431", "14,876", "5,591"], + ["Drunkenness . .. . . . . . ... . ... . ..... . .......", "469,958", "387,542", "71,020", "8,552", "2,844"], + ["Disorderly conduct . . .. . . . . .. .. . ..... .. .....", "515,689", "326,563", "176,169", "8,783", "4,174"], + ["Vagrancy . . .. .. . . .. ... .... .... .... .... . . .", "26,347", "14,581", "11,031", "543", "192"], + ["All other offenses (except traffic) . .. .. .. ..... ..", "2,929,217", "1,937,221", "911,670", "43,880", "36,446"], + ["Suspicion . . .. . . . .. .. .. .. .. .. .. ...... .. . . .", "1,513", "677", "828", "1", "7"], + ["Curfew and loitering law violations . .. ... .. ....", "89,578", "54,439", "33,207", "872", "1,060"], + ["Runaways . . . . . . . .. .. .. .. .. .. .... .. ..... .", "73,616", "48,343", "19,670", "1,653", "3,950"], + ["1 Except forcible rape and prostitution.", "", "", "", "", ""], + ["", "Source: U.S. Department of Justice, Federal Bureau of Investigation, “Crime in the United States, Arrests,” September 2010,", "", "", "", ""] +] + data_stream_table_areas = [ ["", "One Withholding"], ["Payroll Period", "Allowance"], @@ -248,6 +341,38 @@ data_lattice_table_rotated = [ ["Pooled", "38742", "53618", "60601", "86898", "4459", "21918", "27041", "14312", "18519"] ] +data_lattice_two_tables_1 = [ + ["State", "n", "Literacy Status", "", "", "", "", ""], + ["", "", "Illiterate", "Read & Write", "1-4 std.", "5-8 std.", "9-12 std.", "College"], + ["Kerala", "2400", "7.2", "0.5", "25.3", "20.1", "41.5", "5.5"], + ["Tamil Nadu", "2400", "21.4", "2.3", "8.8", "35.5", "25.8", "6.2"], + ["Karnataka", "2399", "37.4", "2.8", "12.5", "18.3", "23.1", "5.8"], + ["Andhra Pradesh", "2400", "54.0", "1.7", "8.4", "13.2", "18.8", "3.9"], + ["Maharashtra", "2400", "22.0", "0.9", "17.3", "20.3", "32.6", "7.0"], + ["Gujarat", "2390", "28.6", "0.1", "14.4", "23.1", "26.9", "6.8"], + ["Madhya Pradesh", "2402", "29.1", "3.4", "8.5", "35.1", "13.3", "10.6"], + ["Orissa", "2405", "33.2", "1.0", "10.4", "25.7", "21.2", "8.5"], + ["West Bengal", "2293", "41.7", "4.4", "13.2", "17.1", "21.2", "2.4"], + ["Uttar Pradesh", "2400", "35.3", "2.1", "4.5", "23.3", "27.1", "7.6"], + ["Pooled", "23889", "30.9", "1.9", "12.3", "23.2", "25.2", "6.4"] +] + +data_lattice_two_tables_2 = [ + ["State", "n", "Literacy Status", "", "", "", "", ""], + ["", "", "Illiterate", "Read & Write", "1-4 std.", "5-8 std.", "9-12 std.", "College"], + ["Kerala", "2400", "8.8", "0.3", "20.1", "17.0", "45.6", "8.2"], + ["Tamil Nadu", "2400", "29.9", "1.5", "8.5", "33.1", "22.3", "4.8"], + ["Karnataka", "2399", "47.9", "2.5", "10.2", "18.8", "18.4", "2.3"], + ["Andhra Pradesh", "2400", "66.4", "0.7", "6.8", "12.9", "11.4", "1.8"], + ["Maharashtra", "2400", "41.3", "0.6", "14.1", "20.1", "21.6", "2.2"], + ["Gujarat", "2390", "57.6", "0.1", "10.3", "16.5", "12.9", "2.7"], + ["Madhya Pradesh", "2402", "58.7", "2.2", "6.6", "24.1", "5.3", "3.0"], + ["Orissa", "2405", "50.0", "0.9", "8.1", "21.9", "15.1", "4.0"], + ["West Bengal", "2293", "49.1", "4.8", "11.2", "16.8", "17.1", "1.1"], + ["Uttar Pradesh", "2400", "67.3", "2.0", "3.1", "17.2", "7.7", "2.7"], + ["Pooled", "23889", "47.7", "1.5", "9.9", "19.9", "17.8", "3.3"] +] + data_lattice_table_areas = [ ["", "", "", "", "", "", "", "", ""], ["State", "n", "Literacy Status", "", "", "", "", "", ""], diff --git a/tests/test_common.py b/tests/test_common.py index bfd1ea6..708d61c 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -56,6 +56,17 @@ def test_stream_table_rotated(): assert df.equals(tables[0].df) +def test_stream_two_tables(): + df1 = pd.DataFrame(data_stream_two_tables_1) + df2 = pd.DataFrame(data_stream_two_tables_2) + + filename = os.path.join(testdir, "tabula/12s0324.pdf") + tables = camelot.read_pdf(filename, flavor='stream') + assert len(tables) == 2 + assert df1.equals(tables[0].df) + assert df2.equals(tables[1].df) + + def test_stream_table_areas(): df = pd.DataFrame(data_stream_table_areas) @@ -111,6 +122,17 @@ def test_lattice_table_rotated(): assert df.equals(tables[0].df) +def test_lattice_two_tables(): + df1 = pd.DataFrame(data_lattice_two_tables_1) + df2 = pd.DataFrame(data_lattice_two_tables_2) + + filename = os.path.join(testdir, "twotables_2.pdf") + tables = camelot.read_pdf(filename) + assert len(tables) == 2 + assert df1.equals(tables[0].df) + assert df2.equals(tables[1].df) + + def test_lattice_table_areas(): df = pd.DataFrame(data_lattice_table_areas) From 23ec6b55f70d1a8ce99630cd984565e29eaf8092 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 23 Nov 2018 21:04:10 +0530 Subject: [PATCH 61/89] Add docstrings and update docs --- README.md | 4 +- camelot/__version__.py | 21 ++++++---- camelot/core.py | 52 ++++++++++++++++++++++++- camelot/parsers/stream.py | 11 ++++-- docs/dev/contributing.rst | 2 +- docs/index.rst | 3 +- docs/user/how-it-works.rst | 16 ++++---- docs/user/install-deps.rst | 76 ++++++++++++++++++++++++++++++++++++ docs/user/install.rst | 79 +++----------------------------------- 9 files changed, 165 insertions(+), 99 deletions(-) create mode 100755 docs/user/install-deps.rst diff --git a/README.md b/README.md index 93b7215..1d89c30 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ $ conda install -c conda-forge camelot-py ### Using pip -After [installing the dependencies](https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip) ([tk](https://packages.ubuntu.com/trusty/python-tk) and [ghostscript](https://www.ghostscript.com/)), you can simply use pip to install Camelot: +After [installing the dependencies](https://camelot-py.readthedocs.io/en/master/user/install-deps.html) ([tk](https://packages.ubuntu.com/trusty/python-tk) and [ghostscript](https://www.ghostscript.com/)), you can simply use pip to install Camelot:
 $ pip install camelot-py[cv]
@@ -128,4 +128,4 @@ Camelot uses [Semantic Versioning](https://semver.org/). For the available versi
 
 ## License
 
-This project is licensed under the MIT License, see the [LICENSE](https://github.com/socialcopsdev/camelot/blob/master/LICENSE) file for details.
\ No newline at end of file
+This project is licensed under the MIT License, see the [LICENSE](https://github.com/socialcopsdev/camelot/blob/master/LICENSE) file for details.
diff --git a/camelot/__version__.py b/camelot/__version__.py
index f19ff5e..a48c9db 100644
--- a/camelot/__version__.py
+++ b/camelot/__version__.py
@@ -1,18 +1,23 @@
 # -*- coding: utf-8 -*-
 
 VERSION = (0, 4, 0)
-PHASE = 'alpha' # alpha, beta or rc
-PHASE_VERSION = '1'
+PRERELEASE = None # alpha, beta or rc
+REVISION = None
+
+
+def generate_version(version, prerelease=None, revision=None):
+    version_parts = ['.'.join(map(str, version))]
+    if prerelease is not None:
+        version_parts.append('-{}'.format(prerelease))
+    if revision is not None:
+        version_parts.append('.{}'.format(revision))
+    return ''.join(version_parts)
+
 
 __title__ = 'camelot-py'
 __description__ = 'PDF Table Extraction for Humans.'
 __url__ = 'http://camelot-py.readthedocs.io/'
-if PHASE:
-    __version__ = '{}-{}'.format('.'.join(map(str, VERSION)), PHASE)
-    if PHASE_VERSION:
-        __version__ = '{}.{}'.format(__version__, PHASE_VERSION)
-else:
-    __version__ = '.'.join(map(str, VERSION))
+__version__ = generate_version(VERSION, prerelease=PRERELEASE, revision=REVISION)
 __author__ = 'Vinayak Mehta'
 __author_email__ = 'vmehta94@gmail.com'
 __license__ = 'MIT License'
diff --git a/camelot/core.py b/camelot/core.py
index cc0b5a3..f11fcc1 100644
--- a/camelot/core.py
+++ b/camelot/core.py
@@ -20,6 +20,29 @@ TABLE_AREA_PADDING = 10
 
 
 class TextEdge(object):
+    """Defines a text edge coordinates relative to a left-bottom
+    origin. (PDF coordinate space)
+
+    Parameters
+    ----------
+    x : float
+        x-coordinate of the text edge.
+    y0 : float
+        y-coordinate of bottommost point.
+    y1 : float
+        y-coordinate of topmost point.
+    align : string, optional (default: 'left')
+        {'left', 'right', 'middle'}
+
+    Attributes
+    ----------
+    intersections: int
+        Number of intersections with horizontal text rows.
+    is_valid: bool
+        A text edge is valid if it intersections with at least
+        TEXTEDGE_REQUIRED_ELEMENTS horizontal text rows.
+
+    """
     def __init__(self, x, y0, y1, align='left'):
         self.x = x
         self.y0 = y0
@@ -33,6 +56,9 @@ class TextEdge(object):
             round(self.x, 2), round(self.y0, 2), round(self.y1, 2), self.align, self.is_valid)
 
     def update_coords(self, x, y0):
+        """Updates the text edge's x and bottom y coordinates and sets
+        the is_valid attribute.
+        """
         if np.isclose(self.y0, y0, atol=TEXTEDGE_EXTEND_TOLERANCE):
             self.x = (self.intersections * self.x + x) / float(self.intersections + 1)
             self.y0 = y0
@@ -44,11 +70,18 @@ class TextEdge(object):
 
 
 class TextEdges(object):
+    """Defines a dict of left, right and middle text edges found on
+    the PDF page. The dict has three keys based on the alignments,
+    and each key's value is a list of camelot.core.TextEdge objects.
+    """
     def __init__(self):
-        self._textedges = {'left': [], 'middle': [], 'right': []}
+        self._textedges = {'left': [], 'right': [], 'middle': []}
 
     @staticmethod
     def get_x_coord(textline, align):
+        """Returns the x coordinate of a text row based on the
+        specified alignment.
+        """
         x_left = textline.x0
         x_right = textline.x1
         x_middle = x_left + (x_right - x_left) / 2.0
@@ -56,12 +89,17 @@ class TextEdges(object):
         return x_coord[align]
 
     def find(self, x_coord, align):
+        """Returns the index of an existing text edge using
+        the specified x coordinate and alignment.
+        """
         for i, te in enumerate(self._textedges[align]):
             if np.isclose(te.x, x_coord, atol=0.5):
                 return i
         return None
 
     def add(self, textline, align):
+        """Adds a new text edge to the current dict.
+        """
         x = self.get_x_coord(textline, align)
         y0 = textline.y0
         y1 = textline.y1
@@ -69,6 +107,8 @@ class TextEdges(object):
         self._textedges[align].append(te)
 
     def update(self, textline):
+        """Updates an existing text edge in the current dict.
+        """
         for align in ['left', 'right', 'middle']:
             x_coord = self.get_x_coord(textline, align)
             idx = self.find(x_coord, align)
@@ -78,11 +118,18 @@ class TextEdges(object):
                 self._textedges[align][idx].update_coords(x_coord, textline.y0)
 
     def generate(self, textlines):
+        """Generates the text edges dict based on horizontal text
+        rows.
+        """
         for tl in textlines:
             if len(tl.get_text().strip()) > 1: # TODO: hacky
                 self.update(tl)
 
     def get_relevant(self):
+        """Returns the list of relevant text edges (all share the same
+        alignment) based on which list intersects horizontal text rows
+        the most.
+        """
         intersections_sum = {
             'left': sum(te.intersections for te in self._textedges['left'] if te.is_valid),
             'right': sum(te.intersections for te in self._textedges['right'] if te.is_valid),
@@ -96,6 +143,9 @@ class TextEdges(object):
         return self._textedges[relevant_align]
 
     def get_table_areas(self, textlines, relevant_textedges):
+        """Returns a dict of interesting table areas on the PDF page
+        calculated using relevant text edges.
+        """
         def pad(area, average_row_height):
             x0 = area[0] - TABLE_AREA_PADDING
             y0 = area[1] - TABLE_AREA_PADDING
diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py
index 178e005..3b9c068 100644
--- a/camelot/parsers/stream.py
+++ b/camelot/parsers/stream.py
@@ -247,10 +247,13 @@ class Stream(BaseParser):
                                  " should be equal")
 
     def _nurminen_table_detection(self, textlines):
-        # a general heuristic implementation of the table detection
-        # algorithm described by Anssi Nurminen's master's thesis:
-        # https://dspace.cc.tut.fi/dpub/bitstream/handle/123456789/21520/Nurminen.pdf?sequence=3
-        # assumes that tables are situated relatively apart vertically
+        """A general implementation of the table detection algorithm
+        described by Anssi Nurminen's master's thesis.
+        Link: https://dspace.cc.tut.fi/dpub/bitstream/handle/123456789/21520/Nurminen.pdf?sequence=3
+
+        Assumes that tables are situated relatively far apart
+        vertically.
+        """
 
         # TODO: add support for arabic text #141
         # sort textlines in reading order
diff --git a/docs/dev/contributing.rst b/docs/dev/contributing.rst
index 1ecaee0..21cdb36 100644
--- a/docs/dev/contributing.rst
+++ b/docs/dev/contributing.rst
@@ -7,7 +7,7 @@ If you're reading this, you're probably looking to contributing to Camelot. *Tim
 
 This document will help you get started with contributing documentation, code, testing and filing issues. If you have any questions, feel free to reach out to `Vinayak Mehta`_, the author and maintainer.
 
-.. _Vinayak Mehta: https://vinayak-mehta.github.io
+.. _Vinayak Mehta: https://www.vinayakmehta.com
 
 Code Of Conduct
 ---------------
diff --git a/docs/index.rst b/docs/index.rst
index 2d69510..4c2bf07 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -92,6 +92,7 @@ This part of the documentation begins with some background information about why
    :maxdepth: 2
 
    user/intro
+   user/install-deps
    user/install
    user/how-it-works
    user/quickstart
@@ -118,4 +119,4 @@ you.
 .. toctree::
    :maxdepth: 2
 
-   dev/contributing
\ No newline at end of file
+   dev/contributing
diff --git a/docs/user/how-it-works.rst b/docs/user/how-it-works.rst
index 0783c60..13004da 100644
--- a/docs/user/how-it-works.rst
+++ b/docs/user/how-it-works.rst
@@ -5,24 +5,24 @@ How It Works
 
 This part of the documentation includes a high-level explanation of how Camelot extracts tables from PDF files.
 
-You can choose between two table parsing methods, *Stream* and *Lattice*. These names for parsing methods inside Camelot were inspired from `Tabula`_.
-
-.. _Tabula: https://github.com/tabulapdf/tabula
+You can choose between two table parsing methods, *Stream* and *Lattice*. These names for parsing methods inside Camelot were inspired from `Tabula `_.
 
 .. _stream:
 
 Stream
 ------
 
-Stream can be used to parse tables that have whitespaces between cells to simulate a table structure. It looks for these spaces between text to form a table representation.
+Stream can be used to parse tables that have whitespaces between cells to simulate a table structure. It is built on top of PDFMiner's functionality of grouping characters on a page into words and sentences, using `margins `_.
 
-It is built on top of PDFMiner's functionality of grouping characters on a page into words and sentences, using `margins`_. After getting the words on a page, it groups them into rows based on their *y* coordinates. It then tries to guess the number of columns the table might have by calculating the mode of the number of words in each row. This mode is used to calculate *x* ranges for the table's columns. It then adds columns to this column range list based on any words that may lie outside or inside the current column *x* ranges.
+1. Words on the PDF page are grouped into text rows based on their *y* axis overlaps.
 
-.. _margins: https://euske.github.io/pdfminer/#tools
+2. Textedges are calculated and then used to guess interesting table areas on the PDF page. You can read `Anssi Nurminen's master's thesis `_ to know more about this table detection technique. [See pages 20, 35 and 40]
 
-.. note:: By default, Stream treats the whole PDF page as a table, which isn't ideal when there are more than two tables on a page with different number of columns. Automatic table detection for Stream is `in the works`_.
+3. The number of columns inside each table area are then guessed. This is done by calculating the mode of number of words in each text row. Based on this mode, words in each text row are chosen to calculate a list of column *x* ranges.
 
-.. _in the works: https://github.com/socialcopsdev/camelot/issues/102
+4. Words that lie inside/outside the current column *x* ranges are then used to extend extend the current list of columns.
+
+5. Finally, a table is formed using the text rows' *y* ranges and column *x* ranges and words found on the page are assigned to the table's cells based on their *x* and *y* coordinates.
 
 .. _lattice:
 
diff --git a/docs/user/install-deps.rst b/docs/user/install-deps.rst
new file mode 100755
index 0000000..287af3a
--- /dev/null
+++ b/docs/user/install-deps.rst
@@ -0,0 +1,76 @@
+.. _install_deps:
+
+Installation of dependencies
+============================
+
+The dependencies `Tkinter`_ and `ghostscript`_ can be installed using your system's package manager. You can run one of the following, based on your OS.
+
+.. _Tkinter: https://wiki.python.org/moin/TkInter
+.. _ghostscript: https://www.ghostscript.com
+
+OS-specific instructions
+------------------------
+
+For Ubuntu
+^^^^^^^^^^
+::
+
+    $ apt install python-tk ghostscript
+
+Or for Python 3::
+
+    $ apt install python3-tk ghostscript
+
+For macOS
+^^^^^^^^^
+::
+
+    $ brew install tcl-tk ghostscript
+
+For Windows
+^^^^^^^^^^^
+
+For Tkinter, you can download the `ActiveTcl Community Edition`_ from ActiveState. For ghostscript, you can get the installer at the `ghostscript downloads page`_.
+
+After installing ghostscript, you'll need to reboot your system to make sure that the ghostscript executable's path is in the windows PATH environment variable. In case you don't want to reboot, you can manually add the ghostscript executable's path to the PATH variable, `as shown here`_.
+
+.. _ActiveTcl Community Edition: https://www.activestate.com/activetcl/downloads
+.. _ghostscript downloads page: https://www.ghostscript.com/download/gsdnld.html
+.. _as shown here: https://java.com/en/download/help/path.xml
+
+Checks to see if dependencies were installed correctly
+------------------------------------------------------
+
+You can do the following checks to see if the dependencies were installed correctly.
+
+For Tkinter
+^^^^^^^^^^^
+
+Launch Python, and then at the prompt, type::
+
+    >>> import Tkinter
+
+Or in Python 3::
+
+    >>> import tkinter
+
+If you have Tkinter, Python will not print an error message, and if not, you will see an ``ImportError``.
+
+For ghostscript
+^^^^^^^^^^^^^^^
+
+Run the following to check the ghostscript version.
+
+For Ubuntu/macOS::
+
+    $ gs -version
+
+For Windows::
+
+    C:\> gswin64c.exe -version
+
+Or for Windows 32-bit::
+
+    C:\> gswin32c.exe -version
+
+If you have ghostscript, you should see the ghostscript version and copyright information.
diff --git a/docs/user/install.rst b/docs/user/install.rst
index e28e546..fc9fc82 100644
--- a/docs/user/install.rst
+++ b/docs/user/install.rst
@@ -3,7 +3,7 @@
 Installation of Camelot
 =======================
 
-This part of the documentation covers how to install Camelot.
+This part of the documentation covers the steps to install Camelot.
 
 Using conda
 -----------
@@ -23,84 +23,17 @@ The easiest way to install Camelot is to install it with `conda`_, which is a pa
 Using pip
 ---------
 
-First, you'll need to install the dependencies, which include `Tkinter`_ and `ghostscript`_.
+After :ref:`installing the dependencies `, which include `Tkinter`_ and `ghostscript`_, you can simply use pip to install Camelot::
+
+    $ pip install camelot-py[cv]
 
 .. _Tkinter: https://wiki.python.org/moin/TkInter
 .. _ghostscript: https://www.ghostscript.com
 
-These can be installed using your system's package manager. You can run one of the following, based on your OS.
-
-For Ubuntu
-^^^^^^^^^^
-::
-
-    $ apt install python-tk ghostscript
-
-Or for Python 3::
-
-    $ apt install python3-tk ghostscript
-
-For macOS
-^^^^^^^^^
-::
-
-    $ brew install tcl-tk ghostscript
-
-For Windows
-^^^^^^^^^^^
-
-For Tkinter, you can download the `ActiveTcl Community Edition`_ from ActiveState. For ghostscript, you can get the installer at the `ghostscript downloads page`_.
-
-After installing ghostscript, you'll need to reboot your system to make sure that the ghostscript executable's path is in the windows PATH environment variable. In case you don't want to reboot, you can manually add the ghostscript executable's path to the PATH variable, `as shown here`_.
-
-.. _ActiveTcl Community Edition: https://www.activestate.com/activetcl/downloads
-.. _ghostscript downloads page: https://www.ghostscript.com/download/gsdnld.html
-.. _as shown here: https://java.com/en/download/help/path.xml
-
-----
-
-You can do the following checks to see if the dependencies were installed correctly.
-
-For Tkinter
-^^^^^^^^^^^
-
-Launch Python, and then at the prompt, type::
-
-    >>> import Tkinter
-
-Or in Python 3::
-
-    >>> import tkinter
-
-If you have Tkinter, Python will not print an error message, and if not, you will see an ``ImportError``.
-
-For ghostscript
-^^^^^^^^^^^^^^^
-
-Run the following to check the ghostscript version.
-
-For Ubuntu/macOS::
-
-    $ gs -version
-
-For Windows::
-
-    C:\> gswin64c.exe -version
-
-Or for Windows 32-bit::
-
-    C:\> gswin32c.exe -version
-
-If you have ghostscript, you should see the ghostscript version and copyright information.
-
-Finally, you can use pip to install Camelot::
-
-    $ pip install camelot-py[cv]
-
 From the source code
 --------------------
 
-After `installing the dependencies`_, you can install from the source by:
+After :ref:`installing the dependencies `, you can install from the source by:
 
 1. Cloning the GitHub repository.
 ::
@@ -112,5 +45,3 @@ After `installing the dependencies`_, you can install from the source by:
 
     $ cd camelot
     $ pip install ".[cv]"
-
-.. _installing the dependencies: https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip

From e4af2522801fb0205c9feeba29f4ce4030fae0d7 Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Fri, 23 Nov 2018 21:37:40 +0530
Subject: [PATCH 62/89] Update HISTORY.md

---
 HISTORY.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/HISTORY.md b/HISTORY.md
index 1c74e4d..84f8e84 100755
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -4,6 +4,13 @@ Release History
 master
 ------
 
+0.4.0 (2018-11-23)
+------------------
+
+**Improvements**
+
+* [#102](https://github.com/socialcopsdev/camelot/issues/102) Detect tables automatically when Stream is used. [#206](https://github.com/socialcopsdev/camelot/pull/206) by Vinayak Mehta.
+
 0.3.2 (2018-11-04)
 ------------------
 

From 6df88f90fbf3db151edbe139f860b9e7276a8be5 Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Fri, 23 Nov 2018 21:39:42 +0530
Subject: [PATCH 63/89] Update HISTORY.md

---
 HISTORY.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/HISTORY.md b/HISTORY.md
index 84f8e84..a2fff7f 100755
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -9,7 +9,7 @@ master
 
 **Improvements**
 
-* [#102](https://github.com/socialcopsdev/camelot/issues/102) Detect tables automatically when Stream is used. [#206](https://github.com/socialcopsdev/camelot/pull/206) by Vinayak Mehta.
+* [#102](https://github.com/socialcopsdev/camelot/issues/102) Detect tables automatically when Stream is used. [#206](https://github.com/socialcopsdev/camelot/pull/206) Add implementation of Anssi Nurminen's table detection algorithm by Vinayak Mehta.
 
 0.3.2 (2018-11-04)
 ------------------

From 7bdd9a315609d695b17a4c9f340783923a0eaed4 Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Sat, 1 Dec 2018 06:29:35 +0530
Subject: [PATCH 64/89] Update docs

---
 docs/user/advanced.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst
index e26d22c..9610408 100644
--- a/docs/user/advanced.rst
+++ b/docs/user/advanced.rst
@@ -146,7 +146,7 @@ Finally, let's plot all line intersections present on the table's PDF page.
 Specify table areas
 -------------------
 
-Since :ref:`Stream ` treats the whole page as a table, `for now`_, it's useful to specify table boundaries in cases such as `these <../_static/pdf/table_areas.pdf>`__. You can plot the text on this page and note the top left and bottom right coordinates of the table.
+In cases such as `these <../_static/pdf/table_areas.pdf>`__, it can be useful to specify table boundaries. You can plot the text on this page and note the top left and bottom right coordinates of the table.
 
 Table areas that you want Camelot to analyze can be passed as a list of comma-separated strings to :meth:`read_pdf() `, using the ``table_areas`` keyword argument.
 

From 2635f910e49cbd533ddc00680d5f2ef7eec4524c Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Wed, 5 Dec 2018 20:08:37 +0530
Subject: [PATCH 65/89] Add chardet to install_requires

---
 HISTORY.md | 7 +++++++
 setup.py   | 1 +
 2 files changed, 8 insertions(+)

diff --git a/HISTORY.md b/HISTORY.md
index a2fff7f..4cf77ba 100755
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -4,6 +4,13 @@ Release History
 master
 ------
 
+0.4.1 (2018-12-05)
+------------------
+
+**Bugfixes**
+
+* Add chardet to `install_requires` to fix [#210](https://github.com/socialcopsdev/camelot/issues/210). More details in [pdfminer.six#213](https://github.com/pdfminer/pdfminer.six/issues/213).
+
 0.4.0 (2018-11-23)
 ------------------
 
diff --git a/setup.py b/setup.py
index 417b460..b83f566 100644
--- a/setup.py
+++ b/setup.py
@@ -14,6 +14,7 @@ with open('README.md', 'r') as f:
 
 
 requires = [
+    'chardet>=3.0.4',
     'click>=6.7',
     'numpy>=1.13.3',
     'openpyxl>=2.5.8',

From cb3e76726bfbb4322a2caf8e996eaecb6f7db9fd Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Wed, 5 Dec 2018 20:10:25 +0530
Subject: [PATCH 66/89] Bump version

---
 camelot/__version__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/camelot/__version__.py b/camelot/__version__.py
index a48c9db..48246be 100644
--- a/camelot/__version__.py
+++ b/camelot/__version__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-VERSION = (0, 4, 0)
+VERSION = (0, 4, 1)
 PRERELEASE = None # alpha, beta or rc
 REVISION = None
 

From 8d8ca6e4358fce6683c5d5061be811ac0b45dac0 Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Fri, 7 Dec 2018 18:45:23 +0530
Subject: [PATCH 67/89] Fix variable name

---
 camelot/core.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/camelot/core.py b/camelot/core.py
index f11fcc1..ac63e54 100644
--- a/camelot/core.py
+++ b/camelot/core.py
@@ -448,7 +448,7 @@ class Table(object):
                         self.cells[L][J].top = True
                         J += 1
             elif i == []:  # only bottom edge
-                I = len(self.rows) - 1
+                L = len(self.rows) - 1
                 if k:
                     K = k[0]
                     while J < K:

From 619ce2e2a45286716c5bd15927e7f64173d9d0a6 Mon Sep 17 00:00:00 2001
From: Vinayak Mehta 
Date: Fri, 7 Dec 2018 20:22:56 +0530
Subject: [PATCH 68/89] Fix grid plot baseline image

---
 tests/files/baseline_plots/test_grid_plot.png | Bin 8440 -> 8367 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)

diff --git a/tests/files/baseline_plots/test_grid_plot.png b/tests/files/baseline_plots/test_grid_plot.png
index d60f69f3e88878eb196022b97f7ed81ea1d67e95..4487eb315d39fbc6a724ab191a13a4f433be72ff 100644
GIT binary patch
literal 8367
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 Q8ozCya64XkEb#K*0aqVwpa1{> literal 8440 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 From d6ffe0f1a9462fae748eb470a066ff31a7e67519 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 11 Dec 2018 20:34:43 +0530 Subject: [PATCH 69/89] Add pdfplumber benchmark --- .../lattice/agstat/agstat-data-pdfplumber.csv | 96 +++++++++++++++++++ .../column_span_1-data-pdfplumber.csv | 56 +++++++++++ .../rotated/rotated-data-pdfplumber.csv | 18 ++++ .../twotables_1-data-pdfplumber.csv | 3 + .../twotables_2-data-pdfplumber.csv | 3 + .../district_health-data-pdfplumber.csv | 66 +++++++++++++ .../stream/health/health-data-pdfplumber.csv | 44 +++++++++ .../missing_values-data-pdfplumber.csv | 71 ++++++++++++++ 8 files changed, 357 insertions(+) create mode 100644 docs/benchmark/lattice/agstat/agstat-data-pdfplumber.csv create mode 100644 docs/benchmark/lattice/column_span_1/column_span_1-data-pdfplumber.csv create mode 100644 docs/benchmark/lattice/rotated/rotated-data-pdfplumber.csv create mode 100644 docs/benchmark/lattice/twotables_1/twotables_1-data-pdfplumber.csv create mode 100644 docs/benchmark/lattice/twotables_2/twotables_2-data-pdfplumber.csv create mode 100644 docs/benchmark/stream/district_health/district_health-data-pdfplumber.csv create mode 100644 docs/benchmark/stream/health/health-data-pdfplumber.csv create mode 100644 docs/benchmark/stream/missing_values/missing_values-data-pdfplumber.csv diff --git a/docs/benchmark/lattice/agstat/agstat-data-pdfplumber.csv b/docs/benchmark/lattice/agstat/agstat-data-pdfplumber.csv new file mode 100644 index 0000000..7733ba8 --- /dev/null +++ b/docs/benchmark/lattice/agstat/agstat-data-pdfplumber.csv @@ -0,0 +1,96 @@ +"0","1","2","3","4","5","6","7","8","9","10" +"Sl. +No.","District","n +o +i +t +a +l3 +opu2-1hs) +P1k +d 20 la +er n +cto(I +ef +j +o +r +P","% +8 +8 +o ) +s +ult t tkh +dna +Aalen l +v(I +i +u +q +E",") +y +n a +umptiomentadult/donnes) +nsres/h t +ouimk +Cqga +al re00n L +ot 4(I +T @ +(","menteds, age)nes) +uireg sewastton +qn h +Reudis &ak +al cld L +tnen +To(Ife(I","","","","","" +"","","","","","","f +i +r +a +h +K","i +b +a +R","l +a +t +o +T","e +c +i +R","y +d +d +a +P" +"1","Balasore","23.65","20.81","3.04","3.47","2.78","0.86","3.64","0.17","0.25" +"2","Bhadrak","15.34","13.50","1.97","2.25","3.50","0.05","3.55","1.30","1.94" +"3","Balangir","17.01","14.97","2.19","2.50","6.23","0.10","6.33","3.83","5.72" +"4","Subarnapur","6.70","5.90","0.86","0.98","4.48","1.13","5.61","4.63","6.91" +"5","Cuttack","26.63","23.43","3.42","3.91","3.75","0.06","3.81","-0.10","-0.15" +"6","Jagatsingpur","11.49","10.11","1.48","1.69","2.10","0.02","2.12","0.43","0.64" +"7","Jajpur","18.59","16.36","2.39","2.73","2.13","0.04","2.17","-0.56","-0.84" +"8","Kendrapara","14.62","12.87","1.88","2.15","2.60","0.07","2.67","0.52","0.78" +"9","Dhenkanal","12.13","10.67","1.56","1.78","2.26","0.02","2.28","0.50","0.75" +"10","Angul","12.93","11.38","1.66","1.90","1.73","0.02","1.75","-0.15","-0.22" +"11","Ganjam","35.77","31.48","4.60","5.26","4.57","0.00","4.57","-0.69","-1.03" +"12","Gajapati","5.85","5.15","0.75","0.86","0.68","0.01","0.69","-0.17","-0.25" +"13","Kalahandi","16.12","14.19","2.07","2.37","5.42","1.13","6.55","4.18","6.24" +"14","Nuapada","6.18","5.44","0.79","0.90","1.98","0.08","2.06","1.16","1.73" +"15","Keonjhar","18.42","16.21","2.37","2.71","2.76","0.08","2.84","0.13","0.19" +"16","Koraput","14.09","12.40","1.81","2.07","2.08","0.34","2.42","0.35","0.52" +"17","Malkangiri","6.31","5.55","0.81","0.93","1.78","0.04","1.82","0.89","1.33" +"18","Nabarangpur","12.50","11.00","1.61","1.84","3.26","0.02","3.28","1.44","2.15" +"19","Rayagada","9.83","8.65","1.26","1.44","1.15","0.03","1.18","-0.26","-0.39" +"20","Mayurbhanj","25.61","22.54","3.29","3.76","4.90","0.06","4.96","1.20","1.79" +"21","Kandhamal","7.45","6.56","0.96","1.10","0.70","0.01","0.71","-0.39","-0.58" +"22","Boudh","4.51","3.97","0.58","0.66","1.73","0.03","1.76","1.10","1.64" +"23","Puri","17.29","15.22","2.22","2.54","2.45","0.99","3.44","0.90","1.34" +"24","Khordha","23.08","20.31","2.97","3.39","2.02","0.03","2.05","-1.34","-2.00" +"25","Nayagarh","9.78","8.61","1.26","1.44","2.10","0.00","2.10","0.66","0.99" +"26","Sambalpur","10.62","9.35","1.37","1.57","3.45","0.71","4.16","2.59","3.87" +"27","Bargarh","15.00","13.20","1.93","2.21","6.87","2.65","9.52","7.31","10.91" +"28","Deogarh","3.18","2.80","0.41","0.47","1.12","0.07","1.19","0.72","1.07" +"29","Jharsuguda","5.91","5.20","0.76","0.87","0.99","0.01","1.00","0.13","0.19" +"30","","","18.66","2.72","3.11","4.72","0.02","4.74","1.63","2.43" diff --git a/docs/benchmark/lattice/column_span_1/column_span_1-data-pdfplumber.csv b/docs/benchmark/lattice/column_span_1/column_span_1-data-pdfplumber.csv new file mode 100644 index 0000000..040edc5 --- /dev/null +++ b/docs/benchmark/lattice/column_span_1/column_span_1-data-pdfplumber.csv @@ -0,0 +1,56 @@ +"0","1","2","3","4","5","6","7" +"Rate of Accidental Deaths & Suicides and Population Growth During 1967 to 2013","","","","","","","" +"Sl. +No.","Year","Population +(in Lakh)","Accidental Deaths","","Suicides","","Percentage +Population +growth" +"","","","Incidence","Rate","Incidence","Rate","" +"(1)","(2)","(3)","(4)","(5)","(6)","(7)","(8)" +"1.","1967","4999","126762","25.4","38829","7.8","2.2" +"2.","1968","5111","126232","24.7","40688","8.0","2.2" +"3.","1969","5225","130755","25.0","43633","8.4","2.2" +"4.","1970","5343","139752","26.2","48428","9.1","2.3" +"5.","1971","5512","105601","19.2","43675","7.9","3.2" +"6.","1972","5635","106184","18.8","43601","7.7","2.2" +"7.","1973","5759","130654","22.7","40807","7.1","2.2" +"8.","1974","5883","110624","18.8","46008","7.8","2.2" +"9.","1975","6008","113016","18.8","42890","7.1","2.1" +"10.","1976","6136","111611","18.2","41415","6.7","2.1" +"11.","1977","6258","117338","18.8","39718","6.3","2.0" +"12.","1978","6384","118594","18.6","40207","6.3","2.0" +"13.","1979","6510","108987","16.7","38217","5.9","2.0" +"14.","1980","6636","116912","17.6","41663","6.3","1.9" +"15.","1981","6840","122221","17.9","40245","5.9","3.1" +"16.","1982","7052","125993","17.9","44732","6.3","3.1" +"17.","1983","7204","128576","17.8","46579","6.5","2.2" +"18.","1984","7356","134628","18.3","50571","6.9","2.1" +"19.","1985","7509","139657","18.6","52811","7.0","2.1" +"20.","1986","7661","147023","19.2","54357","7.1","2.0" +"21.","1987","7814","152314","19.5","58568","7.5","2.0" +"22.","1988","7966","163522","20.5","64270","8.1","1.9" +"23.","1989","8118","169066","20.8","68744","8.5","1.9" +"24.","1990","8270","174401","21.1","73911","8.9","1.9" +"25.","1991","8496","188003","22.1","78450","9.2","2.7" +"26.","1992","8677","194910","22.5","80149","9.2","2.1" +"27.","1993","8838","192357","21.8","84244","9.5","1.9" +"28.","1994","8997","190435","21.2","89195","9.9","1.8" +"29.","1995","9160","222487","24.3","89178","9.7","1.8" +"30.","1996","9319","220094","23.6","88241","9.5","1.7" +"31.","1997","9552","233903","24.5","95829","10.0","2.5" +"32.","1998","9709","258409","26.6","104713","10.8","1.6" +"33.","1999","9866","271918","27.6","110587","11.2","1.6" +"34.","2000","10021","255883","25.5","108593","10.8","1.6" +"35.","2001","10270","271019","26.4","108506","10.6","2.5" +"36.","2002","10506","260122","24.8","110417","10.5","2.3" +"37.","2003","10682","259625","24.3","110851","10.4","1.7" +"38.","2004","10856","277263","25.5","113697","10.5","1.6" +"39.","2005","11028","294175","26.7","113914","10.3","1.6" +"40.","2006","11198","314704","28.1","118112","10.5","1.5" +"41.","2007","11366","340794","30.0","122637","10.8","1.5" +"42.","2008","11531","342309","29.7","125017","10.8","1.4" +"43.","2009","11694","357021","30.5","127151","10.9","1.4" +"44.","2010","11858","384649","32.4","134599","11.4","1.4" +"45.","2011","12102","390884","32.3","135585","11.2","2.1" +"46.","2012","12134","394982","32.6","135445","11.2","1.0" +"47.","2013","12288","400517","32.6","134799","11.0","1.0" diff --git a/docs/benchmark/lattice/rotated/rotated-data-pdfplumber.csv b/docs/benchmark/lattice/rotated/rotated-data-pdfplumber.csv new file mode 100644 index 0000000..e274cc5 --- /dev/null +++ b/docs/benchmark/lattice/rotated/rotated-data-pdfplumber.csv @@ -0,0 +1,18 @@ +"0","1","2" +"","e +bl +a +ail +v +a + +t +o +n + +a +t +a +D + +*","" diff --git a/docs/benchmark/lattice/twotables_1/twotables_1-data-pdfplumber.csv b/docs/benchmark/lattice/twotables_1/twotables_1-data-pdfplumber.csv new file mode 100644 index 0000000..3f296db --- /dev/null +++ b/docs/benchmark/lattice/twotables_1/twotables_1-data-pdfplumber.csv @@ -0,0 +1,3 @@ +"0" +"Sl." +"No." diff --git a/docs/benchmark/lattice/twotables_2/twotables_2-data-pdfplumber.csv b/docs/benchmark/lattice/twotables_2/twotables_2-data-pdfplumber.csv new file mode 100644 index 0000000..22c00d2 --- /dev/null +++ b/docs/benchmark/lattice/twotables_2/twotables_2-data-pdfplumber.csv @@ -0,0 +1,3 @@ +"0" +"Table 6 : DISTRIBUTION (%) OF HOUSEHOLDS BY LITERACY STATUS OF" +"MALE HEAD OF THE HOUSEHOLD" diff --git a/docs/benchmark/stream/district_health/district_health-data-pdfplumber.csv b/docs/benchmark/stream/district_health/district_health-data-pdfplumber.csv new file mode 100644 index 0000000..cc079c0 --- /dev/null +++ b/docs/benchmark/stream/district_health/district_health-data-pdfplumber.csv @@ -0,0 +1,66 @@ +"0","1","2","3","4" +"","DLHS-4 (2012-13)","","DLHS-3 (2007-08)","" +"Indicators","TOTAL","RURAL","TOTAL","RURAL" +"Child feeding practices (based on last-born child in the reference period) (%)","","","","" +"Children age 0-5 months exclusively breastfed9 .......................................................................... 76.9 80.0 +Children age 6-9 months receiving solid/semi-solid food and breast milk .................................... 78.6 75.0 +Children age 12-23 months receiving breast feeding along with complementary feeding ........... 31.8 24.2 +Children age 6-35 months exclusively breastfed for at least 6 months ........................................ 4.7 3.4 +Children under 3 years breastfed within one hour of birth ............................................................ 42.9 46.5","","","NA","NA" +"","","","85.9","89.3" +"","","","NA","NA" +"","","","30.0","27.7" +"","","","50.6","52.9" +"Birth Weight (%) (age below 36 months)","","","","" +"Percentage of Children weighed at birth ...................................................................................... 38.8 41.0 NA NA +Percentage of Children with low birth weight (out of those who weighted) ( below 2.5 kg) ......... 12.8 14.6 NA NA","","","","" +"Awareness about Diarrhoea (%)","","","","" +"Women know about what to do when a child gets diarrhoea ..................................................... 96.3 96.2","","","94.4","94.2" +"Awareness about ARI (%)","","","","" +"Women aware about danger signs of ARI10 ................................................................................. 55.9 59.7","","","32.8","34.7" +"Treatment of childhood diseases (based on last two surviving children born during the","","","","" +"","","","","" +"reference period) (%)","","","","" +"","","","","" +"Prevalence of diarrhoea in last 2 weeks for under 5 years old children ....................................... 1.6 1.3 6.5 7.0 +Children with diarrhoea in the last 2 weeks and received ORS11 ................................................. 100.0 100.0 54.8 53.3 +Children with diarrhoea in the last 2 weeks and sought advice/treatment ................................... 100.0 50.0 72.9 73.3 +Prevalence of ARI in last 2 weeks for under 5 years old children ............................................ 4.3 3.9 3.9 4.2 +Children with acute respiratory infection or fever in last 2 weeks and sought advice/treatment 37.5 33.3 69.8 68.0 +Children with diarrhoea in the last 2 weeks given Zinc along with ORS ...................................... 66.6 50.0 NA NA","","","6.5","7.0" +"","","","54.8","53.3" +"","","","72.9","73.3" +"","","","3.9","4.2" +"","","","69.8","68.0" +"Awareness of RTI/STI and HIV/AIDS (%)","","","","" +"Women who have heard of RTI/STI ............................................................................................. 55.8 57.1 +Women who have heard of HIV/AIDS .......................................................................................... 98.9 99.0 +Women who have any symptoms of RTI/STI .............................................................................. 13.9 13.5 +Women who know the place to go for testing of HIV/AIDS12 ....................................................... 59.9 57.1 +Women underwent test for detecting HIV/AIDS12 ........................................................................ 37.3 36.8","","","34.8","38.2" +"","","","98.3","98.1" +"","","","15.6","16.1" +"","","","48.6","46.3" +"","","","14.1","12.3" +"Utilization of Government Health Services (%)","","","","" +"Antenatal care .............................................................................................................................. 69.7 66.7 79.0 81.0 +Treatment for pregnancy complications ....................................................................................... 57.1 59.3 88.0 87.8 +Treatment for post-delivery complications ................................................................................... 33.3 33.3 68.4 68.4 +Treatment for vaginal discharge ................................................................................................... 20.0 25.0 73.9 71.4 +Treatment for children with diarrhoea13 ........................................................................................ 50.0 100.0 NA NA +Treatment for children with ARI13 ................................................................................................. NA NA NA NA","","","79.0","81.0" +"","","","88.0","87.8" +"","","","68.4","68.4" +"","","","73.9","71.4" +"Birth Registration (%)","","","","" +"Children below age 5 years having birth registration done .......................................................... 40.6 44.3 NA NA +Children below age 5 years who received birth certificate (out of those registered) .................... 65.9 63.6 NA NA","","","","" +"Personal Habits (age 15 years and above) (%)","","","","" +"Men who use any kind of smokeless tobacco ............................................................................. 74.6 74.2 NA NA +Women who use any kind of smokeless tobacco ........................................................................ 59.5 58.9 NA NA +Men who smoke ........................................................................................................................... 56.0 56.4 NA NA +Women who smoke ...................................................................................................................... 18.4 18.0 NA NA +Men who consume alcohol ........................................................................................................... 58.4 58.2 NA NA +Women who consume alcohol ..................................................................................................... 10.9 9.3 NA NA","","","","" +"9 Children Who were given nothing but breast milk till the survey date 10Acute Respiratory Infections11Oral Rehydration Solutions/Salts.12Based on","","","","" +"the women who have heard of HIV/AIDS.13 Last two weeks","","","","" diff --git a/docs/benchmark/stream/health/health-data-pdfplumber.csv b/docs/benchmark/stream/health/health-data-pdfplumber.csv new file mode 100644 index 0000000..36c67b0 --- /dev/null +++ b/docs/benchmark/stream/health/health-data-pdfplumber.csv @@ -0,0 +1,44 @@ +"0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23" +"","Table: 5 Public Health Outlay 2012-13 (Budget Estimates) (Rs. in 000)","","","","","","","","","","","","","","","","","","","","","","" +"","States-A","","","Revenue","","","","","","Capital","","","","","","Total","","","Others(1)","","","Total","" +"","","","","","","","","","","","","","","","","Revenue &","","","","","","","" +"","","","Medical & Family Medical & Family +Public Welfare Public Welfare +Health Health","","","","","","","","","","","","","","","","","","","","" +"","","","","","","","","","","","","","","","","Capital","","","","","","","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"","Andhra Pradesh","","","47,824,589","","","9,967,837","","","1,275,000","","","15,000","","","59,082,426","","","14,898,243","","","73,980,669","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Arunachal Pradesh 2,241,609 107,549 23,000 0 2,372,158 86,336 2,458,494","","","","","","","","","","","","","","","","","","","","","","","" +"","Assam","","","14,874,821","","","2,554,197","","","161,600","","","0","","","17,590,618","","","4,408,505","","","21,999,123","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Bihar 21,016,708 4,332,141 5,329,000 0 30,677,849 2,251,571 32,929,420","","","","","","","","","","","","","","","","","","","","","","","" +"","Chhattisgarh","","","11,427,311","","","1,415,660","","","2,366,592","","","0","","","15,209,563","","","311,163","","","15,520,726","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Delhi 28,084,780 411,700 4,550,000 0 33,046,480 5,000 33,051,480","","","","","","","","","","","","","","","","","","","","","","","" +"","Goa","","","4,055,567","","","110,000","","","330,053","","","0","","","4,495,620","","","12,560","","","4,508,180","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Gujarat 26,328,400 6,922,900 12,664,000 42,000 45,957,300 455,860 46,413,160","","","","","","","","","","","","","","","","","","","","","","","" +"","Haryana","","","15,156,681","","","1,333,527","","","40,100","","","0","","","16,530,308","","","1,222,698","","","17,753,006","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Himachal Pradesh 8,647,229 1,331,529 580,800 0 10,559,558 725,315 11,284,873","","","","","","","","","","","","","","","","","","","","","","","" +"","Jammu & Kashmir","","","14,411,984","","","270,840","","","3,188,550","","","0","","","17,871,374","","","166,229","","","18,037,603","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Jharkhand 8,185,079 3,008,077 3,525,558 0 14,718,714 745,139 15,463,853","","","","","","","","","","","","","","","","","","","","","","","" +"","Karnataka","","","34,939,843","","","4,317,801","","","3,669,700","","","0","","","42,927,344","","","631,088","","","43,558,432","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Kerala 27,923,965 3,985,473 929,503 0 32,838,941 334,640 33,173,581","","","","","","","","","","","","","","","","","","","","","","","" +"","Madhya Pradesh","","","28,459,540","","","4,072,016","","","3,432,711","","","0","","","35,964,267","","","472,139","","","36,436,406","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Maharashtra 55,011,100 6,680,721 5,038,576 0 66,730,397 313,762 67,044,159","","","","","","","","","","","","","","","","","","","","","","","" +"","Manipur","","","2,494,600","","","187,700","","","897,400","","","0","","","3,579,700","","","0","","","3,579,700","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Meghalaya 2,894,093 342,893 705,500 5,000 3,947,486 24,128 3,971,614","","","","","","","","","","","","","","","","","","","","","","","" +"","Mizoram","","","1,743,501","","","84,185","","","10,250","","","0","","","1,837,936","","","17,060","","","1,854,996","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Nagaland 2,368,724 204,329 226,400 0 2,799,453 783,054 3,582,507","","","","","","","","","","","","","","","","","","","","","","","" +"","Odisha","","","14,317,179","","","2,552,292","","","1,107,250","","","0","","","17,976,721","","","451,438","","","18,428,159","" +"","","","","","","","","","","","","","","","","","","","","","","","" +"Puducherry 4,191,757 52,249 192,400 0 4,436,406 2,173 4,438,579","","","","","","","","","","","","","","","","","","","","","","","" +"","Punjab","","","19,775,485","","","2,208,343","","","2,470,882","","","0","","","24,454,710","","","1,436,522","","","25,891,232","" +"","","","","","","","","","","","","","","","","","","","","","","","" diff --git a/docs/benchmark/stream/missing_values/missing_values-data-pdfplumber.csv b/docs/benchmark/stream/missing_values/missing_values-data-pdfplumber.csv new file mode 100644 index 0000000..e8c5eff --- /dev/null +++ b/docs/benchmark/stream/missing_values/missing_values-data-pdfplumber.csv @@ -0,0 +1,71 @@ +"0","1","2","3","4" +"","DLHS-4 (2012-13)","","DLHS-3 (2007-08)","" +"Indicators","TOTAL","RURAL","TOTAL","RURAL" +"Reported Prevalence of Morbidity","","","","" +"Any Injury ..................................................................................................................................... 1.9 2.1 +Acute Illness ................................................................................................................................. 4.5 5.6 +Chronic Illness .............................................................................................................................. 5.1 4.1","","","","" +"","","","","" +"","","","","" +"Reported Prevalence of Chronic Illness during last one year (%)","","","","" +"Disease of respiratory system ...................................................................................................... 11.7 15.0 +Disease of cardiovascular system ................................................................................................ 8.9 9.3 +Persons suffering from tuberculosis ............................................................................................. 2.2 1.5","","","","" +"","","","","" +"","","","","" +"Anaemia Status by Haemoglobin Level14 (%)","","","","" +"Children (6-59 months) having anaemia ...................................................................................... 68.5 71.9 +Children (6-59 months) having severe anaemia .......................................................................... 6.7 9.4 +Children (6-9 Years) having anaemia - Male ................................................................................ 67.1 71.4 +Children (6-9 Years) having severe anaemia - Male .................................................................... 4.4 2.4 +Children (6-9 Years) having anaemia - Female ........................................................................... 52.4 48.8 +Children (6-9 Years) having severe anaemia - Female ................................................................ 1.2 0.0 +Children (6-14 years) having anaemia - Male ............................................................................. 50.8 62.5 +Children (6-14 years) having severe anaemia - Male .................................................................. 3.7 3.6 +Children (6-14 years) having anaemia - Female ......................................................................... 48.3 50.0 +Children (6-14 years) having severe anaemia - Female .............................................................. 4.3 6.1 +Children (10-19 Years15) having anaemia - Male ......................................................................... 37.9 51.2 +Children (10-19 Years15) having severe anaemia - Male ............................................................. 3.5 4.0 +Children (10-19 Years15) having anaemia - Female ..................................................................... 46.6 52.1 +Children (10-19 Years15) having severe anaemia - Female ......................................................... 6.4 6.5 +Adolescents (15-19 years) having anaemia ................................................................................ 39.4 46.5 +Adolescents (15-19 years) having severe anaemia ..................................................................... 5.4 5.1 +Pregnant women (15-49 aged) having anaemia .......................................................................... 48.8 51.5 +Pregnant women (15-49 aged) having severe anaemia .............................................................. 7.1 8.8 +Women (15-49 aged) having anaemia ......................................................................................... 45.2 51.7 +Women (15-49 aged) having severe anaemia ............................................................................. 4.8 5.9 +Persons (20 years and above) having anaemia ........................................................................... 37.8 42.1 +Persons (20 years and above) having Severe anaemia .............................................................. 4.6 4.8","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"","","","","" +"Blood Sugar Level (age 18 years and above) (%)","","","","" +"Blood Sugar Level >140 mg/dl (high) ........................................................................................... 12.9 11.1 +Blood Sugar Level >160 mg/dl (very high) ................................................................................... 7.0 5.1","","","","" +"","","","","" +"Hypertension (age 18 years and above) (%)","","","","" +"Above Normal Range (Systolic >140 mm of Hg & Diastolic >90 mm of Hg ) .............................. 23.8 22.8 +Moderately High (Systolic >160 mm of Hg & Diastolic >100 mm of Hg ) ..................................... 8.2 7.1 +Very High (Systolic >180 mm of Hg & Diastolic >110 mm of Hg ) ............................................... 3.7 3.1","","","","" +"","","","","" +"","","","","" +"14 Any anaemia below 11g/dl, severe anaemia below 7g/dl. 15 Excluding age group 19 years","","","","" +"Chronic Illness :Any person with symptoms persisting for longer than one month is defined as suffering from chronic illness","","","","" From e45e7478bfa6931f951cbd3069a641c8319f61e2 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 11 Dec 2018 21:16:16 +0530 Subject: [PATCH 70/89] Add updated stream benchmark --- .../stream/12s0324/12s0324-page-1-table-1.csv | 43 +++++++++++++++++++ .../stream/12s0324/12s0324-page-1-table-2.csv | 41 ++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 docs/benchmark/stream/12s0324/12s0324-page-1-table-1.csv create mode 100644 docs/benchmark/stream/12s0324/12s0324-page-1-table-2.csv diff --git a/docs/benchmark/stream/12s0324/12s0324-page-1-table-1.csv b/docs/benchmark/stream/12s0324/12s0324-page-1-table-1.csv new file mode 100644 index 0000000..56e45eb --- /dev/null +++ b/docs/benchmark/stream/12s0324/12s0324-page-1-table-1.csv @@ -0,0 +1,43 @@ +"[In thousands (11,062.6 represents 11,062,600) For year ending December 31. Based on Uniform Crime Reporting (UCR)","","","","","","","","","" +"Program. Represents arrests reported (not charged) by 12,910 agencies with a total population of 247,526,916 as estimated","","","","","","","","","" +"by the FBI. Some persons may be arrested more than once during a year, therefore, the data in this table, in some cases,","","","","","","","","","" +"could represent multiple arrests of the same person. See text, this section and source]","","","","","","","","","" +"","","Total","","","Male","","","Female","" +"Offense charged","","Under 18","18 years","","Under 18","18 years","","Under 18","18 years" +"","Total","years","and over","Total","years","and over","Total","years","and over" +"Total . . . . . . . . . . . . . . . . . . . . . . . . .","11,062 .6","1,540 .0","9,522 .6","8,263 .3","1,071 .6","7,191 .7","2,799 .2","468 .3","2,330 .9" +"Violent crime . . . . . . . . . . . . . . . . . .","467 .9","69 .1","398 .8","380 .2","56 .5","323 .7","87 .7","12 .6","75 .2" +"Murder and nonnegligent","","","","","","","","","" +"manslaughter . . . . . . . .. .. .. .. ..","10.0","0.9","9.1","9.0","0.9","8.1","1.1","–","1.0" +"Forcible rape . . . . . . . .. .. .. .. .. .","17.5","2.6","14.9","17.2","2.5","14.7","–","–","–" +"Robbery . . . .. .. . .. . ... . ... . ...","102.1","25.5","76.6","90.0","22.9","67.1","12.1","2.5","9.5" +"Aggravated assault . . . . . . . .. .. ..","338.4","40.1","298.3","264.0","30.2","233.8","74.4","9.9","64.5" +"Property crime . . . . . . . . . . . . . . . . .","1,396 .4","338 .7","1,057 .7","875 .9","210 .8","665 .1","608 .2","127 .9","392 .6" +"Burglary . .. . . . . .. ... .... .... ..","240.9","60.3","180.6","205.0","53.4","151.7","35.9","6.9","29.0" +"Larceny-theft . . . . . . . .. .. .. .. .. .","1,080.1","258.1","822.0","608.8","140.5","468.3","471.3","117.6","353.6" +"Motor vehicle theft . . . . .. .. . .... .","65.6","16.0","49.6","53.9","13.3","40.7","11.7","2.7","8.9" +"Arson .. . . . .. . ... .... .... .... .","9.8","4.3","5.5","8.1","3.7","4.4","1.7","0.6","1.1" +"Other assaults .. . . . . .. . ... . ... ..","1,061.3","175.3","886.1","785.4","115.4","670.0","276.0","59.9","216.1" +"Forgery and counterfeiting .. . . . . . ..","68.9","1.7","67.2","42.9","1.2","41.7","26.0","0.5","25.5" +"Fraud .... .. . . .. ... .... .... ....","173.7","5.1","168.5","98.4","3.3","95.0","75.3","1.8","73.5" +"Embezzlement . . .. . . . .. . ... . ....","14.6","–","14.1","7.2","–","6.9","7.4","–","7.2" +"Stolen property 1 . . . . . . .. . .. .. ...","84.3","15.1","69.2","66.7","12.2","54.5","17.6","2.8","14.7" +"Vandalism . . . . . . . .. .. .. .. .. ....","217.4","72.7","144.7","178.1","62.8","115.3","39.3","9.9","29.4" +"Weapons; carrying, possessing, etc. .","132.9","27.1","105.8","122.1","24.3","97.8","10.8","2.8","8.0" +"Prostitution and commercialized vice","56.9","1.1","55.8","17.3","–","17.1","39.6","0.8","38.7" +"Sex offenses 2 . . . . .. . . . .. .. .. . ..","61.5","10.7","50.7","56.1","9.6","46.5","5.4","1.1","4.3" +"Drug abuse violations . . . . . . . .. ...","1,333.0","136.6","1,196.4","1,084.3","115.2","969.1","248.7","21.4","227.3" +"Gambling .. . . . . .. ... . ... . ... ...","8.2","1.4","6.8","7.2","1.4","5.9","0.9","–","0.9" +"Offenses against the family and","","","","","","","","","" +"children . . . .. . . .. .. .. .. .. .. . ..","92.4","3.7","88.7","68.9","2.4","66.6","23.4","1.3","22.1" +"Driving under the influence . . . . . .. .","1,158.5","109.2","1,147.5","895.8","8.2","887.6","262.7","2.7","260.0" +"Liquor laws . . . . . . . .. .. .. .. .. .. .","48.2","90.2","368.0","326.8","55.4","271.4","131.4","34.7","96.6" +"Drunkenness . . .. . . . .. . ... . ... ..","488.1","11.4","476.8","406.8","8.5","398.3","81.3","2.9","78.4" +"Disorderly conduct . .. . . . . . .. .. .. .","529.5","136.1","393.3","387.1","90.8","296.2","142.4","45.3","97.1" +"Vagrancy . . . .. . . . ... .... .... ...","26.6","2.2","24.4","20.9","1.6","19.3","5.7","0.6","5.1" +"All other offenses (except traffic) . . ..","306.1","263.4","2,800.8","2,337.1","194.2","2,142.9","727.0","69.2","657.9" +"Suspicion . . . .. . . .. .. .. .. .. .. . ..","1.6","–","1.4","1.2","–","1.0","–","–","–" +"Curfew and loitering law violations ..","91.0","91.0","(X)","63.1","63.1","(X)","28.0","28.0","(X)" +"Runaways . . . . . . . .. .. .. .. .. ....","75.8","75.8","(X)","34.0","34.0","(X)","41.8","41.8","(X)" +"","– Represents zero. X Not applicable. 1 Buying, receiving, possessing stolen property. 2 Except forcible rape and prostitution.","","","","","","","","" +"","Source: U.S. Department of Justice, Federal Bureau of Investigation, Uniform Crime Reports, Arrests Master Files.","","","","","","","","" diff --git a/docs/benchmark/stream/12s0324/12s0324-page-1-table-2.csv b/docs/benchmark/stream/12s0324/12s0324-page-1-table-2.csv new file mode 100644 index 0000000..5455767 --- /dev/null +++ b/docs/benchmark/stream/12s0324/12s0324-page-1-table-2.csv @@ -0,0 +1,41 @@ +"","Source: U.S. Department of Justice, Federal Bureau of Investigation, Uniform Crime Reports, Arrests Master Files.","","","","" +"Table 325. Arrests by Race: 2009","","","","","" +"[Based on Uniform Crime Reporting (UCR) Program. Represents arrests reported (not charged) by 12,371 agencies","","","","","" +"with a total population of 239,839,971 as estimated by the FBI. See headnote, Table 324]","","","","","" +"","","","","American","" +"Offense charged","","","","Indian/Alaskan","Asian Pacific" +"","Total","White","Black","Native","Islander" +"Total . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .","10,690,561","7,389,208","3,027,153","150,544","123,656" +"Violent crime . . . . . . . . . . . . . . . . . . . . . . . . . . . .","456,965","268,346","177,766","5,608","5,245" +"Murder and nonnegligent manslaughter . .. ... .","9,739","4,741","4,801","100","97" +"Forcible rape . . . . . . . .. .. .. .. .... .. ...... .","16,362","10,644","5,319","169","230" +"Robbery . . . . .. . . . ... . ... . .... .... .... . . .","100,496","43,039","55,742","726","989" +"Aggravated assault . . . . . . . .. .. ...... .. ....","330,368","209,922","111,904","4,613","3,929" +"Property crime . . . . . . . . . . . . . . . . . . . . . . . . . . .","1,364,409","922,139","406,382","17,599","18,289" +"Burglary . . .. . . . .. . .... .... .... .... ... . . .","234,551","155,994","74,419","2,021","2,117" +"Larceny-theft . . . . . . . .. .. .. .. .... .. ...... .","1,056,473","719,983","306,625","14,646","15,219" +"Motor vehicle theft . . . . . .. ... . ... ..... ... ..","63,919","39,077","23,184","817","841" +"Arson .. . . .. .. .. ... .... .... .... .... . . . . .","9,466","7,085","2,154","115","112" +"Other assaults .. . . . . . ... . ... . ... ..... ... ..","1,032,502","672,865","332,435","15,127","12,075" +"Forgery and counterfeiting .. . . . . . ... ..... .. ..","67,054","44,730","21,251","345","728" +"Fraud ... . . . . .. .. .. .. .. .. .. .. .. .... . . . . . .","161,233","108,032","50,367","1,315","1,519" +"Embezzlement . . . .. . . . ... . ... . .... ... .....","13,960","9,208","4,429","75","248" +"Stolen property; buying, receiving, possessing .. .","82,714","51,953","29,357","662","742" +"Vandalism . . . . . . . .. .. .. .. .. .. .... .. ..... .","212,173","157,723","48,746","3,352","2,352" +"Weapons—carrying, possessing, etc. .. .. ... .. .","130,503","74,942","53,441","951","1,169" +"Prostitution and commercialized vice . ... .. .. ..","56,560","31,699","23,021","427","1,413" +"Sex offenses 1 . . . . . . . .. .. .. .. .... .. ...... .","60,175","44,240","14,347","715","873" +"Drug abuse violations . . . . . . . .. . ..... .. .....","1,301,629","845,974","437,623","8,588","9,444" +"Gambling . . . . .. . . . ... . ... . .. ... . ...... .. .","8,046","2,290","5,518","27","211" +"Offenses against the family and children ... .. .. .","87,232","58,068","26,850","1,690","624" +"Driving under the influence . . . . . . .. ... ...... .","1,105,401","954,444","121,594","14,903","14,460" +"Liquor laws . . . . . . . .. .. .. .. .. . ..... .. .....","444,087","373,189","50,431","14,876","5,591" +"Drunkenness . .. . . . . . ... . ... . ..... . .......","469,958","387,542","71,020","8,552","2,844" +"Disorderly conduct . . .. . . . . .. .. . ..... .. .....","515,689","326,563","176,169","8,783","4,174" +"Vagrancy . . .. .. . . .. ... .... .... .... .... . . .","26,347","14,581","11,031","543","192" +"All other offenses (except traffic) . .. .. .. ..... ..","2,929,217","1,937,221","911,670","43,880","36,446" +"Suspicion . . .. . . . .. .. .. .. .. .. .. ...... .. . . .","1,513","677","828","1","7" +"Curfew and loitering law violations . .. ... .. ....","89,578","54,439","33,207","872","1,060" +"Runaways . . . . . . . .. .. .. .. .. .. .... .. ..... .","73,616","48,343","19,670","1,653","3,950" +"1 Except forcible rape and prostitution.","","","","","" +"","Source: U.S. Department of Justice, Federal Bureau of Investigation, “Crime in the United States, Arrests,” September 2010,","","","","" From 649fd67c44b72eff436eafc72dc1e7040b871a62 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 11 Dec 2018 21:17:50 +0530 Subject: [PATCH 71/89] Rename file --- .../12s0324-data-camelot-page-1-table-1.csv | 5 +++ .../12s0324-data-camelot-page-1-table-2.csv | 5 +++ .../stream/12s0324/12s0324-page-1-table-1.csv | 43 ------------------- .../stream/12s0324/12s0324-page-1-table-2.csv | 41 ------------------ 4 files changed, 10 insertions(+), 84 deletions(-) mode change 100755 => 100644 docs/benchmark/stream/12s0324/12s0324-data-camelot-page-1-table-1.csv mode change 100755 => 100644 docs/benchmark/stream/12s0324/12s0324-data-camelot-page-1-table-2.csv delete mode 100644 docs/benchmark/stream/12s0324/12s0324-page-1-table-1.csv delete mode 100644 docs/benchmark/stream/12s0324/12s0324-page-1-table-2.csv diff --git a/docs/benchmark/stream/12s0324/12s0324-data-camelot-page-1-table-1.csv b/docs/benchmark/stream/12s0324/12s0324-data-camelot-page-1-table-1.csv old mode 100755 new mode 100644 index a8cffd6..56e45eb --- a/docs/benchmark/stream/12s0324/12s0324-data-camelot-page-1-table-1.csv +++ b/docs/benchmark/stream/12s0324/12s0324-data-camelot-page-1-table-1.csv @@ -1,3 +1,7 @@ +"[In thousands (11,062.6 represents 11,062,600) For year ending December 31. Based on Uniform Crime Reporting (UCR)","","","","","","","","","" +"Program. Represents arrests reported (not charged) by 12,910 agencies with a total population of 247,526,916 as estimated","","","","","","","","","" +"by the FBI. Some persons may be arrested more than once during a year, therefore, the data in this table, in some cases,","","","","","","","","","" +"could represent multiple arrests of the same person. See text, this section and source]","","","","","","","","","" "","","Total","","","Male","","","Female","" "Offense charged","","Under 18","18 years","","Under 18","18 years","","Under 18","18 years" "","Total","years","and over","Total","years","and over","Total","years","and over" @@ -36,3 +40,4 @@ "Curfew and loitering law violations ..","91.0","91.0","(X)","63.1","63.1","(X)","28.0","28.0","(X)" "Runaways . . . . . . . .. .. .. .. .. ....","75.8","75.8","(X)","34.0","34.0","(X)","41.8","41.8","(X)" "","– Represents zero. X Not applicable. 1 Buying, receiving, possessing stolen property. 2 Except forcible rape and prostitution.","","","","","","","","" +"","Source: U.S. Department of Justice, Federal Bureau of Investigation, Uniform Crime Reports, Arrests Master Files.","","","","","","","","" diff --git a/docs/benchmark/stream/12s0324/12s0324-data-camelot-page-1-table-2.csv b/docs/benchmark/stream/12s0324/12s0324-data-camelot-page-1-table-2.csv old mode 100755 new mode 100644 index b5b7f28..5455767 --- a/docs/benchmark/stream/12s0324/12s0324-data-camelot-page-1-table-2.csv +++ b/docs/benchmark/stream/12s0324/12s0324-data-camelot-page-1-table-2.csv @@ -1,3 +1,7 @@ +"","Source: U.S. Department of Justice, Federal Bureau of Investigation, Uniform Crime Reports, Arrests Master Files.","","","","" +"Table 325. Arrests by Race: 2009","","","","","" +"[Based on Uniform Crime Reporting (UCR) Program. Represents arrests reported (not charged) by 12,371 agencies","","","","","" +"with a total population of 239,839,971 as estimated by the FBI. See headnote, Table 324]","","","","","" "","","","","American","" "Offense charged","","","","Indian/Alaskan","Asian Pacific" "","Total","White","Black","Native","Islander" @@ -34,3 +38,4 @@ "Curfew and loitering law violations . .. ... .. ....","89,578","54,439","33,207","872","1,060" "Runaways . . . . . . . .. .. .. .. .. .. .... .. ..... .","73,616","48,343","19,670","1,653","3,950" "1 Except forcible rape and prostitution.","","","","","" +"","Source: U.S. Department of Justice, Federal Bureau of Investigation, “Crime in the United States, Arrests,” September 2010,","","","","" diff --git a/docs/benchmark/stream/12s0324/12s0324-page-1-table-1.csv b/docs/benchmark/stream/12s0324/12s0324-page-1-table-1.csv deleted file mode 100644 index 56e45eb..0000000 --- a/docs/benchmark/stream/12s0324/12s0324-page-1-table-1.csv +++ /dev/null @@ -1,43 +0,0 @@ -"[In thousands (11,062.6 represents 11,062,600) For year ending December 31. Based on Uniform Crime Reporting (UCR)","","","","","","","","","" -"Program. Represents arrests reported (not charged) by 12,910 agencies with a total population of 247,526,916 as estimated","","","","","","","","","" -"by the FBI. Some persons may be arrested more than once during a year, therefore, the data in this table, in some cases,","","","","","","","","","" -"could represent multiple arrests of the same person. See text, this section and source]","","","","","","","","","" -"","","Total","","","Male","","","Female","" -"Offense charged","","Under 18","18 years","","Under 18","18 years","","Under 18","18 years" -"","Total","years","and over","Total","years","and over","Total","years","and over" -"Total . . . . . . . . . . . . . . . . . . . . . . . . .","11,062 .6","1,540 .0","9,522 .6","8,263 .3","1,071 .6","7,191 .7","2,799 .2","468 .3","2,330 .9" -"Violent crime . . . . . . . . . . . . . . . . . .","467 .9","69 .1","398 .8","380 .2","56 .5","323 .7","87 .7","12 .6","75 .2" -"Murder and nonnegligent","","","","","","","","","" -"manslaughter . . . . . . . .. .. .. .. ..","10.0","0.9","9.1","9.0","0.9","8.1","1.1","–","1.0" -"Forcible rape . . . . . . . .. .. .. .. .. .","17.5","2.6","14.9","17.2","2.5","14.7","–","–","–" -"Robbery . . . .. .. . .. . ... . ... . ...","102.1","25.5","76.6","90.0","22.9","67.1","12.1","2.5","9.5" -"Aggravated assault . . . . . . . .. .. ..","338.4","40.1","298.3","264.0","30.2","233.8","74.4","9.9","64.5" -"Property crime . . . . . . . . . . . . . . . . .","1,396 .4","338 .7","1,057 .7","875 .9","210 .8","665 .1","608 .2","127 .9","392 .6" -"Burglary . .. . . . . .. ... .... .... ..","240.9","60.3","180.6","205.0","53.4","151.7","35.9","6.9","29.0" -"Larceny-theft . . . . . . . .. .. .. .. .. .","1,080.1","258.1","822.0","608.8","140.5","468.3","471.3","117.6","353.6" -"Motor vehicle theft . . . . .. .. . .... .","65.6","16.0","49.6","53.9","13.3","40.7","11.7","2.7","8.9" -"Arson .. . . . .. . ... .... .... .... .","9.8","4.3","5.5","8.1","3.7","4.4","1.7","0.6","1.1" -"Other assaults .. . . . . .. . ... . ... ..","1,061.3","175.3","886.1","785.4","115.4","670.0","276.0","59.9","216.1" -"Forgery and counterfeiting .. . . . . . ..","68.9","1.7","67.2","42.9","1.2","41.7","26.0","0.5","25.5" -"Fraud .... .. . . .. ... .... .... ....","173.7","5.1","168.5","98.4","3.3","95.0","75.3","1.8","73.5" -"Embezzlement . . .. . . . .. . ... . ....","14.6","–","14.1","7.2","–","6.9","7.4","–","7.2" -"Stolen property 1 . . . . . . .. . .. .. ...","84.3","15.1","69.2","66.7","12.2","54.5","17.6","2.8","14.7" -"Vandalism . . . . . . . .. .. .. .. .. ....","217.4","72.7","144.7","178.1","62.8","115.3","39.3","9.9","29.4" -"Weapons; carrying, possessing, etc. .","132.9","27.1","105.8","122.1","24.3","97.8","10.8","2.8","8.0" -"Prostitution and commercialized vice","56.9","1.1","55.8","17.3","–","17.1","39.6","0.8","38.7" -"Sex offenses 2 . . . . .. . . . .. .. .. . ..","61.5","10.7","50.7","56.1","9.6","46.5","5.4","1.1","4.3" -"Drug abuse violations . . . . . . . .. ...","1,333.0","136.6","1,196.4","1,084.3","115.2","969.1","248.7","21.4","227.3" -"Gambling .. . . . . .. ... . ... . ... ...","8.2","1.4","6.8","7.2","1.4","5.9","0.9","–","0.9" -"Offenses against the family and","","","","","","","","","" -"children . . . .. . . .. .. .. .. .. .. . ..","92.4","3.7","88.7","68.9","2.4","66.6","23.4","1.3","22.1" -"Driving under the influence . . . . . .. .","1,158.5","109.2","1,147.5","895.8","8.2","887.6","262.7","2.7","260.0" -"Liquor laws . . . . . . . .. .. .. .. .. .. .","48.2","90.2","368.0","326.8","55.4","271.4","131.4","34.7","96.6" -"Drunkenness . . .. . . . .. . ... . ... ..","488.1","11.4","476.8","406.8","8.5","398.3","81.3","2.9","78.4" -"Disorderly conduct . .. . . . . . .. .. .. .","529.5","136.1","393.3","387.1","90.8","296.2","142.4","45.3","97.1" -"Vagrancy . . . .. . . . ... .... .... ...","26.6","2.2","24.4","20.9","1.6","19.3","5.7","0.6","5.1" -"All other offenses (except traffic) . . ..","306.1","263.4","2,800.8","2,337.1","194.2","2,142.9","727.0","69.2","657.9" -"Suspicion . . . .. . . .. .. .. .. .. .. . ..","1.6","–","1.4","1.2","–","1.0","–","–","–" -"Curfew and loitering law violations ..","91.0","91.0","(X)","63.1","63.1","(X)","28.0","28.0","(X)" -"Runaways . . . . . . . .. .. .. .. .. ....","75.8","75.8","(X)","34.0","34.0","(X)","41.8","41.8","(X)" -"","– Represents zero. X Not applicable. 1 Buying, receiving, possessing stolen property. 2 Except forcible rape and prostitution.","","","","","","","","" -"","Source: U.S. Department of Justice, Federal Bureau of Investigation, Uniform Crime Reports, Arrests Master Files.","","","","","","","","" diff --git a/docs/benchmark/stream/12s0324/12s0324-page-1-table-2.csv b/docs/benchmark/stream/12s0324/12s0324-page-1-table-2.csv deleted file mode 100644 index 5455767..0000000 --- a/docs/benchmark/stream/12s0324/12s0324-page-1-table-2.csv +++ /dev/null @@ -1,41 +0,0 @@ -"","Source: U.S. Department of Justice, Federal Bureau of Investigation, Uniform Crime Reports, Arrests Master Files.","","","","" -"Table 325. Arrests by Race: 2009","","","","","" -"[Based on Uniform Crime Reporting (UCR) Program. Represents arrests reported (not charged) by 12,371 agencies","","","","","" -"with a total population of 239,839,971 as estimated by the FBI. See headnote, Table 324]","","","","","" -"","","","","American","" -"Offense charged","","","","Indian/Alaskan","Asian Pacific" -"","Total","White","Black","Native","Islander" -"Total . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .","10,690,561","7,389,208","3,027,153","150,544","123,656" -"Violent crime . . . . . . . . . . . . . . . . . . . . . . . . . . . .","456,965","268,346","177,766","5,608","5,245" -"Murder and nonnegligent manslaughter . .. ... .","9,739","4,741","4,801","100","97" -"Forcible rape . . . . . . . .. .. .. .. .... .. ...... .","16,362","10,644","5,319","169","230" -"Robbery . . . . .. . . . ... . ... . .... .... .... . . .","100,496","43,039","55,742","726","989" -"Aggravated assault . . . . . . . .. .. ...... .. ....","330,368","209,922","111,904","4,613","3,929" -"Property crime . . . . . . . . . . . . . . . . . . . . . . . . . . .","1,364,409","922,139","406,382","17,599","18,289" -"Burglary . . .. . . . .. . .... .... .... .... ... . . .","234,551","155,994","74,419","2,021","2,117" -"Larceny-theft . . . . . . . .. .. .. .. .... .. ...... .","1,056,473","719,983","306,625","14,646","15,219" -"Motor vehicle theft . . . . . .. ... . ... ..... ... ..","63,919","39,077","23,184","817","841" -"Arson .. . . .. .. .. ... .... .... .... .... . . . . .","9,466","7,085","2,154","115","112" -"Other assaults .. . . . . . ... . ... . ... ..... ... ..","1,032,502","672,865","332,435","15,127","12,075" -"Forgery and counterfeiting .. . . . . . ... ..... .. ..","67,054","44,730","21,251","345","728" -"Fraud ... . . . . .. .. .. .. .. .. .. .. .. .... . . . . . .","161,233","108,032","50,367","1,315","1,519" -"Embezzlement . . . .. . . . ... . ... . .... ... .....","13,960","9,208","4,429","75","248" -"Stolen property; buying, receiving, possessing .. .","82,714","51,953","29,357","662","742" -"Vandalism . . . . . . . .. .. .. .. .. .. .... .. ..... .","212,173","157,723","48,746","3,352","2,352" -"Weapons—carrying, possessing, etc. .. .. ... .. .","130,503","74,942","53,441","951","1,169" -"Prostitution and commercialized vice . ... .. .. ..","56,560","31,699","23,021","427","1,413" -"Sex offenses 1 . . . . . . . .. .. .. .. .... .. ...... .","60,175","44,240","14,347","715","873" -"Drug abuse violations . . . . . . . .. . ..... .. .....","1,301,629","845,974","437,623","8,588","9,444" -"Gambling . . . . .. . . . ... . ... . .. ... . ...... .. .","8,046","2,290","5,518","27","211" -"Offenses against the family and children ... .. .. .","87,232","58,068","26,850","1,690","624" -"Driving under the influence . . . . . . .. ... ...... .","1,105,401","954,444","121,594","14,903","14,460" -"Liquor laws . . . . . . . .. .. .. .. .. . ..... .. .....","444,087","373,189","50,431","14,876","5,591" -"Drunkenness . .. . . . . . ... . ... . ..... . .......","469,958","387,542","71,020","8,552","2,844" -"Disorderly conduct . . .. . . . . .. .. . ..... .. .....","515,689","326,563","176,169","8,783","4,174" -"Vagrancy . . .. .. . . .. ... .... .... .... .... . . .","26,347","14,581","11,031","543","192" -"All other offenses (except traffic) . .. .. .. ..... ..","2,929,217","1,937,221","911,670","43,880","36,446" -"Suspicion . . .. . . . .. .. .. .. .. .. .. ...... .. . . .","1,513","677","828","1","7" -"Curfew and loitering law violations . .. ... .. ....","89,578","54,439","33,207","872","1,060" -"Runaways . . . . . . . .. .. .. .. .. .. .... .. ..... .","73,616","48,343","19,670","1,653","3,950" -"1 Except forcible rape and prostitution.","","","","","" -"","Source: U.S. Department of Justice, Federal Bureau of Investigation, “Crime in the United States, Arrests,” September 2010,","","","","" From 451fac9e53fcc621e02413fda6789d501dc8b6de Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Tue, 11 Dec 2018 21:28:42 +0530 Subject: [PATCH 72/89] Add updated stream benchmark --- ...birdisland-data-camelot-page-1-table-1.csv | 78 ++++++++++--------- ...birdisland-data-camelot-page-1-table-2.csv | 39 ++++++++++ 2 files changed, 82 insertions(+), 35 deletions(-) create mode 100644 docs/benchmark/stream/birdisland/birdisland-data-camelot-page-1-table-2.csv diff --git a/docs/benchmark/stream/birdisland/birdisland-data-camelot-page-1-table-1.csv b/docs/benchmark/stream/birdisland/birdisland-data-camelot-page-1-table-1.csv index a05fcaa..e2fe7c7 100755 --- a/docs/benchmark/stream/birdisland/birdisland-data-camelot-page-1-table-1.csv +++ b/docs/benchmark/stream/birdisland/birdisland-data-camelot-page-1-table-1.csv @@ -1,35 +1,43 @@ -"","","","","","SCN","Seed","Yield","Moisture","Lodgingg","g","Stand","","Gross" -"Company/Brandpy","","Product/Brand†","Technol.†","Mat.","Resist.","Trmt.†","Bu/A","%","%","","(x 1000)(",")","Income" -"KrugerKruger","","K2-1901K2 1901","RR2YRR2Y","1.91.9","RR","Ac,PVAc,PV","56.456.4","7.67.6","00","","126.3126.3","","$846$846" -"StineStine","","19RA02 §19RA02 §","RR2YRR2Y","1 91.9","RR","CMBCMB","55.355.3","7 67.6","00","","120 0120.0","","$830$830" -"WensmanWensman","","W 3190NR2W 3190NR2","RR2YRR2Y","1 91.9","RR","AcAc","54 554.5","7 67.6","00","","119 5119.5","","$818$818" -"H ftHefty","","H17Y12H17Y12","RR2YRR2Y","1 71.7","MRMR","II","53 753.7","7 77.7","00","","124 4124.4","","$806$806" -"Dyna-Gro","","S15RY53","RR2Y","1.5","R","Ac","53.6","7.7","0","","126.8","","$804" -"LG SeedsLG Seeds","","C2050R2C2050R2","RR2YRR2Y","2.12.1","RR","AcAc","53.653.6","7.77.7","00","","123.9123.9","","$804$804" -"Titan ProTitan Pro","","19M4219M42","RR2YRR2Y","1.91.9","RR","CMBCMB","53.653.6","7.77.7","00","","121.0121.0","","$804$804" -"StineStine","","19RA02 (2) §19RA02 (2) §","RR2YRR2Y","1 91.9","RR","CMBCMB","53 453.4","7 77.7","00","","123 9123.9","","$801$801" -"AsgrowAsgrow","","AG1832 §AG1832 §","RR2YRR2Y","1 81.8","MRMR","Ac PVAc,PV","52 952.9","7 77.7","00","","122 0122.0","","$794$794" -"Prairie Brandiid","","PB-1566R2662","RR2Y2","1.5","R","CMB","52.8","7.7","0","","122.9","","$792$" -"Channel","","1901R2","RR2Y","1.9","R","Ac,PV,","52.8","7.6","0","","123.4","","$791$" -"Titan ProTitan Pro","","20M120M1","RR2YRR2Y","2.02.0","RR","AmAm","52.552.5","7.57.5","00","","124.4124.4","","$788$788" -"KrugerKruger","","K2-2002K2-2002","RR2YRR2Y","2 02.0","RR","Ac PVAc,PV","52 452.4","7 97.9","00","","125 4125.4","","$786$786" -"ChannelChannel","","1700R21700R2","RR2YRR2Y","1 71.7","RR","Ac PVAc,PV","52 352.3","7 97.9","00","","123 9123.9","","$784$784" -"H ftHefty","","H16Y11H16Y11","RR2YRR2Y","1 61.6","MRMR","II","51 451.4","7 67.6","00","","123 9123.9","","$771$771" -"Anderson","","162R2Y","RR2Y","1.6","R","None","51.3","7.5","0","","119.5","","$770" -"Titan ProTitan Pro","","15M2215M22","RR2YRR2Y","1.51.5","RR","CMBCMB","51.351.3","7.87.8","00","","125.4125.4","","$769$769" -"DairylandDairyland","","DSR-1710R2YDSR-1710R2Y","RR2YRR2Y","1 71.7","RR","CMBCMB","51 351.3","7 77.7","00","","122 0122.0","","$769$769" -"HeftyHefty","","H20R3H20R3","RR2YRR2Y","2 02.0","MRMR","II","50 550.5","8 28.2","00","","121 0121.0","","$757$757" -"PPrairie BrandiiBd","","PB 1743R2PB-1743R2","RR2YRR2Y","1 71.7","RR","CMBCMB","50 250.2","7 77.7","00","","125 8125.8","","$752$752" -"Gold Country","","1741","RR2Y","1.7","R","Ac","50.1","7.8","0","","123.9","","$751" -"Trelaye ay","","20RR4303","RR2Y","2.00","R","Ac,Exc,","49.99 9","7.66","00","","127.88","","$749$9" -"HeftyHefty","","H14R3H14R3","RR2YRR2Y","1.41.4","MRMR","II","49.749.7","7.77.7","00","","122.9122.9","","$746$746" -"Prairie BrandPrairie Brand","","PB-2099NRR2PB-2099NRR2","RR2YRR2Y","2 02.0","RR","CMBCMB","49 649.6","7 87.8","00","","126 3126.3","","$743$743" -"WensmanWensman","","W 3174NR2W 3174NR2","RR2YRR2Y","1 71.7","RR","AcAc","49 349.3","7 67.6","00","","122 5122.5","","$740$740" -"KKruger","","K2 1602K2-1602","RR2YRR2Y","1 61.6","R","Ac,PV","48.78","7.66","00","","125.412","","$731$31" -"NK Brand","","S18-C2 §§","RR2Y","1.8","R","CMB","48.7","7.7","0","","126.8","","$731$" -"KrugerKruger","","K2-1902K2 1902","RR2YRR2Y","1.91.9","RR","Ac,PVAc,PV","48.748.7","7.57.5","00","","124.4124.4","","$730$730" -"Prairie BrandPrairie Brand","","PB-1823R2PB-1823R2","RR2YRR2Y","1 81.8","RR","NoneNone","48 548.5","7 67.6","00","","121 0121.0","","$727$727" -"Gold CountryGold Country","","15411541","RR2YRR2Y","1 51.5","RR","AcAc","48 448.4","7 67.6","00","","110 4110.4","","$726$726" -"","","","","","","Test Average =","47 647.6","7 77.7","00","","122 9122.9","","$713$713" -"","","","","","","LSD (0.10) =","5.7","0.3","ns","","37.8","","566.4" -"","F.I.R.S.T. Managerg","","","","","C.V. =","8.8","2.9","","","56.4","","846.2" +"","2012 BETTER VARIETIES Harvest Report for Minnesota Central [ MNCE ]2012 BETTER VARIETIES Harvest Report for Minnesota Central [ MNCE ]","","","","","","","","","","","","ALL SEASON TESTALL SEASON TEST","" +"","Doug Toreen, Renville County, MN 55310 [ BIRD ISLAND ]Doug Toreen, Renville County, MN 55310","","","","","[ BIRD ISLAND ]","","","","","","","1.3 - 2.0 MAT. GROUP1.3 - 2.0 MAT. GROUP","" +"PREVPREV. CROP/HERB:","CROP/HERB","C/ S","Corn / Surpass, RoundupR","d","","","","","","","","","","S2MNCE01S2MNCE01" +"SOIL DESCRIPTION:","","C","Canisteo clay loam, mod. well drained, non-irrigated","","","","","","","","","","","" +"SOIL CONDITIONS:","","","High P, high K, 6.7 pH, 3.9% OM, Low SCN","","","","","","","","","","","30"" ROW SPACING" +"TILLAGE/CULTIVATION:TILLAGE/CULTIVATION:","","","conventional w/ fall tillconventional w/ fall till","","","","","","","","","","","" +"PEST MANAGEMENT:PEST MANAGEMENT:","","Roundup twiceRoundup twice","","","","","","","","","","","","" +"SEEDED - RATE:","","May 15M15","140,000 /A140 000 /A","","","","","","","","TOP 30 foTOP 30 for YIELD of 63 TESTED","","YIELD of 63 TESTED","" +"HARVESTEDHARVESTED - STAND:","STAND","O t 3Oct 3","122 921 /A122,921 /A","","","","","","","","","AVERAGE of (3) REPLICATIONSAVERAGE of (3) REPLICATIONS","","" +"","","","","","","SCN","Seed","Yield","Moisture","Lodgingg","g","Stand","","Gross" +"","Company/Brandpy","Product/Brand†","","Technol.†","Mat.","Resist.","Trmt.†","Bu/A","%","%","","(x 1000)(",")","Income" +"","KrugerKruger","K2-1901K2 1901","","RR2YRR2Y","1.91.9","RR","Ac,PVAc,PV","56.456.4","7.67.6","00","","126.3126.3","","$846$846" +"","StineStine","19RA02 §19RA02 §","","RR2YRR2Y","1 91.9","RR","CMBCMB","55.355.3","7 67.6","00","","120 0120.0","","$830$830" +"","WensmanWensman","W 3190NR2W 3190NR2","","RR2YRR2Y","1 91.9","RR","AcAc","54 554.5","7 67.6","00","","119 5119.5","","$818$818" +"","H ftHefty","H17Y12H17Y12","","RR2YRR2Y","1 71.7","MRMR","II","53 753.7","7 77.7","00","","124 4124.4","","$806$806" +"","Dyna-Gro","S15RY53","","RR2Y","1.5","R","Ac","53.6","7.7","0","","126.8","","$804" +"","LG SeedsLG Seeds","C2050R2C2050R2","","RR2YRR2Y","2.12.1","RR","AcAc","53.653.6","7.77.7","00","","123.9123.9","","$804$804" +"","Titan ProTitan Pro","19M4219M42","","RR2YRR2Y","1.91.9","RR","CMBCMB","53.653.6","7.77.7","00","","121.0121.0","","$804$804" +"","StineStine","19RA02 (2) §19RA02 (2) §","","RR2YRR2Y","1 91.9","RR","CMBCMB","53 453.4","7 77.7","00","","123 9123.9","","$801$801" +"","AsgrowAsgrow","AG1832 §AG1832 §","","RR2YRR2Y","1 81.8","MRMR","Ac PVAc,PV","52 952.9","7 77.7","00","","122 0122.0","","$794$794" +"","Prairie Brandiid","PB-1566R2662","","RR2Y2","1.5","R","CMB","52.8","7.7","0","","122.9","","$792$" +"","Channel","1901R2","","RR2Y","1.9","R","Ac,PV,","52.8","7.6","0","","123.4","","$791$" +"","Titan ProTitan Pro","20M120M1","","RR2YRR2Y","2.02.0","RR","AmAm","52.552.5","7.57.5","00","","124.4124.4","","$788$788" +"","KrugerKruger","K2-2002K2-2002","","RR2YRR2Y","2 02.0","RR","Ac PVAc,PV","52 452.4","7 97.9","00","","125 4125.4","","$786$786" +"","ChannelChannel","1700R21700R2","","RR2YRR2Y","1 71.7","RR","Ac PVAc,PV","52 352.3","7 97.9","00","","123 9123.9","","$784$784" +"","H ftHefty","H16Y11H16Y11","","RR2YRR2Y","1 61.6","MRMR","II","51 451.4","7 67.6","00","","123 9123.9","","$771$771" +"","Anderson","162R2Y","","RR2Y","1.6","R","None","51.3","7.5","0","","119.5","","$770" +"","Titan ProTitan Pro","15M2215M22","","RR2YRR2Y","1.51.5","RR","CMBCMB","51.351.3","7.87.8","00","","125.4125.4","","$769$769" +"","DairylandDairyland","DSR-1710R2YDSR-1710R2Y","","RR2YRR2Y","1 71.7","RR","CMBCMB","51 351.3","7 77.7","00","","122 0122.0","","$769$769" +"","HeftyHefty","H20R3H20R3","","RR2YRR2Y","2 02.0","MRMR","II","50 550.5","8 28.2","00","","121 0121.0","","$757$757" +"","PPrairie BrandiiBd","PB 1743R2PB-1743R2","","RR2YRR2Y","1 71.7","RR","CMBCMB","50 250.2","7 77.7","00","","125 8125.8","","$752$752" +"","Gold Country","1741","","RR2Y","1.7","R","Ac","50.1","7.8","0","","123.9","","$751" +"","Trelaye ay","20RR4303","","RR2Y","2.00","R","Ac,Exc,","49.99 9","7.66","00","","127.88","","$749$9" +"","HeftyHefty","H14R3H14R3","","RR2YRR2Y","1.41.4","MRMR","II","49.749.7","7.77.7","00","","122.9122.9","","$746$746" +"","Prairie BrandPrairie Brand","PB-2099NRR2PB-2099NRR2","","RR2YRR2Y","2 02.0","RR","CMBCMB","49 649.6","7 87.8","00","","126 3126.3","","$743$743" +"","WensmanWensman","W 3174NR2W 3174NR2","","RR2YRR2Y","1 71.7","RR","AcAc","49 349.3","7 67.6","00","","122 5122.5","","$740$740" +"","KKruger","K2 1602K2-1602","","RR2YRR2Y","1 61.6","R","Ac,PV","48.78","7.66","00","","125.412","","$731$31" +"","NK Brand","S18-C2 §§","","RR2Y","1.8","R","CMB","48.7","7.7","0","","126.8","","$731$" +"","KrugerKruger","K2-1902K2 1902","","RR2YRR2Y","1.91.9","RR","Ac,PVAc,PV","48.748.7","7.57.5","00","","124.4124.4","","$730$730" +"","Prairie BrandPrairie Brand","PB-1823R2PB-1823R2","","RR2YRR2Y","1 81.8","RR","NoneNone","48 548.5","7 67.6","00","","121 0121.0","","$727$727" +"","Gold CountryGold Country","15411541","","RR2YRR2Y","1 51.5","RR","AcAc","48 448.4","7 67.6","00","","110 4110.4","","$726$726" +"","","","","","","","Test Average =","47 647.6","7 77.7","00","","122 9122.9","","$713$713" +"","","","","","","","LSD (0.10) =","5.7","0.3","ns","","37.8","","566.4" diff --git a/docs/benchmark/stream/birdisland/birdisland-data-camelot-page-1-table-2.csv b/docs/benchmark/stream/birdisland/birdisland-data-camelot-page-1-table-2.csv new file mode 100644 index 0000000..8695bf3 --- /dev/null +++ b/docs/benchmark/stream/birdisland/birdisland-data-camelot-page-1-table-2.csv @@ -0,0 +1,39 @@ +"TILLAGE/CULTIVATION:TILLAGE/CULTIVATION:","","conventional w/ fall tillconventional w/ fall till","","","","","","","","","","","" +"PEST MANAGEMENT:PEST MANAGEMENT:","","Roundup twiceRoundup twice","","","","","","","","","","","" +"SEEDED - RATE:","","May 15M15","140,000 /A140 000 /A","","","","","","","TOP 30 foTOP 30 for YIELD of 63 TESTED","","YIELD of 63 TESTED","" +"HARVESTEDHARVESTED - STAND:STAND","","O t 3Oct 3","122 921 /A122,921 /A","","","","","","","","AVERAGE of (3) REPLICATIONSAVERAGE of (3) REPLICATIONS","","" +"","","","","","SCN","Seed","Yield","Moisture","Lodgingg","g","Stand","","Gross" +"Company/Brandpy","","Product/Brand†","Technol.†","Mat.","Resist.","Trmt.†","Bu/A","%","%","","(x 1000)(",")","Income" +"KrugerKruger","","K2-1901K2 1901","RR2YRR2Y","1.91.9","RR","Ac,PVAc,PV","56.456.4","7.67.6","00","","126.3126.3","","$846$846" +"StineStine","","19RA02 §19RA02 §","RR2YRR2Y","1 91.9","RR","CMBCMB","55.355.3","7 67.6","00","","120 0120.0","","$830$830" +"WensmanWensman","","W 3190NR2W 3190NR2","RR2YRR2Y","1 91.9","RR","AcAc","54 554.5","7 67.6","00","","119 5119.5","","$818$818" +"H ftHefty","","H17Y12H17Y12","RR2YRR2Y","1 71.7","MRMR","II","53 753.7","7 77.7","00","","124 4124.4","","$806$806" +"Dyna-Gro","","S15RY53","RR2Y","1.5","R","Ac","53.6","7.7","0","","126.8","","$804" +"LG SeedsLG Seeds","","C2050R2C2050R2","RR2YRR2Y","2.12.1","RR","AcAc","53.653.6","7.77.7","00","","123.9123.9","","$804$804" +"Titan ProTitan Pro","","19M4219M42","RR2YRR2Y","1.91.9","RR","CMBCMB","53.653.6","7.77.7","00","","121.0121.0","","$804$804" +"StineStine","","19RA02 (2) §19RA02 (2) §","RR2YRR2Y","1 91.9","RR","CMBCMB","53 453.4","7 77.7","00","","123 9123.9","","$801$801" +"AsgrowAsgrow","","AG1832 §AG1832 §","RR2YRR2Y","1 81.8","MRMR","Ac PVAc,PV","52 952.9","7 77.7","00","","122 0122.0","","$794$794" +"Prairie Brandiid","","PB-1566R2662","RR2Y2","1.5","R","CMB","52.8","7.7","0","","122.9","","$792$" +"Channel","","1901R2","RR2Y","1.9","R","Ac,PV,","52.8","7.6","0","","123.4","","$791$" +"Titan ProTitan Pro","","20M120M1","RR2YRR2Y","2.02.0","RR","AmAm","52.552.5","7.57.5","00","","124.4124.4","","$788$788" +"KrugerKruger","","K2-2002K2-2002","RR2YRR2Y","2 02.0","RR","Ac PVAc,PV","52 452.4","7 97.9","00","","125 4125.4","","$786$786" +"ChannelChannel","","1700R21700R2","RR2YRR2Y","1 71.7","RR","Ac PVAc,PV","52 352.3","7 97.9","00","","123 9123.9","","$784$784" +"H ftHefty","","H16Y11H16Y11","RR2YRR2Y","1 61.6","MRMR","II","51 451.4","7 67.6","00","","123 9123.9","","$771$771" +"Anderson","","162R2Y","RR2Y","1.6","R","None","51.3","7.5","0","","119.5","","$770" +"Titan ProTitan Pro","","15M2215M22","RR2YRR2Y","1.51.5","RR","CMBCMB","51.351.3","7.87.8","00","","125.4125.4","","$769$769" +"DairylandDairyland","","DSR-1710R2YDSR-1710R2Y","RR2YRR2Y","1 71.7","RR","CMBCMB","51 351.3","7 77.7","00","","122 0122.0","","$769$769" +"HeftyHefty","","H20R3H20R3","RR2YRR2Y","2 02.0","MRMR","II","50 550.5","8 28.2","00","","121 0121.0","","$757$757" +"PPrairie BrandiiBd","","PB 1743R2PB-1743R2","RR2YRR2Y","1 71.7","RR","CMBCMB","50 250.2","7 77.7","00","","125 8125.8","","$752$752" +"Gold Country","","1741","RR2Y","1.7","R","Ac","50.1","7.8","0","","123.9","","$751" +"Trelaye ay","","20RR4303","RR2Y","2.00","R","Ac,Exc,","49.99 9","7.66","00","","127.88","","$749$9" +"HeftyHefty","","H14R3H14R3","RR2YRR2Y","1.41.4","MRMR","II","49.749.7","7.77.7","00","","122.9122.9","","$746$746" +"Prairie BrandPrairie Brand","","PB-2099NRR2PB-2099NRR2","RR2YRR2Y","2 02.0","RR","CMBCMB","49 649.6","7 87.8","00","","126 3126.3","","$743$743" +"WensmanWensman","","W 3174NR2W 3174NR2","RR2YRR2Y","1 71.7","RR","AcAc","49 349.3","7 67.6","00","","122 5122.5","","$740$740" +"KKruger","","K2 1602K2-1602","RR2YRR2Y","1 61.6","R","Ac,PV","48.78","7.66","00","","125.412","","$731$31" +"NK Brand","","S18-C2 §§","RR2Y","1.8","R","CMB","48.7","7.7","0","","126.8","","$731$" +"KrugerKruger","","K2-1902K2 1902","RR2YRR2Y","1.91.9","RR","Ac,PVAc,PV","48.748.7","7.57.5","00","","124.4124.4","","$730$730" +"Prairie BrandPrairie Brand","","PB-1823R2PB-1823R2","RR2YRR2Y","1 81.8","RR","NoneNone","48 548.5","7 67.6","00","","121 0121.0","","$727$727" +"Gold CountryGold Country","","15411541","RR2YRR2Y","1 51.5","RR","AcAc","48 448.4","7 67.6","00","","110 4110.4","","$726$726" +"","","","","","","Test Average =","47 647.6","7 77.7","00","","122 9122.9","","$713$713" +"","","","","","","LSD (0.10) =","5.7","0.3","ns","","37.8","","566.4" +"","F.I.R.S.T. Managerg","","","","","C.V. =","8.8","2.9","","","56.4","","846.2" From 87a2f4fdc9590e5df90a813df5bf27d4b7eb983c Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Wed, 12 Dec 2018 07:36:07 +0530 Subject: [PATCH 73/89] Add textedge plot type --- camelot/cli.py | 2 +- camelot/parsers/lattice.py | 1 + camelot/parsers/stream.py | 3 ++ camelot/plotting.py | 75 +++++++++++++++++++++++++++++++++++--- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/camelot/cli.py b/camelot/cli.py index eaae955..1b995aa 100644 --- a/camelot/cli.py +++ b/camelot/cli.py @@ -138,7 +138,7 @@ def lattice(c, *args, **kwargs): @click.option('-c', '--col_close_tol', default=0, help='Tolerance parameter' ' used to combine text horizontally, to generate columns.') @click.option('-plot', '--plot_type', - type=click.Choice(['text', 'grid']), + type=click.Choice(['text', 'grid', 'contour', 'textedge']), help='Plot elements found on PDF page for visual debugging.') @click.argument('filepath', type=click.Path(exists=True)) @pass_config diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index 22c77e8..14d8f6c 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -341,6 +341,7 @@ class Lattice(BaseParser): table._text = _text table._image = (self.image, self.table_bbox_unscaled) table._segments = (self.vertical_segments, self.horizontal_segments) + table._textedges = None return table diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 3b9c068..b6785df 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -263,6 +263,7 @@ class Stream(BaseParser): textedges.generate(textlines) # select relevant edges relevant_textedges = textedges.get_relevant() + self.textedges.extend(relevant_textedges) # guess table areas using textlines and relevant edges table_bbox = textedges.get_table_areas(textlines, relevant_textedges) # treat whole page as table area if no table areas found @@ -272,6 +273,7 @@ class Stream(BaseParser): return table_bbox def _generate_table_bbox(self): + self.textedges = [] if self.table_areas is not None: table_bbox = {} for area in self.table_areas: @@ -378,6 +380,7 @@ class Stream(BaseParser): table._text = _text table._image = None table._segments = None + table._textedges = self.textedges return table diff --git a/camelot/plotting.py b/camelot/plotting.py index 3b91cee..1320267 100644 --- a/camelot/plotting.py +++ b/camelot/plotting.py @@ -33,7 +33,10 @@ class PlotMethods(object): if not _HAS_MPL: raise ImportError('matplotlib is required for plotting.') - if table.flavor == 'stream' and kind in ['contour', 'joint', 'line']: + if table.flavor == 'lattice' and kind in ['textedge']: + raise NotImplementedError("Lattice flavor does not support kind='{}'".format( + kind)) + elif table.flavor == 'stream' and kind in ['joint', 'line']: raise NotImplementedError("Stream flavor does not support kind='{}'".format( kind)) @@ -114,20 +117,82 @@ class PlotMethods(object): fig : matplotlib.fig.Figure """ - img, table_bbox = table._image + try: + img, table_bbox = table._image + _FOR_LATTICE = True + except TypeError: + img, table_bbox = (None, {table._bbox: None}) + _FOR_LATTICE = False fig = plt.figure() ax = fig.add_subplot(111, aspect='equal') + + xs, ys = [], [] + if not _FOR_LATTICE: + for t in table._text: + xs.extend([t[0], t[2]]) + ys.extend([t[1], t[3]]) + ax.add_patch( + patches.Rectangle( + (t[0], t[1]), + t[2] - t[0], + t[3] - t[1], + color='blue' + ) + ) + for t in table_bbox.keys(): ax.add_patch( patches.Rectangle( (t[0], t[1]), t[2] - t[0], t[3] - t[1], - fill=None, - edgecolor='red' + fill=False, + color='red' ) ) - ax.imshow(img) + if not _FOR_LATTICE: + xs.extend([t[0], t[2]]) + ys.extend([t[1], t[3]]) + ax.set_xlim(min(xs) - 10, max(xs) + 10) + ax.set_ylim(min(ys) - 10, max(ys) + 10) + + if _FOR_LATTICE: + ax.imshow(img) + return fig + + def textedge(self, table): + """Generates a plot for relevant textedges. + + Parameters + ---------- + table : camelot.core.Table + + Returns + ------- + fig : matplotlib.fig.Figure + + """ + fig = plt.figure() + ax = fig.add_subplot(111, aspect='equal') + xs, ys = [], [] + for t in table._text: + xs.extend([t[0], t[2]]) + ys.extend([t[1], t[3]]) + ax.add_patch( + patches.Rectangle( + (t[0], t[1]), + t[2] - t[0], + t[3] - t[1], + color='blue' + ) + ) + ax.set_xlim(min(xs) - 10, max(xs) + 10) + ax.set_ylim(min(ys) - 10, max(ys) + 10) + + for te in table._textedges: + ax.plot([te.x, te.x], + [te.y0, te.y1]) + return fig def joint(self, table): From b56d2246ad8f3ee39b9db0076ffc559a579a6678 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Wed, 12 Dec 2018 08:09:48 +0530 Subject: [PATCH 74/89] Add new plot type tests --- .../test_lattice_contour_plot.png | Bin 0 -> 34094 bytes .../test_stream_contour_plot.png | Bin 0 -> 13521 bytes .../baseline_plots/test_textedge_plot.png | Bin 0 -> 17978 bytes tests/test_plotting.py | 18 +++++++++++++++++- 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 tests/files/baseline_plots/test_lattice_contour_plot.png create mode 100644 tests/files/baseline_plots/test_stream_contour_plot.png create mode 100644 tests/files/baseline_plots/test_textedge_plot.png diff --git a/tests/files/baseline_plots/test_lattice_contour_plot.png b/tests/files/baseline_plots/test_lattice_contour_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..57b39620e37c4fe51e1396af240658aa73955494 GIT binary patch literal 34094 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�aY!gywdD3bdpKO(l<4#_x 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 CtK;JU literal 0 HcmV?d00001 diff --git a/tests/files/baseline_plots/test_stream_contour_plot.png b/tests/files/baseline_plots/test_stream_contour_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..a6e77f77c06075cd2be7ccb2c2fbbf0b50aa4025 GIT binary patch literal 13521 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 Dfuj$d literal 0 HcmV?d00001 diff --git a/tests/files/baseline_plots/test_textedge_plot.png b/tests/files/baseline_plots/test_textedge_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..63fc23664a941e856f3e8b1d95825ab67175eeb0 GIT binary patch literal 17978 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 ZH`May{61Zp1ej#lIX%PE*}C@k{tu9|tfc?| literal 0 HcmV?d00001 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index eeea81a..f267e29 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -29,12 +29,20 @@ def test_grid_plot(): @pytest.mark.mpl_image_compare( baseline_dir="files/baseline_plots", remove_text=True) -def test_contour_plot(): +def test_lattice_contour_plot(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) return camelot.plot(tables[0], kind='contour') +@pytest.mark.mpl_image_compare( + baseline_dir="files/baseline_plots", remove_text=True) +def test_stream_contour_plot(): + filename = os.path.join(testdir, "tabula/12s0324.pdf") + tables = camelot.read_pdf(filename, flavor='stream') + return camelot.plot(tables[0], kind='contour') + + @pytest.mark.mpl_image_compare( baseline_dir="files/baseline_plots", remove_text=True) def test_line_plot(): @@ -49,3 +57,11 @@ def test_joint_plot(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) return camelot.plot(tables[0], kind='joint') + + +@pytest.mark.mpl_image_compare( + baseline_dir="files/baseline_plots", remove_text=True) +def test_textedge_plot(): + filename = os.path.join(testdir, "tabula/12s0324.pdf") + tables = camelot.read_pdf(filename, flavor='stream') + return camelot.plot(tables[0], kind='textedge') From 656c4e09bc9d6d46878422b5619d55687b5572e1 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Wed, 12 Dec 2018 08:18:49 +0530 Subject: [PATCH 75/89] Update docs --- docs/_static/png/plot_textedge.png | Bin 0 -> 69180 bytes docs/user/advanced.rst | 20 +++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100755 docs/_static/png/plot_textedge.png diff --git a/docs/_static/png/plot_textedge.png b/docs/_static/png/plot_textedge.png new file mode 100755 index 0000000000000000000000000000000000000000..fb2f36b141c71a539639e09671e59c716483491d GIT binary patch literal 69180 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, i.e. when ``flavor='lattice'``. +.. note:: 'line' and 'joint' can only be used with :ref:`Lattice ` and 'textedge' can only be used with :ref:`Stream `. Let's generate a plot for each type using this `PDF <../_static/pdf/foo.pdf>`__ as an example. First, let's get all the tables out. @@ -143,6 +144,23 @@ Finally, let's plot all line intersections present on the table's PDF page. :alt: A plot of all line intersections on a PDF page :align: left +textedge +^^^^^^^^ + +You can also visualize the textedges found on a page by specifying ``kind='textedge'``. To know more about what a "textedge" is, you can see pages 20, 35 and 40 of `Anssi Nurminen's master's thesis `_. + +:: + + >>> camelot.plot(tables[0], kind='textedge') + >>> plt.show() + +.. figure:: ../_static/png/plot_textedge.png + :height: 674 + :width: 1366 + :scale: 50% + :alt: A plot of relevant textedges on a PDF page + :align: left + Specify table areas ------------------- From 92e02fa03d9bc7903806cb36de5d730969500f09 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Wed, 12 Dec 2018 08:26:59 +0530 Subject: [PATCH 76/89] Update HISTORY.md --- HISTORY.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 4cf77ba..878dc08 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,18 @@ Release History master ------ +**Improvements** + +* [#207](https://github.com/socialcopsdev/camelot/issues/207) Add a plot type for Stream text edges and detected table areas. [#224](https://github.com/socialcopsdev/camelot/pull/224) by Vinayak Mehta. + +**Bugfixes** + +* [#217](https://github.com/socialcopsdev/camelot/issues/217) Fix IndexError when scale is large. + +**Documentation** + +* Add pdfplumber comparison and update Tabula (stream) comparison. Check out the [wiki page](https://github.com/socialcopsdev/camelot/wiki/Comparison-with-other-PDF-Table-Extraction-libraries-and-tools). + 0.4.1 (2018-12-05) ------------------ From e50f9c8847a54bb1fe152e87447cbfcf1fdb5b9d Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Wed, 12 Dec 2018 09:58:34 +0530 Subject: [PATCH 77/89] Change suppress_warnings to verbose --- HISTORY.md | 1 + camelot/cli.py | 10 +++++----- camelot/handlers.py | 6 ++++-- camelot/io.py | 10 +++++----- camelot/parsers/lattice.py | 5 +++-- camelot/parsers/stream.py | 5 +++-- docs/user/cli.rst | 2 +- tests/test_errors.py | 2 +- 8 files changed, 23 insertions(+), 18 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 878dc08..2270f56 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,6 +7,7 @@ master **Improvements** * [#207](https://github.com/socialcopsdev/camelot/issues/207) Add a plot type for Stream text edges and detected table areas. [#224](https://github.com/socialcopsdev/camelot/pull/224) by Vinayak Mehta. +* [#204](https://github.com/socialcopsdev/camelot/issues/204) `suppress_warnings` is now called `verbose`. By default, all logs and warnings will be printed. **Bugfixes** diff --git a/camelot/cli.py b/camelot/cli.py index 1b995aa..f304670 100644 --- a/camelot/cli.py +++ b/camelot/cli.py @@ -30,6 +30,7 @@ pass_config = click.make_pass_decorator(Config) @click.group() @click.version_option(version=__version__) +@click.option('-v', '--verbose', is_flag=True, help='Verbose.') @click.option('-p', '--pages', default='1', help='Comma-separated page numbers.' ' Example: 1,3,4 or 1,4-end.') @click.option('-pw', '--password', help='Password for decryption.') @@ -44,7 +45,6 @@ pass_config = click.make_pass_decorator(Config) ' font size. Useful to detect super/subscripts.') @click.option('-M', '--margins', nargs=3, default=(1.0, 0.5, 0.1), help='PDFMiner char_margin, line_margin and word_margin.') -@click.option('-q', '--quiet', is_flag=True, help='Suppress warnings.') @click.pass_context def cli(ctx, *args, **kwargs): """Camelot: PDF Table Extraction for Humans""" @@ -96,7 +96,7 @@ def lattice(c, *args, **kwargs): output = conf.pop('output') f = conf.pop('format') compress = conf.pop('zip') - suppress_warnings = conf.pop('quiet') + verbose = conf.pop('verbose') plot_type = kwargs.pop('plot_type') filepath = kwargs.pop('filepath') kwargs.update(conf) @@ -117,7 +117,7 @@ def lattice(c, *args, **kwargs): raise click.UsageError('Please specify output file format using --format') tables = read_pdf(filepath, pages=pages, flavor='lattice', - suppress_warnings=suppress_warnings, **kwargs) + verbose=verbose, **kwargs) click.echo('Found {} tables'.format(tables.n)) if plot_type is not None: for table in tables: @@ -149,7 +149,7 @@ def stream(c, *args, **kwargs): output = conf.pop('output') f = conf.pop('format') compress = conf.pop('zip') - suppress_warnings = conf.pop('quiet') + verbose = conf.pop('verbose') plot_type = kwargs.pop('plot_type') filepath = kwargs.pop('filepath') kwargs.update(conf) @@ -169,7 +169,7 @@ def stream(c, *args, **kwargs): raise click.UsageError('Please specify output file format using --format') tables = read_pdf(filepath, pages=pages, flavor='stream', - suppress_warnings=suppress_warnings, **kwargs) + verbose=verbose, **kwargs) click.echo('Found {} tables'.format(tables.n)) if plot_type is not None: for table in tables: diff --git a/camelot/handlers.py b/camelot/handlers.py index 47070a1..619a5c1 100644 --- a/camelot/handlers.py +++ b/camelot/handlers.py @@ -125,7 +125,7 @@ class PDFHandler(object): with open(fpath, 'wb') as f: outfile.write(f) - def parse(self, flavor='lattice', **kwargs): + def parse(self, flavor='lattice', verbose=True, **kwargs): """Extracts tables by calling parser.get_tables on all single page PDFs. @@ -134,6 +134,8 @@ class PDFHandler(object): flavor : str (default: 'lattice') The parsing method to use ('lattice' or 'stream'). Lattice is used by default. + verbose : str (default: True) + Print all logs and warnings. kwargs : dict See camelot.read_pdf kwargs. @@ -151,6 +153,6 @@ class PDFHandler(object): for p in self.pages] parser = Lattice(**kwargs) if flavor == 'lattice' else Stream(**kwargs) for p in pages: - t = parser.extract_tables(p) + t = parser.extract_tables(p, verbose=verbose) tables.extend(t) return TableList(tables) diff --git a/camelot/io.py b/camelot/io.py index 3766a7b..2bf5965 100644 --- a/camelot/io.py +++ b/camelot/io.py @@ -6,7 +6,7 @@ from .utils import validate_input, remove_extra def read_pdf(filepath, pages='1', password=None, flavor='lattice', - suppress_warnings=False, **kwargs): + verbose=True, **kwargs): """Read PDF and return extracted tables. Note: kwargs annotated with ^ can only be used with flavor='stream' @@ -24,8 +24,8 @@ def read_pdf(filepath, pages='1', password=None, flavor='lattice', flavor : str (default: 'lattice') The parsing method to use ('lattice' or 'stream'). Lattice is used by default. - suppress_warnings : bool, optional (default: False) - Prevent warnings from being emitted by Camelot. + verbose : bool, optional (default: True) + Print all logs and warnings. table_areas : list, optional (default: None) List of table area strings of the form x1,y1,x2,y2 where (x1, y1) -> left-top and (x2, y2) -> right-bottom @@ -92,11 +92,11 @@ def read_pdf(filepath, pages='1', password=None, flavor='lattice', " Use either 'lattice' or 'stream'") with warnings.catch_warnings(): - if suppress_warnings: + if not verbose: warnings.simplefilter("ignore") validate_input(kwargs, flavor=flavor) p = PDFHandler(filepath, pages=pages, password=password) kwargs = remove_extra(kwargs, flavor=flavor) - tables = p.parse(flavor=flavor, **kwargs) + tables = p.parse(flavor=flavor, verbose=verbose, **kwargs) return tables diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index 14d8f6c..ff8b5e1 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -345,9 +345,10 @@ class Lattice(BaseParser): return table - def extract_tables(self, filename): + def extract_tables(self, filename, verbose=True): self._generate_layout(filename) - logger.info('Processing {}'.format(os.path.basename(self.rootname))) + if verbose: + logger.info('Processing {}'.format(os.path.basename(self.rootname))) if not self.horizontal_text: warnings.warn("No tables found on {}".format( diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index b6785df..6713635 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -384,9 +384,10 @@ class Stream(BaseParser): return table - def extract_tables(self, filename): + def extract_tables(self, filename, verbose=True): self._generate_layout(filename) - logger.info('Processing {}'.format(os.path.basename(self.rootname))) + if verbose: + logger.info('Processing {}'.format(os.path.basename(self.rootname))) if not self.horizontal_text: warnings.warn("No tables found on {}".format( diff --git a/docs/user/cli.rst b/docs/user/cli.rst index 384b985..81dd0bc 100644 --- a/docs/user/cli.rst +++ b/docs/user/cli.rst @@ -15,6 +15,7 @@ You can print the help for the interface by typing ``camelot --help`` in your fa Options: --version Show the version and exit. + -v, --verbose Verbose. -p, --pages TEXT Comma-separated page numbers. Example: 1,3,4 or 1,4-end. -pw, --password TEXT Password for decryption. @@ -28,7 +29,6 @@ You can print the help for the interface by typing ``camelot --help`` in your fa -M, --margins ... PDFMiner char_margin, line_margin and word_margin. - -q, --quiet Suppress warnings. --help Show this message and exit. Commands: diff --git a/tests/test_errors.py b/tests/test_errors.py index a52aae4..07e052e 100755 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -56,7 +56,7 @@ def test_no_tables_found_warnings_suppressed(): # the test should fail if any warning is thrown warnings.simplefilter('error') try: - tables = camelot.read_pdf(filename, suppress_warnings=True) + tables = camelot.read_pdf(filename, verbose=False) except Warning as e: warning_text = str(e) pytest.fail('Unexpected warning: {}'.format(warning_text)) From de0079a711765c165726cdb08b31eec5991e71ff Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Wed, 12 Dec 2018 09:59:22 +0530 Subject: [PATCH 78/89] Update HISTORY.md --- HISTORY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 2270f56..56c7fc7 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,7 +7,7 @@ master **Improvements** * [#207](https://github.com/socialcopsdev/camelot/issues/207) Add a plot type for Stream text edges and detected table areas. [#224](https://github.com/socialcopsdev/camelot/pull/224) by Vinayak Mehta. -* [#204](https://github.com/socialcopsdev/camelot/issues/204) `suppress_warnings` is now called `verbose`. By default, all logs and warnings will be printed. +* [#204](https://github.com/socialcopsdev/camelot/issues/204) `suppress_warnings` is now called `verbose`. By default, all logs and warnings will be printed. [#225](https://github.com/socialcopsdev/camelot/pull/225) by Vinayak Mehta. **Bugfixes** From 591cfd529105bc27e648e5bca59672d1dfc1bb99 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Wed, 12 Dec 2018 10:15:04 +0530 Subject: [PATCH 79/89] Change kwarg name --- HISTORY.md | 2 +- camelot/cli.py | 10 +++++----- camelot/handlers.py | 8 ++++---- camelot/io.py | 8 ++++---- camelot/parsers/lattice.py | 4 ++-- camelot/parsers/stream.py | 4 ++-- tests/test_errors.py | 14 +++++++++++++- 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 56c7fc7..851a71d 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,7 +7,7 @@ master **Improvements** * [#207](https://github.com/socialcopsdev/camelot/issues/207) Add a plot type for Stream text edges and detected table areas. [#224](https://github.com/socialcopsdev/camelot/pull/224) by Vinayak Mehta. -* [#204](https://github.com/socialcopsdev/camelot/issues/204) `suppress_warnings` is now called `verbose`. By default, all logs and warnings will be printed. [#225](https://github.com/socialcopsdev/camelot/pull/225) by Vinayak Mehta. +* [#204](https://github.com/socialcopsdev/camelot/issues/204) `suppress_warnings` is now called `suppress_stdout`. [#225](https://github.com/socialcopsdev/camelot/pull/225) by Vinayak Mehta. **Bugfixes** diff --git a/camelot/cli.py b/camelot/cli.py index f304670..e978a3c 100644 --- a/camelot/cli.py +++ b/camelot/cli.py @@ -30,7 +30,7 @@ pass_config = click.make_pass_decorator(Config) @click.group() @click.version_option(version=__version__) -@click.option('-v', '--verbose', is_flag=True, help='Verbose.') +@click.option('-q', '--quiet', is_flag=False, help='Suppress logs and warnings.') @click.option('-p', '--pages', default='1', help='Comma-separated page numbers.' ' Example: 1,3,4 or 1,4-end.') @click.option('-pw', '--password', help='Password for decryption.') @@ -96,7 +96,7 @@ def lattice(c, *args, **kwargs): output = conf.pop('output') f = conf.pop('format') compress = conf.pop('zip') - verbose = conf.pop('verbose') + quiet = conf.pop('quiet') plot_type = kwargs.pop('plot_type') filepath = kwargs.pop('filepath') kwargs.update(conf) @@ -117,7 +117,7 @@ def lattice(c, *args, **kwargs): raise click.UsageError('Please specify output file format using --format') tables = read_pdf(filepath, pages=pages, flavor='lattice', - verbose=verbose, **kwargs) + suppress_stdout=quiet, **kwargs) click.echo('Found {} tables'.format(tables.n)) if plot_type is not None: for table in tables: @@ -149,7 +149,7 @@ def stream(c, *args, **kwargs): output = conf.pop('output') f = conf.pop('format') compress = conf.pop('zip') - verbose = conf.pop('verbose') + quiet = conf.pop('quiet') plot_type = kwargs.pop('plot_type') filepath = kwargs.pop('filepath') kwargs.update(conf) @@ -169,7 +169,7 @@ def stream(c, *args, **kwargs): raise click.UsageError('Please specify output file format using --format') tables = read_pdf(filepath, pages=pages, flavor='stream', - verbose=verbose, **kwargs) + suppress_stdout=quiet, **kwargs) click.echo('Found {} tables'.format(tables.n)) if plot_type is not None: for table in tables: diff --git a/camelot/handlers.py b/camelot/handlers.py index 619a5c1..a312131 100644 --- a/camelot/handlers.py +++ b/camelot/handlers.py @@ -125,7 +125,7 @@ class PDFHandler(object): with open(fpath, 'wb') as f: outfile.write(f) - def parse(self, flavor='lattice', verbose=True, **kwargs): + def parse(self, flavor='lattice', suppress_stdout=False, **kwargs): """Extracts tables by calling parser.get_tables on all single page PDFs. @@ -134,8 +134,8 @@ class PDFHandler(object): flavor : str (default: 'lattice') The parsing method to use ('lattice' or 'stream'). Lattice is used by default. - verbose : str (default: True) - Print all logs and warnings. + suppress_stdout : str (default: False) + Suppress logs and warnings. kwargs : dict See camelot.read_pdf kwargs. @@ -153,6 +153,6 @@ class PDFHandler(object): for p in self.pages] parser = Lattice(**kwargs) if flavor == 'lattice' else Stream(**kwargs) for p in pages: - t = parser.extract_tables(p, verbose=verbose) + t = parser.extract_tables(p, suppress_stdout=suppress_stdout) tables.extend(t) return TableList(tables) diff --git a/camelot/io.py b/camelot/io.py index 2bf5965..4b436ff 100644 --- a/camelot/io.py +++ b/camelot/io.py @@ -6,7 +6,7 @@ from .utils import validate_input, remove_extra def read_pdf(filepath, pages='1', password=None, flavor='lattice', - verbose=True, **kwargs): + suppress_stdout=False, **kwargs): """Read PDF and return extracted tables. Note: kwargs annotated with ^ can only be used with flavor='stream' @@ -24,7 +24,7 @@ def read_pdf(filepath, pages='1', password=None, flavor='lattice', flavor : str (default: 'lattice') The parsing method to use ('lattice' or 'stream'). Lattice is used by default. - verbose : bool, optional (default: True) + suppress_stdout : bool, optional (default: True) Print all logs and warnings. table_areas : list, optional (default: None) List of table area strings of the form x1,y1,x2,y2 @@ -92,11 +92,11 @@ def read_pdf(filepath, pages='1', password=None, flavor='lattice', " Use either 'lattice' or 'stream'") with warnings.catch_warnings(): - if not verbose: + if suppress_stdout: warnings.simplefilter("ignore") validate_input(kwargs, flavor=flavor) p = PDFHandler(filepath, pages=pages, password=password) kwargs = remove_extra(kwargs, flavor=flavor) - tables = p.parse(flavor=flavor, verbose=verbose, **kwargs) + tables = p.parse(flavor=flavor, suppress_stdout=suppress_stdout, **kwargs) return tables diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index ff8b5e1..cca6789 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -345,9 +345,9 @@ class Lattice(BaseParser): return table - def extract_tables(self, filename, verbose=True): + def extract_tables(self, filename, suppress_stdout=False): self._generate_layout(filename) - if verbose: + if not suppress_stdout: logger.info('Processing {}'.format(os.path.basename(self.rootname))) if not self.horizontal_text: diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 6713635..0fbae71 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -384,9 +384,9 @@ class Stream(BaseParser): return table - def extract_tables(self, filename, verbose=True): + def extract_tables(self, filename, suppress_stdout=False): self._generate_layout(filename) - if verbose: + if not suppress_stdout: logger.info('Processing {}'.format(os.path.basename(self.rootname))) if not self.horizontal_text: diff --git a/tests/test_errors.py b/tests/test_errors.py index 07e052e..e0dc24a 100755 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -50,13 +50,25 @@ def test_no_tables_found(): assert str(e.value) == 'No tables found on page-1' +def test_no_tables_found_logs_suppressed(): + filename = os.path.join(testdir, 'foo.pdf') + with warnings.catch_warnings(): + # the test should fail if any warning is thrown + warnings.simplefilter('error') + try: + tables = camelot.read_pdf(filename, suppress_stdout=True) + except Warning as e: + warning_text = str(e) + pytest.fail('Unexpected warning: {}'.format(warning_text)) + + def test_no_tables_found_warnings_suppressed(): filename = os.path.join(testdir, 'blank.pdf') with warnings.catch_warnings(): # the test should fail if any warning is thrown warnings.simplefilter('error') try: - tables = camelot.read_pdf(filename, verbose=False) + tables = camelot.read_pdf(filename, suppress_stdout=True) except Warning as e: warning_text = str(e) pytest.fail('Unexpected warning: {}'.format(warning_text)) From 33cea4534683bdc77cd49348b5e650bdb68a9ad0 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 13 Dec 2018 00:45:22 +0530 Subject: [PATCH 80/89] Fix #105 --- camelot/parsers/lattice.py | 4 ++-- camelot/parsers/stream.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index cca6789..14430c8 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -273,8 +273,8 @@ class Lattice(BaseParser): t_bbox['vertical'] = text_in_bbox(tk, self.vertical_text) self.t_bbox = t_bbox - for direction in t_bbox: - t_bbox[direction].sort(key=lambda x: (-x.y0, x.x0)) + for direction in self.t_bbox: + self.t_bbox[direction].sort(key=lambda x: (x.x0, -x.y0)) cols, rows = zip(*self.table_bbox[tk]) cols, rows = list(cols), list(rows) diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index 0fbae71..d9481cc 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -296,7 +296,7 @@ class Stream(BaseParser): self.t_bbox = t_bbox for direction in self.t_bbox: - self.t_bbox[direction].sort(key=lambda x: (-x.y0, x.x0)) + self.t_bbox[direction].sort(key=lambda x: (x.x0, -x.y0)) text_x_min, text_y_min, text_x_max, text_y_max = self._text_bbox(self.t_bbox) rows_grouped = self._group_rows(self.t_bbox['horizontal'], row_close_tol=self.row_close_tol) From 5e71f0b0e6a3aec75bf4d2b17010a8707feccbc7 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 13 Dec 2018 12:50:30 +0530 Subject: [PATCH 81/89] Fix #192 --- camelot/parsers/lattice.py | 7 ++++--- camelot/parsers/stream.py | 7 ++++--- camelot/utils.py | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index 14430c8..8a85d1d 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -271,10 +271,11 @@ class Lattice(BaseParser): tk, self.vertical_segments, self.horizontal_segments) t_bbox['horizontal'] = text_in_bbox(tk, self.horizontal_text) t_bbox['vertical'] = text_in_bbox(tk, self.vertical_text) - self.t_bbox = t_bbox - for direction in self.t_bbox: - self.t_bbox[direction].sort(key=lambda x: (x.x0, -x.y0)) + t_bbox['horizontal'].sort(key=lambda x: (-x.y0, x.x0)) + t_bbox['vertical'].sort(key=lambda x: (x.x0, -x.y0)) + + self.t_bbox = t_bbox cols, rows = zip(*self.table_bbox[tk]) cols, rows = list(cols), list(rows) diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index d9481cc..eab8276 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -293,10 +293,11 @@ class Stream(BaseParser): t_bbox = {} t_bbox['horizontal'] = text_in_bbox(tk, self.horizontal_text) t_bbox['vertical'] = text_in_bbox(tk, self.vertical_text) - self.t_bbox = t_bbox - for direction in self.t_bbox: - self.t_bbox[direction].sort(key=lambda x: (x.x0, -x.y0)) + t_bbox['horizontal'].sort(key=lambda x: (-x.y0, x.x0)) + t_bbox['vertical'].sort(key=lambda x: (x.x0, -x.y0)) + + self.t_bbox = t_bbox text_x_min, text_y_min, text_x_max, text_y_max = self._text_bbox(self.t_bbox) rows_grouped = self._group_rows(self.t_bbox['horizontal'], row_close_tol=self.row_close_tol) diff --git a/camelot/utils.py b/camelot/utils.py index 2d735c8..cd55e4e 100644 --- a/camelot/utils.py +++ b/camelot/utils.py @@ -344,9 +344,9 @@ def flag_font_size(textline, direction): fchars = [t[0] for t in chars] if ''.join(fchars).strip(): flist.append(''.join(fchars)) - fstring = ''.join(flist).strip('\n') + fstring = ''.join(flist) else: - fstring = ''.join([t.get_text() for t in textline]).strip('\n') + fstring = ''.join([t.get_text() for t in textline]) return fstring @@ -419,7 +419,7 @@ def split_textline(table, textline, direction, flag_size=False): grouped_chars.append((key[0], key[1], flag_font_size([t[2] for t in chars], direction))) else: gchars = [t[2].get_text() for t in chars] - grouped_chars.append((key[0], key[1], ''.join(gchars).strip('\n'))) + grouped_chars.append((key[0], key[1], ''.join(gchars))) return grouped_chars @@ -500,7 +500,7 @@ def get_table_index(table, t, direction, split_text=False, flag_size=False): if flag_size: return [(r_idx, c_idx, flag_font_size(t._objs, direction))], error else: - return [(r_idx, c_idx, t.get_text().strip('\n'))], error + return [(r_idx, c_idx, t.get_text())], error def compute_accuracy(error_weights): From ff4d8ce2289e8f418ba381913b7efae5c30dedc6 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 13 Dec 2018 13:13:07 +0530 Subject: [PATCH 82/89] Add test for arabic --- tests/data.py | 8 ++++++++ tests/test_common.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/data.py b/tests/data.py index 4cc6f89..d9b3c6e 100755 --- a/tests/data.py +++ b/tests/data.py @@ -486,3 +486,11 @@ data_lattice_shift_text_right_bottom = [ ["", "2400", "Men (≥ 18 yrs)", "-", "-", "-", "1728"], ["Knowledge &Practices on HTN &DM", "2400", "Women (≥ 18 yrs)", "-", "-", "-", "1728"] ] + +data_arabic = [ + ['ً\n\xa0\nﺎﺒﺣﺮﻣ', 'ﻥﺎﻄﻠﺳ\xa0ﻲﻤﺳﺍ'], + ['ﻝﺎﻤﺸﻟﺍ\xa0ﺎﻨﻴﻟﻭﺭﺎﻛ\xa0ﺔﻳﻻﻭ\xa0ﻦﻣ\xa0ﺎﻧﺍ', '؟ﺖﻧﺍ\xa0ﻦﻳﺍ\xa0ﻦﻣ'], + ['1234', 'ﻂﻄﻗ\xa047\xa0ﻱﺪﻨﻋ'], + ['؟ﻙﺎﺒﺷ\xa0ﺖﻧﺍ\xa0ﻞﻫ', 'ﺔﻳﺰﻴﻠﺠﻧﻻﺍ\xa0ﻲﻓ\xa0Jeremy\xa0ﻲﻤﺳﺍ'], + ['Jeremy\xa0is\xa0ﻲﻣﺮﺟ\xa0in\xa0Arabic', ''] +] diff --git a/tests/test_common.py b/tests/test_common.py index 708d61c..7430924 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -179,3 +179,11 @@ def test_repr(): assert repr(tables) == "" assert repr(tables[0]) == "" assert repr(tables[0].cells[0][0]) == "" + + +def test_arabic(): + df = pd.DataFrame(data_arabic) + + filename = os.path.join(testdir, "tabula/arabic.pdf") + tables = camelot.read_pdf(filename) + assert df.equals(tables[0].df) From 69136431b66d6ff6336b50f15604a5bcf016c8a2 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 13 Dec 2018 14:36:50 +0530 Subject: [PATCH 83/89] Fix #215 --- camelot/parsers/lattice.py | 4 +++- camelot/parsers/stream.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index 8a85d1d..da3524f 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -309,7 +309,9 @@ class Lattice(BaseParser): table = table.set_span() pos_errors = [] - for direction in self.t_bbox: + # TODO: have a single list in place of two directional ones? + # sorted on x-coordinate based on reading order i.e. LTR or RTL + for direction in ['vertical', 'horizontal']: for t in self.t_bbox[direction]: indices, error = get_table_index( table, t, direction, split_text=self.split_text, diff --git a/camelot/parsers/stream.py b/camelot/parsers/stream.py index eab8276..4bf482d 100644 --- a/camelot/parsers/stream.py +++ b/camelot/parsers/stream.py @@ -351,8 +351,11 @@ class Stream(BaseParser): def _generate_table(self, table_idx, cols, rows, **kwargs): table = Table(cols, rows) table = table.set_all_edges() + pos_errors = [] - for direction in self.t_bbox: + # TODO: have a single list in place of two directional ones? + # sorted on x-coordinate based on reading order i.e. LTR or RTL + for direction in ['vertical', 'horizontal']: for t in self.t_bbox[direction]: indices, error = get_table_index( table, t, direction, split_text=self.split_text, From d83d5fae4255750029904e08e6cfd3aa3cf70140 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 13 Dec 2018 15:16:51 +0530 Subject: [PATCH 84/89] Fix tests Fix tests --- tests/data.py | 171 +++++++++++++++++++++---------------------- tests/test_common.py | 1 + 2 files changed, 85 insertions(+), 87 deletions(-) diff --git a/tests/data.py b/tests/data.py index d9b3c6e..677c58b 100755 --- a/tests/data.py +++ b/tests/data.py @@ -82,42 +82,40 @@ data_stream_two_tables_1 = [ ["", "", "Total", "", "", "Male", "", "", "Female", ""], ["Offense charged", "", "Under 18", "18 years", "", "Under 18", "18 years", "", "Under 18", "18 years"], ["", "Total", "years", "and over", "Total", "years", "and over", "Total", "years", "and over"], - ["Total . . . . . . . . . . . . . . . . . . . . . . . . .", "11,062 .6", "1,540 .0", "9,522 .6", "8,263 .3", "1,071 .6", "7,191 .7", "2,799 .2", "468 .3", "2,330 .9"], - ["Violent crime . . . . . . . . . . . . . . . . . .", "467 .9", "69 .1", "398 .8", "380 .2", "56 .5", "323 .7", "87 .7", "12 .6", "75 .2"], + ["Total .\n .\n . . . . . .\n . .\n . .\n . .\n . .\n . .\n . .\n . .\n . . .", "11,062 .6", "1,540 .0", "9,522 .6", "8,263 .3", "1,071 .6", "7,191 .7", "2,799 .2", "468 .3", "2,330 .9"], + ["Violent crime . . . . . . . .\n . .\n . .\n . .\n . .\n . .", "467 .9", "69 .1", "398 .8", "380 .2", "56 .5", "323 .7", "87 .7", "12 .6", "75 .2"], ["Murder and nonnegligent", "", "", "", "", "", "", "", "", ""], - ["manslaughter . . . . . . . .. .. .. .. ..", "10.0", "0.9", "9.1", "9.0", "0.9", "8.1", "1.1", "–", "1.0"], - ["Forcible rape . . . . . . . .. .. .. .. .. .", "17.5", "2.6", "14.9", "17.2", "2.5", "14.7", "–", "–", "–"], - ["Robbery . . . .. .. . .. . ... . ... . ...", "102.1", "25.5", "76.6", "90.0", "22.9", "67.1", "12.1", "2.5", "9.5"], - ["Aggravated assault . . . . . . . .. .. ..", "338.4", "40.1", "298.3", "264.0", "30.2", "233.8", "74.4", "9.9", "64.5"], - ["Property crime . . . . . . . . . . . . . . . . .", "1,396 .4", "338 .7", "1,057 .7", "875 .9", "210 .8", "665 .1", "608 .2", "127 .9", "392 .6"], - ["Burglary . .. . . . . .. ... .... .... ..", "240.9", "60.3", "180.6", "205.0", "53.4", "151.7", "35.9", "6.9", "29.0"], - ["Larceny-theft . . . . . . . .. .. .. .. .. .", "1,080.1", "258.1", "822.0", "608.8", "140.5", "468.3", "471.3", "117.6", "353.6"], - ["Motor vehicle theft . . . . .. .. . .... .", "65.6", "16.0", "49.6", "53.9", "13.3", "40.7", "11.7", "2.7", "8.9"], - ["Arson .. . . . .. . ... .... .... .... .", "9.8", "4.3", "5.5", "8.1", "3.7", "4.4", "1.7", "0.6", "1.1"], - ["Other assaults .. . . . . .. . ... . ... ..", "1,061.3", "175.3", "886.1", "785.4", "115.4", "670.0", "276.0", "59.9", "216.1"], - ["Forgery and counterfeiting .. . . . . . ..", "68.9", "1.7", "67.2", "42.9", "1.2", "41.7", "26.0", "0.5", "25.5"], - ["Fraud .... .. . . .. ... .... .... ....", "173.7", "5.1", "168.5", "98.4", "3.3", "95.0", "75.3", "1.8", "73.5"], - ["Embezzlement . . .. . . . .. . ... . ....", "14.6", "–", "14.1", "7.2", "–", "6.9", "7.4", "–", "7.2"], - ["Stolen property 1 . . . . . . .. . .. .. ...", "84.3", "15.1", "69.2", "66.7", "12.2", "54.5", "17.6", "2.8", "14.7"], - ["Vandalism . . . . . . . .. .. .. .. .. ....", "217.4", "72.7", "144.7", "178.1", "62.8", "115.3", "39.3", "9.9", "29.4"], + ["manslaughter . . . . . . . .\n. .\n. .\n. .\n. .\n.", "10.0", "0.9", "9.1", "9.0", "0.9", "8.1", "1.1", "–", "1.0"], + ["Forcible rape . . . . . . . .\n. .\n. .\n. .\n. .\n. .", "17.5", "2.6", "14.9", "17.2", "2.5", "14.7", "–", "–", "–"], + ["Robbery . . . .\n. .\n. . .\n. . .\n.\n. . .\n.\n. . .\n.\n.", "102.1", "25.5", "76.6", "90.0", "22.9", "67.1", "12.1", "2.5", "9.5"], + ["Aggravated assault . . . . . . . .\n. .\n. .\n.", "338.4", "40.1", "298.3", "264.0", "30.2", "233.8", "74.4", "9.9", "64.5"], + ["Property crime . . . .\n . .\n . . .\n . . .\n .\n . . . .", "1,396 .4", "338 .7", "1,057 .7", "875 .9", "210 .8", "665 .1", "608 .2", "127 .9", "392 .6"], + ["Burglary . .\n. . . . . .\n. .\n.\n. .\n.\n.\n. .\n.\n.\n. .\n.", "240.9", "60.3", "180.6", "205.0", "53.4", "151.7", "35.9", "6.9", "29.0"], + ["Larceny-theft . . . . . . . .\n. .\n. .\n. .\n. .\n. .", "1,080.1", "258.1", "822.0", "608.8", "140.5", "468.3", "471.3", "117.6", "353.6"], + ["Motor vehicle theft . . . . .\n. .\n. . .\n.\n.\n. .", "65.6", "16.0", "49.6", "53.9", "13.3", "40.7", "11.7", "2.7", "8.9"], + ["Arson .\n. . . . .\n. . .\n.\n. .\n.\n.\n. .\n.\n.\n. .\n.\n.\n. .", "9.8", "4.3", "5.5", "8.1", "3.7", "4.4", "1.7", "0.6", "1.1"], + ["Other assaults .\n. . . . . .\n. . .\n.\n. . .\n.\n. .\n.", "1,061.3", "175.3", "886.1", "785.4", "115.4", "670.0", "276.0", "59.9", "216.1"], + ["Forgery and counterfeiting .\n. . . . . . .\n.", "68.9", "1.7", "67.2", "42.9", "1.2", "41.7", "26.0", "0.5", "25.5"], + ["Fraud .\n.\n.\n. .\n. . . .\n. .\n.\n. .\n.\n.\n. .\n.\n.\n. .\n.\n.\n.", "173.7", "5.1", "168.5", "98.4", "3.3", "95.0", "75.3", "1.8", "73.5"], + ["Embezzlement . . .\n. . . . .\n. . .\n.\n. . .\n.\n.\n.", "14.6", "–", "14.1", "7.2", "–", "6.9", "7.4", "–", "7.2"], + ["Stolen property 1 . . . . . . .\n. . .\n. .\n. .\n.\n.", "84.3", "15.1", "69.2", "66.7", "12.2", "54.5", "17.6", "2.8", "14.7"], + ["Vandalism . . . . . . . .\n. .\n. .\n. .\n. .\n. .\n.\n.\n.", "217.4", "72.7", "144.7", "178.1", "62.8", "115.3", "39.3", "9.9", "29.4"], ["Weapons; carrying, possessing, etc. .", "132.9", "27.1", "105.8", "122.1", "24.3", "97.8", "10.8", "2.8", "8.0"], - ["Prostitution and commercialized vice", - "56.9", "1.1", "55.8", "17.3", "–", "17.1", "39.6", "0.8", "38.7"], - ["Sex offenses 2 . . . . .. . . . .. .. .. . ..", "61.5", "10.7", "50.7", "56.1", "9.6", "46.5", "5.4", "1.1", "4.3"], - ["Drug abuse violations . . . . . . . .. ...", "1,333.0", "136.6", "1,196.4", "1,084.3", "115.2", "969.1", "248.7", "21.4", "227.3"], - ["Gambling .. . . . . .. ... . ... . ... ...", "8.2", "1.4", "6.8", "7.2", "1.4", "5.9", "0.9", "–", "0.9"], + ["Prostitution and commercialized vice", "56.9", "1.1", "55.8", "17.3", "–", "17.1", "39.6", "0.8", "38.7"], + ["Sex offenses 2 . . . . .\n. . . . .\n. .\n. .\n. . .\n.", "61.5", "10.7", "50.7", "56.1", "9.6", "46.5", "5.4", "1.1", "4.3"], + ["Drug abuse violations . . . . . . . .\n. .\n.\n.", "1,333.0", "136.6", "1,196.4", "1,084.3", "115.2", "969.1", "248.7", "21.4", "227.3"], + ["Gambling .\n. . . . . .\n. .\n.\n. . .\n.\n. . .\n.\n. .\n.\n.", "8.2", "1.4", "6.8", "7.2", "1.4", "5.9", "0.9", "–", "0.9"], ["Offenses against the family and", "", "", "", "", "", "", "", "", ""], - ["children . . . .. . . .. .. .. .. .. .. . ..", "92.4", "3.7", "88.7", "68.9", "2.4", "66.6", "23.4", "1.3", "22.1"], - ["Driving under the influence . . . . . .. .", "1,158.5", "109.2", "1,147.5", "895.8", "8.2", "887.6", "262.7", "2.7", "260.0"], - ["Liquor laws . . . . . . . .. .. .. .. .. .. .", "48.2", "90.2", "368.0", "326.8", "55.4", "271.4", - "131.4", "34.7", "96.6"], - ["Drunkenness . . .. . . . .. . ... . ... ..", "488.1", "11.4", "476.8", "406.8", "8.5", "398.3", "81.3", "2.9", "78.4"], - ["Disorderly conduct . .. . . . . . .. .. .. .", "529.5", "136.1", "393.3", "387.1", "90.8", "296.2", "142.4", "45.3", "97.1"], - ["Vagrancy . . . .. . . . ... .... .... ...", "26.6", "2.2", "24.4", "20.9", "1.6", "19.3", "5.7", "0.6", "5.1"], - ["All other offenses (except traffic) . . ..", "306.1", "263.4", "2,800.8", "2,337.1", "194.2", "2,142.9", "727.0", "69.2", "657.9"], - ["Suspicion . . . .. . . .. .. .. .. .. .. . ..", "1.6", "–", "1.4", "1.2", "–", "1.0", "–", "–", "–"], - ["Curfew and loitering law violations ..", "91.0", "91.0", "(X)", "63.1", "63.1", "(X)", "28.0", "28.0", "(X)"], - ["Runaways . . . . . . . .. .. .. .. .. ....", "75.8", "75.8", "(X)", "34.0", "34.0", "(X)", "41.8", "41.8", "(X)"], + ["children . . . .\n. . . .\n. .\n. .\n. .\n. .\n. .\n. . .\n.", "92.4", "3.7", "88.7", "68.9", "2.4", "66.6", "23.4", "1.3", "22.1"], + ["Driving under the influence . . . . . .\n. .", "1,158.5", "109.2", "1,147.5", "895.8", "8.2", "887.6", "262.7", "2.7", "260.0"], + ["Liquor laws . . . . . . . .\n. .\n. .\n. .\n. .\n. .\n. .", "48.2", "90.2", "368.0", "326.8", "55.4", "271.4", "131.4", "34.7", "96.6"], + ["Drunkenness . . .\n. . . . .\n. . .\n.\n. . .\n.\n. .\n.", "488.1", "11.4", "476.8", "406.8", "8.5", "398.3", "81.3", "2.9", "78.4"], + ["Disorderly conduct . .\n. . . . . . .\n. .\n. .\n. .", "529.5", "136.1", "393.3", "387.1", "90.8", "296.2", "142.4", "45.3", "97.1"], + ["Vagrancy . . . .\n. . . . .\n.\n. .\n.\n.\n. .\n.\n.\n. .\n.\n.", "26.6", "2.2", "24.4", "20.9", "1.6", "19.3", "5.7", "0.6", "5.1"], + ["All other offenses (except traffic) . . .\n.", "306.1", "263.4", "2,800.8", "2,337.1", "194.2", "2,142.9", "727.0", "69.2", "657.9"], + ["Suspicion . . . .\n. . . .\n. .\n. .\n. .\n. .\n. .\n. . .\n.", "1.6", "–", "1.4", "1.2", "–", "1.0", "–", "–", "–"], + ["Curfew and loitering law violations .\n.", "91.0", "91.0", "(X)", "63.1", "63.1", "(X)", "28.0", "28.0", "(X)"], + ["Runaways . . . . . . . .\n. .\n. .\n. .\n. .\n. .\n.\n.\n.", "75.8", "75.8", "(X)", "34.0", "34.0", "(X)", "41.8", "41.8", "(X)"], ["", "– Represents zero. X Not applicable. 1 Buying, receiving, possessing stolen property. 2 Except forcible rape and prostitution.", "", "", "", "", "", "", "", ""], ["", "Source: U.S. Department of Justice, Federal Bureau of Investigation, Uniform Crime Reports, Arrests Master Files.", "", "", "", "", "", "", "", ""] ] @@ -128,41 +126,40 @@ data_stream_two_tables_2 = [ ["[Based on Uniform Crime Reporting (UCR) Program. Represents arrests reported (not charged) by 12,371 agencies", "", "", "", "", ""], ["with a total population of 239,839,971 as estimated by the FBI. See headnote, Table 324]", "", "", "", "", ""], ["", "", "", "", "American", ""], - ["Offense charged", "", "", "", - "Indian/Alaskan", "Asian Pacific"], + ["Offense charged", "", "", "", "Indian/Alaskan", "Asian Pacific"], ["", "Total", "White", "Black", "Native", "Islander"], - ["Total . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .", "10,690,561", "7,389,208", "3,027,153", "150,544", "123,656"], - ["Violent crime . . . . . . . . . . . . . . . . . . . . . . . . . . . .", "456,965", "268,346", "177,766", "5,608", "5,245"], - ["Murder and nonnegligent manslaughter . .. ... .", "9,739", "4,741", "4,801", "100", "97"], - ["Forcible rape . . . . . . . .. .. .. .. .... .. ...... .", "16,362", "10,644", "5,319", "169", "230"], - ["Robbery . . . . .. . . . ... . ... . .... .... .... . . .", "100,496", "43,039", "55,742", "726", "989"], - ["Aggravated assault . . . . . . . .. .. ...... .. ....", "330,368", "209,922", "111,904", "4,613", "3,929"], - ["Property crime . . . . . . . . . . . . . . . . . . . . . . . . . . .", "1,364,409", "922,139", "406,382", "17,599", "18,289"], - ["Burglary . . .. . . . .. . .... .... .... .... ... . . .", "234,551", "155,994", "74,419", "2,021", "2,117"], - ["Larceny-theft . . . . . . . .. .. .. .. .... .. ...... .", "1,056,473", "719,983", "306,625", "14,646", "15,219"], - ["Motor vehicle theft . . . . . .. ... . ... ..... ... ..", "63,919", "39,077", "23,184", "817", "841"], - ["Arson .. . . .. .. .. ... .... .... .... .... . . . . .", "9,466", "7,085", "2,154", "115", "112"], - ["Other assaults .. . . . . . ... . ... . ... ..... ... ..", "1,032,502", "672,865", "332,435", "15,127", "12,075"], - ["Forgery and counterfeiting .. . . . . . ... ..... .. ..", "67,054", "44,730", "21,251", "345", "728"], - ["Fraud ... . . . . .. .. .. .. .. .. .. .. .. .... . . . . . .", "161,233", "108,032", "50,367", "1,315", "1,519"], - ["Embezzlement . . . .. . . . ... . ... . .... ... .....", "13,960", "9,208", "4,429", "75", "248"], - ["Stolen property; buying, receiving, possessing .. .", "82,714", "51,953", "29,357", "662", "742"], - ["Vandalism . . . . . . . .. .. .. .. .. .. .... .. ..... .", "212,173", "157,723", "48,746", "3,352", "2,352"], - ["Weapons—carrying, possessing, etc. .. .. ... .. .", "130,503", "74,942", "53,441", "951", "1,169"], - ["Prostitution and commercialized vice . ... .. .. ..", "56,560", "31,699", "23,021", "427", "1,413"], - ["Sex offenses 1 . . . . . . . .. .. .. .. .... .. ...... .", "60,175", "44,240", "14,347", "715", "873"], - ["Drug abuse violations . . . . . . . .. . ..... .. .....", "1,301,629", "845,974", "437,623", "8,588", "9,444"], - ["Gambling . . . . .. . . . ... . ... . .. ... . ...... .. .", "8,046", "2,290", "5,518", "27", "211"], - ["Offenses against the family and children ... .. .. .", "87,232", "58,068", "26,850", "1,690", "624"], - ["Driving under the influence . . . . . . .. ... ...... .", "1,105,401", "954,444", "121,594", "14,903", "14,460"], - ["Liquor laws . . . . . . . .. .. .. .. .. . ..... .. .....", "444,087", "373,189", "50,431", "14,876", "5,591"], - ["Drunkenness . .. . . . . . ... . ... . ..... . .......", "469,958", "387,542", "71,020", "8,552", "2,844"], - ["Disorderly conduct . . .. . . . . .. .. . ..... .. .....", "515,689", "326,563", "176,169", "8,783", "4,174"], - ["Vagrancy . . .. .. . . .. ... .... .... .... .... . . .", "26,347", "14,581", "11,031", "543", "192"], - ["All other offenses (except traffic) . .. .. .. ..... ..", "2,929,217", "1,937,221", "911,670", "43,880", "36,446"], - ["Suspicion . . .. . . . .. .. .. .. .. .. .. ...... .. . . .", "1,513", "677", "828", "1", "7"], - ["Curfew and loitering law violations . .. ... .. ....", "89,578", "54,439", "33,207", "872", "1,060"], - ["Runaways . . . . . . . .. .. .. .. .. .. .... .. ..... .", "73,616", "48,343", "19,670", "1,653", "3,950"], + ["Total .\n .\n .\n .\n . .\n . . .\n . . .\n .\n . . .\n .\n . . .\n . .\n .\n . . .\n .\n .\n .\n . .\n . .\n . .", "10,690,561", "7,389,208", "3,027,153", "150,544", "123,656"], + ["Violent crime . . . . . . . .\n . .\n . .\n . .\n . .\n .\n .\n . .\n . .\n .\n .\n .\n .\n . .", "456,965", "268,346", "177,766", "5,608", "5,245"], + ["Murder and nonnegligent manslaughter . .\n. .\n.\n. .", "9,739", "4,741", "4,801", "100", "97"], + ["Forcible rape . . . . . . . .\n. .\n. .\n. .\n. .\n.\n.\n. .\n. .\n.\n.\n.\n.\n. .", "16,362", "10,644", "5,319", "169", "230"], + ["Robbery . . . . .\n. . . . .\n.\n. . .\n.\n. . .\n.\n.\n. .\n.\n.\n. .\n.\n.\n. . . .", "100,496", "43,039", "55,742", "726", "989"], + ["Aggravated assault . . . . . . . .\n. .\n. .\n.\n.\n.\n.\n. .\n. .\n.\n.\n.", "330,368", "209,922", "111,904", "4,613", "3,929"], + ["Property crime . . . . .\n . . . . .\n .\n . . .\n .\n . .\n .\n .\n .\n . .\n .\n . .\n .\n .", "1,364,409", "922,139", "406,382", "17,599", "18,289"], + ["Burglary . . .\n. . . . .\n. . .\n.\n.\n. .\n.\n.\n. .\n.\n.\n. .\n.\n.\n. .\n.\n. . . .", "234,551", "155,994", "74,419", "2,021", "2,117"], + ["Larceny-theft . . . . . . . .\n. .\n. .\n. .\n. .\n.\n.\n. .\n. .\n.\n.\n.\n.\n. .", "1,056,473", "719,983", "306,625", "14,646", "15,219"], + ["Motor vehicle theft . . . . . .\n. .\n.\n. . .\n.\n. .\n.\n.\n.\n. .\n.\n. .\n.", "63,919", "39,077", "23,184", "817", "841"], + ["Arson .\n. . . .\n. .\n. .\n. .\n.\n. .\n.\n.\n. .\n.\n.\n. .\n.\n.\n. .\n.\n.\n. . . . . .", "9,466", "7,085", "2,154", "115", "112"], + ["Other assaults .\n. . . . . . .\n.\n. . .\n.\n. . .\n.\n. .\n.\n.\n.\n. .\n.\n. .\n.", "1,032,502", "672,865", "332,435", "15,127", "12,075"], + ["Forgery and counterfeiting .\n. . . . . . .\n.\n. .\n.\n.\n.\n. .\n. .\n.", "67,054", "44,730", "21,251", "345", "728"], + ["Fraud .\n.\n. . . . . .\n. .\n. .\n. .\n. .\n. .\n. .\n. .\n. .\n. .\n.\n.\n. . . . . . .", "161,233", "108,032", "50,367", "1,315", "1,519"], + ["Embezzlement . . . .\n. . . . .\n.\n. . .\n.\n. . .\n.\n.\n. .\n.\n. .\n.\n.\n.\n.", "13,960", "9,208", "4,429", "75", "248"], + ["Stolen property; buying, receiving, possessing .\n. .", "82,714", "51,953", "29,357", "662", "742"], + ["Vandalism . . . . . . . .\n. .\n. .\n. .\n. .\n. .\n. .\n.\n.\n. .\n. .\n.\n.\n.\n. .", "212,173", "157,723", "48,746", "3,352", "2,352"], + ["Weapons—carrying, possessing, etc. .\n. .\n. .\n.\n. .\n. .", "130,503", "74,942", "53,441", "951", "1,169"], + ["Prostitution and commercialized vice . .\n.\n. .\n. .\n. .\n.", "56,560", "31,699", "23,021", "427", "1,413"], + ["Sex offenses 1 . . . . . . . .\n. .\n. .\n. .\n. .\n.\n.\n. .\n. .\n.\n.\n.\n.\n. .", "60,175", "44,240", "14,347", "715", "873"], + ["Drug abuse violations . . . . . . . .\n. . .\n.\n.\n.\n. .\n. .\n.\n.\n.\n.", "1,301,629", "845,974", "437,623", "8,588", "9,444"], + ["Gambling . . . . .\n. . . . .\n.\n. . .\n.\n. . .\n. .\n.\n. . .\n.\n.\n.\n.\n. .\n. .", "8,046", "2,290", "5,518", "27", "211"], + ["Offenses against the family and children .\n.\n. .\n. .\n. .", "87,232", "58,068", "26,850", "1,690", "624"], + ["Driving under the influence . . . . . . .\n. .\n.\n. .\n.\n.\n.\n.\n. .", "1,105,401", "954,444", "121,594", "14,903", "14,460"], + ["Liquor laws . . . . . . . .\n. .\n. .\n. .\n. .\n. . .\n.\n.\n.\n. .\n. .\n.\n.\n.\n.", "444,087", "373,189", "50,431", "14,876", "5,591"], + ["Drunkenness . .\n. . . . . . .\n.\n. . .\n.\n. . .\n.\n.\n.\n. . .\n.\n.\n.\n.\n.\n.", "469,958", "387,542", "71,020", "8,552", "2,844"], + ["Disorderly conduct . . .\n. . . . . .\n. .\n. . .\n.\n.\n.\n. .\n. .\n.\n.\n.\n.", "515,689", "326,563", "176,169", "8,783", "4,174"], + ["Vagrancy . . .\n. .\n. . . .\n. .\n.\n. .\n.\n.\n. .\n.\n.\n. .\n.\n.\n. .\n.\n.\n. . . .", "26,347", "14,581", "11,031", "543", "192"], + ["All other offenses (except traffic) . .\n. .\n. .\n. .\n.\n.\n.\n. .\n.", "2,929,217", "1,937,221", "911,670", "43,880", "36,446"], + ["Suspicion . . .\n. . . . .\n. .\n. .\n. .\n. .\n. .\n. .\n. .\n.\n.\n.\n.\n. .\n. . . .", "1,513", "677", "828", "1", "7"], + ["Curfew and loitering law violations . .\n. .\n.\n. .\n. .\n.\n.\n.", "89,578", "54,439", "33,207", "872", "1,060"], + ["Runaways . . . . . . . .\n. .\n. .\n. .\n. .\n. .\n. .\n.\n.\n. .\n. .\n.\n.\n.\n. .", "73,616", "48,343", "19,670", "1,653", "3,950"], ["1 Except forcible rape and prostitution.", "", "", "", "", ""], ["", "Source: U.S. Department of Justice, Federal Bureau of Investigation, “Crime in the United States, Arrests,” September 2010,", "", "", "", ""] ] @@ -170,7 +167,7 @@ data_stream_two_tables_2 = [ data_stream_table_areas = [ ["", "One Withholding"], ["Payroll Period", "Allowance"], - ["Weekly", "$71.15"], + ["Weekly", "$\n71.15"], ["Biweekly", "142.31"], ["Semimonthly", "154.17"], ["Monthly", "308.33"], @@ -316,8 +313,8 @@ data_stream_flag_size = [ ] data_lattice = [ - ["Cycle Name", "KI (1/km)", "Distance (mi)", "Percent Fuel Savings", "", "", ""], - ["", "", "", "Improved Speed", "Decreased Accel", "Eliminate Stops", "Decreased Idle"], + ["Cycle \nName", "KI \n(1/km)", "Distance \n(mi)", "Percent Fuel Savings", "", "", ""], + ["", "", "", "Improved \nSpeed", "Decreased \nAccel", "Eliminate \nStops", "Decreased \nIdle"], ["2012_2", "3.30", "1.3", "5.9%", "9.5%", "29.2%", "17.4%"], ["2145_1", "0.68", "11.2", "2.4%", "0.1%", "9.5%", "2.7%"], ["4234_1", "0.59", "58.7", "8.5%", "1.3%", "8.5%", "3.3%"], @@ -326,7 +323,7 @@ data_lattice = [ ] data_lattice_table_rotated = [ - ["State", "Nutritional Assessment (No. of individuals)", "", "", "", "IYCF Practices (No. of mothers: 2011-12)", "Blood Pressure (No. of adults: 2011-12)", "", "Fasting Blood Sugar (No. of adults:2011-12)", ""], + ["State", "Nutritional Assessment \n(No. of individuals)", "", "", "", "IYCF Practices \n(No. of mothers: \n2011-12)", "Blood Pressure \n(No. of adults: \n2011-12)", "", "Fasting Blood Sugar \n(No. of adults:\n2011-12)", ""], ["", "1975-79", "1988-90", "1996-97", "2011-12", "", "Men", "Women", "Men", "Women"], ["Kerala", "5738", "6633", "8864", "8297", "245", "2161", "3195", "1645", "2391"], ["Tamil Nadu", "7387", "10217", "5813", "7851", "413", "2134", "2858", "1119", "1739"], @@ -343,7 +340,7 @@ data_lattice_table_rotated = [ data_lattice_two_tables_1 = [ ["State", "n", "Literacy Status", "", "", "", "", ""], - ["", "", "Illiterate", "Read & Write", "1-4 std.", "5-8 std.", "9-12 std.", "College"], + ["", "", "Illiterate", "Read & \nWrite", "1-4 std.", "5-8 std.", "9-12 std.", "College"], ["Kerala", "2400", "7.2", "0.5", "25.3", "20.1", "41.5", "5.5"], ["Tamil Nadu", "2400", "21.4", "2.3", "8.8", "35.5", "25.8", "6.2"], ["Karnataka", "2399", "37.4", "2.8", "12.5", "18.3", "23.1", "5.8"], @@ -359,7 +356,7 @@ data_lattice_two_tables_1 = [ data_lattice_two_tables_2 = [ ["State", "n", "Literacy Status", "", "", "", "", ""], - ["", "", "Illiterate", "Read & Write", "1-4 std.", "5-8 std.", "9-12 std.", "College"], + ["", "", "Illiterate", "Read & \nWrite", "1-4 std.", "5-8 std.", "9-12 std.", "College"], ["Kerala", "2400", "8.8", "0.3", "20.1", "17.0", "45.6", "8.2"], ["Tamil Nadu", "2400", "29.9", "1.5", "8.5", "33.1", "22.3", "4.8"], ["Karnataka", "2399", "47.9", "2.5", "10.2", "18.8", "18.4", "2.3"], @@ -376,7 +373,7 @@ data_lattice_two_tables_2 = [ data_lattice_table_areas = [ ["", "", "", "", "", "", "", "", ""], ["State", "n", "Literacy Status", "", "", "", "", "", ""], - ["", "", "Illiterate", "Read & Write", "1-4 std.", "5-8 std.", "9-12 std.", "College", ""], + ["", "", "Illiterate", "Read & \nWrite", "1-4 std.", "5-8 std.", "9-12 std.", "College", ""], ["Kerala", "2400", "7.2", "0.5", "25.3", "20.1", "41.5", "5.5", ""], ["Tamil Nadu", "2400", "21.4", "2.3", "8.8", "35.5", "25.8", "6.2", ""], ["Karnataka", "2399", "37.4", "2.8", "12.5", "18.3", "23.1", "5.8", ""], @@ -392,13 +389,13 @@ data_lattice_table_areas = [ ] data_lattice_process_background = [ - ["State", "Date", "Halt stations", "Halt days", "Persons directly reached(in lakh)", "Persons trained", "Persons counseled" ,"Persons testedfor HIV"], + ["State", "Date", "Halt \nstations", "Halt \ndays", "Persons \ndirectly \nreached\n(in lakh)", "Persons \ntrained", "Persons \ncounseled", "Persons \ntested\nfor HIV"], ["Delhi", "1.12.2009", "8", "17", "1.29", "3,665", "2,409", "1,000"], - ["Rajasthan", "2.12.2009 to 19.12.2009", "", "", "", "", "", ""], - ["Gujarat", "20.12.2009 to 3.1.2010", "6", "13", "6.03", "3,810", "2,317", "1,453"], - ["Maharashtra", "4.01.2010 to 1.2.2010", "13", "26", "1.27", "5,680", "9,027", "4,153"], - ["Karnataka", "2.2.2010 to 22.2.2010", "11", "19", "1.80", "5,741", "3,658", "3,183"], - ["Kerala", "23.2.2010 to 11.3.2010", "9", "17", "1.42", "3,559", "2,173", "855"], + ["Rajasthan", "2.12.2009 to \n19.12.2009", "", "", "", "", "", ""], + ["Gujarat", "20.12.2009 to \n3.1.2010", "6", "13", "6.03", "3,810", "2,317", "1,453"], + ["Maharashtra", "4.01.2010 to \n1.2.2010", "13", "26", "1.27", "5,680", "9,027", "4,153"], + ["Karnataka", "2.2.2010 to \n22.2.2010", "11", "19", "1.80", "5,741", "3,658", "3,183"], + ["Kerala", "23.2.2010 to \n11.3.2010", "9", "17", "1.42", "3,559", "2,173", "855"], ["Total", "", "47", "92", "11.81", "22,455", "19,584", "10,644"] ] @@ -442,11 +439,11 @@ data_lattice_copy_text = [ ["PCCM", "San Francisco", "Family Mosaic", "25"], ["PCCM", "Total PHP Enrollment", "", "853"], ["All Models Total Enrollments", "", "", "10,132,875"], - ["Source: Data Warehouse 12/14/15", "", "", ""] + ["Source: Data Warehouse \n12/14/15", "", "", ""] ] data_lattice_shift_text_left_top = [ - ["Investigations", "No. ofHHs", "Age/Sex/Physiological Group", "Preva-lence", "C.I*", "RelativePrecision", "Sample sizeper State"], + ["Investigations", "No. of\nHHs", "Age/Sex/\nPhysiological Group", "Preva-\nlence", "C.I*", "Relative\nPrecision", "Sample size\nper State"], ["Anthropometry", "2400", "All the available individuals", "", "", "", ""], ["Clinical Examination", "", "", "", "", "", ""], ["History of morbidity", "", "", "", "", "", ""], @@ -455,12 +452,12 @@ data_lattice_shift_text_left_top = [ ["", "", "Women (≥ 18 yrs)", "", "", "", "1728"], ["Fasting blood glucose", "2400", "Men (≥ 18 yrs)", "5%", "95%", "20%", "1825"], ["", "", "Women (≥ 18 yrs)", "", "", "", "1825"], - ["Knowledge &Practices on HTN &DM", "2400", "Men (≥ 18 yrs)", "-", "-", "-", "1728"], + ["Knowledge &\nPractices on HTN &\nDM", "2400", "Men (≥ 18 yrs)", "-", "-", "-", "1728"], ["", "2400", "Women (≥ 18 yrs)", "-", "-", "-", "1728"] ] data_lattice_shift_text_disable = [ - ["Investigations", "No. ofHHs", "Age/Sex/Physiological Group", "Preva-lence", "C.I*", "RelativePrecision", "Sample sizeper State"], + ["Investigations", "No. of\nHHs", "Age/Sex/\nPhysiological Group", "Preva-\nlence", "C.I*", "Relative\nPrecision", "Sample size\nper State"], ["Anthropometry", "", "", "", "", "", ""], ["Clinical Examination", "2400", "", "All the available individuals", "", "", ""], ["History of morbidity", "", "", "", "", "", ""], @@ -469,12 +466,12 @@ data_lattice_shift_text_disable = [ ["Blood Pressure #", "2400", "Women (≥ 18 yrs)", "10%", "95%", "20%", "1728"], ["", "", "Men (≥ 18 yrs)", "", "", "", "1825"], ["Fasting blood glucose", "2400", "Women (≥ 18 yrs)", "5%", "95%", "20%", "1825"], - ["Knowledge &Practices on HTN &", "2400", "Men (≥ 18 yrs)", "-", "-", "-", "1728"], + ["Knowledge &\nPractices on HTN &", "2400", "Men (≥ 18 yrs)", "-", "-", "-", "1728"], ["DM", "2400", "Women (≥ 18 yrs)", "-", "-", "-", "1728"] ] data_lattice_shift_text_right_bottom = [ - ["Investigations", "No. ofHHs", "Age/Sex/Physiological Group", "Preva-lence", "C.I*", "RelativePrecision", "Sample sizeper State"], + ["Investigations", "No. of\nHHs", "Age/Sex/\nPhysiological Group", "Preva-\nlence", "C.I*", "Relative\nPrecision", "Sample size\nper State"], ["Anthropometry", "", "", "", "", "", ""], ["Clinical Examination", "", "", "", "", "", ""], ["History of morbidity", "2400", "", "", "", "", "All the available individuals"], @@ -484,7 +481,7 @@ data_lattice_shift_text_right_bottom = [ ["", "", "Men (≥ 18 yrs)", "", "", "", "1825"], ["Fasting blood glucose", "2400", "Women (≥ 18 yrs)", "5%", "95%", "20%", "1825"], ["", "2400", "Men (≥ 18 yrs)", "-", "-", "-", "1728"], - ["Knowledge &Practices on HTN &DM", "2400", "Women (≥ 18 yrs)", "-", "-", "-", "1728"] + ["Knowledge &\nPractices on HTN &\nDM", "2400", "Women (≥ 18 yrs)", "-", "-", "-", "1728"] ] data_arabic = [ diff --git a/tests/test_common.py b/tests/test_common.py index 7430924..5f8c81c 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -62,6 +62,7 @@ def test_stream_two_tables(): filename = os.path.join(testdir, "tabula/12s0324.pdf") tables = camelot.read_pdf(filename, flavor='stream') + assert len(tables) == 2 assert df1.equals(tables[0].df) assert df2.equals(tables[1].df) From 153869fda27d0dfb800dd03e5c47262bc7a3e8a8 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Thu, 13 Dec 2018 16:42:03 +0530 Subject: [PATCH 85/89] Update HISTORY.md and bump version Update HISTORY.md --- HISTORY.md | 4 ++++ camelot/__version__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 851a71d..9b06b01 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,9 @@ Release History master ------ +0.5.0 (2018-12-13) +------------------ + **Improvements** * [#207](https://github.com/socialcopsdev/camelot/issues/207) Add a plot type for Stream text edges and detected table areas. [#224](https://github.com/socialcopsdev/camelot/pull/224) by Vinayak Mehta. @@ -12,6 +15,7 @@ master **Bugfixes** * [#217](https://github.com/socialcopsdev/camelot/issues/217) Fix IndexError when scale is large. +* [#105](https://github.com/socialcopsdev/camelot/issues/105), [#192](https://github.com/socialcopsdev/camelot/issues/192) and [#215](https://github.com/socialcopsdev/camelot/issues/215) in [#227](https://github.com/socialcopsdev/camelot/pull/227) by Vinayak Mehta. **Documentation** diff --git a/camelot/__version__.py b/camelot/__version__.py index 48246be..3f619b1 100644 --- a/camelot/__version__.py +++ b/camelot/__version__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -VERSION = (0, 4, 1) +VERSION = (0, 5, 0) PRERELEASE = None # alpha, beta or rc REVISION = None From d6628197551e1e0c5ba173a2baf7cb4a76bd69de Mon Sep 17 00:00:00 2001 From: Emmanuel Arias Date: Sun, 14 Oct 2018 21:00:33 -0300 Subject: [PATCH 86/89] Add usage example to cli --- camelot/cli.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/camelot/cli.py b/camelot/cli.py index e978a3c..9f4e038 100644 --- a/camelot/cli.py +++ b/camelot/cli.py @@ -177,3 +177,46 @@ def stream(c, *args, **kwargs): plt.show() else: tables.export(output, f=f, compress=compress) + + +@cli.command('examples') +def examples(*arg, **kwargs): + """Usage example""" + sample = """ + >>> import camelot + >>> tables = camelot.read_pdf('foo.pdf') + >>> tables + + >>> tables.export('foo.csv', f='csv', compress=True) # json, excel, html + >>> tables[0] +
+ >>> tables[0].parsing_report + { + 'accuracy': 99.02, + 'whitespace': 12.24, + 'order': 1, + 'page': 1 + } + >>> tables[0].to_csv('foo.csv') # to_json, to_excel, to_html + >>> tables[0].df # get a pandas DataFrame! + + |-------|-----------|---------------|--------------|-----------|------------|-----------| + | Cycle | KI (1/km) | Distance (mi) | Percent | | | | + | Name | | | Fuel Savings | | | | + |-------|-----------|---------------|--------------|-----------|------------|-----------| + | | | | Improved | Decreased | Eliminate | Decreased | + | | | | Speed | Accel | Stops | Idle | + |-------|-----------|---------------|--------------|-----------|------------|-----------| + | 2012_2| 3.30 | 1.3 | 5.9% | 9.5% | 29.2% | 17.4% | + |-------|-----------|---------------|--------------|-----------|------------|-----------| + | 2145_1| 0.68 | 11.2 | 2.4% | 0.1% | 9.5% | 2.7% | + |-------|-----------|---------------|--------------|-----------|------------|-----------| + | 4234_1| 0.59 | 58.7 | 8.5% | 1.3% | 8.5% | 3.3% | + |-------|-----------|---------------|--------------|-----------|------------|-----------| + | 2032_2| 0.17 | 57.8 | 21.7% | 0.3% | 2.7% | 1.2% | + |-------|-----------|---------------|--------------|-----------|------------|-----------| + | 4171_1| 0.07 | 173.9 | 58.1% | 1.6% | 2.1% | 0.5% | + |-------|-----------|---------------|--------------|-----------|------------|-----------| + + """ + print(sample) From 2dc48f43d6e1a86a2716f7d2cbd6703e9a6a539f Mon Sep 17 00:00:00 2001 From: Emmanuel Arias Date: Tue, 16 Oct 2018 22:46:12 -0300 Subject: [PATCH 87/89] Add CLI documentation, clean cli example command --- camelot/cli.py | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/camelot/cli.py b/camelot/cli.py index 9f4e038..e978a3c 100644 --- a/camelot/cli.py +++ b/camelot/cli.py @@ -177,46 +177,3 @@ def stream(c, *args, **kwargs): plt.show() else: tables.export(output, f=f, compress=compress) - - -@cli.command('examples') -def examples(*arg, **kwargs): - """Usage example""" - sample = """ - >>> import camelot - >>> tables = camelot.read_pdf('foo.pdf') - >>> tables - - >>> tables.export('foo.csv', f='csv', compress=True) # json, excel, html - >>> tables[0] -
- >>> tables[0].parsing_report - { - 'accuracy': 99.02, - 'whitespace': 12.24, - 'order': 1, - 'page': 1 - } - >>> tables[0].to_csv('foo.csv') # to_json, to_excel, to_html - >>> tables[0].df # get a pandas DataFrame! - - |-------|-----------|---------------|--------------|-----------|------------|-----------| - | Cycle | KI (1/km) | Distance (mi) | Percent | | | | - | Name | | | Fuel Savings | | | | - |-------|-----------|---------------|--------------|-----------|------------|-----------| - | | | | Improved | Decreased | Eliminate | Decreased | - | | | | Speed | Accel | Stops | Idle | - |-------|-----------|---------------|--------------|-----------|------------|-----------| - | 2012_2| 3.30 | 1.3 | 5.9% | 9.5% | 29.2% | 17.4% | - |-------|-----------|---------------|--------------|-----------|------------|-----------| - | 2145_1| 0.68 | 11.2 | 2.4% | 0.1% | 9.5% | 2.7% | - |-------|-----------|---------------|--------------|-----------|------------|-----------| - | 4234_1| 0.59 | 58.7 | 8.5% | 1.3% | 8.5% | 3.3% | - |-------|-----------|---------------|--------------|-----------|------------|-----------| - | 2032_2| 0.17 | 57.8 | 21.7% | 0.3% | 2.7% | 1.2% | - |-------|-----------|---------------|--------------|-----------|------------|-----------| - | 4171_1| 0.07 | 173.9 | 58.1% | 1.6% | 2.1% | 0.5% | - |-------|-----------|---------------|--------------|-----------|------------|-----------| - - """ - print(sample) From 3ef50f6f8d828a4ff3980523ad110bcd56c361a1 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 14 Dec 2018 12:57:32 +0530 Subject: [PATCH 88/89] Fix cli.rst --- docs/user/cli.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/cli.rst b/docs/user/cli.rst index 81dd0bc..a7113f5 100644 --- a/docs/user/cli.rst +++ b/docs/user/cli.rst @@ -15,7 +15,7 @@ You can print the help for the interface by typing ``camelot --help`` in your fa Options: --version Show the version and exit. - -v, --verbose Verbose. + -q, --quiet TEXT Suppress logs and warnings. -p, --pages TEXT Comma-separated page numbers. Example: 1,3,4 or 1,4-end. -pw, --password TEXT Password for decryption. From eb7be9c8e60ffb303b5befaa0fea69107ac41c61 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 14 Dec 2018 13:39:05 +0530 Subject: [PATCH 89/89] Add equivalent CLI examples --- docs/user/advanced.rst | 90 ++++++++++++++++++++++++++++++++++++++++ docs/user/quickstart.rst | 18 ++++++++ 2 files changed, 108 insertions(+) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index d2c8b35..37e8d01 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -24,6 +24,12 @@ To process background lines, you can pass ``process_background=True``. >>> tables = camelot.read_pdf('background_lines.pdf', process_background=True) >>> tables[1].df +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot lattice -back background_lines.pdf + .. csv-table:: :file: ../_static/csv/background_lines.csv @@ -63,6 +69,12 @@ Let's plot all the text present on the table's PDF page. >>> camelot.plot(tables[0], kind='text') >>> plt.show() +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot lattice -plot text foo.pdf + .. figure:: ../_static/png/plot_text.png :height: 674 :width: 1366 @@ -84,6 +96,12 @@ Let's plot the table (to see if it was detected correctly or not). This plot typ >>> camelot.plot(tables[0], kind='grid') >>> plt.show() +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot lattice -plot grid foo.pdf + .. figure:: ../_static/png/plot_table.png :height: 674 :width: 1366 @@ -103,6 +121,12 @@ Now, let's plot all table boundaries present on the table's PDF page. >>> camelot.plot(tables[0], kind='contour') >>> plt.show() +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot lattice -plot contour foo.pdf + .. figure:: ../_static/png/plot_contour.png :height: 674 :width: 1366 @@ -120,6 +144,12 @@ Cool, let's plot all line segments present on the table's PDF page. >>> camelot.plot(tables[0], kind='line') >>> plt.show() +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot lattice -plot line foo.pdf + .. figure:: ../_static/png/plot_line.png :height: 674 :width: 1366 @@ -137,6 +167,12 @@ Finally, let's plot all line intersections present on the table's PDF page. >>> camelot.plot(tables[0], kind='joint') >>> plt.show() +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot lattice -plot joint foo.pdf + .. figure:: ../_static/png/plot_joint.png :height: 674 :width: 1366 @@ -154,6 +190,12 @@ You can also visualize the textedges found on a page by specifying ``kind='texte >>> camelot.plot(tables[0], kind='textedge') >>> plt.show() +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot stream -plot textedge foo.pdf + .. figure:: ../_static/png/plot_textedge.png :height: 674 :width: 1366 @@ -175,6 +217,12 @@ Table areas that you want Camelot to analyze can be passed as a list of comma-se >>> tables = camelot.read_pdf('table_areas.pdf', flavor='stream', table_areas=['316,499,566,337']) >>> tables[0].df +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot stream -T 316,499,566,337 table_areas.pdf + .. csv-table:: :file: ../_static/csv/table_areas.csv @@ -196,6 +244,12 @@ Let's get back to the *x* coordinates we got from plotting the text that exists >>> tables = camelot.read_pdf('column_separators.pdf', flavor='stream', columns=['72,95,209,327,442,529,566,606,683']) >>> tables[0].df +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot stream -C 72,95,209,327,442,529,566,606,683 column_separators.pdf + .. csv-table:: "...","...","...","...","...","...","...","...","...","..." @@ -215,6 +269,12 @@ To deal with cases like the output from the previous section, you can pass ``spl >>> tables = camelot.read_pdf('column_separators.pdf', flavor='stream', columns=['72,95,209,327,442,529,566,606,683'], split_text=True) >>> tables[0].df +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot -split stream -C 72,95,209,327,442,529,566,606,683 column_separators.pdf + .. csv-table:: "...","...","...","...","...","...","...","...","...","..." @@ -242,6 +302,12 @@ You can solve this by passing ``flag_size=True``, which will enclose the supersc >>> tables = camelot.read_pdf('superscript.pdf', flavor='stream', flag_size=True) >>> tables[0].df +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot -flag stream superscript.pdf + .. csv-table:: "...","...","...","...","...","...","...","...","...","...","..." @@ -274,6 +340,12 @@ You can pass ``row_close_tol=<+int>`` to group the rows closer together, as show >>> tables = camelot.read_pdf('group_rows.pdf', flavor='stream', row_close_tol=10) >>> tables[0].df +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot stream -r 10 group_rows.pdf + .. csv-table:: "Clave","Nombre Entidad","Clave","","Nombre Municipio","Clave","Nombre Localidad" @@ -317,6 +389,12 @@ Clearly, the smaller lines separating the headers, couldn't be detected. Let's t >>> camelot.plot(tables[0], kind='grid') >>> plt.show() +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot lattice -scale 40 -plot grid short_lines.pdf + .. figure:: ../_static/png/short_lines_2.png :alt: An improved plot of the PDF table with short lines :align: left @@ -380,6 +458,12 @@ No surprises there — it did remain in place (observe the strings "2400" and "A >>> tables = camelot.read_pdf('short_lines.pdf', line_size_scaling=40, shift_text=['r', 'b']) >>> tables[0].df +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot lattice -scale 40 -shift r -shift b short_lines.pdf + .. csv-table:: "Investigations","No. ofHHs","Age/Sex/Physiological Group","Preva-lence","C.I*","RelativePrecision","Sample sizeper State" @@ -425,6 +509,12 @@ We don't need anything else. Now, let's pass ``copy_text=['v']`` to copy text in >>> tables = camelot.read_pdf('copy_text.pdf', copy_text=['v']) >>> tables[0].df +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot lattice -copy v copy_text.pdf + .. csv-table:: "Sl. No.","Name of State/UT","Name of District","Disease/ Illness","No. of Cases","No. of Deaths","Date of start of outbreak","Date of reporting","Current Status","..." diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 5fb5bc0..d9d704a 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -70,6 +70,12 @@ You can also export all tables at once, using the :class:`tables >> tables.export('foo.csv', f='csv') +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot --format csv --output foo.csv lattice foo.pdf + This will export all tables as CSV files at the path specified. Alternatively, you can use ``f='json'``, ``f='excel'`` or ``f='html'``. .. note:: The :meth:`export() ` method exports files with a ``page-*-table-*`` suffix. In the example above, the single table in the list will be exported to ``foo-page-1-table-1.csv``. If the list contains multiple tables, multiple CSV files will be created. To avoid filling up your path with multiple files, you can use ``compress=True``, which will create a single ZIP file at your path with all the CSV files. @@ -85,6 +91,12 @@ By default, Camelot only uses the first page of the PDF to extract tables. To sp >>> camelot.read_pdf('your.pdf', pages='1,2,3') +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot --pages 1,2,3 lattice your.pdf + The ``pages`` keyword argument accepts pages as comma-separated string of page numbers. You can also specify page ranges — for example, ``pages=1,4-10,20-30`` or ``pages=1,4-10,20-end``. Reading encrypted PDFs @@ -98,6 +110,12 @@ To extract tables from encrypted PDF files you must provide a password when call >>> tables +.. tip:: + Here's how you can do the same with the :ref:`command-line interface `. + :: + + $ camelot --password userpass lattice foo.pdf + Currently Camelot only supports PDFs encrypted with ASCII passwords and algorithm `code 1 or 2`_. An exception is thrown if the PDF cannot be read. This may be due to no password being provided, an incorrect password, or an unsupported encryption algorithm. Further encryption support may be added in future, however in the meantime if your PDF files are using unsupported encryption algorithms you are advised to remove encryption before calling :meth:`read_pdf() `. This can been successfully achieved with third-party tools such as `QPDF`_.