Repository: chezou/tabula-py (python)

Issue:
It would be nice to be able to include a template (json file tabula-template) directly as an option of tabula-py
(This is a reopening of #110, opened by made to fit the issue template. This was originally submitted by @papagala but I have the same need, AFAICT)

**Is your feature request related to a problem? Please describe.**
I have PDFs with many pages, each presenting a table with the same (lined) structure, but filled differently (some boxes are empty, some are filled, the length varies, it depends on the page). The empty cell and varying lengths of entries mess up the table structure detection, enough that each table is understood to have a slightly different structure, and as a result the JSON output has a different structure page-per-page. 

Note that, in my case, I am actually trying to devise a solution that will assist each of my users in analysing their PDF file consisting of many pages, many tables, etc. The actual content is personal data. To fix ideas, think of me pre-programming a little python script to assist in scraping a PDF printout of their bank statement. 

This is my own problem, but maybe @papagala's is different. 

**Describe the solution you'd like**
My problem can already be partially managed in the following way: from the structure extracted from each page (through method described in #102, for instance), I can figure out programmatically, with some basic logic, which unique structure to use across all the pages. The problem is that I don't have a way to then tell `tabula-py` to use this structure to reanalyse the whole document. 

I imagine I could pass on that structure as a tabula template to `read_pdf`, but AFAICT this functionality is not available right now (but is available in tabula java?). 

**Describe alternatives you've considered**
I could:
 1. use tabula on each page; 
 2. output some JSON;
 3. look at the position of the cells (see #102);
 4. try to reconcile these cell positions across the pages into a global template;
 5. go back, page by page, to transform the JSON output to fit the global template.

I assess 4. to be of medium difficulty and 5. to be hard. Much better would be to, instead of 5., re-use tabula based on the global template.

In my particular situation, where I have to prepare this for others to use, this alternative would be messy, error-prone, and very unlikely to be fixed by end-users. 


Pull request:
Implement read_pdf_with_template() function
This PR introduces extract PDF with Tabula app JSON templates.

Close #111 

Example:

```py
In [7]: dfs = tabula.read_pdf_with_template('./examples/data.pdf', './examples/data.tabula-template.json', pandas_options={'header': 0})

In [8]: dfs[0]
Out[8]:
             Unnamed: 0   mpg  cyl   disp   hp  drat     wt   qsec  vs  am  \
0             Mazda RX4  21.0    6  160.0  110  3.90  2.620  16.46   0   1
1         Mazda RX4 Wag  21.0    6  160.0  110  3.90  2.875  17.02   0   1
2            Datsun 710  22.8    4  108.0   93  3.85  2.320  18.61   1   1
3        Hornet 4 Drive  21.4    6  258.0  110  3.08  3.215  19.44   1   0
4     Hornet Sportabout  18.7    8  360.0  175  3.15  3.440  17.02   0   0
5               Valiant  18.1    6  225.0  105  2.76  3.460  20.22   1   0
6            Duster 360  14.3    8  360.0  245  3.21  3.570  15.84   0   0
7             Merc 240D  24.4    4  146.7   62  3.69  3.190  20.00   1   0
8              Merc 230  22.8    4  140.8   95  3.92  3.150  22.90   1   0
9              Merc 280  19.2    6  167.6  123  3.92  3.440  18.30   1   0
10            Merc 280C  17.8    6  167.6  123  3.92  3.440  18.90   1   0
11           Merc 450SE  16.4    8  275.8  180  3.07  4.070  17.40   0   0
12           Merc 450SL  17.3    8  275.8  180  3.07  3.730  17.60   0   0
13          Merc 450SLC  15.2    8  275.8  180  3.07  3.780  18.00   0   0
14   Cadillac Fleetwood  10.4    8  472.0  205  2.93  5.250  17.98   0   0
15  Lincoln Continental  10.4    8  460.0  215  3.00  5.424  17.82   0   0
16    Chrysler Imperial  14.7    8  440.0  230  3.23  5.345  17.42   0   0
17             Fiat 128  32.4    4   78.7   66  4.08  2.200  19.47   1   1
18          Honda Civic  30.4    4   75.7   52  4.93  1.615  18.52   1   1
19       Toyota Corolla  33.9    4   71.1   65  4.22  1.835  19.90   1   1
20        Toyota Corona  21.5    4  120.1   97  3.70  2.465  20.01   1   0
21     Dodge Challenger  15.5    8  318.0  150  2.76  3.520  16.87   0   0
22          AMC Javelin  15.2    8  304.0  150  3.15  3.435  17.30   0   0
23           Camaro Z28  13.3    8  350.0  245  3.73  3.840  15.41   0   0
24     Pontiac Firebird  19.2    8  400.0  175  3.08  3.845  17.05   0   0
25            Fiat X1-9  27.3    4   79.0   66  4.08  1.935  18.90   1   1
26        Porsche 914-2  26.0    4  120.3   91  4.43  2.140  16.70   0   1
27         Lotus Europa  30.4    4   95.1  113  3.77  1.513  16.90   1   1
28       Ford Pantera L  15.8    8  351.0  264  4.22  3.170  14.50   0   1
29         Ferrari Dino  19.7    6  145.0  175  3.62  2.770  15.50   0   1
30        Maserati Bora  15.0    8  301.0  335  3.54  3.570  14.60   0   1
31           Volvo 142E  21.4    4  121.0  109  4.11  2.780  18.60   1   1

    gear  carb
0      4     4
1      4     4
2      4     1
3      3     1
4      3     2
5      3     1
6      3     4
7      4     2
8      4     2
9      4     4
10     4     4
11     3     3
12     3     3
13     3     3
14     3     4
15     3     4
16     3     4
17     4     1
18     4     2
19     4     1
20     3     1
21     3     2
22     3     2
23     3     4
24     3     2
25     4     1
26     5     2
27     5     2
28     5     4
29     5     6
30     5     8
31     4     2

In [9]: dfs[1]
Out[9]:
       Sepal.Width Petal.Length Petal.Width Species
0  5.1         3.5          1.4         0.2  setosa
1  4.9         3.0          1.4         0.2  setosa
2  4.7         3.2          1.3         0.2  setosa
3  4.6         3.1          1.5         0.2  setosa
4  5.0         3.6          1.4         0.2  setosa

In [10]: dfs[2]
Out[10]:
       Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
0  145          6.7         3.3          5.7         2.5  virginica
1  146          6.7         3.0          5.2         2.3  virginica
2  147          6.3         2.5          5.0         1.9  virginica
3  148          6.5         3.0          5.2         2.0  virginica
4  149          6.2         3.4          5.4         2.3  virginica

In [11]: dfs[3]
Out[11]:
     len supp  dose
0    4.2   VC   0.5
1   11.5   VC   0.5
2    7.3   VC   0.5
3    5.8   VC   0.5
4    6.4   VC   0.5
5   10.0   VC   0.5
6   11.2   VC   0.5
7   11.2   VC   0.5
8    5.2   VC   0.5
9    7.0   VC   0.5
10  16.5   VC   1.0
11  16.5   VC   1.0
12  15.2   VC   1.0
13  17.3   VC   1.0
14  22.5   VC   1.0
```

Patch:
diff --git a/examples/data.tabula-template.json b/examples/data.tabula-template.json
new file mode 100644
index 0000000..47159ad
--- /dev/null
+++ b/examples/data.tabula-template.json
@@ -0,0 +1,1 @@
+[{"page":1,"extraction_method":"guess","x1":153.99985500000003,"x2":565.5698550000001,"y1":123.999615,"y2":531.7446150000001,"width":411.57,"height":407.745},{"page":2,"extraction_method":"guess","x1":153.99985500000003,"x2":453.879855,"y1":123.99884999999993,"y2":210.44384999999994,"width":299.88,"height":86.44500000000001},{"page":2,"extraction_method":"guess","x1":153.99985500000003,"x2":487.53985500000005,"y1":410.99625000000003,"y2":497.44125,"width":333.54,"height":86.44500000000001},{"page":3,"extraction_method":"guess","x1":153.99985500000003,"x2":235.85485500000001,"y1":123.99885000000012,"y2":322.8988500000001,"width":81.855,"height":198.9}]
\ No newline at end of file
diff --git a/tabula/__init__.py b/tabula/__init__.py
index 10da73c..15b14db 100644
--- a/tabula/__init__.py
+++ b/tabula/__init__.py
@@ -1,4 +1,5 @@
 from .wrapper import read_pdf
+from .wrapper import read_pdf_with_template
 from .wrapper import convert_into
 from .wrapper import convert_into_by_batch
 from .util import environment_info
diff --git a/tabula/file_util.py b/tabula/file_util.py
new file mode 100644
index 0000000..e6fa311
--- /dev/null
+++ b/tabula/file_util.py
@@ -0,0 +1,116 @@
+import sys
+import os
+import shutil
+
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] >= 3
+
+if PY3:
+    from urllib.request import urlopen
+    from urllib.parse import urlparse as parse_url
+    from urllib.parse import uses_relative, uses_netloc, uses_params
+    text_type = str
+else:
+    from urllib2 import urlopen
+    from urlparse import urlparse as parse_url
+    from urlparse import uses_relative, uses_netloc, uses_params
+    text_type = unicode
+
+
+_VALID_URLS = set(uses_relative + uses_netloc + uses_params)
+_VALID_URLS.discard('')
+
+
+def localize_file(path_or_buffer):
+    '''Ensure localize target file.
+
+    If the target file is remote, this function fetches into local storage.
+
+    Args:
+        path (str):
+            File path or file like object or URL of target file.
+
+    Returns:
+        filename (str): file name in local storage
+        temporary_file_flag (bool): temporary file flag
+    '''
+
+    path_or_buffer = _stringify_path(path_or_buffer)
+
+    if _is_url(path_or_buffer):
+        req = urlopen(path_or_buffer)
+        filename = os.path.basename(req.geturl())
+        if os.path.splitext(filename)[-1] is not ".pdf":
+            pid = os.getpid()
+            filename = "{0}.pdf".format(pid)
+
+        with open(filename, 'wb') as f:
+            shutil.copyfileobj(req, f)
+
+        return filename, True
+
+    elif is_file_like(path_or_buffer):
+        pid = os.getpid()
+        filename = "{0}.pdf".format(pid)
+
+        with open(filename, 'wb') as f:
+            shutil.copyfileobj(path_or_buffer, f)
+
+        return filename, True
+
+    # File path case
+    else:
+        return os.path.expanduser(path_or_buffer), False
+
+
+def _is_url(url):
+    try:
+        return parse_url(url).scheme in _VALID_URLS
+
+    except Exception:
+        return False
+
+
+def is_file_like(obj):
+    '''Check file like object
+
+    Args:
+        obj:
+            file like object.
+
+    Returns:
+        bool: file like object or not
+    '''
+
+    if not (hasattr(obj, 'read') or hasattr(obj, 'write')):
+        return False
+
+    if not hasattr(obj, "__iter__"):
+        return False
+
+    return True
+
+
+def _stringify_path(path_or_buffer):
+    '''Convert path like object to string
+
+    Args:
+        path_or_buffer: object to be converted
+
+    Returns:
+        string_path_or_buffer: maybe string version of path_or_buffer
+    '''
+
+    try:
+        import pathlib
+        _PATHLIB_INSTALLED = True
+    except ImportError:
+        _PATHLIB_INSTALLED = False
+
+    if hasattr(path_or_buffer, '__fspath__'):
+        return path_or_buffer.__fspath__()
+
+    if _PATHLIB_INSTALLED and isinstance(path_or_buffer, pathlib.Path):
+        return text_type(path_or_buffer)
+
+    return path_or_buffer
diff --git a/tabula/template.py b/tabula/template.py
new file mode 100644
index 0000000..e599dde
--- /dev/null
+++ b/tabula/template.py
@@ -0,0 +1,69 @@
+import json
+from .file_util import is_file_like
+from .file_util import _stringify_path
+
+
+def load_template(path_or_buffer):
+    '''Build tabula-py option from template file
+
+    Args:
+        file_like_obj: File like object of Tabula app template
+
+    Returns:
+        `obj`:dict: tabula-py options
+    '''
+
+    from itertools import groupby
+    from operator import itemgetter
+
+    path_or_buffer = _stringify_path(path_or_buffer)
+
+    if is_file_like(path_or_buffer):
+        templates = json.load(path_or_buffer)
+    else:
+        with open(path_or_buffer, 'r') as f:
+            templates = json.load(f)
+
+    options = []
+
+    grouper = itemgetter('page', 'extraction_method')
+
+    for key, grp in groupby(sorted(templates, key=grouper), grouper):
+        tmp_options = [_convert_template_option(e) for e in grp]
+
+        if len(tmp_options) == 1:
+            options.append(tmp_options[0])
+            continue
+
+        option = tmp_options[0]
+        areas = [e.get('area') for e in tmp_options]
+        option['area'] = areas
+        option['multiple_tables'] = True
+        options.append(option)
+
+    return options
+
+
+def _convert_template_option(template):
+    '''Convert Tabula app template to tabula-py option
+
+    Args:
+        template (dict): Tabula app template
+
+    Returns:
+        `obj`:dict: tabula-py option
+    '''
+
+    option = {}
+    extraction_method = template.get('extraction_method')
+    if extraction_method == 'guess':
+        option['guess'] = True
+    elif extraction_method == 'lattice':
+        option['lattice'] = True
+    elif extraction_method == 'stream':
+        option['stream'] = True
+
+    option['pages'] = template.get('page')
+    option['area'] = [round(template['y1'], 3), round(template['x1'], 3), round(template['y2'], 3), round(template['x2'], 3)]
+
+    return option
diff --git a/tabula/wrapper.py b/tabula/wrapper.py
index 18c7136..0f2c78f 100644
--- a/tabula/wrapper.py
+++ b/tabula/wrapper.py
@@ -17,30 +17,12 @@ import subprocess
 
 import numpy as np
 import pandas as pd
-import shutil
 import sys
 import errno
 
 from .util import deprecated_option
-
-PY2 = sys.version_info[0] == 2
-PY3 = sys.version_info[0] >= 3
-
-if PY3:
-    from urllib.request import urlopen
-    from urllib.parse import urlparse as parse_url
-    from urllib.parse import uses_relative, uses_netloc, uses_params
-    text_type = str
-else:
-    from urllib2 import urlopen
-    from urlparse import urlparse as parse_url
-    from urlparse import uses_relative, uses_netloc, uses_params
-    text_type = unicode
-
-
-_VALID_URLS = set(uses_relative + uses_netloc + uses_params)
-_VALID_URLS.discard('')
-
+from .file_util import localize_file
+from .template import load_template
 
 TABULA_JAVA_VERSION = "1.0.2"
 JAR_NAME = "tabula-{}-jar-with-dependencies.jar".format(TABULA_JAVA_VERSION)
@@ -66,8 +48,8 @@ def read_pdf(input_path,
     '''Read tables in PDF.
 
     Args:
-        input_path (str):
-            File path of tareget PDF file.
+        input_path (file like obj):
+            File like object of tareget PDF file.
         output_format (str, optional):
             Output format of this function (dataframe or json)
         encoding (str, optional):
@@ -115,7 +97,7 @@ def read_pdf(input_path,
 
     options = build_options(kwargs)
 
-    path, temporary = _localize_file(input_path)
+    path, temporary = localize_file(input_path)
     args = ["java"] + java_options + ["-jar", JAR_PATH] + options + [path]
 
     if not os.path.exists(path):
@@ -146,7 +128,7 @@ def read_pdf(input_path,
     fmt = kwargs.get('format')
     if fmt == 'JSON':
         if multiple_tables:
-            return extract_from(json.loads(output.decode(encoding)), pandas_options)
+            return _extract_from(json.loads(output.decode(encoding)), pandas_options)
 
         else:
             return json.loads(output.decode(encoding))
@@ -163,12 +145,57 @@ def read_pdf(input_path,
             raise
 
 
+def read_pdf_with_template(
+        input_path,
+        template_path,
+        pandas_options=None,
+        encoding='utf-8',
+        java_options=None,
+        **kwargs):
+    '''Read tables in PDF.
+
+    Args:
+        input_path (file like obj):
+            File like object of tareget PDF file.
+        template_path (file_like_obj):
+            File like object for Tabula app template.
+        pandas_options (dict, optional):
+            Set pandas options like {'header': None}.
+        encoding (str, optional):
+            Encoding type for pandas. Default is 'utf-8'
+        java_options (list, optional):
+            Set java options like `-Xmx256m`.
+        kwargs (dict):
+            Dictionary of option for tabula-java. Details are shown in `build_options()`
+
+    Returns:
+        Extracted pandas DataFrame or list.
+    '''
+
+    from itertools import chain
+
+    options = load_template(template_path)
+    dataframes = []
+
+    for option in options:
+        _df = read_pdf(
+            input_path, pandas_options=pandas_options,
+            encoding=encoding, java_options=java_options,
+            **dict(kwargs, **option))
+        if isinstance(_df, list):
+            dataframes.extend(_df)
+        else:
+            dataframes.append(_df)
+
+    return dataframes
+
+
 def convert_into(input_path, output_path, output_format='csv', java_options=None, **kwargs):
     '''Convert tables from PDF into a file.
 
     Args:
-        input_path (str):
-            File path of tareget PDF file.
+        input_path (file like obj):
+            File like object of tareget PDF file.
         output_path (str):
             File path of output file.
         output_format (str, optional):
@@ -186,7 +213,7 @@ def convert_into(input_path, output_path, output_format='csv', java_options=None
         raise AttributeError("'output_path' shoud not be None or empty")
 
     kwargs['output_path'] = output_path
-    kwargs['format'] = extract_format_for_conversion(output_format)
+    kwargs['format'] = _extract_format_for_conversion(output_format)
 
     if java_options is None:
         java_options = []
@@ -195,7 +222,7 @@ def convert_into(input_path, output_path, output_format='csv', java_options=None
         java_options = shlex.split(java_options)
 
     options = build_options(kwargs)
-    path, temporary = _localize_file(input_path)
+    path, temporary = localize_file(input_path)
     args = ["java"] + java_options + ["-jar", JAR_PATH] + options + [path]
 
     if not os.path.exists(path):
@@ -238,7 +265,7 @@ def convert_into_by_batch(input_dir, output_format='csv', java_options=None, **k
     if input_dir is None or not os.path.isdir(input_dir):
         raise AttributeError("'input_dir' shoud be directory path")
 
-    kwargs['format'] = extract_format_for_conversion(output_format)
+    kwargs['format'] = _extract_format_for_conversion(output_format)
 
     if java_options is None:
         java_options = []
@@ -266,7 +293,7 @@ def convert_into_by_batch(input_dir, output_format='csv', java_options=None, **k
         raise
 
 
-def extract_format_for_conversion(output_format='csv'):
+def _extract_format_for_conversion(output_format='csv'):
     if output_format == 'csv':
         return 'CSV'
 
@@ -280,7 +307,7 @@ def extract_format_for_conversion(output_format='csv'):
         raise AttributeError("'output_format' has no attribute 'dataframe'")
 
 
-def extract_from(raw_json, pandas_options=None):
+def _extract_from(raw_json, pandas_options=None):
     '''Extract tables from json.
 
     Args:
@@ -295,7 +322,7 @@ def extract_from(raw_json, pandas_options=None):
         pandas_options = {}
 
     columns = pandas_options.pop('columns', None)
-    columns, header_line_number = convert_pandas_csv_options(pandas_options, columns)
+    columns, header_line_number = _convert_pandas_csv_options(pandas_options, columns)
 
     for table in raw_json:
         list_data = [[np.nan if not e['text'] else e['text'] for e in row] for row in table['data']]
@@ -310,7 +337,7 @@ def extract_from(raw_json, pandas_options=None):
     return data_frames
 
 
-def convert_pandas_csv_options(pandas_options, columns):
+def _convert_pandas_csv_options(pandas_options, columns):
     ''' Translate `pd.read_csv()` options into `pd.DataFrame()` especially for header.
 
     Args:
@@ -322,6 +349,7 @@ def convert_pandas_csv_options(pandas_options, columns):
 
     _columns = pandas_options.pop('names', columns)
     header = pandas_options.pop('header', None)
+    pandas_options.pop('encoding', None)
 
     if header == 'infer':
         header_line_number = 0 if not bool(_columns) else None
@@ -331,101 +359,6 @@ def convert_pandas_csv_options(pandas_options, columns):
     return _columns, header_line_number
 
 
-def _localize_file(path_or_buffer):
-    '''Ensure localize target file.
-
-    If the target file is remote, this function fetches into local storage.
-
-    Args:
-        path (str):
-            File path or file like object or URL of target file.
-    
-    Returns:
-        filename (str): file name in local storage
-        temporary_file_flag (bool): temporary file flag
-    '''
-
-    path_or_buffer = _stringify_path(path_or_buffer)
-
-    if _is_url(path_or_buffer):
-        req = urlopen(path_or_buffer)
-        filename = os.path.basename(req.geturl())
-        if os.path.splitext(filename)[-1] is not ".pdf":
-            pid = os.getpid()
-            filename = "{0}.pdf".format(pid)
-
-        with open(filename, 'wb') as f:
-            shutil.copyfileobj(req, f)
-
-        return filename, True
-
-    elif _is_file_like(path_or_buffer):
-        pid = os.getpid()
-        filename = "{0}.pdf".format(pid)
-
-        with open(filename, 'wb') as f:
-            shutil.copyfileobj(path_or_buffer, f)
-
-        return filename, True
-
-    # File path case
-    else:
-        return os.path.expanduser(path_or_buffer), False
-
-
-def _is_url(url):
-    try:
-        return parse_url(url).scheme in _VALID_URLS
-
-    except Exception:
-        return False
-
-
-def _is_file_like(obj):
-    '''Check file like object
-
-    Args:
-        obj:
-            file like object.
-    
-    Returns:
-        bool: file like object or not
-    '''
-
-    if not (hasattr(obj, 'read') or hasattr(obj, 'write')):
-        return False
-
-    if not hasattr(obj, "__iter__"):
-        return False
-
-    return True
-
-
-def _stringify_path(path_or_buffer):
-    '''Convert path like object to string
-
-    Args:
-        path_or_buffer: object to be converted
-
-    Returns:
-        string_path_or_buffer: maybe string version of path_or_buffer
-    '''
-
-    try:
-        import pathlib
-        _PATHLIB_INSTALLED = True
-    except ImportError:
-        _PATHLIB_INSTALLED = False
-
-    if hasattr(path_or_buffer, '__fspath__'):
-        return path_or_buffer.__fspath__()
-
-    if _PATHLIB_INSTALLED and isinstance(path_or_buffer, pathlib.Path):
-        return text_type(path_or_buffer)
-
-    return path_or_buffer
-
-
 def build_options(kwargs=None):
     '''Build options for tabula-java
 
@@ -471,6 +404,7 @@ def build_options(kwargs=None):
     Returns:
         `obj`:list: Built list of options
     '''
+
     __options = []
     if kwargs is None:
         kwargs = {}


Test patch:
diff --git a/tests/resources/data.tabula-template.json b/tests/resources/data.tabula-template.json
new file mode 100644
index 0000000..47159ad
--- /dev/null
+++ b/tests/resources/data.tabula-template.json
@@ -0,0 +1,1 @@
+[{"page":1,"extraction_method":"guess","x1":153.99985500000003,"x2":565.5698550000001,"y1":123.999615,"y2":531.7446150000001,"width":411.57,"height":407.745},{"page":2,"extraction_method":"guess","x1":153.99985500000003,"x2":453.879855,"y1":123.99884999999993,"y2":210.44384999999994,"width":299.88,"height":86.44500000000001},{"page":2,"extraction_method":"guess","x1":153.99985500000003,"x2":487.53985500000005,"y1":410.99625000000003,"y2":497.44125,"width":333.54,"height":86.44500000000001},{"page":3,"extraction_method":"guess","x1":153.99985500000003,"x2":235.85485500000001,"y1":123.99885000000012,"y2":322.8988500000001,"width":81.855,"height":198.9}]
\ No newline at end of file
diff --git a/tests/test_read_pdf_table.py b/tests/test_read_pdf_table.py
index 845581f..e82f559 100644
--- a/tests/test_read_pdf_table.py
+++ b/tests/test_read_pdf_table.py
@@ -13,7 +13,7 @@ try:
     FileNotFoundError
 except NameError:
     FileNotFoundError = IOError
- 
+
 
 class TestReadPdfTable(unittest.TestCase):
     def test_read_pdf(self):
@@ -165,6 +165,18 @@ class TestReadPdfTable(unittest.TestCase):
         with self.assertRaises(AttributeError):
             tabula.convert_into(pdf_path, '')
 
+    def test_read_pdf_with_template(self):
+        pdf_path = 'tests/resources/data.pdf'
+        template_path = 'tests/resources/data.tabula-template.json'
+        expected_csv1 = 'tests/resources/data_1.csv'
+
+        dfs = tabula.read_pdf_with_template(pdf_path, template_path)
+        self.assertEqual(
+            len(dfs),
+            4)
+        self.assertTrue(dfs[0].equals(
+            pd.read_csv(expected_csv1)))
+
 
 if __name__ == '__main__':
     unittest.main()