layout analysis improved

git-svn-id: https://pdfminerr.googlecode.com/svn/trunk/pdfminer@268 1aa58f4a-7d42-0410-adbc-911cccaed67c
pull/1/head
yusuke.shinyama.dummy 2010-11-09 10:40:05 +00:00
parent edbd3764a7
commit 9584845358
3 changed files with 224 additions and 209 deletions

View File

@ -27,28 +27,26 @@ class PDFLayoutAnalyzer(PDFTextDevice):
(x0,y0) = apply_matrix_pt(ctm, (x0,y0)) (x0,y0) = apply_matrix_pt(ctm, (x0,y0))
(x1,y1) = apply_matrix_pt(ctm, (x1,y1)) (x1,y1) = apply_matrix_pt(ctm, (x1,y1))
mediabox = (0, 0, abs(x0-x1), abs(y0-y1)) mediabox = (0, 0, abs(x0-x1), abs(y0-y1))
self.cur_item = LTPage(self.pageno, mediabox) self.cur_item = LTPage(self.pageno, mediabox, laparams=self.laparams)
return return
def end_page(self, page): def end_page(self, page):
assert not self.stack assert not self.stack
assert isinstance(self.cur_item, LTPage) assert isinstance(self.cur_item, LTPage)
self.cur_item.fixate() self.cur_item.finish()
self.cur_item.analyze(self.laparams)
self.pageno += 1 self.pageno += 1
self.receive_layout(self.cur_item) self.receive_layout(self.cur_item)
return return
def begin_figure(self, name, bbox, matrix): def begin_figure(self, name, bbox, matrix):
self.stack.append(self.cur_item) self.stack.append(self.cur_item)
self.cur_item = LTFigure(name, bbox, mult_matrix(matrix, self.ctm)) self.cur_item = LTFigure(name, bbox, mult_matrix(matrix, self.ctm), laparams=self.laparams)
return return
def end_figure(self, _): def end_figure(self, _):
fig = self.cur_item fig = self.cur_item
assert isinstance(self.cur_item, LTFigure) assert isinstance(self.cur_item, LTFigure)
self.cur_item.fixate() self.cur_item.finish()
self.cur_item.analyze(self.laparams)
self.cur_item = self.stack.pop() self.cur_item = self.stack.pop()
self.cur_item.add(fig) self.cur_item.add(fig)
return return
@ -175,7 +173,7 @@ class TextConverter(PDFConverter):
for child in item: for child in item:
render(child) render(child)
elif isinstance(item, LTText): elif isinstance(item, LTText):
self.write(item.get_text()) self.write(item.text)
if isinstance(item, LTTextBox): if isinstance(item, LTTextBox):
self.write('\n') self.write('\n')
if self.showpageno: if self.showpageno:
@ -190,17 +188,17 @@ class TextConverter(PDFConverter):
class HTMLConverter(PDFConverter): class HTMLConverter(PDFConverter):
RECT_COLORS = { RECT_COLORS = {
'char': 'green', #'char': 'green',
'figure': 'yellow', #'figure': 'yellow',
'textline': 'magenta', #'textline': 'magenta',
'polygon': 'black',
'textbox': 'cyan', 'textbox': 'cyan',
'textgroup': 'red', 'textgroup': 'red',
'polygon': 'black',
'page': 'gray', 'page': 'gray',
} }
TEXT_COLORS = { TEXT_COLORS = {
'textbox': 'blue',
'char': 'black', 'char': 'black',
'textbox': 'black',
} }
def __init__(self, rsrcmgr, outfp, codec='utf-8', pageno=1, laparams=None, def __init__(self, rsrcmgr, outfp, codec='utf-8', pageno=1, laparams=None,
@ -275,7 +273,7 @@ class HTMLConverter(PDFConverter):
item.width*self.scale, item.height*self.scale)) item.width*self.scale, item.height*self.scale))
return return
render(ltpage) render(ltpage)
if self.debug and ltpage.layout: if ltpage.layout:
def show_layout(item): def show_layout(item):
if isinstance(item, LTTextGroup): if isinstance(item, LTTextGroup):
self.write_rect('textgroup', 1, item.x0, item.y1, item.width, item.height) self.write_rect('textgroup', 1, item.x0, item.y1, item.width, item.height)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
import sys import sys
from utils import apply_matrix_pt, get_bound, INF from utils import apply_matrix_pt, get_bound, INF
from utils import bsearch, bbox2str, matrix2str from utils import bsearch, bbox2str, matrix2str, Plane
from pdffont import PDFUnicodeNotDefined from pdffont import PDFUnicodeNotDefined
@ -55,8 +55,6 @@ class LTItem(object):
(self.__class__.__name__, bbox2str(self.bbox))) (self.__class__.__name__, bbox2str(self.bbox)))
def set_bbox(self, (x0,y0,x1,y1)): def set_bbox(self, (x0,y0,x1,y1)):
if x1 < x0: (x0,x1) = (x1,x0)
if y1 < y0: (y0,y1) = (y1,y0)
self.x0 = x0 self.x0 = x0
self.y0 = y0 self.y0 = y0
self.x1 = x1 self.x1 = x1
@ -169,10 +167,7 @@ class LTText(object):
def __repr__(self): def __repr__(self):
return ('<%s %r>' % return ('<%s %r>' %
(self.__class__.__name__, self.get_text())) (self.__class__.__name__, self.text))
def get_text(self):
return self.text
## LTAnon ## LTAnon
@ -222,9 +217,13 @@ class LTChar(LTItem, LTText):
bur = (self.adv, ty+height) bur = (self.adv, ty+height)
(a,b,c,d,e,f) = self.matrix (a,b,c,d,e,f) = self.matrix
self.upright = (0 < a*d*scaling and b*c <= 0) self.upright = (0 < a*d*scaling and b*c <= 0)
bbox = (apply_matrix_pt(self.matrix, bll) + (x0,y0) = apply_matrix_pt(self.matrix, bll)
apply_matrix_pt(self.matrix, bur)) (x1,y1) = apply_matrix_pt(self.matrix, bur)
LTItem.__init__(self, bbox) if x1 < x0:
(x0,x1) = (x1,x0)
if y1 < y0:
(y0,y1) = (y1,y0)
LTItem.__init__(self, (x0,y0,x1,y1))
if self.font.is_vertical(): if self.font.is_vertical():
self.size = self.width self.size = self.width
else: else:
@ -236,11 +235,12 @@ class LTChar(LTItem, LTText):
return ('<%s %s matrix=%s font=%r fontsize=%.1f adv=%s text=%r>' % return ('<%s %s matrix=%s font=%r fontsize=%.1f adv=%s text=%r>' %
(self.__class__.__name__, bbox2str(self.bbox), (self.__class__.__name__, bbox2str(self.bbox),
matrix2str(self.matrix), self.font, self.fontsize, matrix2str(self.matrix), self.font, self.fontsize,
self.adv, self.get_text())) self.adv, self.text))
else: else:
return '<char %r>' % self.text return '<char %r>' % self.text
def is_compatible(self, obj): def is_compatible(self, obj):
"""Returns True if two characters can coexist in the same line."""
return True return True
@ -248,81 +248,80 @@ class LTChar(LTItem, LTText):
## ##
class LTContainer(LTItem): class LTContainer(LTItem):
def __init__(self, objs=None, bbox=(0,0,0,0)): def __init__(self, bbox):
LTItem.__init__(self, bbox) LTItem.__init__(self, bbox)
if objs:
self._objs = objs[:]
else:
self._objs = [] self._objs = []
return return
def __iter__(self): def __iter__(self):
return iter(self.get_objs()) return iter(self._objs)
def __len__(self): def __len__(self):
return len(self.get_objs()) return len(self._objs)
def add(self, obj): def add(self, obj):
self._objs.append(obj) self._objs.append(obj)
return return
def merge(self, container): def extend(self, objs):
self._objs.extend(container._objs) for obj in objs:
self.add(obj)
return return
def get_objs(self):
return self._objs
# fixate(): determines its boundery. ## LTExpandableContainer
def fixate(self): ##
if not self.width and self._objs: class LTExpandableContainer(LTContainer):
(bx0, by0, bx1, by1) = (INF, INF, -INF, -INF)
for obj in self._objs: def __init__(self):
bx0 = min(bx0, obj.x0) LTContainer.__init__(self, (+INF,+INF,-INF,-INF))
by0 = min(by0, obj.y0)
bx1 = max(bx1, obj.x1)
by1 = max(by1, obj.y1)
self.set_bbox((bx0, by0, bx1, by1))
return return
def add(self, obj):
LTContainer.add(self, obj)
self.set_bbox((min(self.x0, obj.x0), min(self.y0, obj.y0),
max(self.x1, obj.x1), max(self.y1, obj.y1)))
return
def finish(self):
return self
## LTTextLine ## LTTextLine
## ##
class LTTextLine(LTContainer, LTText): class LTTextLine(LTExpandableContainer, LTText):
def __init__(self, laparams=None): def __init__(self, word_margin):
self.laparams = laparams LTExpandableContainer.__init__(self)
LTContainer.__init__(self) self.word_margin = word_margin
return return
def __repr__(self): def __repr__(self):
return ('<%s %s %r>' % return ('<%s %s %r>' %
(self.__class__.__name__, bbox2str(self.bbox), (self.__class__.__name__, bbox2str(self.bbox), self.text))
self.get_text()))
def get_text(self): def finish(self):
return ''.join( obj.text for obj in self.get_objs() if isinstance(obj, LTText) ) LTContainer.add(self, LTAnon('\n'))
self.text = ''.join( obj.text for obj in self if isinstance(obj, LTText) )
return LTExpandableContainer.finish(self)
def find_neighbors(self, plane, ratio): def find_neighbors(self, plane, ratio):
raise NotImplementedError raise NotImplementedError
class LTTextLineHorizontal(LTTextLine): class LTTextLineHorizontal(LTTextLine):
def get_objs(self): def __init__(self, word_margin):
if self.laparams is None: LTTextLine.__init__(self, word_margin)
for obj in self._objs: self._x1 = +INF
yield obj
return return
word_margin = self.laparams.word_margin
x1 = INF def add(self, obj):
for obj in csort(self._objs, key=lambda obj: obj.x0): if isinstance(obj, LTChar) and self.word_margin:
if isinstance(obj, LTChar) and word_margin: margin = self.word_margin * obj.width
margin = word_margin * obj.width if self._x1 < obj.x0-margin:
if x1 < obj.x0-margin: LTContainer.add(self, LTAnon(' '))
yield LTAnon(' ') self._x1 = obj.x1
yield obj LTTextLine.add(self, obj)
x1 = obj.x1
yield LTAnon('\n')
return return
def find_neighbors(self, plane, ratio): def find_neighbors(self, plane, ratio):
@ -332,21 +331,18 @@ class LTTextLineHorizontal(LTTextLine):
class LTTextLineVertical(LTTextLine): class LTTextLineVertical(LTTextLine):
def get_objs(self): def __init__(self, word_margin):
if self.laparams is None: LTTextLine.__init__(self, word_margin)
for obj in self._objs: self._y0 = -INF
yield obj
return return
word_margin = self.laparams.word_margin
y0 = -INF def add(self, obj):
for obj in csort(self._objs, key=lambda obj: -obj.y1): if isinstance(obj, LTChar) and self.word_margin:
if isinstance(obj, LTChar) and word_margin: margin = self.word_margin * obj.height
margin = word_margin * obj.height if obj.y1+margin < self._y0:
if obj.y1+margin < y0: LTContainer.add(self, LTAnon(' '))
yield LTAnon(' ') self._y0 = obj.y0
yield obj LTTextLine.add(self, obj)
y0 = obj.y0
yield LTAnon('\n')
return return
def find_neighbors(self, plane, ratio): def find_neighbors(self, plane, ratio):
@ -360,110 +356,84 @@ class LTTextLineVertical(LTTextLine):
## A set of text objects that are grouped within ## A set of text objects that are grouped within
## a certain rectangular area. ## a certain rectangular area.
## ##
class LTTextBox(LTContainer): class LTTextBox(LTExpandableContainer):
def __init__(self, objs): def __init__(self):
LTContainer.__init__(self, objs=objs) LTExpandableContainer.__init__(self)
self.index = None self.index = None
return return
def __repr__(self): def __repr__(self):
return ('<%s(%s) %s %r...>' % return ('<%s(%s) %s %r...>' %
(self.__class__.__name__, self.index, (self.__class__.__name__, self.index,
bbox2str(self.bbox), self.get_text()[:20])) bbox2str(self.bbox), self.text[:20]))
def get_text(self): def finish(self):
return ''.join( obj.get_text() for obj in self.get_objs() if isinstance(obj, LTTextLine) ) self.text = ''.join( obj.text for obj in self if isinstance(obj, LTTextLine) )
return LTExpandableContainer.finish(self)
class LTTextBoxHorizontal(LTTextBox): class LTTextBoxHorizontal(LTTextBox):
def get_objs(self): def finish(self):
return csort(self._objs, key=lambda obj: -obj.y1) self._objs = csort(self._objs, key=lambda obj: -obj.y1)
return LTTextBox.finish(self)
class LTTextBoxVertical(LTTextBox): class LTTextBoxVertical(LTTextBox):
def get_objs(self): def finish(self):
return csort(self._objs, key=lambda obj: -obj.x1) self._objs = csort(self._objs, key=lambda obj: -obj.x1)
return LTTextBox.finish(self)
## LTTextGroup ## LTTextGroup
## ##
class LTTextGroup(LTContainer): class LTTextGroup(LTExpandableContainer):
def __init__(self, objs): def __init__(self, objs):
LTContainer.__init__(self, objs=objs) LTExpandableContainer.__init__(self)
LTContainer.fixate(self) self.extend(objs)
return return
class LTTextGroupLRTB(LTTextGroup): class LTTextGroupLRTB(LTTextGroup):
def get_objs(self): def finish(self):
# reorder the objects from top-left to bottom-right. # reorder the objects from top-left to bottom-right.
return csort(self._objs, key=lambda obj: obj.x0+obj.x1-(obj.y0+obj.y1)) self._objs = csort(self._objs, key=lambda obj: obj.x0+obj.x1-(obj.y0+obj.y1))
return LTTextGroup.finish(self)
class LTTextGroupTBRL(LTTextGroup): class LTTextGroupTBRL(LTTextGroup):
def get_objs(self): def finish(self):
# reorder the objects from top-right to bottom-left. # reorder the objects from top-right to bottom-left.
return csort(self._objs, key=lambda obj: -(obj.x0+obj.x1)-(obj.y0+obj.y1)) self._objs = csort(self._objs, key=lambda obj: -(obj.x0+obj.x1)-(obj.y0+obj.y1))
return LTTextGroup.finish(self)
## Plane ## LTLayoutContainer
## ##
## A data structure for objects placed on a plane. class LTLayoutContainer(LTContainer):
## Can efficiently find objects in a certain rectangular area.
## It maintains two parallel lists of objects, each of
## which is sorted by its x or y coordinate.
##
class Plane(object):
def __init__(self, objs): def __init__(self, bbox, laparams=None):
self.xobjs = [] LTContainer.__init__(self, bbox)
self.yobjs = [] self.laparams = laparams
self.idxs = dict( (obj,i) for (i,obj) in enumerate(objs) ) self.layout = None
for obj in objs:
self.place(obj)
self.xobjs.sort()
self.yobjs.sort()
return return
# place(obj): place an object in a certain area. def finish(self):
def place(self, obj):
assert isinstance(obj, LTItem)
self.xobjs.append((obj.x0, obj))
self.xobjs.append((obj.x1, obj))
self.yobjs.append((obj.y0, obj))
self.yobjs.append((obj.y1, obj))
return
# find(): finds objects that are in a certain area.
def find(self, (x0,y0,x1,y1)):
i0 = bsearch(self.xobjs, x0)[0]
i1 = bsearch(self.xobjs, x1)[1]
xobjs = set( obj for (_,obj) in self.xobjs[i0:i1] )
i0 = bsearch(self.yobjs, y0)[0]
i1 = bsearch(self.yobjs, y1)[1]
yobjs = set( obj for (_,obj) in self.yobjs[i0:i1] )
xobjs.intersection_update(yobjs)
return sorted(xobjs, key=lambda obj: self.idxs[obj])
## LTAnalyzer
##
class LTAnalyzer(LTContainer):
def analyze(self, laparams=None):
"""Perform the layout analysis.""" """Perform the layout analysis."""
if laparams is None: return if self.laparams is None: return
# textobjs is a list of LTChar objects, i.e. # textobjs is a list of LTChar objects, i.e.
# it has all the individual characters in the page. # it has all the individual characters in the page.
(textobjs, otherobjs) = self.get_textobjs(self._objs, laparams) (textobjs, otherobjs) = self.get_textobjs(self._objs)
if not textobjs: return if not textobjs: return
textlines = list(self.get_textlines(textobjs, laparams)) textlines = list(self.get_textlines(textobjs,
assert sum( len(line._objs) for line in textlines ) == len(textobjs) self.laparams.line_overlap,
textboxes = list(self.get_textboxes(textlines, laparams)) self.laparams.char_margin,
assert sum( len(box._objs) for box in textboxes ) == len(textlines) self.laparams.word_margin))
top = self.group_textboxes(textboxes, laparams) assert len(textobjs) <= sum( len(line._objs) for line in textlines )
textboxes = list(self.get_textboxes(textlines, self.laparams.line_margin))
assert len(textlines) == sum( len(box._objs) for box in textboxes )
top = self.group_textboxes(textboxes)
def assign_index(obj, i): def assign_index(obj, i):
if isinstance(obj, LTTextBox): if isinstance(obj, LTTextBox):
obj.index = i obj.index = i
@ -476,9 +446,9 @@ class LTAnalyzer(LTContainer):
textboxes.sort(key=lambda box:box.index) textboxes.sort(key=lambda box:box.index)
self._objs = textboxes + otherobjs self._objs = textboxes + otherobjs
self.layout = top self.layout = top
return return self
def get_textobjs(self, objs, laparams): def get_textobjs(self, objs):
"""Split all the objects in the page into text-related objects and others.""" """Split all the objects in the page into text-related objects and others."""
textobjs = [] textobjs = []
otherobjs = [] otherobjs = []
@ -489,15 +459,15 @@ class LTAnalyzer(LTContainer):
otherobjs.append(obj) otherobjs.append(obj)
return (textobjs, otherobjs) return (textobjs, otherobjs)
def get_textlines(self, objs, laparams): def get_textlines(self, objs, line_overlap, char_margin, word_margin):
obj0 = None obj0 = None
line = None line = None
for obj1 in objs: for obj1 in objs:
if obj0 is not None: if obj0 is not None:
k = 0 k = 0
if (obj0.is_compatible(obj1) and obj0.is_voverlap(obj1) and if (obj0.is_compatible(obj1) and obj0.is_voverlap(obj1) and
min(obj0.height, obj1.height) * laparams.line_overlap < obj0.voverlap(obj1) and min(obj0.height, obj1.height) * line_overlap < obj0.voverlap(obj1) and
obj0.hdistance(obj1) < min(obj0.width, obj1.width) * laparams.char_margin): obj0.hdistance(obj1) < max(obj0.width, obj1.width) * char_margin):
# obj0 and obj1 is horizontally aligned: # obj0 and obj1 is horizontally aligned:
# #
# +------+ - - - # +------+ - - -
@ -510,8 +480,8 @@ class LTAnalyzer(LTContainer):
# (char_margin) # (char_margin)
k |= 1 k |= 1
if (obj0.is_compatible(obj1) and obj0.is_hoverlap(obj1) and if (obj0.is_compatible(obj1) and obj0.is_hoverlap(obj1) and
min(obj0.width, obj1.width) * laparams.line_overlap < obj0.hoverlap(obj1) and min(obj0.width, obj1.width) * line_overlap < obj0.hoverlap(obj1) and
obj0.vdistance(obj1) < min(obj0.height, obj1.height) * laparams.char_margin): obj0.vdistance(obj1) < max(obj0.height, obj1.height) * char_margin):
# obj0 and obj1 is vertically aligned: # obj0 and obj1 is vertically aligned:
# #
# +------+ # +------+
@ -531,59 +501,59 @@ class LTAnalyzer(LTContainer):
(k & 2 and isinstance(line, LTTextLineVertical)) ): (k & 2 and isinstance(line, LTTextLineVertical)) ):
line.add(obj1) line.add(obj1)
elif line is not None: elif line is not None:
line.fixate() yield line.finish()
yield line
line = None line = None
else: else:
if k == 2: if k == 2:
line = LTTextLineVertical(laparams) line = LTTextLineVertical(word_margin)
line.add(obj0) line.add(obj0)
line.add(obj1) line.add(obj1)
elif k == 1: elif k == 1:
line = LTTextLineHorizontal(laparams) line = LTTextLineHorizontal(word_margin)
line.add(obj0) line.add(obj0)
line.add(obj1) line.add(obj1)
else: else:
line = LTTextLineHorizontal(laparams) line = LTTextLineHorizontal(word_margin)
line.add(obj0) line.add(obj0)
line.fixate() yield line.finish()
yield line
line = None line = None
obj0 = obj1 obj0 = obj1
if line is None: if line is None:
line = LTTextLineHorizontal(laparams) line = LTTextLineHorizontal(word_margin)
line.add(obj0) line.add(obj0)
line.fixate() yield line.finish()
yield line
return return
def get_textboxes(self, lines, laparams): def get_textboxes(self, lines, line_margin):
plane = Plane(lines) plane = Plane(lines)
groups = {}
for line in lines: for line in lines:
neighbors = line.find_neighbors(plane, laparams.line_margin) plane.add(line)
plane.finish()
boxes = {}
for line in lines:
neighbors = line.find_neighbors(plane, line_margin)
assert line in neighbors, line assert line in neighbors, line
members = neighbors[:] members = []
for obj1 in neighbors: for obj1 in neighbors:
if obj1 in groups: members.append(obj1)
members.extend(groups.pop(obj1)) if obj1 in boxes:
members = list(uniq(members)) members.extend(boxes.pop(obj1))
if isinstance(line, LTTextLineHorizontal): if isinstance(line, LTTextLineHorizontal):
group = LTTextBoxHorizontal(members) box = LTTextBoxHorizontal()
else: else:
group = LTTextBoxVertical(members) box = LTTextBoxVertical()
for obj in members: for obj in uniq(members):
groups[obj] = group box.add(obj)
boxes[obj] = box
done = set() done = set()
for line in lines: for line in lines:
group = groups[line] box = boxes[line]
if group in done: continue if box in done: continue
done.add(group) done.add(box)
group.fixate() yield box.finish()
yield group
return return
def group_textboxes(self, textboxes, laparams): def group_textboxes(self, boxes):
def dist(obj1, obj2): def dist(obj1, obj2):
"""A distance function between two TextBoxes. """A distance function between two TextBoxes.
@ -599,43 +569,44 @@ class LTAnalyzer(LTContainer):
return ((max(obj1.x1,obj2.x1) - min(obj1.x0,obj2.x0)) * return ((max(obj1.x1,obj2.x1) - min(obj1.x0,obj2.x0)) *
(max(obj1.y1,obj2.y1) - min(obj1.y0,obj2.y0)) - (max(obj1.y1,obj2.y1) - min(obj1.y0,obj2.y0)) -
(obj1.width*obj1.height + obj2.width*obj2.height)) (obj1.width*obj1.height + obj2.width*obj2.height))
textboxes = textboxes[:] boxes = boxes[:]
# XXX this is slow when there're many textboxes. # XXX this is slow when there're many textboxes.
while 2 <= len(textboxes): while 2 <= len(boxes):
mindist = INF mindist = INF
minpair = None minpair = None
textboxes = csort(textboxes, key=lambda obj: obj.width*obj.height) boxes = csort(boxes, key=lambda obj: obj.width*obj.height)
for i in xrange(len(textboxes)): for i in xrange(len(boxes)):
for j in xrange(i+1, len(textboxes)): for j in xrange(i+1, len(boxes)):
(obj1, obj2) = (textboxes[i], textboxes[j]) (obj1, obj2) = (boxes[i], boxes[j])
d = dist(obj1, obj2) d = dist(obj1, obj2)
if d < mindist: if d < mindist:
mindist = d mindist = d
minpair = (obj1, obj2) minpair = (obj1, obj2)
assert minpair assert minpair
(obj1, obj2) = minpair (obj1, obj2) = minpair
textboxes.remove(obj1) boxes.remove(obj1)
textboxes.remove(obj2) boxes.remove(obj2)
if isinstance(obj1, LTTextBoxHorizontal): if (isinstance(obj1, LTTextBoxVertical) or
group = LTTextGroupLRTB([obj1, obj2]) isinstance(obj1, LTTextGroupTBRL)):
else:
group = LTTextGroupTBRL([obj1, obj2]) group = LTTextGroupTBRL([obj1, obj2])
textboxes.append(group) else:
assert len(textboxes) == 1 group = LTTextGroupLRTB([obj1, obj2])
return textboxes.pop() boxes.append(group.finish())
assert len(boxes) == 1
return boxes.pop()
## LTFigure ## LTFigure
## ##
class LTFigure(LTAnalyzer): class LTFigure(LTLayoutContainer):
def __init__(self, name, bbox, matrix): def __init__(self, name, bbox, matrix, laparams=None):
self.name = name self.name = name
self.matrix = matrix self.matrix = matrix
(x,y,w,h) = bbox (x,y,w,h) = bbox
bbox = get_bound( apply_matrix_pt(matrix, (p,q)) bbox = get_bound( apply_matrix_pt(matrix, (p,q))
for (p,q) in ((x,y), (x+w,y), (x,y+h), (x+w,y+h)) ) for (p,q) in ((x,y), (x+w,y), (x,y+h), (x+w,y+h)) )
LTAnalyzer.__init__(self, bbox=bbox) LTLayoutContainer.__init__(self, bbox, laparams=laparams)
return return
def __repr__(self): def __repr__(self):
@ -643,21 +614,19 @@ class LTFigure(LTAnalyzer):
(self.__class__.__name__, self.name, (self.__class__.__name__, self.name,
bbox2str(self.bbox), matrix2str(self.matrix))) bbox2str(self.bbox), matrix2str(self.matrix)))
def analyze(self, laparams=None): def finish(self):
if laparams is not None and laparams.all_texts: if self.laparams is None or not self.laparams.all_texts: return
LTAnalyzer.analyze(self, laparams=laparams) return LTLayoutContainer.finish(self)
return
## LTPage ## LTPage
## ##
class LTPage(LTAnalyzer): class LTPage(LTLayoutContainer):
def __init__(self, pageid, bbox, rotate=0): def __init__(self, pageid, bbox, rotate=0, laparams=None):
LTAnalyzer.__init__(self, bbox=bbox) LTLayoutContainer.__init__(self, bbox, laparams=laparams)
self.pageid = pageid self.pageid = pageid
self.rotate = rotate self.rotate = rotate
self.layout = None
return return
def __repr__(self): def __repr__(self):

View File

@ -182,6 +182,54 @@ class ObjIdRange(object):
return self.nobjs return self.nobjs
## Plane
##
## A data structure for objects placed on a plane.
## Can efficiently find objects in a certain rectangular area.
## It maintains two parallel lists of objects, each of
## which is sorted by its x or y coordinate.
##
class Plane(object):
def __init__(self, objs):
self._idxs = {}
self._xobjs = []
self._yobjs = []
return
def __repr__(self):
return ('<Plane objs=%r>' % list(self))
def __iter__(self):
return self._idxs.iterkeys()
# add(obj): place an object in a certain area.
def add(self, obj):
self._idxs[obj] = len(self._idxs)
self._xobjs.append((obj.x0, obj))
self._xobjs.append((obj.x1, obj))
self._yobjs.append((obj.y0, obj))
self._yobjs.append((obj.y1, obj))
return
# finish()
def finish(self):
self._xobjs.sort()
self._yobjs.sort()
return
# find(): finds objects that are in a certain area.
def find(self, (x0,y0,x1,y1)):
i0 = bsearch(self._xobjs, x0)[0]
i1 = bsearch(self._xobjs, x1)[1]
xobjs = set( obj for (_,obj) in self._xobjs[i0:i1] )
i0 = bsearch(self._yobjs, y0)[0]
i1 = bsearch(self._yobjs, y1)[1]
yobjs = set( obj for (_,obj) in self._yobjs[i0:i1] )
xobjs.intersection_update(yobjs)
return sorted(xobjs, key=lambda obj: self._idxs[obj])
# create_bmp # create_bmp
def create_bmp(data, bits, width, height): def create_bmp(data, bits, width, height):
info = pack('<IiiHHIIIIII', 40, width, height, 1, bits, 0, len(data), 0, 0, 0, 0) info = pack('<IiiHHIIIIII', 40, width, height, 1, bits, 0, len(data), 0, 0, 0, 0)