Re-implemented rendering engine without using Qt's rich text facilities. Much faster this way.

This commit is contained in:
Kovid Goyal 2007-09-24 01:49:50 +00:00
parent 126a8771fe
commit 4da344abf5
2 changed files with 534 additions and 508 deletions

View File

@ -14,23 +14,20 @@
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
''''''
import operator, collections, copy, re, sys
import collections
from PyQt4.QtCore import Qt, QByteArray, SIGNAL, QVariant, QUrl
from PyQt4.QtCore import Qt, QByteArray, SIGNAL
from PyQt4.QtGui import QGraphicsRectItem, QGraphicsScene, QPen, \
QBrush, QColor, QGraphicsTextItem, QFontDatabase, \
QFont, QGraphicsItem, QGraphicsLineItem, QPixmap, \
QGraphicsPixmapItem, QTextCharFormat, QTextFrameFormat, \
QTextBlockFormat, QTextCursor, QTextImageFormat, \
QTextDocument, QTextOption
QBrush, QColor, QFontDatabase, \
QGraphicsItem, QGraphicsLineItem
from libprs500.gui2.lrf_renderer.text import TextBlock, FontLoader, COLOR, PixmapItem
from libprs500.ebooks.lrf.fonts import FONT_MAP
from libprs500.gui2 import qstring_to_unicode
from libprs500.ebooks.hyphenate import hyphenate_word
from libprs500.ebooks.BeautifulSoup import Tag
from libprs500.ebooks.lrf.objects import RuledLine as _RuledLine
from libprs500.ebooks.lrf.objects import Canvas as __Canvas
class Color(QColor):
def __init__(self, color):
QColor.__init__(self, color.r, color.g, color.b, 0xff-color.a)
@ -40,280 +37,6 @@ class Pen(QPen):
QPen.__init__(self, QBrush(Color(color)), width,
(Qt.SolidLine if width > 0 else Qt.NoPen))
WEIGHT_MAP = lambda wt : int((wt/10.)-1)
class FontLoader(object):
font_map = {
'Swis721 BT Roman' : 'Liberation Sans',
'Dutch801 Rm BT Roman' : 'Liberation Serif',
'Courier10 BT Roman' : 'Liberation Mono',
}
def __init__(self, font_map, dpi):
self.face_map = {}
self.cache = {}
self.dpi = dpi
self.face_map = font_map
def font(self, text_style):
device_font = text_style.fontfacename in FONT_MAP
if device_font:
face = self.font_map[text_style.fontfacename]
else:
face = self.face_map[text_style.fontfacename]
sz = text_style.fontsize
wt = text_style.fontweight
style = text_style.fontstyle
font = (face, wt, style, sz,)
if font in self.cache:
rfont = self.cache[font]
else:
italic = font[2] == QFont.StyleItalic
rfont = QFont(font[0], font[3], font[1], italic)
rfont.setPixelSize(font[3])
rfont.setBold(wt>=69)
self.cache[font] = rfont
qfont = rfont
if text_style.emplinetype != 'none':
qfont = QFont(rfont)
qfont.setOverline(text_style.emplineposition == 'before')
qfont.setUnderline(text_style.emplineposition == 'after')
return qfont
class ParSkip(object):
def __init__(self, parskip):
self.height = parskip
def __str__(self):
return 'Parskip: '+str(self.height)
class PixmapItem(QGraphicsPixmapItem):
def __init__(self, data, encoding, x0, y0, x1, y1, xsize, ysize):
p = QPixmap()
p.loadFromData(data, encoding, Qt.AutoColor)
w, h = p.width(), p.height()
p = p.copy(x0, y0, min(w, x1-x0), min(h, y1-y0))
if p.width() != xsize or p.height() != ysize:
p = p.scaled(xsize, ysize, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
QGraphicsPixmapItem.__init__(self, p)
self.height, self.width = ysize, xsize
self.setTransformationMode(Qt.SmoothTransformation)
self.setShapeMode(QGraphicsPixmapItem.BoundingRectShape)
class Plot(PixmapItem):
def __init__(self, plot, dpi):
img = plot.refobj
xsize, ysize = dpi*plot.attrs['xsize']/720., dpi*plot.attrs['xsize']/720.
x0, y0, x1, y1 = img.x0, img.y0, img.x1, img.y1
data, encoding = img.data, img.encoding
PixmapItem.__init__(self, data, encoding, x0, y0, x1, y1, xsize, ysize)
class Line(QGraphicsRectItem):
whitespace = re.compile(r'\s+')
no_pen = QPen(Qt.NoPen)
inactive_brush = QBrush(QColor(0x00, 0x00, 0x00, 0x09))
active_brush = QBrush(QColor(0x00, 0x00, 0x00, 0x59))
line_map = {
'none' : QTextCharFormat.NoUnderline,
'solid' : QTextCharFormat.SingleUnderline,
'dotted' : QTextCharFormat.DotLine,
'dashed' : QTextCharFormat.DashUnderline,
'double' : QTextCharFormat.WaveUnderline,
}
dto = QTextOption(Qt.AlignJustify)
def __init__(self, offset, linespace, linelength, align, hyphenate, ts, block_id):
QGraphicsRectItem.__init__(self, 0, 0, 0, 0)
self.offset, self.line_space, self.line_length = offset, linespace, linelength
self.align = align
self.do_hyphenation = hyphenate
self.setPen(self.__class__.no_pen)
self.is_empty = True
self.highlight_rect = None
self.cursor = None
self.item = None
self.plot_counter = 0
self.create_text_item(ts)
self.block_id = block_id
def hoverEnterEvent(self, event):
if self.highlight_rect is not None:
self.highlight_rect.setBrush(self.__class__.active_brush)
def hoverLeaveEvent(self, event):
if self.highlight_rect is not None:
self.highlight_rect.setBrush(self.__class__.inactive_brush)
def mousePressEvent(self, event):
if self.highlight_rect is not None:
self.hoverLeaveEvent(None)
self.link[1](self.link[0])
def create_link(self, pos, in_link):
if not self.acceptsHoverEvents():
self.setAcceptsHoverEvents(True)
self.highlight_rect = QGraphicsRectItem(pos, 0, 0, 0, self)
self.highlight_rect.setCursor(Qt.PointingHandCursor)
self.link = in_link
self.link_end = sys.maxint
def end_link(self):
self.link_end = self.item.boundingRect().width() - self.highlight_rect.boundingRect().x()
def add_plot(self, plot, ts, in_link):
label='plot%d'%(self.plot_counter,)
self.plot_counter += 1
pos = self.item.boundingRect().width()
self.item.document().addResource(QTextDocument.ImageResource, QUrl(label),
QVariant(plot.pixmap()))
qif = QTextImageFormat()
qif.setHeight(plot.height)
qif.setWidth(plot.width)
qif.setName(label)
self.cursor.insertImage(qif, QTextFrameFormat.InFlow)
if in_link:
self.create_link(pos, in_link)
def can_add_plot(self, plot):
pos = self.item.boundingRect().width() if self.item is not None else 0
return self.line_length - pos >= plot.width
def create_text_item(self, ts):
self.item = QGraphicsTextItem(self)
doc = self.item.document()
doc.setDefaultTextOption(self.__class__.dto)
self.cursor = QTextCursor(doc)
f = self.cursor.currentFrame()
ff = QTextFrameFormat()
ff.setBorder(0)
ff.setPadding(0)
ff.setMargin(0)
f.setFrameFormat(ff)
bf = QTextBlockFormat()
bf.setTopMargin(0)
bf.setRightMargin(0)
bf.setBottomMargin(0)
bf.setRightMargin(0)
bf.setNonBreakableLines(True)
self.cursor.setBlockFormat(bf)
def build_char_format(self, ts):
tcf = QTextCharFormat()
tcf.setFont(ts.font)
tcf.setVerticalAlignment(ts.valign)
tcf.setForeground(ts.textcolor)
tcf.setUnderlineColor(ts.linecolor)
if ts.emplineposition == 'after':
tcf.setUnderlineStyle(self.line_map[ts.emplinetype])
return tcf
def populate(self, phrase, ts, wordspace, in_link):
phrase_pos = 0
processed = False
matches = self.__class__.whitespace.finditer(phrase)
tcf = self.build_char_format(ts)
if in_link:
start = self.item.boundingRect().width()
for match in matches:
processed = True
left, right = match.span()
if wordspace == 0:
right = left
word = phrase[phrase_pos:right]
self.cursor.insertText(word, tcf)
if self.item.boundingRect().width() > self.line_length:
self.cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor,
right-left)
self.cursor.removeSelectedText()
if self.item.boundingRect().width() <= self.line_length:
if in_link: self.create_link(start, in_link)
return right, True
self.cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor,
left-phrase_pos)
self.cursor.removeSelectedText()
if self.do_hyphenation:
tokens = hyphenate_word(word)
for i in range(len(tokens)-2, -1, -1):
part = ''.join(tokens[0:i+1])
self.cursor.insertText(part+'-', tcf)
if self.item.boundingRect().width() <= self.line_length:
if in_link: self.create_link(start, in_link)
return phrase_pos + len(part), True
self.cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor,
len(part)+1)
self.cursor.removeSelectedText()
if self.cursor.position() < 1: # Force hyphenation as word is longer than line
for i in range(len(word)-5, 0, -5):
part = word[:i]
self.cursor.insertText(part+'-', tcf)
if self.item.boundingRect().width() <= self.line_length:
if in_link: self.create_link(start, in_link)
return phrase_pos + len(part), True
self.cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor,
len(part)+1)
self.cursor.removeSelectedText()
return phrase_pos, True
if in_link: self.create_link(start, in_link)
phrase_pos = right
if not processed:
return self.populate(phrase+' ', ts, 0, in_link)
return phrase_pos, False
def finalize(self, wordspace, vdebug):
crect = self.childrenBoundingRect()
self.width = crect.width() - wordspace
self.height = crect.height() + self.line_space
self.setRect(crect)
if vdebug:
self.setPen(QPen(Qt.yellow, 1, Qt.DotLine))
if self.highlight_rect is not None:
x = self.highlight_rect.boundingRect().x()
if self.link_end == sys.maxint:
self.link_end = crect.width()-x
self.highlight_rect.setRect(crect)
erect = self.highlight_rect.boundingRect()
erect.setX(x)
erect.setWidth(self.link_end)
self.highlight_rect.setRect(erect)
self.highlight_rect.setBrush(self.__class__.inactive_brush)
self.highlight_rect.setZValue(-1)
self.highlight_rect.setPen(self.__class__.no_pen)
return self.height
def getx(self, textwidth):
if self.align == 'head':
return self.offset
if self.align == 'foot':
return textwidth - self.width
if self.align == 'center':
return (textwidth-self.width)/2.
def __unicode__(self):
s = u''
for word in self.children():
if not hasattr(word, 'toPlainText'):
continue
s += qstring_to_unicode(word.toPlainText())
return s
def __str__(self):
return unicode(self).encode('utf-8')
class ContentObject(object):
@ -323,226 +46,6 @@ class ContentObject(object):
self.has_content = True
NULL = lambda a, b: a
COLOR = lambda a, b: QColor(*a)
WEIGHT = lambda a, b: WEIGHT_MAP(a)
class Style(object):
map = collections.defaultdict(lambda : NULL)
def __init__(self, style, dpi):
self.fdpi = dpi/720.
self.update(style.as_dict())
def update(self, *args, **kwds):
if len(args) > 0:
kwds = args[0]
for attr in kwds:
setattr(self, attr, self.__class__.map[attr](kwds[attr], self.fdpi))
def copy(self):
return copy.copy(self)
class TextStyle(Style):
map = collections.defaultdict(lambda : NULL,
fontsize = operator.mul,
fontwidth = operator.mul,
fontweight = WEIGHT,
textcolor = COLOR,
textbgcolor = COLOR,
wordspace = operator.mul,
letterspace = operator.mul,
baselineskip = operator.mul,
linespace = operator.mul,
parindent = operator.mul,
parskip = operator.mul,
textlinewidth = operator.mul,
charspace = operator.mul,
linecolor = COLOR,
)
def __init__(self, style, font_loader, ruby_tags):
self.font_loader = font_loader
self.fontstyle = QFont.StyleNormal
self.valign = QTextCharFormat.AlignBottom
for attr in ruby_tags:
setattr(self, attr, ruby_tags[attr])
Style.__init__(self, style, font_loader.dpi)
self.emplinetype = 'none'
self.font = self.font_loader.font(self)
def update(self, *args, **kwds):
Style.update(self, *args, **kwds)
self.font = self.font_loader.font(self)
class BlockStyle(Style):
map = collections.defaultdict(lambda : NULL,
bgcolor = COLOR,
framecolor = COLOR,
)
class TextBlock(ContentObject):
has_content = property(fget=lambda self: self.peek_index < len(self.lines)-1)
XML_ENTITIES = dict(zip(Tag.XML_SPECIAL_CHARS_TO_ENTITIES.values(), Tag.XML_SPECIAL_CHARS_TO_ENTITIES.keys()))
XML_ENTITIES["quot"] = '"'
class HeightExceeded(Exception):
pass
def __init__(self, tb, font_loader, respect_max_y, text_width, logger,
opts, ruby_tags, link_activated,
parent=None, x=0, y=0):
ContentObject.__init__(self)
self.block_id = tb.id
self.bs, self.ts = BlockStyle(tb.style, font_loader.dpi), \
TextStyle(tb.textstyle, font_loader, ruby_tags)
self.bs.update(tb.attrs)
self.ts.update(tb.attrs)
self.lines = []
self.line_length = min(self.bs.blockwidth, text_width)
self.line_length -= 2*self.bs.sidemargin
self.line_offset = self.bs.sidemargin
self.first_line = True
self.current_style = self.ts.copy()
self.current_line = None
self.font_loader, self.logger, self.opts = font_loader, logger, opts
self.in_link = False
self.link_activated = link_activated
self.max_y = self.bs.blockheight if (respect_max_y or self.bs.blockrule.lower() in ('vert-fixed', 'block-fixed')) else sys.maxint
self.height = 0
try:
if self.max_y > 0:
self.populate(tb.content)
self.end_line()
except TextBlock.HeightExceeded:
logger.warning('TextBlock height exceeded, truncating.')
self.peek_index = -1
def peek(self):
return self.lines[self.peek_index+1]
def commit(self):
self.peek_index += 1
def reset(self):
self.peek_index = -1
def end_link(self):
self.link_activated(self.in_link[0], on_creation=True)
self.in_link = False
def populate(self, tb):
self.create_line()
open_containers = collections.deque()
self.in_para = False
for i in tb.content:
if isinstance(i, basestring):
self.process_text(i)
elif i is None:
if len(open_containers) > 0:
for a, b in open_containers.pop():
if callable(a):
a(*b)
else:
setattr(self, a, b)
elif i.name == 'P':
open_containers.append((('in_para', False),))
self.in_para = True
elif i.name == 'CR':
if self.in_para:
self.end_line()
self.create_line()
else:
self.end_line()
delta = self.current_style.parskip
if isinstance(self.lines[-1], ParSkip):
delta += self.current_style.baselineskip
self.lines.append(ParSkip(delta))
self.first_line = True
elif i.name == 'Span':
open_containers.append((('current_style', self.current_style.copy()),))
self.current_style.update(i.attrs)
elif i.name == 'CharButton':
open_containers.append(((self.end_link, []),))
self.in_link = (i.attrs['refobj'], self.link_activated)
elif i.name == 'Italic':
open_containers.append((('current_style', self.current_style.copy()),))
self.current_style.update(fontstyle=QFont.StyleItalic)
elif i.name == 'Plot':
plot = Plot(i, self.font_loader.dpi)
if self.current_line is None:
self.create_line()
if not self.current_line.can_add_plot(plot):
self.end_line()
self.create_line()
self.current_line.add_plot(plot, self.current_style, self.in_link)
elif i.name == 'Sup':
open_containers.append((('current_style', self.current_style.copy()),))
self.current_style.valign=QTextCharFormat.AlignSuperScript
elif i.name == 'Sub':
open_containers.append((('current_style', self.current_style.copy()),))
self.current_style.valign=QTextCharFormat.AlignSubScript
elif i.name == 'EmpLine':
if i.attrs:
open_containers.append((('current_style', self.current_style.copy()),))
self.current_style.update(i.attrs)
else:
self.logger.warning('Unhandled TextTag %s'%(i.name,))
if not i.self_closing:
open_containers.append([])
def __iter__(self):
for line in self.lines: yield line
def end_line(self):
if self.current_line is not None:
self.height += self.current_line.finalize(self.current_style.wordspace, self.opts.visual_debug)
if self.height > self.max_y+10:
raise TextBlock.HeightExceeded
self.lines.append(self.current_line)
self.current_line = None
def create_line(self):
line_length = self.line_length
line_offset = self.line_offset
if self.first_line:
line_length -= self.current_style.parindent
line_offset += self.current_style.parindent
self.current_line = Line(line_offset, self.current_style.linespace,
line_length, self.current_style.align,
self.opts.hyphenate, self.current_style, self.block_id)
self.first_line = False
def process_text(self, raw):
for ent, rep in TextBlock.XML_ENTITIES.items():
raw = raw.replace(u'&%s;'%ent, rep)
while len(raw) > 0:
if self.current_line is None:
self.create_line()
pos, line_filled = self.current_line.populate(raw, self.current_style,
self.current_style.wordspace, self.in_link)
raw = raw[pos:]
if line_filled:
self.end_line()
def __unicode__(self):
return u'\n'.join(unicode(l) for l in self.lines)
def __str__(self):
return '<TextBlock>\n'+unicode(self).encode('utf-8')+'\n</TextBlock>'
class RuledLine(QGraphicsLineItem, ContentObject):
map = {'solid': Qt.SolidLine, 'dashed': Qt.DashLine, 'dotted': Qt.DotLine, 'double': Qt.DashDotLine}
@ -567,9 +70,10 @@ class ImageBlock(PixmapItem, ContentObject):
def object_factory(container, obj, respect_max_y=False):
if hasattr(obj, 'name'):
if obj.name.endswith('TextBlock'):
return TextBlock(obj, container.font_loader, respect_max_y, container.text_width,
container.logger,
container.opts, container.ruby_tags, container.link_activated)
container.logger, container.opts, container.ruby_tags,
container.link_activated)
elif obj.name.endswith('ImageBlock'):
return ImageBlock(obj)
elif isinstance(obj, _RuledLine):
@ -623,7 +127,9 @@ class _Canvas(QGraphicsRectItem):
if isinstance(line, QGraphicsItem):
line.setParentItem(self)
line.setPos(x + line.getx(textwidth), y)
y += line.height
y += line.height
else:
y += line.height
if not block.has_content:
y += block.bs.footskip
block_consumed = True

View File

@ -0,0 +1,520 @@
## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from libprs500.gui2 import qstring_to_unicode
''''''
import sys, collections, operator, copy, re
from PyQt4.QtCore import Qt, QRectF, QString
from PyQt4.QtGui import QFont, QColor, QPixmap, QGraphicsPixmapItem, \
QGraphicsItem, QFontMetrics, QPen, QBrush, QGraphicsRectItem
from libprs500.ebooks.lrf.fonts import FONT_MAP
from libprs500.ebooks.BeautifulSoup import Tag
from libprs500.ebooks.hyphenate import hyphenate_word
WEIGHT_MAP = lambda wt : int((wt/10.)-1)
NULL = lambda a, b: a
COLOR = lambda a, b: QColor(*a)
WEIGHT = lambda a, b: WEIGHT_MAP(a)
class PixmapItem(QGraphicsPixmapItem):
def __init__(self, data, encoding, x0, y0, x1, y1, xsize, ysize):
p = QPixmap()
p.loadFromData(data, encoding, Qt.AutoColor)
w, h = p.width(), p.height()
p = p.copy(x0, y0, min(w, x1-x0), min(h, y1-y0))
if p.width() != xsize or p.height() != ysize:
p = p.scaled(xsize, ysize, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
QGraphicsPixmapItem.__init__(self, p)
self.height, self.width = ysize, xsize
self.setTransformationMode(Qt.SmoothTransformation)
self.setShapeMode(QGraphicsPixmapItem.BoundingRectShape)
class Plot(PixmapItem):
def __init__(self, plot, dpi):
img = plot.refobj
xsize, ysize = dpi*plot.attrs['xsize']/720., dpi*plot.attrs['xsize']/720.
x0, y0, x1, y1 = img.x0, img.y0, img.x1, img.y1
data, encoding = img.data, img.encoding
PixmapItem.__init__(self, data, encoding, x0, y0, x1, y1, xsize, ysize)
class FontLoader(object):
font_map = {
'Swis721 BT Roman' : 'Liberation Sans',
'Dutch801 Rm BT Roman' : 'Liberation Serif',
'Courier10 BT Roman' : 'Liberation Mono',
}
def __init__(self, font_map, dpi):
self.face_map = {}
self.cache = {}
self.dpi = dpi
self.face_map = font_map
def font(self, text_style):
device_font = text_style.fontfacename in FONT_MAP
if device_font:
face = self.font_map[text_style.fontfacename]
else:
face = self.face_map[text_style.fontfacename]
sz = text_style.fontsize
wt = text_style.fontweight
style = text_style.fontstyle
font = (face, wt, style, sz,)
if font in self.cache:
rfont = self.cache[font]
else:
italic = font[2] == QFont.StyleItalic
rfont = QFont(font[0], font[3], font[1], italic)
rfont.setPixelSize(font[3])
rfont.setBold(wt>=69)
self.cache[font] = rfont
qfont = rfont
if text_style.emplinetype != 'none':
qfont = QFont(rfont)
qfont.setOverline(text_style.emplineposition == 'before')
qfont.setUnderline(text_style.emplineposition == 'after')
return qfont
class Style(object):
map = collections.defaultdict(lambda : NULL)
def __init__(self, style, dpi):
self.fdpi = dpi/720.
self.update(style.as_dict())
def update(self, *args, **kwds):
if len(args) > 0:
kwds = args[0]
for attr in kwds:
setattr(self, attr, self.__class__.map[attr](kwds[attr], self.fdpi))
def copy(self):
return copy.copy(self)
class TextStyle(Style):
map = collections.defaultdict(lambda : NULL,
fontsize = operator.mul,
fontwidth = operator.mul,
fontweight = WEIGHT,
textcolor = COLOR,
textbgcolor = COLOR,
wordspace = operator.mul,
letterspace = operator.mul,
baselineskip = operator.mul,
linespace = operator.mul,
parindent = operator.mul,
parskip = operator.mul,
textlinewidth = operator.mul,
charspace = operator.mul,
linecolor = COLOR,
)
def __init__(self, style, font_loader, ruby_tags):
self.font_loader = font_loader
self.fontstyle = QFont.StyleNormal
for attr in ruby_tags:
setattr(self, attr, ruby_tags[attr])
Style.__init__(self, style, font_loader.dpi)
self.emplinetype = 'none'
self.font = self.font_loader.font(self)
def update(self, *args, **kwds):
Style.update(self, *args, **kwds)
self.font = self.font_loader.font(self)
class BlockStyle(Style):
map = collections.defaultdict(lambda : NULL,
bgcolor = COLOR,
framecolor = COLOR,
)
class ParSkip(object):
def __init__(self, parskip):
self.height = parskip
def __str__(self):
return 'Parskip: '+str(self.height)
class TextBlock(object):
class HeightExceeded(Exception):
pass
has_content = property(fget=lambda self: self.peek_index < len(self.lines)-1)
XML_ENTITIES = dict(zip(Tag.XML_SPECIAL_CHARS_TO_ENTITIES.values(), Tag.XML_SPECIAL_CHARS_TO_ENTITIES.keys()))
XML_ENTITIES["quot"] = '"'
def __init__(self, tb, font_loader, respect_max_y, text_width, logger,
opts, ruby_tags, link_activated):
self.block_id = tb.id
self.bs, self.ts = BlockStyle(tb.style, font_loader.dpi), \
TextStyle(tb.textstyle, font_loader, ruby_tags)
self.bs.update(tb.attrs)
self.ts.update(tb.attrs)
self.lines = collections.deque()
self.line_length = min(self.bs.blockwidth, text_width)
self.line_length -= 2*self.bs.sidemargin
self.line_offset = self.bs.sidemargin
self.first_line = True
self.current_style = self.ts.copy()
self.current_line = None
self.font_loader, self.logger, self.opts = font_loader, logger, opts
self.in_link = False
self.link_activated = link_activated
self.max_y = self.bs.blockheight if (respect_max_y or self.bs.blockrule.lower() in ('vert-fixed', 'block-fixed')) else sys.maxint
self.height = 0
self.peek_index = -1
try:
self.populate(tb.content)
self.end_line()
except TextBlock.HeightExceeded, err:
logger.warning('TextBlock height exceeded, skipping line:\n%s'%(err,))
def peek(self):
return self.lines[self.peek_index+1]
def commit(self):
self.peek_index += 1
def reset(self):
self.peek_index = -1
def create_link(self, refobj):
if self.current_line is None:
self.create_line()
self.current_line.start_link(refobj, self.link_activated)
self.link_activated(refobj, on_creation=True)
def end_link(self):
if self.current_line is not None:
self.current_line.end_link()
def populate(self, tb):
self.create_line()
open_containers = collections.deque()
self.in_para = False
for i in tb.content:
if isinstance(i, basestring):
self.process_text(i)
elif i is None:
if len(open_containers) > 0:
for a, b in open_containers.pop():
if callable(a):
a(*b)
else:
setattr(self, a, b)
elif i.name == 'P':
open_containers.append((('in_para', False),))
self.in_para = True
elif i.name == 'CR':
if self.in_para:
self.end_line()
self.create_line()
else:
self.end_line()
delta = self.current_style.parskip
if isinstance(self.lines[-1], ParSkip):
delta += self.current_style.baselineskip
self.lines.append(ParSkip(delta))
self.first_line = True
elif i.name == 'Span':
open_containers.append((('current_style', self.current_style.copy()),))
self.current_style.update(i.attrs)
elif i.name == 'CharButton':
open_containers.append(((self.end_link, []),))
self.create_link(i.attrs['refobj'])
elif i.name == 'Italic':
open_containers.append((('current_style', self.current_style.copy()),))
self.current_style.update(fontstyle=QFont.StyleItalic)
elif i.name == 'Plot':
plot = Plot(i, self.font_loader.dpi)
if self.current_line is None:
self.create_line()
if not self.current_line.can_add_plot(plot):
self.end_line()
self.create_line()
self.current_line.add_plot(plot)
elif i.name == 'Sup':
open_containers.append((('current_style', self.current_style.copy()),))
elif i.name == 'Sub':
open_containers.append((('current_style', self.current_style.copy()),))
elif i.name == 'EmpLine':
if i.attrs:
open_containers.append((('current_style', self.current_style.copy()),))
self.current_style.update(i.attrs)
else:
self.logger.warning('Unhandled TextTag %s'%(i.name,))
if not i.self_closing:
open_containers.append([])
def end_line(self):
if self.current_line is not None:
self.height += self.current_line.finalize(self.current_style.baselineskip,
self.current_style.linespace,
self.opts.visual_debug)
if self.height > self.max_y+10:
raise TextBlock.HeightExceeded(str(self.current_line))
self.lines.append(self.current_line)
self.current_line = None
def create_line(self):
line_length = self.line_length
line_offset = self.line_offset
if self.first_line:
line_length -= self.current_style.parindent
line_offset += self.current_style.parindent
self.current_line = Line(line_length, line_offset,
self.current_style.linespace,
self.current_style.align,
self.opts.hyphenate, self.block_id)
self.first_line = False
def process_text(self, raw):
for ent, rep in TextBlock.XML_ENTITIES.items():
raw = raw.replace(u'&%s;'%ent, rep)
while len(raw) > 0:
if self.current_line is None:
self.create_line()
pos, line_filled = self.current_line.populate(raw, self.current_style)
raw = raw[pos:]
if line_filled:
self.end_line()
def __iter__(self):
for line in self.lines: yield line
def __str__(self):
s = ''
for line in self:
s += str(line) + '\n'
return s
class Link(QGraphicsRectItem):
inactive_brush = QBrush(QColor(0x00, 0x00, 0x00, 0x09))
active_brush = QBrush(QColor(0x00, 0x00, 0x00, 0x59))
def __init__(self, parent, start, stop, refobj, slot):
QGraphicsRectItem.__init__(self, start, 0, stop-start, parent.height, parent)
self.refobj = refobj
self.slot = slot
self.setBrush(self.__class__.inactive_brush)
self.setPen(QPen(Qt.NoPen))
self.setCursor(Qt.PointingHandCursor)
self.setAcceptsHoverEvents(True)
def hoverEnterEvent(self, event):
self.setBrush(self.__class__.active_brush)
def hoverLeaveEvent(self, event):
self.setBrush(self.__class__.inactive_brush)
def mousePressEvent(self, event):
self.hoverLeaveEvent(None)
self.slot(self.refobj)
class Line(QGraphicsItem):
whitespace = re.compile(r'\s+')
def __init__(self, line_length, offset, linespace, align, hyphenate, block_id):
QGraphicsItem.__init__(self)
self.line_length, self.offset, self.line_space = line_length, offset, linespace
self.align, self.hyphenate, self.block_id = align, hyphenate, block_id
self.tokens = collections.deque()
self.current_width = 0
self.length_in_space = 0
self.height, self.descent = 0, 0
self.links = collections.deque()
self.current_link = None
def start_link(self, refobj, slot):
self.current_link = [self.current_width, sys.maxint, refobj, slot]
def end_link(self):
if self.current_link is not None:
self.current_link[1] = self.current_width
self.links.append(self.current_link)
self.current_link = None
def can_add_plot(self, plot):
return self.line_length - self.current_width >= plot.width
def add_plot(self, plot):
self.tokens.append(plot)
self.current_width += plot.width
self.height = max(self.height, plot.height)
def populate(self, phrase, ts, process_space=True):
phrase_pos = 0
processed = False
matches = self.__class__.whitespace.finditer(phrase)
font = QFont(ts.font)
fm = QFontMetrics(font)
single_space_width = fm.width(' ')
height, descent = fm.height(), fm.descent()
for match in matches:
processed = True
left, right = match.span()
if not process_space:
right = left
space_width = single_space_width * (right-left)
word = phrase[phrase_pos:left]
width = fm.width(word)
if self.current_width + width < self.line_length:
self.commit(word, width, height, descent, ts, font)
if space_width > 0 and self.current_width + space_width < self.line_length:
self.add_space(space_width)
phrase_pos = right
continue
# Word doesn't fit on line
if self.hyphenate and len(word) > 3:
tokens = hyphenate_word(word)
for i in range(len(tokens)-2, -1, -1):
word = ''.join(tokens[0:i+1])+'-'
width = fm.width(word)
if self.current_width + width < self.line_length:
self.commit(word, width, height, descent, ts, font)
return phrase_pos + len(word)-1, True
if self.current_width < 5: # Force hyphenation as word is longer than line
for i in range(len(word)-5, 0, -5):
part = word[:i] + '-'
width = fm.width(part)
if self.current_width + width < self.line_length:
self.commit(part, width, height, descent, ts, font)
return phrase_pos + len(part)-1, True
# Failed to add word.
return phrase_pos, True
if not processed:
return self.populate(phrase+' ', ts, False)
return phrase_pos, False
def commit(self, word, width, height, descent, ts, font):
self.tokens.append(Word(word, width, height, ts, font))
self.current_width += width
self.height = max(self.height, height)
self.descent = max(self.descent, descent)
def add_space(self, min_width):
self.tokens.append(min_width)
self.current_width += min_width
self.length_in_space += min_width
def justify(self):
delta = self.line_length - self.current_width
if self.length_in_space > 0:
frac = 1 + float(delta)/self.length_in_space
for i in range(len(self.tokens)):
if isinstance(self.tokens[i], (int, float)):
self.tokens[i] *= frac
self.current_width = self.line_length
def finalize(self, baselineskip, linespace, vdebug):
if self.current_width >= 0.85 * self.line_length:
self.justify()
self.width = float(self.current_width)
if self.height == 0:
self.height = baselineskip
self.height += linespace
self.height = float(self.height)
self.vdebug = vdebug
if self.current_link is not None:
self.end_link()
for link in self.links:
Link(self, *link)
return self.height
def boundingRect(self):
return QRectF(0, 0, self.width, self.height)
def paint(self, painter, option, widget):
x, y = 0, 0+self.height-self.descent
if self.vdebug:
painter.save()
painter.setPen(QPen(Qt.yellow, 1, Qt.DotLine))
painter.drawRect(self.boundingRect())
painter.restore()
painter.save()
for tok in self.tokens:
if isinstance(tok, (int, float)):
x += tok
elif isinstance(tok, Word):
painter.setFont(tok.font)
p = painter.pen()
painter.setPen(QPen(tok.text_color))
painter.drawText(x, y, tok.string)
painter.setPen(p)
x += tok.width
else:
painter.drawPixmap(x, 0, tok.pixmap())
x += tok.width
painter.restore()
def getx(self, textwidth):
if self.align == 'head':
return self.offset
if self.align == 'foot':
return textwidth - self.width
if self.align == 'center':
return (textwidth-self.width)/2.
def __unicode__(self):
s = u''
for tok in self.tokens:
if isinstance(tok, (int, float)):
s += ' :%.1f: '%(tok,)
elif isinstance(tok, Word):
s += qstring_to_unicode(tok.string)
return s
def __str__(self):
return unicode(self).encode('utf-8')
class Word(object):
def __init__(self, string, width, height, ts, font):
self.string, self.width, self.height = QString(string), width, height
self.font = font
self.text_color = ts.textcolor
def main(args=sys.argv):
return 0
if __name__ == '__main__':
sys.exit(main())