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
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
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
34__lazy_modules__ = {"math", "struct"}
36import math
37import struct
38from typing import cast
40from . import Image, ImageColor, ImageFont, ImageText
42TYPE_CHECKING = False
43if TYPE_CHECKING:
44 from collections.abc import Callable, Sequence
45 from types import ModuleType
46 from typing import Any, AnyStr
48 from . import ImageDraw2
49 from ._typing import Coords, _Ink
51# experimental access to the outline API
52Outline: Callable[[], Image.core._Outline] = Image.core.outline
54"""
55A simple 2D drawing interface for PIL images.
56<p>
57Application code should use the <b>Draw</b> factory, instead of
58directly.
59"""
62class ImageDraw:
63 font: ImageFont.BaseImageFont | None = None
65 def __init__(self, im: Image.Image, mode: str | None = None) -> None:
66 """
67 Create a drawing instance.
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
105 def getfont(
106 self,
107 ) -> ImageFont.BaseImageFont:
108 """
109 Get the current default font.
111 To set the default font for this ImageDraw instance::
113 from PIL import ImageDraw, ImageFont
114 draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
116 To set the default font for all future ImageDraw instances::
118 from PIL import ImageDraw, ImageFont
119 ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
121 If the current default font is ``None``,
122 it is initialized with ``ImageFont.load_default()``.
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
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()
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
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)
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)
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)
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)
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)
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 ]
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
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 )
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)
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)
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)
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)
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)
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)
365 self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im)
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)
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)
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)
415 d = min(x1 - x0, y1 - y0, radius * 2)
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)
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)
441 r = int(d // 2)
442 ink, fill_ink = self._getink(outline, fill)
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)))
478 if fill_ink is not None:
479 draw_corners(True)
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)
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)
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)
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)
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)
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)
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
570 ink = getink(fill)
571 if ink is None:
572 return
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 )
582 for line in image_text._split(xy, anchor, align):
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)
639 if stroke_ink is not None:
640 # Draw stroked text
641 draw_text(stroke_ink, image_text.stroke_width)
643 # Draw normal text
644 if ink != stroke_ink:
645 draw_text(ink)
646 else:
647 # Only draw normal text
648 draw_text(ink)
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 )
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()
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)
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 )
771def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
772 """
773 A simple 2D drawing interface for PIL images.
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)
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
795 draw = ImageDraw2.Draw(im) if im is not None else None
796 return draw, ImageDraw2
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.
809 Fills a bounded region with a given color.
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
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.
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)]``)
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])
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(φ)
894 Note:
895 φ = angle in degrees
896 x = 0
897 y = polygon_radius
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)
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)
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)
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)
932 if len(bounding_circle[0]) != 2:
933 msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
934 raise ValueError(msg)
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)
945 if polygon_radius <= 0:
946 msg = "bounding_circle radius should be > 0"
947 raise ValueError(msg)
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)
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 )
971 def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
972 start_point = [polygon_radius, 0]
973 return _apply_rotation(start_point, angle)
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
987 # 3. Variable Declarations
988 angles = _get_angles(n_sides, rotation)
990 # 4. Compute Vertices
991 return [_compute_polygon_vertex(angle) for angle in angles]
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,)
1003 return sum(abs(first[i] - second[i]) for i in range(len(second)))