Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/ImageText.py: 59%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3from . import ImageFont
4from ._typing import _Ink
7class Text:
8 def __init__(
9 self,
10 text: str | bytes,
11 font: (
12 ImageFont.ImageFont
13 | ImageFont.FreeTypeFont
14 | ImageFont.TransposedFont
15 | None
16 ) = None,
17 mode: str = "RGB",
18 spacing: float = 4,
19 direction: str | None = None,
20 features: list[str] | None = None,
21 language: str | None = None,
22 ) -> None:
23 """
24 :param text: String to be drawn.
25 :param font: Either an :py:class:`~PIL.ImageFont.ImageFont` instance,
26 :py:class:`~PIL.ImageFont.FreeTypeFont` instance,
27 :py:class:`~PIL.ImageFont.TransposedFont` instance or ``None``. If
28 ``None``, the default font from :py:meth:`.ImageFont.load_default`
29 will be used.
30 :param mode: The image mode this will be used with.
31 :param spacing: The number of pixels between lines.
32 :param direction: Direction of the text. It can be ``"rtl"`` (right to left),
33 ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
34 Requires libraqm.
35 :param features: A list of OpenType font features to be used during text
36 layout. This is usually used to turn on optional font features
37 that are not enabled by default, for example ``"dlig"`` or
38 ``"ss01"``, but can be also used to turn off default font
39 features, for example ``"-liga"`` to disable ligatures or
40 ``"-kern"`` to disable kerning. To get all supported
41 features, see `OpenType docs`_.
42 Requires libraqm.
43 :param language: Language of the text. Different languages may use
44 different glyph shapes or ligatures. This parameter tells
45 the font which language the text is in, and to apply the
46 correct substitutions as appropriate, if available.
47 It should be a `BCP 47 language code`_.
48 Requires libraqm.
49 """
50 self.text = text
51 self.font = font or ImageFont.load_default()
53 self.mode = mode
54 self.spacing = spacing
55 self.direction = direction
56 self.features = features
57 self.language = language
59 self.embedded_color = False
61 self.stroke_width: float = 0
62 self.stroke_fill: _Ink | None = None
64 def embed_color(self) -> None:
65 """
66 Use embedded color glyphs (COLR, CBDT, SBIX).
67 """
68 if self.mode not in ("RGB", "RGBA"):
69 msg = "Embedded color supported only in RGB and RGBA modes"
70 raise ValueError(msg)
71 self.embedded_color = True
73 def stroke(self, width: float = 0, fill: _Ink | None = None) -> None:
74 """
75 :param width: The width of the text stroke.
76 :param fill: Color to use for the text stroke when drawing. If not given, will
77 default to the ``fill`` parameter from
78 :py:meth:`.ImageDraw.ImageDraw.text`.
79 """
80 self.stroke_width = width
81 self.stroke_fill = fill
83 def _get_fontmode(self) -> str:
84 if self.mode in ("1", "P", "I", "F"):
85 return "1"
86 elif self.embedded_color:
87 return "RGBA"
88 else:
89 return "L"
91 def get_length(self) -> float:
92 """
93 Returns length (in pixels with 1/64 precision) of text.
95 This is the amount by which following text should be offset.
96 Text bounding box may extend past the length in some fonts,
97 e.g. when using italics or accents.
99 The result is returned as a float; it is a whole number if using basic layout.
101 Note that the sum of two lengths may not equal the length of a concatenated
102 string due to kerning. If you need to adjust for kerning, include the following
103 character and subtract its length.
105 For example, instead of::
107 hello = ImageText.Text("Hello", font).get_length()
108 world = ImageText.Text("World", font).get_length()
109 helloworld = ImageText.Text("HelloWorld", font).get_length()
110 assert hello + world == helloworld
112 use::
114 hello = (
115 ImageText.Text("HelloW", font).get_length() -
116 ImageText.Text("W", font).get_length()
117 ) # adjusted for kerning
118 world = ImageText.Text("World", font).get_length()
119 helloworld = ImageText.Text("HelloWorld", font).get_length()
120 assert hello + world == helloworld
122 or disable kerning with (requires libraqm)::
124 hello = ImageText.Text("Hello", font, features=["-kern"]).get_length()
125 world = ImageText.Text("World", font, features=["-kern"]).get_length()
126 helloworld = ImageText.Text(
127 "HelloWorld", font, features=["-kern"]
128 ).get_length()
129 assert hello + world == helloworld
131 :return: Either width for horizontal text, or height for vertical text.
132 """
133 if isinstance(self.text, str):
134 multiline = "\n" in self.text
135 else:
136 multiline = b"\n" in self.text
137 if multiline:
138 msg = "can't measure length of multiline text"
139 raise ValueError(msg)
140 return self.font.getlength(
141 self.text,
142 self._get_fontmode(),
143 self.direction,
144 self.features,
145 self.language,
146 )
148 def _split(
149 self, xy: tuple[float, float], anchor: str | None, align: str
150 ) -> list[tuple[tuple[float, float], str, str | bytes]]:
151 if anchor is None:
152 anchor = "lt" if self.direction == "ttb" else "la"
153 elif len(anchor) != 2:
154 msg = "anchor must be a 2 character string"
155 raise ValueError(msg)
157 lines = (
158 self.text.split("\n")
159 if isinstance(self.text, str)
160 else self.text.split(b"\n")
161 )
162 if len(lines) == 1:
163 return [(xy, anchor, self.text)]
165 if anchor[1] in "tb" and self.direction != "ttb":
166 msg = "anchor not supported for multiline text"
167 raise ValueError(msg)
169 fontmode = self._get_fontmode()
170 line_spacing = (
171 self.font.getbbox(
172 "A",
173 fontmode,
174 None,
175 self.features,
176 self.language,
177 self.stroke_width,
178 )[3]
179 + self.stroke_width
180 + self.spacing
181 )
183 top = xy[1]
184 parts = []
185 if self.direction == "ttb":
186 left = xy[0]
187 for line in lines:
188 parts.append(((left, top), anchor, line))
189 left += line_spacing
190 else:
191 widths = []
192 max_width: float = 0
193 for line in lines:
194 line_width = self.font.getlength(
195 line, fontmode, self.direction, self.features, self.language
196 )
197 widths.append(line_width)
198 max_width = max(max_width, line_width)
200 if anchor[1] == "m":
201 top -= (len(lines) - 1) * line_spacing / 2.0
202 elif anchor[1] == "d":
203 top -= (len(lines) - 1) * line_spacing
205 idx = -1
206 for line in lines:
207 left = xy[0]
208 idx += 1
209 width_difference = max_width - widths[idx]
211 # align by align parameter
212 if align in ("left", "justify"):
213 pass
214 elif align == "center":
215 left += width_difference / 2.0
216 elif align == "right":
217 left += width_difference
218 else:
219 msg = 'align must be "left", "center", "right" or "justify"'
220 raise ValueError(msg)
222 if (
223 align == "justify"
224 and width_difference != 0
225 and idx != len(lines) - 1
226 ):
227 words = (
228 line.split(" ") if isinstance(line, str) else line.split(b" ")
229 )
230 if len(words) > 1:
231 # align left by anchor
232 if anchor[0] == "m":
233 left -= max_width / 2.0
234 elif anchor[0] == "r":
235 left -= max_width
237 word_widths = [
238 self.font.getlength(
239 word,
240 fontmode,
241 self.direction,
242 self.features,
243 self.language,
244 )
245 for word in words
246 ]
247 word_anchor = "l" + anchor[1]
248 width_difference = max_width - sum(word_widths)
249 i = 0
250 for word in words:
251 parts.append(((left, top), word_anchor, word))
252 left += word_widths[i] + width_difference / (len(words) - 1)
253 i += 1
254 top += line_spacing
255 continue
257 # align left by anchor
258 if anchor[0] == "m":
259 left -= width_difference / 2.0
260 elif anchor[0] == "r":
261 left -= width_difference
262 parts.append(((left, top), anchor, line))
263 top += line_spacing
265 return parts
267 def get_bbox(
268 self,
269 xy: tuple[float, float] = (0, 0),
270 anchor: str | None = None,
271 align: str = "left",
272 ) -> tuple[float, float, float, float]:
273 """
274 Returns bounding box (in pixels) of text.
276 Use :py:meth:`get_length` to get the offset of following text with 1/64 pixel
277 precision. The bounding box includes extra margins for some fonts, e.g. italics
278 or accents.
280 :param xy: The anchor coordinates of the text.
281 :param anchor: The text anchor alignment. Determines the relative location of
282 the anchor to the text. The default alignment is top left,
283 specifically ``la`` for horizontal text and ``lt`` for
284 vertical text. See :ref:`text-anchors` for details.
285 :param align: For multiline text, ``"left"``, ``"center"``, ``"right"`` or
286 ``"justify"`` determines the relative alignment of lines. Use the
287 ``anchor`` parameter to specify the alignment to ``xy``.
289 :return: ``(left, top, right, bottom)`` bounding box
290 """
291 bbox: tuple[float, float, float, float] | None = None
292 fontmode = self._get_fontmode()
293 for xy, anchor, line in self._split(xy, anchor, align):
294 bbox_line = self.font.getbbox(
295 line,
296 fontmode,
297 self.direction,
298 self.features,
299 self.language,
300 self.stroke_width,
301 anchor,
302 )
303 bbox_line = (
304 bbox_line[0] + xy[0],
305 bbox_line[1] + xy[1],
306 bbox_line[2] + xy[0],
307 bbox_line[3] + xy[1],
308 )
309 if bbox is None:
310 bbox = bbox_line
311 else:
312 bbox = (
313 min(bbox[0], bbox_line[0]),
314 min(bbox[1], bbox_line[1]),
315 max(bbox[2], bbox_line[2]),
316 max(bbox[3], bbox_line[3]),
317 )
319 assert bbox is not None
320 return bbox