pdfinfo: further layout improvements

Rather than grouping visible/invisible in a custom analysis step,
use pdfminer's analysis and iterate.
Make iteration predicate and return more generic.
This commit is contained in:
James R. Barlow
2018-10-28 14:05:50 -07:00
parent e6d64be890
commit fda890ab47
2 changed files with 46 additions and 57 deletions
+18 -13
View File
@@ -28,7 +28,7 @@ import xml.etree.ElementTree as ET
from pikepdf import PdfMatrix
import pikepdf
from .layout import get_textblocks, bboxes
from .layout import get_textblocks, filter_textboxes, textbox_predicate
from ..exec import ghostscript
from ..helpers import fspath
@@ -567,18 +567,19 @@ def _page_has_text(text_blocks, page_width, page_height):
margin_ratio = 0.125
interior_bbox = (
margin_ratio * pw, margin_ratio * ph,
(1 - margin_ratio) * pw, (1 - margin_ratio) * ph
margin_ratio * pw, # left
(1 - margin_ratio) * ph, # top
(1 - margin_ratio) * pw, # right
margin_ratio * ph # bottom (first quadrant: bottom < top)
)
def rects_intersect(a, b):
"""
Where (a,b) are 4-tuple rects (left-0, top-1, right-2, bottom-3)
https://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other
Negative signs to account for our coordinates being in the fourth quadrant
and the formula assuming the first
Formula assumes all boxes are in first quadrant
"""
return a[0] < b[2] and a[2] > b[0] and -a[1] > -b[3] and -a[3] < -b[1]
return a[0] < b[2] and a[2] > b[0] and a[1] > b[3] and a[3] < b[1]
has_text = False
for bbox in text_blocks:
@@ -605,8 +606,12 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
width_pt = mediabox[2] - mediabox[0]
height_pt = mediabox[3] - mediabox[1]
bboxes = (textbox.bbox for textbox in filter_textboxes(
pageinfo['objects'], lambda obj: True)
)
pageinfo['has_text'] = _page_has_text(
bboxes(pageinfo['objects']), width_pt, height_pt)
bboxes, width_pt, height_pt
)
userunit = page.get('/UserUnit', Decimal(1.0))
if not isinstance(userunit, Decimal):
@@ -629,7 +634,7 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
if any(isinstance(ci, VectorInfo) for ci in contentsinfo):
pageinfo['has_vector'] = True
textinfos = [ti for ti in contentsinfo if isinstance(ti, TextInfo)]
textinfos = (ti for ti in contentsinfo if isinstance(ti, TextInfo))
all_invisible = not any(ti.visible for ti in textinfos)
pageinfo['only_ocr_text'] = all_invisible
@@ -736,11 +741,11 @@ class PageInfo:
def images(self):
return self._pageinfo['images']
def get_textareas(self, visible=True, invisible=True):
if visible:
yield from bboxes(self._pageinfo['objects'][0])
if invisible:
yield from bboxes(self._pageinfo['objects'][1])
def get_textareas(self, visible=None, corrupt=None):
return (obj.bbox for obj in filter_textboxes(
self._pageinfo['objects'],
textbox_predicate(visible=visible, corrupt=corrupt)
))
@property
def xres(self):
+28 -44
View File
@@ -96,43 +96,6 @@ class LTStateAwareChar(LTChar):
self.get_text()))
class LTStateAwarePage(LTPage):
"""A page container that exploits character type information"""
def __init__(self, pageid, bbox, rotate=0):
LTPage.__init__(self, pageid, bbox, rotate)
def analyze(self, laparams):
"""Analysis taking rendering mode into account
Looks at visible and invisible characters separately.
Depends on some superclass implementation details, largely because only
LTPage has the "group into textboxes" code, so we have to manipulate
our _objs to create multiple collections.
"""
objs = self._objs[:]
# Split into invisible text objects and all others
(invisible_textobjs, other_objs) = fsplit(
lambda obj: getattr(obj, 'rendermode', 0) == 3, self)
# Analyze all invisible text objects and group them into text lines and
# text boxes
self._objs = invisible_textobjs
LTPage.analyze(self, laparams)
invisible_analyzed = self._objs[:]
# Analyze all other objects
self._objs = other_objs
LTPage.analyze(self, laparams)
other_analyzed = self._objs[:]
self._objs = invisible_analyzed + other_analyzed
self.visible = other_analyzed
self.invisible = invisible_analyzed
class TextPositionTracker(PDFLayoutAnalyzer):
"""A page layout analyzer that pays attention to text visibility"""
@@ -143,7 +106,7 @@ class TextPositionTracker(PDFLayoutAnalyzer):
def begin_page(self, page, ctm):
super().begin_page(page, ctm)
self.cur_item = LTStateAwarePage(self.pageno, page.mediabox)
self.cur_item = LTPage(self.pageno, page.mediabox)
def end_page(self, page):
assert not self._stack, str(len(self._stack))
@@ -176,7 +139,7 @@ class TextPositionTracker(PDFLayoutAnalyzer):
return (font, cid)
def receive_layout(self, ltpage):
self.result = (ltpage.visible, ltpage.invisible)
self.result = ltpage
def get_result(self):
return self.result
@@ -194,12 +157,33 @@ def get_textblocks(infile, pageno):
return dev.get_result()
def bboxes(hierarchical_textinfo):
for obj in hierarchical_textinfo:
if isinstance(hierarchical_textinfo, (LTTextBox)):
yield hierarchical_textinfo.bbox
def textbox_predicate(*, visible, corrupt):
def real_predicate(textbox, want_visible=visible, want_corrupt=corrupt):
textline = textbox._objs[0]
first_char = textline._objs[0]
result = True
is_visible = (first_char.rendermode != 3)
if want_visible is not None:
if is_visible != want_visible:
result = False
is_corrupt = (first_char.get_text() == '\ufffd')
if want_corrupt is not None:
if is_corrupt != want_corrupt:
result = False
return result
return real_predicate
def filter_textboxes(obj, predicate):
for child in obj:
if isinstance(child, (LTTextBox)):
if predicate(child):
yield child
else:
try:
yield from bboxes(obj)
yield from filter_textboxes(child, predicate)
except TypeError:
continue