%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/ebooks/pdf/render/ |
| Current File : //usr/lib/calibre/calibre/ebooks/pdf/render/graphics.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from math import sqrt
from collections import namedtuple
from qt.core import (
QBrush, QPen, Qt, QPointF, QTransform, QPaintEngine, QImage)
from calibre.ebooks.pdf.render.common import (
Name, Array, fmtnum, Stream, Dictionary)
from calibre.ebooks.pdf.render.serialize import Path
from calibre.ebooks.pdf.render.gradients import LinearGradientPattern
def convert_path(path): # {{{
p = Path()
i = 0
while i < path.elementCount():
elem = path.elementAt(i)
em = (elem.x, elem.y)
i += 1
if elem.isMoveTo():
p.move_to(*em)
elif elem.isLineTo():
p.line_to(*em)
elif elem.isCurveTo():
added = False
if path.elementCount() > i+1:
c1, c2 = path.elementAt(i), path.elementAt(i+1)
if (c1.type == path.CurveToDataElement and c2.type ==
path.CurveToDataElement):
i += 2
p.curve_to(em[0], em[1], c1.x, c1.y, c2.x, c2.y)
added = True
if not added:
raise ValueError('Invalid curve to operation')
return p
# }}}
Brush = namedtuple('Brush', 'origin brush color')
class TilingPattern(Stream):
def __init__(self, cache_key, matrix, w=8, h=8, paint_type=2, compress=False):
Stream.__init__(self, compress=compress)
self.paint_type = paint_type
self.w, self.h = w, h
self.matrix = (matrix.m11(), matrix.m12(), matrix.m21(), matrix.m22(),
matrix.dx(), matrix.dy())
self.resources = Dictionary()
self.cache_key = (self.__class__.__name__, cache_key, self.matrix)
def add_extra_keys(self, d):
d['Type'] = Name('Pattern')
d['PatternType'] = 1
d['PaintType'] = self.paint_type
d['TilingType'] = 1
d['BBox'] = Array([0, 0, self.w, self.h])
d['XStep'] = self.w
d['YStep'] = self.h
d['Matrix'] = Array(self.matrix)
d['Resources'] = self.resources
class QtPattern(TilingPattern):
qt_patterns = ( # {{{
"0 J\n"
"6 w\n"
"[] 0 d\n"
"4 0 m\n"
"4 8 l\n"
"0 4 m\n"
"8 4 l\n"
"S\n", # Dense1Pattern
"0 J\n"
"2 w\n"
"[6 2] 1 d\n"
"0 0 m\n"
"0 8 l\n"
"8 0 m\n"
"8 8 l\n"
"S\n"
"[] 0 d\n"
"2 0 m\n"
"2 8 l\n"
"6 0 m\n"
"6 8 l\n"
"S\n"
"[6 2] -3 d\n"
"4 0 m\n"
"4 8 l\n"
"S\n", # Dense2Pattern
"0 J\n"
"2 w\n"
"[6 2] 1 d\n"
"0 0 m\n"
"0 8 l\n"
"8 0 m\n"
"8 8 l\n"
"S\n"
"[2 2] -1 d\n"
"2 0 m\n"
"2 8 l\n"
"6 0 m\n"
"6 8 l\n"
"S\n"
"[6 2] -3 d\n"
"4 0 m\n"
"4 8 l\n"
"S\n", # Dense3Pattern
"0 J\n"
"2 w\n"
"[2 2] 1 d\n"
"0 0 m\n"
"0 8 l\n"
"8 0 m\n"
"8 8 l\n"
"S\n"
"[2 2] -1 d\n"
"2 0 m\n"
"2 8 l\n"
"6 0 m\n"
"6 8 l\n"
"S\n"
"[2 2] 1 d\n"
"4 0 m\n"
"4 8 l\n"
"S\n", # Dense4Pattern
"0 J\n"
"2 w\n"
"[2 6] -1 d\n"
"0 0 m\n"
"0 8 l\n"
"8 0 m\n"
"8 8 l\n"
"S\n"
"[2 2] 1 d\n"
"2 0 m\n"
"2 8 l\n"
"6 0 m\n"
"6 8 l\n"
"S\n"
"[2 6] 3 d\n"
"4 0 m\n"
"4 8 l\n"
"S\n", # Dense5Pattern
"0 J\n"
"2 w\n"
"[2 6] -1 d\n"
"0 0 m\n"
"0 8 l\n"
"8 0 m\n"
"8 8 l\n"
"S\n"
"[2 6] 3 d\n"
"4 0 m\n"
"4 8 l\n"
"S\n", # Dense6Pattern
"0 J\n"
"2 w\n"
"[2 6] -1 d\n"
"0 0 m\n"
"0 8 l\n"
"8 0 m\n"
"8 8 l\n"
"S\n", # Dense7Pattern
"1 w\n"
"0 4 m\n"
"8 4 l\n"
"S\n", # HorPattern
"1 w\n"
"4 0 m\n"
"4 8 l\n"
"S\n", # VerPattern
"1 w\n"
"4 0 m\n"
"4 8 l\n"
"0 4 m\n"
"8 4 l\n"
"S\n", # CrossPattern
"1 w\n"
"-1 5 m\n"
"5 -1 l\n"
"3 9 m\n"
"9 3 l\n"
"S\n", # BDiagPattern
"1 w\n"
"-1 3 m\n"
"5 9 l\n"
"3 -1 m\n"
"9 5 l\n"
"S\n", # FDiagPattern
"1 w\n"
"-1 3 m\n"
"5 9 l\n"
"3 -1 m\n"
"9 5 l\n"
"-1 5 m\n"
"5 -1 l\n"
"3 9 m\n"
"9 3 l\n"
"S\n", # DiagCrossPattern
) # }}}
def __init__(self, pattern_num, matrix):
super().__init__(pattern_num, matrix)
self.write(self.qt_patterns[pattern_num-2])
class TexturePattern(TilingPattern):
def __init__(self, pixmap, matrix, pdf, clone=None):
if clone is None:
image = pixmap.toImage()
cache_key = pixmap.cacheKey()
imgref = pdf.add_image(image, cache_key)
paint_type = (2 if image.format() in {QImage.Format.Format_MonoLSB,
QImage.Format.Format_Mono} else 1)
super().__init__(
cache_key, matrix, w=image.width(), h=image.height(),
paint_type=paint_type)
m = (self.w, 0, 0, -self.h, 0, self.h)
self.resources['XObject'] = Dictionary({'Texture':imgref})
self.write_line('%s cm /Texture Do'%(' '.join(map(fmtnum, m))))
else:
super().__init__(
clone.cache_key[1], matrix, w=clone.w, h=clone.h,
paint_type=clone.paint_type)
self.resources['XObject'] = Dictionary(clone.resources['XObject'])
self.write(clone.getvalue())
class GraphicsState:
FIELDS = ('fill', 'stroke', 'opacity', 'transform', 'brush_origin',
'clip_updated', 'do_fill', 'do_stroke')
def __init__(self):
self.fill = QBrush(Qt.GlobalColor.white)
self.stroke = QPen()
self.opacity = 1.0
self.transform = QTransform()
self.brush_origin = QPointF()
self.clip_updated = False
self.do_fill = False
self.do_stroke = True
self.qt_pattern_cache = {}
def __eq__(self, other):
for x in self.FIELDS:
if getattr(other, x) != getattr(self, x):
return False
return True
def copy(self):
ans = GraphicsState()
ans.fill = QBrush(self.fill)
ans.stroke = QPen(self.stroke)
ans.opacity = self.opacity
ans.transform = self.transform * QTransform()
ans.brush_origin = QPointF(self.brush_origin)
ans.clip_updated = self.clip_updated
ans.do_fill, ans.do_stroke = self.do_fill, self.do_stroke
return ans
class Graphics:
def __init__(self, page_width_px, page_height_px):
self.base_state = GraphicsState()
self.current_state = GraphicsState()
self.pending_state = None
self.page_width_px, self.page_height_px = (page_width_px, page_height_px)
def begin(self, pdf):
self.pdf = pdf
def update_state(self, state, painter):
flags = state.state()
if self.pending_state is None:
self.pending_state = self.current_state.copy()
s = self.pending_state
if flags & QPaintEngine.DirtyFlag.DirtyTransform:
s.transform = state.transform()
if flags & QPaintEngine.DirtyFlag.DirtyBrushOrigin:
s.brush_origin = state.brushOrigin()
if flags & QPaintEngine.DirtyFlag.DirtyBrush:
s.fill = state.brush()
if flags & QPaintEngine.DirtyFlag.DirtyPen:
s.stroke = state.pen()
if flags & QPaintEngine.DirtyFlag.DirtyOpacity:
s.opacity = state.opacity()
if flags & QPaintEngine.DirtyFlag.DirtyClipPath or flags & QPaintEngine.DirtyFlag.DirtyClipRegion:
s.clip_updated = True
def reset(self):
self.current_state = GraphicsState()
self.pending_state = None
def __call__(self, pdf_system, painter):
# Apply the currently pending state to the PDF
if self.pending_state is None:
return
pdf_state = self.current_state
ps = self.pending_state
pdf = self.pdf
if ps.transform != pdf_state.transform or ps.clip_updated:
pdf.restore_stack()
pdf.save_stack()
pdf_state = self.base_state
if (pdf_state.transform != ps.transform):
pdf.transform(ps.transform)
if (pdf_state.opacity != ps.opacity or pdf_state.stroke != ps.stroke):
self.apply_stroke(ps, pdf_system, painter)
if (pdf_state.opacity != ps.opacity or pdf_state.fill != ps.fill or
pdf_state.brush_origin != ps.brush_origin):
self.apply_fill(ps, pdf_system, painter)
if ps.clip_updated:
ps.clip_updated = False
path = painter.clipPath()
if not path.isEmpty():
p = convert_path(path)
fill_rule = {Qt.FillRule.OddEvenFill:'evenodd',
Qt.FillRule.WindingFill:'winding'}[path.fillRule()]
pdf.add_clip(p, fill_rule=fill_rule)
self.current_state = self.pending_state
self.pending_state = None
def convert_brush(self, brush, brush_origin, global_opacity,
pdf_system, qt_system):
# Convert a QBrush to PDF operators
style = brush.style()
pdf = self.pdf
pattern = color = pat = None
opacity = global_opacity
do_fill = True
matrix = (QTransform.fromTranslate(brush_origin.x(), brush_origin.y()) * pdf_system * qt_system.inverted()[0])
vals = list(brush.color().getRgbF())
self.brushobj = None
if style <= Qt.BrushStyle.DiagCrossPattern:
opacity *= vals[-1]
color = vals[:3]
if style > Qt.BrushStyle.SolidPattern:
pat = QtPattern(style, matrix)
elif style == Qt.BrushStyle.TexturePattern:
pat = TexturePattern(brush.texture(), matrix, pdf)
if pat.paint_type == 2:
opacity *= vals[-1]
color = vals[:3]
elif style == Qt.BrushStyle.LinearGradientPattern:
pat = LinearGradientPattern(brush, matrix, pdf, self.page_width_px,
self.page_height_px)
opacity *= pat.const_opacity
# TODO: Add support for radial/conical gradient fills
if opacity < 1e-4 or style == Qt.BrushStyle.NoBrush:
do_fill = False
self.brushobj = Brush(brush_origin, pat, color)
if pat is not None:
pattern = pdf.add_pattern(pat)
return color, opacity, pattern, do_fill
def apply_stroke(self, state, pdf_system, painter):
# TODO: Support miter limit by using QPainterPathStroker
pen = state.stroke
self.pending_state.do_stroke = True
pdf = self.pdf
# Width
w = pen.widthF()
if pen.isCosmetic():
t = painter.transform()
try:
w /= sqrt(t.m11()**2 + t.m22()**2)
except ZeroDivisionError:
pass
pdf.serialize(w)
pdf.current_page.write(' w ')
# Line cap
cap = {Qt.PenCapStyle.FlatCap:0, Qt.PenCapStyle.RoundCap:1, Qt.PenCapStyle.SquareCap:
2}.get(pen.capStyle(), 0)
pdf.current_page.write('%d J '%cap)
# Line join
join = {Qt.PenJoinStyle.MiterJoin:0, Qt.PenJoinStyle.RoundJoin:1,
Qt.PenJoinStyle.BevelJoin:2}.get(pen.joinStyle(), 0)
pdf.current_page.write('%d j '%join)
# Dash pattern
if pen.style() == Qt.PenStyle.CustomDashLine:
pdf.serialize(Array(pen.dashPattern()))
pdf.current_page.write(' %d d ' % pen.dashOffset())
else:
ps = {Qt.PenStyle.DashLine:[3], Qt.PenStyle.DotLine:[1,2], Qt.PenStyle.DashDotLine:[3,2,1,2],
Qt.PenStyle.DashDotDotLine:[3, 2, 1, 2, 1, 2]}.get(pen.style(), [])
pdf.serialize(Array(ps))
pdf.current_page.write(' 0 d ')
# Stroke fill
color, opacity, pattern, self.pending_state.do_stroke = self.convert_brush(
pen.brush(), state.brush_origin, state.opacity, pdf_system,
painter.transform())
self.pdf.apply_stroke(color, pattern, opacity)
if pen.style() == Qt.PenStyle.NoPen:
self.pending_state.do_stroke = False
def apply_fill(self, state, pdf_system, painter):
self.pending_state.do_fill = True
color, opacity, pattern, self.pending_state.do_fill = self.convert_brush(
state.fill, state.brush_origin, state.opacity, pdf_system,
painter.transform())
self.pdf.apply_fill(color, pattern, opacity)
self.last_fill = self.brushobj
def __enter__(self):
self.pdf.save_stack()
def __exit__(self, *args):
self.pdf.restore_stack()
def resolve_fill(self, rect, pdf_system, qt_system):
'''
Qt's paint system does not update brushOrigin when using
TexturePatterns and it also uses TexturePatterns to emulate gradients,
leading to brokenness. So this method allows the paint engine to update
the brush origin before painting an object. While not perfect, this is
better than nothing. The problem is that if the rect being filled has a
border, then QtWebKit generates an image of the rect size - border but
fills the full rect, and there's no way for the paint engine to know
that and adjust the brush origin.
'''
if not hasattr(self, 'last_fill') or not self.current_state.do_fill:
return
if isinstance(self.last_fill.brush, TexturePattern):
tl = rect.topLeft()
if tl == self.last_fill.origin:
return
matrix = (QTransform.fromTranslate(tl.x(), tl.y()) * pdf_system * qt_system.inverted()[0])
pat = TexturePattern(None, matrix, self.pdf, clone=self.last_fill.brush)
pattern = self.pdf.add_pattern(pat)
self.pdf.apply_fill(self.last_fill.color, pattern)