73 Commits

Author SHA1 Message Date
Vinayak Mehta c647f573d8 Bump version 2021-06-15 03:58:30 +05:30
Vinayak Mehta 9a3865c716 Update HISTORY.md 2021-06-15 03:55:46 +05:30
Vinayak Mehta ec21904595 Merge pull request #219 from Arnie97/master
[MRG] Add line_overlap and boxes_flow to LAParams
2021-06-15 03:53:40 +05:30
Vinayak Mehta f53be3c73e Update HISTORY.md 2021-06-15 03:53:23 +05:30
Vinayak Mehta 38370cf3f4 Merge pull request #231 from tiagosamaha/master
Fix use resolution argument to generate image with GS
2021-06-15 03:51:00 +05:30
Tiago Samaha Cordeiro 3a8f988740 use resolution argument to generate image with GS 2021-06-15 03:46:07 +05:30
Vinayak Mehta 021be79bf7 Fix README 2021-06-15 03:30:34 +05:30
Vinayak Mehta 2c59e7b0f7 Blacken code 2021-06-15 03:29:35 +05:30
Vinayak Mehta f7c14bf1d4 Update HISTORY.md 2021-06-15 03:28:23 +05:30
Vinayak Mehta a2bb555292 Merge pull request #241 from camelot-dev/add-github-workflow
Add github test workflow and remove travis
2021-06-15 03:06:18 +05:30
Vinayak Mehta cf954a7f6d Rename file and fix badge 2021-06-15 02:41:20 +05:30
Vinayak Mehta 0c9504e1bc Fix workflow and job names 2021-06-15 02:36:18 +05:30
Vinayak Mehta b5cf8a235d Add github test workflow and remove travis 2021-06-15 02:29:25 +05:30
Arnie97 0dee385578 Add line_overlap and boxes_flow to LAParams 2020-12-17 22:12:24 +08:00
Vinayak Mehta 7709e58d64 Merge pull request #206 from edugonza/fix-15
[MRG] Fix #15 extraction of cell data discarding overlapping text boxes
2020-10-28 14:44:35 +05:30
Eduardo Gonzalez Lopez de Murillas 7695d35449 Fix #15 extraction of cell data discarding overlapping text boxes 2020-10-27 18:06:57 +01:00
Vinayak Mehta 8ca30f3a3c Merge pull request #202 from tchx84/close-streams-explicitly
[MRG] handlers: Close file streams explicitly
2020-10-25 05:34:38 +05:30
Martin Abente Lahaye 13a50e2ba2 handlers: Close file streams explicitly
No harm in closing these streams explicitly. Best case
scenario, this prevents descriptors leaks, worse case
scenario, it reduces the amount of  messages like the
following during tests:

ResourceWarning: unclosed file
2020-10-22 11:43:01 -03:00
Vinayak Mehta d17dc43ab2 Merge pull request #196 from jimhall/gs-install-deps
Language added to confirm proper installation of ghostscript libraries
2020-10-18 03:14:24 +05:30
Vinayak Mehta de6faa7af1 Add new checks 2020-10-18 03:13:21 +05:30
Jim Hall 468512a8cd Language added to confirm proper installation of ghostscript libraries 2020-10-08 08:21:34 -04:00
Vinayak Mehta 4edca28c53 Merge branch 'master' of github.com:camelot-dev/camelot 2020-09-08 00:35:57 +05:30
Vinayak Mehta 2a7a4f5b34 Update README and index.rst 2020-09-08 00:35:32 +05:30
Vinayak Mehta 0a3944e54d Add bug report template 2020-09-07 23:39:49 +05:30
Vinayak Mehta 6b42094db5 Update year 2020-08-28 17:52:21 +05:30
Vinayak Mehta 937185412a Merge pull request #189 from camelot-dev/fix-179
[MRG] Prevent taking max of an empty set
2020-08-25 23:03:20 +05:30
Vinayak Mehta 5d20d56e48 Prevent taking max of an empty set 2020-08-25 22:50:31 +05:30
Vinayak Mehta 9087429501 Merge pull request #188 from anakin87/master
[MRG] Add encoding kwarg to camelot.core.Table.to_html method
2020-08-25 19:16:50 +05:30
Vinayak Mehta cc905ff2d9 Merge pull request #186 from pevisscher/patch-1
Use correct re.sub signature
2020-08-25 19:14:58 +05:30
anakin87 eadc54ad25 Merge pull request #1 from anakin87/anakin87-patch-1
Update core.py
2020-08-25 15:28:48 +02:00
anakin87 579bc16be5 Update core.py
Correct method camelot.core.Table.to_html
2020-08-25 15:27:17 +02:00
pevisscher aae2c6b3d4 use correct re.sub signature
`text_strip` currently passes the regex flags as the count parameters, which is hardcoded to `re.UNICODE` (value 32), and thus only replaces the first 32 values.

see https://docs.python.org/3/library/re.html#re.sub for the signature
2020-08-24 16:51:06 +02:00
Vinayak Mehta 705473198f Merge pull request #121 from jedie/patch-2
[MRG] Save plot when filename is specified
2020-08-14 02:36:28 +05:30
Vinayak Mehta b741c0a9e9 Check for none and return none 2020-08-14 02:35:50 +05:30
Vinayak Mehta a6bee88053 Merge pull request #119 from jedie/patch-1
Update advanced.rst
2020-08-14 02:27:49 +05:30
Vinayak Mehta 1e050e1960 Remove plt.show() usage 2020-08-14 02:27:07 +05:30
Vinayak Mehta 28371817db Fix doc link 2020-08-14 02:09:56 +05:30
Vinayak Mehta 7ab5db39d0 Update .readthedocs.yml and remove requirements.txt 2020-08-04 04:37:37 +05:30
Vinayak Mehta 9a5c4b6865 Merge pull request #175 from camelot-dev/revert-0-8-1
[MRG] Revert the changes in v0.8.1
2020-07-27 17:56:48 +05:30
Vinayak Mehta fbe576ffcb Revert the changes in v0.8.1 2020-07-27 17:38:14 +05:30
Vinayak Mehta fcad5067b9 Fix failing test 2020-07-23 00:54:41 +05:30
Vinayak Mehta 1b8ce1d560 Bump requirement versions 2020-07-23 00:40:26 +05:30
Vinayak Mehta 16beb15c43 Bump version and update HISTORY.md 2020-07-21 21:48:29 +05:30
Vinayak Mehta be25e6dbdb Merge pull request #171 from camelot-dev/fix-169
Change error name and update pdfminer.six version
2020-07-21 21:30:14 +05:30
Vinayak Mehta a13e2f6f1f Change error name and update pdfminer.six version 2020-07-21 21:21:01 +05:30
Vinayak Mehta 4b08165328 Merge pull request #166 from stevestock/patch-1
Update install.rst
2020-07-20 16:00:43 +05:30
Vinayak Mehta e5b143d9a8 Update install instructions 2020-07-20 15:59:42 +05:30
Steven Stockhamer 8e5a8e6712 Update install.rst
MacOS now uses zsh by default.  Square brackets must be escaped in zsh
2020-07-19 20:44:16 -04:00
Vinayak Mehta 5efbcdcebb Update requirements.txt 2020-05-24 19:04:50 +05:30
Vinayak Mehta 189fe58bf2 Update requirements.txt 2020-05-24 19:01:03 +05:30
Vinayak Mehta 1575ec1bf0 Add .readthedocs.yml 2020-05-24 18:56:33 +05:30
Vinayak Mehta d5d6a5962b Bump version and update HISTORY.md 2020-05-24 18:36:13 +05:30
Vinayak Mehta 420d5aa624 Merge pull request #146 from camelot-dev/add-python38-travis
[MRG] Fix test data and drop python2 support
2020-05-24 18:31:27 +05:30
Vinayak Mehta a22fa63c4e Fix syntax errors 2020-05-24 18:19:48 +05:30
Vinayak Mehta 52b2a595b4 Add f-strings and remove python3.5 test job 2020-05-24 18:14:43 +05:30
Vinayak Mehta afa1ba7c1f Fix test indent 2020-05-24 17:38:48 +05:30
Vinayak Mehta f725f04223 Remove future imports 2020-05-24 17:33:13 +05:30
Vinayak Mehta 3afb72b872 Fix read_pdf(url) and test data 2020-05-24 17:26:52 +05:30
Vinayak Mehta 6dd9b6ce01 Create FUNDING.yml 2020-05-24 16:14:43 +05:30
Vinayak Mehta fc1b6f6227 Add python38 test job for travis 2020-05-24 15:27:48 +05:30
Vinayak Mehta 7d4c9e53c6 Update README 2020-03-21 18:07:29 +05:30
Jens Diemer f8b6181988 Fix #120 - Save plot 2020-03-15 13:20:27 +01:00
Jens Diemer dbdbc5f19e Update advanced.rst
Bugfix plot example.
2020-03-15 13:12:17 +01:00
Vinayak Mehta 44193e0d26 Add deepsource badge to docs 2019-12-24 13:08:46 +05:30
Vinayak Mehta a9918a78cf Add deepsource badge 2019-12-24 13:07:11 +05:30
Vinayak Mehta 47bb839d7a Create .deepsource.toml 2019-12-24 13:03:45 +05:30
Vinayak Mehta 1b30f8ecf9 Merge pull request #94 from miltonArango/improving-coverage
[MRG] Unit tests for the version generation
2019-11-15 10:50:42 +05:30
Milton Arango 8e28a0cac0 Moved the version tests to test_common PR #94
Applied black formatting
2019-11-14 20:26:20 -05:00
Vinayak Mehta eb2badbbd0 Merge pull request #91 from vasantvohra/patch-1
[MRG] Update how-it-works.rst
2019-11-15 03:36:02 +05:30
Milton Arango 0d1db4b09e Unit Tests for the Version Generation
Unit tests for the __version__.py generate_version method.
2019-10-26 15:41:41 -05:00
Vasant Vohra 167ee9ac69 Update how-it-works.rst
minor typo
2019-10-17 14:04:18 +05:30
Vinayak Mehta 83f816f104 Merge pull request #48 from jnothman/assert_frame_equal
[MRG] Use assert_frame_equal for more informative errors in tests
2019-10-15 15:51:26 +05:30
Joel Nothman 9eb15c09dc Use assert_frame_equal for more informative errors in tests 2019-08-06 11:38:44 +10:00
48 changed files with 1270 additions and 684 deletions
-10
View File
@@ -1,10 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.py]
charset = utf-8
indent_style = space
indent_size = 4
+1
View File
@@ -0,0 +1 @@
open_collective: camelot
+48
View File
@@ -0,0 +1,48 @@
---
name: Bug report
about: Please follow this template to submit bug reports.
title: ''
labels: bug
assignees: ''
---
<!-- Please read the filing issues section of the contributor's guide first: https://camelot-py.readthedocs.io/en/master/dev/contributing.html -->
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to reproduce the bug**
Steps used to install `camelot`:
1. Add step here (you can add more steps too)
Steps to reproduce the behavior:
1. Add step here (you can add more steps too)
**Expected behavior**
A clear and concise description of what you expected to happen.
**Code**
Add the Camelot code snippet that you used.
```
import camelot
# add your code here
```
**PDF**
Add the PDF file that you want to extract tables from.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment**
- OS: [e.g. MacOS]
- Python version:
- Numpy version:
- OpenCV version:
- Ghostscript version:
- Camelot version:
**Additional context**
Add any other context about the problem here.
+23
View File
@@ -0,0 +1,23 @@
name: tests
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install camelot with dependencies
run: |
make install
- name: Test with pytest
run: |
make test
+27
View File
@@ -0,0 +1,27 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Build documentation with MkDocs
#mkdocs:
# configuration: mkdocs.yml
# Optionally build your docs in additional formats such as PDF
formats:
- pdf
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.8
install:
- method: pip
path: .
extra_requirements:
- dev
-32
View File
@@ -1,32 +0,0 @@
sudo: true
language: python
cache: pip
addons:
apt:
update: true
install:
- 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
+1 -1
View File
@@ -23,7 +23,7 @@ A great way to start contributing to Camelot is to pick an issue tagged with the
To install the dependencies needed for development, you can use pip: To install the dependencies needed for development, you can use pip:
<pre> <pre>
$ pip install camelot-py[dev] $ pip install "camelot-py[dev]"
</pre> </pre>
Alternatively, you can clone the project repository, and install using pip: Alternatively, you can clone the project repository, and install using pip:
+49
View File
@@ -4,6 +4,55 @@ Release History
master master
------ ------
0.9.0 (2021-06-15)
------------------
**Bugfixes**
- Fix use of resolution argument to generate image with ghostscript. [#231](https://github.com/camelot-dev/camelot/pull/231) by [Tiago Samaha Cordeiro](https://github.com/tiagosamaha).
- [#15](https://github.com/camelot-dev/camelot/issues/15) Fix duplicate strings being assigned to the same cell. [#206](https://github.com/camelot-dev/camelot/pull/206) by [Eduardo Gonzalez Lopez de Murillas](https://github.com/edugonza).
- Save plot when filename is specified. [#121](https://github.com/camelot-dev/camelot/pull/121) by [Jens Diemer](https://github.com/jedie).
- Close file streams explicitly. [#202](https://github.com/camelot-dev/camelot/pull/202) by [Martin Abente Lahaye](https://github.com/tchx84).
- Use correct re.sub signature. [#186](https://github.com/camelot-dev/camelot/pull/186) by [pevisscher](https://github.com/pevisscher).
- [#183](https://github.com/camelot-dev/camelot/issues/183) Fix UnicodeEncodeError when using Stream flavor by adding encoding kwarg to `to_html`. [#188](https://github.com/camelot-dev/camelot/pull/188) by [Stefano Fiorucci](https://github.com/anakin87).
- [#179](https://github.com/camelot-dev/camelot/issues/179) Fix `max() arg is an empty sequence` error on PDFs with blank pages. [#189](https://github.com/camelot-dev/camelot/pull/189) by Vinayak Mehta.
**Improvements**
- Add `line_overlap` and `boxes_flow` to `LAParams`. [#219](https://github.com/camelot-dev/camelot/pull/219) by [Arnie97](https://github.com/Arnie97).
- [Add bug report template.](https://github.com/camelot-dev/camelot/commit/0a3944e54d133b701edfe9c7546ff11289301ba8)
- Move from [Travis to GitHub Actions](https://github.com/camelot-dev/camelot/pull/241).
- Update `.readthedocs.yml` and [remove requirements.txt](https://github.com/camelot-dev/camelot/commit/7ab5db39d07baa4063f975e9e00f6073340e04c1#diff-cde814ef2f549dc093f5b8fc533b7e8f47e7b32a8081e0760e57d5c25a1139d9)
**Documentation**
- [#193](https://github.com/camelot-dev/camelot/issues/193) Add better checks to confirm proper installation of ghostscript. [#196](https://github.com/camelot-dev/camelot/pull/196) by [jimhall](https://github.com/jimhall).
- Update `advanced.rst` plotting examples. [#119](https://github.com/camelot-dev/camelot/pull/119) by [Jens Diemer](https://github.com/jedie).
0.8.2 (2020-07-27)
------------------
* Revert the changes in `0.8.1`.
0.8.1 (2020-07-21)
------------------
**Bugfixes**
* [#169](https://github.com/camelot-dev/camelot/issues/169) Fix import error caused by `pdfminer.six==20200720`. [#171](https://github.com/camelot-dev/camelot/pull/171) by Vinayak Mehta.
0.8.0 (2020-05-24)
------------------
**Improvements**
* Drop Python 2 support!
* Remove Python 2.7 and 3.5 support.
* Replace all instances of `.format` with f-strings.
* Remove all `__future__` imports.
* Fix HTTP 403 forbidden exception in read_pdf(url) and remove Python 2 urllib support.
* Fix test data.
**Bugfixes** **Bugfixes**
* Fix library discovery on Windows. [#32](https://github.com/camelot-dev/camelot/pull/32) by [KOLANICH](https://github.com/KOLANICH). * Fix library discovery on Windows. [#32](https://github.com/camelot-dev/camelot/pull/32) by [KOLANICH](https://github.com/KOLANICH).
+2 -7
View File
@@ -1,12 +1,7 @@
MIT License MIT License
Modifications: Copyright (c) 2019-2021 Camelot Developers
Copyright (c) 2018-2019 Peeply Private Ltd (Singapore)
Copyright (c) 2019 Camelot Developers
Original project:
Copyright (c) 2018 Peeply Private Ltd (Singapore)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+21 -45
View File
@@ -4,19 +4,18 @@
# Camelot: PDF Table Extraction for Humans # Camelot: PDF Table Extraction for Humans
[![Build Status](https://travis-ci.org/camelot-dev/camelot.svg?branch=master)](https://travis-ci.org/camelot-dev/camelot) [![Documentation Status](https://readthedocs.org/projects/camelot-py/badge/?version=master)](https://camelot-py.readthedocs.io/en/master/) ![Build Status](https://github.com/camelot-dev/camelot/actions/workflows/tests.yml/badge.svg) [![Documentation Status](https://readthedocs.org/projects/camelot-py/badge/?version=master)](https://camelot-py.readthedocs.io/en/master/)
[![codecov.io](https://codecov.io/github/camelot-dev/camelot/badge.svg?branch=master&service=github)](https://codecov.io/github/camelot-dev/camelot?branch=master) [![codecov.io](https://codecov.io/github/camelot-dev/camelot/badge.svg?branch=master&service=github)](https://codecov.io/github/camelot-dev/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/) [![Gitter chat](https://badges.gitter.im/camelot-dev/Lobby.png)](https://gitter.im/camelot-dev/Lobby) [![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)
[![image](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![image](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
**Camelot** is a Python library that can help you extract tables from PDFs!
**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), the web interface to Camelot!
**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/camelot-dev/camelot/blob/master/docs/_static/pdf/foo.pdf). **Here's how you can extract tables from PDFs.** You can check out the PDF used in this example [here](https://github.com/camelot-dev/camelot/blob/master/docs/_static/pdf/foo.pdf).
<pre> <pre>
>>> import camelot >>> import camelot
@@ -46,24 +45,27 @@
| 2032_2 | 0.17 | 57.8 | 21.7% | 0.3% | 2.7% | 1.2% | | 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% | | 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/master/user/cli.html) too! Camelot also comes packaged with a [command-line interface](https://camelot-py.readthedocs.io/en/master/user/cli.html)!
**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".) **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".)
## Why Camelot? ## Why Camelot?
- **You are in control.**: Unlike other libraries and tools which either give a nice output or fail miserably (with no in-between), Camelot gives you the power to tweak table extraction. (This is important since everything in the real world, including PDF table extraction, is fuzzy.) - **Configurability**: Camelot gives you control over the table extraction process with its [tweakable settings](https://camelot-py.readthedocs.io/en/master/user/advanced.html).
- *Bad* tables can be discarded based on **metrics** like accuracy and whitespace, without ever having to manually look at each table. - **Metrics**: Bad tables can be discarded based on metrics like accuracy and whitespace, without having to manually look at each table.
- Each table is a **pandas DataFrame**, which seamlessly integrates into [ETL and data analysis workflows](https://gist.github.com/vinayak-mehta/e5949f7c2410a0e12f25d3682dc9e873). - **Output**: Each table is extracted into a **pandas DataFrame**, which seamlessly integrates into [ETL and data analysis workflows](https://gist.github.com/vinayak-mehta/e5949f7c2410a0e12f25d3682dc9e873). You can also export tables to multiple formats, which include CSV, JSON, Excel, HTML and Sqlite.
- **Export** to multiple formats, including JSON, Excel, HTML and Sqlite.
See [comparison with other PDF table extraction libraries and tools](https://github.com/camelot-dev/camelot/wiki/Comparison-with-other-PDF-Table-Extraction-libraries-and-tools). See [comparison with similar libraries and tools](https://github.com/camelot-dev/camelot/wiki/Comparison-with-other-PDF-Table-Extraction-libraries-and-tools).
## Support the development
If Camelot has helped you, please consider supporting its development with a one-time or monthly donation [on OpenCollective](https://opencollective.com/camelot).
## Installation ## Installation
### Using conda ### Using conda
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. The easiest way to install Camelot is with [conda](https://conda.io/docs/), which is a package manager and environment management system for the [Anaconda](http://docs.continuum.io/anaconda/) distribution.
<pre> <pre>
$ conda install -c conda-forge camelot-py $ conda install -c conda-forge camelot-py
@@ -71,10 +73,10 @@ $ conda install -c conda-forge camelot-py
### Using pip ### Using pip
After [installing the dependencies](https://camelot-py.readthedocs.io/en/master/user/install-deps.html) ([tk](https://packages.ubuntu.com/bionic/python/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/bionic/python/python-tk) and [ghostscript](https://www.ghostscript.com/)), you can also just use pip to install Camelot:
<pre> <pre>
$ pip install camelot-py[cv] $ pip install "camelot-py[cv]"
</pre> </pre>
### From the source code ### From the source code
@@ -94,35 +96,15 @@ $ pip install ".[cv]"
## Documentation ## Documentation
Great documentation is available at [http://camelot-py.readthedocs.io/](http://camelot-py.readthedocs.io/). The documentation is available at [http://camelot-py.readthedocs.io/](http://camelot-py.readthedocs.io/).
## Development ## Wrappers
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. - [camelot-php](https://github.com/randomstate/camelot-php) provides a [PHP](https://www.php.net/) wrapper on Camelot.
### Source code ## Contributing
You can check the latest sources with: The [Contributor's Guide](https://camelot-py.readthedocs.io/en/master/dev/contributing.html) has detailed information about contributing issues, documentation, code, and tests.
<pre>
$ git clone https://www.github.com/camelot-dev/camelot
</pre>
### Setting up a development environment
You can install the development dependencies easily, using pip:
<pre>
$ pip install camelot-py[dev]
</pre>
### Testing
After installation, you can run tests using:
<pre>
$ python setup.py test
</pre>
## Versioning ## Versioning
@@ -131,9 +113,3 @@ Camelot uses [Semantic Versioning](https://semver.org/). For the available versi
## License ## License
This project is licensed under the MIT License, see the [LICENSE](https://github.com/camelot-dev/camelot/blob/master/LICENSE) file for details. This project is licensed under the MIT License, see the [LICENSE](https://github.com/camelot-dev/camelot/blob/master/LICENSE) file for details.
## Support the development
You can support our work on Camelot with a one-time or monthly donation [on OpenCollective](https://opencollective.com/camelot). Organizations who use camelot can also sponsor the project for an acknowledgement on [our documentation site](https://camelot-py.readthedocs.io/en/master/) and this README.
Special thanks to all the users, organizations and contributors that support Camelot!
-2
View File
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import
__all__ = ("main",) __all__ = ("main",)
+3 -3
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
VERSION = (0, 7, 3) VERSION = (0, 9, 0)
PRERELEASE = None # alpha, beta or rc PRERELEASE = None # alpha, beta or rc
REVISION = None REVISION = None
@@ -8,9 +8,9 @@ REVISION = None
def generate_version(version, prerelease=None, revision=None): def generate_version(version, prerelease=None, revision=None):
version_parts = [".".join(map(str, version))] version_parts = [".".join(map(str, version))]
if prerelease is not None: if prerelease is not None:
version_parts.append("-{}".format(prerelease)) version_parts.append(f"-{prerelease}")
if revision is not None: if revision is not None:
version_parts.append(".{}".format(revision)) version_parts.append(f".{revision}")
return "".join(version_parts) return "".join(version_parts)
+2 -2
View File
@@ -204,7 +204,7 @@ def lattice(c, *args, **kwargs):
tables = read_pdf( tables = read_pdf(
filepath, pages=pages, flavor="lattice", suppress_stdout=quiet, **kwargs filepath, pages=pages, flavor="lattice", suppress_stdout=quiet, **kwargs
) )
click.echo("Found {} tables".format(tables.n)) click.echo(f"Found {tables.n} tables")
if plot_type is not None: if plot_type is not None:
for table in tables: for table in tables:
plot(table, kind=plot_type) plot(table, kind=plot_type)
@@ -295,7 +295,7 @@ def stream(c, *args, **kwargs):
tables = read_pdf( tables = read_pdf(
filepath, pages=pages, flavor="stream", suppress_stdout=quiet, **kwargs filepath, pages=pages, flavor="stream", suppress_stdout=quiet, **kwargs
) )
click.echo("Found {} tables".format(tables.n)) click.echo(f"Found {tables.n} tables")
if plot_type is not None: if plot_type is not None:
for table in tables: for table in tables:
plot(table, kind=plot_type) plot(table, kind=plot_type)
+25 -34
View File
@@ -52,12 +52,11 @@ class TextEdge(object):
self.is_valid = False self.is_valid = False
def __repr__(self): def __repr__(self):
return "<TextEdge x={} y0={} y1={} align={} valid={}>".format( x = round(self.x, 2)
round(self.x, 2), y0 = round(self.y0, 2)
round(self.y0, 2), y1 = round(self.y1, 2)
round(self.y1, 2), return (
self.align, f"<TextEdge x={x} y0={y0} y1={y1} align={self.align} valid={self.is_valid}>"
self.is_valid,
) )
def update_coords(self, x, y0, edge_tol=50): def update_coords(self, x, y0, edge_tol=50):
@@ -105,8 +104,7 @@ class TextEdges(object):
return None return None
def add(self, textline, align): def add(self, textline, align):
"""Adds a new text edge to the current dict. """Adds a new text edge to the current dict."""
"""
x = self.get_x_coord(textline, align) x = self.get_x_coord(textline, align)
y0 = textline.y0 y0 = textline.y0
y1 = textline.y1 y1 = textline.y1
@@ -114,8 +112,7 @@ class TextEdges(object):
self._textedges[align].append(te) self._textedges[align].append(te)
def update(self, textline): def update(self, textline):
"""Updates an existing text edge in the current dict. """Updates an existing text edge in the current dict."""
"""
for align in ["left", "right", "middle"]: for align in ["left", "right", "middle"]:
x_coord = self.get_x_coord(textline, align) x_coord = self.get_x_coord(textline, align)
idx = self.find(x_coord, align) idx = self.find(x_coord, align)
@@ -291,9 +288,11 @@ class Cell(object):
self._text = "" self._text = ""
def __repr__(self): def __repr__(self):
return "<Cell x1={} y1={} x2={} y2={}>".format( x1 = round(self.x1, 2)
round(self.x1, 2), round(self.y1, 2), round(self.x2, 2), round(self.y2, 2) y1 = round(self.y1, 2)
) x2 = round(self.x2, 2)
y2 = round(self.y2, 2)
return f"<Cell x1={x1} y1={y1} x2={x2} y2={y2}>"
@property @property
def text(self): def text(self):
@@ -305,8 +304,7 @@ class Cell(object):
@property @property
def bound(self): def bound(self):
"""The number of sides on which the cell is bounded. """The number of sides on which the cell is bounded."""
"""
return self.top + self.bottom + self.left + self.right return self.top + self.bottom + self.left + self.right
@@ -351,7 +349,7 @@ class Table(object):
self.page = None self.page = None
def __repr__(self): def __repr__(self):
return "<{} shape={}>".format(self.__class__.__name__, self.shape) return f"<{self.__class__.__name__} shape={self.shape}>"
def __lt__(self, other): def __lt__(self, other):
if self.page == other.page: if self.page == other.page:
@@ -362,8 +360,7 @@ class Table(object):
@property @property
def data(self): def data(self):
"""Returns two-dimensional list of strings in table. """Returns two-dimensional list of strings in table."""
"""
d = [] d = []
for row in self.cells: for row in self.cells:
d.append([cell.text.strip() for cell in row]) d.append([cell.text.strip() for cell in row])
@@ -384,8 +381,7 @@ class Table(object):
return report return report
def set_all_edges(self): def set_all_edges(self):
"""Sets all table edges to True. """Sets all table edges to True."""
"""
for row in self.cells: for row in self.cells:
for cell in row: for cell in row:
cell.left = cell.right = cell.top = cell.bottom = True cell.left = cell.right = cell.top = cell.bottom = True
@@ -527,8 +523,7 @@ class Table(object):
return self return self
def set_border(self): def set_border(self):
"""Sets table border edges to True. """Sets table border edges to True."""
"""
for r in range(len(self.rows)): for r in range(len(self.rows)):
self.cells[r][0].left = True self.cells[r][0].left = True
self.cells[r][len(self.cols) - 1].right = True self.cells[r][len(self.cols) - 1].right = True
@@ -612,7 +607,7 @@ class Table(object):
""" """
kw = { kw = {
"sheet_name": "page-{}-table-{}".format(self.page, self.order), "sheet_name": f"page-{self.page}-table-{self.order}",
"encoding": "utf-8", "encoding": "utf-8",
} }
kw.update(kwargs) kw.update(kwargs)
@@ -632,7 +627,7 @@ class Table(object):
""" """
html_string = self.df.to_html(**kwargs) html_string = self.df.to_html(**kwargs)
with open(path, "w") as f: with open(path, "w", encoding="utf-8") as f:
f.write(html_string) f.write(html_string)
def to_sqlite(self, path, **kwargs): def to_sqlite(self, path, **kwargs):
@@ -649,7 +644,7 @@ class Table(object):
kw = {"if_exists": "replace", "index": False} kw = {"if_exists": "replace", "index": False}
kw.update(kwargs) kw.update(kwargs)
conn = sqlite3.connect(path) conn = sqlite3.connect(path)
table_name = "page-{}-table-{}".format(self.page, self.order) table_name = f"page-{self.page}-table-{self.order}"
self.df.to_sql(table_name, conn, **kw) self.df.to_sql(table_name, conn, **kw)
conn.commit() conn.commit()
conn.close() conn.close()
@@ -670,7 +665,7 @@ class TableList(object):
self._tables = tables self._tables = tables
def __repr__(self): def __repr__(self):
return "<{} n={}>".format(self.__class__.__name__, self.n) return f"<{self.__class__.__name__} n={self.n}>"
def __len__(self): def __len__(self):
return len(self._tables) return len(self._tables)
@@ -680,7 +675,7 @@ class TableList(object):
@staticmethod @staticmethod
def _format_func(table, f): def _format_func(table, f):
return getattr(table, "to_{}".format(f)) return getattr(table, f"to_{f}")
@property @property
def n(self): def n(self):
@@ -691,9 +686,7 @@ class TableList(object):
root = kwargs.get("root") root = kwargs.get("root")
ext = kwargs.get("ext") ext = kwargs.get("ext")
for table in self._tables: for table in self._tables:
filename = os.path.join( filename = f"{root}-page-{table.page}-table-{table.order}{ext}"
"{}-page-{}-table-{}{}".format(root, table.page, table.order, ext)
)
filepath = os.path.join(dirname, filename) filepath = os.path.join(dirname, filename)
to_format = self._format_func(table, f) to_format = self._format_func(table, f)
to_format(filepath) to_format(filepath)
@@ -706,9 +699,7 @@ class TableList(object):
zipname = os.path.join(os.path.dirname(path), root) + ".zip" zipname = os.path.join(os.path.dirname(path), root) + ".zip"
with zipfile.ZipFile(zipname, "w", allowZip64=True) as z: with zipfile.ZipFile(zipname, "w", allowZip64=True) as z:
for table in self._tables: for table in self._tables:
filename = os.path.join( filename = f"{root}-page-{table.page}-table-{table.order}{ext}"
"{}-page-{}-table-{}{}".format(root, table.page, table.order, ext)
)
filepath = os.path.join(dirname, filename) filepath = os.path.join(dirname, filename)
z.write(filepath, os.path.basename(filepath)) z.write(filepath, os.path.basename(filepath))
@@ -741,7 +732,7 @@ class TableList(object):
filepath = os.path.join(dirname, basename) filepath = os.path.join(dirname, basename)
writer = pd.ExcelWriter(filepath) writer = pd.ExcelWriter(filepath)
for table in self._tables: for table in self._tables:
sheet_name = "page-{}-table-{}".format(table.page, table.order) sheet_name = f"page-{table.page}-table-{table.order}"
table.df.to_excel(writer, sheet_name=sheet_name, encoding="utf-8") table.df.to_excel(writer, sheet_name=sheet_name, encoding="utf-8")
writer.save() writer.save()
if compress: if compress:
+1 -2
View File
@@ -81,8 +81,7 @@ class __Ghostscript(object):
def Ghostscript(*args, **kwargs): def Ghostscript(*args, **kwargs):
"""Factory function for setting up a Ghostscript instance """Factory function for setting up a Ghostscript instance"""
"""
global __instance__ global __instance__
# Ghostscript only supports a single instance # Ghostscript only supports a single instance
if __instance__ is None: if __instance__ is None:
+5 -1
View File
@@ -81,6 +81,7 @@ def delete_instance(instance):
""" """
return libgs.gsapi_delete_instance(instance) return libgs.gsapi_delete_instance(instance)
if sys.platform == "win32": if sys.platform == "win32":
c_stdstream_call_t = WINFUNCTYPE(c_int, gs_main_instance, POINTER(c_char), c_int) c_stdstream_call_t = WINFUNCTYPE(c_int, gs_main_instance, POINTER(c_char), c_int)
else: else:
@@ -247,7 +248,10 @@ if sys.platform == "win32":
libgs = __win32_finddll() libgs = __win32_finddll()
if not libgs: if not libgs:
import ctypes.util import ctypes.util
libgs = ctypes.util.find_library("".join(("gsdll", str(ctypes.sizeof(ctypes.c_voidp) * 8), ".dll"))) # finds in %PATH%
libgs = ctypes.util.find_library(
"".join(("gsdll", str(ctypes.sizeof(ctypes.c_voidp) * 8), ".dll"))
) # finds in %PATH%
if not libgs: if not libgs:
raise RuntimeError("Please make sure that Ghostscript is installed") raise RuntimeError("Please make sure that Ghostscript is installed")
libgs = windll.LoadLibrary(libgs) libgs = windll.LoadLibrary(libgs)
+8 -6
View File
@@ -70,7 +70,8 @@ class PDFHandler(object):
if pages == "1": if pages == "1":
page_numbers.append({"start": 1, "end": 1}) page_numbers.append({"start": 1, "end": 1})
else: else:
infile = PdfFileReader(open(filepath, "rb"), strict=False) instream = open(filepath, "rb")
infile = PdfFileReader(instream, strict=False)
if infile.isEncrypted: if infile.isEncrypted:
infile.decrypt(self.password) infile.decrypt(self.password)
if pages == "all": if pages == "all":
@@ -84,6 +85,7 @@ class PDFHandler(object):
page_numbers.append({"start": int(a), "end": int(b)}) page_numbers.append({"start": int(a), "end": int(b)})
else: else:
page_numbers.append({"start": int(r), "end": int(r)}) page_numbers.append({"start": int(r), "end": int(r)})
instream.close()
P = [] P = []
for p in page_numbers: for p in page_numbers:
P.extend(range(p["start"], p["end"] + 1)) P.extend(range(p["start"], p["end"] + 1))
@@ -106,7 +108,7 @@ class PDFHandler(object):
infile = PdfFileReader(fileobj, strict=False) infile = PdfFileReader(fileobj, strict=False)
if infile.isEncrypted: if infile.isEncrypted:
infile.decrypt(self.password) infile.decrypt(self.password)
fpath = os.path.join(temp, "page-{0}.pdf".format(page)) fpath = os.path.join(temp, f"page-{page}.pdf")
froot, fext = os.path.splitext(fpath) froot, fext = os.path.splitext(fpath)
p = infile.getPage(page - 1) p = infile.getPage(page - 1)
outfile = PdfFileWriter() outfile = PdfFileWriter()
@@ -122,7 +124,8 @@ class PDFHandler(object):
if rotation != "": if rotation != "":
fpath_new = "".join([froot.replace("page", "p"), "_rotated", fext]) fpath_new = "".join([froot.replace("page", "p"), "_rotated", fext])
os.rename(fpath, fpath_new) os.rename(fpath, fpath_new)
infile = PdfFileReader(open(fpath_new, "rb"), strict=False) instream = open(fpath_new, "rb")
infile = PdfFileReader(instream, strict=False)
if infile.isEncrypted: if infile.isEncrypted:
infile.decrypt(self.password) infile.decrypt(self.password)
outfile = PdfFileWriter() outfile = PdfFileWriter()
@@ -134,6 +137,7 @@ class PDFHandler(object):
outfile.addPage(p) outfile.addPage(p)
with open(fpath, "wb") as f: with open(fpath, "wb") as f:
outfile.write(f) outfile.write(f)
instream.close()
def parse( def parse(
self, flavor="lattice", suppress_stdout=False, layout_kwargs={}, **kwargs self, flavor="lattice", suppress_stdout=False, layout_kwargs={}, **kwargs
@@ -163,9 +167,7 @@ class PDFHandler(object):
with TemporaryDirectory() as tempdir: with TemporaryDirectory() as tempdir:
for p in self.pages: for p in self.pages:
self._save_page(self.filepath, p, tempdir) self._save_page(self.filepath, p, tempdir)
pages = [ pages = [os.path.join(tempdir, f"page-{p}.pdf") for p in self.pages]
os.path.join(tempdir, "page-{0}.pdf".format(p)) for p in self.pages
]
parser = Lattice(**kwargs) if flavor == "lattice" else Stream(**kwargs) parser = Lattice(**kwargs) if flavor == "lattice" else Stream(**kwargs)
for p in pages: for p in pages:
t = parser.extract_tables( t = parser.extract_tables(
-2
View File
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
import cv2 import cv2
import numpy as np import numpy as np
+1 -2
View File
@@ -6,8 +6,7 @@ from ..utils import get_page_layout, get_text_objects
class BaseParser(object): class BaseParser(object):
"""Defines a base parser. """Defines a base parser."""
"""
def _generate_layout(self, filename, layout_kwargs): def _generate_layout(self, filename, layout_kwargs):
self.filename = filename self.filename = filename
+2 -3
View File
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
import os import os
import sys import sys
import copy import copy
@@ -212,8 +211,8 @@ class Lattice(BaseParser):
from ..ext.ghostscript import Ghostscript from ..ext.ghostscript import Ghostscript
self.imagename = "".join([self.rootname, ".png"]) self.imagename = "".join([self.rootname, ".png"])
gs_call = "-q -sDEVICE=png16m -o {} -r300 {}".format( gs_call = "-q -sDEVICE=png16m -o {} -r{} {}".format(
self.imagename, self.filename self.imagename, self.resolution, self.filename
) )
gs_call = gs_call.encode().split() gs_call = gs_call.encode().split()
null = open(os.devnull, "wb") null = open(os.devnull, "wb")
+50 -45
View File
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
import os import os
import logging import logging
import warnings import warnings
@@ -66,7 +65,7 @@ class Stream(BaseParser):
edge_tol=50, edge_tol=50,
row_tol=2, row_tol=2,
column_tol=0, column_tol=0,
**kwargs **kwargs,
): ):
self.table_regions = table_regions self.table_regions = table_regions
self.table_areas = table_areas self.table_areas = table_areas
@@ -122,6 +121,7 @@ class Stream(BaseParser):
row_y = 0 row_y = 0
rows = [] rows = []
temp = [] temp = []
for t in text: for t in text:
# is checking for upright necessary? # is checking for upright necessary?
# if t.get_text().strip() and all([obj.upright for obj in t._objs if # if t.get_text().strip() and all([obj.upright for obj in t._objs if
@@ -132,8 +132,10 @@ class Stream(BaseParser):
temp = [] temp = []
row_y = t.y0 row_y = t.y0
temp.append(t) temp.append(t)
rows.append(sorted(temp, key=lambda t: t.x0)) rows.append(sorted(temp, key=lambda t: t.x0))
__ = rows.pop(0) # TODO: hacky if len(rows) > 1:
__ = rows.pop(0) # TODO: hacky
return rows return rows
@staticmethod @staticmethod
@@ -346,43 +348,46 @@ class Stream(BaseParser):
else: else:
# calculate mode of the list of number of elements in # calculate mode of the list of number of elements in
# each row to guess the number of columns # each row to guess the number of columns
ncols = max(set(elements), key=elements.count) if not len(elements):
if ncols == 1: cols = [(text_x_min, text_x_max)]
# if mode is 1, the page usually contains not tables else:
# but there can be cases where the list can be skewed, ncols = max(set(elements), key=elements.count)
# try to remove all 1s from list in this case and if ncols == 1:
# see if the list contains elements, if yes, then use # if mode is 1, the page usually contains not tables
# the mode after removing 1s # but there can be cases where the list can be skewed,
elements = list(filter(lambda x: x != 1, elements)) # try to remove all 1s from list in this case and
if len(elements): # see if the list contains elements, if yes, then use
ncols = max(set(elements), key=elements.count) # the mode after removing 1s
else: elements = list(filter(lambda x: x != 1, elements))
warnings.warn( if len(elements):
"No tables found in table area {}".format(table_idx + 1) ncols = max(set(elements), key=elements.count)
else:
warnings.warn(f"No tables found in table area {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), column_tol=self.column_tol)
inner_text = []
for i in range(1, len(cols)):
left = cols[i - 1][1]
right = cols[i][0]
inner_text.extend(
[
t
for direction in self.t_bbox
for t in self.t_bbox[direction]
if t.x0 > left and t.x1 < right
]
) )
cols = [(t.x0, t.x1) for r in rows_grouped if len(r) == ncols for t in r] outer_text = [
cols = self._merge_columns(sorted(cols), column_tol=self.column_tol) t
inner_text = [] for direction in self.t_bbox
for i in range(1, len(cols)): for t in self.t_bbox[direction]
left = cols[i - 1][1] if t.x0 > cols[-1][1] or t.x1 < cols[0][0]
right = cols[i][0] ]
inner_text.extend( inner_text.extend(outer_text)
[ cols = self._add_columns(cols, inner_text, self.row_tol)
t cols = self._join_columns(cols, text_x_min, text_x_max)
for direction in self.t_bbox
for t in self.t_bbox[direction]
if t.x0 > left and t.x1 < right
]
)
outer_text = [
t
for direction in self.t_bbox
for t in self.t_bbox[direction]
if t.x0 > cols[-1][1] or t.x1 < cols[0][0]
]
inner_text.extend(outer_text)
cols = self._add_columns(cols, inner_text, self.row_tol)
cols = self._join_columns(cols, text_x_min, text_x_max)
return cols, rows return cols, rows
@@ -433,19 +438,19 @@ class Stream(BaseParser):
def extract_tables(self, filename, suppress_stdout=False, layout_kwargs={}): def extract_tables(self, filename, suppress_stdout=False, layout_kwargs={}):
self._generate_layout(filename, layout_kwargs) self._generate_layout(filename, layout_kwargs)
base_filename = os.path.basename(self.rootname)
if not suppress_stdout: if not suppress_stdout:
logger.info("Processing {}".format(os.path.basename(self.rootname))) logger.info(f"Processing {base_filename}")
if not self.horizontal_text: if not self.horizontal_text:
if self.images: if self.images:
warnings.warn( warnings.warn(
"{} is image-based, camelot only works on" f"{base_filename} is image-based, camelot only works on"
" text-based pages.".format(os.path.basename(self.rootname)) " text-based pages."
) )
else: else:
warnings.warn( warnings.warn(f"No tables found on {base_filename}")
"No tables found on {}".format(os.path.basename(self.rootname))
)
return [] return []
self._generate_table_bbox() self._generate_table_bbox()
+9 -7
View File
@@ -34,16 +34,18 @@ class PlotMethods(object):
raise ImportError("matplotlib is required for plotting.") raise ImportError("matplotlib is required for plotting.")
if table.flavor == "lattice" and kind in ["textedge"]: if table.flavor == "lattice" and kind in ["textedge"]:
raise NotImplementedError( raise NotImplementedError(f"Lattice flavor does not support kind='{kind}'")
"Lattice flavor does not support kind='{}'".format(kind)
)
elif table.flavor == "stream" and kind in ["joint", "line"]: elif table.flavor == "stream" and kind in ["joint", "line"]:
raise NotImplementedError( raise NotImplementedError(f"Stream flavor does not support kind='{kind}'")
"Stream flavor does not support kind='{}'".format(kind)
)
plot_method = getattr(self, kind) plot_method = getattr(self, kind)
return plot_method(table) fig = plot_method(table)
if filename is not None:
fig.savefig(filename)
return None
return fig
def text(self, table): def text(self, table):
"""Generates a plot for all text elements present """Generates a plot for all text elements present
+115 -31
View File
@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
import re
import os import os
import sys import re
import random import random
import shutil import shutil
import string import string
@@ -29,16 +27,9 @@ from pdfminer.layout import (
LTImage, LTImage,
) )
from urllib.request import Request, urlopen
PY3 = sys.version_info[0] >= 3 from urllib.parse import urlparse as parse_url
if PY3: from urllib.parse import uses_relative, uses_netloc, uses_params
from urllib.request import urlopen
from urllib.parse import urlparse as parse_url
from urllib.parse import uses_relative, uses_netloc, uses_params
else:
from urllib2 import urlopen
from urlparse import urlparse as parse_url
from urlparse import uses_relative, uses_netloc, uses_params
_VALID_URLS = set(uses_relative + uses_netloc + uses_params) _VALID_URLS = set(uses_relative + uses_netloc + uses_params)
@@ -88,13 +79,12 @@ def download_url(url):
Temporary filepath. Temporary filepath.
""" """
filename = "{}.pdf".format(random_string(6)) filename = f"{random_string(6)}.pdf"
with tempfile.NamedTemporaryFile("wb", delete=False) as f: with tempfile.NamedTemporaryFile("wb", delete=False) as f:
obj = urlopen(url) headers = {"User-Agent": "Mozilla/5.0"}
if PY3: request = Request(url, None, headers)
content_type = obj.info().get_content_type() obj = urlopen(request)
else: content_type = obj.info().get_content_type()
content_type = obj.info().getheader("Content-Type")
if content_type != "application/pdf": if content_type != "application/pdf":
raise NotImplementedError("File format not supported") raise NotImplementedError("File format not supported")
f.write(obj.read()) f.write(obj.read())
@@ -123,9 +113,7 @@ def validate_input(kwargs, flavor="lattice"):
isec = set(parser_kwargs).intersection(set(input_kwargs.keys())) isec = set(parser_kwargs).intersection(set(input_kwargs.keys()))
if isec: if isec:
raise ValueError( raise ValueError(
"{} cannot be used with flavor='{}'".format( f"{','.join(sorted(isec))} cannot be used with flavor='{flavor}'"
",".join(sorted(isec)), flavor
)
) )
if flavor == "lattice": if flavor == "lattice":
@@ -365,7 +353,7 @@ def text_in_bbox(bbox, text):
Returns Returns
------- -------
t_bbox : list t_bbox : list
List of PDFMiner text objects that lie inside table. List of PDFMiner text objects that lie inside table, discarding the overlapping ones
""" """
lb = (bbox[0], bbox[1]) lb = (bbox[0], bbox[1])
@@ -376,7 +364,97 @@ def text_in_bbox(bbox, text):
if lb[0] - 2 <= (t.x0 + t.x1) / 2.0 <= rt[0] + 2 if lb[0] - 2 <= (t.x0 + t.x1) / 2.0 <= rt[0] + 2
and lb[1] - 2 <= (t.y0 + t.y1) / 2.0 <= rt[1] + 2 and lb[1] - 2 <= (t.y0 + t.y1) / 2.0 <= rt[1] + 2
] ]
return t_bbox
# Avoid duplicate text by discarding overlapping boxes
rest = {t for t in t_bbox}
for ba in t_bbox:
for bb in rest.copy():
if ba == bb:
continue
if bbox_intersect(ba, bb):
# if the intersection is larger than 80% of ba's size, we keep the longest
if (bbox_intersection_area(ba, bb) / bbox_area(ba)) > 0.8:
if bbox_longer(bb, ba):
rest.discard(ba)
unique_boxes = list(rest)
return unique_boxes
def bbox_intersection_area(ba, bb) -> float:
"""Returns area of the intersection of the bounding boxes of two PDFMiner objects.
Parameters
----------
ba : PDFMiner text object
bb : PDFMiner text object
Returns
-------
intersection_area : float
Area of the intersection of the bounding boxes of both objects
"""
x_left = max(ba.x0, bb.x0)
y_top = min(ba.y1, bb.y1)
x_right = min(ba.x1, bb.x1)
y_bottom = max(ba.y0, bb.y0)
if x_right < x_left or y_bottom > y_top:
return 0.0
intersection_area = (x_right - x_left) * (y_top - y_bottom)
return intersection_area
def bbox_area(bb) -> float:
"""Returns area of the bounding box of a PDFMiner object.
Parameters
----------
bb : PDFMiner text object
Returns
-------
area : float
Area of the bounding box of the object
"""
return (bb.x1 - bb.x0) * (bb.y1 - bb.y0)
def bbox_intersect(ba, bb) -> bool:
"""Returns True if the bounding boxes of two PDFMiner objects intersect.
Parameters
----------
ba : PDFMiner text object
bb : PDFMiner text object
Returns
-------
overlaps : bool
True if the bounding boxes intersect
"""
return ba.x1 >= bb.x0 and bb.x1 >= ba.x0 and ba.y1 >= bb.y0 and bb.y1 >= ba.y0
def bbox_longer(ba, bb) -> bool:
"""Returns True if the bounding box of the first PDFMiner object is longer or equal to the second.
Parameters
----------
ba : PDFMiner text object
bb : PDFMiner text object
Returns
-------
longer : bool
True if the bounding box of the first object is longer or equal
"""
return (ba.x1 - ba.x0) >= (bb.x1 - bb.x0)
def merge_close_lines(ar, line_tol=2): def merge_close_lines(ar, line_tol=2):
@@ -423,7 +501,7 @@ def text_strip(text, strip=""):
return text return text
stripped = re.sub( stripped = re.sub(
r"[{}]".format("".join(map(re.escape, strip))), "", text, re.UNICODE fr"[{''.join(map(re.escape, strip))}]", "", text, flags=re.UNICODE
) )
return stripped return stripped
@@ -660,9 +738,7 @@ def get_table_index(
text_range = (t.x0, t.x1) text_range = (t.x0, t.x1)
col_range = (table.cols[0][0], table.cols[-1][1]) col_range = (table.cols[0][0], table.cols[-1][1])
warnings.warn( warnings.warn(
"{} {} does not lie in column range {}".format( f"{text} {text_range} does not lie in column range {col_range}"
text, text_range, col_range
)
) )
r_idx = r r_idx = r
c_idx = lt_col_overlap.index(max(lt_col_overlap)) c_idx = lt_col_overlap.index(max(lt_col_overlap))
@@ -762,23 +838,27 @@ def compute_whitespace(d):
def get_page_layout( def get_page_layout(
filename, filename,
line_overlap=0.5,
char_margin=1.0, char_margin=1.0,
line_margin=0.5, line_margin=0.5,
word_margin=0.1, word_margin=0.1,
boxes_flow=0.5,
detect_vertical=True, detect_vertical=True,
all_texts=True, all_texts=True,
): ):
"""Returns a PDFMiner LTPage object and page dimension of a single """Returns a PDFMiner LTPage object and page dimension of a single
page pdf. See https://euske.github.io/pdfminer/ to get definitions page pdf. To get the definitions of kwargs, see
of kwargs. https://pdfminersix.rtfd.io/en/latest/reference/composable.html.
Parameters Parameters
---------- ----------
filename : string filename : string
Path to pdf file. Path to pdf file.
line_overlap : float
char_margin : float char_margin : float
line_margin : float line_margin : float
word_margin : float word_margin : float
boxes_flow : float
detect_vertical : bool detect_vertical : bool
all_texts : bool all_texts : bool
@@ -794,11 +874,15 @@ def get_page_layout(
parser = PDFParser(f) parser = PDFParser(f)
document = PDFDocument(parser) document = PDFDocument(parser)
if not document.is_extractable: if not document.is_extractable:
raise PDFTextExtractionNotAllowed raise PDFTextExtractionNotAllowed(
f"Text extraction is not allowed: {filename}"
)
laparams = LAParams( laparams = LAParams(
line_overlap=line_overlap,
char_margin=char_margin, char_margin=char_margin,
line_margin=line_margin, line_margin=line_margin,
word_margin=word_margin, word_margin=word_margin,
boxes_flow=boxes_flow,
detect_vertical=detect_vertical, detect_vertical=detect_vertical,
all_texts=all_texts, all_texts=all_texts,
) )
+74 -71
View File
@@ -1,7 +1,19 @@
# flasky pygments style based on tango style # flasky pygments style based on tango style
from pygments.style import Style from pygments.style import Style
from pygments.token import Keyword, Name, Comment, String, Error, \ from pygments.token import (
Number, Operator, Generic, Whitespace, Punctuation, Other, Literal Keyword,
Name,
Comment,
String,
Error,
Number,
Operator,
Generic,
Whitespace,
Punctuation,
Other,
Literal,
)
class FlaskyStyle(Style): class FlaskyStyle(Style):
@@ -11,76 +23,67 @@ class FlaskyStyle(Style):
styles = { styles = {
# No corresponding class for the following: # No corresponding class for the following:
# Text: "", # class: '' # Text: "", # class: ''
Whitespace: "underline #f8f8f8", # class: 'w' Whitespace: "underline #f8f8f8", # class: 'w'
Error: "#a40000 border:#ef2929", # class: 'err' Error: "#a40000 border:#ef2929", # class: 'err'
Other: "#000000", # class 'x' Other: "#000000", # class 'x'
Comment: "italic #8f5902", # class: 'c'
Comment: "italic #8f5902", # class: 'c' Comment.Preproc: "noitalic", # class: 'cp'
Comment.Preproc: "noitalic", # class: 'cp' Keyword: "bold #004461", # class: 'k'
Keyword.Constant: "bold #004461", # class: 'kc'
Keyword: "bold #004461", # class: 'k' Keyword.Declaration: "bold #004461", # class: 'kd'
Keyword.Constant: "bold #004461", # class: 'kc' Keyword.Namespace: "bold #004461", # class: 'kn'
Keyword.Declaration: "bold #004461", # class: 'kd' Keyword.Pseudo: "bold #004461", # class: 'kp'
Keyword.Namespace: "bold #004461", # class: 'kn' Keyword.Reserved: "bold #004461", # class: 'kr'
Keyword.Pseudo: "bold #004461", # class: 'kp' Keyword.Type: "bold #004461", # class: 'kt'
Keyword.Reserved: "bold #004461", # class: 'kr' Operator: "#582800", # class: 'o'
Keyword.Type: "bold #004461", # class: 'kt' Operator.Word: "bold #004461", # class: 'ow' - like keywords
Punctuation: "bold #000000", # class: 'p'
Operator: "#582800", # class: 'o'
Operator.Word: "bold #004461", # class: 'ow' - like keywords
Punctuation: "bold #000000", # class: 'p'
# because special names such as Name.Class, Name.Function, etc. # because special names such as Name.Class, Name.Function, etc.
# are not recognized as such later in the parsing, we choose them # are not recognized as such later in the parsing, we choose them
# to look the same as ordinary variables. # to look the same as ordinary variables.
Name: "#000000", # class: 'n' Name: "#000000", # class: 'n'
Name.Attribute: "#c4a000", # class: 'na' - to be revised Name.Attribute: "#c4a000", # class: 'na' - to be revised
Name.Builtin: "#004461", # class: 'nb' Name.Builtin: "#004461", # class: 'nb'
Name.Builtin.Pseudo: "#3465a4", # class: 'bp' Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
Name.Class: "#000000", # class: 'nc' - to be revised Name.Class: "#000000", # class: 'nc' - to be revised
Name.Constant: "#000000", # class: 'no' - to be revised Name.Constant: "#000000", # class: 'no' - to be revised
Name.Decorator: "#888", # class: 'nd' - to be revised Name.Decorator: "#888", # class: 'nd' - to be revised
Name.Entity: "#ce5c00", # class: 'ni' Name.Entity: "#ce5c00", # class: 'ni'
Name.Exception: "bold #cc0000", # class: 'ne' Name.Exception: "bold #cc0000", # class: 'ne'
Name.Function: "#000000", # class: 'nf' Name.Function: "#000000", # class: 'nf'
Name.Property: "#000000", # class: 'py' Name.Property: "#000000", # class: 'py'
Name.Label: "#f57900", # class: 'nl' Name.Label: "#f57900", # class: 'nl'
Name.Namespace: "#000000", # class: 'nn' - to be revised Name.Namespace: "#000000", # class: 'nn' - to be revised
Name.Other: "#000000", # class: 'nx' Name.Other: "#000000", # class: 'nx'
Name.Tag: "bold #004461", # class: 'nt' - like a keyword Name.Tag: "bold #004461", # class: 'nt' - like a keyword
Name.Variable: "#000000", # class: 'nv' - to be revised Name.Variable: "#000000", # class: 'nv' - to be revised
Name.Variable.Class: "#000000", # class: 'vc' - to be revised Name.Variable.Class: "#000000", # class: 'vc' - to be revised
Name.Variable.Global: "#000000", # class: 'vg' - to be revised Name.Variable.Global: "#000000", # class: 'vg' - to be revised
Name.Variable.Instance: "#000000", # class: 'vi' - to be revised Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
Number: "#990000", # class: 'm'
Number: "#990000", # class: 'm' Literal: "#000000", # class: 'l'
Literal.Date: "#000000", # class: 'ld'
Literal: "#000000", # class: 'l' String: "#4e9a06", # class: 's'
Literal.Date: "#000000", # class: 'ld' String.Backtick: "#4e9a06", # class: 'sb'
String.Char: "#4e9a06", # class: 'sc'
String: "#4e9a06", # class: 's' String.Doc: "italic #8f5902", # class: 'sd' - like a comment
String.Backtick: "#4e9a06", # class: 'sb' String.Double: "#4e9a06", # class: 's2'
String.Char: "#4e9a06", # class: 'sc' String.Escape: "#4e9a06", # class: 'se'
String.Doc: "italic #8f5902", # class: 'sd' - like a comment String.Heredoc: "#4e9a06", # class: 'sh'
String.Double: "#4e9a06", # class: 's2' String.Interpol: "#4e9a06", # class: 'si'
String.Escape: "#4e9a06", # class: 'se' String.Other: "#4e9a06", # class: 'sx'
String.Heredoc: "#4e9a06", # class: 'sh' String.Regex: "#4e9a06", # class: 'sr'
String.Interpol: "#4e9a06", # class: 'si' String.Single: "#4e9a06", # class: 's1'
String.Other: "#4e9a06", # class: 'sx' String.Symbol: "#4e9a06", # class: 'ss'
String.Regex: "#4e9a06", # class: 'sr' Generic: "#000000", # class: 'g'
String.Single: "#4e9a06", # class: 's1' Generic.Deleted: "#a40000", # class: 'gd'
String.Symbol: "#4e9a06", # class: 'ss' Generic.Emph: "italic #000000", # class: 'ge'
Generic.Error: "#ef2929", # class: 'gr'
Generic: "#000000", # class: 'g' Generic.Heading: "bold #000080", # class: 'gh'
Generic.Deleted: "#a40000", # class: 'gd' Generic.Inserted: "#00A000", # class: 'gi'
Generic.Emph: "italic #000000", # class: 'ge' Generic.Output: "#888", # class: 'go'
Generic.Error: "#ef2929", # class: 'gr' Generic.Prompt: "#745334", # class: 'gp'
Generic.Heading: "bold #000080", # class: 'gh' Generic.Strong: "bold #000000", # class: 'gs'
Generic.Inserted: "#00A000", # class: 'gi' Generic.Subheading: "bold #800080", # class: 'gu'
Generic.Output: "#888", # class: 'go' Generic.Traceback: "bold #a40000", # class: 'gt'
Generic.Prompt: "#745334", # class: 'gp'
Generic.Strong: "bold #000000", # class: 'gs'
Generic.Subheading: "bold #800080", # class: 'gu'
Generic.Traceback: "bold #a40000", # class: 'gt'
} }
+65 -55
View File
@@ -22,8 +22,8 @@ import sys
# sys.path.insert(0, os.path.abspath('..')) # sys.path.insert(0, os.path.abspath('..'))
# Insert Camelot's path into the system. # Insert Camelot's path into the system.
sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath(".."))
sys.path.insert(0, os.path.abspath('_themes')) sys.path.insert(0, os.path.abspath("_themes"))
import camelot import camelot
@@ -38,33 +38,33 @@ import camelot
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', "sphinx.ext.autodoc",
'sphinx.ext.napoleon', "sphinx.ext.napoleon",
'sphinx.ext.intersphinx', "sphinx.ext.intersphinx",
'sphinx.ext.todo', "sphinx.ext.todo",
'sphinx.ext.viewcode', "sphinx.ext.viewcode",
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix(es) of source filenames. # The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string: # You can specify multiple suffix as a list of string:
# #
# source_suffix = ['.rst', '.md'] # source_suffix = ['.rst', '.md']
source_suffix = '.rst' source_suffix = ".rst"
# The encoding of source files. # The encoding of source files.
# #
# source_encoding = 'utf-8-sig' # source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = u'Camelot' project = u"Camelot"
copyright = u'2019, Camelot Developers' copyright = u"2021, Camelot Developers"
author = u'Vinayak Mehta' author = u"Vinayak Mehta"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@@ -94,7 +94,7 @@ language = None
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path # This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build'] exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all # The reST default role (used for this markup: `text`) to use for all
# documents. # documents.
@@ -114,7 +114,7 @@ add_module_names = True
# show_authors = False # show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'flask_theme_support.FlaskyStyle' pygments_style = "flask_theme_support.FlaskyStyle"
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
# modindex_common_prefix = [] # modindex_common_prefix = []
@@ -130,18 +130,18 @@ todo_include_todos = True
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
html_theme = 'alabaster' html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
# documentation. # documentation.
html_theme_options = { html_theme_options = {
'show_powered_by': False, "show_powered_by": False,
'github_user': 'camelot-dev', "github_user": "camelot-dev",
'github_repo': 'camelot', "github_repo": "camelot",
'github_banner': True, "github_banner": True,
'show_related': False, "show_related": False,
'note_bg': '#FFF59C' "note_bg": "#FFF59C",
} }
# Add any paths that contain custom themes here, relative to this directory. # Add any paths that contain custom themes here, relative to this directory.
@@ -164,12 +164,12 @@ html_theme_options = {
# The name of an image file (relative to this directory) to use as a favicon of # The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large. # pixels large.
html_favicon = '_static/favicon.ico' html_favicon = "_static/favicon.ico"
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or # Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied # .htaccess) here, relative to this directory. These files are copied
@@ -189,10 +189,21 @@ html_use_smartypants = True
# Custom sidebar templates, maps document names to template names. # Custom sidebar templates, maps document names to template names.
html_sidebars = { html_sidebars = {
'index': ['sidebarintro.html', 'relations.html', 'sourcelink.html', "index": [
'searchbox.html', 'hacks.html'], "sidebarintro.html",
'**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', "relations.html",
'sourcelink.html', 'searchbox.html', 'hacks.html'] "sourcelink.html",
"searchbox.html",
"hacks.html",
],
"**": [
"sidebarlogo.html",
"localtoc.html",
"relations.html",
"sourcelink.html",
"searchbox.html",
"hacks.html",
],
} }
# Additional templates that should be rendered to pages, maps page names to # Additional templates that should be rendered to pages, maps page names to
@@ -249,34 +260,30 @@ html_show_copyright = True
# html_search_scorer = 'scorer.js' # html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'Camelotdoc' htmlhelp_basename = "Camelotdoc"
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# #
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# The font size ('10pt', '11pt' or '12pt'). #
# # 'pointsize': '10pt',
# 'pointsize': '10pt', # Additional stuff for the LaTeX preamble.
#
# Additional stuff for the LaTeX preamble. # 'preamble': '',
# # Latex figure (float) alignment
# 'preamble': '', #
# 'figure_align': 'htbp',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, 'Camelot.tex', u'Camelot Documentation', (master_doc, "Camelot.tex", u"Camelot Documentation", u"Vinayak Mehta", "manual"),
u'Vinayak Mehta', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@@ -316,10 +323,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, "Camelot", u"Camelot Documentation", [author], 1)]
(master_doc, 'Camelot', u'Camelot Documentation',
[author], 1)
]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
# #
@@ -332,9 +336,15 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'Camelot', u'Camelot Documentation', (
author, 'Camelot', 'One line description of project.', master_doc,
'Miscellaneous'), "Camelot",
u"Camelot Documentation",
author,
"Camelot",
"One line description of project.",
"Miscellaneous",
),
] ]
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
@@ -356,6 +366,6 @@ texinfo_documents = [
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = { intersphinx_mapping = {
'https://docs.python.org/2': None, "https://docs.python.org/2": None,
'http://pandas.pydata.org/pandas-docs/stable': None "http://pandas.pydata.org/pandas-docs/stable": None,
} }
+1 -1
View File
@@ -37,7 +37,7 @@ Setting up a development environment
To install the dependencies needed for development, you can use pip:: To install the dependencies needed for development, you can use pip::
$ pip install camelot-py[dev] $ pip install "camelot-py[dev]"
Alternatively, you can clone the project repository, and install using pip:: Alternatively, you can clone the project repository, and install using pip::
+21 -20
View File
@@ -33,15 +33,18 @@ Release v\ |version|. (:ref:`Installation <install>`)
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black :target: https://github.com/ambv/black
**Camelot** is a Python library that makes it easy for *anyone* to extract tables from PDF files! .. image:: https://img.shields.io/badge/continous%20quality-deepsource-lightgrey
:target: https://deepsource.io/gh/camelot-dev/camelot/?ref=repository-badge
.. note:: You can also check out `Excalibur`_, which is a web interface for Camelot! **Camelot** is a Python library that can help you extract tables from PDFs!
.. note:: You can also check out `Excalibur`_, the web interface to Camelot!
.. _Excalibur: https://github.com/camelot-dev/excalibur .. _Excalibur: https://github.com/camelot-dev/excalibur
---- ----
**Here's how you can extract tables from PDF files.** Check out the PDF used in this example `here`_. **Here's how you can extract tables from PDFs.** You can check out the PDF used in this example `here`_.
.. _here: _static/pdf/foo.pdf .. _here: _static/pdf/foo.pdf
@@ -67,7 +70,7 @@ Release v\ |version|. (:ref:`Installation <install>`)
.. csv-table:: .. csv-table::
:file: _static/csv/foo.csv :file: _static/csv/foo.csv
There's a :ref:`command-line interface <cli>` too! Camelot also comes packaged with a :ref:`command-line interface <cli>`!
.. note:: Camelot only works with text-based PDFs and not scanned documents. (As Tabula `explains`_, "If you can click and drag to select text in your table in a PDF viewer, then your PDF is text-based".) .. note:: Camelot only works with text-based PDFs and not scanned documents. (As Tabula `explains`_, "If you can click and drag to select text in your table in a PDF viewer, then your PDF is text-based".)
@@ -76,27 +79,27 @@ There's a :ref:`command-line interface <cli>` too!
Why Camelot? Why Camelot?
------------ ------------
- **You are in control.** Unlike other libraries and tools which either give a nice output or fail miserably (with no in-between), Camelot gives you the power to tweak table extraction. (This is important since everything in the real world, including PDF table extraction, is fuzzy.) - **Configurability**: Camelot gives you control over the table extraction process with its :ref:`tweakable settings <advanced>`.
- *Bad* tables can be discarded based on **metrics** like accuracy and whitespace, without ever having to manually look at each table. - **Metrics**: Bad tables can be discarded based on metrics like accuracy and whitespace, without having to manually look at each table.
- Each table is a **pandas DataFrame**, which seamlessly integrates into `ETL and data analysis workflows`_. - **Output**: Each table is extracted into a **pandas DataFrame**, which seamlessly integrates into `ETL and data analysis workflows`_. You can also export tables to multiple formats, which include CSV, JSON, Excel, HTML and Sqlite.
- **Export** to multiple formats, including JSON, Excel and HTML.
See `comparison with other PDF table extraction libraries and tools`_.
.. _ETL and data analysis workflows: https://gist.github.com/vinayak-mehta/e5949f7c2410a0e12f25d3682dc9e873 .. _ETL and data analysis workflows: https://gist.github.com/vinayak-mehta/e5949f7c2410a0e12f25d3682dc9e873
.. _comparison with other PDF table extraction libraries and tools: https://github.com/camelot-dev/camelot/wiki/Comparison-with-other-PDF-Table-Extraction-libraries-and-tools
Support us on OpenCollective See `comparison with similar libraries and tools`_.
----------------------------
If Camelot helped you extract tables from PDFs, please consider supporting its development by `becoming a backer or a sponsor on OpenCollective`_! .. _comparison with similar libraries and tools: https://github.com/camelot-dev/camelot/wiki/Comparison-with-other-PDF-Table-Extraction-libraries-and-tools
.. _becoming a backer or a sponsor on OpenCollective: https://opencollective.com/camelot Support the development
-----------------------
If Camelot has helped you, please consider supporting its development with a one-time or monthly donation `on OpenCollective`_!
.. _on OpenCollective: https://opencollective.com/camelot
The User Guide The User Guide
-------------- --------------
This part of the documentation begins with some background information about why Camelot was created, takes a small dip into the implementation details and then focuses on step-by-step instructions for getting the most out of Camelot. This part of the documentation begins with some background information about why Camelot was created, takes you through some implementation details, and then focuses on step-by-step instructions for getting the most out of Camelot.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
@@ -112,8 +115,7 @@ This part of the documentation begins with some background information about why
The API Documentation/Guide The API Documentation/Guide
--------------------------- ---------------------------
If you are looking for information on a specific function, class, or method, If you are looking for information on a specific function, class, or method, this part of the documentation is for you.
this part of the documentation is for you.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
@@ -123,8 +125,7 @@ this part of the documentation is for you.
The Contributor Guide The Contributor Guide
--------------------- ---------------------
If you want to contribute to the project, this part of the documentation is for If you want to contribute to the project, this part of the documentation is for you.
you.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
+11 -21
View File
@@ -66,8 +66,7 @@ Let's plot all the text present on the table's PDF page.
:: ::
>>> camelot.plot(tables[0], kind='text') >>> camelot.plot(tables[0], kind='text').show()
>>> plt.show()
.. tip:: .. tip::
Here's how you can do the same with the :ref:`command-line interface <cli>`. Here's how you can do the same with the :ref:`command-line interface <cli>`.
@@ -93,8 +92,7 @@ Let's plot the table (to see if it was detected correctly or not). This plot typ
:: ::
>>> camelot.plot(tables[0], kind='grid') >>> camelot.plot(tables[0], kind='grid').show()
>>> plt.show()
.. tip:: .. tip::
Here's how you can do the same with the :ref:`command-line interface <cli>`. Here's how you can do the same with the :ref:`command-line interface <cli>`.
@@ -118,8 +116,7 @@ Now, let's plot all table boundaries present on the table's PDF page.
:: ::
>>> camelot.plot(tables[0], kind='contour') >>> camelot.plot(tables[0], kind='contour').show()
>>> plt.show()
.. tip:: .. tip::
Here's how you can do the same with the :ref:`command-line interface <cli>`. Here's how you can do the same with the :ref:`command-line interface <cli>`.
@@ -141,8 +138,7 @@ Cool, let's plot all line segments present on the table's PDF page.
:: ::
>>> camelot.plot(tables[0], kind='line') >>> camelot.plot(tables[0], kind='line').show()
>>> plt.show()
.. tip:: .. tip::
Here's how you can do the same with the :ref:`command-line interface <cli>`. Here's how you can do the same with the :ref:`command-line interface <cli>`.
@@ -164,8 +160,7 @@ Finally, let's plot all line intersections present on the table's PDF page.
:: ::
>>> camelot.plot(tables[0], kind='joint') >>> camelot.plot(tables[0], kind='joint').show()
>>> plt.show()
.. tip:: .. tip::
Here's how you can do the same with the :ref:`command-line interface <cli>`. Here's how you can do the same with the :ref:`command-line interface <cli>`.
@@ -187,8 +182,7 @@ You can also visualize the textedges found on a page by specifying ``kind='texte
:: ::
>>> camelot.plot(tables[0], kind='textedge') >>> camelot.plot(tables[0], kind='textedge').show()
>>> plt.show()
.. tip:: .. tip::
Here's how you can do the same with the :ref:`command-line interface <cli>`. Here's how you can do the same with the :ref:`command-line interface <cli>`.
@@ -375,8 +369,7 @@ Let's see the table area that is detected by default.
:: ::
>>> tables = camelot.read_pdf('edge_tol.pdf', flavor='stream') >>> tables = camelot.read_pdf('edge_tol.pdf', flavor='stream')
>>> camelot.plot(tables[0], kind='contour') >>> camelot.plot(tables[0], kind='contour').show()
>>> plt.show()
.. tip:: .. tip::
Here's how you can do the same with the :ref:`command-line interface <cli>`. Here's how you can do the same with the :ref:`command-line interface <cli>`.
@@ -396,8 +389,7 @@ To improve the detected area, you can increase the ``edge_tol`` (default: 50) va
:: ::
>>> tables = camelot.read_pdf('edge_tol.pdf', flavor='stream', edge_tol=500) >>> tables = camelot.read_pdf('edge_tol.pdf', flavor='stream', edge_tol=500)
>>> camelot.plot(tables[0], kind='contour') >>> camelot.plot(tables[0], kind='contour').show()
>>> plt.show()
.. tip:: .. tip::
Here's how you can do the same with the :ref:`command-line interface <cli>`. Here's how you can do the same with the :ref:`command-line interface <cli>`.
@@ -472,8 +464,7 @@ Let's plot the table for this PDF.
:: ::
>>> tables = camelot.read_pdf('short_lines.pdf') >>> tables = camelot.read_pdf('short_lines.pdf')
>>> camelot.plot(tables[0], kind='grid') >>> camelot.plot(tables[0], kind='grid').show()
>>> plt.show()
.. figure:: ../_static/png/short_lines_1.png .. figure:: ../_static/png/short_lines_1.png
:alt: A plot of the PDF table with short lines :alt: A plot of the PDF table with short lines
@@ -484,8 +475,7 @@ Clearly, the smaller lines separating the headers, couldn't be detected. Let's t
:: ::
>>> tables = camelot.read_pdf('short_lines.pdf', line_scale=40) >>> tables = camelot.read_pdf('short_lines.pdf', line_scale=40)
>>> camelot.plot(tables[0], kind='grid') >>> camelot.plot(tables[0], kind='grid').show()
>>> plt.show()
.. tip:: .. tip::
Here's how you can do the same with the :ref:`command-line interface <cli>`. Here's how you can do the same with the :ref:`command-line interface <cli>`.
@@ -628,7 +618,7 @@ Tweak layout generation
Camelot is built on top of PDFMiner's functionality of grouping characters on a page into words and sentences. In some cases (such as `#170 <https://github.com/camelot-dev/camelot/issues/170>`_ and `#215 <https://github.com/camelot-dev/camelot/issues/215>`_), PDFMiner can group characters that should belong to the same sentence into separate sentences. Camelot is built on top of PDFMiner's functionality of grouping characters on a page into words and sentences. In some cases (such as `#170 <https://github.com/camelot-dev/camelot/issues/170>`_ and `#215 <https://github.com/camelot-dev/camelot/issues/215>`_), PDFMiner can group characters that should belong to the same sentence into separate sentences.
To deal with such cases, you can tweak PDFMiner's `LAParams kwargs <https://github.com/euske/pdfminer/blob/master/pdfminer/layout.py#L33>`_ to improve layout generation, by passing the keyword arguments as a dict using ``layout_kwargs`` in :meth:`read_pdf() <camelot.read_pdf>`. To know more about the parameters you can tweak, you can check out `PDFMiner docs <https://euske.github.io/pdfminer/>`_. To deal with such cases, you can tweak PDFMiner's `LAParams kwargs <https://github.com/euske/pdfminer/blob/master/pdfminer/layout.py#L33>`_ to improve layout generation, by passing the keyword arguments as a dict using ``layout_kwargs`` in :meth:`read_pdf() <camelot.read_pdf>`. To know more about the parameters you can tweak, you can check out `PDFMiner docs <https://pdfminersix.rtfd.io/en/latest/reference/composable.html>`_.
:: ::
+2 -2
View File
@@ -16,11 +16,11 @@ Stream can be used to parse tables that have whitespaces between cells to simula
1. Words on the PDF page are grouped into text rows based on their *y* axis overlaps. 1. Words on the PDF page are grouped into text rows based on their *y* axis overlaps.
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 <http://dspace.cc.tut.fi/dpub/bitstream/handle/123456789/21520/Nurminen.pdf?sequence=3>`_ to know more about this table detection technique. [See pages 20, 35 and 40] 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 <https://pdfs.semanticscholar.org/a9b1/67a86fb189bfcd366c3839f33f0404db9c10.pdf>`_ to know more about this table detection technique. [See pages 20, 35 and 40]
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. 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.
4. Words that lie inside/outside the current column *x* ranges are then used to extend extend the current list of columns. 4. Words that lie inside/outside the current column *x* ranges are then used to 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. 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.
+35 -48
View File
@@ -3,72 +3,59 @@
Installation of dependencies 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. The dependencies `Ghostscript <https://www.ghostscript.com>`_ and `Tkinter <https://wiki.python.org/moin/TkInter>`_ can be installed using your system's package manager or by running their installer.
.. _Tkinter: https://wiki.python.org/moin/TkInter
.. _ghostscript: https://www.ghostscript.com
OS-specific instructions OS-specific instructions
------------------------ ------------------------
For Ubuntu Ubuntu
^^^^^^^^^^ ^^^^^^
:: ::
$ apt install python-tk ghostscript $ apt install ghostscript python3-tk
Or for Python 3:: MacOS
^^^^^
$ apt install python3-tk ghostscript
For macOS
^^^^^^^^^
:: ::
$ brew install tcl-tk ghostscript $ brew install ghostscript tcl-tk
For Windows Windows
^^^^^^^^^^^ ^^^^^^^
For Tkinter, you can download the `ActiveTcl Community Edition`_ from ActiveState. For ghostscript, you can get the installer at the `ghostscript downloads page`_. For Ghostscript, you can get the installer at their `downloads page <https://www.ghostscript.com/download/gsdnld.html>`_. And for Tkinter, you can download the `ActiveTcl Community Edition <https://www.activestate.com/activetcl/downloads>`_ from ActiveState.
.. _ActiveTcl Community Edition: https://www.activestate.com/activetcl/downloads Checks to see if dependencies are installed correctly
.. _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 run the following checks to see if the dependencies were installed correctly.
------------------------------------------------------
You can do the following checks to see if the dependencies were installed correctly. For Ghostscript
^^^^^^^^^^^^^^^
Open the Python REPL and run the following:
For Ubuntu/MacOS::
>>> from ctypes.util import find_library
>>> find_library("gs")
"libgs.so.9"
For Windows::
>>> from ctypes.util import find_library
>>> find_library("".join(("gsdll", str(ctypes.sizeof(ctypes.c_voidp) * 8), ".dll"))
<name-of-ghostscript-library-on-windows>
**Check:** The output of the ``find_library`` function should not be empty.
If the output is empty, then it's possible that the Ghostscript library is not available one of the ``LD_LIBRARY_PATH``/``DYLD_LIBRARY_PATH``/``PATH`` variables depending on your operating system. In this case, you may have to modify one of those path variables.
For Tkinter For Tkinter
^^^^^^^^^^^ ^^^^^^^^^^^
Launch Python, and then at the prompt, type:: Launch Python and then import Tkinter::
>>> import Tkinter
Or in Python 3::
>>> import tkinter >>> import tkinter
If you have Tkinter, Python will not print an error message, and if not, you will see an ``ImportError``. **Check:** Importing ``tkinter`` should not raise an import error.
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.
+16 -23
View File
@@ -5,42 +5,35 @@ Installation of Camelot
This part of the documentation covers the steps to install Camelot. This part of the documentation covers the steps to install Camelot.
Using conda After :ref:`installing the dependencies <install_deps>`, which include `Ghostscript <https://www.ghostscript.com>`_ and `Tkinter <https://wiki.python.org/moin/TkInter>`_, you can use one of the following methods to install Camelot:
-----------
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. .. warning:: The ``lattice`` flavor will fail to run if Ghostscript is not installed. You may run into errors as shown in `issue #193 <https://github.com/camelot-dev/camelot/issues/193>`_.
::
pip
---
To install Camelot from PyPI using ``pip``, please include the extra ``cv`` requirement as shown::
$ pip install "camelot-py[cv]"
conda
-----
`conda`_ is a package manager and environment management system for the `Anaconda <https://anaconda.org>`_ distribution. It can be used to install Camelot from the ``conda-forge`` channel::
$ conda install -c conda-forge camelot-py $ conda install -c conda-forge camelot-py
.. note:: Camelot is available for Python 2.7, 3.5, 3.6 and 3.7 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
.. _conda-forge: https://conda-forge.org/
Using pip
---------
After :ref:`installing the dependencies <install_deps>`, 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
From the source code From the source code
-------------------- --------------------
After :ref:`installing the dependencies <install_deps>`, you can install from the source by: After :ref:`installing the dependencies <install_deps>`, you can install Camelot from source by:
1. Cloning the GitHub repository. 1. Cloning the GitHub repository.
:: ::
$ git clone https://www.github.com/camelot-dev/camelot $ git clone https://www.github.com/camelot-dev/camelot
2. Then simply using pip again. 2. And then simply using pip again.
:: ::
$ cd camelot $ cd camelot
-8
View File
@@ -1,8 +0,0 @@
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
PyPDF2>=1.26.0
+51 -51
View File
@@ -6,38 +6,36 @@ from setuptools import find_packages
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
about = {} about = {}
with open(os.path.join(here, 'camelot', '__version__.py'), 'r') as f: with open(os.path.join(here, "camelot", "__version__.py"), "r") as f:
exec(f.read(), about) exec(f.read(), about)
with open('README.md', 'r') as f: with open("README.md", "r") as f:
readme = f.read() readme = f.read()
requires = [ requires = [
'chardet>=3.0.4', "chardet>=3.0.4",
'click>=6.7', "click>=6.7",
'numpy>=1.13.3', "numpy>=1.13.3",
'openpyxl>=2.5.8', "openpyxl>=2.5.8",
'pandas>=0.23.4', "pandas>=0.23.4",
'pdfminer.six>=20170720', "pdfminer.six>=20200726",
'PyPDF2>=1.26.0' "PyPDF2>=1.26.0",
] ]
cv_requires = [ cv_requires = ["opencv-python>=3.4.2.17"]
'opencv-python>=3.4.2.17'
]
plot_requires = [ plot_requires = [
'matplotlib>=2.2.3', "matplotlib>=2.2.3",
] ]
dev_requires = [ dev_requires = [
'codecov>=2.0.15', "codecov>=2.0.15",
'pytest>=3.8.0', "pytest>=5.4.3",
'pytest-cov>=2.6.0', "pytest-cov>=2.10.0",
'pytest-mpl>=0.10', "pytest-mpl>=0.11",
'pytest-runner>=4.2', "pytest-runner>=5.2",
'Sphinx>=1.7.9' "Sphinx>=3.1.2",
] ]
all_requires = cv_requires + plot_requires all_requires = cv_requires + plot_requires
@@ -45,37 +43,39 @@ dev_requires = dev_requires + all_requires
def setup_package(): def setup_package():
metadata = dict(name=about['__title__'], metadata = dict(
version=about['__version__'], name=about["__title__"],
description=about['__description__'], version=about["__version__"],
long_description=readme, description=about["__description__"],
long_description_content_type="text/markdown", long_description=readme,
url=about['__url__'], long_description_content_type="text/markdown",
author=about['__author__'], url=about["__url__"],
author_email=about['__author_email__'], author=about["__author__"],
license=about['__license__'], author_email=about["__author_email__"],
packages=find_packages(exclude=('tests',)), license=about["__license__"],
install_requires=requires, packages=find_packages(exclude=("tests",)),
extras_require={ install_requires=requires,
'all': all_requires, extras_require={
'cv': cv_requires, "all": all_requires,
'dev': dev_requires, "cv": cv_requires,
'plot': plot_requires "dev": dev_requires,
}, "plot": plot_requires,
entry_points={ },
'console_scripts': [ entry_points={
'camelot = camelot.cli:cli', "console_scripts": [
], "camelot = camelot.cli:cli",
}, ],
classifiers=[ },
# Trove classifiers classifiers=[
# Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers # Trove classifiers
'License :: OSI Approved :: MIT License', # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
'Programming Language :: Python :: 2.7', "License :: OSI Approved :: MIT License",
'Programming Language :: Python :: 3.5', "Programming Language :: Python :: 3.6",
'Programming Language :: Python :: 3.6', "Programming Language :: Python :: 3.7",
'Programming Language :: Python :: 3.7' "Programming Language :: Python :: 3.8",
]) "Programming Language :: Python :: 3.9",
],
)
try: try:
from setuptools import setup from setuptools import setup
@@ -85,5 +85,5 @@ def setup_package():
setup(**metadata) setup(**metadata)
if __name__ == '__main__': if __name__ == "__main__":
setup_package() setup_package()
+2 -1
View File
@@ -1,2 +1,3 @@
import matplotlib import matplotlib
matplotlib.use('agg')
matplotlib.use("agg")
+473 -67
View File
@@ -1,19 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
data_stream = [ data_stream = [
[
"",
"Table: 5 Public Health Outlay 2012-13 (Budget Estimates) (Rs. in 000)",
"",
"",
"",
"",
"",
"",
],
["States-A", "Revenue", "", "Capital", "", "Total", "Others(1)", "Total"], ["States-A", "Revenue", "", "Capital", "", "Total", "Others(1)", "Total"],
["", "", "", "", "", "Revenue &", "", ""], ["", "", "", "", "", "Revenue &", "", ""],
["", "Medical &", "Family", "Medical &", "Family", "", "", ""], ["", "Medical &", "Family", "Medical &", "Family", "", "", ""],
@@ -829,18 +817,6 @@ data_stream_table_rotated = [
] ]
data_stream_two_tables_1 = [ 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", "Program. Represents arrests reported (not charged) by 12,910 agencies with a total population of 247,526,916 as estimated",
"", "",
@@ -915,7 +891,7 @@ data_stream_two_tables_1 = [
"2,330 .9", "2,330 .9",
], ],
[ [
"Violent crime . . . . . . . .\n . .\n . .\n . .\n . .\n . .", "Violent crime . . . . . . . .\n . .\n . .\n . .\n . .\n . .",
"467 .9", "467 .9",
"69 .1", "69 .1",
"398 .8", "398 .8",
@@ -1300,29 +1276,10 @@ data_stream_two_tables_1 = [
"", "",
"", "",
], ],
[
"",
"Source: U.S. Department of Justice, Federal Bureau of Investigation, Uniform Crime Reports, Arrests Master Files.",
"",
"",
"",
"",
"",
"",
"",
"",
],
] ]
data_stream_two_tables_2 = [ 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", "", "", "", "", ""], ["Table 325. Arrests by Race: 2009", "", "", "", "", ""],
[ [
"[Based on Uniform Crime Reporting (UCR) Program. Represents arrests reported (not charged) by 12,371 agencies", "[Based on Uniform Crime Reporting (UCR) Program. Represents arrests reported (not charged) by 12,371 agencies",
@@ -1352,7 +1309,7 @@ data_stream_two_tables_2 = [
"123,656", "123,656",
], ],
[ [
"Violent crime . . . . . . . .\n . .\n . .\n . .\n . .\n .\n .\n . .\n . .\n .\n .\n .\n .\n . .", "Violent crime . . . . . . . .\n . .\n . .\n . .\n . .\n .\n .\n . .\n . .\n .\n .\n .\n .\n . .",
"456,965", "456,965",
"268,346", "268,346",
"177,766", "177,766",
@@ -1600,16 +1557,9 @@ data_stream_two_tables_2 = [
"3,950", "3,950",
], ],
["1 Except forcible rape and prostitution.", "", "", "", "", ""], ["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 = [ data_stream_table_areas = [
["", "One Withholding"], ["", "One Withholding"],
["Payroll Period", "Allowance"], ["Payroll Period", "Allowance"],
@@ -1776,18 +1726,7 @@ data_stream_columns = [
] ]
data_stream_split_text = [ data_stream_split_text = [
[ ["FEB", "RUAR", "Y 2014 M27 (BUS)", "", "", "", "", "", "", ""],
"FEB",
"RUAR",
"Y 2014 M27 (BUS)",
"",
"ALPHABETIC LISTING BY T",
"YPE",
"",
"",
"",
"ABLPDM27",
],
["", "", "", "", "OF ACTIVE LICENSES", "", "", "", "", "3/19/2014"], ["", "", "", "", "OF ACTIVE LICENSES", "", "", "", "", "3/19/2014"],
["", "", "", "", "OKLAHOMA ABLE COMMIS", "SION", "", "", "", ""], ["", "", "", "", "OKLAHOMA ABLE COMMIS", "SION", "", "", "", ""],
["LICENSE", "", "", "", "PREMISE", "", "", "", "", ""], ["LICENSE", "", "", "", "PREMISE", "", "", "", "", ""],
@@ -2121,6 +2060,7 @@ data_stream_split_text = [
], ],
] ]
data_stream_flag_size = [ data_stream_flag_size = [
[ [
"States", "States",
@@ -2820,7 +2760,7 @@ data_arabic = [
] ]
data_stream_layout_kwargs = [ data_stream_layout_kwargs = [
["V i n s a u Ve r r e", ""], ["V i n s a u V e r r e", ""],
["Les Blancs", "12.5CL"], ["Les Blancs", "12.5CL"],
["A.O.P Côtes du Rhône", ""], ["A.O.P Côtes du Rhône", ""],
["Domaine de la Guicharde « Autour de la chapelle » 2016", "8 €"], ["Domaine de la Guicharde « Autour de la chapelle » 2016", "8 €"],
@@ -2858,3 +2798,469 @@ data_stream_layout_kwargs = [
["A.O.P Cornas", ""], ["A.O.P Cornas", ""],
["Domaine Lionnet « Terre Brûlée » 2012", "15 €"], ["Domaine Lionnet « Terre Brûlée » 2012", "15 €"],
] ]
data_stream_duplicated_text = [
[
"",
"2012 BETTER VARIETIES Harvest Report for Minnesota Central [ MNCE ]",
"",
"",
"",
"",
"",
"",
"",
"",
"ALL SEASON TEST",
],
[
"",
"Doug Toreen, Renville County, MN 55310 [ BIRD ISLAND ]",
"",
"",
"",
"",
"",
"",
"",
"",
"1.3 - 2.0 MAT. GROUP",
],
[
"PREV. CROP/HERB:",
"Corn / Surpass, Roundup",
"",
"",
"",
"",
"",
"",
"",
"",
"S2MNCE01",
],
[
"SOIL DESCRIPTION:",
"",
"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:",
"conventional w/ fall till",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
["PEST MANAGEMENT:", "Roundup twice", "", "", "", "", "", "", "", "", ""],
[
"SEEDED - RATE:",
"May 15",
"140 000 /A",
"",
"",
"",
"",
"",
"",
"TOP 30 for YIELD of 63 TESTED",
"",
],
[
"HARVESTED - STAND:",
"Oct 3",
"122 921 /A",
"",
"",
"",
"",
"",
"",
"AVERAGE of (3) REPLICATIONS",
"",
],
["", "", "", "", "SCN", "Seed", "Yield", "Moisture", "Lodging", "Stand", "Gross"],
[
"Company/Brand",
"Product/Brand†",
"Technol.†",
"Mat.",
"Resist.",
"Trmt.†",
"Bu/A",
"%",
"%",
"(x 1000)",
"Income",
],
[
"Kruger",
"K2 1901",
"RR2Y",
"1.9",
"R",
"Ac,PV",
"56.4",
"7.6",
"0",
"126.3",
"$846",
],
[
"Stine",
"19RA02 §",
"RR2Y",
"1.9",
"R",
"CMB",
"55.3",
"7.6",
"0",
"120.0",
"$830",
],
[
"Wensman",
"W 3190NR2",
"RR2Y",
"1.9",
"R",
"Ac",
"54.5",
"7.6",
"0",
"119.5",
"$818",
],
["Hefty", "H17Y12", "RR2Y", "1.7", "MR", "I", "53.7", "7.7", "0", "124.4", "$806"],
[
"Dyna-Gro",
"S15RY53",
"RR2Y",
"1.5",
"R",
"Ac",
"53.6",
"7.7",
"0",
"126.8",
"$804",
],
[
"LG Seeds",
"C2050R2",
"RR2Y",
"2.1",
"R",
"Ac",
"53.6",
"7.7",
"0",
"123.9",
"$804",
],
[
"Titan Pro",
"19M42",
"RR2Y",
"1.9",
"R",
"CMB",
"53.6",
"7.7",
"0",
"121.0",
"$804",
],
[
"Stine",
"19RA02 (2) §",
"RR2Y",
"1.9",
"R",
"CMB",
"53.4",
"7.7",
"0",
"123.9",
"$801",
],
[
"Asgrow",
"AG1832 §",
"RR2Y",
"1.8",
"MR",
"Ac,PV",
"52.9",
"7.7",
"0",
"122.0",
"$794",
],
[
"Prairie Brand",
"PB-1566R2",
"RR2Y",
"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 Pro",
"20M1",
"RR2Y",
"2.0",
"R",
"Am",
"52.5",
"7.5",
"0",
"124.4",
"$788",
],
[
"Kruger",
"K2-2002",
"RR2Y",
"2.0",
"R",
"Ac,PV",
"52.4",
"7.9",
"0",
"125.4",
"$786",
],
[
"Channel",
"1700R2",
"RR2Y",
"1.7",
"R",
"Ac,PV",
"52.3",
"7.9",
"0",
"123.9",
"$784",
],
["Hefty", "H16Y11", "RR2Y", "1.6", "MR", "I", "51.4", "7.6", "0", "123.9", "$771"],
[
"Anderson",
"162R2Y",
"RR2Y",
"1.6",
"R",
"None",
"51.3",
"7.5",
"0",
"119.5",
"$770",
],
[
"Titan Pro",
"15M22",
"RR2Y",
"1.5",
"R",
"CMB",
"51.3",
"7.8",
"0",
"125.4",
"$769",
],
[
"Dairyland",
"DSR-1710R2Y",
"RR2Y",
"1.7",
"R",
"CMB",
"51.3",
"7.7",
"0",
"122.0",
"$769",
],
["Hefty", "H20R3", "RR2Y", "2.0", "MR", "I", "50.5", "8.2", "0", "121.0", "$757"],
[
"Prairie Brand",
"PB 1743R2",
"RR2Y",
"1.7",
"R",
"CMB",
"50.2",
"7.7",
"0",
"125.8",
"$752",
],
[
"Gold Country",
"1741",
"RR2Y",
"1.7",
"R",
"Ac",
"50.1",
"7.8",
"0",
"123.9",
"$751",
],
[
"Trelay",
"20RR43",
"RR2Y",
"2.0",
"R",
"Ac,Ex",
"49.9",
"7.6",
"0",
"127.8",
"$749",
],
["Hefty", "H14R3", "RR2Y", "1.4", "MR", "I", "49.7", "7.7", "0", "122.9", "$746"],
[
"Prairie Brand",
"PB-2099NRR2",
"RR2Y",
"2.0",
"R",
"CMB",
"49.6",
"7.8",
"0",
"126.3",
"$743",
],
[
"Wensman",
"W 3174NR2",
"RR2Y",
"1.7",
"R",
"Ac",
"49.3",
"7.6",
"0",
"122.5",
"$740",
],
[
"Kruger",
"K2 1602",
"RR2Y",
"1.6",
"R",
"Ac,PV",
"48.7",
"7.6",
"0",
"125.4",
"$731",
],
[
"NK Brand",
"S18-C2 §",
"RR2Y",
"1.8",
"R",
"CMB",
"48.7",
"7.7",
"0",
"126.8",
"$731",
],
[
"Kruger",
"K2 1902",
"RR2Y",
"1.9",
"R",
"Ac,PV",
"48.7",
"7.5",
"0",
"124.4",
"$730",
],
[
"Prairie Brand",
"PB-1823R2",
"RR2Y",
"1.8",
"R",
"None",
"48.5",
"7.6",
"0",
"121.0",
"$727",
],
[
"Gold Country",
"1541",
"RR2Y",
"1.5",
"R",
"Ac",
"48.4",
"7.6",
"0",
"110.4",
"$726",
],
["", "", "", "", "", "Test Average =", "47.6", "7.7", "0", "122.9", "$713"],
["", "", "", "", "", "LSD (0.10) =", "5.7", "0.3", "ns", "37.8", "566.4"],
]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11 -7
View File
@@ -114,31 +114,35 @@ def test_cli_password():
def test_cli_output_format(): def test_cli_output_format():
with TemporaryDirectory() as tempdir: with TemporaryDirectory() as tempdir:
infile = os.path.join(testdir, "health.pdf") infile = os.path.join(testdir, "health.pdf")
outfile = os.path.join(tempdir, "health.{}")
runner = CliRunner() runner = CliRunner()
# json # json
outfile = os.path.join(tempdir, "health.json")
result = runner.invoke( result = runner.invoke(
cli, cli,
["--format", "json", "--output", outfile.format("json"), "stream", infile], ["--format", "json", "--output", outfile, "stream", infile],
) )
assert result.exit_code == 0 assert result.exit_code == 0
# excel # excel
outfile = os.path.join(tempdir, "health.xlsx")
result = runner.invoke( result = runner.invoke(
cli, cli,
["--format", "excel", "--output", outfile.format("xlsx"), "stream", infile], ["--format", "excel", "--output", outfile, "stream", infile],
) )
assert result.exit_code == 0 assert result.exit_code == 0
# html # html
outfile = os.path.join(tempdir, "health.html")
result = runner.invoke( result = runner.invoke(
cli, cli,
["--format", "html", "--output", outfile.format("html"), "stream", infile], ["--format", "html", "--output", outfile, "stream", infile],
) )
assert result.exit_code == 0 assert result.exit_code == 0
# zip # zip
outfile = os.path.join(tempdir, "health.csv")
result = runner.invoke( result = runner.invoke(
cli, cli,
[ [
@@ -146,7 +150,7 @@ def test_cli_output_format():
"--format", "--format",
"csv", "csv",
"--output", "--output",
outfile.format("csv"), outfile,
"stream", "stream",
infile, infile,
], ],
@@ -156,8 +160,8 @@ def test_cli_output_format():
def test_cli_quiet(): def test_cli_quiet():
with TemporaryDirectory() as tempdir: with TemporaryDirectory() as tempdir:
infile = os.path.join(testdir, "blank.pdf") infile = os.path.join(testdir, "empty.pdf")
outfile = os.path.join(tempdir, "blank.csv") outfile = os.path.join(tempdir, "empty.csv")
runner = CliRunner() runner = CliRunner()
result = runner.invoke( result = runner.invoke(
+46 -21
View File
@@ -3,9 +3,11 @@
import os import os
import pandas as pd import pandas as pd
from pandas.testing import assert_frame_equal
import camelot import camelot
from camelot.core import Table, TableList from camelot.core import Table, TableList
from camelot.__version__ import generate_version
from .data import * from .data import *
@@ -26,10 +28,10 @@ def test_password():
filename = os.path.join(testdir, "health_protected.pdf") filename = os.path.join(testdir, "health_protected.pdf")
tables = camelot.read_pdf(filename, password="ownerpass", flavor="stream") tables = camelot.read_pdf(filename, password="ownerpass", flavor="stream")
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
tables = camelot.read_pdf(filename, password="userpass", flavor="stream") tables = camelot.read_pdf(filename, password="userpass", flavor="stream")
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_stream(): def test_stream():
@@ -37,7 +39,7 @@ def test_stream():
filename = os.path.join(testdir, "health.pdf") filename = os.path.join(testdir, "health.pdf")
tables = camelot.read_pdf(filename, flavor="stream") tables = camelot.read_pdf(filename, flavor="stream")
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_stream_table_rotated(): def test_stream_table_rotated():
@@ -45,11 +47,11 @@ def test_stream_table_rotated():
filename = os.path.join(testdir, "clockwise_table_2.pdf") filename = os.path.join(testdir, "clockwise_table_2.pdf")
tables = camelot.read_pdf(filename, flavor="stream") tables = camelot.read_pdf(filename, flavor="stream")
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
filename = os.path.join(testdir, "anticlockwise_table_2.pdf") filename = os.path.join(testdir, "anticlockwise_table_2.pdf")
tables = camelot.read_pdf(filename, flavor="stream") tables = camelot.read_pdf(filename, flavor="stream")
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_stream_two_tables(): def test_stream_two_tables():
@@ -71,7 +73,7 @@ def test_stream_table_regions():
tables = camelot.read_pdf( tables = camelot.read_pdf(
filename, flavor="stream", table_regions=["320,460,573,335"] filename, flavor="stream", table_regions=["320,460,573,335"]
) )
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_stream_table_areas(): def test_stream_table_areas():
@@ -81,7 +83,7 @@ def test_stream_table_areas():
tables = camelot.read_pdf( tables = camelot.read_pdf(
filename, flavor="stream", table_areas=["320,500,573,335"] filename, flavor="stream", table_areas=["320,500,573,335"]
) )
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_stream_columns(): def test_stream_columns():
@@ -91,7 +93,7 @@ def test_stream_columns():
tables = camelot.read_pdf( tables = camelot.read_pdf(
filename, flavor="stream", columns=["67,180,230,425,475"], row_tol=10 filename, flavor="stream", columns=["67,180,230,425,475"], row_tol=10
) )
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_stream_split_text(): def test_stream_split_text():
@@ -104,7 +106,7 @@ def test_stream_split_text():
columns=["72,95,209,327,442,529,566,606,683"], columns=["72,95,209,327,442,529,566,606,683"],
split_text=True, split_text=True,
) )
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_stream_flag_size(): def test_stream_flag_size():
@@ -112,7 +114,7 @@ def test_stream_flag_size():
filename = os.path.join(testdir, "superscript.pdf") filename = os.path.join(testdir, "superscript.pdf")
tables = camelot.read_pdf(filename, flavor="stream", flag_size=True) tables = camelot.read_pdf(filename, flavor="stream", flag_size=True)
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_stream_strip_text(): def test_stream_strip_text():
@@ -120,7 +122,7 @@ def test_stream_strip_text():
filename = os.path.join(testdir, "detect_vertical_false.pdf") filename = os.path.join(testdir, "detect_vertical_false.pdf")
tables = camelot.read_pdf(filename, flavor="stream", strip_text=" ,\n") tables = camelot.read_pdf(filename, flavor="stream", strip_text=" ,\n")
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_stream_edge_tol(): def test_stream_edge_tol():
@@ -128,7 +130,7 @@ def test_stream_edge_tol():
filename = os.path.join(testdir, "edge_tol.pdf") filename = os.path.join(testdir, "edge_tol.pdf")
tables = camelot.read_pdf(filename, flavor="stream", edge_tol=500) tables = camelot.read_pdf(filename, flavor="stream", edge_tol=500)
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_stream_layout_kwargs(): def test_stream_layout_kwargs():
@@ -138,7 +140,7 @@ def test_stream_layout_kwargs():
tables = camelot.read_pdf( tables = camelot.read_pdf(
filename, flavor="stream", layout_kwargs={"detect_vertical": False} filename, flavor="stream", layout_kwargs={"detect_vertical": False}
) )
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_lattice(): def test_lattice():
@@ -148,7 +150,7 @@ def test_lattice():
testdir, "tabula/icdar2013-dataset/competition-dataset-us/us-030.pdf" testdir, "tabula/icdar2013-dataset/competition-dataset-us/us-030.pdf"
) )
tables = camelot.read_pdf(filename, pages="2") tables = camelot.read_pdf(filename, pages="2")
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_lattice_table_rotated(): def test_lattice_table_rotated():
@@ -156,11 +158,11 @@ def test_lattice_table_rotated():
filename = os.path.join(testdir, "clockwise_table_1.pdf") filename = os.path.join(testdir, "clockwise_table_1.pdf")
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
filename = os.path.join(testdir, "anticlockwise_table_1.pdf") filename = os.path.join(testdir, "anticlockwise_table_1.pdf")
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_lattice_two_tables(): def test_lattice_two_tables():
@@ -179,7 +181,7 @@ def test_lattice_table_regions():
filename = os.path.join(testdir, "table_region.pdf") filename = os.path.join(testdir, "table_region.pdf")
tables = camelot.read_pdf(filename, table_regions=["170,370,560,270"]) tables = camelot.read_pdf(filename, table_regions=["170,370,560,270"])
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_lattice_table_areas(): def test_lattice_table_areas():
@@ -187,7 +189,7 @@ def test_lattice_table_areas():
filename = os.path.join(testdir, "twotables_2.pdf") filename = os.path.join(testdir, "twotables_2.pdf")
tables = camelot.read_pdf(filename, table_areas=["80,693,535,448"]) tables = camelot.read_pdf(filename, table_areas=["80,693,535,448"])
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_lattice_process_background(): def test_lattice_process_background():
@@ -195,7 +197,7 @@ def test_lattice_process_background():
filename = os.path.join(testdir, "background_lines_1.pdf") filename = os.path.join(testdir, "background_lines_1.pdf")
tables = camelot.read_pdf(filename, process_background=True) tables = camelot.read_pdf(filename, process_background=True)
assert df.equals(tables[1].df) assert_frame_equal(df, tables[1].df)
def test_lattice_copy_text(): def test_lattice_copy_text():
@@ -203,7 +205,7 @@ def test_lattice_copy_text():
filename = os.path.join(testdir, "row_span_1.pdf") filename = os.path.join(testdir, "row_span_1.pdf")
tables = camelot.read_pdf(filename, line_scale=60, copy_text="v") tables = camelot.read_pdf(filename, line_scale=60, copy_text="v")
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_lattice_shift_text(): def test_lattice_shift_text():
@@ -271,7 +273,7 @@ def test_arabic():
filename = os.path.join(testdir, "tabula/arabic.pdf") filename = os.path.join(testdir, "tabula/arabic.pdf")
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
assert df.equals(tables[0].df) assert_frame_equal(df, tables[0].df)
def test_table_order(): def test_table_order():
@@ -297,3 +299,26 @@ def test_table_order():
(1, 2), (1, 2),
(1, 1), (1, 1),
] ]
def test_version_generation():
version = (0, 7, 3)
assert generate_version(version, prerelease=None, revision=None) == "0.7.3"
def test_version_generation_with_prerelease_revision():
version = (0, 7, 3)
prerelease = "alpha"
revision = 2
assert (
generate_version(version, prerelease=prerelease, revision=revision)
== "0.7.3-alpha.2"
)
def test_stream_duplicated_text():
df = pd.DataFrame(data_stream_duplicated_text)
filename = os.path.join(testdir, "birdisland.pdf")
tables = camelot.read_pdf(filename, flavor="stream")
assert_frame_equal(df, tables[0].df)
+53 -30
View File
@@ -10,88 +10,111 @@ import camelot
testdir = os.path.dirname(os.path.abspath(__file__)) testdir = os.path.dirname(os.path.abspath(__file__))
testdir = os.path.join(testdir, "files") testdir = os.path.join(testdir, "files")
filename = os.path.join(testdir, 'foo.pdf') filename = os.path.join(testdir, "foo.pdf")
def test_unknown_flavor(): def test_unknown_flavor():
message = ("Unknown flavor specified." message = "Unknown flavor specified." " Use either 'lattice' or 'stream'"
" Use either 'lattice' or 'stream'")
with pytest.raises(NotImplementedError, match=message): with pytest.raises(NotImplementedError, match=message):
tables = camelot.read_pdf(filename, flavor='chocolate') tables = camelot.read_pdf(filename, flavor="chocolate")
def test_input_kwargs(): def test_input_kwargs():
message = "columns cannot be used with flavor='lattice'" message = "columns cannot be used with flavor='lattice'"
with pytest.raises(ValueError, match=message): with pytest.raises(ValueError, match=message):
tables = camelot.read_pdf(filename, columns=['10,20,30,40']) tables = camelot.read_pdf(filename, columns=["10,20,30,40"])
def test_unsupported_format(): def test_unsupported_format():
message = 'File format not supported' message = "File format not supported"
filename = os.path.join(testdir, 'foo.csv') filename = os.path.join(testdir, "foo.csv")
with pytest.raises(NotImplementedError, match=message): with pytest.raises(NotImplementedError, match=message):
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
def test_stream_equal_length(): def test_stream_equal_length():
message = ("Length of table_areas and columns" message = "Length of table_areas and columns" " should be equal"
" should be equal")
with pytest.raises(ValueError, match=message): with pytest.raises(ValueError, match=message):
tables = camelot.read_pdf(filename, flavor='stream', tables = camelot.read_pdf(
table_areas=['10,20,30,40'], columns=['10,20,30,40', '10,20,30,40']) filename,
flavor="stream",
table_areas=["10,20,30,40"],
columns=["10,20,30,40", "10,20,30,40"],
)
def test_image_warning(): def test_image_warning():
filename = os.path.join(testdir, 'image.pdf') filename = os.path.join(testdir, "image.pdf")
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter('error') warnings.simplefilter("error")
with pytest.raises(UserWarning) as e: with pytest.raises(UserWarning) as e:
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
assert str(e.value) == 'page-1 is image-based, camelot only works on text-based pages.' assert (
str(e.value)
== "page-1 is image-based, camelot only works on text-based pages."
)
def test_no_tables_found(): def test_lattice_no_tables_on_page():
filename = os.path.join(testdir, 'blank.pdf') filename = os.path.join(testdir, "empty.pdf")
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter('error') warnings.simplefilter("error")
with pytest.raises(UserWarning) as e: with pytest.raises(UserWarning) as e:
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename, flavor="lattice")
assert str(e.value) == 'No tables found on page-1' assert str(e.value) == "No tables found on page-1"
def test_stream_no_tables_on_page():
filename = os.path.join(testdir, "empty.pdf")
with warnings.catch_warnings():
warnings.simplefilter("error")
with pytest.raises(UserWarning) as e:
tables = camelot.read_pdf(filename, flavor="stream")
assert str(e.value) == "No tables found on page-1"
def test_stream_no_tables_in_area():
filename = os.path.join(testdir, "only_page_number.pdf")
with warnings.catch_warnings():
warnings.simplefilter("error")
with pytest.raises(UserWarning) as e:
tables = camelot.read_pdf(filename, flavor="stream")
assert str(e.value) == "No tables found in table area 1"
def test_no_tables_found_logs_suppressed(): def test_no_tables_found_logs_suppressed():
filename = os.path.join(testdir, 'foo.pdf') filename = os.path.join(testdir, "foo.pdf")
with warnings.catch_warnings(): with warnings.catch_warnings():
# the test should fail if any warning is thrown # the test should fail if any warning is thrown
warnings.simplefilter('error') warnings.simplefilter("error")
try: try:
tables = camelot.read_pdf(filename, suppress_stdout=True) tables = camelot.read_pdf(filename, suppress_stdout=True)
except Warning as e: except Warning as e:
warning_text = str(e) warning_text = str(e)
pytest.fail('Unexpected warning: {}'.format(warning_text)) pytest.fail(f"Unexpected warning: {warning_text}")
def test_no_tables_found_warnings_suppressed(): def test_no_tables_found_warnings_suppressed():
filename = os.path.join(testdir, 'blank.pdf') filename = os.path.join(testdir, "empty.pdf")
with warnings.catch_warnings(): with warnings.catch_warnings():
# the test should fail if any warning is thrown # the test should fail if any warning is thrown
warnings.simplefilter('error') warnings.simplefilter("error")
try: try:
tables = camelot.read_pdf(filename, suppress_stdout=True) tables = camelot.read_pdf(filename, suppress_stdout=True)
except Warning as e: except Warning as e:
warning_text = str(e) warning_text = str(e)
pytest.fail('Unexpected warning: {}'.format(warning_text)) pytest.fail(f"Unexpected warning: {warning_text}")
def test_no_password(): def test_no_password():
filename = os.path.join(testdir, 'health_protected.pdf') filename = os.path.join(testdir, "health_protected.pdf")
message = 'file has not been decrypted' message = "file has not been decrypted"
with pytest.raises(Exception, match=message): with pytest.raises(Exception, match=message):
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
def test_bad_password(): def test_bad_password():
filename = os.path.join(testdir, 'health_protected.pdf') filename = os.path.join(testdir, "health_protected.pdf")
message = 'file has not been decrypted' message = "file has not been decrypted"
with pytest.raises(Exception, match=message): with pytest.raises(Exception, match=message):
tables = camelot.read_pdf(filename, password='wrongpass') tables = camelot.read_pdf(filename, password="wrongpass")
+16 -23
View File
@@ -11,57 +11,50 @@ testdir = os.path.dirname(os.path.abspath(__file__))
testdir = os.path.join(testdir, "files") testdir = os.path.join(testdir, "files")
@pytest.mark.mpl_image_compare( @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True)
baseline_dir="files/baseline_plots", remove_text=True)
def test_text_plot(): def test_text_plot():
filename = os.path.join(testdir, "foo.pdf") filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
return camelot.plot(tables[0], kind='text') return camelot.plot(tables[0], kind="text")
@pytest.mark.mpl_image_compare( @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True)
baseline_dir="files/baseline_plots", remove_text=True)
def test_grid_plot(): def test_grid_plot():
filename = os.path.join(testdir, "foo.pdf") filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
return camelot.plot(tables[0], kind='grid') return camelot.plot(tables[0], kind="grid")
@pytest.mark.mpl_image_compare( @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True)
baseline_dir="files/baseline_plots", remove_text=True)
def test_lattice_contour_plot(): def test_lattice_contour_plot():
filename = os.path.join(testdir, "foo.pdf") filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
return camelot.plot(tables[0], kind='contour') return camelot.plot(tables[0], kind="contour")
@pytest.mark.mpl_image_compare( @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True)
baseline_dir="files/baseline_plots", remove_text=True)
def test_stream_contour_plot(): def test_stream_contour_plot():
filename = os.path.join(testdir, "tabula/12s0324.pdf") filename = os.path.join(testdir, "tabula/12s0324.pdf")
tables = camelot.read_pdf(filename, flavor='stream') tables = camelot.read_pdf(filename, flavor="stream")
return camelot.plot(tables[0], kind='contour') return camelot.plot(tables[0], kind="contour")
@pytest.mark.mpl_image_compare( @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True)
baseline_dir="files/baseline_plots", remove_text=True)
def test_line_plot(): def test_line_plot():
filename = os.path.join(testdir, "foo.pdf") filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
return camelot.plot(tables[0], kind='line') return camelot.plot(tables[0], kind="line")
@pytest.mark.mpl_image_compare( @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True)
baseline_dir="files/baseline_plots", remove_text=True)
def test_joint_plot(): def test_joint_plot():
filename = os.path.join(testdir, "foo.pdf") filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename) tables = camelot.read_pdf(filename)
return camelot.plot(tables[0], kind='joint') return camelot.plot(tables[0], kind="joint")
@pytest.mark.mpl_image_compare( @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True)
baseline_dir="files/baseline_plots", remove_text=True)
def test_textedge_plot(): def test_textedge_plot():
filename = os.path.join(testdir, "tabula/12s0324.pdf") filename = os.path.join(testdir, "tabula/12s0324.pdf")
tables = camelot.read_pdf(filename, flavor='stream') tables = camelot.read_pdf(filename, flavor="stream")
return camelot.plot(tables[0], kind='textedge') return camelot.plot(tables[0], kind="textedge")