dcgraphics.py
12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# Name: dcgraphics.py
# Package: wx.lib.pdfviewer
#
# Purpose: A wx.GraphicsContext-like API implemented using wx.DC
# based on wx.lib.graphics by Robin Dunn
#
# Author: David Hughes dfh@forestfield.co.uk
# Copyright: Forestfield Software Ltd
# Licence: Same as wxPython host
# History: 8 Aug 2009 - created
# 12 Dec 2011 - amended DrawText
#
#----------------------------------------------------------------------------
"""
This module implements an API similar to wx.GraphicsContext and the
related classes. The implementation is done using wx.DC
Why do this? Neither wx.GraphicsContext not the Cairo-based
GraphicsContext API provided by wx.lib.graphics can be written
directly to a PrintDC. It can be done via an intermediate bitmap in
a MemoryDC but transferring this to a PrintDC is an order of magnitude
slower than writing directly.
Why not just use wxPrintDC directly? There may be times when you do want
to use wx.GraphicsContext for its displayed appearance and for its
clean(er) API, so being able to use the same code for printing as well is nice.
It started out with the intention of being a full implementation of the
GraphicsContext API but so far only contains the sub-set required to render PDF.
It also contains the co-ordinate twiddles for the PDF origin, which would need
to be separated out if this was ever developed to be more general purpose.
"""
import copy
from math import asin, pi
import bezier
import wx
class dcGraphicsState:
""" Each instance holds the current graphics state. It can be
saved (pushed) and restored (popped) by the owning parent
"""
def __init__ (self):
""" Creates an instance with default values """
self.Yoffset = 0.0
self.Xtrans = 0.0
self.Ytrans = 0.0
self.Xscale = 1.0
self.Yscale = 1.0
self.sinA = 0.0
self.cosA = 1.0
self.tanAlpha = 0.0
self.tanBeta = 0.0
self.rotDegrees = 0
def Ytop(self, y):
""" Return y co-ordinate wrt top of current page """
return self.Yoffset + y
def Translate(self, dx, dy):
"""move the origin from the current point to (dx,dy) """
self.Xtrans += (dx * self.Xscale)
self.Ytrans += (dy * self.Yscale)
def Scale(self, sx, sy):
"""scale the current co-ordinates """
self.Xscale *= sx
self.Yscale *= sy
def Rotate(self, cosA, sinA):
""" Compute the (text only) rotation angle
sinA is inverted to cancel the original inversion in
pdfviewer.drawfile that was introduced because of the difference
in y direction between pdf and GraphicsContext co-ordinates
"""
self.cosA = cosA
self.sinA = sinA
self.rotDegrees += asin(-self.sinA) * 180 / pi
def Skew(self, tanAlpha, tanBeta):
self.tanAlpha = tanAlpha
self.tanBeta = tanBeta
def Get_x(self, x=0, y=0):
""" Return x co-ordinate using graphic states and transforms
Input x,y are current co-ords
"""
return ((x*self.cosA*self.Xscale - y*self.sinA*self.Yscale) + self.Xtrans)
def Get_y(self, x=0, y=0):
""" Return y co-ordinate using graphic states and transforms
Input x,y are current co-ords
"""
return self.Ytop((x*self.sinA*self.Xscale + y*self.cosA*self.Yscale) + self.Ytrans)
def Get_angle(self):
""" Return rotation angle in degrees """
return self.rotDegrees
#----------------------------------------------------------------------------
class dcGraphicsContext(object):
def __init__(self, context=None, yoffset=0, have_cairo=False):
""" The incoming co-ordinates have a bottom left origin with increasing
y downwards (so y values are all negative). The DC origin is top left
also with increasing y down. yoffset informs us of the page height.
wx.DC and wx.GraphicsContext fonts are too big in the ratio of pixels
per inch to points per inch. If screen rendering used Cairo, printed
fonts need to be scaled but if wx.GC was used, they are already scaled
"""
self._context = context
self.gstate = dcGraphicsState()
self.saved_state = []
self.gstate.Yoffset = yoffset
self.fontscale = 1.0
if have_cairo and wx.PlatformInfo[1] == 'wxMSW':
self.fontscale = 72.0 / 96.0
@staticmethod
def Create(dc, yoffset, have_cairo):
""" The created pGraphicsContext instance uses the dc itself """
assert isinstance(dc, wx.DC)
return dcGraphicsContext(dc, yoffset, have_cairo)
def CreateMatrix(self, a=1.0, b=0, c=0, d=1.0, tx=0, ty=0):
"""
Create a new matrix object.
"""
m = dcGraphicsMatrix()
m.Set(a, b, c, d, tx, ty)
return m
def CreatePath(self):
"""
Create a new path obejct.
"""
return dcGraphicsPath(parent=self)
def PushState(self):
"""
Makes a copy of the current state of the context and saves it
on an internal stack of saved states. The saved state will be
restored when PopState is called.
"""
self.saved_state.append(copy.deepcopy(self.gstate))
def PopState(self):
"""
Restore the most recently saved state which was saved with PushState.
"""
self.gstate = self.saved_state.pop()
def Scale(self, xScale, yScale):
"""
Sets the dc userscale factor
"""
self._context.SetUserScale(xScale, yScale)
def ConcatTransform(self, matrix):
"""
Modifies the current transformation matrix by applying matrix
as an additional transformation.
"""
g = self.gstate
a, b, c, d, e, f = map(float, matrix.Get())
g.Translate(e, f)
if d == a and c == -b and b <> 0:
g.Rotate(a, b)
else:
g.Scale(a, d)
g.Skew(b,c)
def SetPen(self, pen):
"""
Set the wx.Pen to be used for stroking lines in future drawing
operations.
"""
self._context.SetPen(pen)
def SetBrush(self, brush):
"""
Set the Brush to be used for filling shapes in future drawing
operations.
"""
self._context.SetBrush(brush)
def SetFont(self, font, colour=None):
"""
Sets the wx.Font to be used for drawing text.
Don't set the dc font yet as it may need to be scaled
"""
self._font = font
if colour is not None:
self._context.SetTextForeground(colour)
def StrokePath(self, path):
"""
Strokes the path (draws the lines) using the current pen.
"""
raise NotImplementedError("TODO")
def FillPath(self, path, fillStyle=wx.ODDEVEN_RULE):
"""
Fills the path using the current brush.
"""
raise NotImplementedError("TODO")
def DrawPath(self, path, fillStyle=wx.ODDEVEN_RULE):
"""
Draws the path using current pen and brush.
"""
pathdict = {'SetPen': self._context.SetPen,
'DrawLine': self._context.DrawLine,
'DrawRectangle': self._context.DrawRectangle,
'DrawSpline': self._context.DrawSpline}
for pathcmd, args, kwargs in path.commands:
pathdict[pathcmd](*args, **kwargs)
if path.allpoints:
self._context.DrawPolygon(path.allpoints, 0, 0, fillStyle)
def DrawText(self, text, x, y, backgroundBrush=None):
"""
Set the dc font at the required size.
Ensure original font is not altered
Draw the text at (x,y) using the current font.
Ignore backgroundBrush
"""
g = self.gstate
orgsize = self._font.GetPointSize()
newsize = orgsize * (g.Xscale * self.fontscale)
self._font.SetPointSize(newsize)
self._context.SetFont(self._font)
self._context.DrawRotatedText(text, g.Get_x(x, y), g.Get_y(x, y), g.Get_angle())
self._font.SetPointSize(orgsize)
def DrawBitmap(self, bmp, x, y, w=-1, h=-1):
"""
Draw the bitmap at (x,y), ignoring w, h
"""
g = self.gstate
self._context.DrawBitmap(bmp, g.Get_x(x, y), g.Get_y(x, y))
#---------------------------------------------------------------------------
class dcGraphicsMatrix(object):
"""
A matrix holds an affine transformations, such as a scale,
rotation, shear, or a combination of these, and is used to convert
between different coordinante spaces.
"""
def __init__(self):
self._matrix = ()
def Set(self, a=1.0, b=0.0, c=0.0, d=1.0, tx=0.0, ty=0.0):
"""Set the componenets of the matrix by value, default values
are the identity matrix."""
self._matrix = (a, b, c, d, tx, ty)
def Get(self):
"""Return the component values of the matrix as a tuple."""
return tuple(self._matrix)
#---------------------------------------------------------------------------
class dcGraphicsPath(object):
"""
A GraphicsPath is a representaion of a geometric path, essentially
a collection of lines and curves. Paths can be used to define
areas to be stroked and filled on a GraphicsContext.
"""
def __init__(self, parent=None):
""" A path is essentially an object that we use just for
collecting path moves, lines, and curves in order to apply
them to the real context using DrawPath
"""
self.commands = []
self.allpoints = []
if parent:
self.gstate = parent.gstate
self.fillcolour = parent._context.GetBrush().GetColour()
self.isfilled = parent._context.GetBrush().GetStyle() <> wx.TRANSPARENT
def AddCurveToPoint(self, cx1, cy1, cx2, cy2, x, y):
"""
Adds a cubic Bezier curve from the current point, using two
control points and an end point.
"""
g = self.gstate
clist = []
clist.append(wx.RealPoint(self.xc, self.yc))
clist.append(wx.RealPoint(g.Get_x(cx1, cy1), g.Get_y(cx1, cy1)))
clist.append(wx.RealPoint(g.Get_x(cx2, cy2), g.Get_y(cx2, cy2)))
clist.append(wx.RealPoint(g.Get_x(x, y), g.Get_y(x, y)))
self.xc, self.yc = clist[-1]
plist = bezier.compute_points(clist, 64)
if self.isfilled:
self.allpoints.extend(plist)
else:
self.commands.append(['DrawSpline', (plist,), {}])
def AddLineToPoint(self, x, y):
"""
Adds a straight line from the current point to (x,y)
"""
x2 = self.gstate.Get_x(x, y)
y2 = self.gstate.Get_y(x, y)
if self.isfilled:
self.allpoints.extend([wx.Point(self.xc, self.yc), wx.Point(x2, y2)])
else:
self.commands.append(['DrawLine', (self.xc, self.yc, x2, y2), {}])
self.xc = x2
self.yc = y2
def AddRectangle(self, x, y, w, h):
"""
Adds a new rectangle as a closed sub-path.
"""
g = self.gstate
xr = g.Get_x(x, y)
yr = g.Get_y(x, y)
wr = w*g.Xscale*g.cosA - h*g.Yscale*g.sinA
hr = w*g.Xscale*g.sinA + h*g.Yscale*g.cosA
if round(wr) == 1 or round(hr) == 1: # draw thin rectangles as lines
self.commands.append(['SetPen', (wx.Pen(self.fillcolour, 1.0),), {}])
self.commands.append(['DrawLine', (xr, yr, xr+wr-1, yr+hr), {}])
else:
self.commands.append(['DrawRectangle', (xr, yr, wr, hr), {}])
def CloseSubpath(self):
"""
Adds a line segment to the path from the current point to the
beginning of the current sub-path, and closes this sub-path.
"""
if self.isfilled:
self.allpoints.extend([wx.Point(self.xc, self.yc), wx.Point(self.x0, self.y0)])
def MoveToPoint(self, x, y):
"""
Begins a new sub-path at (x,y) by moving the "current point" there.
"""
self.x0 = self.xc = self.gstate.Get_x(x, y)
self.y0 = self.yc = self.gstate.Get_y(x, y)