%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)