Refactor graphics state handling

This commit is contained in:
Kovid Goyal 2012-12-30 15:32:23 +05:30
parent df25363d3e
commit d03a5f252c
3 changed files with 240 additions and 237 deletions

View File

@ -14,12 +14,13 @@ from future_builtins import map
import sip import sip
from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter, from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter,
QTransform, QPainterPath, QImage, QByteArray, QBuffer, QTransform, QImage, QByteArray, QBuffer,
qRgba) qRgba)
from calibre.constants import plugins from calibre.constants import plugins
from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path) from calibre.ebooks.pdf.render.serialize import (PDFStream, Path)
from calibre.ebooks.pdf.render.common import inch, A4, fmtnum from calibre.ebooks.pdf.render.common import inch, A4, fmtnum
from calibre.ebooks.pdf.render.graphics import convert_path, Graphics
from calibre.utils.fonts.sfnt.container import Sfnt from calibre.utils.fonts.sfnt.container import Sfnt
from calibre.utils.fonts.sfnt.metrics import FontMetrics from calibre.utils.fonts.sfnt.metrics import FontMetrics
@ -42,146 +43,6 @@ def store_error(func):
return errh return errh
class GraphicsState(object): # {{{
def __init__(self):
self.ops = {}
self.initial_state = {
'fill': ColorState(Color(0., 0., 0., 1.), 1.0, False),
'transform': QTransform(),
'dash': [],
'line_width': 0,
'stroke': ColorState(Color(0., 0., 0., 1.), 1.0, True),
'line_cap': 'flat',
'line_join': 'miter',
'clip': (Qt.NoClip, QPainterPath()),
}
self.current_state = self.initial_state.copy()
def reset(self):
self.current_state = self.initial_state.copy()
def update_color_state(self, which, color=None, opacity=None,
brush_style=None, pen_style=None):
current = self.ops.get(which, self.current_state[which])
n = ColorState(*current)
if color is not None:
n = n._replace(color=Color(*color.getRgbF()))
if opacity is not None:
n = n._replace(opacity=opacity)
if opacity is not None:
opacity *= n.color.opacity
if brush_style is not None:
if which == 'fill':
do = (False if opacity == 0.0 or brush_style == Qt.NoBrush else
True)
else:
do = (False if opacity == 0.0 or brush_style == Qt.NoBrush or
pen_style == Qt.NoPen else True)
n = n._replace(do=do)
self.ops[which] = n
def read(self, state):
flags = state.state()
if flags & QPaintEngine.DirtyTransform:
self.ops['transform'] = state.transform()
# TODO: Add support for brush patterns
if flags & QPaintEngine.DirtyBrush:
brush = state.brush()
color = brush.color()
self.update_color_state('fill', color=color,
brush_style=brush.style())
if flags & QPaintEngine.DirtyPen:
pen = state.pen()
brush = pen.brush()
color = pen.color()
self.update_color_state('stroke', color, brush_style=brush.style(),
pen_style=pen.style())
ps = {Qt.DashLine:[3], Qt.DotLine:[1,2], Qt.DashDotLine:[3,2,1,2],
Qt.DashDotDotLine:[3, 2, 1, 2, 1, 2]}.get(pen.style(), [])
self.ops['dash'] = ps
self.ops['line_width'] = pen.widthF()
self.ops['line_cap'] = {Qt.FlatCap:'flat', Qt.RoundCap:'round',
Qt.SquareCap:'square'}.get(pen.capStyle(), 'flat')
self.ops['line_join'] = {Qt.MiterJoin:'miter', Qt.RoundJoin:'round',
Qt.BevelJoin:'bevel'}.get(pen.joinStyle(), 'miter')
if flags & QPaintEngine.DirtyOpacity:
self.update_color_state('fill', opacity=state.opacity())
self.update_color_state('stroke', opacity=state.opacity())
if flags & QPaintEngine.DirtyClipPath or flags & QPaintEngine.DirtyClipRegion:
self.ops['clip'] = True
def __call__(self, engine):
if not self.ops:
return
pdf = engine.pdf
ops = self.ops
current_transform = self.current_state['transform']
transform_changed = 'transform' in ops and ops['transform'] != current_transform
reset_stack = transform_changed or 'clip' in ops
if reset_stack:
pdf.restore_stack()
pdf.save_stack()
# Since we have reset the stack we need to re-apply all previous
# operations, that are different from the default value (clip is
# handled separately).
for op in set(self.initial_state) - {'clip'}:
if op in ops: # These will be applied below
self.current_state[op] = self.initial_state[op]
elif self.current_state[op] != self.initial_state[op]:
self.apply(op, self.current_state[op], engine, pdf)
# Now apply the new operations
for op, val in ops.iteritems():
if op != 'clip' and self.current_state[op] != val:
self.apply(op, val, engine, pdf)
self.current_state[op] = val
if 'clip' in ops:
# Get the current clip
path = engine.painter().clipPath()
if not path.isEmpty():
engine.add_clip(path)
self.ops = {}
def apply(self, op, val, engine, pdf):
getattr(self, 'apply_'+op)(val, engine, pdf)
def apply_transform(self, val, engine, pdf):
if not val.isIdentity():
pdf.transform(val)
def apply_stroke(self, val, engine, pdf):
self.apply_color_state('stroke', val, engine, pdf)
def apply_fill(self, val, engine, pdf):
self.apply_color_state('fill', val, engine, pdf)
def apply_color_state(self, which, val, engine, pdf):
color = val.color._replace(opacity=val.opacity*val.color.opacity)
getattr(pdf, 'set_%s_color'%which)(color)
setattr(engine, 'do_%s'%which, val.do)
def apply_dash(self, val, engine, pdf):
pdf.set_dash(val)
def apply_line_width(self, val, engine, pdf):
pdf.set_line_width(val)
def apply_line_cap(self, val, engine, pdf):
pdf.set_line_cap(val)
def apply_line_join(self, val, engine, pdf):
pdf.set_line_join(val)
# }}}
class Font(FontMetrics): class Font(FontMetrics):
def __init__(self, sfnt): def __init__(self, sfnt):
@ -215,9 +76,7 @@ class PdfEngine(QPaintEngine):
self.bottom_margin) / self.pixel_height self.bottom_margin) / self.pixel_height
self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy) self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy)
self.do_stroke = True self.graphics = Graphics()
self.do_fill = False
self.graphics_state = GraphicsState()
self.errors_occurred = False self.errors_occurred = False
self.errors, self.debug = errors, debug self.errors, self.debug = errors, debug
self.fonts = {} self.fonts = {}
@ -230,14 +89,21 @@ class PdfEngine(QPaintEngine):
if err: if err:
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):
self.graphics(self.pdf, self.pdf_system, self.painter())
@property
def do_fill(self):
return self.graphics.current_state.do_fill
@property
def do_stroke(self):
return self.graphics.current_state.do_stroke
def init_page(self): def init_page(self):
self.pdf.transform(self.pdf_system) self.pdf.transform(self.pdf_system)
self.pdf.set_rgb_colorspace() self.pdf.set_rgb_colorspace()
width = self.painter().pen().widthF() if self.isActive() else 0 self.graphics.reset()
self.pdf.set_line_width(width)
self.do_stroke = True
self.do_fill = False
self.graphics_state.reset()
self.pdf.save_stack() self.pdf.save_stack()
self.current_page_inited = True self.current_page_inited = True
@ -287,7 +153,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPixmap(self, rect, pixmap, source_rect): def drawPixmap(self, rect, pixmap, source_rect):
self.graphics_state(self) self.apply_graphics_state()
source_rect = source_rect.toRect() source_rect = source_rect.toRect()
pixmap = (pixmap if source_rect == pixmap.rect() else pixmap = (pixmap if source_rect == pixmap.rect() else
pixmap.copy(source_rect)) pixmap.copy(source_rect))
@ -299,7 +165,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor): def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor):
self.graphics_state(self) self.apply_graphics_state()
source_rect = source_rect.toRect() source_rect = source_rect.toRect()
image = (image if source_rect == image.rect() else image = (image if source_rect == image.rect() else
image.copy(source_rect)) image.copy(source_rect))
@ -374,50 +240,20 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def updateState(self, state): def updateState(self, state):
self.graphics_state.read(state) self.graphics.update_state(state, self.painter())
def convert_path(self, 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
@store_error @store_error
def drawPath(self, path): def drawPath(self, path):
self.graphics_state(self) self.apply_graphics_state()
p = self.convert_path(path) p = convert_path(path)
fill_rule = {Qt.OddEvenFill:'evenodd', fill_rule = {Qt.OddEvenFill:'evenodd',
Qt.WindingFill:'winding'}[path.fillRule()] Qt.WindingFill:'winding'}[path.fillRule()]
self.pdf.draw_path(p, stroke=self.do_stroke, self.pdf.draw_path(p, stroke=self.do_stroke,
fill=self.do_fill, fill_rule=fill_rule) fill=self.do_fill, fill_rule=fill_rule)
def add_clip(self, path):
p = self.convert_path(path)
fill_rule = {Qt.OddEvenFill:'evenodd',
Qt.WindingFill:'winding'}[path.fillRule()]
self.pdf.add_clip(p, fill_rule=fill_rule)
@store_error @store_error
def drawPoints(self, points): def drawPoints(self, points):
self.graphics_state(self) self.apply_graphics_state()
p = Path() p = Path()
for point in points: for point in points:
p.move_to(point.x(), point.y()) p.move_to(point.x(), point.y())
@ -426,7 +262,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawRects(self, rects): def drawRects(self, rects):
self.graphics_state(self) self.apply_graphics_state()
for rect in rects: for rect in rects:
bl = rect.topLeft() bl = rect.topLeft()
self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(),
@ -446,7 +282,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawTextItem(self, point, text_item): def drawTextItem(self, point, text_item):
# super(PdfEngine, self).drawTextItem(point, text_item) # super(PdfEngine, self).drawTextItem(point, text_item)
self.graphics_state(self) self.apply_graphics_state()
gi = self.qt_hack.get_glyphs(point, text_item) gi = self.qt_hack.get_glyphs(point, text_item)
if not gi.indices: if not gi.indices:
sip.delete(gi) sip.delete(gi)
@ -477,7 +313,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPolygon(self, points, mode): def drawPolygon(self, points, mode):
self.graphics_state(self) self.apply_graphics_state()
if not points: return if not points: return
p = Path() p = Path()
p.move_to(points[0].x(), points[0].y()) p.move_to(points[0].x(), points[0].y())
@ -510,14 +346,6 @@ class PdfEngine(QPaintEngine):
link.append((llx, lly, urx, ury)) link.append((llx, lly, urx, ury))
self.pdf.links.add(current_item, start_page, links, anchors) self.pdf.links.add(current_item, start_page, links, anchors)
def __enter__(self):
self.pdf.save_stack()
self.saved_ps = (self.do_stroke, self.do_fill)
def __exit__(self, *args):
self.do_stroke, self.do_fill = self.saved_ps
self.pdf.restore_stack()
class PdfDevice(QPaintDevice): # {{{ class PdfDevice(QPaintDevice): # {{{
@ -584,8 +412,8 @@ class PdfDevice(QPaintDevice): # {{{
# }}} # }}}
if __name__ == '__main__': if __name__ == '__main__':
from PyQt4.Qt import (QBrush, QColor, QPoint, QPixmap) from PyQt4.Qt import (QBrush, QColor, QPoint, QPixmap, QPainterPath)
QBrush, QColor, QPoint, QPixmap QBrush, QColor, QPoint, QPixmap, QPainterPath
app = QApplication([]) app = QApplication([])
p = QPainter() p = QPainter()
with open('/t/painter.pdf', 'wb') as f: with open('/t/painter.pdf', 'wb') as f:
@ -593,6 +421,7 @@ if __name__ == '__main__':
p.begin(dev) p.begin(dev)
dev.init_page() dev.init_page()
xmax, ymax = p.viewport().width(), p.viewport().height() xmax, ymax = p.viewport().width(), p.viewport().height()
b = p.brush()
try: try:
p.drawRect(0, 0, xmax, ymax) p.drawRect(0, 0, xmax, ymax)
# p.drawPolyline(QPoint(0, 0), QPoint(xmax, 0), QPoint(xmax, ymax), # p.drawPolyline(QPoint(0, 0), QPoint(xmax, 0), QPoint(xmax, ymax),
@ -600,27 +429,22 @@ if __name__ == '__main__':
# pp = QPainterPath() # pp = QPainterPath()
# pp.addRect(0, 0, xmax, ymax) # pp.addRect(0, 0, xmax, ymax)
# p.drawPath(pp) # p.drawPath(pp)
# p.save() p.save()
# for i in xrange(3): for i in xrange(3):
# col = [0, 0, 0, 200] col = [0, 0, 0, 200]
# col[i] = 255 col[i] = 255
# p.setOpacity(0.3) p.setOpacity(0.3)
# p.setBrush(QBrush(QColor(*col))) p.fillRect(0, 0, xmax/10, xmax/10, QBrush(QColor(*col)))
# p.drawRect(0, 0, xmax/10, xmax/10) p.setOpacity(1)
# p.translate(xmax/10, xmax/10) p.drawRect(0, 0, xmax/10, xmax/10)
# p.scale(1, 1.5) p.translate(xmax/10, xmax/10)
# p.restore() p.scale(1, 1.5)
p.restore()
# # p.scale(2, 2) # p.scale(2, 2)
# # p.rotate(45)
# p.drawPixmap(0, 0, 2048, 2048, QPixmap(I('library.png')))
# p.drawRect(0, 0, 2048, 2048)
# p.save()
# p.drawLine(0, 0, 5000, 0)
# p.rotate(45) # p.rotate(45)
# p.drawLine(0, 0, 5000, 0) p.drawPixmap(0, 0, 2048, 2048, QPixmap(I('library.png')))
# p.restore() p.drawRect(0, 0, 2048, 2048)
f = p.font() f = p.font()
f.setPointSize(20) f.setPointSize(20)

View File

@ -0,0 +1,196 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from math import sqrt
from PyQt4.Qt import (QBrush, QPen, Qt, QPointF, QTransform, QPainterPath,
QPaintEngine)
from calibre.ebooks.pdf.render.common import Array
from calibre.ebooks.pdf.render.serialize import Path, Color
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
class GraphicsState(object):
FIELDS = ('fill', 'stroke', 'opacity', 'transform', 'brush_origin',
'clip', 'do_fill', 'do_stroke')
def __init__(self):
self.fill = QBrush()
self.stroke = QPen()
self.opacity = 1.0
self.transform = QTransform()
self.brush_origin = QPointF()
self.clip = QPainterPath()
self.do_fill = False
self.do_stroke = True
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 = self.clip
ans.do_fill, ans.do_stroke = self.do_fill, self.do_stroke
return ans
class Graphics(object):
def __init__(self):
self.base_state = GraphicsState()
self.current_state = GraphicsState()
self.pending_state = None
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.DirtyTransform:
s.transform = state.transform()
if flags & QPaintEngine.DirtyBrushOrigin:
s.brush_origin = state.brushOrigin()
if flags & QPaintEngine.DirtyBrush:
s.fill = state.brush()
if flags & QPaintEngine.DirtyPen:
s.stroke = state.pen()
if flags & QPaintEngine.DirtyOpacity:
s.opacity = state.opacity()
if flags & QPaintEngine.DirtyClipPath or flags & QPaintEngine.DirtyClipRegion:
s.clip = painter.clipPath()
def reset(self):
self.current_state = GraphicsState()
self.pending_state = None
def __call__(self, pdf, 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
if (ps.transform != pdf_state.transform or ps.clip != pdf_state.clip):
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, 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)
if (pdf_state.clip != ps.clip):
p = convert_path(ps.clip)
fill_rule = {Qt.OddEvenFill:'evenodd',
Qt.WindingFill:'winding'}[ps.clip.fillRule()]
pdf.add_clip(p, fill_rule=fill_rule)
self.current_state = self.pending_state
self.pending_state = None
def apply_stroke(self, state, pdf, 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
if pen.style() == Qt.NoPen:
self.pending_state.do_stroke = False
# Width
w = pen.widthF()
if pen.isCosmetic():
t = painter.transform()
w /= sqrt(t.m11()**2 + t.m22()**2)
pdf.serialize(w)
pdf.current_page.write(' w ')
# Line cap
cap = {Qt.FlatCap:0, Qt.RoundCap:1, Qt.SquareCap:
2}.get(pen.capStyle(), 0)
pdf.current_page.write('%d J '%cap)
# Line join
join = {Qt.MiterJoin:0, Qt.RoundJoin:1,
Qt.BevelJoin:2}.get(pen.joinStyle(), 0)
pdf.current_page.write('%d j '%join)
# Dash pattern
ps = {Qt.DashLine:[3], Qt.DotLine:[1,2], Qt.DashDotLine:[3,2,1,2],
Qt.DashDotDotLine:[3, 2, 1, 2, 1, 2]}.get(pen.style(), [])
if ps:
pdf.serialize(Array(ps))
pdf.current_page.write(' d ')
# Stroke fill
b = pen.brush()
vals = list(b.color().getRgbF())
vals[-1] *= state.opacity
color = Color(*vals)
pdf.set_stroke_color(color)
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):
self.pending_state.do_fill = True
b = state.fill
if b.style() == Qt.NoBrush:
self.pending_state.do_fill = False
vals = list(b.color().getRgbF())
vals[-1] *= state.opacity
color = Color(*vals)
pdf.set_fill_color(color)

View File

@ -369,25 +369,8 @@ class PDFStream(object):
op = 'W' if fill_rule == 'winding' else 'W*' op = 'W' if fill_rule == 'winding' else 'W*'
self.current_page.write_line(op + ' ' + 'n') self.current_page.write_line(op + ' ' + 'n')
def set_dash(self, array, phase=0): def serialize(self, o):
array = Array(array) serialize(o, self.current_page)
serialize(array, self.current_page)
self.current_page.write(b' ')
serialize(phase, self.current_page)
self.current_page.write_line(' d')
def set_line_width(self, width):
serialize(width, self.current_page)
self.current_page.write_line(' w')
def set_line_cap(self, style):
serialize({'flat':0, 'round':1, 'square':2}.get(style),
self.current_page)
self.current_page.write_line(' J')
def set_line_join(self, style):
serialize({'miter':0, 'round':1, 'bevel':2}[style], self.current_page)
self.current_page.write_line(' j')
def set_stroke_color(self, color): def set_stroke_color(self, color):
opacity = color.opacity opacity = color.opacity