TTFQuery 2.0.1a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ttfquery/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """Fonttools package for querying and sorting system fonts"""
2
+ __version__ = "2.0.1a1"
@@ -0,0 +1,16 @@
1
+ """The singleton "registry" object (an instance of :class:`ttfquery.ttffiles.Registry`
2
+ is the only member of note). This registry with be created in a file
3
+ in the user's $HOME directory ``.font.cache`` if the $HOME environment variable
4
+ is defined. Otherwise will be created in the ttfquery source code directory
5
+ (which is obviously not a very good solution).
6
+
7
+ .. note::
8
+
9
+ This module is basically here for legacy applications that used
10
+ the singleton registry instance. Use scriptregistry.get_registry()
11
+ in new code.
12
+ """
13
+ from ttfquery import scriptregistry
14
+
15
+ _getRegistry = scriptregistry.get_registry
16
+ registry = scriptregistry.get_registry()
ttfquery/describe.py ADDED
@@ -0,0 +1,332 @@
1
+ """Extract meta-data from a font-file to describe the font"""
2
+ from fontTools import ttLib
3
+ import sys
4
+
5
+ try:
6
+ unicode, long
7
+ except NameError:
8
+ unicode = str
9
+ long = int
10
+ try:
11
+ from OpenGLContext.debug.logs import text_log
12
+ except ImportError:
13
+ text_log = None
14
+
15
+
16
+ def openFont(filename):
17
+ """Get a new font object"""
18
+ if isinstance(filename, (bytes, unicode)):
19
+ filename = open(filename, "rb")
20
+ return ttLib.TTFont(filename)
21
+
22
+
23
+ FONT_SPECIFIER_NAME_ID = 4
24
+ FONT_SPECIFIER_FAMILY_ID = 1
25
+
26
+
27
+ def shortName(font):
28
+ """Get the short name from the font's names table"""
29
+ name = ""
30
+ family = ""
31
+ for record in font["name"].names:
32
+ if record.nameID == FONT_SPECIFIER_NAME_ID and not name:
33
+ if b"\000" in record.string:
34
+ name = unicode(record.string, "utf-16-be").encode("utf-8")
35
+ else:
36
+ name = record.string
37
+ elif record.nameID == FONT_SPECIFIER_FAMILY_ID and not family:
38
+ if b"\000" in record.string:
39
+ family = unicode(record.string, "utf-16-be").encode("utf-8")
40
+ else:
41
+ family = record.string
42
+ if name and family:
43
+ break
44
+ return name, family
45
+
46
+
47
+ FAMILY_NAMES = {
48
+ 0: ("ANY", {}),
49
+ 1: (
50
+ "SERIF-OLD",
51
+ {
52
+ 0: "ANY",
53
+ 1: "ROUNDED-LEGIBILITY",
54
+ 2: "GARALDE",
55
+ 3: "VENETIAN",
56
+ 4: "VENETIAN-MODIFIED",
57
+ 5: "DUTCH-MODERN",
58
+ 6: "DUTCH-TRADITIONAL",
59
+ 7: "CONTEMPORARY",
60
+ 8: "CALLIGRAPHIC",
61
+ 15: "MISCELLANEOUS",
62
+ },
63
+ ),
64
+ 2: (
65
+ "SERIF-TRANSITIONAL",
66
+ {
67
+ 0: "ANY",
68
+ 1: "DIRECT-LINE",
69
+ 2: "SCRIPT",
70
+ 15: "MISCELLANEOUS",
71
+ },
72
+ ),
73
+ 3: (
74
+ "SERIF",
75
+ {
76
+ 0: "ANY",
77
+ 1: "ITALIAN",
78
+ 2: "SCRIPT",
79
+ 15: "MISCELLANEOUS",
80
+ },
81
+ ),
82
+ 4: (
83
+ "SERIF-CLARENDON",
84
+ {
85
+ 0: "ANY",
86
+ 1: "CLARENDON",
87
+ 2: "MODERN",
88
+ 3: "TRADITIONAL",
89
+ 4: "NEWSPAPER",
90
+ 5: "STUB-SERIF",
91
+ 6: "MONOTYPE",
92
+ 7: "TYPEWRITER",
93
+ 15: "MISCELLANEOUS",
94
+ },
95
+ ),
96
+ 5: (
97
+ "SERIF-SLAB",
98
+ {
99
+ 0: "ANY",
100
+ 1: "MONOTONE",
101
+ 2: "HUMANIST",
102
+ 3: "GEOMETRIC",
103
+ 4: "SWISS",
104
+ 5: "TYPEWRITER",
105
+ 15: "MISCELLANEOUS",
106
+ },
107
+ ),
108
+ 7: (
109
+ "SERIF-FREEFORM",
110
+ {
111
+ 0: "ANY",
112
+ 1: "MODERN",
113
+ 15: "MISCELLANEOUS",
114
+ },
115
+ ),
116
+ 8: (
117
+ "SANS",
118
+ {
119
+ 0: "ANY",
120
+ 1: "GOTHIC-NEO-GROTESQUE-IBM",
121
+ 2: "HUMANIST",
122
+ 3: "ROUND-GEOMETRIC-LOW-X",
123
+ 4: "ROUND-GEOMETRIC-HIGH-X",
124
+ 5: "GOTHIC-NEO-GROTESQUE",
125
+ 6: "GOTHIC-NEO-GROTESQUE-MODIFIED",
126
+ 9: "GOTHIC-TYPEWRITER",
127
+ 10: "MATRIX",
128
+ 15: "MISCELLANEOUS",
129
+ },
130
+ ),
131
+ 9: (
132
+ "ORNAMENTAL",
133
+ {
134
+ 0: "ANY",
135
+ 1: "ENGRAVER",
136
+ 2: "BLACK-LETTER",
137
+ 3: "DECORATIVE",
138
+ 4: "THREE-DIMENSIONAL",
139
+ 15: "MISCELLANEOUS",
140
+ },
141
+ ),
142
+ 10: (
143
+ "SCRIPT",
144
+ {
145
+ 0: "ANY",
146
+ 1: "UNCIAL",
147
+ 2: "BRUSH-JOINED",
148
+ 3: "FORMAL-JOINED",
149
+ 4: "MONOTONE-JOINED",
150
+ 5: "CALLIGRAPHIC",
151
+ 6: "BRUSH-UNJOINED",
152
+ 7: "FORMAL-UNJOINED",
153
+ 8: "MONOTONE-UNJOINED",
154
+ 15: "MISCELLANEOUS",
155
+ },
156
+ ),
157
+ 12: (
158
+ "SYMBOL",
159
+ {
160
+ 0: "ANY",
161
+ 3: "MIXED-SERIF",
162
+ 6: "OLDSTYLE-SERIF",
163
+ 7: "NEO-GROTESQUE-SANS",
164
+ 15: "MISCELLANEOUS",
165
+ },
166
+ ),
167
+ }
168
+
169
+ WEIGHT_NAMES = {
170
+ "thin": 100,
171
+ "extralight": 200,
172
+ "ultralight": 200,
173
+ "light": 300,
174
+ "normal": 400,
175
+ "regular": 400,
176
+ "plain": 400,
177
+ "medium": 500,
178
+ "semibold": 600,
179
+ "demibold": 600,
180
+ "bold": 700,
181
+ "extrabold": 800,
182
+ "ultrabold": 800,
183
+ "black": 900,
184
+ "heavy": 900,
185
+ }
186
+ WEIGHT_NUMBERS = {}
187
+ for key, value in WEIGHT_NAMES.items():
188
+ WEIGHT_NUMBERS.setdefault(value, []).append(key)
189
+
190
+
191
+ def weightNumber(name):
192
+ """Convert a string-name to a weight number compatible with this module"""
193
+ if isinstance(name, (bytes, unicode)):
194
+ name = name.lower()
195
+ name = name.replace("-", "").replace(" ", "")
196
+ if name and name[-1] == "+":
197
+ name = name[:-1]
198
+ adjust = 50
199
+ elif name and name[-1] == "-":
200
+ name = name[:-1]
201
+ adjust = -50
202
+ else:
203
+ adjust = 0
204
+ return WEIGHT_NAMES[name] + adjust
205
+ else:
206
+ return int(name) or 400 # for cases where number isn't properly specified
207
+
208
+
209
+ def weightName(number):
210
+ """Convert integer number to a human-readable weight-name"""
211
+ number = int(number) or 400
212
+ if number in WEIGHT_NUMBERS:
213
+ return WEIGHT_NUMBERS[number]
214
+ name = "thin-"
215
+ for x in range(100, 1000, 100):
216
+ if number >= x:
217
+ name = WEIGHT_NUMBERS[x][0] + "+"
218
+ return name
219
+
220
+
221
+ def family(font):
222
+ """Get the family (and sub-family) for a font"""
223
+ HIBYTE = 65280
224
+ LOBYTE = 255
225
+ familyID = (font["OS/2"].sFamilyClass & HIBYTE) >> 8
226
+ subFamilyID = font["OS/2"].sFamilyClass & LOBYTE
227
+ return familyNames(familyID, subFamilyID)
228
+
229
+
230
+ def familyNames(familyID, subFamilyID=0):
231
+ """Convert family integers to human-readable names"""
232
+ familyName, subFamilies = FAMILY_NAMES.get(familyID, ("RESERVED", None))
233
+ if familyName == "RESERVED":
234
+ if text_log:
235
+ text_log.warn("Font has invalid (reserved) familyID: %s", familyID)
236
+ if subFamilies:
237
+ subFamily = subFamilies.get(subFamilyID, "RESERVED")
238
+ else:
239
+ subFamily = "ANY"
240
+ return (familyName, subFamily)
241
+
242
+
243
+ def modifiers(font):
244
+ """Get weight and italic modifiers for a font
245
+
246
+ weight is taken from the OS/2 usWeightClass field
247
+ italic is taken from either OS/2 fsSelection or
248
+ head macStyle, if either indicates italics we
249
+ report italics
250
+ """
251
+ return (
252
+ # weight as an integer
253
+ font["OS/2"].usWeightClass,
254
+ (font["OS/2"].fsSelection & 1 or font["head"].macStyle & 2), # italic
255
+ )
256
+
257
+
258
+ def guessEncoding(font, given=None):
259
+ """Attempt to guess/retrieve an encoding from the font itself
260
+
261
+ Basically this will try to get the given encoding
262
+ (unless it is None).
263
+
264
+ If given is a single integer or a single-item tuple,
265
+ we will attempt scan looking for any table matching
266
+ given as the platform ID and returning the first sub
267
+ table.
268
+
269
+ If given is a two-value tuple, we will require
270
+ explicit matching, and raise errors if the encoding
271
+ cannot be retrieved.
272
+
273
+ if given is None, we will return the first encoding
274
+ in the font.
275
+
276
+ XXX This needs some work, particularly for non-win32
277
+ platforms, where there is no preference embodied
278
+ for the native encoding.
279
+ """
280
+ if isinstance(given, tuple) and given:
281
+ if len(given) == 2:
282
+ if __debug__:
283
+ if text_log:
284
+ text_log.info(
285
+ """Checking for explicitly required encoding %r""", given
286
+ )
287
+ if not font["cmap"].getcmap(*given):
288
+ raise ValueError(
289
+ """The specified font encoding %r does not appear to be available within the font %r. Available encodings: %s"""
290
+ % (
291
+ given,
292
+ shortName(font),
293
+ [
294
+ (table.platformID, table.platEncID)
295
+ for table in font["cmap"].tables
296
+ ],
297
+ )
298
+ )
299
+ return given
300
+ elif len(given) > 2:
301
+ raise TypeError(
302
+ """Encoding must be None, a two-tuple, or an integer, got %r"""
303
+ % (given,)
304
+ )
305
+ else:
306
+ # treat as a single integer, regardless of number of integer's
307
+ given = given[0]
308
+ if isinstance(given, (int, long)):
309
+ for table in font["cmap"].tables:
310
+ if table.platformID == given:
311
+ return (table.platformID, table.platEncID)
312
+ raise ValueError(
313
+ """Could not find encoding with specified platformID==%s within the font %r. Available encodings: %s"""
314
+ % (
315
+ given,
316
+ shortName(font),
317
+ [(table.platformID, table.platEncID) for table in font["cmap"].tables],
318
+ )
319
+ )
320
+ if sys.platform == "win32":
321
+ prefered = (3, 1)
322
+ # should have prefered values for Linux and Mac as well...
323
+ for table in font["cmap"].tables:
324
+ if (table.platformID, table.platEncID) == prefered:
325
+ return prefered
326
+ # just retrieve the first table's values
327
+ for table in font["cmap"].tables:
328
+ return (table.platformID, table.platEncID)
329
+ raise ValueError(
330
+ """There are no encoding tables within the font %r, likely a corrupt font-file"""
331
+ % (shortName(font),)
332
+ )
ttfquery/findsystem.py ADDED
@@ -0,0 +1,142 @@
1
+ """Find system fonts (only works on Linux and Win32 at the moment)"""
2
+ from __future__ import print_function
3
+ try:
4
+ unicode
5
+ except NameError:
6
+ unicode = str
7
+ import sys, os, glob, re
8
+
9
+ def win32FontDirectory( ):
10
+ """Get User-specific font directory on Win32"""
11
+ try:
12
+ import _winreg
13
+ except ImportError:
14
+ return os.path.join(os.environ['WINDIR'], 'Fonts')
15
+ else:
16
+ k = _winreg.OpenKey(
17
+ _winreg.HKEY_CURRENT_USER,
18
+ r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
19
+ )
20
+ try:
21
+ # should check that k is valid? How?
22
+ return _winreg.QueryValueEx( k, "Fonts" )[0]
23
+ finally:
24
+ _winreg.CloseKey( k )
25
+
26
+ def win32InstalledFonts( fontDirectory = None ):
27
+ """Get list of explicitly *installed* font names
28
+
29
+ IFF there is no registry entry for installed fonts,
30
+ likely due to a too-new windows install, then we'll
31
+ just return all .ttf files in fontDirectory
32
+ """
33
+ import _winreg
34
+ if fontDirectory is None:
35
+ fontDirectory = win32FontDirectory()
36
+ k = None
37
+ items = {}
38
+ for keyName in (
39
+ r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts",
40
+ r"SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts",
41
+ ):
42
+ try:
43
+ k = _winreg.OpenKey(
44
+ _winreg.HKEY_LOCAL_MACHINE,
45
+ keyName
46
+ )
47
+ except OSError as err:
48
+ pass
49
+ if not k:
50
+ # couldn't open either WinNT or Win98 key???
51
+ return glob.glob( os.path.join(fontDirectory, '*.ttf'))
52
+ try:
53
+ # should check that k is valid? How?
54
+ for index in range( _winreg.QueryInfoKey(k)[1]):
55
+ key,value,_ = _winreg.EnumValue( k, index )
56
+ if not os.path.dirname( value ):
57
+ value = os.path.join( fontDirectory, value )
58
+ value = os.path.abspath( value ).lower()
59
+ if value[-4:] == '.ttf':
60
+ items[ value ] = 1
61
+ return items.keys()
62
+ finally:
63
+ _winreg.CloseKey( k )
64
+
65
+
66
+ def linuxFontDirectories( ):
67
+ """Get system font directories on Linux/Unix
68
+
69
+ Uses /usr/sbin/chkfontpath to get the list
70
+ of system-font directories, note that many
71
+ of these will *not* be truetype font directories.
72
+
73
+ If /usr/sbin/chkfontpath isn't available, uses
74
+ returns a set of common Linux/Unix paths
75
+ """
76
+ executable = '/usr/sbin/chkfontpath'
77
+ if os.path.isfile( executable ):
78
+ data = os.popen( executable ).readlines()
79
+ match = re.compile( r'\d+: (.+)')
80
+ set = []
81
+ for line in data:
82
+ result = match.match( line )
83
+ if result:
84
+ set.append(result.group(1))
85
+ return set
86
+ else:
87
+ directories = [
88
+ # what seems to be the standard installation point
89
+ "/usr/X11R6/lib/X11/fonts/TTF/",
90
+ # common application, not really useful
91
+ "/usr/lib/openoffice/share/fonts/truetype/",
92
+ # documented as a good place to install new fonts...
93
+ "/usr/share/fonts",
94
+ "/usr/local/share/fonts",
95
+ # seems to be where fonts are installed for an individual user?
96
+ "~/.fonts",
97
+ # okay, now the OS X variants...
98
+ "~/Library/Fonts/",
99
+ "/Library/Fonts/",
100
+ "/Network/Library/Fonts/",
101
+ "/System/Library/Fonts/",
102
+ "System Folder:Fonts:",
103
+ ]
104
+
105
+ set = []
106
+ for directory in directories:
107
+ directory = directory = os.path.expanduser( os.path.expandvars(directory))
108
+ for (dirname,_,_) in os.walk(directory):
109
+ if os.path.exists(dirname) and dirname not in set:
110
+ set.append(dirname)
111
+ return set
112
+
113
+ def findFonts(paths = None):
114
+ """Find fonts in paths, or the system paths if not given
115
+
116
+ XXX Doesn't current support OS-X system paths
117
+ """
118
+ files = {}
119
+ if paths is None:
120
+ if sys.platform == 'win32':
121
+ fontDirectory = win32FontDirectory()
122
+ paths = [
123
+ fontDirectory,
124
+ ]
125
+ # now get all installed fonts directly...
126
+ for f in win32InstalledFonts(fontDirectory):
127
+ # yes, it's inefficient, the interface
128
+ # for win32InstalledFonts really should be
129
+ # using sets, not lists
130
+ files[f] = 1
131
+ else:
132
+ paths = linuxFontDirectories()
133
+ elif isinstance( paths, (bytes, unicode)):
134
+ paths = [paths]
135
+ for path in paths:
136
+ for file in glob.glob( os.path.join(path, '*.ttf')):
137
+ files[os.path.abspath(file)] = 1
138
+ return list(files.keys())
139
+
140
+ if __name__ == "__main__":
141
+ print('linux font directories', linuxFontDirectories())
142
+ print('font names', findFonts())
ttfquery/glyph.py ADDED
@@ -0,0 +1,194 @@
1
+ """Representation of a single glyph including contour extraction"""
2
+ from ttfquery import glyphquery
3
+ import numpy
4
+
5
+ class Glyph( object):
6
+ """Object encapsulating metadata regarding a particular glyph"""
7
+ def __init__(self, glyphName ):
8
+ """Initialize the glyph object
9
+
10
+ glyphName -- font's glyphName for this glyph, see
11
+ glyphquery.glyphName() for extracting the
12
+ appropriate name from character and encoding.
13
+ """
14
+ self.glyphName = glyphName
15
+ def compile( self, font, steps = 3 ):
16
+ """Compile the glyph to a set of poly-line outlines"""
17
+ self.contours = self.calculateContours( font )
18
+ self.outlines = [
19
+ decomposeOutline(contour,steps)
20
+ for contour in self.contours
21
+ ]
22
+ self.width = glyphquery.width( font, self.glyphName )
23
+ self.height = glyphquery.lineHeight( font )
24
+
25
+ def calculateContours( self, font ):
26
+ """Given a character, determine contours to draw
27
+
28
+ returns a list of contours, with each contour
29
+ being a list of ((x,y),flag) elements. There may
30
+ be only a single contour.
31
+ """
32
+ glyf = font['glyf']
33
+ charglyf = glyf[self.glyphName]
34
+ return self._calculateContours( charglyf, glyf, font )
35
+ def _calculateContours( self, charglyf, glyf, font ):
36
+ """Create expanded contour data-set from TTF charglyph entry
37
+
38
+ This is called recursively for composite glyphs, so
39
+ I suppose a badly-formed TTF file could cause an infinite
40
+ loop.
41
+
42
+ charglyph -- glyf table's entry for the target character
43
+ glyf -- the glyf table (used for recursive calls)
44
+ """
45
+ charglyf.expand( font ) # XXX is this extraneous?
46
+ contours = []
47
+ if charglyf.numberOfContours == 0:
48
+ # does not display at all, for instance, space
49
+ pass
50
+ elif charglyf.isComposite():
51
+ # composed out of other items...
52
+ for component in charglyf.components:
53
+ subContours = self._calculateContours(
54
+ glyf[component.glyphName],
55
+ glyf,
56
+ font
57
+ )
58
+ dx,dy = component.getComponentInfo()[1][-2:]
59
+ # XXX we're ignoring the scaling/shearing/etceteras transformation
60
+ # matrix which is given by component.getComponentInfo()[1][:4]
61
+ subContours = [
62
+ [((x+dx,y+dy),f) for ((x,y),f) in subContour]
63
+ for subContour in subContours
64
+ ]
65
+ contours.extend(
66
+ subContours
67
+ )
68
+ else:
69
+ flags = charglyf.flags
70
+ coordinates = charglyf.coordinates
71
+ # We need to convert from the "end point" representation
72
+ # to a distinct contour representation, requires slicing
73
+ # each contour out of the list and then closing it
74
+ last = 0
75
+ for e in charglyf.endPtsOfContours:
76
+ set = zip(
77
+ list(coordinates[last:e+1])+list(coordinates[last:last+1]),
78
+ list(flags[last:e+1])+list(flags[last:last+1])
79
+ )
80
+ contours.append( list(set) )
81
+ last = e+1
82
+ if coordinates[last:]:
83
+ contours.append( list(zip(
84
+ list(coordinates[last:])+list(coordinates[last:last+1]),
85
+ list(flags[last:])+list(flags[last:])
86
+ ) ))
87
+ return contours
88
+
89
+ def decomposeOutline( contour, steps=3 ):
90
+ """Decompose a single TrueType contour to a line-loop
91
+
92
+ In essence, this is the "interpretation" of the font
93
+ as geometric primitives. I only support line and
94
+ quadratic (conic) segments, which should support most
95
+ TrueType fonts as far as I know.
96
+
97
+ The process consists of first scanning for any multi-
98
+ off-curve control-point runs. For each pair of such
99
+ control points, we insert a new on-curve control point.
100
+
101
+ Once we have the "expanded" control point array we
102
+ scan through looking for each segment which includes
103
+ an off-curve control point. These should only be
104
+ bracketed by on-curve control points. For each
105
+ found segment, we call our integrateQuadratic method
106
+ to produce a set of points interpolating between the
107
+ end points as affected by the middle control point.
108
+
109
+ All other control points merely generate a single
110
+ line-segment between the endpoints.
111
+ """
112
+ # contours must be closed, but they can start with
113
+ # (and even end with) items not on the contour...
114
+ # so if we do, we need to create a new point to serve
115
+ # as the midway...
116
+ if len(contour)<3:
117
+ return ()
118
+ set = contour[:]
119
+ def on( record ):
120
+ """Is this record on the contour?
121
+
122
+ record = ((Ax,Ay),Af)
123
+ """
124
+ return record[-1] == 1
125
+
126
+ def merge( first, second):
127
+ """Merge two off-point records into an on-point record"""
128
+ ((Ax,Ay),Af) = first
129
+ ((Bx,By),Bf) = second
130
+ return (((Ax+Bx)/2.0),((Ay+By))/2.0),1
131
+ # create an expanded set so that all adjacent
132
+ # off-curve items have an on-curve item added
133
+ # in between them
134
+ last = contour[-1]
135
+ expanded = []
136
+ for item in set:
137
+ if (not on(item)) and (not on(last)):
138
+ expanded.append( merge(last, item))
139
+ expanded.append( item )
140
+ last = item
141
+ result = []
142
+ last = expanded[-1]
143
+ while expanded:
144
+ assert on(expanded[0]), "Expanded outline doesn't have proper format! Should always have either [on, off, on] or [on, on] as the first items in the outline"
145
+ if len(expanded)>1:
146
+ if on(expanded[1]):
147
+ # line segment from 0 to 1
148
+ result.append( expanded[0][0] )
149
+ #result.append( expanded[1][0] )
150
+ del expanded[:1]
151
+ else:
152
+ if len(expanded) == 2: #KH
153
+ assert on(expanded[0]), """Expanded outline finishes off-curve""" #KH
154
+ result.append( expanded[1][0] ) #KH
155
+ del expanded[:1]
156
+ break
157
+ assert on(expanded[2]), "Expanded outline doesn't have proper format!"
158
+ points = integrateQuadratic( expanded[:3], steps = steps )
159
+ result.extend( points )
160
+ del expanded[:2]
161
+ else:
162
+ assert on(expanded[0]), """Expanded outline finishes off-curve"""
163
+ result.append( expanded[0][0] )
164
+ del expanded[:1]
165
+ result.append( result[-1] )
166
+ return result
167
+ def integrateQuadratic( points, steps=3 ):
168
+ """Get points on curve for quadratic w/ end points A and C
169
+
170
+ Basis Equations are taken from here:
171
+ http://www.truetype.demon.co.uk/ttoutln.htm
172
+
173
+ This is a very crude approach to the integration,
174
+ everything is coded directly in Python, with no
175
+ attempts to speed up the process.
176
+
177
+ XXX Should eventually provide adaptive steps so that
178
+ the angle between the elements can determine how
179
+ many steps are used.
180
+ """
181
+ step = 1.0/steps
182
+ ((Ax,Ay),_),((Bx,By),_),((Cx,Cy),_) = points
183
+ result = [(Ax,Ay)]
184
+ ### XXX This is dangerous, in certain cases floating point error
185
+ ## can wind up creating a new point at 1.0-(some tiny number) if step
186
+ ## is sliaghtly less than precisely 1.0/steps
187
+ for t in numpy.arange( step, 1.0, step ):
188
+ invT = 1.0-t
189
+ px = (invT*invT * Ax) + (2*t*invT*Bx) + (t*t*Cx)
190
+ py = (invT*invT * Ay) + (2*t*invT*By) + (t*t*Cy)
191
+ result.append( (px,py) )
192
+ # the end-point will be added by the next segment...
193
+ #result.append( (Cx,Cy) )
194
+ return result