From 4da344abf5a7ea5dda8f48f5da8fd03e64169cf4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 24 Sep 2007 01:49:50 +0000 Subject: [PATCH] Re-implemented rendering engine without using Qt's rich text facilities. Much faster this way. --- src/libprs500/gui2/lrf_renderer/document.py | 522 +------------------- src/libprs500/gui2/lrf_renderer/text.py | 520 +++++++++++++++++++ 2 files changed, 534 insertions(+), 508 deletions(-) create mode 100644 src/libprs500/gui2/lrf_renderer/text.py diff --git a/src/libprs500/gui2/lrf_renderer/document.py b/src/libprs500/gui2/lrf_renderer/document.py index 6b55346f05..c369176132 100644 --- a/src/libprs500/gui2/lrf_renderer/document.py +++ b/src/libprs500/gui2/lrf_renderer/document.py @@ -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 '\n'+unicode(self).encode('utf-8')+'\n' - - 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 diff --git a/src/libprs500/gui2/lrf_renderer/text.py b/src/libprs500/gui2/lrf_renderer/text.py new file mode 100644 index 0000000000..cd022b7aac --- /dev/null +++ b/src/libprs500/gui2/lrf_renderer/text.py @@ -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()) \ No newline at end of file