Add support for opacity and alpha blending

This commit is contained in:
Kovid Goyal 2012-12-15 21:57:57 +05:30
parent e3121fe618
commit fbcec65b71
2 changed files with 162 additions and 73 deletions

View File

@ -13,29 +13,58 @@ from collections import namedtuple
from future_builtins import map from future_builtins import map
from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter, from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter,
QTransform, QPoint, QPainterPath) QTransform, QPainterPath)
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre.ebooks.pdf.render.serialize import inch, A4, PDFStream, Path from calibre.ebooks.pdf.render.serialize import (Color, inch, A4, PDFStream,
Path)
XDPI = 1200 XDPI = 1200
YDPI = 1200 YDPI = 1200
Point = namedtuple('Point', 'x y') Point = namedtuple('Point', 'x y')
Color = namedtuple('Color', 'red green blue opacity') ColorState = namedtuple('ColorState', 'color opacity do')
class GraphicsState(object): # {{{ class GraphicsState(object): # {{{
def __init__(self, state=None): def __init__(self):
self.ops = {} self.ops = {}
if state is not None: self.current_state = self.initial_state = {
self.read_state(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()),
}
@property def reset(self):
def stack_reset_needed(self): self.current_state = self.initial_state
return 'transform' in self.ops or 'clip' in self.ops
def read_state(self, state): 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):
self.ops = {}
flags = state.state() flags = state.state()
if flags & QPaintEngine.DirtyTransform: if flags & QPaintEngine.DirtyTransform:
@ -45,25 +74,28 @@ class GraphicsState(object): # {{{
if flags & QPaintEngine.DirtyBrush: if flags & QPaintEngine.DirtyBrush:
brush = state.brush() brush = state.brush()
color = brush.color() color = brush.color()
self.ops['do_fill'] = 0 if (color.alpha() == 0 or brush.style() == Qt.NoBrush) else 1 self.update_color_state('fill', color=color,
self.ops['fill_color'] = Color(*color.getRgbF()) brush_style=brush.style())
if flags & QPaintEngine.DirtyPen: if flags & QPaintEngine.DirtyPen:
pen = state.pen() pen = state.pen()
brush = pen.brush() brush = pen.brush()
color = pen.color() color = pen.color()
self.ops['do_stroke'] = 0 if (pen.style() == Qt.NoPen or brush.style() == self.update_color_state('stroke', color, brush_style=brush.style(),
Qt.NoBrush or color.alpha() == 0) else 1 pen_style=pen.style())
ps = {Qt.DashLine:[3], Qt.DotLine:[1,2], Qt.DashDotLine:[3,2,1,2], 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(), []) Qt.DashDotDotLine:[3, 2, 1, 2, 1, 2]}.get(pen.style(), [])
self.ops['dash'] = ps self.ops['dash'] = ps
self.ops['line_width'] = pen.widthF() self.ops['line_width'] = pen.widthF()
self.ops['stroke_color'] = Color(*color.getRgbF())
self.ops['line_cap'] = {Qt.FlatCap:'flat', Qt.RoundCap:'round', self.ops['line_cap'] = {Qt.FlatCap:'flat', Qt.RoundCap:'round',
Qt.SquareCap:'square'}.get(pen.capStyle(), 'flat') Qt.SquareCap:'square'}.get(pen.capStyle(), 'flat')
self.ops['line_join'] = {Qt.MiterJoin:'miter', Qt.RoundJoin:'round', self.ops['line_join'] = {Qt.MiterJoin:'miter', Qt.RoundJoin:'round',
Qt.BevelJoin:'bevel'}.get(pen.joinStyle(), 'miter') 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: if flags & QPaintEngine.DirtyClipPath:
self.ops['clip'] = (state.clipOperation(), state.clipPath()) self.ops['clip'] = (state.clipOperation(), state.clipPath())
elif flags & QPaintEngine.DirtyClipRegion: elif flags & QPaintEngine.DirtyClipRegion:
@ -72,85 +104,91 @@ class GraphicsState(object): # {{{
path.addRect(rect) path.addRect(rect)
self.ops['clip'] = (state.clipOperation(), path) self.ops['clip'] = (state.clipOperation(), path)
# TODO: Add support for opacity
def __call__(self, engine): def __call__(self, engine):
pdf = engine.pdf pdf = engine.pdf
ops = self.ops ops = self.ops
current_transform = ops.get('transform', None) current_transform = self.current_state['transform']
srn = self.stack_reset_needed transform_changed = 'transform' in ops and ops['transform'] != current_transform
reset_stack = transform_changed or 'clip' in ops
if srn: if reset_stack:
pdf.restore_stack() pdf.restore_stack()
pdf.save_stack() pdf.save_stack()
# Since we have reset the stack we need to re-apply all previous
# operations
ops = engine.graphics_state.ops.copy()
ops.pop('clip', None) # Prev clip is handled separately
ops.update(self.ops)
self.ops = ops
# We apply clip before transform as the clip may have to be merged with # We apply clip before transform as the clip may have to be merged with
# the previous clip path so it is easiest to work with clips that are # the previous clip path so it is easiest to work with clips that are
# pre-transformed # pre-transformed
prev_clip_path = engine.graphics_state.ops.get('clip', (None, None))[1] prev_op, prev_clip_path = self.current_state['clip']
if 'clip' in ops: if 'clip' in ops:
op, path = ops['clip'] op, path = ops['clip']
if current_transform is not None and path is not None: self.current_state['clip'] = (op, path)
transform = ops.get('transform', QTransform())
if not transform.isIdentity() and path is not None:
# Pre transform the clip path # Pre transform the clip path
path = current_transform.map(path) path = current_transform.map(path)
ops['clip'] = (op, path) self.current_state['clip'] = (op, path)
if op == Qt.ReplaceClip: if op == Qt.ReplaceClip:
pass pass
elif op == Qt.IntersectClip: elif op == Qt.IntersectClip:
if prev_clip_path is not None: if prev_op != Qt.NoClip:
ops['clip'] = (op, path.intersected(prev_clip_path)) self.current_state['clip'] = (op, path.intersected(prev_clip_path))
elif op == Qt.UniteClip: elif op == Qt.UniteClip:
if prev_clip_path is not None: if prev_clip_path is not None:
path.addPath(prev_clip_path) path.addPath(prev_clip_path)
else: else:
ops['clip'] = (Qt.NoClip, None) self.current_state['clip'] = (Qt.NoClip, QPainterPath())
path = ops['clip'][1] op, path = self.current_state['clip']
if path is not None: if op != Qt.NoClip:
engine.add_clip(path) engine.add_clip(path)
elif prev_clip_path is not None: elif reset_stack and prev_op != Qt.NoClip:
# Re-apply the previous clip path since no clipping operation was # Re-apply the previous clip path since no clipping operation was
# specified # specified
engine.add_clip(prev_clip_path) engine.add_clip(prev_clip_path)
ops['clip'] = (Qt.ReplaceClip, prev_clip_path)
# Apply transform if reset_stack:
if current_transform is not None: # Since we have reset the stack we need to re-apply all previous
engine.qt_system = current_transform # operations, that are different from the default value (clip is
pdf.transform(current_transform) # handled separately).
for op in set(self.current_state) - (set(ops)|{'clip'}):
if self.current_state[op] != self.initial_state[op]:
self.apply(op, self.current_state[op], engine, pdf)
# if 'fill_color' in ops: # Now apply the new operations
# canvas.setFillColor(ops['fill_color']) for op, val in ops.iteritems():
# if 'stroke_color' in ops: self.apply(op, val, engine, pdf)
# canvas.setStrokeColor(ops['stroke_color']) self.current_state[op] = val
for x in ('fill', 'stroke'):
x = 'do_'+x
if x in ops:
setattr(engine, x, ops[x])
if 'dash' in ops:
pdf.set_dash(ops['dash'])
if 'line_width' in ops:
pdf.set_line_width(ops['line_width'])
if 'line_cap' in ops:
pdf.set_line_cap(ops['line_cap'])
if 'line_join' in ops:
pdf.set_line_join(ops['line_join'])
if not srn: def apply(self, op, val, engine, pdf):
# Add the operations from the previous state object that were not getattr(self, 'apply_'+op)(val, engine, pdf)
# updated in this state object. This is needed to allow stack
# resetting to work. def apply_transform(self, val, engine, pdf):
ops = engine.graphics_state.ops.copy() engine.qt_system = val
ops.update(self.ops) pdf.transform(val)
self.ops = ops
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)
return self
# }}} # }}}
class PdfEngine(QPaintEngine): class PdfEngine(QPaintEngine):
@ -178,8 +216,8 @@ class PdfEngine(QPaintEngine):
self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy) self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy)
self.qt_system = QTransform() self.qt_system = QTransform()
self.do_stroke = 1 self.do_stroke = True
self.do_fill = 0 self.do_fill = False
self.scale = sqrt(sy**2 + sx**2) self.scale = sqrt(sy**2 + sx**2)
self.yscale = sy self.yscale = sy
self.graphics_state = GraphicsState() self.graphics_state = GraphicsState()
@ -189,12 +227,16 @@ class PdfEngine(QPaintEngine):
self.pdf.set_rgb_colorspace() self.pdf.set_rgb_colorspace()
width = self.painter.pen().widthF() if self.isActive() else 0 width = self.painter.pen().widthF() if self.isActive() else 0
self.pdf.set_line_width(width) 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()
@property @property
def features(self): def features(self):
return (QPaintEngine.Antialiasing | QPaintEngine.PainterPaths | return (QPaintEngine.Antialiasing | QPaintEngine.AlphaBlend |
QPaintEngine.PaintOutsidePaintEvent | QPaintEngine.PorterDuff | QPaintEngine.ConstantOpacity | QPaintEngine.PainterPaths |
QPaintEngine.PaintOutsidePaintEvent |
QPaintEngine.PrimitiveTransform) QPaintEngine.PrimitiveTransform)
def begin(self, device): def begin(self, device):
@ -226,7 +268,7 @@ class PdfEngine(QPaintEngine):
return True return True
def type(self): def type(self):
return QPaintEngine.User return QPaintEngine.Pdf
def drawPixmap(self, rect, pixmap, source_rect): def drawPixmap(self, rect, pixmap, source_rect):
pass # TODO: Implement me pass # TODO: Implement me
@ -235,8 +277,8 @@ class PdfEngine(QPaintEngine):
pass # TODO: Implement me pass # TODO: Implement me
def updateState(self, state): def updateState(self, state):
state = GraphicsState(state) self.graphics_state.read(state)
self.graphics_state = state(self) self.graphics_state(self)
def convert_path(self, path): def convert_path(self, path):
p = Path() p = Path()
@ -295,7 +337,7 @@ class PdfEngine(QPaintEngine):
if not q.isIdentity() and q.type() > q.TxShear: if not q.isIdentity() and q.type() > q.TxShear:
# We cant map this transform to a PDF text transform operator # We cant map this transform to a PDF text transform operator
f, s = self.do_fill, self.do_stroke f, s = self.do_fill, self.do_stroke
self.do_fill, self.do_stroke = 1, 0 self.do_fill, self.do_stroke = True, False
super(PdfEngine, self).drawTextItem(point, text_item) super(PdfEngine, self).drawTextItem(point, text_item)
self.do_fill, self.do_stroke = f, s self.do_fill, self.do_stroke = f, s
return return
@ -395,7 +437,8 @@ class PdfDevice(QPaintDevice): # {{{
# }}} # }}}
if __name__ == '__main__': if __name__ == '__main__':
QPainterPath, QPoint from PyQt4.Qt import (QBrush, QColor, QPoint)
QBrush, QColor, QPoint
app = QApplication([]) app = QApplication([])
p = QPainter() p = QPainter()
with open('/tmp/painter.pdf', 'wb') as f: with open('/tmp/painter.pdf', 'wb') as f:
@ -411,6 +454,10 @@ if __name__ == '__main__':
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[i] = 255
p.setOpacity(0.3)
p.setBrush(QBrush(QColor(*col)))
p.drawRect(0, 0, xmax/10, xmax/10) p.drawRect(0, 0, xmax/10, xmax/10)
p.translate(xmax/10, xmax/10) p.translate(xmax/10, xmax/10)
p.scale(1, 1.5) p.scale(1, 1.5)

View File

@ -10,12 +10,15 @@ __docformat__ = 'restructuredtext en'
import codecs, zlib, hashlib import codecs, zlib, hashlib
from io import BytesIO from io import BytesIO
from future_builtins import map from future_builtins import map
from collections import namedtuple
from calibre.constants import (__appname__, __version__) from calibre.constants import (__appname__, __version__)
PDFVER = b'%PDF-1.6' PDFVER = b'%PDF-1.6'
EOL = b'\n' EOL = b'\n'
Color = namedtuple('Color', 'red green blue opacity')
# Sizes {{{ # Sizes {{{
inch = 72.0 inch = 72.0
cm = inch / 2.54 cm = inch / 2.54
@ -46,6 +49,8 @@ B1 = (_BH*4, _BW*2)
B0 = (_BW*4, _BH*4) B0 = (_BW*4, _BH*4)
# }}} # }}}
# Basic PDF datatypes {{{
def serialize(o, stream): def serialize(o, stream):
if hasattr(o, 'pdf_serialize'): if hasattr(o, 'pdf_serialize'):
o.pdf_serialize(stream) o.pdf_serialize(stream)
@ -150,6 +155,7 @@ class Reference(object):
def pdf_serialize(self, stream): def pdf_serialize(self, stream):
raw = '%d 0 R'%self.num raw = '%d 0 R'%self.num
stream.write(raw.encode('ascii')) stream.write(raw.encode('ascii'))
# }}}
class IndirectObjects(object): class IndirectObjects(object):
@ -215,11 +221,30 @@ class Page(Stream):
'Type': Name('Page'), 'Type': Name('Page'),
'Parent': parentref, 'Parent': parentref,
}) })
self.opacities = {}
def set_opacity(self, opref):
if opref not in self.opacities:
self.opacities[opref] = 'Opa%d'%len(self.opacities)
name = self.opacities[opref]
serialize(Name(name), self)
self.write(b' gs ')
def add_resources(self):
r = Dictionary()
if self.opacities:
extgs = Dictionary()
for opref, name in self.opacities.iteritems():
extgs[name] = opref
r['ExtGState'] = extgs
if r:
self.page_dict['Resources'] = r
def end(self, objects, stream): def end(self, objects, stream):
contents = objects.add(self) contents = objects.add(self)
objects.commit(contents, stream) objects.commit(contents, stream)
self.page_dict['Contents'] = contents self.page_dict['Contents'] = contents
self.add_resources()
ret = objects.add(self.page_dict) ret = objects.add(self.page_dict)
objects.commit(ret, stream) objects.commit(ret, stream)
return ret return ret
@ -299,6 +324,7 @@ class PDFStream(object):
self.current_page = Page(self.page_tree, compress=self.compress) self.current_page = Page(self.page_tree, compress=self.compress)
self.info = Dictionary({'Creator':String(creator), self.info = Dictionary({'Creator':String(creator),
'Producer':String(creator)}) 'Producer':String(creator)})
self.stroke_opacities, self.fill_opacities = {}, {}
@property @property
def page_tree(self): def page_tree(self):
@ -374,6 +400,22 @@ class PDFStream(object):
serialize({'miter':0, 'round':1, 'bevel':2}[style], self.current_page) serialize({'miter':0, 'round':1, 'bevel':2}[style], self.current_page)
self.current_page.write_line(' j') self.current_page.write_line(' j')
def set_stroke_color(self, color):
opacity = color.opacity
if opacity not in self.stroke_opacities:
op = Dictionary({'Type':Name('ExtGState'), 'CA': opacity})
self.stroke_opacities[opacity] = self.objects.add(op)
self.current_page.set_opacity(self.stroke_opacities[opacity])
self.current_page.write_line(' '.join(map(type(u''), color[:3])) + ' SC')
def set_fill_color(self, color):
opacity = color.opacity
if opacity not in self.fill_opacities:
op = Dictionary({'Type':Name('ExtGState'), 'ca': opacity})
self.fill_opacities[opacity] = self.objects.add(op)
self.current_page.set_opacity(self.fill_opacities[opacity])
self.current_page.write_line(' '.join(map(type(u''), color[:3])) + ' sc')
def end_page(self): def end_page(self):
pageref = self.current_page.end(self.objects, self.stream) pageref = self.current_page.end(self.objects, self.stream)
self.page_tree.obj.add_page(pageref) self.page_tree.obj.add_page(pageref)