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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

416 statements  

1# 

2# The Python Imaging Library 

3# $Id$ 

4# 

5# drawing interface operations 

6# 

7# History: 

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

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

10# 1996-08-13 fl Added text support 

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

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

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

14# 1999-02-06 fl Added bitmap support 

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

16# 1999-02-20 fl Fixed backwards compatibility 

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

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

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

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

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

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

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

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

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

26# 

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

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

29# 

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

31# 

32from __future__ import annotations 

33 

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 == "P": 

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.palette and isinstance(ink, tuple): 

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

152 result_ink = self.draw.draw_ink(ink) 

153 if fill is not None: 

154 if isinstance(fill, str): 

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

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

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

158 result_fill = self.draw.draw_ink(fill) 

159 return result_ink, result_fill 

160 

161 def arc( 

162 self, 

163 xy: Coords, 

164 start: float, 

165 end: float, 

166 fill: _Ink | None = None, 

167 width: int = 1, 

168 ) -> None: 

169 """Draw an arc.""" 

170 ink, fill = self._getink(fill) 

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

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

173 

174 def bitmap( 

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

176 ) -> None: 

177 """Draw a bitmap.""" 

178 bitmap.load() 

179 ink, fill = self._getink(fill) 

180 if ink is None: 

181 ink = fill 

182 if ink is not None: 

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

184 

185 def chord( 

186 self, 

187 xy: Coords, 

188 start: float, 

189 end: float, 

190 fill: _Ink | None = None, 

191 outline: _Ink | None = None, 

192 width: int = 1, 

193 ) -> None: 

194 """Draw a chord.""" 

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

196 if fill_ink is not None: 

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

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

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

200 

201 def ellipse( 

202 self, 

203 xy: Coords, 

204 fill: _Ink | None = None, 

205 outline: _Ink | None = None, 

206 width: int = 1, 

207 ) -> None: 

208 """Draw an ellipse.""" 

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

210 if fill_ink is not None: 

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

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

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

214 

215 def circle( 

216 self, 

217 xy: Sequence[float], 

218 radius: float, 

219 fill: _Ink | None = None, 

220 outline: _Ink | None = None, 

221 width: int = 1, 

222 ) -> None: 

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

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

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

226 

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

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

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

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

231 else: 

232 return [ 

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

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

235 ] 

236 

237 def line( 

238 self, 

239 xy: Coords, 

240 fill: _Ink | None = None, 

241 width: int = 1, 

242 joint: str | None = None, 

243 ) -> None: 

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

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

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

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

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

249 points = self._normalize_coords(xy) 

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

251 point = points[i] 

252 angles = [ 

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

254 % 360 

255 for start, end in ( 

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

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

258 ) 

259 ] 

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

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

262 continue 

263 

264 def coord_at_angle( 

265 coord: Sequence[float], angle: float 

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

267 x, y = coord 

268 angle -= 90 

269 distance = width / 2 - 1 

270 return tuple( 

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

272 for p, p_d in ( 

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

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

275 ) 

276 ) 

277 

278 flipped = ( 

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

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

281 coords = [ 

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

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

284 ] 

285 if flipped: 

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

287 else: 

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

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

290 

291 if width > 8: 

292 # Cover potential gaps between the line and the joint 

293 if flipped: 

294 gap_coords = [ 

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

296 point, 

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

298 ] 

299 else: 

300 gap_coords = [ 

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

302 point, 

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

304 ] 

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

306 

307 def shape( 

308 self, 

309 shape: Image.core._Outline, 

310 fill: _Ink | None = None, 

311 outline: _Ink | None = None, 

312 ) -> None: 

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

314 shape.close() 

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

316 if fill_ink is not None: 

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

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

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

320 

321 def pieslice( 

322 self, 

323 xy: Coords, 

324 start: float, 

325 end: float, 

326 fill: _Ink | None = None, 

327 outline: _Ink | None = None, 

328 width: int = 1, 

329 ) -> None: 

330 """Draw a pieslice.""" 

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

332 if fill_ink is not None: 

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

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

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

336 

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

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

339 ink, fill = self._getink(fill) 

340 if ink is not None: 

341 self.draw.draw_points(xy, ink) 

342 

343 def polygon( 

344 self, 

345 xy: Coords, 

346 fill: _Ink | None = None, 

347 outline: _Ink | None = None, 

348 width: int = 1, 

349 ) -> None: 

350 """Draw a polygon.""" 

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

352 if fill_ink is not None: 

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

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

355 if width == 1: 

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

357 elif self.im is not None: 

358 # To avoid expanding the polygon outwards, 

359 # use the fill as a mask 

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

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

362 draw = Draw(mask) 

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

364 

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

366 

367 def regular_polygon( 

368 self, 

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

370 n_sides: int, 

371 rotation: float = 0, 

372 fill: _Ink | None = None, 

373 outline: _Ink | None = None, 

374 width: int = 1, 

375 ) -> None: 

376 """Draw a regular polygon.""" 

377 xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) 

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

379 

380 def rectangle( 

381 self, 

382 xy: Coords, 

383 fill: _Ink | None = None, 

384 outline: _Ink | None = None, 

385 width: int = 1, 

386 ) -> None: 

387 """Draw a rectangle.""" 

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

389 if fill_ink is not None: 

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

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

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

393 

394 def rounded_rectangle( 

395 self, 

396 xy: Coords, 

397 radius: float = 0, 

398 fill: _Ink | None = None, 

399 outline: _Ink | None = None, 

400 width: int = 1, 

401 *, 

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

403 ) -> None: 

404 """Draw a rounded rectangle.""" 

405 (x0, y0), (x1, y1) = self._normalize_coords(xy) 

406 if x1 < x0: 

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

408 raise ValueError(msg) 

409 if y1 < y0: 

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

411 raise ValueError(msg) 

412 if corners is None: 

413 corners = (True, True, True, True) 

414 

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

416 

417 x0 = round(x0) 

418 y0 = round(y0) 

419 x1 = round(x1) 

420 y1 = round(y1) 

421 full_x, full_y = False, False 

422 if all(corners): 

423 full_x = d >= x1 - x0 - 1 

424 if full_x: 

425 # The two left and two right corners are joined 

426 d = x1 - x0 

427 full_y = d >= y1 - y0 - 1 

428 if full_y: 

429 # The two top and two bottom corners are joined 

430 d = y1 - y0 

431 if full_x and full_y: 

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

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

434 

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

436 # If the corners have no curve, 

437 # or there are no corners, 

438 # that is a rectangle 

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

440 

441 r = int(d // 2) 

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

443 

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

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

446 if full_x: 

447 # Draw top and bottom halves 

448 parts = ( 

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

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

451 ) 

452 elif full_y: 

453 # Draw left and right halves 

454 parts = ( 

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

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

457 ) 

458 else: 

459 # Draw four separate corners 

460 parts = tuple( 

461 part 

462 for i, part in enumerate( 

463 ( 

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

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

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

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

468 ) 

469 ) 

470 if corners[i] 

471 ) 

472 for part in parts: 

473 if pieslice: 

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

475 else: 

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

477 

478 if fill_ink is not None: 

479 draw_corners(True) 

480 

481 if full_x: 

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

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

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

485 if not full_x and not full_y: 

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

487 if corners[0]: 

488 left[1] += r + 1 

489 if corners[3]: 

490 left[3] -= r + 1 

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

492 

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

494 if corners[1]: 

495 right[1] += r + 1 

496 if corners[2]: 

497 right[3] -= r + 1 

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

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

500 draw_corners(False) 

501 

502 if not full_x: 

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

504 if corners[0]: 

505 top[0] += r + 1 

506 if corners[1]: 

507 top[2] -= r + 1 

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

509 

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

511 if corners[3]: 

512 bottom[0] += r + 1 

513 if corners[2]: 

514 bottom[2] -= r + 1 

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

516 if not full_y: 

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

518 if corners[0]: 

519 left[1] += r + 1 

520 if corners[3]: 

521 left[3] -= r + 1 

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

523 

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

525 if corners[1]: 

526 right[1] += r + 1 

527 if corners[2]: 

528 right[3] -= r + 1 

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

530 

531 def text( 

532 self, 

533 xy: tuple[float, float], 

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

535 fill: _Ink | None = None, 

536 font: ImageFont.BaseImageFont | None = None, 

537 anchor: str | None = None, 

538 spacing: float = 4, 

539 align: str = "left", 

540 direction: str | None = None, 

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

542 language: str | None = None, 

543 stroke_width: float = 0, 

544 stroke_fill: _Ink | None = None, 

545 embedded_color: bool = False, 

546 *args: Any, 

547 **kwargs: Any, 

548 ) -> None: 

549 """Draw text.""" 

550 if isinstance(text, ImageText.Text): 

551 image_text = text 

552 else: 

553 if font is None: 

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

555 image_text = ImageText.Text( 

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

557 ) 

558 if embedded_color: 

559 image_text.embed_color() 

560 if stroke_width: 

561 image_text.stroke(stroke_width, stroke_fill) 

562 

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

564 ink, fill_ink = self._getink(fill) 

565 if ink is None: 

566 assert fill_ink is not None 

567 return fill_ink 

568 return ink 

569 

570 ink = getink(fill) 

571 if ink is None: 

572 return 

573 

574 stroke_ink = None 

575 if image_text.stroke_width: 

576 stroke_ink = ( 

577 getink(image_text.stroke_fill) 

578 if image_text.stroke_fill is not None 

579 else ink 

580 ) 

581 

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

583 

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

585 mode = self.fontmode 

586 if stroke_width == 0 and embedded_color: 

587 mode = "RGBA" 

588 x = int(line.x) 

589 y = int(line.y) 

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

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

592 mask, offset = image_text.font.getmask2( 

593 line.text, 

594 mode, 

595 direction, 

596 features, 

597 language, 

598 stroke_width, 

599 line.anchor, 

600 ink, 

601 start, 

602 stroke_filled=True, 

603 *args, 

604 **kwargs, 

605 ) 

606 x += offset[0] 

607 y += offset[1] 

608 else: 

609 try: 

610 mask = image_text.font.getmask( 

611 line.text, 

612 mode, 

613 direction, 

614 features, 

615 language, 

616 stroke_width, 

617 line.anchor, 

618 ink, 

619 start=start, 

620 *args, 

621 **kwargs, 

622 ) 

623 except TypeError: 

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

625 if mode == "RGBA": 

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

627 # returns color in RGB bands and mask in A 

628 # extract mask and set text alpha 

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

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

631 color.fillband(3, ink_alpha) 

632 if self.im is not None: 

633 self.im.paste( 

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

635 ) 

636 else: 

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

638 

639 if stroke_ink is not None: 

640 # Draw stroked text 

641 draw_text(stroke_ink, image_text.stroke_width) 

642 

643 # Draw normal text 

644 if ink != stroke_ink: 

645 draw_text(ink) 

646 else: 

647 # Only draw normal text 

648 draw_text(ink) 

649 

650 def multiline_text( 

651 self, 

652 xy: tuple[float, float], 

653 text: AnyStr, 

654 fill: _Ink | None = None, 

655 font: ImageFont.BaseImageFont | None = None, 

656 anchor: str | None = None, 

657 spacing: float = 4, 

658 align: str = "left", 

659 direction: str | None = None, 

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

661 language: str | None = None, 

662 stroke_width: float = 0, 

663 stroke_fill: _Ink | None = None, 

664 embedded_color: bool = False, 

665 *, 

666 font_size: float | None = None, 

667 ) -> None: 

668 return self.text( 

669 xy, 

670 text, 

671 fill, 

672 font, 

673 anchor, 

674 spacing, 

675 align, 

676 direction, 

677 features, 

678 language, 

679 stroke_width, 

680 stroke_fill, 

681 embedded_color, 

682 font_size=font_size, 

683 ) 

684 

685 def textlength( 

686 self, 

687 text: AnyStr, 

688 font: ImageFont.BaseImageFont | None = None, 

689 direction: str | None = None, 

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

691 language: str | None = None, 

692 embedded_color: bool = False, 

693 *, 

694 font_size: float | None = None, 

695 ) -> float: 

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

697 if font is None: 

698 font = self._getfont(font_size) 

699 image_text = ImageText.Text( 

700 text, 

701 font, 

702 self.mode, 

703 direction=direction, 

704 features=features, 

705 language=language, 

706 ) 

707 if embedded_color: 

708 image_text.embed_color() 

709 return image_text.get_length() 

710 

711 def textbbox( 

712 self, 

713 xy: tuple[float, float], 

714 text: AnyStr, 

715 font: ImageFont.BaseImageFont | None = None, 

716 anchor: str | None = None, 

717 spacing: float = 4, 

718 align: str = "left", 

719 direction: str | None = None, 

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

721 language: str | None = None, 

722 stroke_width: float = 0, 

723 embedded_color: bool = False, 

724 *, 

725 font_size: float | None = None, 

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

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

728 if font is None: 

729 font = self._getfont(font_size) 

730 image_text = ImageText.Text( 

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

732 ) 

733 if embedded_color: 

734 image_text.embed_color() 

735 if stroke_width: 

736 image_text.stroke(stroke_width) 

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

738 

739 def multiline_textbbox( 

740 self, 

741 xy: tuple[float, float], 

742 text: AnyStr, 

743 font: ImageFont.BaseImageFont | None = 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 return self.textbbox( 

756 xy, 

757 text, 

758 font, 

759 anchor, 

760 spacing, 

761 align, 

762 direction, 

763 features, 

764 language, 

765 stroke_width, 

766 embedded_color, 

767 font_size=font_size, 

768 ) 

769 

770 

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

772 """ 

773 A simple 2D drawing interface for PIL images. 

774 

775 :param im: The image to draw in. 

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

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

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

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

780 defaults to the mode of the image. 

781 """ 

782 try: 

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

784 except AttributeError: 

785 return ImageDraw(im, mode) 

786 

787 

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

789 """ 

790 :param im: The image to draw in. 

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

792 """ 

793 from . import ImageDraw2 

794 

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

796 return draw, ImageDraw2 

797 

798 

799def floodfill( 

800 image: Image.Image, 

801 xy: tuple[int, int], 

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

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

804 thresh: float = 0, 

805) -> None: 

806 """ 

807 .. warning:: This method is experimental. 

808 

809 Fills a bounded region with a given color. 

810 

811 :param image: Target image. 

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

813 :ref:`coordinate-system`. 

814 :param value: Fill color. 

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

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

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

818 pixel. 

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

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

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

822 non-homogeneous, but similar, colors. 

823 """ 

824 # based on an implementation by Eric S. Raymond 

825 # amended by yo1995 @20180806 

826 pixel = image.load() 

827 assert pixel is not None 

828 x, y = xy 

829 try: 

830 background = pixel[x, y] 

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

832 return # seed point already has fill color 

833 pixel[x, y] = value 

834 except (ValueError, IndexError): 

835 return # seed point outside image 

836 edge = {(x, y)} 

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

838 # to reduce memory consumption 

839 full_edge = set() 

840 while edge: 

841 new_edge = set() 

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

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

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

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

846 continue 

847 try: 

848 p = pixel[s, t] 

849 except (ValueError, IndexError): 

850 pass 

851 else: 

852 full_edge.add((s, t)) 

853 if border is None: 

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

855 else: 

856 fill = p not in (value, border) 

857 if fill: 

858 pixel[s, t] = value 

859 new_edge.add((s, t)) 

860 full_edge = edge # discard pixels processed 

861 edge = new_edge 

862 

863 

864def _compute_regular_polygon_vertices( 

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

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

867 """ 

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

869 

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

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

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

873 :param n_sides: Number of sides 

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

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

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

877 :return: List of regular polygon vertices 

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

879 

880 How are the vertices computed? 

881 1. Compute the following variables 

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

883 - side_length: Length of each polygon edge 

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

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

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

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

888 

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

890 The vertex is computed using the equation below. 

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

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

893 

894 Note: 

895 φ = angle in degrees 

896 x = 0 

897 y = polygon_radius 

898 

899 The formula above assumes rotation around the origin. 

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

901 To account for this, we use the formula below 

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

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

904 """ 

905 # 1. Error Handling 

906 # 1.1 Check `n_sides` has an appropriate value 

907 if not isinstance(n_sides, int): 

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

909 raise TypeError(msg) 

910 if n_sides < 3: 

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

912 raise ValueError(msg) 

913 

914 # 1.2 Check `bounding_circle` has an appropriate value 

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

916 msg = "bounding_circle should be a sequence" 

917 raise TypeError(msg) 

918 

919 if len(bounding_circle) == 3: 

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

921 msg = "bounding_circle should only contain numeric data" 

922 raise ValueError(msg) 

923 

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

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

926 if not all( 

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

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

929 msg = "bounding_circle should only contain numeric data" 

930 raise ValueError(msg) 

931 

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

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

934 raise ValueError(msg) 

935 

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

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

938 else: 

939 msg = ( 

940 "bounding_circle should contain 2D coordinates " 

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

942 ) 

943 raise ValueError(msg) 

944 

945 if polygon_radius <= 0: 

946 msg = "bounding_circle radius should be > 0" 

947 raise ValueError(msg) 

948 

949 # 1.3 Check `rotation` has an appropriate value 

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

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

952 raise ValueError(msg) 

953 

954 # 2. Define Helper Functions 

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

956 return ( 

957 round( 

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

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

960 + centroid[0], 

961 2, 

962 ), 

963 round( 

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

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

966 + centroid[1], 

967 2, 

968 ), 

969 ) 

970 

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

972 start_point = [polygon_radius, 0] 

973 return _apply_rotation(start_point, angle) 

974 

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

976 angles = [] 

977 degrees = 360 / n_sides 

978 # Start with the bottom left polygon vertex 

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

980 for _ in range(n_sides): 

981 angles.append(current_angle) 

982 current_angle += degrees 

983 if current_angle > 360: 

984 current_angle -= 360 

985 return angles 

986 

987 # 3. Variable Declarations 

988 angles = _get_angles(n_sides, rotation) 

989 

990 # 4. Compute Vertices 

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

992 

993 

994def _color_diff( 

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

996) -> float: 

997 """ 

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

999 """ 

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

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

1002 

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