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

116 statements  

1from __future__ import annotations 

2 

3from . import ImageFont 

4from ._typing import _Ink 

5 

6 

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() 

52 

53 self.mode = mode 

54 self.spacing = spacing 

55 self.direction = direction 

56 self.features = features 

57 self.language = language 

58 

59 self.embedded_color = False 

60 

61 self.stroke_width: float = 0 

62 self.stroke_fill: _Ink | None = None 

63 

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 

72 

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 

82 

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" 

90 

91 def get_length(self) -> float: 

92 """ 

93 Returns length (in pixels with 1/64 precision) of text. 

94 

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. 

98 

99 The result is returned as a float; it is a whole number if using basic layout. 

100 

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. 

104 

105 For example, instead of:: 

106 

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 

111 

112 use:: 

113 

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 

121 

122 or disable kerning with (requires libraqm):: 

123 

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 

130 

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 ) 

147 

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) 

156 

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)] 

164 

165 if anchor[1] in "tb" and self.direction != "ttb": 

166 msg = "anchor not supported for multiline text" 

167 raise ValueError(msg) 

168 

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 ) 

182 

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) 

199 

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 

204 

205 idx = -1 

206 for line in lines: 

207 left = xy[0] 

208 idx += 1 

209 width_difference = max_width - widths[idx] 

210 

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) 

221 

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 

236 

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 

256 

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 

264 

265 return parts 

266 

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. 

275 

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. 

279 

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``. 

288 

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 ) 

318 

319 assert bbox is not None 

320 return bbox