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

425 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 

34__lazy_modules__ = {"math", "struct"} 

35 

36import math 

37import struct 

38from typing import cast 

39 

40from . import Image, ImageColor, ImageFont, ImageText 

41 

42TYPE_CHECKING = False 

43if TYPE_CHECKING: 

44 from collections.abc import Callable, Sequence 

45 from types import ModuleType 

46 from typing import Any, AnyStr 

47 

48 from . import ImageDraw2 

49 from ._typing import Coords, _Ink 

50 

51# experimental access to the outline API 

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

53 

54""" 

55A simple 2D drawing interface for PIL images. 

56<p> 

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

58directly. 

59""" 

60 

61 

62class ImageDraw: 

63 font: ImageFont.BaseImageFont | None = None 

64 

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

66 """ 

67 Create a drawing instance. 

68 

69 :param im: The image to draw in. 

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

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

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

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

74 defaults to the mode of the image. 

75 """ 

76 im._ensure_mutable() 

77 blend = 0 

78 if mode is None: 

79 mode = im.mode 

80 if mode != im.mode: 

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

82 blend = 1 

83 else: 

84 msg = "mode mismatch" 

85 raise ValueError(msg) 

86 if mode in {"P", "PA"}: 

87 self.palette = im.palette 

88 else: 

89 self.palette = None 

90 self._image = im 

91 self.im = im.im 

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

93 self.mode = mode 

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

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

96 else: 

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

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

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

100 self.fontmode = "1" 

101 else: 

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

103 self.fill = False 

104 

105 def getfont( 

106 self, 

107 ) -> ImageFont.BaseImageFont: 

108 """ 

109 Get the current default font. 

110 

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

112 

113 from PIL import ImageDraw, ImageFont 

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

115 

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

117 

118 from PIL import ImageDraw, ImageFont 

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

120 

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

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

123 

124 :returns: An image font.""" 

125 if not self.font: 

126 # FIXME: should add a font repository 

127 self.font = ImageFont.load_default() 

128 return self.font 

129 

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

131 if font_size is not None: 

132 return ImageFont.load_default(font_size) 

133 else: 

134 return self.getfont() 

135 

136 def _getink( 

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

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

139 result_ink = None 

140 result_fill = None 

141 if ink is None and fill is None: 

142 if self.fill: 

143 result_fill = self.ink 

144 else: 

145 result_ink = self.ink 

146 else: 

147 if ink is not None: 

148 if isinstance(ink, str): 

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

150 if self.mode in {"P", "PA"} and isinstance(ink, tuple): 

151 ink = self._image._get_palette_color(ink) 

152 if isinstance(ink, tuple) and len(ink) == 4 and ink[3] == 0: 

153 # Do not use transparent ink 

154 result_ink = None 

155 else: 

156 result_ink = self.draw.draw_ink(ink) 

157 if fill is not None: 

158 if isinstance(fill, str): 

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

160 if self.mode in {"P", "PA"} and isinstance(fill, tuple): 

161 fill = self._image._get_palette_color(fill) 

162 result_fill = self.draw.draw_ink(fill) 

163 return result_ink, result_fill 

164 

165 def arc( 

166 self, 

167 xy: Coords, 

168 start: float, 

169 end: float, 

170 fill: _Ink | None = None, 

171 width: int = 1, 

172 ) -> None: 

173 """Draw an arc.""" 

174 ink, fill = self._getink(fill) 

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

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

177 

178 def bitmap( 

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

180 ) -> None: 

181 """Draw a bitmap.""" 

182 bitmap.load() 

183 ink, fill = self._getink(fill) 

184 if ink is None: 

185 ink = fill 

186 if ink is not None: 

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

188 

189 def chord( 

190 self, 

191 xy: Coords, 

192 start: float, 

193 end: float, 

194 fill: _Ink | None = None, 

195 outline: _Ink | None = None, 

196 width: int = 1, 

197 ) -> None: 

198 """Draw a chord.""" 

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

200 if fill_ink is not None: 

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

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

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

204 

205 def ellipse( 

206 self, 

207 xy: Coords, 

208 fill: _Ink | None = None, 

209 outline: _Ink | None = None, 

210 width: int = 1, 

211 ) -> None: 

212 """Draw an ellipse.""" 

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

214 if fill_ink is not None: 

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

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

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

218 

219 def circle( 

220 self, 

221 xy: Sequence[float], 

222 radius: float, 

223 fill: _Ink | None = None, 

224 outline: _Ink | None = None, 

225 width: int = 1, 

226 ) -> None: 

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

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

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

230 

231 def _normalize_coords(self, xy: Coords) -> Sequence[Sequence[float]]: 

232 """Normalize 1 or 2 dimensional coord sequence into 2d sequence.""" 

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

234 return cast("Sequence[Sequence[float]]", xy) 

235 else: 

236 return [ 

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

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

239 ] 

240 

241 def line( 

242 self, 

243 xy: Coords, 

244 fill: _Ink | None = None, 

245 width: int = 1, 

246 joint: str | None = None, 

247 ) -> None: 

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

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

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

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

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

253 points = self._normalize_coords(xy) 

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 (x0, y0), (x1, y1) = self._normalize_coords(xy) 

410 if x1 < x0: 

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

412 raise ValueError(msg) 

413 if y1 < y0: 

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

415 raise ValueError(msg) 

416 if corners is None: 

417 corners = (True, True, True, True) 

418 

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

420 

421 x0 = round(x0) 

422 y0 = round(y0) 

423 x1 = round(x1) 

424 y1 = round(y1) 

425 full_x, full_y = False, False 

426 if all(corners): 

427 full_x = d >= x1 - x0 - 1 

428 if full_x: 

429 # The two left and two right corners are joined 

430 d = x1 - x0 

431 full_y = d >= y1 - y0 - 1 

432 if full_y: 

433 # The two top and two bottom corners are joined 

434 d = y1 - y0 

435 if full_x and full_y: 

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

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

438 

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

440 # If the corners have no curve, 

441 # or there are no corners, 

442 # that is a rectangle 

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

444 

445 r = int(d // 2) 

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

447 

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

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

450 if full_x: 

451 # Draw top and bottom halves 

452 parts = ( 

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

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

455 ) 

456 elif full_y: 

457 # Draw left and right halves 

458 parts = ( 

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

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

461 ) 

462 else: 

463 # Draw four separate corners 

464 parts = tuple( 

465 part 

466 for i, part in enumerate( 

467 ( 

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

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

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

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

472 ) 

473 ) 

474 if corners[i] 

475 ) 

476 for part in parts: 

477 if pieslice: 

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

479 else: 

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

481 

482 if fill_ink is not None: 

483 draw_corners(True) 

484 

485 if full_x: 

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

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

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

489 if not full_x and not full_y: 

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

491 if corners[0]: 

492 left[1] += r + 1 

493 if corners[3]: 

494 left[3] -= r + 1 

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

496 

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

498 if corners[1]: 

499 right[1] += r + 1 

500 if corners[2]: 

501 right[3] -= r + 1 

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

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

504 draw_corners(False) 

505 

506 if not full_x: 

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

508 if corners[0]: 

509 top[0] += r + 1 

510 if corners[1]: 

511 top[2] -= r + 1 

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

513 

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

515 if corners[3]: 

516 bottom[0] += r + 1 

517 if corners[2]: 

518 bottom[2] -= r + 1 

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

520 if not full_y: 

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

522 if corners[0]: 

523 left[1] += r + 1 

524 if corners[3]: 

525 left[3] -= r + 1 

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

527 

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

529 if corners[1]: 

530 right[1] += r + 1 

531 if corners[2]: 

532 right[3] -= r + 1 

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

534 

535 def text( 

536 self, 

537 xy: tuple[float, float], 

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

539 fill: _Ink | None = None, 

540 font: ImageFont.BaseImageFont | None = None, 

541 anchor: str | None = None, 

542 spacing: float = 4, 

543 align: str = "left", 

544 direction: str | None = None, 

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

546 language: str | None = None, 

547 stroke_width: float = 0, 

548 stroke_fill: _Ink | None = None, 

549 embedded_color: bool = False, 

550 *args: Any, 

551 **kwargs: Any, 

552 ) -> None: 

553 """Draw text.""" 

554 if isinstance(text, ImageText.Text): 

555 image_text = text 

556 else: 

557 if font is None: 

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

559 image_text = ImageText.Text( 

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

561 ) 

562 if embedded_color: 

563 image_text.embed_color() 

564 if stroke_width: 

565 image_text.stroke(stroke_width, stroke_fill) 

566 

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

568 ink, fill_ink = self._getink(fill) 

569 if ink is None: 

570 assert fill_ink is not None 

571 return fill_ink 

572 return ink 

573 

574 ink = getink(fill) 

575 if ink is None: 

576 return 

577 

578 stroke_ink = None 

579 if image_text.stroke_width: 

580 stroke_ink = ( 

581 getink(image_text.stroke_fill) 

582 if image_text.stroke_fill is not None 

583 else ink 

584 ) 

585 

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

587 

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

589 mode = self.fontmode 

590 if stroke_width == 0 and image_text.embedded_color: 

591 mode = "RGBA" 

592 x = int(line.x) 

593 y = int(line.y) 

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

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

596 mask, offset = image_text.font.getmask2( 

597 line.text, 

598 mode, 

599 direction, 

600 features, 

601 language, 

602 stroke_width, 

603 line.anchor, 

604 ink, 

605 start, 

606 stroke_filled=True, 

607 *args, 

608 **kwargs, 

609 ) 

610 x += offset[0] 

611 y += offset[1] 

612 else: 

613 try: 

614 mask = image_text.font.getmask( 

615 line.text, 

616 mode, 

617 direction, 

618 features, 

619 language, 

620 stroke_width, 

621 line.anchor, 

622 ink, 

623 start=start, 

624 *args, 

625 **kwargs, 

626 ) 

627 except TypeError: 

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

629 if mode == "RGBA": 

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

631 # returns color in RGB bands and mask in A 

632 # extract mask and set text alpha 

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

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

635 color.fillband(3, ink_alpha) 

636 if self.im is not None: 

637 self.im.paste( 

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

639 ) 

640 else: 

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

642 

643 if stroke_ink is not None: 

644 # Draw stroked text 

645 draw_text(stroke_ink, image_text.stroke_width) 

646 

647 # Draw normal text 

648 if ink != stroke_ink: 

649 draw_text(ink) 

650 else: 

651 # Only draw normal text 

652 draw_text(ink) 

653 

654 def multiline_text( 

655 self, 

656 xy: tuple[float, float], 

657 text: AnyStr, 

658 fill: _Ink | None = None, 

659 font: ImageFont.BaseImageFont | None = None, 

660 anchor: str | None = None, 

661 spacing: float = 4, 

662 align: str = "left", 

663 direction: str | None = None, 

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

665 language: str | None = None, 

666 stroke_width: float = 0, 

667 stroke_fill: _Ink | None = None, 

668 embedded_color: bool = False, 

669 *, 

670 font_size: float | None = None, 

671 ) -> None: 

672 return self.text( 

673 xy, 

674 text, 

675 fill, 

676 font, 

677 anchor, 

678 spacing, 

679 align, 

680 direction, 

681 features, 

682 language, 

683 stroke_width, 

684 stroke_fill, 

685 embedded_color, 

686 font_size=font_size, 

687 ) 

688 

689 def textlength( 

690 self, 

691 text: AnyStr, 

692 font: ImageFont.BaseImageFont | None = None, 

693 direction: str | None = None, 

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

695 language: str | None = None, 

696 embedded_color: bool = False, 

697 *, 

698 font_size: float | None = None, 

699 ) -> float: 

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

701 if font is None: 

702 font = self._getfont(font_size) 

703 image_text = ImageText.Text( 

704 text, 

705 font, 

706 self.mode, 

707 direction=direction, 

708 features=features, 

709 language=language, 

710 ) 

711 if embedded_color: 

712 image_text.embed_color() 

713 return image_text.get_length() 

714 

715 def textbbox( 

716 self, 

717 xy: tuple[float, float], 

718 text: AnyStr, 

719 font: ImageFont.BaseImageFont | None = None, 

720 anchor: str | None = None, 

721 spacing: float = 4, 

722 align: str = "left", 

723 direction: str | None = None, 

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

725 language: str | None = None, 

726 stroke_width: float = 0, 

727 embedded_color: bool = False, 

728 *, 

729 font_size: float | None = None, 

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

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

732 if font is None: 

733 font = self._getfont(font_size) 

734 image_text = ImageText.Text( 

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

736 ) 

737 if embedded_color: 

738 image_text.embed_color() 

739 if stroke_width: 

740 image_text.stroke(stroke_width) 

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

742 

743 def multiline_textbbox( 

744 self, 

745 xy: tuple[float, float], 

746 text: AnyStr, 

747 font: ImageFont.BaseImageFont | None = None, 

748 anchor: str | None = None, 

749 spacing: float = 4, 

750 align: str = "left", 

751 direction: str | None = None, 

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

753 language: str | None = None, 

754 stroke_width: float = 0, 

755 embedded_color: bool = False, 

756 *, 

757 font_size: float | None = None, 

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

759 return self.textbbox( 

760 xy, 

761 text, 

762 font, 

763 anchor, 

764 spacing, 

765 align, 

766 direction, 

767 features, 

768 language, 

769 stroke_width, 

770 embedded_color, 

771 font_size=font_size, 

772 ) 

773 

774 

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

776 """ 

777 A simple 2D drawing interface for PIL images. 

778 

779 :param im: The image to draw in. 

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

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

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

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

784 defaults to the mode of the image. 

785 """ 

786 try: 

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

788 except AttributeError: 

789 return ImageDraw(im, mode) 

790 

791 

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

793 """ 

794 :param im: The image to draw in. 

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

796 """ 

797 from . import ImageDraw2 

798 

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

800 return draw, ImageDraw2 

801 

802 

803def floodfill( 

804 image: Image.Image, 

805 xy: tuple[int, int], 

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

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

808 thresh: float = 0, 

809) -> None: 

810 """ 

811 .. warning:: This method is experimental. 

812 

813 Fills a bounded region with a given color. 

814 

815 :param image: Target image. 

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

817 :ref:`coordinate-system`. 

818 :param value: Fill color. 

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

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

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

822 pixel. 

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

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

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

826 non-homogeneous, but similar, colors. 

827 """ 

828 # based on an implementation by Eric S. Raymond 

829 # amended by yo1995 @20180806 

830 pixel = image.load() 

831 assert pixel is not None 

832 x, y = xy 

833 try: 

834 background = pixel[x, y] 

835 except IndexError: 

836 return # seed point outside image 

837 

838 def _update_color(color: float | tuple[int, ...]) -> float | tuple[int, ...]: 

839 if image.mode == "RGBA" and isinstance(color, tuple) and len(color) == 3: 

840 color += (255,) 

841 return color 

842 

843 value = _update_color(value) 

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

845 return # seed point already has fill color 

846 

847 if border is not None: 

848 border = _update_color(border) 

849 pixel[x, y] = value 

850 edge = {(x, y)} 

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

852 # to reduce memory consumption 

853 full_edge = set() 

854 while edge: 

855 new_edge = set() 

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

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

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

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

860 continue 

861 try: 

862 p = pixel[s, t] 

863 except (ValueError, IndexError): 

864 pass 

865 else: 

866 full_edge.add((s, t)) 

867 if border is None: 

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

869 else: 

870 fill = p not in (value, border) 

871 if fill: 

872 pixel[s, t] = value 

873 new_edge.add((s, t)) 

874 full_edge = edge # discard pixels processed 

875 edge = new_edge 

876 

877 

878def _compute_regular_polygon_vertices( 

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

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

881 """ 

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

883 

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

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

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

887 :param n_sides: Number of sides 

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

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

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

891 :return: List of regular polygon vertices 

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

893 

894 How are the vertices computed? 

895 1. Compute the following variables 

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

897 - side_length: Length of each polygon edge 

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

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

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

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

902 

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

904 The vertex is computed using the equation below. 

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

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

907 

908 Note: 

909 φ = angle in degrees 

910 x = 0 

911 y = polygon_radius 

912 

913 The formula above assumes rotation around the origin. 

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

915 To account for this, we use the formula below 

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

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

918 """ 

919 # 1. Error Handling 

920 # 1.1 Check `n_sides` has an appropriate value 

921 if not isinstance(n_sides, int): 

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

923 raise TypeError(msg) 

924 if n_sides < 3: 

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

926 raise ValueError(msg) 

927 

928 # 1.2 Check `bounding_circle` has an appropriate value 

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

930 msg = "bounding_circle should be a sequence" 

931 raise TypeError(msg) 

932 

933 if len(bounding_circle) == 3: 

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

935 msg = "bounding_circle should only contain numeric data" 

936 raise ValueError(msg) 

937 

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

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

940 if not all( 

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

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

943 msg = "bounding_circle should only contain numeric data" 

944 raise ValueError(msg) 

945 

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

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

948 raise ValueError(msg) 

949 

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

951 polygon_radius = cast("float", bounding_circle[1]) 

952 else: 

953 msg = ( 

954 "bounding_circle should contain 2D coordinates " 

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

956 ) 

957 raise ValueError(msg) 

958 

959 if polygon_radius <= 0: 

960 msg = "bounding_circle radius should be > 0" 

961 raise ValueError(msg) 

962 

963 # 1.3 Check `rotation` has an appropriate value 

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

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

966 raise ValueError(msg) 

967 

968 # 2. Define Helper Functions 

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

970 return ( 

971 round( 

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

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

974 + centroid[0], 

975 2, 

976 ), 

977 round( 

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

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

980 + centroid[1], 

981 2, 

982 ), 

983 ) 

984 

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

986 start_point = [polygon_radius, 0] 

987 return _apply_rotation(start_point, angle) 

988 

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

990 angles = [] 

991 degrees = 360 / n_sides 

992 # Start with the bottom left polygon vertex 

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

994 for _ in range(n_sides): 

995 angles.append(current_angle) 

996 current_angle += degrees 

997 if current_angle > 360: 

998 current_angle -= 360 

999 return angles 

1000 

1001 # 3. Variable Declarations 

1002 angles = _get_angles(n_sides, rotation) 

1003 

1004 # 4. Compute Vertices 

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

1006 

1007 

1008def _color_diff( 

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

1010) -> float: 

1011 """ 

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

1013 """ 

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

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

1016 

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