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

419 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, 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, ImageFont 

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

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

78 blend = 0 

79 if mode is None: 

80 mode = im.mode 

81 if mode != im.mode: 

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

83 blend = 1 

84 else: 

85 msg = "mode mismatch" 

86 raise ValueError(msg) 

87 if mode == "P": 

88 self.palette = im.palette 

89 else: 

90 self.palette = None 

91 self._image = im 

92 self.im = im.im 

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

94 self.mode = mode 

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

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

97 else: 

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

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

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

101 self.fontmode = "1" 

102 else: 

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

104 self.fill = False 

105 

106 def getfont( 

107 self, 

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

109 """ 

110 Get the current default font. 

111 

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

113 

114 from PIL import ImageDraw, ImageFont 

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

116 

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

118 

119 from PIL import ImageDraw, ImageFont 

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

121 

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

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

124 

125 :returns: An image font.""" 

126 if not self.font: 

127 # FIXME: should add a font repository 

128 from . import ImageFont 

129 

130 self.font = ImageFont.load_default() 

131 return self.font 

132 

133 def _getfont( 

134 self, font_size: float | None 

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

136 if font_size is not None: 

137 from . import ImageFont 

138 

139 return ImageFont.load_default(font_size) 

140 else: 

141 return self.getfont() 

142 

143 def _getink( 

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

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

146 result_ink = None 

147 result_fill = None 

148 if ink is None and fill is None: 

149 if self.fill: 

150 result_fill = self.ink 

151 else: 

152 result_ink = self.ink 

153 else: 

154 if ink is not None: 

155 if isinstance(ink, str): 

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

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

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

159 result_ink = self.draw.draw_ink(ink) 

160 if fill is not None: 

161 if isinstance(fill, str): 

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

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

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

165 result_fill = self.draw.draw_ink(fill) 

166 return result_ink, result_fill 

167 

168 def arc( 

169 self, 

170 xy: Coords, 

171 start: float, 

172 end: float, 

173 fill: _Ink | None = None, 

174 width: int = 1, 

175 ) -> None: 

176 """Draw an arc.""" 

177 ink, fill = self._getink(fill) 

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

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

180 

181 def bitmap( 

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

183 ) -> None: 

184 """Draw a bitmap.""" 

185 bitmap.load() 

186 ink, fill = self._getink(fill) 

187 if ink is None: 

188 ink = fill 

189 if ink is not None: 

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

191 

192 def chord( 

193 self, 

194 xy: Coords, 

195 start: float, 

196 end: float, 

197 fill: _Ink | None = None, 

198 outline: _Ink | None = None, 

199 width: int = 1, 

200 ) -> None: 

201 """Draw a chord.""" 

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

203 if fill_ink is not None: 

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

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

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

207 

208 def ellipse( 

209 self, 

210 xy: Coords, 

211 fill: _Ink | None = None, 

212 outline: _Ink | None = None, 

213 width: int = 1, 

214 ) -> None: 

215 """Draw an ellipse.""" 

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

217 if fill_ink is not None: 

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

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

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

221 

222 def circle( 

223 self, 

224 xy: Sequence[float], 

225 radius: float, 

226 fill: _Ink | None = None, 

227 outline: _Ink | None = None, 

228 width: int = 1, 

229 ) -> None: 

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

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

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

233 

234 def line( 

235 self, 

236 xy: Coords, 

237 fill: _Ink | None = None, 

238 width: int = 1, 

239 joint: str | None = None, 

240 ) -> None: 

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

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

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

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

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

246 points: Sequence[Sequence[float]] 

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

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

249 else: 

250 points = [ 

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

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

253 ] 

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

255 point = points[i] 

256 angles = [ 

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

258 % 360 

259 for start, end in ( 

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

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

262 ) 

263 ] 

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

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

266 continue 

267 

268 def coord_at_angle( 

269 coord: Sequence[float], angle: float 

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

271 x, y = coord 

272 angle -= 90 

273 distance = width / 2 - 1 

274 return tuple( 

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

276 for p, p_d in ( 

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

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

279 ) 

280 ) 

281 

282 flipped = ( 

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

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

285 coords = [ 

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

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

288 ] 

289 if flipped: 

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

291 else: 

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

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

294 

295 if width > 8: 

296 # Cover potential gaps between the line and the joint 

297 if flipped: 

298 gap_coords = [ 

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

300 point, 

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

302 ] 

303 else: 

304 gap_coords = [ 

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

306 point, 

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

308 ] 

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

310 

311 def shape( 

312 self, 

313 shape: Image.core._Outline, 

314 fill: _Ink | None = None, 

315 outline: _Ink | None = None, 

316 ) -> None: 

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

318 shape.close() 

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

320 if fill_ink is not None: 

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

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

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

324 

325 def pieslice( 

326 self, 

327 xy: Coords, 

328 start: float, 

329 end: float, 

330 fill: _Ink | None = None, 

331 outline: _Ink | None = None, 

332 width: int = 1, 

333 ) -> None: 

334 """Draw a pieslice.""" 

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

336 if fill_ink is not None: 

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

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

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

340 

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

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

343 ink, fill = self._getink(fill) 

344 if ink is not None: 

345 self.draw.draw_points(xy, ink) 

346 

347 def polygon( 

348 self, 

349 xy: Coords, 

350 fill: _Ink | None = None, 

351 outline: _Ink | None = None, 

352 width: int = 1, 

353 ) -> None: 

354 """Draw a polygon.""" 

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

356 if fill_ink is not None: 

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

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

359 if width == 1: 

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

361 elif self.im is not None: 

362 # To avoid expanding the polygon outwards, 

363 # use the fill as a mask 

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

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

366 draw = Draw(mask) 

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

368 

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

370 

371 def regular_polygon( 

372 self, 

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

374 n_sides: int, 

375 rotation: float = 0, 

376 fill: _Ink | None = None, 

377 outline: _Ink | None = None, 

378 width: int = 1, 

379 ) -> None: 

380 """Draw a regular polygon.""" 

381 xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) 

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

383 

384 def rectangle( 

385 self, 

386 xy: Coords, 

387 fill: _Ink | None = None, 

388 outline: _Ink | None = None, 

389 width: int = 1, 

390 ) -> None: 

391 """Draw a rectangle.""" 

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

393 if fill_ink is not None: 

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

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

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

397 

398 def rounded_rectangle( 

399 self, 

400 xy: Coords, 

401 radius: float = 0, 

402 fill: _Ink | None = None, 

403 outline: _Ink | None = None, 

404 width: int = 1, 

405 *, 

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

407 ) -> None: 

408 """Draw a rounded rectangle.""" 

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

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

411 else: 

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

413 if x1 < x0: 

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

415 raise ValueError(msg) 

416 if y1 < y0: 

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

418 raise ValueError(msg) 

419 if corners is None: 

420 corners = (True, True, True, True) 

421 

422 d = radius * 2 

423 

424 x0 = round(x0) 

425 y0 = round(y0) 

426 x1 = round(x1) 

427 y1 = round(y1) 

428 full_x, full_y = False, False 

429 if all(corners): 

430 full_x = d >= x1 - x0 - 1 

431 if full_x: 

432 # The two left and two right corners are joined 

433 d = x1 - x0 

434 full_y = d >= y1 - y0 - 1 

435 if full_y: 

436 # The two top and two bottom corners are joined 

437 d = y1 - y0 

438 if full_x and full_y: 

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

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

441 

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

443 # If the corners have no curve, 

444 # or there are no corners, 

445 # that is a rectangle 

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

447 

448 r = int(d // 2) 

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

450 

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

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

453 if full_x: 

454 # Draw top and bottom halves 

455 parts = ( 

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

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

458 ) 

459 elif full_y: 

460 # Draw left and right halves 

461 parts = ( 

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

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

464 ) 

465 else: 

466 # Draw four separate corners 

467 parts = tuple( 

468 part 

469 for i, part in enumerate( 

470 ( 

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

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

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

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

475 ) 

476 ) 

477 if corners[i] 

478 ) 

479 for part in parts: 

480 if pieslice: 

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

482 else: 

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

484 

485 if fill_ink is not None: 

486 draw_corners(True) 

487 

488 if full_x: 

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

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

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

492 if not full_x and not full_y: 

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

494 if corners[0]: 

495 left[1] += r + 1 

496 if corners[3]: 

497 left[3] -= r + 1 

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

499 

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

501 if corners[1]: 

502 right[1] += r + 1 

503 if corners[2]: 

504 right[3] -= r + 1 

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

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

507 draw_corners(False) 

508 

509 if not full_x: 

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

511 if corners[0]: 

512 top[0] += r + 1 

513 if corners[1]: 

514 top[2] -= r + 1 

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

516 

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

518 if corners[3]: 

519 bottom[0] += r + 1 

520 if corners[2]: 

521 bottom[2] -= r + 1 

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

523 if not full_y: 

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

525 if corners[0]: 

526 left[1] += r + 1 

527 if corners[3]: 

528 left[3] -= r + 1 

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

530 

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

532 if corners[1]: 

533 right[1] += r + 1 

534 if corners[2]: 

535 right[3] -= r + 1 

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

537 

538 def text( 

539 self, 

540 xy: tuple[float, float], 

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

542 fill: _Ink | None = None, 

543 font: ( 

544 ImageFont.ImageFont 

545 | ImageFont.FreeTypeFont 

546 | ImageFont.TransposedFont 

547 | None 

548 ) = None, 

549 anchor: str | None = None, 

550 spacing: float = 4, 

551 align: str = "left", 

552 direction: str | None = None, 

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

554 language: str | None = None, 

555 stroke_width: float = 0, 

556 stroke_fill: _Ink | None = None, 

557 embedded_color: bool = False, 

558 *args: Any, 

559 **kwargs: Any, 

560 ) -> None: 

561 """Draw text.""" 

562 if isinstance(text, ImageText.Text): 

563 image_text = text 

564 else: 

565 if font is None: 

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

567 image_text = ImageText.Text( 

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

569 ) 

570 if embedded_color: 

571 image_text.embed_color() 

572 if stroke_width: 

573 image_text.stroke(stroke_width, stroke_fill) 

574 

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

576 ink, fill_ink = self._getink(fill) 

577 if ink is None: 

578 assert fill_ink is not None 

579 return fill_ink 

580 return ink 

581 

582 ink = getink(fill) 

583 if ink is None: 

584 return 

585 

586 stroke_ink = None 

587 if image_text.stroke_width: 

588 stroke_ink = ( 

589 getink(image_text.stroke_fill) 

590 if image_text.stroke_fill is not None 

591 else ink 

592 ) 

593 

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

595 

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

597 mode = self.fontmode 

598 if stroke_width == 0 and embedded_color: 

599 mode = "RGBA" 

600 x = int(line.x) 

601 y = int(line.y) 

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

603 try: 

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

605 line.text, 

606 mode, 

607 direction=direction, 

608 features=features, 

609 language=language, 

610 stroke_width=stroke_width, 

611 stroke_filled=True, 

612 anchor=line.anchor, 

613 ink=ink, 

614 start=start, 

615 *args, 

616 **kwargs, 

617 ) 

618 x += offset[0] 

619 y += offset[1] 

620 except AttributeError: 

621 try: 

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

623 line.text, 

624 mode, 

625 direction, 

626 features, 

627 language, 

628 stroke_width, 

629 line.anchor, 

630 ink, 

631 start=start, 

632 *args, 

633 **kwargs, 

634 ) 

635 except TypeError: 

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

637 if mode == "RGBA": 

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

639 # returns color in RGB bands and mask in A 

640 # extract mask and set text alpha 

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

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

643 color.fillband(3, ink_alpha) 

644 if self.im is not None: 

645 self.im.paste( 

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

647 ) 

648 else: 

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

650 

651 if stroke_ink is not None: 

652 # Draw stroked text 

653 draw_text(stroke_ink, image_text.stroke_width) 

654 

655 # Draw normal text 

656 if ink != stroke_ink: 

657 draw_text(ink) 

658 else: 

659 # Only draw normal text 

660 draw_text(ink) 

661 

662 def multiline_text( 

663 self, 

664 xy: tuple[float, float], 

665 text: AnyStr, 

666 fill: _Ink | None = None, 

667 font: ( 

668 ImageFont.ImageFont 

669 | ImageFont.FreeTypeFont 

670 | ImageFont.TransposedFont 

671 | None 

672 ) = None, 

673 anchor: str | None = None, 

674 spacing: float = 4, 

675 align: str = "left", 

676 direction: str | None = None, 

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

678 language: str | None = None, 

679 stroke_width: float = 0, 

680 stroke_fill: _Ink | None = None, 

681 embedded_color: bool = False, 

682 *, 

683 font_size: float | None = None, 

684 ) -> None: 

685 return self.text( 

686 xy, 

687 text, 

688 fill, 

689 font, 

690 anchor, 

691 spacing, 

692 align, 

693 direction, 

694 features, 

695 language, 

696 stroke_width, 

697 stroke_fill, 

698 embedded_color, 

699 font_size=font_size, 

700 ) 

701 

702 def textlength( 

703 self, 

704 text: AnyStr, 

705 font: ( 

706 ImageFont.ImageFont 

707 | ImageFont.FreeTypeFont 

708 | ImageFont.TransposedFont 

709 | None 

710 ) = None, 

711 direction: str | None = None, 

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

713 language: str | None = None, 

714 embedded_color: bool = False, 

715 *, 

716 font_size: float | None = None, 

717 ) -> float: 

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

719 if font is None: 

720 font = self._getfont(font_size) 

721 image_text = ImageText.Text( 

722 text, 

723 font, 

724 self.mode, 

725 direction=direction, 

726 features=features, 

727 language=language, 

728 ) 

729 if embedded_color: 

730 image_text.embed_color() 

731 return image_text.get_length() 

732 

733 def textbbox( 

734 self, 

735 xy: tuple[float, float], 

736 text: AnyStr, 

737 font: ( 

738 ImageFont.ImageFont 

739 | ImageFont.FreeTypeFont 

740 | ImageFont.TransposedFont 

741 | None 

742 ) = 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 """Get the bounding box of a given string, in pixels.""" 

755 if font is None: 

756 font = self._getfont(font_size) 

757 image_text = ImageText.Text( 

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

759 ) 

760 if embedded_color: 

761 image_text.embed_color() 

762 if stroke_width: 

763 image_text.stroke(stroke_width) 

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

765 

766 def multiline_textbbox( 

767 self, 

768 xy: tuple[float, float], 

769 text: AnyStr, 

770 font: ( 

771 ImageFont.ImageFont 

772 | ImageFont.FreeTypeFont 

773 | ImageFont.TransposedFont 

774 | None 

775 ) = None, 

776 anchor: str | None = None, 

777 spacing: float = 4, 

778 align: str = "left", 

779 direction: str | None = None, 

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

781 language: str | None = None, 

782 stroke_width: float = 0, 

783 embedded_color: bool = False, 

784 *, 

785 font_size: float | None = None, 

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

787 return self.textbbox( 

788 xy, 

789 text, 

790 font, 

791 anchor, 

792 spacing, 

793 align, 

794 direction, 

795 features, 

796 language, 

797 stroke_width, 

798 embedded_color, 

799 font_size=font_size, 

800 ) 

801 

802 

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

804 """ 

805 A simple 2D drawing interface for PIL images. 

806 

807 :param im: The image to draw in. 

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

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

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

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

812 defaults to the mode of the image. 

813 """ 

814 try: 

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

816 except AttributeError: 

817 return ImageDraw(im, mode) 

818 

819 

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

821 """ 

822 :param im: The image to draw in. 

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

824 """ 

825 from . import ImageDraw2 

826 

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

828 return draw, ImageDraw2 

829 

830 

831def floodfill( 

832 image: Image.Image, 

833 xy: tuple[int, int], 

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

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

836 thresh: float = 0, 

837) -> None: 

838 """ 

839 .. warning:: This method is experimental. 

840 

841 Fills a bounded region with a given color. 

842 

843 :param image: Target image. 

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

845 :ref:`coordinate-system`. 

846 :param value: Fill color. 

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

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

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

850 pixel. 

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

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

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

854 non-homogeneous, but similar, colors. 

855 """ 

856 # based on an implementation by Eric S. Raymond 

857 # amended by yo1995 @20180806 

858 pixel = image.load() 

859 assert pixel is not None 

860 x, y = xy 

861 try: 

862 background = pixel[x, y] 

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

864 return # seed point already has fill color 

865 pixel[x, y] = value 

866 except (ValueError, IndexError): 

867 return # seed point outside image 

868 edge = {(x, y)} 

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

870 # to reduce memory consumption 

871 full_edge = set() 

872 while edge: 

873 new_edge = set() 

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

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

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

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

878 continue 

879 try: 

880 p = pixel[s, t] 

881 except (ValueError, IndexError): 

882 pass 

883 else: 

884 full_edge.add((s, t)) 

885 if border is None: 

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

887 else: 

888 fill = p not in (value, border) 

889 if fill: 

890 pixel[s, t] = value 

891 new_edge.add((s, t)) 

892 full_edge = edge # discard pixels processed 

893 edge = new_edge 

894 

895 

896def _compute_regular_polygon_vertices( 

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

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

899 """ 

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

901 

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

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

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

905 :param n_sides: Number of sides 

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

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

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

909 :return: List of regular polygon vertices 

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

911 

912 How are the vertices computed? 

913 1. Compute the following variables 

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

915 - side_length: Length of each polygon edge 

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

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

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

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

920 

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

922 The vertex is computed using the equation below. 

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

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

925 

926 Note: 

927 φ = angle in degrees 

928 x = 0 

929 y = polygon_radius 

930 

931 The formula above assumes rotation around the origin. 

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

933 To account for this, we use the formula below 

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

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

936 """ 

937 # 1. Error Handling 

938 # 1.1 Check `n_sides` has an appropriate value 

939 if not isinstance(n_sides, int): 

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

941 raise TypeError(msg) 

942 if n_sides < 3: 

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

944 raise ValueError(msg) 

945 

946 # 1.2 Check `bounding_circle` has an appropriate value 

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

948 msg = "bounding_circle should be a sequence" 

949 raise TypeError(msg) 

950 

951 if len(bounding_circle) == 3: 

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

953 msg = "bounding_circle should only contain numeric data" 

954 raise ValueError(msg) 

955 

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

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

958 if not all( 

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

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

961 msg = "bounding_circle should only contain numeric data" 

962 raise ValueError(msg) 

963 

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

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

966 raise ValueError(msg) 

967 

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

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

970 else: 

971 msg = ( 

972 "bounding_circle should contain 2D coordinates " 

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

974 ) 

975 raise ValueError(msg) 

976 

977 if polygon_radius <= 0: 

978 msg = "bounding_circle radius should be > 0" 

979 raise ValueError(msg) 

980 

981 # 1.3 Check `rotation` has an appropriate value 

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

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

984 raise ValueError(msg) 

985 

986 # 2. Define Helper Functions 

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

988 return ( 

989 round( 

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

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

992 + centroid[0], 

993 2, 

994 ), 

995 round( 

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

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

998 + centroid[1], 

999 2, 

1000 ), 

1001 ) 

1002 

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

1004 start_point = [polygon_radius, 0] 

1005 return _apply_rotation(start_point, angle) 

1006 

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

1008 angles = [] 

1009 degrees = 360 / n_sides 

1010 # Start with the bottom left polygon vertex 

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

1012 for _ in range(n_sides): 

1013 angles.append(current_angle) 

1014 current_angle += degrees 

1015 if current_angle > 360: 

1016 current_angle -= 360 

1017 return angles 

1018 

1019 # 3. Variable Declarations 

1020 angles = _get_angles(n_sides, rotation) 

1021 

1022 # 4. Compute Vertices 

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

1024 

1025 

1026def _color_diff( 

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

1028) -> float: 

1029 """ 

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

1031 """ 

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

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

1034 

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