Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/ImageDraw.py: 31%

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

493 statements  

1# 

2# The Python Imaging Library 

3# $Id$ 

4# 

5# drawing interface operations 

6# 

7# History: 

8# 1996-04-13 fl Created (experimental) 

9# 1996-08-07 fl Filled polygons, ellipses. 

10# 1996-08-13 fl Added text support 

11# 1998-06-28 fl Handle I and F images 

12# 1998-12-29 fl Added arc; use arc primitive to draw ellipses 

13# 1999-01-10 fl Added shape stuff (experimental) 

14# 1999-02-06 fl Added bitmap support 

15# 1999-02-11 fl Changed all primitives to take options 

16# 1999-02-20 fl Fixed backwards compatibility 

17# 2000-10-12 fl Copy on write, when necessary 

18# 2001-02-18 fl Use default ink for bitmap/text also in fill mode 

19# 2002-10-24 fl Added support for CSS-style color strings 

20# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing 

21# 2002-12-11 fl Refactored low-level drawing API (work in progress) 

22# 2004-08-26 fl Made Draw() a factory function, added getdraw() support 

23# 2004-09-04 fl Added width support to line primitive 

24# 2004-09-10 fl Added font mode handling 

25# 2006-06-19 fl Added font bearing support (getmask2) 

26# 

27# Copyright (c) 1997-2006 by Secret Labs AB 

28# Copyright (c) 1996-2006 by Fredrik Lundh 

29# 

30# See the README file for information on usage and redistribution. 

31# 

32from __future__ import annotations 

33 

34import math 

35import struct 

36from collections.abc import Sequence 

37from types import ModuleType 

38from typing import Any, AnyStr, Callable, Union, cast 

39 

40from . import Image, ImageColor 

41from ._deprecate import deprecate 

42from ._typing import Coords 

43 

44# experimental access to the outline API 

45Outline: Callable[[], Image.core._Outline] = Image.core.outline 

46 

47TYPE_CHECKING = False 

48if TYPE_CHECKING: 

49 from . import ImageDraw2, ImageFont 

50 

51_Ink = Union[float, tuple[int, ...], str] 

52 

53""" 

54A simple 2D drawing interface for PIL images. 

55<p> 

56Application code should use the <b>Draw</b> factory, instead of 

57directly. 

58""" 

59 

60 

61class ImageDraw: 

62 font: ( 

63 ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None 

64 ) = None 

65 

66 def __init__(self, im: Image.Image, mode: str | None = None) -> None: 

67 """ 

68 Create a drawing instance. 

69 

70 :param im: The image to draw in. 

71 :param mode: Optional mode to use for color values. For RGB 

72 images, this argument can be RGB or RGBA (to blend the 

73 drawing into the image). For all other modes, this argument 

74 must be the same as the image mode. If omitted, the mode 

75 defaults to the mode of the image. 

76 """ 

77 im.load() 

78 if im.readonly: 

79 im._copy() # make it writeable 

80 blend = 0 

81 if mode is None: 

82 mode = im.mode 

83 if mode != im.mode: 

84 if mode == "RGBA" and im.mode == "RGB": 

85 blend = 1 

86 else: 

87 msg = "mode mismatch" 

88 raise ValueError(msg) 

89 if mode == "P": 

90 self.palette = im.palette 

91 else: 

92 self.palette = None 

93 self._image = im 

94 self.im = im.im 

95 self.draw = Image.core.draw(self.im, blend) 

96 self.mode = mode 

97 if mode in ("I", "F"): 

98 self.ink = self.draw.draw_ink(1) 

99 else: 

100 self.ink = self.draw.draw_ink(-1) 

101 if mode in ("1", "P", "I", "F"): 

102 # FIXME: fix Fill2 to properly support matte for I+F images 

103 self.fontmode = "1" 

104 else: 

105 self.fontmode = "L" # aliasing is okay for other modes 

106 self.fill = False 

107 

108 def getfont( 

109 self, 

110 ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: 

111 """ 

112 Get the current default font. 

113 

114 To set the default font for this ImageDraw instance:: 

115 

116 from PIL import ImageDraw, ImageFont 

117 draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") 

118 

119 To set the default font for all future ImageDraw instances:: 

120 

121 from PIL import ImageDraw, ImageFont 

122 ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") 

123 

124 If the current default font is ``None``, 

125 it is initialized with ``ImageFont.load_default()``. 

126 

127 :returns: An image font.""" 

128 if not self.font: 

129 # FIXME: should add a font repository 

130 from . import ImageFont 

131 

132 self.font = ImageFont.load_default() 

133 return self.font 

134 

135 def _getfont( 

136 self, font_size: float | None 

137 ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: 

138 if font_size is not None: 

139 from . import ImageFont 

140 

141 return ImageFont.load_default(font_size) 

142 else: 

143 return self.getfont() 

144 

145 def _getink( 

146 self, ink: _Ink | None, fill: _Ink | None = None 

147 ) -> tuple[int | None, int | None]: 

148 result_ink = None 

149 result_fill = None 

150 if ink is None and fill is None: 

151 if self.fill: 

152 result_fill = self.ink 

153 else: 

154 result_ink = self.ink 

155 else: 

156 if ink is not None: 

157 if isinstance(ink, str): 

158 ink = ImageColor.getcolor(ink, self.mode) 

159 if self.palette and isinstance(ink, tuple): 

160 ink = self.palette.getcolor(ink, self._image) 

161 result_ink = self.draw.draw_ink(ink) 

162 if fill is not None: 

163 if isinstance(fill, str): 

164 fill = ImageColor.getcolor(fill, self.mode) 

165 if self.palette and isinstance(fill, tuple): 

166 fill = self.palette.getcolor(fill, self._image) 

167 result_fill = self.draw.draw_ink(fill) 

168 return result_ink, result_fill 

169 

170 def arc( 

171 self, 

172 xy: Coords, 

173 start: float, 

174 end: float, 

175 fill: _Ink | None = None, 

176 width: int = 1, 

177 ) -> None: 

178 """Draw an arc.""" 

179 ink, fill = self._getink(fill) 

180 if ink is not None: 

181 self.draw.draw_arc(xy, start, end, ink, width) 

182 

183 def bitmap( 

184 self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None 

185 ) -> None: 

186 """Draw a bitmap.""" 

187 bitmap.load() 

188 ink, fill = self._getink(fill) 

189 if ink is None: 

190 ink = fill 

191 if ink is not None: 

192 self.draw.draw_bitmap(xy, bitmap.im, ink) 

193 

194 def chord( 

195 self, 

196 xy: Coords, 

197 start: float, 

198 end: float, 

199 fill: _Ink | None = None, 

200 outline: _Ink | None = None, 

201 width: int = 1, 

202 ) -> None: 

203 """Draw a chord.""" 

204 ink, fill_ink = self._getink(outline, fill) 

205 if fill_ink is not None: 

206 self.draw.draw_chord(xy, start, end, fill_ink, 1) 

207 if ink is not None and ink != fill_ink and width != 0: 

208 self.draw.draw_chord(xy, start, end, ink, 0, width) 

209 

210 def ellipse( 

211 self, 

212 xy: Coords, 

213 fill: _Ink | None = None, 

214 outline: _Ink | None = None, 

215 width: int = 1, 

216 ) -> None: 

217 """Draw an ellipse.""" 

218 ink, fill_ink = self._getink(outline, fill) 

219 if fill_ink is not None: 

220 self.draw.draw_ellipse(xy, fill_ink, 1) 

221 if ink is not None and ink != fill_ink and width != 0: 

222 self.draw.draw_ellipse(xy, ink, 0, width) 

223 

224 def circle( 

225 self, 

226 xy: Sequence[float], 

227 radius: float, 

228 fill: _Ink | None = None, 

229 outline: _Ink | None = None, 

230 width: int = 1, 

231 ) -> None: 

232 """Draw a circle given center coordinates and a radius.""" 

233 ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius) 

234 self.ellipse(ellipse_xy, fill, outline, width) 

235 

236 def line( 

237 self, 

238 xy: Coords, 

239 fill: _Ink | None = None, 

240 width: int = 0, 

241 joint: str | None = None, 

242 ) -> None: 

243 """Draw a line, or a connected sequence of line segments.""" 

244 ink = self._getink(fill)[0] 

245 if ink is not None: 

246 self.draw.draw_lines(xy, ink, width) 

247 if joint == "curve" and width > 4: 

248 points: Sequence[Sequence[float]] 

249 if isinstance(xy[0], (list, tuple)): 

250 points = cast(Sequence[Sequence[float]], xy) 

251 else: 

252 points = [ 

253 cast(Sequence[float], tuple(xy[i : i + 2])) 

254 for i in range(0, len(xy), 2) 

255 ] 

256 for i in range(1, len(points) - 1): 

257 point = points[i] 

258 angles = [ 

259 math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) 

260 % 360 

261 for start, end in ( 

262 (points[i - 1], point), 

263 (point, points[i + 1]), 

264 ) 

265 ] 

266 if angles[0] == angles[1]: 

267 # This is a straight line, so no joint is required 

268 continue 

269 

270 def coord_at_angle( 

271 coord: Sequence[float], angle: float 

272 ) -> tuple[float, ...]: 

273 x, y = coord 

274 angle -= 90 

275 distance = width / 2 - 1 

276 return tuple( 

277 p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) 

278 for p, p_d in ( 

279 (x, distance * math.cos(math.radians(angle))), 

280 (y, distance * math.sin(math.radians(angle))), 

281 ) 

282 ) 

283 

284 flipped = ( 

285 angles[1] > angles[0] and angles[1] - 180 > angles[0] 

286 ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0]) 

287 coords = [ 

288 (point[0] - width / 2 + 1, point[1] - width / 2 + 1), 

289 (point[0] + width / 2 - 1, point[1] + width / 2 - 1), 

290 ] 

291 if flipped: 

292 start, end = (angles[1] + 90, angles[0] + 90) 

293 else: 

294 start, end = (angles[0] - 90, angles[1] - 90) 

295 self.pieslice(coords, start - 90, end - 90, fill) 

296 

297 if width > 8: 

298 # Cover potential gaps between the line and the joint 

299 if flipped: 

300 gap_coords = [ 

301 coord_at_angle(point, angles[0] + 90), 

302 point, 

303 coord_at_angle(point, angles[1] + 90), 

304 ] 

305 else: 

306 gap_coords = [ 

307 coord_at_angle(point, angles[0] - 90), 

308 point, 

309 coord_at_angle(point, angles[1] - 90), 

310 ] 

311 self.line(gap_coords, fill, width=3) 

312 

313 def shape( 

314 self, 

315 shape: Image.core._Outline, 

316 fill: _Ink | None = None, 

317 outline: _Ink | None = None, 

318 ) -> None: 

319 """(Experimental) Draw a shape.""" 

320 shape.close() 

321 ink, fill_ink = self._getink(outline, fill) 

322 if fill_ink is not None: 

323 self.draw.draw_outline(shape, fill_ink, 1) 

324 if ink is not None and ink != fill_ink: 

325 self.draw.draw_outline(shape, ink, 0) 

326 

327 def pieslice( 

328 self, 

329 xy: Coords, 

330 start: float, 

331 end: float, 

332 fill: _Ink | None = None, 

333 outline: _Ink | None = None, 

334 width: int = 1, 

335 ) -> None: 

336 """Draw a pieslice.""" 

337 ink, fill_ink = self._getink(outline, fill) 

338 if fill_ink is not None: 

339 self.draw.draw_pieslice(xy, start, end, fill_ink, 1) 

340 if ink is not None and ink != fill_ink and width != 0: 

341 self.draw.draw_pieslice(xy, start, end, ink, 0, width) 

342 

343 def point(self, xy: Coords, fill: _Ink | None = None) -> None: 

344 """Draw one or more individual pixels.""" 

345 ink, fill = self._getink(fill) 

346 if ink is not None: 

347 self.draw.draw_points(xy, ink) 

348 

349 def polygon( 

350 self, 

351 xy: Coords, 

352 fill: _Ink | None = None, 

353 outline: _Ink | None = None, 

354 width: int = 1, 

355 ) -> None: 

356 """Draw a polygon.""" 

357 ink, fill_ink = self._getink(outline, fill) 

358 if fill_ink is not None: 

359 self.draw.draw_polygon(xy, fill_ink, 1) 

360 if ink is not None and ink != fill_ink and width != 0: 

361 if width == 1: 

362 self.draw.draw_polygon(xy, ink, 0, width) 

363 elif self.im is not None: 

364 # To avoid expanding the polygon outwards, 

365 # use the fill as a mask 

366 mask = Image.new("1", self.im.size) 

367 mask_ink = self._getink(1)[0] 

368 draw = Draw(mask) 

369 draw.draw.draw_polygon(xy, mask_ink, 1) 

370 

371 self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im) 

372 

373 def regular_polygon( 

374 self, 

375 bounding_circle: Sequence[Sequence[float] | float], 

376 n_sides: int, 

377 rotation: float = 0, 

378 fill: _Ink | None = None, 

379 outline: _Ink | None = None, 

380 width: int = 1, 

381 ) -> None: 

382 """Draw a regular polygon.""" 

383 xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) 

384 self.polygon(xy, fill, outline, width) 

385 

386 def rectangle( 

387 self, 

388 xy: Coords, 

389 fill: _Ink | None = None, 

390 outline: _Ink | None = None, 

391 width: int = 1, 

392 ) -> None: 

393 """Draw a rectangle.""" 

394 ink, fill_ink = self._getink(outline, fill) 

395 if fill_ink is not None: 

396 self.draw.draw_rectangle(xy, fill_ink, 1) 

397 if ink is not None and ink != fill_ink and width != 0: 

398 self.draw.draw_rectangle(xy, ink, 0, width) 

399 

400 def rounded_rectangle( 

401 self, 

402 xy: Coords, 

403 radius: float = 0, 

404 fill: _Ink | None = None, 

405 outline: _Ink | None = None, 

406 width: int = 1, 

407 *, 

408 corners: tuple[bool, bool, bool, bool] | None = None, 

409 ) -> None: 

410 """Draw a rounded rectangle.""" 

411 if isinstance(xy[0], (list, tuple)): 

412 (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) 

413 else: 

414 x0, y0, x1, y1 = cast(Sequence[float], xy) 

415 if x1 < x0: 

416 msg = "x1 must be greater than or equal to x0" 

417 raise ValueError(msg) 

418 if y1 < y0: 

419 msg = "y1 must be greater than or equal to y0" 

420 raise ValueError(msg) 

421 if corners is None: 

422 corners = (True, True, True, True) 

423 

424 d = radius * 2 

425 

426 x0 = round(x0) 

427 y0 = round(y0) 

428 x1 = round(x1) 

429 y1 = round(y1) 

430 full_x, full_y = False, False 

431 if all(corners): 

432 full_x = d >= x1 - x0 - 1 

433 if full_x: 

434 # The two left and two right corners are joined 

435 d = x1 - x0 

436 full_y = d >= y1 - y0 - 1 

437 if full_y: 

438 # The two top and two bottom corners are joined 

439 d = y1 - y0 

440 if full_x and full_y: 

441 # If all corners are joined, that is a circle 

442 return self.ellipse(xy, fill, outline, width) 

443 

444 if d == 0 or not any(corners): 

445 # If the corners have no curve, 

446 # or there are no corners, 

447 # that is a rectangle 

448 return self.rectangle(xy, fill, outline, width) 

449 

450 r = int(d // 2) 

451 ink, fill_ink = self._getink(outline, fill) 

452 

453 def draw_corners(pieslice: bool) -> None: 

454 parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] 

455 if full_x: 

456 # Draw top and bottom halves 

457 parts = ( 

458 ((x0, y0, x0 + d, y0 + d), 180, 360), 

459 ((x0, y1 - d, x0 + d, y1), 0, 180), 

460 ) 

461 elif full_y: 

462 # Draw left and right halves 

463 parts = ( 

464 ((x0, y0, x0 + d, y0 + d), 90, 270), 

465 ((x1 - d, y0, x1, y0 + d), 270, 90), 

466 ) 

467 else: 

468 # Draw four separate corners 

469 parts = tuple( 

470 part 

471 for i, part in enumerate( 

472 ( 

473 ((x0, y0, x0 + d, y0 + d), 180, 270), 

474 ((x1 - d, y0, x1, y0 + d), 270, 360), 

475 ((x1 - d, y1 - d, x1, y1), 0, 90), 

476 ((x0, y1 - d, x0 + d, y1), 90, 180), 

477 ) 

478 ) 

479 if corners[i] 

480 ) 

481 for part in parts: 

482 if pieslice: 

483 self.draw.draw_pieslice(*(part + (fill_ink, 1))) 

484 else: 

485 self.draw.draw_arc(*(part + (ink, width))) 

486 

487 if fill_ink is not None: 

488 draw_corners(True) 

489 

490 if full_x: 

491 self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) 

492 elif x1 - r - 1 > x0 + r + 1: 

493 self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) 

494 if not full_x and not full_y: 

495 left = [x0, y0, x0 + r, y1] 

496 if corners[0]: 

497 left[1] += r + 1 

498 if corners[3]: 

499 left[3] -= r + 1 

500 self.draw.draw_rectangle(left, fill_ink, 1) 

501 

502 right = [x1 - r, y0, x1, y1] 

503 if corners[1]: 

504 right[1] += r + 1 

505 if corners[2]: 

506 right[3] -= r + 1 

507 self.draw.draw_rectangle(right, fill_ink, 1) 

508 if ink is not None and ink != fill_ink and width != 0: 

509 draw_corners(False) 

510 

511 if not full_x: 

512 top = [x0, y0, x1, y0 + width - 1] 

513 if corners[0]: 

514 top[0] += r + 1 

515 if corners[1]: 

516 top[2] -= r + 1 

517 self.draw.draw_rectangle(top, ink, 1) 

518 

519 bottom = [x0, y1 - width + 1, x1, y1] 

520 if corners[3]: 

521 bottom[0] += r + 1 

522 if corners[2]: 

523 bottom[2] -= r + 1 

524 self.draw.draw_rectangle(bottom, ink, 1) 

525 if not full_y: 

526 left = [x0, y0, x0 + width - 1, y1] 

527 if corners[0]: 

528 left[1] += r + 1 

529 if corners[3]: 

530 left[3] -= r + 1 

531 self.draw.draw_rectangle(left, ink, 1) 

532 

533 right = [x1 - width + 1, y0, x1, y1] 

534 if corners[1]: 

535 right[1] += r + 1 

536 if corners[2]: 

537 right[3] -= r + 1 

538 self.draw.draw_rectangle(right, ink, 1) 

539 

540 def _multiline_check(self, text: AnyStr) -> bool: 

541 split_character = "\n" if isinstance(text, str) else b"\n" 

542 

543 return split_character in text 

544 

545 def text( 

546 self, 

547 xy: tuple[float, float], 

548 text: AnyStr, 

549 fill: _Ink | None = None, 

550 font: ( 

551 ImageFont.ImageFont 

552 | ImageFont.FreeTypeFont 

553 | ImageFont.TransposedFont 

554 | None 

555 ) = None, 

556 anchor: str | None = None, 

557 spacing: float = 4, 

558 align: str = "left", 

559 direction: str | None = None, 

560 features: list[str] | None = None, 

561 language: str | None = None, 

562 stroke_width: float = 0, 

563 stroke_fill: _Ink | None = None, 

564 embedded_color: bool = False, 

565 *args: Any, 

566 **kwargs: Any, 

567 ) -> None: 

568 """Draw text.""" 

569 if embedded_color and self.mode not in ("RGB", "RGBA"): 

570 msg = "Embedded color supported only in RGB and RGBA modes" 

571 raise ValueError(msg) 

572 

573 if font is None: 

574 font = self._getfont(kwargs.get("font_size")) 

575 

576 if self._multiline_check(text): 

577 return self.multiline_text( 

578 xy, 

579 text, 

580 fill, 

581 font, 

582 anchor, 

583 spacing, 

584 align, 

585 direction, 

586 features, 

587 language, 

588 stroke_width, 

589 stroke_fill, 

590 embedded_color, 

591 ) 

592 

593 def getink(fill: _Ink | None) -> int: 

594 ink, fill_ink = self._getink(fill) 

595 if ink is None: 

596 assert fill_ink is not None 

597 return fill_ink 

598 return ink 

599 

600 def draw_text(ink: int, stroke_width: float = 0) -> None: 

601 mode = self.fontmode 

602 if stroke_width == 0 and embedded_color: 

603 mode = "RGBA" 

604 coord = [] 

605 for i in range(2): 

606 coord.append(int(xy[i])) 

607 start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) 

608 try: 

609 mask, offset = font.getmask2( # type: ignore[union-attr,misc] 

610 text, 

611 mode, 

612 direction=direction, 

613 features=features, 

614 language=language, 

615 stroke_width=stroke_width, 

616 stroke_filled=True, 

617 anchor=anchor, 

618 ink=ink, 

619 start=start, 

620 *args, 

621 **kwargs, 

622 ) 

623 coord = [coord[0] + offset[0], coord[1] + offset[1]] 

624 except AttributeError: 

625 try: 

626 mask = font.getmask( # type: ignore[misc] 

627 text, 

628 mode, 

629 direction, 

630 features, 

631 language, 

632 stroke_width, 

633 anchor, 

634 ink, 

635 start=start, 

636 *args, 

637 **kwargs, 

638 ) 

639 except TypeError: 

640 mask = font.getmask(text) 

641 if mode == "RGBA": 

642 # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A 

643 # extract mask and set text alpha 

644 color, mask = mask, mask.getband(3) 

645 ink_alpha = struct.pack("i", ink)[3] 

646 color.fillband(3, ink_alpha) 

647 x, y = coord 

648 if self.im is not None: 

649 self.im.paste( 

650 color, (x, y, x + mask.size[0], y + mask.size[1]), mask 

651 ) 

652 else: 

653 self.draw.draw_bitmap(coord, mask, ink) 

654 

655 ink = getink(fill) 

656 if ink is not None: 

657 stroke_ink = None 

658 if stroke_width: 

659 stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink 

660 

661 if stroke_ink is not None: 

662 # Draw stroked text 

663 draw_text(stroke_ink, stroke_width) 

664 

665 # Draw normal text 

666 if ink != stroke_ink: 

667 draw_text(ink) 

668 else: 

669 # Only draw normal text 

670 draw_text(ink) 

671 

672 def _prepare_multiline_text( 

673 self, 

674 xy: tuple[float, float], 

675 text: AnyStr, 

676 font: ( 

677 ImageFont.ImageFont 

678 | ImageFont.FreeTypeFont 

679 | ImageFont.TransposedFont 

680 | None 

681 ), 

682 anchor: str | None, 

683 spacing: float, 

684 align: str, 

685 direction: str | None, 

686 features: list[str] | None, 

687 language: str | None, 

688 stroke_width: float, 

689 embedded_color: bool, 

690 font_size: float | None, 

691 ) -> tuple[ 

692 ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, 

693 str, 

694 list[tuple[tuple[float, float], AnyStr]], 

695 ]: 

696 if direction == "ttb": 

697 msg = "ttb direction is unsupported for multiline text" 

698 raise ValueError(msg) 

699 

700 if anchor is None: 

701 anchor = "la" 

702 elif len(anchor) != 2: 

703 msg = "anchor must be a 2 character string" 

704 raise ValueError(msg) 

705 elif anchor[1] in "tb": 

706 msg = "anchor not supported for multiline text" 

707 raise ValueError(msg) 

708 

709 if font is None: 

710 font = self._getfont(font_size) 

711 

712 widths = [] 

713 max_width: float = 0 

714 lines = text.split("\n" if isinstance(text, str) else b"\n") 

715 line_spacing = ( 

716 self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] 

717 + stroke_width 

718 + spacing 

719 ) 

720 

721 for line in lines: 

722 line_width = self.textlength( 

723 line, 

724 font, 

725 direction=direction, 

726 features=features, 

727 language=language, 

728 embedded_color=embedded_color, 

729 ) 

730 widths.append(line_width) 

731 max_width = max(max_width, line_width) 

732 

733 top = xy[1] 

734 if anchor[1] == "m": 

735 top -= (len(lines) - 1) * line_spacing / 2.0 

736 elif anchor[1] == "d": 

737 top -= (len(lines) - 1) * line_spacing 

738 

739 parts = [] 

740 for idx, line in enumerate(lines): 

741 left = xy[0] 

742 width_difference = max_width - widths[idx] 

743 

744 # first align left by anchor 

745 if anchor[0] == "m": 

746 left -= width_difference / 2.0 

747 elif anchor[0] == "r": 

748 left -= width_difference 

749 

750 # then align by align parameter 

751 if align in ("left", "justify"): 

752 pass 

753 elif align == "center": 

754 left += width_difference / 2.0 

755 elif align == "right": 

756 left += width_difference 

757 else: 

758 msg = 'align must be "left", "center", "right" or "justify"' 

759 raise ValueError(msg) 

760 

761 if align == "justify" and width_difference != 0: 

762 words = line.split(" " if isinstance(text, str) else b" ") 

763 word_widths = [ 

764 self.textlength( 

765 word, 

766 font, 

767 direction=direction, 

768 features=features, 

769 language=language, 

770 embedded_color=embedded_color, 

771 ) 

772 for word in words 

773 ] 

774 width_difference = max_width - sum(word_widths) 

775 for i, word in enumerate(words): 

776 parts.append(((left, top), word)) 

777 left += word_widths[i] + width_difference / (len(words) - 1) 

778 else: 

779 parts.append(((left, top), line)) 

780 

781 top += line_spacing 

782 

783 return font, anchor, parts 

784 

785 def multiline_text( 

786 self, 

787 xy: tuple[float, float], 

788 text: AnyStr, 

789 fill: _Ink | None = None, 

790 font: ( 

791 ImageFont.ImageFont 

792 | ImageFont.FreeTypeFont 

793 | ImageFont.TransposedFont 

794 | None 

795 ) = None, 

796 anchor: str | None = None, 

797 spacing: float = 4, 

798 align: str = "left", 

799 direction: str | None = None, 

800 features: list[str] | None = None, 

801 language: str | None = None, 

802 stroke_width: float = 0, 

803 stroke_fill: _Ink | None = None, 

804 embedded_color: bool = False, 

805 *, 

806 font_size: float | None = None, 

807 ) -> None: 

808 font, anchor, lines = self._prepare_multiline_text( 

809 xy, 

810 text, 

811 font, 

812 anchor, 

813 spacing, 

814 align, 

815 direction, 

816 features, 

817 language, 

818 stroke_width, 

819 embedded_color, 

820 font_size, 

821 ) 

822 

823 for xy, line in lines: 

824 self.text( 

825 xy, 

826 line, 

827 fill, 

828 font, 

829 anchor, 

830 direction=direction, 

831 features=features, 

832 language=language, 

833 stroke_width=stroke_width, 

834 stroke_fill=stroke_fill, 

835 embedded_color=embedded_color, 

836 ) 

837 

838 def textlength( 

839 self, 

840 text: AnyStr, 

841 font: ( 

842 ImageFont.ImageFont 

843 | ImageFont.FreeTypeFont 

844 | ImageFont.TransposedFont 

845 | None 

846 ) = None, 

847 direction: str | None = None, 

848 features: list[str] | None = None, 

849 language: str | None = None, 

850 embedded_color: bool = False, 

851 *, 

852 font_size: float | None = None, 

853 ) -> float: 

854 """Get the length of a given string, in pixels with 1/64 precision.""" 

855 if self._multiline_check(text): 

856 msg = "can't measure length of multiline text" 

857 raise ValueError(msg) 

858 if embedded_color and self.mode not in ("RGB", "RGBA"): 

859 msg = "Embedded color supported only in RGB and RGBA modes" 

860 raise ValueError(msg) 

861 

862 if font is None: 

863 font = self._getfont(font_size) 

864 mode = "RGBA" if embedded_color else self.fontmode 

865 return font.getlength(text, mode, direction, features, language) 

866 

867 def textbbox( 

868 self, 

869 xy: tuple[float, float], 

870 text: AnyStr, 

871 font: ( 

872 ImageFont.ImageFont 

873 | ImageFont.FreeTypeFont 

874 | ImageFont.TransposedFont 

875 | None 

876 ) = None, 

877 anchor: str | None = None, 

878 spacing: float = 4, 

879 align: str = "left", 

880 direction: str | None = None, 

881 features: list[str] | None = None, 

882 language: str | None = None, 

883 stroke_width: float = 0, 

884 embedded_color: bool = False, 

885 *, 

886 font_size: float | None = None, 

887 ) -> tuple[float, float, float, float]: 

888 """Get the bounding box of a given string, in pixels.""" 

889 if embedded_color and self.mode not in ("RGB", "RGBA"): 

890 msg = "Embedded color supported only in RGB and RGBA modes" 

891 raise ValueError(msg) 

892 

893 if font is None: 

894 font = self._getfont(font_size) 

895 

896 if self._multiline_check(text): 

897 return self.multiline_textbbox( 

898 xy, 

899 text, 

900 font, 

901 anchor, 

902 spacing, 

903 align, 

904 direction, 

905 features, 

906 language, 

907 stroke_width, 

908 embedded_color, 

909 ) 

910 

911 mode = "RGBA" if embedded_color else self.fontmode 

912 bbox = font.getbbox( 

913 text, mode, direction, features, language, stroke_width, anchor 

914 ) 

915 return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] 

916 

917 def multiline_textbbox( 

918 self, 

919 xy: tuple[float, float], 

920 text: AnyStr, 

921 font: ( 

922 ImageFont.ImageFont 

923 | ImageFont.FreeTypeFont 

924 | ImageFont.TransposedFont 

925 | None 

926 ) = None, 

927 anchor: str | None = None, 

928 spacing: float = 4, 

929 align: str = "left", 

930 direction: str | None = None, 

931 features: list[str] | None = None, 

932 language: str | None = None, 

933 stroke_width: float = 0, 

934 embedded_color: bool = False, 

935 *, 

936 font_size: float | None = None, 

937 ) -> tuple[float, float, float, float]: 

938 font, anchor, lines = self._prepare_multiline_text( 

939 xy, 

940 text, 

941 font, 

942 anchor, 

943 spacing, 

944 align, 

945 direction, 

946 features, 

947 language, 

948 stroke_width, 

949 embedded_color, 

950 font_size, 

951 ) 

952 

953 bbox: tuple[float, float, float, float] | None = None 

954 

955 for xy, line in lines: 

956 bbox_line = self.textbbox( 

957 xy, 

958 line, 

959 font, 

960 anchor, 

961 direction=direction, 

962 features=features, 

963 language=language, 

964 stroke_width=stroke_width, 

965 embedded_color=embedded_color, 

966 ) 

967 if bbox is None: 

968 bbox = bbox_line 

969 else: 

970 bbox = ( 

971 min(bbox[0], bbox_line[0]), 

972 min(bbox[1], bbox_line[1]), 

973 max(bbox[2], bbox_line[2]), 

974 max(bbox[3], bbox_line[3]), 

975 ) 

976 

977 if bbox is None: 

978 return xy[0], xy[1], xy[0], xy[1] 

979 return bbox 

980 

981 

982def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: 

983 """ 

984 A simple 2D drawing interface for PIL images. 

985 

986 :param im: The image to draw in. 

987 :param mode: Optional mode to use for color values. For RGB 

988 images, this argument can be RGB or RGBA (to blend the 

989 drawing into the image). For all other modes, this argument 

990 must be the same as the image mode. If omitted, the mode 

991 defaults to the mode of the image. 

992 """ 

993 try: 

994 return getattr(im, "getdraw")(mode) 

995 except AttributeError: 

996 return ImageDraw(im, mode) 

997 

998 

999def getdraw( 

1000 im: Image.Image | None = None, hints: list[str] | None = None 

1001) -> tuple[ImageDraw2.Draw | None, ModuleType]: 

1002 """ 

1003 :param im: The image to draw in. 

1004 :param hints: An optional list of hints. Deprecated. 

1005 :returns: A (drawing context, drawing resource factory) tuple. 

1006 """ 

1007 if hints is not None: 

1008 deprecate("'hints' parameter", 12) 

1009 from . import ImageDraw2 

1010 

1011 draw = ImageDraw2.Draw(im) if im is not None else None 

1012 return draw, ImageDraw2 

1013 

1014 

1015def floodfill( 

1016 image: Image.Image, 

1017 xy: tuple[int, int], 

1018 value: float | tuple[int, ...], 

1019 border: float | tuple[int, ...] | None = None, 

1020 thresh: float = 0, 

1021) -> None: 

1022 """ 

1023 .. warning:: This method is experimental. 

1024 

1025 Fills a bounded region with a given color. 

1026 

1027 :param image: Target image. 

1028 :param xy: Seed position (a 2-item coordinate tuple). See 

1029 :ref:`coordinate-system`. 

1030 :param value: Fill color. 

1031 :param border: Optional border value. If given, the region consists of 

1032 pixels with a color different from the border color. If not given, 

1033 the region consists of pixels having the same color as the seed 

1034 pixel. 

1035 :param thresh: Optional threshold value which specifies a maximum 

1036 tolerable difference of a pixel value from the 'background' in 

1037 order for it to be replaced. Useful for filling regions of 

1038 non-homogeneous, but similar, colors. 

1039 """ 

1040 # based on an implementation by Eric S. Raymond 

1041 # amended by yo1995 @20180806 

1042 pixel = image.load() 

1043 assert pixel is not None 

1044 x, y = xy 

1045 try: 

1046 background = pixel[x, y] 

1047 if _color_diff(value, background) <= thresh: 

1048 return # seed point already has fill color 

1049 pixel[x, y] = value 

1050 except (ValueError, IndexError): 

1051 return # seed point outside image 

1052 edge = {(x, y)} 

1053 # use a set to keep record of current and previous edge pixels 

1054 # to reduce memory consumption 

1055 full_edge = set() 

1056 while edge: 

1057 new_edge = set() 

1058 for x, y in edge: # 4 adjacent method 

1059 for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): 

1060 # If already processed, or if a coordinate is negative, skip 

1061 if (s, t) in full_edge or s < 0 or t < 0: 

1062 continue 

1063 try: 

1064 p = pixel[s, t] 

1065 except (ValueError, IndexError): 

1066 pass 

1067 else: 

1068 full_edge.add((s, t)) 

1069 if border is None: 

1070 fill = _color_diff(p, background) <= thresh 

1071 else: 

1072 fill = p not in (value, border) 

1073 if fill: 

1074 pixel[s, t] = value 

1075 new_edge.add((s, t)) 

1076 full_edge = edge # discard pixels processed 

1077 edge = new_edge 

1078 

1079 

1080def _compute_regular_polygon_vertices( 

1081 bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float 

1082) -> list[tuple[float, float]]: 

1083 """ 

1084 Generate a list of vertices for a 2D regular polygon. 

1085 

1086 :param bounding_circle: The bounding circle is a sequence defined 

1087 by a point and radius. The polygon is inscribed in this circle. 

1088 (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) 

1089 :param n_sides: Number of sides 

1090 (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) 

1091 :param rotation: Apply an arbitrary rotation to the polygon 

1092 (e.g. ``rotation=90``, applies a 90 degree rotation) 

1093 :return: List of regular polygon vertices 

1094 (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) 

1095 

1096 How are the vertices computed? 

1097 1. Compute the following variables 

1098 - theta: Angle between the apothem & the nearest polygon vertex 

1099 - side_length: Length of each polygon edge 

1100 - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) 

1101 - polygon_radius: Polygon radius (last element of bounding_circle) 

1102 - angles: Location of each polygon vertex in polar grid 

1103 (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) 

1104 

1105 2. For each angle in angles, get the polygon vertex at that angle 

1106 The vertex is computed using the equation below. 

1107 X= xcos(φ) + ysin(φ) 

1108 Y= −xsin(φ) + ycos(φ) 

1109 

1110 Note: 

1111 φ = angle in degrees 

1112 x = 0 

1113 y = polygon_radius 

1114 

1115 The formula above assumes rotation around the origin. 

1116 In our case, we are rotating around the centroid. 

1117 To account for this, we use the formula below 

1118 X = xcos(φ) + ysin(φ) + centroid_x 

1119 Y = −xsin(φ) + ycos(φ) + centroid_y 

1120 """ 

1121 # 1. Error Handling 

1122 # 1.1 Check `n_sides` has an appropriate value 

1123 if not isinstance(n_sides, int): 

1124 msg = "n_sides should be an int" # type: ignore[unreachable] 

1125 raise TypeError(msg) 

1126 if n_sides < 3: 

1127 msg = "n_sides should be an int > 2" 

1128 raise ValueError(msg) 

1129 

1130 # 1.2 Check `bounding_circle` has an appropriate value 

1131 if not isinstance(bounding_circle, (list, tuple)): 

1132 msg = "bounding_circle should be a sequence" 

1133 raise TypeError(msg) 

1134 

1135 if len(bounding_circle) == 3: 

1136 if not all(isinstance(i, (int, float)) for i in bounding_circle): 

1137 msg = "bounding_circle should only contain numeric data" 

1138 raise ValueError(msg) 

1139 

1140 *centroid, polygon_radius = cast(list[float], list(bounding_circle)) 

1141 elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)): 

1142 if not all( 

1143 isinstance(i, (int, float)) for i in bounding_circle[0] 

1144 ) or not isinstance(bounding_circle[1], (int, float)): 

1145 msg = "bounding_circle should only contain numeric data" 

1146 raise ValueError(msg) 

1147 

1148 if len(bounding_circle[0]) != 2: 

1149 msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" 

1150 raise ValueError(msg) 

1151 

1152 centroid = cast(list[float], list(bounding_circle[0])) 

1153 polygon_radius = cast(float, bounding_circle[1]) 

1154 else: 

1155 msg = ( 

1156 "bounding_circle should contain 2D coordinates " 

1157 "and a radius (e.g. (x, y, r) or ((x, y), r) )" 

1158 ) 

1159 raise ValueError(msg) 

1160 

1161 if polygon_radius <= 0: 

1162 msg = "bounding_circle radius should be > 0" 

1163 raise ValueError(msg) 

1164 

1165 # 1.3 Check `rotation` has an appropriate value 

1166 if not isinstance(rotation, (int, float)): 

1167 msg = "rotation should be an int or float" # type: ignore[unreachable] 

1168 raise ValueError(msg) 

1169 

1170 # 2. Define Helper Functions 

1171 def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]: 

1172 return ( 

1173 round( 

1174 point[0] * math.cos(math.radians(360 - degrees)) 

1175 - point[1] * math.sin(math.radians(360 - degrees)) 

1176 + centroid[0], 

1177 2, 

1178 ), 

1179 round( 

1180 point[1] * math.cos(math.radians(360 - degrees)) 

1181 + point[0] * math.sin(math.radians(360 - degrees)) 

1182 + centroid[1], 

1183 2, 

1184 ), 

1185 ) 

1186 

1187 def _compute_polygon_vertex(angle: float) -> tuple[float, float]: 

1188 start_point = [polygon_radius, 0] 

1189 return _apply_rotation(start_point, angle) 

1190 

1191 def _get_angles(n_sides: int, rotation: float) -> list[float]: 

1192 angles = [] 

1193 degrees = 360 / n_sides 

1194 # Start with the bottom left polygon vertex 

1195 current_angle = (270 - 0.5 * degrees) + rotation 

1196 for _ in range(n_sides): 

1197 angles.append(current_angle) 

1198 current_angle += degrees 

1199 if current_angle > 360: 

1200 current_angle -= 360 

1201 return angles 

1202 

1203 # 3. Variable Declarations 

1204 angles = _get_angles(n_sides, rotation) 

1205 

1206 # 4. Compute Vertices 

1207 return [_compute_polygon_vertex(angle) for angle in angles] 

1208 

1209 

1210def _color_diff( 

1211 color1: float | tuple[int, ...], color2: float | tuple[int, ...] 

1212) -> float: 

1213 """ 

1214 Uses 1-norm distance to calculate difference between two values. 

1215 """ 

1216 first = color1 if isinstance(color1, tuple) else (color1,) 

1217 second = color2 if isinstance(color2, tuple) else (color2,) 

1218 

1219 return sum(abs(first[i] - second[i]) for i in range(len(second)))