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

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

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 = 0, 

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: 

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, 

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 xy, anchor, 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 coord = [] 

601 for i in range(2): 

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

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

604 try: 

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

606 line, 

607 mode, 

608 direction=direction, 

609 features=features, 

610 language=language, 

611 stroke_width=stroke_width, 

612 stroke_filled=True, 

613 anchor=anchor, 

614 ink=ink, 

615 start=start, 

616 *args, 

617 **kwargs, 

618 ) 

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

620 except AttributeError: 

621 try: 

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

623 line, 

624 mode, 

625 direction, 

626 features, 

627 language, 

628 stroke_width, 

629 anchor, 

630 ink, 

631 start=start, 

632 *args, 

633 **kwargs, 

634 ) 

635 except TypeError: 

636 mask = image_text.font.getmask(line) 

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 x, y = coord 

645 if self.im is not None: 

646 self.im.paste( 

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

648 ) 

649 else: 

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

651 

652 if stroke_ink is not None: 

653 # Draw stroked text 

654 draw_text(stroke_ink, image_text.stroke_width) 

655 

656 # Draw normal text 

657 if ink != stroke_ink: 

658 draw_text(ink) 

659 else: 

660 # Only draw normal text 

661 draw_text(ink) 

662 

663 def multiline_text( 

664 self, 

665 xy: tuple[float, float], 

666 text: AnyStr, 

667 fill: _Ink | None = None, 

668 font: ( 

669 ImageFont.ImageFont 

670 | ImageFont.FreeTypeFont 

671 | ImageFont.TransposedFont 

672 | None 

673 ) = None, 

674 anchor: str | None = None, 

675 spacing: float = 4, 

676 align: str = "left", 

677 direction: str | None = None, 

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

679 language: str | None = None, 

680 stroke_width: float = 0, 

681 stroke_fill: _Ink | None = None, 

682 embedded_color: bool = False, 

683 *, 

684 font_size: float | None = None, 

685 ) -> None: 

686 return self.text( 

687 xy, 

688 text, 

689 fill, 

690 font, 

691 anchor, 

692 spacing, 

693 align, 

694 direction, 

695 features, 

696 language, 

697 stroke_width, 

698 stroke_fill, 

699 embedded_color, 

700 font_size=font_size, 

701 ) 

702 

703 def textlength( 

704 self, 

705 text: AnyStr, 

706 font: ( 

707 ImageFont.ImageFont 

708 | ImageFont.FreeTypeFont 

709 | ImageFont.TransposedFont 

710 | None 

711 ) = None, 

712 direction: str | None = None, 

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

714 language: str | None = None, 

715 embedded_color: bool = False, 

716 *, 

717 font_size: float | None = None, 

718 ) -> float: 

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

720 if font is None: 

721 font = self._getfont(font_size) 

722 image_text = ImageText.Text( 

723 text, 

724 font, 

725 self.mode, 

726 direction=direction, 

727 features=features, 

728 language=language, 

729 ) 

730 if embedded_color: 

731 image_text.embed_color() 

732 return image_text.get_length() 

733 

734 def textbbox( 

735 self, 

736 xy: tuple[float, float], 

737 text: AnyStr, 

738 font: ( 

739 ImageFont.ImageFont 

740 | ImageFont.FreeTypeFont 

741 | ImageFont.TransposedFont 

742 | None 

743 ) = None, 

744 anchor: str | None = None, 

745 spacing: float = 4, 

746 align: str = "left", 

747 direction: str | None = None, 

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

749 language: str | None = None, 

750 stroke_width: float = 0, 

751 embedded_color: bool = False, 

752 *, 

753 font_size: float | None = None, 

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

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

756 if font is None: 

757 font = self._getfont(font_size) 

758 image_text = ImageText.Text( 

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

760 ) 

761 if embedded_color: 

762 image_text.embed_color() 

763 if stroke_width: 

764 image_text.stroke(stroke_width) 

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

766 

767 def multiline_textbbox( 

768 self, 

769 xy: tuple[float, float], 

770 text: AnyStr, 

771 font: ( 

772 ImageFont.ImageFont 

773 | ImageFont.FreeTypeFont 

774 | ImageFont.TransposedFont 

775 | None 

776 ) = None, 

777 anchor: str | None = None, 

778 spacing: float = 4, 

779 align: str = "left", 

780 direction: str | None = None, 

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

782 language: str | None = None, 

783 stroke_width: float = 0, 

784 embedded_color: bool = False, 

785 *, 

786 font_size: float | None = None, 

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

788 return self.textbbox( 

789 xy, 

790 text, 

791 font, 

792 anchor, 

793 spacing, 

794 align, 

795 direction, 

796 features, 

797 language, 

798 stroke_width, 

799 embedded_color, 

800 font_size=font_size, 

801 ) 

802 

803 

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

805 """ 

806 A simple 2D drawing interface for PIL images. 

807 

808 :param im: The image to draw in. 

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

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

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

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

813 defaults to the mode of the image. 

814 """ 

815 try: 

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

817 except AttributeError: 

818 return ImageDraw(im, mode) 

819 

820 

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

822 """ 

823 :param im: The image to draw in. 

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

825 """ 

826 from . import ImageDraw2 

827 

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

829 return draw, ImageDraw2 

830 

831 

832def floodfill( 

833 image: Image.Image, 

834 xy: tuple[int, int], 

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

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

837 thresh: float = 0, 

838) -> None: 

839 """ 

840 .. warning:: This method is experimental. 

841 

842 Fills a bounded region with a given color. 

843 

844 :param image: Target image. 

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

846 :ref:`coordinate-system`. 

847 :param value: Fill color. 

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

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

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

851 pixel. 

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

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

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

855 non-homogeneous, but similar, colors. 

856 """ 

857 # based on an implementation by Eric S. Raymond 

858 # amended by yo1995 @20180806 

859 pixel = image.load() 

860 assert pixel is not None 

861 x, y = xy 

862 try: 

863 background = pixel[x, y] 

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

865 return # seed point already has fill color 

866 pixel[x, y] = value 

867 except (ValueError, IndexError): 

868 return # seed point outside image 

869 edge = {(x, y)} 

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

871 # to reduce memory consumption 

872 full_edge = set() 

873 while edge: 

874 new_edge = set() 

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

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

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

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

879 continue 

880 try: 

881 p = pixel[s, t] 

882 except (ValueError, IndexError): 

883 pass 

884 else: 

885 full_edge.add((s, t)) 

886 if border is None: 

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

888 else: 

889 fill = p not in (value, border) 

890 if fill: 

891 pixel[s, t] = value 

892 new_edge.add((s, t)) 

893 full_edge = edge # discard pixels processed 

894 edge = new_edge 

895 

896 

897def _compute_regular_polygon_vertices( 

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

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

900 """ 

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

902 

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

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

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

906 :param n_sides: Number of sides 

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

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

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

910 :return: List of regular polygon vertices 

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

912 

913 How are the vertices computed? 

914 1. Compute the following variables 

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

916 - side_length: Length of each polygon edge 

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

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

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

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

921 

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

923 The vertex is computed using the equation below. 

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

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

926 

927 Note: 

928 φ = angle in degrees 

929 x = 0 

930 y = polygon_radius 

931 

932 The formula above assumes rotation around the origin. 

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

934 To account for this, we use the formula below 

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

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

937 """ 

938 # 1. Error Handling 

939 # 1.1 Check `n_sides` has an appropriate value 

940 if not isinstance(n_sides, int): 

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

942 raise TypeError(msg) 

943 if n_sides < 3: 

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

945 raise ValueError(msg) 

946 

947 # 1.2 Check `bounding_circle` has an appropriate value 

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

949 msg = "bounding_circle should be a sequence" 

950 raise TypeError(msg) 

951 

952 if len(bounding_circle) == 3: 

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

954 msg = "bounding_circle should only contain numeric data" 

955 raise ValueError(msg) 

956 

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

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

959 if not all( 

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

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

962 msg = "bounding_circle should only contain numeric data" 

963 raise ValueError(msg) 

964 

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

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

967 raise ValueError(msg) 

968 

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

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

971 else: 

972 msg = ( 

973 "bounding_circle should contain 2D coordinates " 

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

975 ) 

976 raise ValueError(msg) 

977 

978 if polygon_radius <= 0: 

979 msg = "bounding_circle radius should be > 0" 

980 raise ValueError(msg) 

981 

982 # 1.3 Check `rotation` has an appropriate value 

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

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

985 raise ValueError(msg) 

986 

987 # 2. Define Helper Functions 

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

989 return ( 

990 round( 

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

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

993 + centroid[0], 

994 2, 

995 ), 

996 round( 

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

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

999 + centroid[1], 

1000 2, 

1001 ), 

1002 ) 

1003 

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

1005 start_point = [polygon_radius, 0] 

1006 return _apply_rotation(start_point, angle) 

1007 

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

1009 angles = [] 

1010 degrees = 360 / n_sides 

1011 # Start with the bottom left polygon vertex 

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

1013 for _ in range(n_sides): 

1014 angles.append(current_angle) 

1015 current_angle += degrees 

1016 if current_angle > 360: 

1017 current_angle -= 360 

1018 return angles 

1019 

1020 # 3. Variable Declarations 

1021 angles = _get_angles(n_sides, rotation) 

1022 

1023 # 4. Compute Vertices 

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

1025 

1026 

1027def _color_diff( 

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

1029) -> float: 

1030 """ 

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

1032 """ 

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

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

1035 

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