%PDF- %PDF-
| Direktori : /proc/thread-self/root/lib/calibre/calibre/gui2/lrf_renderer/ |
| Current File : //proc/thread-self/root/lib/calibre/calibre/gui2/lrf_renderer/text.py |
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, collections, operator, copy, re, numbers
from qt.core import (
Qt, QRectF, QFont, QColor, QPixmap, QGraphicsPixmapItem, QGraphicsItem,
QFontMetrics, QPen, QBrush, QGraphicsRectItem)
from calibre.ebooks.lrf.fonts import LIBERATION_FONT_MAP
from calibre.ebooks.hyphenate import hyphenate_word
from polyglot.builtins import string_or_bytes
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.ImageConversionFlag.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(int(xsize), int(ysize), Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation)
QGraphicsPixmapItem.__init__(self, p)
self.height, self.width = ysize, xsize
self.setTransformationMode(Qt.TransformationMode.SmoothTransformation)
self.setShapeMode(QGraphicsPixmapItem.ShapeMode.BoundingRectShape)
def resize(self, width, height):
p = self.pixmap()
self.setPixmap(p.scaled(int(width), int(height), Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation))
self.width, self.height = width, height
class Plot(PixmapItem):
def __init__(self, plot, dpi):
img = plot.refobj
xsize, ysize = dpi*plot.attrs['xsize']/720, dpi*plot.attrs['ysize']/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:
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 LIBERATION_FONT_MAP
try:
if device_font:
face = self.font_map[text_style.fontfacename]
else:
face = self.face_map[text_style.fontfacename]
except KeyError: # Bad fontfacename field in LRF
face = self.font_map['Dutch801 Rm BT Roman']
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.Style.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:
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.Style.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:
def __init__(self, parskip):
self.height = parskip
def __str__(self):
return 'Parskip: '+str(self.height)
class TextBlock:
class HeightExceeded(Exception):
pass
has_content = property(fget=lambda self: self.peek_index < len(self.lines)-1)
XML_ENTITIES = {
"apos" : "'",
"quot" : '"',
"amp" : "&",
"lt" : "<",
"gt" : ">"
}
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.maxsize
self.height = 0
self.peek_index = -1
try:
self.populate(tb.content)
self.end_line()
except TextBlock.HeightExceeded:
pass
# 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 close_valign(self):
if self.current_line is not None:
self.current_line.valign = None
def populate(self, tb):
self.create_line()
open_containers = collections.deque()
self.in_para = False
for i in tb.content:
if isinstance(i, string_or_bytes):
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 = getattr(self.current_style, 'parskip', 0)
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.Style.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 in ['Sup', 'Sub']:
if self.current_line is None:
self.create_line()
self.current_line.valign = i.name
open_containers.append(((self.close_valign, []),))
elif i.name == 'Space' and self.current_line is not None:
self.current_line.add_space(i.attrs['xsize'])
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('&%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()
if not pos:
break
def __iter__(self):
yield from self.lines
def __str__(self):
s = ''
for line in self:
s += str(line) + '\n'
return s
class Link(QGraphicsRectItem):
inactive_brush = QBrush(QColor(0xff, 0xff, 0xff, 0xff))
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.brush = self.__class__.inactive_brush
self.setPen(QPen(Qt.PenStyle.NoPen))
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setAcceptHoverEvents(True)
def hoverEnterEvent(self, event):
self.brush = self.__class__.active_brush
self.parentItem().update()
def hoverLeaveEvent(self, event):
self.brush = self.__class__.inactive_brush
self.parentItem().update()
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, self.width = 0, 0, 0
self.links = collections.deque()
self.current_link = None
self.valign = None
if not hasattr(self, 'children'):
self.children = self.childItems
def start_link(self, refobj, slot):
self.current_link = [self.current_width, sys.maxsize, 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)
self.add_space(6)
def populate(self, phrase, ts, process_space=True):
phrase_pos = 0
processed = False
matches = self.__class__.whitespace.finditer(phrase)
font = QFont(ts.font)
if self.valign is not None:
font.setPixelSize(font.pixelSize()/1.5)
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.valign))
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], numbers.Number):
self.tokens[i] *= frac
self.current_width = self.line_length
def finalize(self, baselineskip, linespace, vdebug):
if self.current_link is not None:
self.end_link()
# We justify if line is small and it doesn't have links in it
# If it has links, justification would cause the boundingrect of the link to
# be too small
if self.current_width >= 0.85 * self.line_length and len(self.links) == 0:
self.justify()
self.width = float(self.current_width)
if self.height == 0:
self.height = baselineskip
self.height = float(self.height)
self.vdebug = vdebug
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.GlobalColor.yellow, 1, Qt.PenStyle.DotLine))
painter.drawRect(self.boundingRect())
painter.restore()
painter.save()
painter.setPen(QPen(Qt.PenStyle.NoPen))
for c in self.children():
painter.setBrush(c.brush)
painter.drawRect(c.boundingRect())
painter.restore()
painter.save()
for tok in self.tokens:
if isinstance(tok, numbers.Number):
x += tok
elif isinstance(tok, Word):
painter.setFont(tok.font)
if tok.highlight:
painter.save()
painter.setPen(QPen(Qt.PenStyle.NoPen))
painter.setBrush(QBrush(Qt.GlobalColor.yellow))
painter.drawRect(x, 0, tok.width, tok.height)
painter.restore()
painter.setPen(QPen(tok.text_color))
if tok.valign is None:
painter.drawText(x, y, tok.string)
elif tok.valign == 'Sub':
painter.drawText(x+1, y+self.descent/1.5, tok.string)
elif tok.valign == 'Sup':
painter.drawText(x+1, y-2.*self.descent, tok.string)
x += tok.width
else:
painter.drawPixmap(x, 0, tok.pixmap())
x += tok.width
painter.restore()
def words(self):
for w in self.tokens:
if isinstance(w, Word):
yield w
def search(self, phrase):
tokens = phrase.lower().split()
if len(tokens) < 1:
return None
words = self.words()
matches = []
try:
while True:
word = next(words)
word.highlight = False
if tokens[0] in str(word.string).lower():
matches.append(word)
for c in range(1, len(tokens)):
word = next(words)
print(tokens[c], word.string)
if tokens[c] not in str(word.string):
return None
matches.append(word)
for w in matches:
w.highlight = True
return self
except StopIteration:
return None
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 = ''
for tok in self.tokens:
if isinstance(tok, numbers.Number):
s += ' '
elif isinstance(tok, Word):
s += str(tok.string)
return s
def __str__(self):
return str(self).encode('utf-8')
class Word:
def __init__(self, string, width, height, ts, font, valign):
self.string, self.width, self.height = string, width, height
self.font = font
self.text_color = ts.textcolor
self.highlight = False
self.valign = valign
def main(args=sys.argv):
return 0
if __name__ == '__main__':
sys.exit(main())