Workaround for Qt's broken emulation of gradients with texture patterns

This commit is contained in:
Kovid Goyal 2012-12-31 18:42:08 +05:30
parent 32165539ea
commit 9f13e30737
3 changed files with 93 additions and 37 deletions

View File

@ -93,7 +93,11 @@ class PdfEngine(QPaintEngine):
raise RuntimeError('Failed to load qt_hack with err: %s'%err) raise RuntimeError('Failed to load qt_hack with err: %s'%err)
def apply_graphics_state(self): 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 @property
def do_fill(self): def do_fill(self):
@ -117,6 +121,7 @@ class PdfEngine(QPaintEngine):
self.page_height), compress=self.compress, self.page_height), compress=self.compress,
mark_links=self.mark_links, mark_links=self.mark_links,
debug=self.debug) debug=self.debug)
self.graphics.begin(self.pdf)
except: except:
self.errors(traceback.format_exc()) self.errors(traceback.format_exc())
self.errors_occurred = True self.errors_occurred = True
@ -155,7 +160,7 @@ class PdfEngine(QPaintEngine):
brush = QBrush(pixmap) brush = QBrush(pixmap)
bl = rect.topLeft() bl = rect.topLeft()
color, opacity, pattern, do_fill = self.graphics.convert_brush( 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.painter().transform())
self.pdf.save_stack() self.pdf.save_stack()
self.pdf.apply_fill(color, pattern) self.pdf.apply_fill(color, pattern)
@ -211,10 +216,12 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawRects(self, rects): def drawRects(self, rects):
self.apply_graphics_state() self.apply_graphics_state()
for rect in rects: with self.graphics:
bl = rect.topLeft() for rect in rects:
self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), self.resolve_fill(rect)
stroke=self.do_stroke, fill=self.do_fill) 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): def create_sfnt(self, text_item):
get_table = partial(self.qt_hack.get_sfnt_table, text_item) get_table = partial(self.qt_hack.get_sfnt_table, text_item)

View File

@ -8,6 +8,7 @@ __copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from math import sqrt from math import sqrt
from collections import namedtuple
from PyQt4.Qt import ( from PyQt4.Qt import (
QBrush, QPen, Qt, QPointF, QTransform, QPainterPath, QPaintEngine, QImage) QBrush, QPen, Qt, QPointF, QTransform, QPainterPath, QPaintEngine, QImage)
@ -41,6 +42,8 @@ def convert_path(path): # {{{
return p return p
# }}} # }}}
Brush = namedtuple('Brush', 'origin brush color')
class TilingPattern(Stream): class TilingPattern(Stream):
def __init__(self, cache_key, matrix, w=8, h=8, paint_type=2, compress=False): 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): class TexturePattern(TilingPattern):
def __init__(self, pixmap, matrix, pdf): def __init__(self, pixmap, matrix, pdf, clone=None):
image = pixmap.toImage() if clone is None:
cache_key = pixmap.cacheKey() image = pixmap.toImage()
imgref = pdf.add_image(image, cache_key) cache_key = pixmap.cacheKey()
paint_type = (2 if image.format() in {QImage.Format_MonoLSB, imgref = pdf.add_image(image, cache_key)
QImage.Format_Mono} else 1) paint_type = (2 if image.format() in {QImage.Format_MonoLSB,
super(TexturePattern, self).__init__( QImage.Format_Mono} else 1)
cache_key, matrix, w=image.width(), h=image.height(), super(TexturePattern, self).__init__(
paint_type=paint_type) cache_key, matrix, w=image.width(), h=image.height(),
m = (self.w, 0, 0, -self.h, 0, self.h) paint_type=paint_type)
self.resources['XObject'] = Dictionary({'Texture':imgref}) m = (self.w, 0, 0, -self.h, 0, self.h)
self.write_line('%s cm /Texture Do'%(' '.join(map(fmtnum, m)))) 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): class GraphicsState(object):
@ -275,6 +285,9 @@ class Graphics(object):
self.current_state = GraphicsState() self.current_state = GraphicsState()
self.pending_state = None self.pending_state = None
def begin(self, pdf):
self.pdf = pdf
def update_state(self, state, painter): def update_state(self, state, painter):
flags = state.state() flags = state.state()
if self.pending_state is None: if self.pending_state is None:
@ -304,13 +317,14 @@ class Graphics(object):
self.current_state = GraphicsState() self.current_state = GraphicsState()
self.pending_state = None 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 # Apply the currently pending state to the PDF
if self.pending_state is None: if self.pending_state is None:
return return
pdf_state = self.current_state pdf_state = self.current_state
ps = self.pending_state ps = self.pending_state
pdf = self.pdf
if (ps.transform != pdf_state.transform or ps.clip != pdf_state.clip): if (ps.transform != pdf_state.transform or ps.clip != pdf_state.clip):
pdf.restore_stack() pdf.restore_stack()
@ -321,11 +335,11 @@ class Graphics(object):
pdf.transform(ps.transform) pdf.transform(ps.transform)
if (pdf_state.opacity != ps.opacity or pdf_state.stroke != ps.stroke): 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 if (pdf_state.opacity != ps.opacity or pdf_state.fill != ps.fill or
pdf_state.brush_origin != ps.brush_origin): 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): if (pdf_state.clip != ps.clip):
p = convert_path(ps.clip) p = convert_path(ps.clip)
@ -336,25 +350,28 @@ class Graphics(object):
self.current_state = self.pending_state self.current_state = self.pending_state
self.pending_state = None 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): pdf_system, qt_system):
# Convert a QBrush to PDF operators # Convert a QBrush to PDF operators
style = brush.style() style = brush.style()
pdf = self.pdf
pattern = color = None pattern = color = pat = None
opacity = 1.0 opacity = 1.0
do_fill = True do_fill = True
matrix = (QTransform.fromTranslate(brush_origin.x(), brush_origin.y()) matrix = (QTransform.fromTranslate(brush_origin.x(), brush_origin.y())
* pdf_system * qt_system.inverted()[0]) * pdf_system * qt_system.inverted()[0])
vals = list(brush.color().getRgbF()) vals = list(brush.color().getRgbF())
self.brushobj = None
if style <= Qt.DiagCrossPattern: if style <= Qt.DiagCrossPattern:
opacity = global_opacity * vals[-1] opacity = global_opacity * vals[-1]
color = vals[:3] color = vals[:3]
if style > Qt.SolidPattern: 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: if opacity < 1e-4 or style == Qt.NoBrush:
do_fill = False do_fill = False
@ -370,15 +387,17 @@ class Graphics(object):
if opacity < 1e-4 or style == Qt.NoBrush: if opacity < 1e-4 or style == Qt.NoBrush:
do_fill = False do_fill = False
self.brushobj = Brush(brush_origin, pat, color)
# TODO: Add support for gradient fills # TODO: Add support for gradient fills
return color, opacity, pattern, do_fill 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 # TODO: Handle pens with non solid brushes by setting the colorspace
# for stroking to a pattern # for stroking to a pattern
# TODO: Support miter limit by using QPainterPathStroker # TODO: Support miter limit by using QPainterPathStroker
pen = state.stroke pen = state.stroke
self.pending_state.do_stroke = True self.pending_state.do_stroke = True
pdf = self.pdf
if pen.style() == Qt.NoPen: if pen.style() == Qt.NoPen:
self.pending_state.do_stroke = False self.pending_state.do_stroke = False
@ -417,10 +436,41 @@ class Graphics(object):
if vals[-1] < 1e-5 or b.style() == Qt.NoBrush: if vals[-1] < 1e-5 or b.style() == Qt.NoBrush:
self.pending_state.do_stroke = False 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 self.pending_state.do_fill = True
color, opacity, pattern, self.pending_state.do_fill = self.convert_brush( 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()) 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)

View File

@ -83,21 +83,20 @@ def run(dev, func):
raise SystemExit(1) raise SystemExit(1)
def brush(p, xmax, ymax): def brush(p, xmax, ymax):
x = y = 0 x = xmax/3
w = xmax/3 + 10 y = 0
g = QLinearGradient(QPointF(0, 0), QPointF(0, w)) w = xmax/2
g.setSpread(g.RepeatSpread) pix = QPixmap(I('console.png'))
g.setColorAt(0, QColor('#00f')) p.fillRect(x, y, w, w, QBrush(pix))
g.setColorAt(1, QColor('#fff'))
p.fillRect(x, y, w, w, QBrush(g)) p.fillRect(0, y+xmax/1.9, w, w, QBrush(pix))
p.drawRect(x, y, w, w)
def main(): def main():
app = QApplication([]) app = QApplication([])
app app
tdir = gettempdir() tdir = gettempdir()
pdf = os.path.join(tdir, 'painter.pdf') pdf = os.path.join(tdir, 'painter.pdf')
func = brush func = full
dpi = 100 dpi = 100
with open(pdf, 'wb') as f: with open(pdf, 'wb') as f:
dev = PdfDevice(f, xdpi=dpi, ydpi=dpi, compress=False) dev = PdfDevice(f, xdpi=dpi, ydpi=dpi, compress=False)