mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Refactor graphics state handling
This commit is contained in:
parent
df25363d3e
commit
d03a5f252c
@ -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)
|
||||||
|
196
src/calibre/ebooks/pdf/render/graphics.py
Normal file
196
src/calibre/ebooks/pdf/render/graphics.py
Normal 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)
|
||||||
|
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user