mirror of
				https://github.com/kovidgoyal/calibre.git
				synced 2025-11-04 03:27:00 -05: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.
 | 
			
		||||
''''''
 | 
			
		||||
 | 
			
		||||
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):
 | 
			
		||||
@ -624,6 +128,8 @@ class _Canvas(QGraphicsRectItem):
 | 
			
		||||
                line.setParentItem(self)
 | 
			
		||||
                line.setPos(x + line.getx(textwidth), y)
 | 
			
		||||
                y += line.height
 | 
			
		||||
            else:
 | 
			
		||||
                y += line.height
 | 
			
		||||
            if not block.has_content:
 | 
			
		||||
                y += block.bs.footskip
 | 
			
		||||
                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