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 +2 -0
- ttfquery/_scriptregistry.py +16 -0
- ttfquery/describe.py +332 -0
- ttfquery/findsystem.py +142 -0
- ttfquery/glyph.py +194 -0
- ttfquery/glyphquery.py +90 -0
- ttfquery/guessdescription.py +135 -0
- ttfquery/scriptregistry.py +37 -0
- ttfquery/ttffamily.py +37 -0
- ttfquery/ttffiles.py +417 -0
- ttfquery/ttfgroups.py +50 -0
- ttfquery/ttfmetadata.py +41 -0
- ttfquery-2.0.1a1.dist-info/METADATA +83 -0
- ttfquery-2.0.1a1.dist-info/RECORD +17 -0
- ttfquery-2.0.1a1.dist-info/WHEEL +5 -0
- ttfquery-2.0.1a1.dist-info/entry_points.txt +5 -0
- ttfquery-2.0.1a1.dist-info/top_level.txt +1 -0
ttfquery/__init__.py
ADDED
|
@@ -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
|