python-pptx2 2.13.0__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.
- pptx2/__init__.py +152 -0
- pptx2/_color.py +75 -0
- pptx2/_slide_importer.py +597 -0
- pptx2/_svg.py +155 -0
- pptx2/_template_applier.py +292 -0
- pptx2/_textstyle.py +187 -0
- pptx2/accessibility.py +365 -0
- pptx2/action.py +270 -0
- pptx2/animation.py +2237 -0
- pptx2/api.py +49 -0
- pptx2/audit.py +258 -0
- pptx2/chart/__init__.py +0 -0
- pptx2/chart/analytics.py +381 -0
- pptx2/chart/axis.py +543 -0
- pptx2/chart/category.py +200 -0
- pptx2/chart/chart.py +670 -0
- pptx2/chart/data.py +864 -0
- pptx2/chart/datalabel.py +406 -0
- pptx2/chart/legend.py +86 -0
- pptx2/chart/marker.py +70 -0
- pptx2/chart/palettes.py +129 -0
- pptx2/chart/plot.py +462 -0
- pptx2/chart/point.py +101 -0
- pptx2/chart/quick_layouts.py +325 -0
- pptx2/chart/series.py +334 -0
- pptx2/chart/xlsx.py +272 -0
- pptx2/chart/xmlwriter.py +1845 -0
- pptx2/compose/__init__.py +28 -0
- pptx2/compose/from_spec.py +1094 -0
- pptx2/design/__init__.py +8 -0
- pptx2/design/components.py +607 -0
- pptx2/design/figures.py +389 -0
- pptx2/design/layout.py +370 -0
- pptx2/design/recipes.py +1967 -0
- pptx2/design/style.py +209 -0
- pptx2/design/tokens.py +915 -0
- pptx2/diagrams.py +754 -0
- pptx2/dml/__init__.py +0 -0
- pptx2/dml/chtfmt.py +40 -0
- pptx2/dml/color.py +496 -0
- pptx2/dml/effect.py +909 -0
- pptx2/dml/fill.py +691 -0
- pptx2/dml/line.py +287 -0
- pptx2/dml/picture.py +212 -0
- pptx2/dml/three_d.py +381 -0
- pptx2/enum/__init__.py +0 -0
- pptx2/enum/action.py +71 -0
- pptx2/enum/animation.py +31 -0
- pptx2/enum/base.py +218 -0
- pptx2/enum/chart.py +574 -0
- pptx2/enum/dml.py +740 -0
- pptx2/enum/lang.py +685 -0
- pptx2/enum/presentation.py +133 -0
- pptx2/enum/shapes.py +1029 -0
- pptx2/enum/text.py +230 -0
- pptx2/exc.py +42 -0
- pptx2/formats.py +139 -0
- pptx2/geometry.py +420 -0
- pptx2/inherit.py +109 -0
- pptx2/lint.py +2256 -0
- pptx2/math.py +177 -0
- pptx2/media.py +197 -0
- pptx2/opc/__init__.py +0 -0
- pptx2/opc/constants.py +332 -0
- pptx2/opc/oxml.py +188 -0
- pptx2/opc/package.py +762 -0
- pptx2/opc/packuri.py +109 -0
- pptx2/opc/serialized.py +296 -0
- pptx2/opc/shared.py +20 -0
- pptx2/opc/spec.py +45 -0
- pptx2/oxml/__init__.py +555 -0
- pptx2/oxml/action.py +53 -0
- pptx2/oxml/chart/__init__.py +0 -0
- pptx2/oxml/chart/axis.py +337 -0
- pptx2/oxml/chart/chart.py +481 -0
- pptx2/oxml/chart/datalabel.py +253 -0
- pptx2/oxml/chart/legend.py +72 -0
- pptx2/oxml/chart/marker.py +61 -0
- pptx2/oxml/chart/plot.py +365 -0
- pptx2/oxml/chart/series.py +425 -0
- pptx2/oxml/chart/shared.py +220 -0
- pptx2/oxml/coreprops.py +288 -0
- pptx2/oxml/dml/__init__.py +0 -0
- pptx2/oxml/dml/color.py +135 -0
- pptx2/oxml/dml/effect.py +213 -0
- pptx2/oxml/dml/fill.py +316 -0
- pptx2/oxml/dml/line.py +12 -0
- pptx2/oxml/dml/three_d.py +110 -0
- pptx2/oxml/ns.py +135 -0
- pptx2/oxml/presentation.py +313 -0
- pptx2/oxml/shapes/__init__.py +19 -0
- pptx2/oxml/shapes/autoshape.py +467 -0
- pptx2/oxml/shapes/connector.py +107 -0
- pptx2/oxml/shapes/graphfrm.py +347 -0
- pptx2/oxml/shapes/groupshape.py +329 -0
- pptx2/oxml/shapes/picture.py +270 -0
- pptx2/oxml/shapes/shared.py +577 -0
- pptx2/oxml/simpletypes.py +1027 -0
- pptx2/oxml/slide.py +563 -0
- pptx2/oxml/table.py +650 -0
- pptx2/oxml/text.py +815 -0
- pptx2/oxml/theme.py +36 -0
- pptx2/oxml/xmlchemy.py +717 -0
- pptx2/package.py +222 -0
- pptx2/parts/__init__.py +0 -0
- pptx2/parts/chart.py +95 -0
- pptx2/parts/coreprops.py +167 -0
- pptx2/parts/diagram.py +37 -0
- pptx2/parts/embeddedpackage.py +93 -0
- pptx2/parts/image.py +275 -0
- pptx2/parts/media.py +37 -0
- pptx2/parts/presentation.py +136 -0
- pptx2/parts/slide.py +371 -0
- pptx2/presentation.py +408 -0
- pptx2/py.typed +0 -0
- pptx2/render.py +586 -0
- pptx2/section.py +272 -0
- pptx2/shapes/__init__.py +26 -0
- pptx2/shapes/autoshape.py +442 -0
- pptx2/shapes/base.py +1078 -0
- pptx2/shapes/connector.py +297 -0
- pptx2/shapes/freeform.py +337 -0
- pptx2/shapes/graphfrm.py +316 -0
- pptx2/shapes/group.py +264 -0
- pptx2/shapes/picture.py +422 -0
- pptx2/shapes/placeholder.py +468 -0
- pptx2/shapes/shapetree.py +2027 -0
- pptx2/shared.py +82 -0
- pptx2/skill/SKILL.md +450 -0
- pptx2/skill/__init__.py +78 -0
- pptx2/skill/__main__.py +64 -0
- pptx2/skill/references/animations.md +189 -0
- pptx2/skill/references/basics.md +421 -0
- pptx2/skill/references/charts.md +254 -0
- pptx2/skill/references/compose.md +234 -0
- pptx2/skill/references/design.md +366 -0
- pptx2/skill/references/effects.md +249 -0
- pptx2/skill/references/end-to-end-deck.md +231 -0
- pptx2/skill/references/geometry-and-arrows.md +334 -0
- pptx2/skill/references/lint.md +275 -0
- pptx2/skill/references/math.md +86 -0
- pptx2/skill/references/picture-effects.md +129 -0
- pptx2/skill/references/render.md +151 -0
- pptx2/skill/references/smart-art.md +75 -0
- pptx2/skill/references/space-aware-authoring.md +249 -0
- pptx2/skill/references/tables.md +244 -0
- pptx2/skill/references/theme.md +127 -0
- pptx2/skill/references/three-d.md +109 -0
- pptx2/skill/references/transitions.md +100 -0
- pptx2/slide.py +1244 -0
- pptx2/smart_art.py +220 -0
- pptx2/spec.py +633 -0
- pptx2/table.py +1181 -0
- pptx2/table_styles.py +184 -0
- pptx2/templates/default.pptx +0 -0
- pptx2/templates/docx-icon.emf +0 -0
- pptx2/templates/generic-icon.emf +0 -0
- pptx2/templates/notes.xml +23 -0
- pptx2/templates/notesMaster.xml +352 -0
- pptx2/templates/pptx-icon.emf +0 -0
- pptx2/templates/theme.xml +321 -0
- pptx2/templates/xlsx-icon.emf +0 -0
- pptx2/text/__init__.py +0 -0
- pptx2/text/fonts.py +482 -0
- pptx2/text/layout.py +374 -0
- pptx2/text/text.py +1272 -0
- pptx2/theme.py +721 -0
- pptx2/types.py +36 -0
- pptx2/util.py +263 -0
- python_pptx2-2.13.0.dist-info/METADATA +351 -0
- python_pptx2-2.13.0.dist-info/RECORD +175 -0
- python_pptx2-2.13.0.dist-info/WHEEL +5 -0
- python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
- python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
- python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/text/fonts.py
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""Objects related to system font file lookup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from struct import calcsize, error as struct_error, unpack_from
|
|
8
|
+
|
|
9
|
+
from pptx2.util import lazyproperty
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FontFiles(object):
|
|
13
|
+
"""A class-based singleton serving as a lazy cache for system font details."""
|
|
14
|
+
|
|
15
|
+
_font_files = None
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def find(cls, family_name: str, is_bold: bool, is_italic: bool) -> str:
|
|
19
|
+
"""Return the absolute path to an installed OpenType font.
|
|
20
|
+
|
|
21
|
+
File is matched by `family_name` and the styles `is_bold` and `is_italic`.
|
|
22
|
+
"""
|
|
23
|
+
if cls._font_files is None:
|
|
24
|
+
cls._font_files = cls._installed_fonts()
|
|
25
|
+
return cls._font_files[(family_name, is_bold, is_italic)]
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def _installed_fonts(cls):
|
|
29
|
+
"""
|
|
30
|
+
Return a dict mapping a font descriptor to its font file path,
|
|
31
|
+
containing all the font files resident on the current machine. The
|
|
32
|
+
font descriptor is a (family_name, is_bold, is_italic) 3-tuple.
|
|
33
|
+
"""
|
|
34
|
+
fonts = {}
|
|
35
|
+
for d in cls._font_directories():
|
|
36
|
+
for key, path in cls._iter_font_files_in(d):
|
|
37
|
+
fonts[key] = path
|
|
38
|
+
return fonts
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def _font_directories(cls):
|
|
42
|
+
"""
|
|
43
|
+
Return a sequence of directory paths likely to contain fonts on the
|
|
44
|
+
current platform. Returns an empty sequence on unrecognised platforms,
|
|
45
|
+
in which case `find()` will not match any installed font and callers
|
|
46
|
+
are expected to fall back to a default font.
|
|
47
|
+
"""
|
|
48
|
+
if sys.platform.startswith("darwin"):
|
|
49
|
+
return cls._os_x_font_directories()
|
|
50
|
+
if sys.platform.startswith("win32"):
|
|
51
|
+
return cls._windows_font_directories()
|
|
52
|
+
if sys.platform.startswith("linux"):
|
|
53
|
+
return cls._linux_font_directories()
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def _iter_font_files_in(cls, directory):
|
|
58
|
+
"""
|
|
59
|
+
Generate the OpenType font files found in and under *directory*. Each
|
|
60
|
+
item is a key/value pair. The key is a (family_name, is_bold,
|
|
61
|
+
is_italic) 3-tuple, like ('Arial', True, False), and the value is the
|
|
62
|
+
absolute path to the font file. Directories that do not exist or are
|
|
63
|
+
not readable are skipped silently so a probe list of conventional
|
|
64
|
+
platform paths can be passed in without pre-filtering.
|
|
65
|
+
"""
|
|
66
|
+
if not os.path.isdir(directory):
|
|
67
|
+
return
|
|
68
|
+
for root, dirs, files in os.walk(directory):
|
|
69
|
+
for filename in files:
|
|
70
|
+
file_ext = os.path.splitext(filename)[1]
|
|
71
|
+
if file_ext.lower() not in (".otf", ".ttf"):
|
|
72
|
+
continue
|
|
73
|
+
path = os.path.abspath(os.path.join(root, filename))
|
|
74
|
+
try:
|
|
75
|
+
with _Font.open(path) as f:
|
|
76
|
+
yield ((f.family_name, f.is_bold, f.is_italic), path)
|
|
77
|
+
except (OSError, KeyError, ValueError, struct_error):
|
|
78
|
+
# Skip malformed or unreadable font files rather than
|
|
79
|
+
# aborting the whole scan; one bad font on disk should
|
|
80
|
+
# not break `fit_text` for everyone else.
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def _os_x_font_directories(cls):
|
|
85
|
+
"""
|
|
86
|
+
Return a sequence of directory paths on a Mac in which fonts are
|
|
87
|
+
likely to be located.
|
|
88
|
+
"""
|
|
89
|
+
os_x_font_dirs = [
|
|
90
|
+
"/Library/Fonts",
|
|
91
|
+
"/Network/Library/Fonts",
|
|
92
|
+
"/System/Library/Fonts",
|
|
93
|
+
]
|
|
94
|
+
home = os.environ.get("HOME")
|
|
95
|
+
if home is not None:
|
|
96
|
+
os_x_font_dirs.extend(
|
|
97
|
+
[os.path.join(home, "Library", "Fonts"), os.path.join(home, ".fonts")]
|
|
98
|
+
)
|
|
99
|
+
return os_x_font_dirs
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def _windows_font_directories(cls):
|
|
103
|
+
"""
|
|
104
|
+
Return a sequence of directory paths on Windows in which fonts are
|
|
105
|
+
likely to be located.
|
|
106
|
+
"""
|
|
107
|
+
return [r"C:\Windows\Fonts"]
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def _linux_font_directories(cls):
|
|
111
|
+
"""
|
|
112
|
+
Return a sequence of directory paths on Linux in which fonts are
|
|
113
|
+
likely to be located.
|
|
114
|
+
"""
|
|
115
|
+
linux_font_dirs = [
|
|
116
|
+
"/usr/share/fonts",
|
|
117
|
+
"/usr/local/share/fonts",
|
|
118
|
+
"/usr/share/fonts/truetype",
|
|
119
|
+
]
|
|
120
|
+
home = os.environ.get("HOME")
|
|
121
|
+
if home is not None:
|
|
122
|
+
linux_font_dirs.extend(
|
|
123
|
+
[
|
|
124
|
+
os.path.join(home, ".fonts"),
|
|
125
|
+
os.path.join(home, ".local", "share", "fonts"),
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
return linux_font_dirs
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _Font(object):
|
|
132
|
+
"""
|
|
133
|
+
A wrapper around an OTF/TTF font file stream that knows how to parse it
|
|
134
|
+
for its name and style characteristics, e.g. bold and italic.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, stream):
|
|
138
|
+
self._stream = stream
|
|
139
|
+
|
|
140
|
+
def __enter__(self):
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def __exit__(self, exception_type, exception_value, exception_tb):
|
|
144
|
+
self._stream.close()
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def is_bold(self):
|
|
148
|
+
"""
|
|
149
|
+
|True| if this font is marked as a bold style of its font family.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
return self._tables["head"].is_bold
|
|
153
|
+
except KeyError:
|
|
154
|
+
# some files don't have a head table
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def is_italic(self):
|
|
159
|
+
"""
|
|
160
|
+
|True| if this font is marked as an italic style of its font family.
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
return self._tables["head"].is_italic
|
|
164
|
+
except KeyError:
|
|
165
|
+
# some files don't have a head table
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def open(cls, font_file_path):
|
|
170
|
+
"""
|
|
171
|
+
Return a |_Font| instance loaded from *font_file_path*.
|
|
172
|
+
"""
|
|
173
|
+
return cls(_Stream.open(font_file_path))
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def family_name(self):
|
|
177
|
+
"""
|
|
178
|
+
The name of the typeface family for this font, e.g. 'Arial'. The full
|
|
179
|
+
typeface name includes optional style names, such as 'Regular' or
|
|
180
|
+
'Bold Italic'. This attribute is only the common base name shared by
|
|
181
|
+
all fonts in the family.
|
|
182
|
+
"""
|
|
183
|
+
return self._tables["name"].family_name
|
|
184
|
+
|
|
185
|
+
@lazyproperty
|
|
186
|
+
def _fields(self):
|
|
187
|
+
"""5-tuple containing the fields read from the font file header.
|
|
188
|
+
|
|
189
|
+
Also known as the offset table.
|
|
190
|
+
"""
|
|
191
|
+
# sfnt_version, tbl_count, search_range, entry_selector, range_shift
|
|
192
|
+
return self._stream.read_fields(">4sHHHH", 0)
|
|
193
|
+
|
|
194
|
+
def _iter_table_records(self):
|
|
195
|
+
"""
|
|
196
|
+
Generate a (tag, offset, length) 3-tuple for each of the tables in
|
|
197
|
+
this font file.
|
|
198
|
+
"""
|
|
199
|
+
count = self._table_count
|
|
200
|
+
bufr = self._stream.read(offset=12, length=count * 16)
|
|
201
|
+
tmpl = ">4sLLL"
|
|
202
|
+
for i in range(count):
|
|
203
|
+
offset = i * 16
|
|
204
|
+
tag, checksum, off, len_ = unpack_from(tmpl, bufr, offset)
|
|
205
|
+
yield tag.decode("utf-8"), off, len_
|
|
206
|
+
|
|
207
|
+
@lazyproperty
|
|
208
|
+
def _tables(self):
|
|
209
|
+
"""
|
|
210
|
+
A mapping of OpenType table tag, e.g. 'name', to a table object
|
|
211
|
+
providing access to the contents of that table.
|
|
212
|
+
"""
|
|
213
|
+
return dict(
|
|
214
|
+
(tag, _TableFactory(tag, self._stream, off, len_))
|
|
215
|
+
for tag, off, len_ in self._iter_table_records()
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def _table_count(self):
|
|
220
|
+
"""
|
|
221
|
+
The number of tables in this OpenType font file.
|
|
222
|
+
"""
|
|
223
|
+
return self._fields[1]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class _Stream(object):
|
|
227
|
+
"""A thin wrapper around a binary file that facilitates reading C-struct values."""
|
|
228
|
+
|
|
229
|
+
def __init__(self, file):
|
|
230
|
+
self._file = file
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def open(cls, path):
|
|
234
|
+
"""Return |_Stream| providing binary access to contents of file at `path`."""
|
|
235
|
+
return cls(open(path, "rb"))
|
|
236
|
+
|
|
237
|
+
def close(self):
|
|
238
|
+
"""
|
|
239
|
+
Close the wrapped file. Using the stream after closing raises an
|
|
240
|
+
exception.
|
|
241
|
+
"""
|
|
242
|
+
self._file.close()
|
|
243
|
+
|
|
244
|
+
def read(self, offset, length):
|
|
245
|
+
"""
|
|
246
|
+
Return *length* bytes from this stream starting at *offset*.
|
|
247
|
+
"""
|
|
248
|
+
self._file.seek(offset)
|
|
249
|
+
return self._file.read(length)
|
|
250
|
+
|
|
251
|
+
def read_fields(self, template, offset=0):
|
|
252
|
+
"""
|
|
253
|
+
Return a tuple containing the C-struct fields in this stream
|
|
254
|
+
specified by *template* and starting at *offset*.
|
|
255
|
+
"""
|
|
256
|
+
self._file.seek(offset)
|
|
257
|
+
bufr = self._file.read(calcsize(template))
|
|
258
|
+
return unpack_from(template, bufr)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class _BaseTable(object):
|
|
262
|
+
"""
|
|
263
|
+
Base class for OpenType font file table objects.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(self, tag, stream, offset, length):
|
|
267
|
+
self._tag = tag
|
|
268
|
+
self._stream = stream
|
|
269
|
+
self._offset = offset
|
|
270
|
+
self._length = length
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class _HeadTable(_BaseTable):
|
|
274
|
+
"""
|
|
275
|
+
OpenType font table having the tag 'head' and containing certain header
|
|
276
|
+
information for the font, including its bold and/or italic style.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(self, tag, stream, offset, length):
|
|
280
|
+
super(_HeadTable, self).__init__(tag, stream, offset, length)
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def is_bold(self):
|
|
284
|
+
"""
|
|
285
|
+
|True| if this font is marked as having emboldened characters.
|
|
286
|
+
"""
|
|
287
|
+
return bool(self._macStyle & 1)
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def is_italic(self):
|
|
291
|
+
"""
|
|
292
|
+
|True| if this font is marked as having italicized characters.
|
|
293
|
+
"""
|
|
294
|
+
return bool(self._macStyle & 2)
|
|
295
|
+
|
|
296
|
+
@lazyproperty
|
|
297
|
+
def _fields(self):
|
|
298
|
+
"""
|
|
299
|
+
A 17-tuple containing the fields in this table.
|
|
300
|
+
"""
|
|
301
|
+
return self._stream.read_fields(">4s4sLLHHqqhhhhHHHHH", self._offset)
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def _macStyle(self):
|
|
305
|
+
"""
|
|
306
|
+
The unsigned short value of the 'macStyle' field in this head table.
|
|
307
|
+
"""
|
|
308
|
+
return self._fields[12]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class _NameTable(_BaseTable):
|
|
312
|
+
"""
|
|
313
|
+
An OpenType font table having the tag 'name' and containing the
|
|
314
|
+
name-related strings for the font.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
def __init__(self, tag, stream, offset, length):
|
|
318
|
+
super(_NameTable, self).__init__(tag, stream, offset, length)
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def family_name(self):
|
|
322
|
+
"""
|
|
323
|
+
The name of the typeface family for this font, e.g. 'Arial'.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
def find_first(dict_, keys, default=None):
|
|
327
|
+
for key in keys:
|
|
328
|
+
value = dict_.get(key)
|
|
329
|
+
if value is not None:
|
|
330
|
+
return value
|
|
331
|
+
return default
|
|
332
|
+
|
|
333
|
+
# keys for Unicode, Mac, and Windows family name, respectively
|
|
334
|
+
return find_first(self._names, ((0, 1), (1, 1), (3, 1)))
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def _decode_name(raw_name, platform_id, encoding_id):
|
|
338
|
+
"""
|
|
339
|
+
Return the unicode name decoded from *raw_name* using the encoding
|
|
340
|
+
implied by the combination of *platform_id* and *encoding_id*.
|
|
341
|
+
"""
|
|
342
|
+
if platform_id == 1:
|
|
343
|
+
# reject non-Roman Mac font names
|
|
344
|
+
if encoding_id != 0:
|
|
345
|
+
return None
|
|
346
|
+
return raw_name.decode("mac-roman")
|
|
347
|
+
elif platform_id in (0, 3):
|
|
348
|
+
return raw_name.decode("utf-16-be")
|
|
349
|
+
else:
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
def _iter_names(self):
|
|
353
|
+
"""Generate a key/value pair for each name in this table.
|
|
354
|
+
|
|
355
|
+
The key is a (platform_id, name_id) 2-tuple and the value is the unicode text
|
|
356
|
+
corresponding to that key.
|
|
357
|
+
"""
|
|
358
|
+
table_format, count, strings_offset = self._table_header
|
|
359
|
+
table_bytes = self._table_bytes
|
|
360
|
+
|
|
361
|
+
for idx in range(count):
|
|
362
|
+
platform_id, name_id, name = self._read_name(table_bytes, idx, strings_offset)
|
|
363
|
+
if name is None:
|
|
364
|
+
continue
|
|
365
|
+
yield ((platform_id, name_id), name)
|
|
366
|
+
|
|
367
|
+
@staticmethod
|
|
368
|
+
def _name_header(bufr, idx):
|
|
369
|
+
"""
|
|
370
|
+
The (platform_id, encoding_id, language_id, name_id, length,
|
|
371
|
+
name_str_offset) 6-tuple encoded in each name record C-struct.
|
|
372
|
+
"""
|
|
373
|
+
name_hdr_offset = 6 + idx * 12
|
|
374
|
+
return unpack_from(">HHHHHH", bufr, name_hdr_offset)
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def _raw_name_string(bufr, strings_offset, str_offset, length):
|
|
378
|
+
"""
|
|
379
|
+
Return the *length* bytes comprising the encoded string in *bufr* at
|
|
380
|
+
*str_offset* in the strings area beginning at *strings_offset*.
|
|
381
|
+
"""
|
|
382
|
+
offset = strings_offset + str_offset
|
|
383
|
+
tmpl = "%ds" % length
|
|
384
|
+
return unpack_from(tmpl, bufr, offset)[0]
|
|
385
|
+
|
|
386
|
+
def _read_name(self, bufr, idx, strings_offset):
|
|
387
|
+
"""Return a (platform_id, name_id, name) 3-tuple for name at `idx` in `bufr`.
|
|
388
|
+
|
|
389
|
+
The triple looks like (0, 1, 'Arial'). `strings_offset` is the for the name at
|
|
390
|
+
`idx` position in `bufr`. `strings_offset` is the index into `bufr` where actual
|
|
391
|
+
name strings begin. The returned name is a unicode string.
|
|
392
|
+
"""
|
|
393
|
+
platform_id, enc_id, lang_id, name_id, length, str_offset = self._name_header(bufr, idx)
|
|
394
|
+
name = self._read_name_text(bufr, platform_id, enc_id, strings_offset, str_offset, length)
|
|
395
|
+
return platform_id, name_id, name
|
|
396
|
+
|
|
397
|
+
def _read_name_text(
|
|
398
|
+
self, bufr, platform_id, encoding_id, strings_offset, name_str_offset, length
|
|
399
|
+
):
|
|
400
|
+
"""
|
|
401
|
+
Return the unicode name string at *name_str_offset* or |None| if
|
|
402
|
+
decoding its format is not supported.
|
|
403
|
+
"""
|
|
404
|
+
raw_name = self._raw_name_string(bufr, strings_offset, name_str_offset, length)
|
|
405
|
+
return self._decode_name(raw_name, platform_id, encoding_id)
|
|
406
|
+
|
|
407
|
+
@lazyproperty
|
|
408
|
+
def _table_bytes(self):
|
|
409
|
+
"""
|
|
410
|
+
The binary contents of this name table.
|
|
411
|
+
"""
|
|
412
|
+
return self._stream.read(self._offset, self._length)
|
|
413
|
+
|
|
414
|
+
@property
|
|
415
|
+
def _table_header(self):
|
|
416
|
+
"""
|
|
417
|
+
The (table_format, name_count, strings_offset) 3-tuple contained
|
|
418
|
+
in the header of this table.
|
|
419
|
+
"""
|
|
420
|
+
return unpack_from(">HHH", self._table_bytes)
|
|
421
|
+
|
|
422
|
+
@lazyproperty
|
|
423
|
+
def _names(self):
|
|
424
|
+
"""A mapping of (platform_id, name_id) keys to string names for this font."""
|
|
425
|
+
return dict(self._iter_names())
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _TableFactory(tag, stream, offset, length):
|
|
429
|
+
"""
|
|
430
|
+
Return an instance of |Table| appropriate to *tag*, loaded from
|
|
431
|
+
*font_file* with content of *length* starting at *offset*.
|
|
432
|
+
"""
|
|
433
|
+
TableClass = {"head": _HeadTable, "name": _NameTable}.get(tag, _BaseTable)
|
|
434
|
+
return TableClass(tag, stream, offset, length)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def find_font_file(family_name: str, bold: bool = False, italic: bool = False) -> str | None:
|
|
438
|
+
"""Return the path to the installed font file for `family_name`, or None.
|
|
439
|
+
|
|
440
|
+
The non-raising form of :meth:`FontFiles.find` — useful for deciding, up
|
|
441
|
+
front, whether font-metric measurement (`TextFrame.fit_text`, and any
|
|
442
|
+
layout maths derived from it) will be exact or a fallback estimate::
|
|
443
|
+
|
|
444
|
+
path = find_font_file("Inter", bold=True)
|
|
445
|
+
if path is None:
|
|
446
|
+
... # brand font missing in this environment
|
|
447
|
+
"""
|
|
448
|
+
try:
|
|
449
|
+
return FontFiles.find(family_name, bold, italic)
|
|
450
|
+
except (KeyError, OSError):
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def font_is_installed(family_name: str, bold: bool = False, italic: bool = False) -> bool:
|
|
455
|
+
"""True when `family_name` (in the given style) is installed on this machine.
|
|
456
|
+
|
|
457
|
+
The space-aware guarantee degrades to a best guess for fonts that aren't
|
|
458
|
+
installed, so a build that must be exact can check first and either bundle
|
|
459
|
+
the font file (``fit_text(font_file=...)``) or fall back to a family it
|
|
460
|
+
knows is present::
|
|
461
|
+
|
|
462
|
+
family = "Inter" if font_is_installed("Inter") else "Arial"
|
|
463
|
+
"""
|
|
464
|
+
return find_font_file(family_name, bold, italic) is not None
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def installed_font_families() -> tuple[str, ...]:
|
|
468
|
+
"""Return the sorted family names of every font installed on this machine.
|
|
469
|
+
|
|
470
|
+
Handy when a deck renders with the wrong metrics in CI and you need to see
|
|
471
|
+
what the build box actually has. Font files that report no family name are
|
|
472
|
+
skipped rather than breaking the listing.
|
|
473
|
+
"""
|
|
474
|
+
if FontFiles._font_files is None: # pyright: ignore[reportPrivateUsage]
|
|
475
|
+
FontFiles._font_files = FontFiles._installed_fonts() # pyright: ignore[reportPrivateUsage]
|
|
476
|
+
# A font file whose `name` table carries no family record for platform 0, 1
|
|
477
|
+
# or 3 is keyed under `None`. One such file on the machine would otherwise
|
|
478
|
+
# make this diagnostic helper raise `TypeError` from `sorted()` — the least
|
|
479
|
+
# helpful moment for a diagnostic to fail.
|
|
480
|
+
return tuple(
|
|
481
|
+
sorted({family for family, _, _ in FontFiles._font_files if isinstance(family, str)})
|
|
482
|
+
)
|