1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>' 7__docformat__ = 'restructuredtext en' 8 9from math import sqrt 10from collections import namedtuple 11 12from qt.core import ( 13 QBrush, QPen, Qt, QPointF, QTransform, QPaintEngine, QImage) 14 15from calibre.ebooks.pdf.render.common import ( 16 Name, Array, fmtnum, Stream, Dictionary) 17from calibre.ebooks.pdf.render.serialize import Path 18from calibre.ebooks.pdf.render.gradients import LinearGradientPattern 19 20 21def convert_path(path): # {{{ 22 p = Path() 23 i = 0 24 while i < path.elementCount(): 25 elem = path.elementAt(i) 26 em = (elem.x, elem.y) 27 i += 1 28 if elem.isMoveTo(): 29 p.move_to(*em) 30 elif elem.isLineTo(): 31 p.line_to(*em) 32 elif elem.isCurveTo(): 33 added = False 34 if path.elementCount() > i+1: 35 c1, c2 = path.elementAt(i), path.elementAt(i+1) 36 if (c1.type == path.CurveToDataElement and c2.type == 37 path.CurveToDataElement): 38 i += 2 39 p.curve_to(em[0], em[1], c1.x, c1.y, c2.x, c2.y) 40 added = True 41 if not added: 42 raise ValueError('Invalid curve to operation') 43 return p 44# }}} 45 46 47Brush = namedtuple('Brush', 'origin brush color') 48 49 50class TilingPattern(Stream): 51 52 def __init__(self, cache_key, matrix, w=8, h=8, paint_type=2, compress=False): 53 Stream.__init__(self, compress=compress) 54 self.paint_type = paint_type 55 self.w, self.h = w, h 56 self.matrix = (matrix.m11(), matrix.m12(), matrix.m21(), matrix.m22(), 57 matrix.dx(), matrix.dy()) 58 self.resources = Dictionary() 59 self.cache_key = (self.__class__.__name__, cache_key, self.matrix) 60 61 def add_extra_keys(self, d): 62 d['Type'] = Name('Pattern') 63 d['PatternType'] = 1 64 d['PaintType'] = self.paint_type 65 d['TilingType'] = 1 66 d['BBox'] = Array([0, 0, self.w, self.h]) 67 d['XStep'] = self.w 68 d['YStep'] = self.h 69 d['Matrix'] = Array(self.matrix) 70 d['Resources'] = self.resources 71 72 73class QtPattern(TilingPattern): 74 75 qt_patterns = ( # {{{ 76 "0 J\n" 77 "6 w\n" 78 "[] 0 d\n" 79 "4 0 m\n" 80 "4 8 l\n" 81 "0 4 m\n" 82 "8 4 l\n" 83 "S\n", # Dense1Pattern 84 85 "0 J\n" 86 "2 w\n" 87 "[6 2] 1 d\n" 88 "0 0 m\n" 89 "0 8 l\n" 90 "8 0 m\n" 91 "8 8 l\n" 92 "S\n" 93 "[] 0 d\n" 94 "2 0 m\n" 95 "2 8 l\n" 96 "6 0 m\n" 97 "6 8 l\n" 98 "S\n" 99 "[6 2] -3 d\n" 100 "4 0 m\n" 101 "4 8 l\n" 102 "S\n", # Dense2Pattern 103 104 "0 J\n" 105 "2 w\n" 106 "[6 2] 1 d\n" 107 "0 0 m\n" 108 "0 8 l\n" 109 "8 0 m\n" 110 "8 8 l\n" 111 "S\n" 112 "[2 2] -1 d\n" 113 "2 0 m\n" 114 "2 8 l\n" 115 "6 0 m\n" 116 "6 8 l\n" 117 "S\n" 118 "[6 2] -3 d\n" 119 "4 0 m\n" 120 "4 8 l\n" 121 "S\n", # Dense3Pattern 122 123 "0 J\n" 124 "2 w\n" 125 "[2 2] 1 d\n" 126 "0 0 m\n" 127 "0 8 l\n" 128 "8 0 m\n" 129 "8 8 l\n" 130 "S\n" 131 "[2 2] -1 d\n" 132 "2 0 m\n" 133 "2 8 l\n" 134 "6 0 m\n" 135 "6 8 l\n" 136 "S\n" 137 "[2 2] 1 d\n" 138 "4 0 m\n" 139 "4 8 l\n" 140 "S\n", # Dense4Pattern 141 142 "0 J\n" 143 "2 w\n" 144 "[2 6] -1 d\n" 145 "0 0 m\n" 146 "0 8 l\n" 147 "8 0 m\n" 148 "8 8 l\n" 149 "S\n" 150 "[2 2] 1 d\n" 151 "2 0 m\n" 152 "2 8 l\n" 153 "6 0 m\n" 154 "6 8 l\n" 155 "S\n" 156 "[2 6] 3 d\n" 157 "4 0 m\n" 158 "4 8 l\n" 159 "S\n", # Dense5Pattern 160 161 "0 J\n" 162 "2 w\n" 163 "[2 6] -1 d\n" 164 "0 0 m\n" 165 "0 8 l\n" 166 "8 0 m\n" 167 "8 8 l\n" 168 "S\n" 169 "[2 6] 3 d\n" 170 "4 0 m\n" 171 "4 8 l\n" 172 "S\n", # Dense6Pattern 173 174 "0 J\n" 175 "2 w\n" 176 "[2 6] -1 d\n" 177 "0 0 m\n" 178 "0 8 l\n" 179 "8 0 m\n" 180 "8 8 l\n" 181 "S\n", # Dense7Pattern 182 183 "1 w\n" 184 "0 4 m\n" 185 "8 4 l\n" 186 "S\n", # HorPattern 187 188 "1 w\n" 189 "4 0 m\n" 190 "4 8 l\n" 191 "S\n", # VerPattern 192 193 "1 w\n" 194 "4 0 m\n" 195 "4 8 l\n" 196 "0 4 m\n" 197 "8 4 l\n" 198 "S\n", # CrossPattern 199 200 "1 w\n" 201 "-1 5 m\n" 202 "5 -1 l\n" 203 "3 9 m\n" 204 "9 3 l\n" 205 "S\n", # BDiagPattern 206 207 "1 w\n" 208 "-1 3 m\n" 209 "5 9 l\n" 210 "3 -1 m\n" 211 "9 5 l\n" 212 "S\n", # FDiagPattern 213 214 "1 w\n" 215 "-1 3 m\n" 216 "5 9 l\n" 217 "3 -1 m\n" 218 "9 5 l\n" 219 "-1 5 m\n" 220 "5 -1 l\n" 221 "3 9 m\n" 222 "9 3 l\n" 223 "S\n", # DiagCrossPattern 224 ) # }}} 225 226 def __init__(self, pattern_num, matrix): 227 super().__init__(pattern_num, matrix) 228 self.write(self.qt_patterns[pattern_num-2]) 229 230 231class TexturePattern(TilingPattern): 232 233 def __init__(self, pixmap, matrix, pdf, clone=None): 234 if clone is None: 235 image = pixmap.toImage() 236 cache_key = pixmap.cacheKey() 237 imgref = pdf.add_image(image, cache_key) 238 paint_type = (2 if image.format() in {QImage.Format.Format_MonoLSB, 239 QImage.Format.Format_Mono} else 1) 240 super().__init__( 241 cache_key, matrix, w=image.width(), h=image.height(), 242 paint_type=paint_type) 243 m = (self.w, 0, 0, -self.h, 0, self.h) 244 self.resources['XObject'] = Dictionary({'Texture':imgref}) 245 self.write_line('%s cm /Texture Do'%(' '.join(map(fmtnum, m)))) 246 else: 247 super().__init__( 248 clone.cache_key[1], matrix, w=clone.w, h=clone.h, 249 paint_type=clone.paint_type) 250 self.resources['XObject'] = Dictionary(clone.resources['XObject']) 251 self.write(clone.getvalue()) 252 253 254class GraphicsState: 255 256 FIELDS = ('fill', 'stroke', 'opacity', 'transform', 'brush_origin', 257 'clip_updated', 'do_fill', 'do_stroke') 258 259 def __init__(self): 260 self.fill = QBrush(Qt.GlobalColor.white) 261 self.stroke = QPen() 262 self.opacity = 1.0 263 self.transform = QTransform() 264 self.brush_origin = QPointF() 265 self.clip_updated = False 266 self.do_fill = False 267 self.do_stroke = True 268 self.qt_pattern_cache = {} 269 270 def __eq__(self, other): 271 for x in self.FIELDS: 272 if getattr(other, x) != getattr(self, x): 273 return False 274 return True 275 276 def copy(self): 277 ans = GraphicsState() 278 ans.fill = QBrush(self.fill) 279 ans.stroke = QPen(self.stroke) 280 ans.opacity = self.opacity 281 ans.transform = self.transform * QTransform() 282 ans.brush_origin = QPointF(self.brush_origin) 283 ans.clip_updated = self.clip_updated 284 ans.do_fill, ans.do_stroke = self.do_fill, self.do_stroke 285 return ans 286 287 288class Graphics: 289 290 def __init__(self, page_width_px, page_height_px): 291 self.base_state = GraphicsState() 292 self.current_state = GraphicsState() 293 self.pending_state = None 294 self.page_width_px, self.page_height_px = (page_width_px, page_height_px) 295 296 def begin(self, pdf): 297 self.pdf = pdf 298 299 def update_state(self, state, painter): 300 flags = state.state() 301 if self.pending_state is None: 302 self.pending_state = self.current_state.copy() 303 304 s = self.pending_state 305 306 if flags & QPaintEngine.DirtyFlag.DirtyTransform: 307 s.transform = state.transform() 308 309 if flags & QPaintEngine.DirtyFlag.DirtyBrushOrigin: 310 s.brush_origin = state.brushOrigin() 311 312 if flags & QPaintEngine.DirtyFlag.DirtyBrush: 313 s.fill = state.brush() 314 315 if flags & QPaintEngine.DirtyFlag.DirtyPen: 316 s.stroke = state.pen() 317 318 if flags & QPaintEngine.DirtyFlag.DirtyOpacity: 319 s.opacity = state.opacity() 320 321 if flags & QPaintEngine.DirtyFlag.DirtyClipPath or flags & QPaintEngine.DirtyFlag.DirtyClipRegion: 322 s.clip_updated = True 323 324 def reset(self): 325 self.current_state = GraphicsState() 326 self.pending_state = None 327 328 def __call__(self, pdf_system, painter): 329 # Apply the currently pending state to the PDF 330 if self.pending_state is None: 331 return 332 333 pdf_state = self.current_state 334 ps = self.pending_state 335 pdf = self.pdf 336 337 if ps.transform != pdf_state.transform or ps.clip_updated: 338 pdf.restore_stack() 339 pdf.save_stack() 340 pdf_state = self.base_state 341 342 if (pdf_state.transform != ps.transform): 343 pdf.transform(ps.transform) 344 345 if (pdf_state.opacity != ps.opacity or pdf_state.stroke != ps.stroke): 346 self.apply_stroke(ps, pdf_system, painter) 347 348 if (pdf_state.opacity != ps.opacity or pdf_state.fill != ps.fill or 349 pdf_state.brush_origin != ps.brush_origin): 350 self.apply_fill(ps, pdf_system, painter) 351 352 if ps.clip_updated: 353 ps.clip_updated = False 354 path = painter.clipPath() 355 if not path.isEmpty(): 356 p = convert_path(path) 357 fill_rule = {Qt.FillRule.OddEvenFill:'evenodd', 358 Qt.FillRule.WindingFill:'winding'}[path.fillRule()] 359 pdf.add_clip(p, fill_rule=fill_rule) 360 361 self.current_state = self.pending_state 362 self.pending_state = None 363 364 def convert_brush(self, brush, brush_origin, global_opacity, 365 pdf_system, qt_system): 366 # Convert a QBrush to PDF operators 367 style = brush.style() 368 pdf = self.pdf 369 370 pattern = color = pat = None 371 opacity = global_opacity 372 do_fill = True 373 374 matrix = (QTransform.fromTranslate(brush_origin.x(), brush_origin.y()) * pdf_system * qt_system.inverted()[0]) 375 vals = list(brush.color().getRgbF()) 376 self.brushobj = None 377 378 if style <= Qt.BrushStyle.DiagCrossPattern: 379 opacity *= vals[-1] 380 color = vals[:3] 381 382 if style > Qt.BrushStyle.SolidPattern: 383 pat = QtPattern(style, matrix) 384 385 elif style == Qt.BrushStyle.TexturePattern: 386 pat = TexturePattern(brush.texture(), matrix, pdf) 387 if pat.paint_type == 2: 388 opacity *= vals[-1] 389 color = vals[:3] 390 391 elif style == Qt.BrushStyle.LinearGradientPattern: 392 pat = LinearGradientPattern(brush, matrix, pdf, self.page_width_px, 393 self.page_height_px) 394 opacity *= pat.const_opacity 395 # TODO: Add support for radial/conical gradient fills 396 397 if opacity < 1e-4 or style == Qt.BrushStyle.NoBrush: 398 do_fill = False 399 self.brushobj = Brush(brush_origin, pat, color) 400 401 if pat is not None: 402 pattern = pdf.add_pattern(pat) 403 return color, opacity, pattern, do_fill 404 405 def apply_stroke(self, state, pdf_system, painter): 406 # TODO: Support miter limit by using QPainterPathStroker 407 pen = state.stroke 408 self.pending_state.do_stroke = True 409 pdf = self.pdf 410 411 # Width 412 w = pen.widthF() 413 if pen.isCosmetic(): 414 t = painter.transform() 415 try: 416 w /= sqrt(t.m11()**2 + t.m22()**2) 417 except ZeroDivisionError: 418 pass 419 pdf.serialize(w) 420 pdf.current_page.write(' w ') 421 422 # Line cap 423 cap = {Qt.PenCapStyle.FlatCap:0, Qt.PenCapStyle.RoundCap:1, Qt.PenCapStyle.SquareCap: 424 2}.get(pen.capStyle(), 0) 425 pdf.current_page.write('%d J '%cap) 426 427 # Line join 428 join = {Qt.PenJoinStyle.MiterJoin:0, Qt.PenJoinStyle.RoundJoin:1, 429 Qt.PenJoinStyle.BevelJoin:2}.get(pen.joinStyle(), 0) 430 pdf.current_page.write('%d j '%join) 431 432 # Dash pattern 433 if pen.style() == Qt.PenStyle.CustomDashLine: 434 pdf.serialize(Array(pen.dashPattern())) 435 pdf.current_page.write(' %d d ' % pen.dashOffset()) 436 else: 437 ps = {Qt.PenStyle.DashLine:[3], Qt.PenStyle.DotLine:[1,2], Qt.PenStyle.DashDotLine:[3,2,1,2], 438 Qt.PenStyle.DashDotDotLine:[3, 2, 1, 2, 1, 2]}.get(pen.style(), []) 439 pdf.serialize(Array(ps)) 440 pdf.current_page.write(' 0 d ') 441 442 # Stroke fill 443 color, opacity, pattern, self.pending_state.do_stroke = self.convert_brush( 444 pen.brush(), state.brush_origin, state.opacity, pdf_system, 445 painter.transform()) 446 self.pdf.apply_stroke(color, pattern, opacity) 447 if pen.style() == Qt.PenStyle.NoPen: 448 self.pending_state.do_stroke = False 449 450 def apply_fill(self, state, pdf_system, painter): 451 self.pending_state.do_fill = True 452 color, opacity, pattern, self.pending_state.do_fill = self.convert_brush( 453 state.fill, state.brush_origin, state.opacity, pdf_system, 454 painter.transform()) 455 self.pdf.apply_fill(color, pattern, opacity) 456 self.last_fill = self.brushobj 457 458 def __enter__(self): 459 self.pdf.save_stack() 460 461 def __exit__(self, *args): 462 self.pdf.restore_stack() 463 464 def resolve_fill(self, rect, pdf_system, qt_system): 465 ''' 466 Qt's paint system does not update brushOrigin when using 467 TexturePatterns and it also uses TexturePatterns to emulate gradients, 468 leading to brokenness. So this method allows the paint engine to update 469 the brush origin before painting an object. While not perfect, this is 470 better than nothing. The problem is that if the rect being filled has a 471 border, then QtWebKit generates an image of the rect size - border but 472 fills the full rect, and there's no way for the paint engine to know 473 that and adjust the brush origin. 474 ''' 475 if not hasattr(self, 'last_fill') or not self.current_state.do_fill: 476 return 477 478 if isinstance(self.last_fill.brush, TexturePattern): 479 tl = rect.topLeft() 480 if tl == self.last_fill.origin: 481 return 482 483 matrix = (QTransform.fromTranslate(tl.x(), tl.y()) * pdf_system * qt_system.inverted()[0]) 484 485 pat = TexturePattern(None, matrix, self.pdf, clone=self.last_fill.brush) 486 pattern = self.pdf.add_pattern(pat) 487 self.pdf.apply_fill(self.last_fill.color, pattern) 488