layout analysis improved
git-svn-id: https://pdfminerr.googlecode.com/svn/trunk/pdfminer@268 1aa58f4a-7d42-0410-adbc-911cccaed67cpull/1/head
parent
edbd3764a7
commit
9584845358
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue