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

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

416 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 typing import cast 

38 

39from . import Image, ImageColor, ImageFont, ImageText 

40 

41TYPE_CHECKING = False 

42if TYPE_CHECKING: 

43 from collections.abc import Callable 

44 from types import ModuleType 

45 from typing import Any, AnyStr 

46 

47 from . import ImageDraw2 

48 from ._typing import Coords, _Ink 

49 

50# experimental access to the outline API 

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

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: ImageFont.BaseImageFont | None = None 

63 

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

65 """ 

66 Create a drawing instance. 

67 

68 :param im: The image to draw in. 

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

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

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

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

73 defaults to the mode of the image. 

74 """ 

75 im._ensure_mutable() 

76 blend = 0 

77 if mode is None: 

78 mode = im.mode 

79 if mode != im.mode: 

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

81 blend = 1 

82 else: 

83 msg = "mode mismatch" 

84 raise ValueError(msg) 

85 if mode == "P": 

86 self.palette = im.palette 

87 else: 

88 self.palette = None 

89 self._image = im 

90 self.im = im.im 

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

92 self.mode = mode 

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

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

95 else: 

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

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

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

99 self.fontmode = "1" 

100 else: 

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

102 self.fill = False 

103 

104 def getfont( 

105 self, 

106 ) -> ImageFont.BaseImageFont: 

107 """ 

108 Get the current default font. 

109 

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

111 

112 from PIL import ImageDraw, ImageFont 

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

114 

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

116 

117 from PIL import ImageDraw, ImageFont 

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

119 

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

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

122 

123 :returns: An image font.""" 

124 if not self.font: 

125 # FIXME: should add a font repository 

126 self.font = ImageFont.load_default() 

127 return self.font 

128 

129 def _getfont(self, font_size: float | None) -> ImageFont.BaseImageFont: 

130 if font_size is not None: 

131 return ImageFont.load_default(font_size) 

132 else: 

133 return self.getfont() 

134 

135 def _getink( 

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

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

138 result_ink = None 

139 result_fill = None 

140 if ink is None and fill is None: 

141 if self.fill: 

142 result_fill = self.ink 

143 else: 

144 result_ink = self.ink 

145 else: 

146 if ink is not None: 

147 if isinstance(ink, str): 

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

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

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

151 result_ink = self.draw.draw_ink(ink) 

152 if fill is not None: 

153 if isinstance(fill, str): 

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

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

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

157 result_fill = self.draw.draw_ink(fill) 

158 return result_ink, result_fill 

159 

160 def arc( 

161 self, 

162 xy: Coords, 

163 start: float, 

164 end: float, 

165 fill: _Ink | None = None, 

166 width: int = 1, 

167 ) -> None: 

168 """Draw an arc.""" 

169 ink, fill = self._getink(fill) 

170 if ink is not None and width != 0: 

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

172 

173 def bitmap( 

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

175 ) -> None: 

176 """Draw a bitmap.""" 

177 bitmap.load() 

178 ink, fill = self._getink(fill) 

179 if ink is None: 

180 ink = fill 

181 if ink is not None: 

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

183 

184 def chord( 

185 self, 

186 xy: Coords, 

187 start: float, 

188 end: float, 

189 fill: _Ink | None = None, 

190 outline: _Ink | None = None, 

191 width: int = 1, 

192 ) -> None: 

193 """Draw a chord.""" 

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

195 if fill_ink is not None: 

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

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

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

199 

200 def ellipse( 

201 self, 

202 xy: Coords, 

203 fill: _Ink | None = None, 

204 outline: _Ink | None = None, 

205 width: int = 1, 

206 ) -> None: 

207 """Draw an ellipse.""" 

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

209 if fill_ink is not None: 

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

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

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

213 

214 def circle( 

215 self, 

216 xy: Sequence[float], 

217 radius: float, 

218 fill: _Ink | None = None, 

219 outline: _Ink | None = None, 

220 width: int = 1, 

221 ) -> None: 

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

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

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

225 

226 def line( 

227 self, 

228 xy: Coords, 

229 fill: _Ink | None = None, 

230 width: int = 1, 

231 joint: str | None = None, 

232 ) -> None: 

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

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

235 if ink is not None and width != 0: 

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

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

238 points: Sequence[Sequence[float]] 

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

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

241 else: 

242 points = [ 

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

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

245 ] 

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

247 point = points[i] 

248 angles = [ 

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

250 % 360 

251 for start, end in ( 

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

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

254 ) 

255 ] 

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

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

258 continue 

259 

260 def coord_at_angle( 

261 coord: Sequence[float], angle: float 

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

263 x, y = coord 

264 angle -= 90 

265 distance = width / 2 - 1 

266 return tuple( 

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

268 for p, p_d in ( 

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

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

271 ) 

272 ) 

273 

274 flipped = ( 

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

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

277 coords = [ 

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

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

280 ] 

281 if flipped: 

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

283 else: 

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

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

286 

287 if width > 8: 

288 # Cover potential gaps between the line and the joint 

289 if flipped: 

290 gap_coords = [ 

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

292 point, 

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

294 ] 

295 else: 

296 gap_coords = [ 

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

298 point, 

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

300 ] 

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

302 

303 def shape( 

304 self, 

305 shape: Image.core._Outline, 

306 fill: _Ink | None = None, 

307 outline: _Ink | None = None, 

308 ) -> None: 

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

310 shape.close() 

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

312 if fill_ink is not None: 

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

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

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

316 

317 def pieslice( 

318 self, 

319 xy: Coords, 

320 start: float, 

321 end: float, 

322 fill: _Ink | None = None, 

323 outline: _Ink | None = None, 

324 width: int = 1, 

325 ) -> None: 

326 """Draw a pieslice.""" 

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

328 if fill_ink is not None: 

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

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

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

332 

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

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

335 ink, fill = self._getink(fill) 

336 if ink is not None: 

337 self.draw.draw_points(xy, ink) 

338 

339 def polygon( 

340 self, 

341 xy: Coords, 

342 fill: _Ink | None = None, 

343 outline: _Ink | None = None, 

344 width: int = 1, 

345 ) -> None: 

346 """Draw a polygon.""" 

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

348 if fill_ink is not None: 

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

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

351 if width == 1: 

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

353 elif self.im is not None: 

354 # To avoid expanding the polygon outwards, 

355 # use the fill as a mask 

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

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

358 draw = Draw(mask) 

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

360 

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

362 

363 def regular_polygon( 

364 self, 

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

366 n_sides: int, 

367 rotation: float = 0, 

368 fill: _Ink | None = None, 

369 outline: _Ink | None = None, 

370 width: int = 1, 

371 ) -> None: 

372 """Draw a regular polygon.""" 

373 xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) 

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

375 

376 def rectangle( 

377 self, 

378 xy: Coords, 

379 fill: _Ink | None = None, 

380 outline: _Ink | None = None, 

381 width: int = 1, 

382 ) -> None: 

383 """Draw a rectangle.""" 

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

385 if fill_ink is not None: 

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

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

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

389 

390 def rounded_rectangle( 

391 self, 

392 xy: Coords, 

393 radius: float = 0, 

394 fill: _Ink | None = None, 

395 outline: _Ink | None = None, 

396 width: int = 1, 

397 *, 

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

399 ) -> None: 

400 """Draw a rounded rectangle.""" 

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

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

403 else: 

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

405 if x1 < x0: 

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

407 raise ValueError(msg) 

408 if y1 < y0: 

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

410 raise ValueError(msg) 

411 if corners is None: 

412 corners = (True, True, True, True) 

413 

414 d = min(x1 - x0, y1 - y0, radius * 2) 

415 

416 x0 = round(x0) 

417 y0 = round(y0) 

418 x1 = round(x1) 

419 y1 = round(y1) 

420 full_x, full_y = False, False 

421 if all(corners): 

422 full_x = d >= x1 - x0 - 1 

423 if full_x: 

424 # The two left and two right corners are joined 

425 d = x1 - x0 

426 full_y = d >= y1 - y0 - 1 

427 if full_y: 

428 # The two top and two bottom corners are joined 

429 d = y1 - y0 

430 if full_x and full_y: 

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

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

433 

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

435 # If the corners have no curve, 

436 # or there are no corners, 

437 # that is a rectangle 

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

439 

440 r = int(d // 2) 

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

442 

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

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

445 if full_x: 

446 # Draw top and bottom halves 

447 parts = ( 

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

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

450 ) 

451 elif full_y: 

452 # Draw left and right halves 

453 parts = ( 

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

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

456 ) 

457 else: 

458 # Draw four separate corners 

459 parts = tuple( 

460 part 

461 for i, part in enumerate( 

462 ( 

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

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

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

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

467 ) 

468 ) 

469 if corners[i] 

470 ) 

471 for part in parts: 

472 if pieslice: 

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

474 else: 

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

476 

477 if fill_ink is not None: 

478 draw_corners(True) 

479 

480 if full_x: 

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

482 elif x1 - r - 1 >= x0 + r + 1: 

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

484 if not full_x and not full_y: 

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

486 if corners[0]: 

487 left[1] += r + 1 

488 if corners[3]: 

489 left[3] -= r + 1 

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

491 

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

493 if corners[1]: 

494 right[1] += r + 1 

495 if corners[2]: 

496 right[3] -= r + 1 

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

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

499 draw_corners(False) 

500 

501 if not full_x: 

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

503 if corners[0]: 

504 top[0] += r + 1 

505 if corners[1]: 

506 top[2] -= r + 1 

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

508 

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

510 if corners[3]: 

511 bottom[0] += r + 1 

512 if corners[2]: 

513 bottom[2] -= r + 1 

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

515 if not full_y: 

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

517 if corners[0]: 

518 left[1] += r + 1 

519 if corners[3]: 

520 left[3] -= r + 1 

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

522 

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

524 if corners[1]: 

525 right[1] += r + 1 

526 if corners[2]: 

527 right[3] -= r + 1 

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

529 

530 def text( 

531 self, 

532 xy: tuple[float, float], 

533 text: AnyStr | ImageText.Text[AnyStr], 

534 fill: _Ink | None = None, 

535 font: ImageFont.BaseImageFont | None = None, 

536 anchor: str | None = None, 

537 spacing: float = 4, 

538 align: str = "left", 

539 direction: str | None = None, 

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

541 language: str | None = None, 

542 stroke_width: float = 0, 

543 stroke_fill: _Ink | None = None, 

544 embedded_color: bool = False, 

545 *args: Any, 

546 **kwargs: Any, 

547 ) -> None: 

548 """Draw text.""" 

549 if isinstance(text, ImageText.Text): 

550 image_text = text 

551 else: 

552 if font is None: 

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

554 image_text = ImageText.Text( 

555 text, font, self.mode, spacing, direction, features, language 

556 ) 

557 if embedded_color: 

558 image_text.embed_color() 

559 if stroke_width: 

560 image_text.stroke(stroke_width, stroke_fill) 

561 

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

563 ink, fill_ink = self._getink(fill) 

564 if ink is None: 

565 assert fill_ink is not None 

566 return fill_ink 

567 return ink 

568 

569 ink = getink(fill) 

570 if ink is None: 

571 return 

572 

573 stroke_ink = None 

574 if image_text.stroke_width: 

575 stroke_ink = ( 

576 getink(image_text.stroke_fill) 

577 if image_text.stroke_fill is not None 

578 else ink 

579 ) 

580 

581 for line in image_text._split(xy, anchor, align): 

582 

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

584 mode = self.fontmode 

585 if stroke_width == 0 and embedded_color: 

586 mode = "RGBA" 

587 x = int(line.x) 

588 y = int(line.y) 

589 start = (math.modf(line.x)[0], math.modf(line.y)[0]) 

590 if isinstance(image_text.font, ImageFont.FreeTypeFont): 

591 mask, offset = image_text.font.getmask2( 

592 line.text, 

593 mode, 

594 direction, 

595 features, 

596 language, 

597 stroke_width, 

598 line.anchor, 

599 ink, 

600 start, 

601 stroke_filled=True, 

602 *args, 

603 **kwargs, 

604 ) 

605 x += offset[0] 

606 y += offset[1] 

607 else: 

608 try: 

609 mask = image_text.font.getmask( 

610 line.text, 

611 mode, 

612 direction, 

613 features, 

614 language, 

615 stroke_width, 

616 line.anchor, 

617 ink, 

618 start=start, 

619 *args, 

620 **kwargs, 

621 ) 

622 except TypeError: 

623 mask = image_text.font.getmask(line.text) 

624 if mode == "RGBA": 

625 # image_text.font.getmask2(mode="RGBA") 

626 # returns color in RGB bands and mask in A 

627 # extract mask and set text alpha 

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

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

630 color.fillband(3, ink_alpha) 

631 if self.im is not None: 

632 self.im.paste( 

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

634 ) 

635 else: 

636 self.draw.draw_bitmap((x, y), mask, ink) 

637 

638 if stroke_ink is not None: 

639 # Draw stroked text 

640 draw_text(stroke_ink, image_text.stroke_width) 

641 

642 # Draw normal text 

643 if ink != stroke_ink: 

644 draw_text(ink) 

645 else: 

646 # Only draw normal text 

647 draw_text(ink) 

648 

649 def multiline_text( 

650 self, 

651 xy: tuple[float, float], 

652 text: AnyStr, 

653 fill: _Ink | None = None, 

654 font: ImageFont.BaseImageFont | None = None, 

655 anchor: str | None = None, 

656 spacing: float = 4, 

657 align: str = "left", 

658 direction: str | None = None, 

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

660 language: str | None = None, 

661 stroke_width: float = 0, 

662 stroke_fill: _Ink | None = None, 

663 embedded_color: bool = False, 

664 *, 

665 font_size: float | None = None, 

666 ) -> None: 

667 return self.text( 

668 xy, 

669 text, 

670 fill, 

671 font, 

672 anchor, 

673 spacing, 

674 align, 

675 direction, 

676 features, 

677 language, 

678 stroke_width, 

679 stroke_fill, 

680 embedded_color, 

681 font_size=font_size, 

682 ) 

683 

684 def textlength( 

685 self, 

686 text: AnyStr, 

687 font: ImageFont.BaseImageFont | None = None, 

688 direction: str | None = None, 

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

690 language: str | None = None, 

691 embedded_color: bool = False, 

692 *, 

693 font_size: float | None = None, 

694 ) -> float: 

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

696 if font is None: 

697 font = self._getfont(font_size) 

698 image_text = ImageText.Text( 

699 text, 

700 font, 

701 self.mode, 

702 direction=direction, 

703 features=features, 

704 language=language, 

705 ) 

706 if embedded_color: 

707 image_text.embed_color() 

708 return image_text.get_length() 

709 

710 def textbbox( 

711 self, 

712 xy: tuple[float, float], 

713 text: AnyStr, 

714 font: ImageFont.BaseImageFont | None = None, 

715 anchor: str | None = None, 

716 spacing: float = 4, 

717 align: str = "left", 

718 direction: str | None = None, 

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

720 language: str | None = None, 

721 stroke_width: float = 0, 

722 embedded_color: bool = False, 

723 *, 

724 font_size: float | None = None, 

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

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

727 if font is None: 

728 font = self._getfont(font_size) 

729 image_text = ImageText.Text( 

730 text, font, self.mode, spacing, direction, features, language 

731 ) 

732 if embedded_color: 

733 image_text.embed_color() 

734 if stroke_width: 

735 image_text.stroke(stroke_width) 

736 return image_text.get_bbox(xy, anchor, align) 

737 

738 def multiline_textbbox( 

739 self, 

740 xy: tuple[float, float], 

741 text: AnyStr, 

742 font: ImageFont.BaseImageFont | None = None, 

743 anchor: str | None = None, 

744 spacing: float = 4, 

745 align: str = "left", 

746 direction: str | None = None, 

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

748 language: str | None = None, 

749 stroke_width: float = 0, 

750 embedded_color: bool = False, 

751 *, 

752 font_size: float | None = None, 

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

754 return self.textbbox( 

755 xy, 

756 text, 

757 font, 

758 anchor, 

759 spacing, 

760 align, 

761 direction, 

762 features, 

763 language, 

764 stroke_width, 

765 embedded_color, 

766 font_size=font_size, 

767 ) 

768 

769 

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

771 """ 

772 A simple 2D drawing interface for PIL images. 

773 

774 :param im: The image to draw in. 

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

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

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

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

779 defaults to the mode of the image. 

780 """ 

781 try: 

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

783 except AttributeError: 

784 return ImageDraw(im, mode) 

785 

786 

787def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]: 

788 """ 

789 :param im: The image to draw in. 

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

791 """ 

792 from . import ImageDraw2 

793 

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

795 return draw, ImageDraw2 

796 

797 

798def floodfill( 

799 image: Image.Image, 

800 xy: tuple[int, int], 

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

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

803 thresh: float = 0, 

804) -> None: 

805 """ 

806 .. warning:: This method is experimental. 

807 

808 Fills a bounded region with a given color. 

809 

810 :param image: Target image. 

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

812 :ref:`coordinate-system`. 

813 :param value: Fill color. 

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

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

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

817 pixel. 

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

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

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

821 non-homogeneous, but similar, colors. 

822 """ 

823 # based on an implementation by Eric S. Raymond 

824 # amended by yo1995 @20180806 

825 pixel = image.load() 

826 assert pixel is not None 

827 x, y = xy 

828 try: 

829 background = pixel[x, y] 

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

831 return # seed point already has fill color 

832 pixel[x, y] = value 

833 except (ValueError, IndexError): 

834 return # seed point outside image 

835 edge = {(x, y)} 

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

837 # to reduce memory consumption 

838 full_edge = set() 

839 while edge: 

840 new_edge = set() 

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

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

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

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

845 continue 

846 try: 

847 p = pixel[s, t] 

848 except (ValueError, IndexError): 

849 pass 

850 else: 

851 full_edge.add((s, t)) 

852 if border is None: 

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

854 else: 

855 fill = p not in (value, border) 

856 if fill: 

857 pixel[s, t] = value 

858 new_edge.add((s, t)) 

859 full_edge = edge # discard pixels processed 

860 edge = new_edge 

861 

862 

863def _compute_regular_polygon_vertices( 

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

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

866 """ 

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

868 

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

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

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

872 :param n_sides: Number of sides 

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

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

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

876 :return: List of regular polygon vertices 

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

878 

879 How are the vertices computed? 

880 1. Compute the following variables 

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

882 - side_length: Length of each polygon edge 

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

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

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

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

887 

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

889 The vertex is computed using the equation below. 

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

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

892 

893 Note: 

894 φ = angle in degrees 

895 x = 0 

896 y = polygon_radius 

897 

898 The formula above assumes rotation around the origin. 

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

900 To account for this, we use the formula below 

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

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

903 """ 

904 # 1. Error Handling 

905 # 1.1 Check `n_sides` has an appropriate value 

906 if not isinstance(n_sides, int): 

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

908 raise TypeError(msg) 

909 if n_sides < 3: 

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

911 raise ValueError(msg) 

912 

913 # 1.2 Check `bounding_circle` has an appropriate value 

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

915 msg = "bounding_circle should be a sequence" 

916 raise TypeError(msg) 

917 

918 if len(bounding_circle) == 3: 

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

920 msg = "bounding_circle should only contain numeric data" 

921 raise ValueError(msg) 

922 

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

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

925 if not all( 

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

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

928 msg = "bounding_circle should only contain numeric data" 

929 raise ValueError(msg) 

930 

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

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

933 raise ValueError(msg) 

934 

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

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

937 else: 

938 msg = ( 

939 "bounding_circle should contain 2D coordinates " 

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

941 ) 

942 raise ValueError(msg) 

943 

944 if polygon_radius <= 0: 

945 msg = "bounding_circle radius should be > 0" 

946 raise ValueError(msg) 

947 

948 # 1.3 Check `rotation` has an appropriate value 

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

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

951 raise ValueError(msg) 

952 

953 # 2. Define Helper Functions 

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

955 return ( 

956 round( 

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

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

959 + centroid[0], 

960 2, 

961 ), 

962 round( 

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

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

965 + centroid[1], 

966 2, 

967 ), 

968 ) 

969 

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

971 start_point = [polygon_radius, 0] 

972 return _apply_rotation(start_point, angle) 

973 

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

975 angles = [] 

976 degrees = 360 / n_sides 

977 # Start with the bottom left polygon vertex 

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

979 for _ in range(n_sides): 

980 angles.append(current_angle) 

981 current_angle += degrees 

982 if current_angle > 360: 

983 current_angle -= 360 

984 return angles 

985 

986 # 3. Variable Declarations 

987 angles = _get_angles(n_sides, rotation) 

988 

989 # 4. Compute Vertices 

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

991 

992 

993def _color_diff( 

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

995) -> float: 

996 """ 

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

998 """ 

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

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

1001 

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