Support for vertical tables in Stream
* Change var names * Add test pdf * Add tests for Lattice rotation * Add support for vertical tables in Stream, test pdfs * Add tests for Stream rotation
This commit is contained in:
+18
-11
@@ -9,9 +9,10 @@ from .imgproc import (adaptive_threshold, find_lines, find_table_contours,
|
||||
find_table_joints)
|
||||
from .table import Table
|
||||
from .utils import (scale_to_pdf, scale_to_image, segments_bbox, text_bbox,
|
||||
detect_vertical, merge_close_values, get_row_index,
|
||||
get_rotation, merge_close_values, get_row_index,
|
||||
get_column_index, get_score, reduce_index, outline,
|
||||
fill_spanning, count_empty, encode_list, pdf_to_text)
|
||||
fill_spanning, count_empty, encode_list, get_page_layout,
|
||||
get_text_objects)
|
||||
|
||||
|
||||
__all__ = ['Lattice']
|
||||
@@ -62,7 +63,7 @@ class Lattice:
|
||||
page as value.
|
||||
"""
|
||||
def __init__(self, table_area=None, fill=None, mtol=[2], scale=15,
|
||||
invert=False, margins=(2.0, 0.5, 0.1), debug=None):
|
||||
invert=False, margins=(1.0, 0.5, 0.1), debug=None):
|
||||
|
||||
self.method = 'lattice'
|
||||
self.table_area = table_area
|
||||
@@ -82,10 +83,14 @@ class Lattice:
|
||||
Dictionary with page number as key and list of tables on that
|
||||
page as value.
|
||||
"""
|
||||
text, __, width, height = pdf_to_text(pdfname, self.char_margin,
|
||||
self.line_margin, self.word_margin)
|
||||
layout, dim = get_page_layout(pdfname, char_margin=self.char_margin,
|
||||
line_margin=self.line_margin, word_margin=self.word_margin)
|
||||
ltchar = get_text_objects(layout, LTType="char")
|
||||
lttextlh = get_text_objects(layout, LTType="lh")
|
||||
lttextlv = get_text_objects(layout, LTType="lv")
|
||||
width, height = dim
|
||||
bname, __ = os.path.splitext(pdfname)
|
||||
if not text:
|
||||
if not ltchar:
|
||||
logging.warning("{0}: PDF has no text. It may be an image.".format(
|
||||
os.path.basename(bname)))
|
||||
return None
|
||||
@@ -156,9 +161,11 @@ class Lattice:
|
||||
# select elements which lie within table_bbox
|
||||
table_data = {}
|
||||
v_s, h_s = segments_bbox(k, v_segments, h_segments)
|
||||
t_bbox = text_bbox(k, text)
|
||||
table_data['text_p'] = 100 * (1 - (len(t_bbox) / len(text)))
|
||||
table_rotation = detect_vertical(t_bbox)
|
||||
char_bbox = text_bbox(k, ltchar)
|
||||
lh_bbox = text_bbox(k, lttextlh)
|
||||
lv_bbox = text_bbox(k, lttextlv)
|
||||
table_data['text_p'] = 100 * (1 - (len(char_bbox) / len(ltchar)))
|
||||
table_rotation = get_rotation(char_bbox, lh_bbox, lv_bbox)
|
||||
cols, rows = zip(*table_bbox[k])
|
||||
cols, rows = list(cols), list(rows)
|
||||
cols.extend([k[0], k[2]])
|
||||
@@ -187,7 +194,7 @@ class Lattice:
|
||||
|
||||
rerror = []
|
||||
cerror = []
|
||||
for t in text:
|
||||
for t in char_bbox:
|
||||
try:
|
||||
r_idx, rass_error = get_row_index(t, rows)
|
||||
except TypeError:
|
||||
@@ -207,7 +214,7 @@ class Lattice:
|
||||
for j in range(len(table.cells[i])):
|
||||
t_bbox = table.cells[i][j].get_objects()
|
||||
try:
|
||||
cell_rotation = detect_vertical(t_bbox)
|
||||
cell_rotation = get_rotation(t_bbox)
|
||||
except ZeroDivisionError:
|
||||
cell_rotation = ''
|
||||
pass
|
||||
|
||||
+46
-14
@@ -7,8 +7,8 @@ import copy_reg
|
||||
import numpy as np
|
||||
|
||||
from .table import Table
|
||||
from .utils import (get_row_index, get_score, count_empty, encode_list,
|
||||
pdf_to_text, text_bbox)
|
||||
from .utils import (rotate, get_row_index, get_score, count_empty, encode_list,
|
||||
get_page_layout, get_text_objects, text_bbox, get_rotation)
|
||||
|
||||
|
||||
__all__ = ['Stream']
|
||||
@@ -199,7 +199,7 @@ class Stream:
|
||||
page as value.
|
||||
"""
|
||||
def __init__(self, table_area=None, columns=None, ncolumns=None, ytol=[2],
|
||||
mtol=[2], margins=(2.0, 0.5, 0.1), debug=False):
|
||||
mtol=[0], margins=(1.0, 0.5, 0.1), debug=False):
|
||||
|
||||
self.method = 'stream'
|
||||
self.table_area = table_area
|
||||
@@ -219,17 +219,20 @@ class Stream:
|
||||
Dictionary with page number as key and list of tables on that
|
||||
page as value.
|
||||
"""
|
||||
__, text, width, height = pdf_to_text(pdfname, self.char_margin,
|
||||
self.line_margin, self.word_margin)
|
||||
layout, dim = get_page_layout(pdfname, char_margin=self.char_margin,
|
||||
line_margin=self.line_margin, word_margin=self.word_margin)
|
||||
ltchar = get_text_objects(layout, LTType="char")
|
||||
lttextlh = get_text_objects(layout, LTType="lh")
|
||||
lttextlv = get_text_objects(layout, LTType="lv")
|
||||
width, height = dim
|
||||
bname, __ = os.path.splitext(pdfname)
|
||||
if not text:
|
||||
if not lttextlh:
|
||||
logging.warning("{0}: PDF has no text. It may be an image.".format(
|
||||
os.path.basename(bname)))
|
||||
return None
|
||||
|
||||
if self.debug:
|
||||
self.debug_text = [(t.x0, t.y0, t.x1, t.y1) for t in text]
|
||||
return None
|
||||
self.debug_text = []
|
||||
|
||||
if self.table_area is not None:
|
||||
if self.columns is not None:
|
||||
@@ -261,11 +264,35 @@ class Stream:
|
||||
for k in sorted(table_bbox.keys(), key=lambda x: x[1], reverse=True):
|
||||
# select elements which lie within table_bbox
|
||||
table_data = {}
|
||||
t_bbox = text_bbox(k, text)
|
||||
table_rotation = get_rotation(ltchar, lttextlh, lttextlv)
|
||||
if table_rotation != '':
|
||||
t_bbox = text_bbox(k, lttextlv)
|
||||
if table_rotation == 'left':
|
||||
if self.debug:
|
||||
self.debug_text.extend([(t.x0, t.y0, t.x1, t.y1) for t in lttextlv])
|
||||
for t in t_bbox:
|
||||
x0, y0, x1, y1 = t.bbox
|
||||
x0, y0 = rotate(0, 0, x0, y0, -np.pi / 2)
|
||||
x1, y1 = rotate(0, 0, x1, y1, -np.pi / 2)
|
||||
t.set_bbox((x0, y1, x1, y0))
|
||||
elif table_rotation == 'right':
|
||||
for t in t_bbox:
|
||||
x0, y0, x1, y1 = t.bbox
|
||||
x0, y0 = rotate(0, 0, x0, y0, np.pi / 2)
|
||||
x1, y1 = rotate(0, 0, x1, y1, np.pi / 2)
|
||||
t.set_bbox((x1, y0, x0, y1))
|
||||
else:
|
||||
if self.debug:
|
||||
self.debug_text.extend([(t.x0, t.y0, t.x1, t.y1) for t in lttextlh])
|
||||
t_bbox = text_bbox(k, lttextlh)
|
||||
t_bbox.sort(key=lambda x: (-x.y0, x.x0))
|
||||
|
||||
text_x_min = min([t.x0 for t in t_bbox])
|
||||
text_y_min = min([t.y0 for t in t_bbox])
|
||||
text_x_max = max([t.x1 for t in t_bbox])
|
||||
text_y_max = max([t.y1 for t in t_bbox])
|
||||
rows_grouped = _group_rows(t_bbox, ytol=self.ytol[table_no])
|
||||
rows = _join_rows(rows_grouped, k[3], k[1])
|
||||
rows = _join_rows(rows_grouped, text_y_max, text_y_min)
|
||||
elements = [len(r) for r in rows_grouped]
|
||||
|
||||
guess = False
|
||||
@@ -275,8 +302,13 @@ class Stream:
|
||||
# similar to else condition
|
||||
# len can't be 1
|
||||
cols = self.columns[table_no].split(',')
|
||||
cols = [(float(cols[i]), float(cols[i + 1]))
|
||||
for i in range(0, len(cols) - 1)]
|
||||
cols = [float(c) for c in cols]
|
||||
if table_rotation != '':
|
||||
if table_rotation == 'left':
|
||||
cols = [rotate(0, 0, 0, c, -np.pi / 2)[0] for c in cols]
|
||||
elif table_rotation == 'right':
|
||||
cols = [rotate(0, 0, 0, c, np.pi / 2)[0] for c in cols]
|
||||
cols = [(cols[i], cols[i + 1]) for i in range(0, len(cols) - 1)]
|
||||
else:
|
||||
if self.ncolumns is not None and self.ncolumns[table_no] != -1:
|
||||
ncols = self.ncolumns[table_no]
|
||||
@@ -288,7 +320,7 @@ class Stream:
|
||||
" isn't the same as what you specified."
|
||||
" Change the value of mtol.".format(
|
||||
os.path.basename(bname)))
|
||||
cols = _join_columns(cols, k[0], k[2])
|
||||
cols = _join_columns(cols, text_x_min, text_x_max)
|
||||
else:
|
||||
guess = True
|
||||
ncols = max(set(elements), key=elements.count)
|
||||
@@ -310,7 +342,7 @@ class Stream:
|
||||
outer_text = [t for t in t_bbox if t.x0 > cols[-1][1] or t.x1 < cols[0][0]]
|
||||
inner_text.extend(outer_text)
|
||||
cols = _add_columns(cols, inner_text, self.ytol[table_no])
|
||||
cols = _join_columns(cols, k[0], k[2])
|
||||
cols = _join_columns(cols, text_x_min, text_x_max)
|
||||
|
||||
table = Table(cols, rows)
|
||||
rerror = []
|
||||
|
||||
+28
-18
@@ -11,7 +11,7 @@ from pdfminer.pdfinterp import PDFResourceManager
|
||||
from pdfminer.pdfinterp import PDFPageInterpreter
|
||||
from pdfminer.pdfdevice import PDFDevice
|
||||
from pdfminer.converter import PDFPageAggregator
|
||||
from pdfminer.layout import LAParams, LTChar, LTTextLineHorizontal
|
||||
from pdfminer.layout import LAParams, LTChar, LTTextLineHorizontal, LTTextLineVertical
|
||||
|
||||
|
||||
def translate(x1, x2):
|
||||
@@ -144,7 +144,7 @@ def scale_to_pdf(tables, v_segments, h_segments, factors):
|
||||
return tables_new, v_segments_new, h_segments_new
|
||||
|
||||
|
||||
def detect_vertical(text):
|
||||
def get_rotation(ltchar, lttextlh=None, lttextlv=None):
|
||||
"""Detects if text in table is vertical or not and returns
|
||||
its orientation.
|
||||
|
||||
@@ -156,13 +156,18 @@ def detect_vertical(text):
|
||||
-------
|
||||
rotation : string
|
||||
"""
|
||||
num_v = [t for t in text if (not t.upright) and t.get_text().strip()]
|
||||
num_h = [t for t in text if t.upright and t.get_text().strip()]
|
||||
vger = len(num_v) / float(len(num_v) + len(num_h))
|
||||
rotation = ''
|
||||
if vger > 0.8:
|
||||
clockwise = sum(t.matrix[1] < 0 and t.matrix[2] > 0 for t in text)
|
||||
anticlockwise = sum(t.matrix[1] > 0 and t.matrix[2] < 0 for t in text)
|
||||
if lttextlh is not None and lttextlv is not None:
|
||||
hlen = len([t for t in lttextlh if t.get_text().strip()])
|
||||
vlen = len([t for t in lttextlv if t.get_text().strip()])
|
||||
vger = 0.0
|
||||
else:
|
||||
hlen = len([t for t in ltchar if t.upright and t.get_text().strip()])
|
||||
vlen = len([t for t in ltchar if (not t.upright) and t.get_text().strip()])
|
||||
vger = vlen / float(hlen+vlen)
|
||||
if hlen < vlen or vger > 0.8:
|
||||
clockwise = sum(t.matrix[1] < 0 and t.matrix[2] > 0 for t in ltchar)
|
||||
anticlockwise = sum(t.matrix[1] > 0 and t.matrix[2] < 0 for t in ltchar)
|
||||
rotation = 'left' if clockwise < anticlockwise else 'right'
|
||||
return rotation
|
||||
|
||||
@@ -520,7 +525,7 @@ def encode_list(ar):
|
||||
return ar
|
||||
|
||||
|
||||
def extract_text_objects(layout, LTObject, t=None):
|
||||
def get_text_objects(layout, LTType="char", t=None):
|
||||
"""Recursively parses pdf layout to get a list of
|
||||
text objects.
|
||||
|
||||
@@ -539,6 +544,12 @@ def extract_text_objects(layout, LTObject, t=None):
|
||||
t : list
|
||||
List of text objects.
|
||||
"""
|
||||
if LTType == "char":
|
||||
LTObject = LTChar
|
||||
elif LTType == "lh":
|
||||
LTObject = LTTextLineHorizontal
|
||||
elif LTType == "lv":
|
||||
LTObject = LTTextLineVertical
|
||||
if t is None:
|
||||
t = []
|
||||
try:
|
||||
@@ -546,15 +557,14 @@ def extract_text_objects(layout, LTObject, t=None):
|
||||
if isinstance(obj, LTObject):
|
||||
t.append(obj)
|
||||
else:
|
||||
t += extract_text_objects(obj, LTObject)
|
||||
t += get_text_objects(obj, LTType=LTType)
|
||||
except AttributeError:
|
||||
pass
|
||||
return t
|
||||
|
||||
|
||||
def pdf_to_text(pname, char_margin, line_margin, word_margin):
|
||||
# pkey = 'page-{0}'.format(p)
|
||||
# pname = os.path.join(self.temp, '{}.pdf'.format(pkey))
|
||||
def get_page_layout(pname, char_margin=2.0, line_margin=0.5, word_margin=0.1,
|
||||
detect_vertical=True, all_texts=True):
|
||||
with open(pname, 'r') as f:
|
||||
parser = PDFParser(f)
|
||||
document = PDFDocument(parser)
|
||||
@@ -562,16 +572,16 @@ def pdf_to_text(pname, char_margin, line_margin, word_margin):
|
||||
raise PDFTextExtractionNotAllowed
|
||||
laparams = LAParams(char_margin=char_margin,
|
||||
line_margin=line_margin,
|
||||
word_margin=word_margin)
|
||||
word_margin=word_margin,
|
||||
detect_vertical=detect_vertical,
|
||||
all_texts=all_texts)
|
||||
rsrcmgr = PDFResourceManager()
|
||||
device = PDFPageAggregator(rsrcmgr, laparams=laparams)
|
||||
interpreter = PDFPageInterpreter(rsrcmgr, device)
|
||||
for page in PDFPage.create_pages(document):
|
||||
interpreter.process_page(page)
|
||||
layout = device.get_result()
|
||||
lattice_objects = extract_text_objects(layout, LTChar)
|
||||
stream_objects = extract_text_objects(
|
||||
layout, LTTextLineHorizontal)
|
||||
width = layout.bbox[2]
|
||||
height = layout.bbox[3]
|
||||
return lattice_objects, stream_objects, width, height
|
||||
dim = (width, height)
|
||||
return layout, dim
|
||||
Reference in New Issue
Block a user