2009-03-28 17:23:53 +00:00
|
|
|
#!/usr/bin/env python
|
|
|
|
import sys
|
2009-11-03 01:27:30 +00:00
|
|
|
from sys import maxint as INF
|
2010-01-31 02:09:28 +00:00
|
|
|
from utils import apply_matrix_norm, apply_matrix_pt
|
2010-02-27 03:59:25 +00:00
|
|
|
from utils import bsearch, bbox2str, matrix2str
|
|
|
|
from pdffont import PDFUnicodeNotDefined
|
2009-03-28 17:23:53 +00:00
|
|
|
|
|
|
|
|
2009-12-20 02:38:01 +00:00
|
|
|
|
|
|
|
## get_bounds
|
|
|
|
##
|
|
|
|
def get_bounds(pts):
|
|
|
|
"""Compute a maximal rectangle that covers all the points."""
|
|
|
|
(x0, y0, x1, y1) = (INF, INF, -INF, -INF)
|
|
|
|
for (x,y) in pts:
|
|
|
|
x0 = min(x0, x)
|
|
|
|
y0 = min(y0, y)
|
|
|
|
x1 = max(x1, x)
|
|
|
|
y1 = max(y1, y)
|
|
|
|
return (x0,y0,x1,y1)
|
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
def uniq(objs):
|
|
|
|
done = set()
|
|
|
|
for obj in objs:
|
|
|
|
if obj in done: continue
|
|
|
|
done.add(obj)
|
|
|
|
yield obj
|
|
|
|
return
|
|
|
|
|
2010-03-22 06:04:54 +00:00
|
|
|
def csort(objs, key):
|
|
|
|
idxs = dict( (obj,i) for (i,obj) in enumerate(objs) )
|
|
|
|
return sorted(objs, key=lambda obj:(key(obj), idxs[obj]))
|
|
|
|
|
|
|
|
def is_uniq(objs):
|
|
|
|
for (i,obj1) in enumerate(objs):
|
|
|
|
for obj2 in objs[i+1:]:
|
|
|
|
if obj1 == obj2: return False
|
|
|
|
return True
|
|
|
|
|
2009-12-20 02:38:01 +00:00
|
|
|
|
2009-07-21 07:55:19 +00:00
|
|
|
## LAParams
|
2009-05-04 08:29:36 +00:00
|
|
|
##
|
2009-07-21 07:55:19 +00:00
|
|
|
class LAParams(object):
|
2009-10-24 04:41:59 +00:00
|
|
|
|
|
|
|
def __init__(self,
|
2010-03-25 11:38:47 +00:00
|
|
|
writing_mode='lr-tb',
|
2009-10-24 04:41:59 +00:00
|
|
|
line_overlap=0.5,
|
2010-03-20 05:43:34 +00:00
|
|
|
char_margin=3.0,
|
2009-10-24 04:41:59 +00:00
|
|
|
line_margin=0.5,
|
|
|
|
word_margin=0.1):
|
2010-03-25 11:38:47 +00:00
|
|
|
self.writing_mode = writing_mode
|
2009-10-24 04:41:59 +00:00
|
|
|
self.line_overlap = line_overlap
|
|
|
|
self.char_margin = char_margin
|
|
|
|
self.line_margin = line_margin
|
|
|
|
self.word_margin = word_margin
|
|
|
|
return
|
|
|
|
|
|
|
|
def __repr__(self):
|
2010-03-25 11:38:47 +00:00
|
|
|
return ('<LAParams: writing_mode=%r, char_margin=%.1f, line_margin=%.1f, word_margin=%.1f>' %
|
|
|
|
(self.writing_mode, self.char_margin, self.line_margin, self.word_margin))
|
2009-05-04 08:29:36 +00:00
|
|
|
|
|
|
|
|
2009-05-05 12:26:29 +00:00
|
|
|
## LayoutItem
|
|
|
|
##
|
|
|
|
class LayoutItem(object):
|
|
|
|
|
2009-10-24 04:41:59 +00:00
|
|
|
def __init__(self, bbox):
|
|
|
|
self.set_bbox(bbox)
|
|
|
|
return
|
|
|
|
|
2009-11-06 15:06:59 +00:00
|
|
|
def __repr__(self):
|
2010-02-27 03:59:25 +00:00
|
|
|
return ('<item bbox=%s>' % bbox2str(self.bbox))
|
2009-11-06 15:06:59 +00:00
|
|
|
|
2009-10-24 04:41:59 +00:00
|
|
|
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.y0 = y0
|
|
|
|
self.x1 = x1
|
|
|
|
self.y1 = y1
|
|
|
|
self.width = x1-x0
|
|
|
|
self.height = y1-y0
|
2010-01-31 02:09:28 +00:00
|
|
|
self.bbox = (x0, y0, x1, y1)
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
|
|
|
|
2009-11-14 11:29:40 +00:00
|
|
|
def is_hoverlap(self, obj):
|
|
|
|
assert isinstance(obj, LayoutItem)
|
2010-03-20 05:43:34 +00:00
|
|
|
return obj.x0 <= self.x1 and self.x0 <= obj.x1
|
2009-11-14 11:29:40 +00:00
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
def hdistance(self, obj):
|
2009-10-24 04:41:59 +00:00
|
|
|
assert isinstance(obj, LayoutItem)
|
2010-03-20 05:43:34 +00:00
|
|
|
if self.is_hoverlap(obj):
|
2009-10-24 04:41:59 +00:00
|
|
|
return 0
|
|
|
|
else:
|
|
|
|
return min(abs(self.x0-obj.x1), abs(self.x1-obj.x0))
|
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
def hoverlap(self, obj):
|
2009-11-14 11:29:40 +00:00
|
|
|
assert isinstance(obj, LayoutItem)
|
2010-03-20 05:43:34 +00:00
|
|
|
if self.is_hoverlap(obj):
|
|
|
|
return min(abs(self.x0-obj.x1), abs(self.x1-obj.x0))
|
2009-11-14 11:29:40 +00:00
|
|
|
else:
|
2010-03-20 05:43:34 +00:00
|
|
|
return 0
|
2009-11-14 11:29:40 +00:00
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
def is_voverlap(self, obj):
|
|
|
|
assert isinstance(obj, LayoutItem)
|
|
|
|
return obj.y0 <= self.y1 and self.y0 <= obj.y1
|
|
|
|
|
|
|
|
def vdistance(self, obj):
|
2009-10-24 04:41:59 +00:00
|
|
|
assert isinstance(obj, LayoutItem)
|
2010-03-20 05:43:34 +00:00
|
|
|
if self.is_voverlap(obj):
|
2009-10-24 04:41:59 +00:00
|
|
|
return 0
|
|
|
|
else:
|
|
|
|
return min(abs(self.y0-obj.y1), abs(self.y1-obj.y0))
|
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
def voverlap(self, obj):
|
|
|
|
assert isinstance(obj, LayoutItem)
|
|
|
|
if self.is_voverlap(obj):
|
|
|
|
return min(abs(self.y0-obj.y1), abs(self.y1-obj.y0))
|
|
|
|
else:
|
|
|
|
return 0
|
|
|
|
|
2009-06-20 10:44:00 +00:00
|
|
|
|
2009-05-05 12:26:29 +00:00
|
|
|
## LayoutContainer
|
|
|
|
##
|
|
|
|
class LayoutContainer(LayoutItem):
|
2009-10-24 04:41:59 +00:00
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
def __init__(self, bbox, objs=None):
|
2009-10-24 04:41:59 +00:00
|
|
|
LayoutItem.__init__(self, bbox)
|
|
|
|
if objs:
|
2010-03-20 05:43:34 +00:00
|
|
|
self.objs = objs[:]
|
2009-10-24 04:41:59 +00:00
|
|
|
else:
|
2010-03-20 05:43:34 +00:00
|
|
|
self.objs = []
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
def __repr__(self):
|
2010-03-20 05:43:34 +00:00
|
|
|
return ('<container %s>' % bbox2str(self.bbox))
|
2009-10-24 04:41:59 +00:00
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
return iter(self.objs)
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
return len(self.objs)
|
|
|
|
|
|
|
|
def add(self, obj):
|
2010-03-20 05:43:34 +00:00
|
|
|
self.objs.append(obj)
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
def merge(self, container):
|
|
|
|
self.objs.extend(container.objs)
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
|
|
|
|
2010-03-25 11:38:47 +00:00
|
|
|
# fixate(): determines its boundery.
|
2009-10-24 04:41:59 +00:00
|
|
|
def fixate(self):
|
|
|
|
if not self.width and self.objs:
|
|
|
|
(bx0, by0, bx1, by1) = (INF, INF, -INF, -INF)
|
|
|
|
for obj in self.objs:
|
|
|
|
bx0 = min(bx0, obj.x0)
|
|
|
|
by0 = min(by0, obj.y0)
|
|
|
|
bx1 = max(bx1, obj.x1)
|
|
|
|
by1 = max(by1, obj.y1)
|
|
|
|
self.set_bbox((bx0, by0, bx1, by1))
|
|
|
|
return
|
|
|
|
|
2009-05-05 12:26:29 +00:00
|
|
|
|
2009-12-20 02:38:01 +00:00
|
|
|
## LTPolygon
|
2009-05-15 14:25:32 +00:00
|
|
|
##
|
2009-12-20 02:38:01 +00:00
|
|
|
class LTPolygon(LayoutItem):
|
2009-05-15 14:25:32 +00:00
|
|
|
|
2009-12-20 02:38:01 +00:00
|
|
|
def __init__(self, linewidth, pts):
|
|
|
|
LayoutItem.__init__(self, get_bounds(pts))
|
|
|
|
self.pts = pts
|
2009-10-24 04:41:59 +00:00
|
|
|
self.linewidth = linewidth
|
2009-12-20 02:38:01 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
def get_pts(self):
|
|
|
|
return ','.join( '%.3f,%.3f' % p for p in self.pts )
|
|
|
|
|
|
|
|
|
|
|
|
## LTLine
|
|
|
|
##
|
|
|
|
class LTLine(LTPolygon):
|
|
|
|
|
|
|
|
def __init__(self, linewidth, p0, p1):
|
|
|
|
LTPolygon.__init__(self, linewidth, [p0, p1])
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
2009-05-15 14:25:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
## LTRect
|
2009-05-05 12:26:29 +00:00
|
|
|
##
|
2009-12-20 02:38:01 +00:00
|
|
|
class LTRect(LTPolygon):
|
2009-05-15 14:25:32 +00:00
|
|
|
|
2009-12-20 02:38:01 +00:00
|
|
|
def __init__(self, linewidth, (x0,y0,x1,y1)):
|
|
|
|
LTPolygon.__init__(self, linewidth, [(x0,y0), (x1,y0), (x1,y1), (x0,y1)])
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
|
|
|
|
2009-05-15 14:25:32 +00:00
|
|
|
|
2010-01-30 07:30:01 +00:00
|
|
|
## LTImage
|
|
|
|
##
|
2010-01-31 02:09:28 +00:00
|
|
|
class LTImage(LayoutItem):
|
2010-01-30 07:30:01 +00:00
|
|
|
|
2010-04-10 11:05:02 +00:00
|
|
|
def __init__(self, name, stream, bbox):
|
2010-01-31 02:09:28 +00:00
|
|
|
LayoutItem.__init__(self, bbox)
|
2010-01-30 07:30:01 +00:00
|
|
|
self.name = name
|
2010-04-10 11:05:02 +00:00
|
|
|
self.stream = stream
|
|
|
|
self.srcsize = (stream.get_any(('W', 'Width')),
|
|
|
|
stream.get_any(('H', 'Height')))
|
|
|
|
self.imagemask = stream.get_any(('IM', 'ImageMask'))
|
|
|
|
self.bits = stream.get_any(('BPC', 'BitsPerComponent'), 1)
|
|
|
|
self.colorspace = stream.get_any(('CS', 'ColorSpace'))
|
|
|
|
if not isinstance(self.colorspace, list):
|
|
|
|
self.colorspace = [colorspace]
|
2010-01-30 07:30:01 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
(w,h) = self.srcsize
|
2010-04-10 11:05:02 +00:00
|
|
|
return '<image %s %dx%d>' % (self.name, w, h)
|
2010-01-30 07:30:01 +00:00
|
|
|
|
|
|
|
|
2009-07-21 07:55:19 +00:00
|
|
|
## LTText
|
2009-06-20 10:44:00 +00:00
|
|
|
##
|
2009-07-21 07:55:19 +00:00
|
|
|
class LTText(object):
|
2009-06-20 10:44:00 +00:00
|
|
|
|
2009-10-24 04:41:59 +00:00
|
|
|
def __init__(self, text):
|
|
|
|
self.text = text
|
|
|
|
return
|
2009-06-20 10:44:00 +00:00
|
|
|
|
2009-10-24 04:41:59 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return '<text %r>' % self.text
|
2009-07-21 07:55:19 +00:00
|
|
|
|
2009-10-24 04:41:59 +00:00
|
|
|
def is_upright(self):
|
|
|
|
return True
|
2009-07-21 07:55:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
## LTAnon
|
|
|
|
##
|
|
|
|
class LTAnon(LTText):
|
|
|
|
|
2010-03-21 02:21:37 +00:00
|
|
|
pass
|
2009-06-20 10:44:00 +00:00
|
|
|
|
|
|
|
|
2010-02-27 03:59:25 +00:00
|
|
|
## LTChar
|
2009-05-05 12:26:29 +00:00
|
|
|
##
|
2010-02-27 03:59:25 +00:00
|
|
|
class LTChar(LayoutItem, LTText):
|
2009-07-21 07:55:19 +00:00
|
|
|
|
2010-03-26 11:11:35 +00:00
|
|
|
debug = 0
|
2009-10-24 04:41:59 +00:00
|
|
|
|
2010-02-27 03:59:25 +00:00
|
|
|
def __init__(self, matrix, font, fontsize, scaling, cid):
|
2009-10-24 04:41:59 +00:00
|
|
|
self.matrix = matrix
|
|
|
|
self.font = font
|
2010-02-27 03:59:25 +00:00
|
|
|
self.fontsize = fontsize
|
2009-10-24 04:41:59 +00:00
|
|
|
self.vertical = font.is_vertical()
|
2010-02-27 03:59:25 +00:00
|
|
|
self.adv = font.char_width(cid) * fontsize * scaling
|
|
|
|
try:
|
|
|
|
text = font.to_unichr(cid)
|
|
|
|
except PDFUnicodeNotDefined:
|
|
|
|
text = '?'
|
|
|
|
LTText.__init__(self, text)
|
|
|
|
# compute the boundary rectangle.
|
|
|
|
if self.vertical:
|
|
|
|
# vertical
|
|
|
|
size = font.get_size() * fontsize
|
|
|
|
displacement = (1000 - font.char_disp(cid)) * fontsize * .001
|
|
|
|
(_,displacement) = apply_matrix_norm(self.matrix, (0, displacement))
|
|
|
|
(dx,dy) = apply_matrix_norm(self.matrix, (size, self.adv))
|
|
|
|
(_,_,_,_,tx,ty) = self.matrix
|
2009-10-24 04:41:59 +00:00
|
|
|
tx -= dx/2
|
2010-02-27 03:59:25 +00:00
|
|
|
ty += displacement
|
2009-10-24 04:41:59 +00:00
|
|
|
bbox = (tx, ty+dy, tx+dx, ty)
|
2010-02-27 03:59:25 +00:00
|
|
|
else:
|
|
|
|
# horizontal
|
|
|
|
size = font.get_size() * fontsize
|
|
|
|
descent = font.get_descent() * fontsize
|
|
|
|
(_,descent) = apply_matrix_norm(self.matrix, (0, descent))
|
|
|
|
(dx,dy) = apply_matrix_norm(self.matrix, (self.adv, size))
|
|
|
|
(_,_,_,_,tx,ty) = self.matrix
|
|
|
|
ty += descent
|
|
|
|
bbox = (tx, ty, tx+dx, ty+dy)
|
2009-10-24 04:41:59 +00:00
|
|
|
LayoutItem.__init__(self, bbox)
|
|
|
|
return
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
if self.debug:
|
2010-02-27 03:59:25 +00:00
|
|
|
return ('<char matrix=%s font=%r fontsize=%.1f bbox=%s adv=%s text=%r>' %
|
|
|
|
(matrix2str(self.matrix), self.font, self.fontsize,
|
|
|
|
bbox2str(self.bbox), self.adv, self.text))
|
2009-10-24 04:41:59 +00:00
|
|
|
else:
|
2010-02-27 03:59:25 +00:00
|
|
|
return '<char %r>' % self.text
|
2009-10-24 04:41:59 +00:00
|
|
|
|
2010-02-27 03:59:25 +00:00
|
|
|
def get_size(self):
|
|
|
|
return max(self.width, self.height)
|
2009-10-24 04:41:59 +00:00
|
|
|
|
|
|
|
def is_vertical(self):
|
|
|
|
return self.vertical
|
|
|
|
|
|
|
|
def is_upright(self):
|
|
|
|
(a,b,c,d,e,f) = self.matrix
|
|
|
|
return 0 < a*d and b*c <= 0
|
2009-07-21 07:55:19 +00:00
|
|
|
|
2010-02-27 03:59:25 +00:00
|
|
|
|
2009-05-16 04:16:00 +00:00
|
|
|
## LTFigure
|
|
|
|
##
|
|
|
|
class LTFigure(LayoutContainer):
|
2009-10-24 04:41:59 +00:00
|
|
|
|
2010-03-22 04:00:18 +00:00
|
|
|
def __init__(self, name, bbox, matrix):
|
2009-10-24 04:41:59 +00:00
|
|
|
(x,y,w,h) = bbox
|
2009-12-20 02:38:01 +00:00
|
|
|
bbox = get_bounds( apply_matrix_pt(matrix, (p,q))
|
|
|
|
for (p,q) in ((x,y), (x+w,y), (x,y+h), (x+w,y+h)) )
|
2010-03-22 04:00:18 +00:00
|
|
|
self.name = name
|
2009-10-24 04:41:59 +00:00
|
|
|
self.matrix = matrix
|
2010-03-20 05:43:34 +00:00
|
|
|
LayoutContainer.__init__(self, bbox)
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
def __repr__(self):
|
2010-03-22 04:00:18 +00:00
|
|
|
return ('<figure %r bbox=%s matrix=%s>' %
|
|
|
|
(self.name, bbox2str(self.bbox), matrix2str(self.matrix)))
|
2009-05-16 04:16:00 +00:00
|
|
|
|
|
|
|
|
2009-07-21 07:55:19 +00:00
|
|
|
## LTTextLine
|
|
|
|
##
|
|
|
|
class LTTextLine(LayoutContainer):
|
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
def __init__(self, objs):
|
|
|
|
LayoutContainer.__init__(self, (0,0,0,0), objs)
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
def __repr__(self):
|
2010-03-20 05:43:34 +00:00
|
|
|
return ('<textline %s>' % bbox2str(self.bbox))
|
2009-10-24 04:41:59 +00:00
|
|
|
|
|
|
|
def get_text(self):
|
|
|
|
return ''.join( obj.text for obj in self.objs if isinstance(obj, LTText) )
|
|
|
|
|
2010-03-21 02:21:37 +00:00
|
|
|
def find_neighbors(self, plane, ratio):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
class LTTextLineHorizontal(LTTextLine):
|
|
|
|
|
|
|
|
def __init__(self, objs, word_margin):
|
|
|
|
LTTextLine.__init__(self, objs)
|
2009-10-24 04:41:59 +00:00
|
|
|
LayoutContainer.fixate(self)
|
|
|
|
objs = []
|
2010-03-20 05:43:34 +00:00
|
|
|
x1 = INF
|
2010-03-22 06:04:54 +00:00
|
|
|
for obj in csort(self.objs, key=lambda obj: obj.x0):
|
2010-03-20 05:43:34 +00:00
|
|
|
if isinstance(obj, LTChar) and word_margin:
|
2010-03-21 02:21:37 +00:00
|
|
|
margin = word_margin * obj.width
|
2010-03-20 05:43:34 +00:00
|
|
|
if x1 < obj.x0-margin:
|
|
|
|
objs.append(LTAnon(' '))
|
|
|
|
objs.append(obj)
|
|
|
|
x1 = obj.x1
|
|
|
|
self.objs = objs + [LTAnon('\n')]
|
|
|
|
return
|
|
|
|
|
2010-03-21 02:21:37 +00:00
|
|
|
def find_neighbors(self, plane, ratio):
|
|
|
|
h = ratio*self.height
|
|
|
|
return plane.find((self.x0, self.y0-h, self.x1, self.y1+h))
|
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
class LTTextLineVertical(LTTextLine):
|
|
|
|
|
|
|
|
def __init__(self, objs, word_margin):
|
|
|
|
LTTextLine.__init__(self, objs)
|
|
|
|
LayoutContainer.fixate(self)
|
|
|
|
objs = []
|
|
|
|
y0 = -INF
|
2010-03-22 06:04:54 +00:00
|
|
|
for obj in csort(self.objs, key=lambda obj: -obj.y1):
|
2010-03-20 05:43:34 +00:00
|
|
|
if isinstance(obj, LTChar) and word_margin:
|
2010-03-21 02:21:37 +00:00
|
|
|
margin = word_margin * obj.height
|
2010-03-20 05:43:34 +00:00
|
|
|
if obj.y1+margin < y0:
|
|
|
|
objs.append(LTAnon(' '))
|
|
|
|
objs.append(obj)
|
|
|
|
y0 = obj.y0
|
|
|
|
self.objs = objs + [LTAnon('\n')]
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
2009-07-21 07:55:19 +00:00
|
|
|
|
2010-03-21 02:21:37 +00:00
|
|
|
def find_neighbors(self, plane, ratio):
|
|
|
|
w = ratio*self.width
|
|
|
|
return plane.find((self.x0-w, self.y0, self.x1+w, self.y1))
|
|
|
|
|
2009-07-21 07:55:19 +00:00
|
|
|
|
2009-05-15 14:25:32 +00:00
|
|
|
## LTTextBox
|
2009-05-05 12:26:29 +00:00
|
|
|
##
|
2009-05-15 14:25:32 +00:00
|
|
|
## A set of text objects that are grouped within
|
2009-05-05 12:26:29 +00:00
|
|
|
## a certain rectangular area.
|
|
|
|
##
|
2009-05-15 14:25:32 +00:00
|
|
|
class LTTextBox(LayoutContainer):
|
2009-05-05 12:26:29 +00:00
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
def __init__(self, objs):
|
|
|
|
LayoutContainer.__init__(self, (0,0,0,0), objs)
|
2010-03-22 04:00:18 +00:00
|
|
|
self.index = None
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
def __repr__(self):
|
2010-03-22 04:00:18 +00:00
|
|
|
return ('<textbox(%s) %s %r...>' % (self.index, bbox2str(self.bbox), self.get_text()[:20]))
|
2009-05-05 12:26:29 +00:00
|
|
|
|
2009-10-24 04:41:59 +00:00
|
|
|
def get_text(self):
|
|
|
|
return ''.join( obj.get_text() for obj in self.objs if isinstance(obj, LTTextLine) )
|
2009-07-21 07:55:19 +00:00
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
class LTTextBoxHorizontal(LTTextBox):
|
|
|
|
|
2009-10-24 04:41:59 +00:00
|
|
|
def fixate(self):
|
2010-03-20 05:43:34 +00:00
|
|
|
LTTextBox.fixate(self)
|
2010-03-22 06:04:54 +00:00
|
|
|
self.objs = csort(self.objs, key=lambda obj: -obj.y1)
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
2009-05-04 08:29:36 +00:00
|
|
|
|
2010-03-20 05:43:34 +00:00
|
|
|
class LTTextBoxVertical(LTTextBox):
|
|
|
|
|
|
|
|
def fixate(self):
|
|
|
|
LTTextBox.fixate(self)
|
2010-03-22 06:04:54 +00:00
|
|
|
self.objs = csort(self.objs, key=lambda obj: -obj.x1)
|
2010-03-20 05:43:34 +00:00
|
|
|
return
|
2009-05-05 12:26:29 +00:00
|
|
|
|
2009-07-21 07:55:19 +00:00
|
|
|
|
2010-03-22 04:00:18 +00:00
|
|
|
## LTTextGroup
|
2010-03-21 02:21:37 +00:00
|
|
|
##
|
2010-03-22 04:00:18 +00:00
|
|
|
class LTTextGroup(LayoutContainer):
|
2010-03-21 02:21:37 +00:00
|
|
|
|
|
|
|
def __init__(self, objs):
|
|
|
|
assert objs
|
|
|
|
LayoutContainer.__init__(self, (0,0,0,0), objs)
|
|
|
|
LayoutContainer.fixate(self)
|
|
|
|
return
|
|
|
|
|
2010-03-25 11:38:47 +00:00
|
|
|
class LTTextGroupLRTB(LTTextGroup):
|
2010-03-21 02:21:37 +00:00
|
|
|
|
|
|
|
def __init__(self, objs):
|
2010-03-22 04:00:18 +00:00
|
|
|
LTTextGroup.__init__(self, objs)
|
2010-03-21 02:21:37 +00:00
|
|
|
# reorder the objects from top-left to bottom-right.
|
2010-03-26 11:11:35 +00:00
|
|
|
self.objs = csort(self.objs, key=lambda obj: obj.x0+obj.x1-(obj.y0+obj.y1))
|
2010-03-21 02:21:37 +00:00
|
|
|
return
|
|
|
|
|
2010-03-25 11:38:47 +00:00
|
|
|
class LTTextGroupTBRL(LTTextGroup):
|
2010-03-21 02:21:37 +00:00
|
|
|
|
|
|
|
def __init__(self, objs):
|
2010-03-22 04:00:18 +00:00
|
|
|
LTTextGroup.__init__(self, objs)
|
2010-03-21 02:21:37 +00:00
|
|
|
# reorder the objects from top-right to bottom-left.
|
2010-03-26 11:11:35 +00:00
|
|
|
self.objs = csort(self.objs, key=lambda obj: -(obj.x0+obj.x1)-(obj.y0+obj.y1))
|
2010-03-21 02:21:37 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
## 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.xobjs = []
|
|
|
|
self.yobjs = []
|
2010-03-22 06:04:54 +00:00
|
|
|
self.idxs = dict( (obj,i) for (i,obj) in enumerate(objs) )
|
2010-03-21 02:21:37 +00:00
|
|
|
for obj in objs:
|
|
|
|
self.place(obj)
|
|
|
|
self.xobjs.sort()
|
|
|
|
self.yobjs.sort()
|
|
|
|
return
|
|
|
|
|
|
|
|
# place(obj): place an object in a certain area.
|
|
|
|
def place(self, obj):
|
|
|
|
assert isinstance(obj, LayoutItem)
|
|
|
|
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)
|
2010-03-22 06:04:54 +00:00
|
|
|
return sorted(xobjs, key=lambda obj: self.idxs[obj])
|
2010-03-21 02:21:37 +00:00
|
|
|
|
|
|
|
|
2010-03-25 11:38:47 +00:00
|
|
|
## guess_wmode
|
|
|
|
##
|
|
|
|
def guess_wmode(objs):
|
|
|
|
"""Guess the writing mode by looking at the order of text objects."""
|
|
|
|
xy = tb = lr = 0
|
|
|
|
obj0 = None
|
|
|
|
for obj1 in objs:
|
|
|
|
if obj0 is not None:
|
|
|
|
dx = obj1.x0+obj1.x1-(obj0.x0+obj0.x1)
|
|
|
|
dy = obj1.y0+obj1.y1-(obj0.y0+obj0.y1)
|
|
|
|
if abs(dy) < abs(dx):
|
|
|
|
xy += 1
|
|
|
|
else:
|
|
|
|
xy -= 1
|
|
|
|
if 0 < dx:
|
|
|
|
lr += 1
|
|
|
|
else:
|
|
|
|
lr -= 1
|
|
|
|
if dy < 0:
|
|
|
|
tb += 1
|
|
|
|
else:
|
|
|
|
tb -= 1
|
|
|
|
obj0 = obj1
|
|
|
|
if 0 < lr:
|
|
|
|
lr = 'lr'
|
|
|
|
else:
|
|
|
|
lr = 'rl'
|
|
|
|
if 0 < tb:
|
|
|
|
tb = 'tb'
|
|
|
|
else:
|
|
|
|
tb = 'bt'
|
|
|
|
if 0 < xy:
|
|
|
|
return lr+'-'+tb
|
|
|
|
else:
|
|
|
|
return tb+'-'+lr
|
|
|
|
|
|
|
|
|
2010-03-22 04:00:18 +00:00
|
|
|
## group_lines
|
2010-03-21 02:21:37 +00:00
|
|
|
##
|
2010-03-26 11:11:35 +00:00
|
|
|
def group_lines(groupfunc, objs, findfunc, debug=0):
|
2010-03-25 11:38:47 +00:00
|
|
|
"""Group LTTextLine objects to form a LTTextBox."""
|
2010-03-21 02:21:37 +00:00
|
|
|
plane = Plane(objs)
|
2010-03-22 04:00:18 +00:00
|
|
|
groups = {}
|
2010-03-21 02:21:37 +00:00
|
|
|
for obj in objs:
|
2010-03-26 11:11:35 +00:00
|
|
|
neighbors = findfunc(obj, plane)
|
2010-03-21 02:21:37 +00:00
|
|
|
assert obj in neighbors, obj
|
2010-03-22 04:00:18 +00:00
|
|
|
members = neighbors[:]
|
|
|
|
for obj1 in neighbors:
|
|
|
|
if obj1 in groups:
|
|
|
|
members.extend(groups.pop(obj1))
|
2010-03-26 11:11:35 +00:00
|
|
|
if debug:
|
|
|
|
print >>sys.stderr, 'group:', members
|
2010-03-22 04:00:18 +00:00
|
|
|
group = groupfunc(list(uniq(members)))
|
|
|
|
for obj in members:
|
|
|
|
groups[obj] = group
|
2010-03-22 06:04:54 +00:00
|
|
|
done = set()
|
|
|
|
r = []
|
|
|
|
for obj in objs:
|
|
|
|
group = groups[obj]
|
|
|
|
if group in done: continue
|
|
|
|
done.add(group)
|
2010-03-22 04:00:18 +00:00
|
|
|
group.fixate()
|
2010-03-22 06:04:54 +00:00
|
|
|
r.append(group)
|
|
|
|
return r
|
2010-03-22 04:00:18 +00:00
|
|
|
|
|
|
|
|
|
|
|
## group_boxes
|
|
|
|
##
|
2010-03-26 11:11:35 +00:00
|
|
|
def group_boxes(groupfunc, objs, distfunc, debug=0):
|
2010-03-21 02:21:37 +00:00
|
|
|
assert objs
|
|
|
|
while 2 <= len(objs):
|
|
|
|
mindist = INF
|
|
|
|
minpair = None
|
2010-03-22 06:04:54 +00:00
|
|
|
objs = csort(objs, key=lambda obj: obj.width*obj.height)
|
2010-03-22 04:34:52 +00:00
|
|
|
for i in xrange(len(objs)):
|
|
|
|
for j in xrange(i+1, len(objs)):
|
|
|
|
d = distfunc(objs[i], objs[j])
|
2010-03-21 02:21:37 +00:00
|
|
|
if d < mindist:
|
|
|
|
mindist = d
|
2010-03-22 04:34:52 +00:00
|
|
|
minpair = (objs[i], objs[j])
|
2010-03-21 02:21:37 +00:00
|
|
|
assert minpair
|
2010-03-22 04:34:52 +00:00
|
|
|
(obj1, obj2) = minpair
|
2010-03-21 02:21:37 +00:00
|
|
|
objs.remove(obj1)
|
2010-03-22 04:34:52 +00:00
|
|
|
objs.remove(obj2)
|
2010-03-26 11:11:35 +00:00
|
|
|
if debug:
|
|
|
|
print >>sys.stderr, 'group:', obj1, obj2
|
2010-03-22 04:34:52 +00:00
|
|
|
objs.append(groupfunc([obj1, obj2]))
|
2010-03-21 02:21:37 +00:00
|
|
|
assert len(objs) == 1
|
|
|
|
return objs.pop()
|
2009-05-04 08:29:36 +00:00
|
|
|
|
|
|
|
|
2009-05-15 14:25:32 +00:00
|
|
|
## LTPage
|
2009-05-04 08:29:36 +00:00
|
|
|
##
|
2009-05-15 14:25:32 +00:00
|
|
|
class LTPage(LayoutContainer):
|
2009-10-24 04:41:59 +00:00
|
|
|
|
2010-03-22 04:00:18 +00:00
|
|
|
def __init__(self, pageid, bbox, rotate=0):
|
2010-03-20 05:43:34 +00:00
|
|
|
LayoutContainer.__init__(self, bbox)
|
2010-03-22 04:00:18 +00:00
|
|
|
self.pageid = pageid
|
2009-10-24 04:41:59 +00:00
|
|
|
self.rotate = rotate
|
2010-03-22 04:00:18 +00:00
|
|
|
self.layout = None
|
2009-10-24 04:41:59 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
def __repr__(self):
|
2010-03-22 04:00:18 +00:00
|
|
|
return ('<page(%r) bbox=%s rotate=%r>' % (self.pageid, bbox2str(self.bbox), self.rotate))
|
|
|
|
|
|
|
|
def fixate(self, laparams):
|
|
|
|
"""Perform the layout analysis."""
|
|
|
|
LayoutContainer.fixate(self)
|
|
|
|
(textobjs, otherobjs) = self.get_textobjs()
|
|
|
|
if not laparams or not textobjs: return
|
2010-03-25 11:38:47 +00:00
|
|
|
if laparams.writing_mode not in ('lr-tb', 'tb-rl'):
|
|
|
|
laparams.writing_mode = guess_wmode(textobjs)
|
|
|
|
if (laparams.writing_mode.startswith('tb-') or
|
|
|
|
laparams.writing_mode.startswith('bt-')):
|
2010-03-22 04:00:18 +00:00
|
|
|
textboxes = self.build_textbox_vertical(textobjs, laparams)
|
2010-03-25 11:38:47 +00:00
|
|
|
top = self.group_textbox_tb_rl(textboxes, laparams)
|
2010-03-22 04:00:18 +00:00
|
|
|
else:
|
|
|
|
textboxes = self.build_textbox_horizontal(textobjs, laparams)
|
2010-03-25 11:38:47 +00:00
|
|
|
top = self.group_textbox_lr_tb(textboxes, laparams)
|
2010-03-22 04:00:18 +00:00
|
|
|
def assign_index(obj, i):
|
|
|
|
if isinstance(obj, LTTextBox):
|
|
|
|
obj.index = i
|
|
|
|
i += 1
|
|
|
|
elif isinstance(obj, LTTextGroup):
|
|
|
|
for x in obj:
|
|
|
|
i = assign_index(x, i)
|
|
|
|
return i
|
|
|
|
assign_index(top, 0)
|
|
|
|
textboxes.sort(key=lambda box:box.index)
|
|
|
|
self.objs = textboxes + otherobjs
|
|
|
|
self.layout = top
|
|
|
|
return
|
2009-10-24 04:41:59 +00:00
|
|
|
|
2010-03-22 04:00:18 +00:00
|
|
|
def get_textobjs(self):
|
|
|
|
"""Split all the objects in the page into text-related objects and others."""
|
2009-10-24 04:41:59 +00:00
|
|
|
textobjs = []
|
|
|
|
otherobjs = []
|
|
|
|
for obj in self.objs:
|
|
|
|
if isinstance(obj, LTText) and obj.is_upright():
|
|
|
|
textobjs.append(obj)
|
|
|
|
else:
|
|
|
|
otherobjs.append(obj)
|
2010-03-22 04:00:18 +00:00
|
|
|
return (textobjs, otherobjs)
|
2010-03-20 05:43:34 +00:00
|
|
|
|
2010-03-22 04:00:18 +00:00
|
|
|
def build_textbox_horizontal(self, objs, laparams):
|
|
|
|
"""Identify horizontal text regions in the page."""
|
|
|
|
def aligned(obj1, obj2):
|
2010-03-20 05:43:34 +00:00
|
|
|
# +------+ - - -
|
|
|
|
# | obj1 | - - +------+ -
|
|
|
|
# | | | obj2 | | (line_overlap)
|
|
|
|
# +------+ - - | | -
|
|
|
|
# - - - +------+
|
|
|
|
#
|
|
|
|
# |<--->|
|
|
|
|
# (char_margin)
|
|
|
|
return ((min(obj1.height, obj2.height) * laparams.line_overlap < obj1.voverlap(obj2)) and
|
|
|
|
(obj1.hdistance(obj2) < min(obj1.width, obj2.width) * laparams.char_margin))
|
|
|
|
lines = []
|
|
|
|
line = []
|
|
|
|
prev = None
|
|
|
|
for cur in objs:
|
2010-03-22 04:00:18 +00:00
|
|
|
if prev is not None and not aligned(prev, cur):
|
2010-03-20 05:43:34 +00:00
|
|
|
if line:
|
|
|
|
lines.append(LTTextLineHorizontal(line, laparams.word_margin))
|
|
|
|
line = []
|
|
|
|
line.append(cur)
|
|
|
|
prev = cur
|
|
|
|
if line:
|
|
|
|
lines.append(LTTextLineHorizontal(line, laparams.word_margin))
|
2010-03-26 11:11:35 +00:00
|
|
|
return group_lines(LTTextBoxHorizontal, lines,
|
|
|
|
lambda obj, plane: obj.find_neighbors(plane, laparams.line_margin))
|
|
|
|
|
2010-03-22 04:00:18 +00:00
|
|
|
def build_textbox_vertical(self, objs, laparams):
|
|
|
|
"""Identify vertical text regions in the page."""
|
|
|
|
def aligned(obj1, obj2):
|
2010-03-20 05:43:34 +00:00
|
|
|
# +------+
|
|
|
|
# | obj1 |
|
|
|
|
# | |
|
|
|
|
# +------+ - - -
|
|
|
|
# | | | (char_margin)
|
|
|
|
# +------+ - -
|
|
|
|
# | obj2 |
|
|
|
|
# | |
|
|
|
|
# +------+
|
|
|
|
#
|
|
|
|
# |<--->|
|
|
|
|
# (line_overlap)
|
|
|
|
return ((min(obj1.width, obj2.width) * laparams.line_overlap < obj1.hoverlap(obj2)) and
|
|
|
|
(obj1.vdistance(obj2) < min(obj1.height, obj2.height) * laparams.char_margin))
|
|
|
|
lines = []
|
|
|
|
line = []
|
|
|
|
prev = None
|
|
|
|
for cur in objs:
|
2010-03-22 04:00:18 +00:00
|
|
|
if prev is not None and not aligned(prev, cur):
|
2010-03-20 05:43:34 +00:00
|
|
|
if line:
|
|
|
|
lines.append(LTTextLineVertical(line, laparams.word_margin))
|
|
|
|
line = []
|
|
|
|
line.append(cur)
|
|
|
|
prev = cur
|
|
|
|
if line:
|
|
|
|
lines.append(LTTextLineVertical(line, laparams.word_margin))
|
2010-03-26 11:11:35 +00:00
|
|
|
return group_lines(LTTextBoxVertical, lines,
|
|
|
|
lambda obj, plane: obj.find_neighbors(plane, laparams.line_margin))
|
2010-03-20 05:43:34 +00:00
|
|
|
|
2010-03-25 11:38:47 +00:00
|
|
|
def group_textbox_lr_tb(self, boxes, laparams):
|
2010-03-21 02:21:37 +00:00
|
|
|
def dist(obj1, obj2):
|
2010-03-26 11:11:35 +00:00
|
|
|
return ((max(obj1.x1,obj2.x1) - min(obj1.x0,obj2.x0)) *
|
2010-03-21 02:21:37 +00:00
|
|
|
(max(obj1.y1,obj2.y1) - min(obj1.y0,obj2.y0)) -
|
2010-03-26 11:11:35 +00:00
|
|
|
(obj1.width*obj1.height + obj2.width*obj2.height))
|
2010-03-25 11:38:47 +00:00
|
|
|
return group_boxes(LTTextGroupLRTB, boxes, dist)
|
2010-03-21 02:21:37 +00:00
|
|
|
|
2010-03-25 11:38:47 +00:00
|
|
|
def group_textbox_tb_rl(self, boxes, laparams):
|
2010-03-22 04:00:18 +00:00
|
|
|
def dist(obj1, obj2):
|
|
|
|
return ((max(obj1.x1,obj2.x1) - min(obj1.x0,obj2.x0)) *
|
|
|
|
(max(obj1.y1,obj2.y1) - min(obj1.y0,obj2.y0)) -
|
2010-03-26 11:11:35 +00:00
|
|
|
(obj1.width*obj1.height + obj2.width*obj2.height))
|
2010-03-25 11:38:47 +00:00
|
|
|
return group_boxes(LTTextGroupTBRL, boxes, dist)
|