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