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/layout.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Objects related to layout of rendered text, such as TextFitter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PIL import ImageFont
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pptx2.util import Length
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TextFitter(tuple):
|
|
14
|
+
"""Value object that knows how to fit text into given rectangular extents."""
|
|
15
|
+
|
|
16
|
+
def __new__(cls, line_source, extents, font_file):
|
|
17
|
+
width, height = extents
|
|
18
|
+
return tuple.__new__(cls, (line_source, width, height, font_file))
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def best_fit_font_size(
|
|
22
|
+
cls, text: str, extents: tuple[Length, Length], max_size: int, font_file: str | None
|
|
23
|
+
) -> int | None:
|
|
24
|
+
"""Return whole-number best fit point size less than or equal to `max_size`.
|
|
25
|
+
|
|
26
|
+
The return value is the largest whole-number point size less than or equal to
|
|
27
|
+
`max_size` that allows `text` to fit completely within `extents` when rendered
|
|
28
|
+
using font defined in `font_file`. Returns ``None`` when even 1pt overflows the
|
|
29
|
+
extents — callers are expected to surface that as an error (see
|
|
30
|
+
:meth:`TextFrame._best_fit_font_size`).
|
|
31
|
+
"""
|
|
32
|
+
line_source = _LineSource(text)
|
|
33
|
+
text_fitter = cls(line_source, extents, font_file)
|
|
34
|
+
return text_fitter._best_fit_font_size(max_size)
|
|
35
|
+
|
|
36
|
+
def _best_fit_font_size(self, max_size: int) -> int | None:
|
|
37
|
+
"""
|
|
38
|
+
Return the largest whole-number point size less than or equal to
|
|
39
|
+
*max_size* that this fitter can fit, or ``None`` when no size fits.
|
|
40
|
+
"""
|
|
41
|
+
predicate = self._fits_inside_predicate
|
|
42
|
+
sizes = _BinarySearchTree.from_ordered_sequence(range(1, int(max_size) + 1))
|
|
43
|
+
return sizes.find_max(predicate)
|
|
44
|
+
|
|
45
|
+
def _break_line(self, line_source, point_size):
|
|
46
|
+
"""
|
|
47
|
+
Return a (line, remainder) pair where *line* is the longest line in
|
|
48
|
+
*line_source* that will fit in this fitter's width and *remainder* is
|
|
49
|
+
a |_LineSource| object containing the text following the break point.
|
|
50
|
+
"""
|
|
51
|
+
lines = _BinarySearchTree.from_ordered_sequence(line_source)
|
|
52
|
+
predicate = self._fits_in_width_predicate(point_size)
|
|
53
|
+
return lines.find_max(predicate)
|
|
54
|
+
|
|
55
|
+
def _fits_in_width_predicate(self, point_size):
|
|
56
|
+
"""
|
|
57
|
+
Return a function taking a text string value and returns |True| if
|
|
58
|
+
that text fits in this fitter when rendered at *point_size*. Used as
|
|
59
|
+
predicate for _break_line()
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def predicate(line):
|
|
63
|
+
"""
|
|
64
|
+
Return |True| if *line* fits in this fitter when rendered at
|
|
65
|
+
*point_size*.
|
|
66
|
+
"""
|
|
67
|
+
cx = _rendered_size(line.text, point_size, self._font_file)[0]
|
|
68
|
+
return cx <= self._width
|
|
69
|
+
|
|
70
|
+
return predicate
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def _fits_inside_predicate(self):
|
|
74
|
+
"""Return function taking an integer point size argument.
|
|
75
|
+
|
|
76
|
+
The function returns |True| if the text in this fitter can be wrapped to fit
|
|
77
|
+
entirely within its extents when rendered at that point size.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def predicate(point_size):
|
|
81
|
+
"""Return |True| when text in `line_source` can be wrapped to fit.
|
|
82
|
+
|
|
83
|
+
Fit means text can be broken into lines that fit entirely within `extents`
|
|
84
|
+
when rendered at `point_size` using the font defined in `font_file`.
|
|
85
|
+
"""
|
|
86
|
+
text_lines = self._wrap_lines(self._line_source, point_size)
|
|
87
|
+
if text_lines is None:
|
|
88
|
+
# No whole word fits in the available width at this point
|
|
89
|
+
# size; treat as "doesn't fit" so the search continues to
|
|
90
|
+
# smaller sizes rather than crashing. Returning ``None``
|
|
91
|
+
# is the explicit no-fit signal from ``_wrap_lines`` /
|
|
92
|
+
# ``_break_line``.
|
|
93
|
+
return False
|
|
94
|
+
# Use the bigger of (Pillow ink-box height) and (1.2× the
|
|
95
|
+
# point-size baseline) per line. Pillow's getbbox on ``"Ty"``
|
|
96
|
+
# returns the *ink box* — descenders and ascender margins
|
|
97
|
+
# included, but not line leading — which under-estimates the
|
|
98
|
+
# vertical space a wrapped line actually occupies in
|
|
99
|
+
# PowerPoint / LibreOffice (where leading bumps each line to
|
|
100
|
+
# roughly 1.2× the point size). The pre-fix predicate
|
|
101
|
+
# accepted 72pt wrapping to 2 lines inside a 2-inch box
|
|
102
|
+
# because 2 × ink-box ≈ 1.6 inches; the rendered layout
|
|
103
|
+
# actually consumed 2.4 inches and overflowed. See
|
|
104
|
+
# IMPROVEMENTS item 7.
|
|
105
|
+
ink_cy = _rendered_size("Ty", point_size, self._font_file)[1]
|
|
106
|
+
leading_cy = int(point_size * 1.2 * 914400 / 72.0)
|
|
107
|
+
cy = max(ink_cy, leading_cy)
|
|
108
|
+
return (cy * len(text_lines)) <= self._height
|
|
109
|
+
|
|
110
|
+
return predicate
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def _font_file(self):
|
|
114
|
+
return self[3]
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def _height(self):
|
|
118
|
+
return self[2]
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def _line_source(self):
|
|
122
|
+
return self[0]
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def _width(self):
|
|
126
|
+
return self[1]
|
|
127
|
+
|
|
128
|
+
def _wrap_lines(self, line_source, point_size):
|
|
129
|
+
"""
|
|
130
|
+
Return a sequence of str values representing the text in
|
|
131
|
+
*line_source* wrapped within this fitter when rendered at
|
|
132
|
+
*point_size*.
|
|
133
|
+
|
|
134
|
+
Returns ``None`` when no whole word from *line_source* fits in
|
|
135
|
+
the available width at *point_size* (``_break_line`` returns
|
|
136
|
+
``None``), signalling "no-fit" to callers without raising.
|
|
137
|
+
"""
|
|
138
|
+
result = self._break_line(line_source, point_size)
|
|
139
|
+
if result is None:
|
|
140
|
+
return None
|
|
141
|
+
text, remainder = result
|
|
142
|
+
lines = [text]
|
|
143
|
+
if remainder:
|
|
144
|
+
sub = self._wrap_lines(remainder, point_size)
|
|
145
|
+
if sub is None:
|
|
146
|
+
return None
|
|
147
|
+
lines.extend(sub)
|
|
148
|
+
return lines
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class _BinarySearchTree(object):
|
|
152
|
+
"""
|
|
153
|
+
A node in a binary search tree. Uniform for root, subtree root, and leaf
|
|
154
|
+
nodes.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, value):
|
|
158
|
+
self._value = value
|
|
159
|
+
self._lesser = None
|
|
160
|
+
self._greater = None
|
|
161
|
+
|
|
162
|
+
def find_max(self, predicate, max_=None):
|
|
163
|
+
"""
|
|
164
|
+
Return the largest item in or under this node that satisfies
|
|
165
|
+
*predicate*.
|
|
166
|
+
"""
|
|
167
|
+
if predicate(self.value):
|
|
168
|
+
max_ = self.value
|
|
169
|
+
next_node = self._greater
|
|
170
|
+
else:
|
|
171
|
+
next_node = self._lesser
|
|
172
|
+
if next_node is None:
|
|
173
|
+
return max_
|
|
174
|
+
return next_node.find_max(predicate, max_)
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def from_ordered_sequence(cls, iseq):
|
|
178
|
+
"""
|
|
179
|
+
Return the root of a balanced binary search tree populated with the
|
|
180
|
+
values in iterable *iseq*.
|
|
181
|
+
"""
|
|
182
|
+
seq = list(iseq)
|
|
183
|
+
# optimize for usually all fits by making longest first
|
|
184
|
+
bst = cls(seq.pop())
|
|
185
|
+
bst._insert_from_ordered_sequence(seq)
|
|
186
|
+
return bst
|
|
187
|
+
|
|
188
|
+
def insert(self, value):
|
|
189
|
+
"""
|
|
190
|
+
Insert a new node containing *value* into this tree such that its
|
|
191
|
+
structure as a binary search tree is preserved.
|
|
192
|
+
"""
|
|
193
|
+
side = "_lesser" if value < self.value else "_greater"
|
|
194
|
+
child = getattr(self, side)
|
|
195
|
+
if child is None:
|
|
196
|
+
setattr(self, side, _BinarySearchTree(value))
|
|
197
|
+
else:
|
|
198
|
+
child.insert(value)
|
|
199
|
+
|
|
200
|
+
def tree(self, level=0, prefix=""):
|
|
201
|
+
"""
|
|
202
|
+
A string representation of the tree rooted in this node, useful for
|
|
203
|
+
debugging purposes.
|
|
204
|
+
"""
|
|
205
|
+
text = "%s%s\n" % (prefix, self.value.text)
|
|
206
|
+
prefix = "%s└── " % (" " * level)
|
|
207
|
+
if self._lesser:
|
|
208
|
+
text += self._lesser.tree(level + 1, prefix)
|
|
209
|
+
if self._greater:
|
|
210
|
+
text += self._greater.tree(level + 1, prefix)
|
|
211
|
+
return text
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def value(self):
|
|
215
|
+
"""
|
|
216
|
+
The value object contained in this node.
|
|
217
|
+
"""
|
|
218
|
+
return self._value
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _bisect(seq):
|
|
222
|
+
"""
|
|
223
|
+
Return a (medial_value, greater_values, lesser_values) 3-tuple
|
|
224
|
+
obtained by bisecting sequence *seq*.
|
|
225
|
+
"""
|
|
226
|
+
if len(seq) == 0:
|
|
227
|
+
return [], None, []
|
|
228
|
+
mid_idx = int(len(seq) / 2)
|
|
229
|
+
mid = seq[mid_idx]
|
|
230
|
+
greater = seq[mid_idx + 1 :]
|
|
231
|
+
lesser = seq[:mid_idx]
|
|
232
|
+
return mid, greater, lesser
|
|
233
|
+
|
|
234
|
+
def _insert_from_ordered_sequence(self, seq):
|
|
235
|
+
"""
|
|
236
|
+
Insert the new values contained in *seq* into this tree such that
|
|
237
|
+
a balanced tree is produced.
|
|
238
|
+
"""
|
|
239
|
+
if len(seq) == 0:
|
|
240
|
+
return
|
|
241
|
+
mid, greater, lesser = self._bisect(seq)
|
|
242
|
+
self.insert(mid)
|
|
243
|
+
self._insert_from_ordered_sequence(greater)
|
|
244
|
+
self._insert_from_ordered_sequence(lesser)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class _LineSource(object):
|
|
248
|
+
"""
|
|
249
|
+
Generates all the possible even-word line breaks in a string of text,
|
|
250
|
+
each in the form of a (line, remainder) 2-tuple where *line* contains the
|
|
251
|
+
text before the break and *remainder* the text after as a |_LineSource|
|
|
252
|
+
object. Its boolean value is |True| when it contains text, |False| when
|
|
253
|
+
its text is the empty string or whitespace only.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
def __init__(self, text):
|
|
257
|
+
self._text = text
|
|
258
|
+
|
|
259
|
+
def __bool__(self):
|
|
260
|
+
"""
|
|
261
|
+
Gives this object boolean behaviors (in Python 3). bool(line_source)
|
|
262
|
+
is False if it contains the empty string or whitespace only.
|
|
263
|
+
"""
|
|
264
|
+
return self._text.strip() != ""
|
|
265
|
+
|
|
266
|
+
def __eq__(self, other):
|
|
267
|
+
return self._text == other._text
|
|
268
|
+
|
|
269
|
+
def __iter__(self):
|
|
270
|
+
"""
|
|
271
|
+
Generate a (text, remainder) pair for each possible even-word line
|
|
272
|
+
break in this line source, where *text* is a str value and remainder
|
|
273
|
+
is a |_LineSource| value.
|
|
274
|
+
"""
|
|
275
|
+
words = self._text.split()
|
|
276
|
+
for idx in range(1, len(words) + 1):
|
|
277
|
+
line_text = " ".join(words[:idx])
|
|
278
|
+
remainder_text = " ".join(words[idx:])
|
|
279
|
+
remainder = _LineSource(remainder_text)
|
|
280
|
+
yield _Line(line_text, remainder)
|
|
281
|
+
|
|
282
|
+
def __nonzero__(self):
|
|
283
|
+
"""
|
|
284
|
+
Gives this object boolean behaviors (in Python 2). bool(line_source)
|
|
285
|
+
is False if it contains the empty string or whitespace only.
|
|
286
|
+
"""
|
|
287
|
+
return self._text.strip() != ""
|
|
288
|
+
|
|
289
|
+
def __repr__(self):
|
|
290
|
+
return "<_LineSource('%s')>" % self._text
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class _Line(tuple):
|
|
294
|
+
"""
|
|
295
|
+
A candidate line broken at an even word boundary from a string of text,
|
|
296
|
+
and a |_LineSource| value containing the text that remains after the line
|
|
297
|
+
is broken at this spot.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __new__(cls, text, remainder):
|
|
301
|
+
return tuple.__new__(cls, (text, remainder))
|
|
302
|
+
|
|
303
|
+
def __gt__(self, other):
|
|
304
|
+
return len(self.text) > len(other.text)
|
|
305
|
+
|
|
306
|
+
def __lt__(self, other):
|
|
307
|
+
return not self.__gt__(other)
|
|
308
|
+
|
|
309
|
+
def __len__(self):
|
|
310
|
+
return len(self.text)
|
|
311
|
+
|
|
312
|
+
def __repr__(self):
|
|
313
|
+
return "'%s' => '%s'" % (self.text, self.remainder)
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def remainder(self):
|
|
317
|
+
return self[1]
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def text(self):
|
|
321
|
+
return self[0]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class _Fonts(object):
|
|
325
|
+
"""
|
|
326
|
+
A memoizing cache for ImageFont objects.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
fonts = {}
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def font(cls, font_path, point_size):
|
|
333
|
+
key = (font_path, point_size)
|
|
334
|
+
if key not in cls.fonts:
|
|
335
|
+
cls.fonts[key] = cls._load(font_path, point_size)
|
|
336
|
+
return cls.fonts[key]
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def _load(font_path, point_size):
|
|
340
|
+
if font_path is None:
|
|
341
|
+
# `font_path=None` opts in to Pillow's bundled default font; this
|
|
342
|
+
# is the fallback when no matching system font can be located, so
|
|
343
|
+
# `TextFrame.fit_text` works on platforms without the requested
|
|
344
|
+
# family installed (notably most Linux runtimes).
|
|
345
|
+
try:
|
|
346
|
+
return ImageFont.load_default(size=point_size)
|
|
347
|
+
except TypeError:
|
|
348
|
+
# Pillow < 10.1 does not accept a `size` argument; fall back
|
|
349
|
+
# to the unsized bitmap default. Less accurate but at least
|
|
350
|
+
# never blows up.
|
|
351
|
+
return ImageFont.load_default()
|
|
352
|
+
return ImageFont.truetype(font_path, point_size)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _rendered_size(text, point_size, font_file):
|
|
356
|
+
"""
|
|
357
|
+
Return a (width, height) pair representing the size of *text* in English
|
|
358
|
+
Metric Units (EMU) when rendered at *point_size* in the font defined in
|
|
359
|
+
*font_file*.
|
|
360
|
+
"""
|
|
361
|
+
emu_per_inch = 914400
|
|
362
|
+
px_per_inch = 72.0
|
|
363
|
+
|
|
364
|
+
font = _Fonts.font(font_file, point_size)
|
|
365
|
+
try:
|
|
366
|
+
px_width, px_height = font.getsize(text)
|
|
367
|
+
except AttributeError:
|
|
368
|
+
left, top, right, bottom = font.getbbox(text)
|
|
369
|
+
px_width, px_height = right - left, bottom - top
|
|
370
|
+
|
|
371
|
+
emu_width = int(px_width / px_per_inch * emu_per_inch)
|
|
372
|
+
emu_height = int(px_height / px_per_inch * emu_per_inch)
|
|
373
|
+
|
|
374
|
+
return emu_width, emu_height
|