From 9f13e30737ef809f731c1c576519acc554b6e63d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 31 Dec 2012 18:42:08 +0530 Subject: [PATCH] Workaround for Qt's broken emulation of gradients with texture patterns --- src/calibre/ebooks/pdf/render/engine.py | 19 +++-- src/calibre/ebooks/pdf/render/graphics.py | 94 +++++++++++++++++------ src/calibre/ebooks/pdf/render/test.py | 17 ++-- 3 files changed, 93 insertions(+), 37 deletions(-) diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py index 6afbef223f..77f1f00c57 100644 --- a/src/calibre/ebooks/pdf/render/engine.py +++ b/src/calibre/ebooks/pdf/render/engine.py @@ -93,7 +93,11 @@ class PdfEngine(QPaintEngine): raise RuntimeError('Failed to load qt_hack with err: %s'%err) def apply_graphics_state(self): - self.graphics(self.pdf, self.pdf_system, self.painter()) + self.graphics(self.pdf_system, self.painter()) + + def resolve_fill(self, rect): + self.graphics.resolve_fill(rect, self.pdf_system, + self.painter().transform()) @property def do_fill(self): @@ -117,6 +121,7 @@ class PdfEngine(QPaintEngine): self.page_height), compress=self.compress, mark_links=self.mark_links, debug=self.debug) + self.graphics.begin(self.pdf) except: self.errors(traceback.format_exc()) self.errors_occurred = True @@ -155,7 +160,7 @@ class PdfEngine(QPaintEngine): brush = QBrush(pixmap) bl = rect.topLeft() color, opacity, pattern, do_fill = self.graphics.convert_brush( - brush, bl-point, 1.0, self.pdf, self.pdf_system, + brush, bl-point, 1.0, self.pdf_system, self.painter().transform()) self.pdf.save_stack() self.pdf.apply_fill(color, pattern) @@ -211,10 +216,12 @@ class PdfEngine(QPaintEngine): @store_error def drawRects(self, rects): self.apply_graphics_state() - for rect in rects: - bl = rect.topLeft() - self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), - stroke=self.do_stroke, fill=self.do_fill) + with self.graphics: + for rect in rects: + self.resolve_fill(rect) + bl = rect.topLeft() + self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), + stroke=self.do_stroke, fill=self.do_fill) def create_sfnt(self, text_item): get_table = partial(self.qt_hack.get_sfnt_table, text_item) diff --git a/src/calibre/ebooks/pdf/render/graphics.py b/src/calibre/ebooks/pdf/render/graphics.py index 384809598a..f9353d5358 100644 --- a/src/calibre/ebooks/pdf/render/graphics.py +++ b/src/calibre/ebooks/pdf/render/graphics.py @@ -8,6 +8,7 @@ __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' from math import sqrt +from collections import namedtuple from PyQt4.Qt import ( QBrush, QPen, Qt, QPointF, QTransform, QPainterPath, QPaintEngine, QImage) @@ -41,6 +42,8 @@ def convert_path(path): # {{{ 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): @@ -222,18 +225,25 @@ class QtPattern(TilingPattern): class TexturePattern(TilingPattern): - def __init__(self, pixmap, matrix, pdf): - image = pixmap.toImage() - cache_key = pixmap.cacheKey() - imgref = pdf.add_image(image, cache_key) - paint_type = (2 if image.format() in {QImage.Format_MonoLSB, - QImage.Format_Mono} else 1) - super(TexturePattern, self).__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)))) + 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_MonoLSB, + QImage.Format_Mono} else 1) + super(TexturePattern, self).__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(TexturePattern, self).__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(object): @@ -275,6 +285,9 @@ class Graphics(object): self.current_state = GraphicsState() self.pending_state = None + def begin(self, pdf): + self.pdf = pdf + def update_state(self, state, painter): flags = state.state() if self.pending_state is None: @@ -304,13 +317,14 @@ class Graphics(object): self.current_state = GraphicsState() self.pending_state = None - def __call__(self, pdf, pdf_system, painter): + 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 != pdf_state.clip): pdf.restore_stack() @@ -321,11 +335,11 @@ class Graphics(object): pdf.transform(ps.transform) if (pdf_state.opacity != ps.opacity or pdf_state.stroke != ps.stroke): - self.apply_stroke(ps, pdf, pdf_system, painter) + 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, pdf_system, painter) + self.apply_fill(ps, pdf_system, painter) if (pdf_state.clip != ps.clip): p = convert_path(ps.clip) @@ -336,25 +350,28 @@ class Graphics(object): self.current_state = self.pending_state self.pending_state = None - def convert_brush(self, brush, brush_origin, global_opacity, pdf, + 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 = None + pattern = color = pat = None opacity = 1.0 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.DiagCrossPattern: opacity = global_opacity * vals[-1] color = vals[:3] if style > Qt.SolidPattern: - pattern = pdf.add_pattern(QtPattern(style, matrix)) + pat = QtPattern(style, matrix) + pattern = pdf.add_pattern(pat) if opacity < 1e-4 or style == Qt.NoBrush: do_fill = False @@ -370,15 +387,17 @@ class Graphics(object): if opacity < 1e-4 or style == Qt.NoBrush: do_fill = False + self.brushobj = Brush(brush_origin, pat, color) # TODO: Add support for gradient fills return color, opacity, pattern, do_fill - def apply_stroke(self, state, pdf, pdf_system, painter): + def apply_stroke(self, state, pdf_system, painter): # TODO: Handle pens with non solid brushes by setting the colorspace # for stroking to a pattern # TODO: Support miter limit by using QPainterPathStroker pen = state.stroke self.pending_state.do_stroke = True + pdf = self.pdf if pen.style() == Qt.NoPen: self.pending_state.do_stroke = False @@ -417,10 +436,41 @@ class Graphics(object): if vals[-1] < 1e-5 or b.style() == Qt.NoBrush: self.pending_state.do_stroke = False - def apply_fill(self, state, pdf, pdf_system, painter): + 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, pdf_system, + state.fill, state.brush_origin, state.opacity, pdf_system, painter.transform()) - pdf.apply_fill(color, pattern, opacity) + 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. + ''' + if 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) + diff --git a/src/calibre/ebooks/pdf/render/test.py b/src/calibre/ebooks/pdf/render/test.py index 55fd5f8aea..555af9206f 100644 --- a/src/calibre/ebooks/pdf/render/test.py +++ b/src/calibre/ebooks/pdf/render/test.py @@ -83,21 +83,20 @@ def run(dev, func): raise SystemExit(1) def brush(p, xmax, ymax): - x = y = 0 - w = xmax/3 + 10 - g = QLinearGradient(QPointF(0, 0), QPointF(0, w)) - g.setSpread(g.RepeatSpread) - g.setColorAt(0, QColor('#00f')) - g.setColorAt(1, QColor('#fff')) - p.fillRect(x, y, w, w, QBrush(g)) - p.drawRect(x, y, w, w) + x = xmax/3 + y = 0 + w = xmax/2 + pix = QPixmap(I('console.png')) + p.fillRect(x, y, w, w, QBrush(pix)) + + p.fillRect(0, y+xmax/1.9, w, w, QBrush(pix)) def main(): app = QApplication([]) app tdir = gettempdir() pdf = os.path.join(tdir, 'painter.pdf') - func = brush + func = full dpi = 100 with open(pdf, 'wb') as f: dev = PdfDevice(f, xdpi=dpi, ydpi=dpi, compress=False)