mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Re-implemented rendering engine without using Qt's rich text facilities. Much faster this way.
This commit is contained in:
parent
126a8771fe
commit
4da344abf5
@ -14,23 +14,20 @@
|
|||||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
## 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, \
|
from PyQt4.QtGui import QGraphicsRectItem, QGraphicsScene, QPen, \
|
||||||
QBrush, QColor, QGraphicsTextItem, QFontDatabase, \
|
QBrush, QColor, QFontDatabase, \
|
||||||
QFont, QGraphicsItem, QGraphicsLineItem, QPixmap, \
|
QGraphicsItem, QGraphicsLineItem
|
||||||
QGraphicsPixmapItem, QTextCharFormat, QTextFrameFormat, \
|
|
||||||
QTextBlockFormat, QTextCursor, QTextImageFormat, \
|
from libprs500.gui2.lrf_renderer.text import TextBlock, FontLoader, COLOR, PixmapItem
|
||||||
QTextDocument, QTextOption
|
|
||||||
|
|
||||||
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 RuledLine as _RuledLine
|
||||||
from libprs500.ebooks.lrf.objects import Canvas as __Canvas
|
from libprs500.ebooks.lrf.objects import Canvas as __Canvas
|
||||||
|
|
||||||
|
|
||||||
class Color(QColor):
|
class Color(QColor):
|
||||||
def __init__(self, color):
|
def __init__(self, color):
|
||||||
QColor.__init__(self, color.r, color.g, color.b, 0xff-color.a)
|
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,
|
QPen.__init__(self, QBrush(Color(color)), width,
|
||||||
(Qt.SolidLine if width > 0 else Qt.NoPen))
|
(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):
|
class ContentObject(object):
|
||||||
|
|
||||||
@ -323,226 +46,6 @@ class ContentObject(object):
|
|||||||
self.has_content = True
|
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):
|
class RuledLine(QGraphicsLineItem, ContentObject):
|
||||||
|
|
||||||
map = {'solid': Qt.SolidLine, 'dashed': Qt.DashLine, 'dotted': Qt.DotLine, 'double': Qt.DashDotLine}
|
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):
|
def object_factory(container, obj, respect_max_y=False):
|
||||||
if hasattr(obj, 'name'):
|
if hasattr(obj, 'name'):
|
||||||
if obj.name.endswith('TextBlock'):
|
if obj.name.endswith('TextBlock'):
|
||||||
|
|
||||||
return TextBlock(obj, container.font_loader, respect_max_y, container.text_width,
|
return TextBlock(obj, container.font_loader, respect_max_y, container.text_width,
|
||||||
container.logger,
|
container.logger, container.opts, container.ruby_tags,
|
||||||
container.opts, container.ruby_tags, container.link_activated)
|
container.link_activated)
|
||||||
elif obj.name.endswith('ImageBlock'):
|
elif obj.name.endswith('ImageBlock'):
|
||||||
return ImageBlock(obj)
|
return ImageBlock(obj)
|
||||||
elif isinstance(obj, _RuledLine):
|
elif isinstance(obj, _RuledLine):
|
||||||
@ -623,7 +127,9 @@ class _Canvas(QGraphicsRectItem):
|
|||||||
if isinstance(line, QGraphicsItem):
|
if isinstance(line, QGraphicsItem):
|
||||||
line.setParentItem(self)
|
line.setParentItem(self)
|
||||||
line.setPos(x + line.getx(textwidth), y)
|
line.setPos(x + line.getx(textwidth), y)
|
||||||
y += line.height
|
y += line.height
|
||||||
|
else:
|
||||||
|
y += line.height
|
||||||
if not block.has_content:
|
if not block.has_content:
|
||||||
y += block.bs.footskip
|
y += block.bs.footskip
|
||||||
block_consumed = True
|
block_consumed = True
|
||||||
|
520
src/libprs500/gui2/lrf_renderer/text.py
Normal file
520
src/libprs500/gui2/lrf_renderer/text.py
Normal 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())
|
Loading…
x
Reference in New Issue
Block a user