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
34import math
35import struct
36from collections.abc import Sequence
37from typing import cast
39from . import Image, ImageColor, ImageFont, ImageText
41TYPE_CHECKING = False
42if TYPE_CHECKING:
43 from collections.abc import Callable
44 from types import ModuleType
45 from typing import Any, AnyStr
47 from . import ImageDraw2
48 from ._typing import Coords, _Ink
50# experimental access to the outline API
51Outline: Callable[[], Image.core._Outline] = Image.core.outline
53"""
54A simple 2D drawing interface for PIL images.
55<p>
56Application code should use the <b>Draw</b> factory, instead of
57directly.
58"""
61class ImageDraw:
62 font: ImageFont.BaseImageFont | None = None
64 def __init__(self, im: Image.Image, mode: str | None = None) -> None:
65 """
66 Create a drawing instance.
68 :param im: The image to draw in.
69 :param mode: Optional mode to use for color values. For RGB
70 images, this argument can be RGB or RGBA (to blend the
71 drawing into the image). For all other modes, this argument
72 must be the same as the image mode. If omitted, the mode
73 defaults to the mode of the image.
74 """
75 im._ensure_mutable()
76 blend = 0
77 if mode is None:
78 mode = im.mode
79 if mode != im.mode:
80 if mode == "RGBA" and im.mode == "RGB":
81 blend = 1
82 else:
83 msg = "mode mismatch"
84 raise ValueError(msg)
85 if mode == "P":
86 self.palette = im.palette
87 else:
88 self.palette = None
89 self._image = im
90 self.im = im.im
91 self.draw = Image.core.draw(self.im, blend)
92 self.mode = mode
93 if mode in ("I", "F"):
94 self.ink = self.draw.draw_ink(1)
95 else:
96 self.ink = self.draw.draw_ink(-1)
97 if mode in ("1", "P", "I", "F"):
98 # FIXME: fix Fill2 to properly support matte for I+F images
99 self.fontmode = "1"
100 else:
101 self.fontmode = "L" # aliasing is okay for other modes
102 self.fill = False
104 def getfont(
105 self,
106 ) -> ImageFont.BaseImageFont:
107 """
108 Get the current default font.
110 To set the default font for this ImageDraw instance::
112 from PIL import ImageDraw, ImageFont
113 draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
115 To set the default font for all future ImageDraw instances::
117 from PIL import ImageDraw, ImageFont
118 ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
120 If the current default font is ``None``,
121 it is initialized with ``ImageFont.load_default()``.
123 :returns: An image font."""
124 if not self.font:
125 # FIXME: should add a font repository
126 self.font = ImageFont.load_default()
127 return self.font
129 def _getfont(self, font_size: float | None) -> ImageFont.BaseImageFont:
130 if font_size is not None:
131 return ImageFont.load_default(font_size)
132 else:
133 return self.getfont()
135 def _getink(
136 self, ink: _Ink | None, fill: _Ink | None = None
137 ) -> tuple[int | None, int | None]:
138 result_ink = None
139 result_fill = None
140 if ink is None and fill is None:
141 if self.fill:
142 result_fill = self.ink
143 else:
144 result_ink = self.ink
145 else:
146 if ink is not None:
147 if isinstance(ink, str):
148 ink = ImageColor.getcolor(ink, self.mode)
149 if self.palette and isinstance(ink, tuple):
150 ink = self.palette.getcolor(ink, self._image)
151 result_ink = self.draw.draw_ink(ink)
152 if fill is not None:
153 if isinstance(fill, str):
154 fill = ImageColor.getcolor(fill, self.mode)
155 if self.palette and isinstance(fill, tuple):
156 fill = self.palette.getcolor(fill, self._image)
157 result_fill = self.draw.draw_ink(fill)
158 return result_ink, result_fill
160 def arc(
161 self,
162 xy: Coords,
163 start: float,
164 end: float,
165 fill: _Ink | None = None,
166 width: int = 1,
167 ) -> None:
168 """Draw an arc."""
169 ink, fill = self._getink(fill)
170 if ink is not None and width != 0:
171 self.draw.draw_arc(xy, start, end, ink, width)
173 def bitmap(
174 self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None
175 ) -> None:
176 """Draw a bitmap."""
177 bitmap.load()
178 ink, fill = self._getink(fill)
179 if ink is None:
180 ink = fill
181 if ink is not None:
182 self.draw.draw_bitmap(xy, bitmap.im, ink)
184 def chord(
185 self,
186 xy: Coords,
187 start: float,
188 end: float,
189 fill: _Ink | None = None,
190 outline: _Ink | None = None,
191 width: int = 1,
192 ) -> None:
193 """Draw a chord."""
194 ink, fill_ink = self._getink(outline, fill)
195 if fill_ink is not None:
196 self.draw.draw_chord(xy, start, end, fill_ink, 1)
197 if ink is not None and ink != fill_ink and width != 0:
198 self.draw.draw_chord(xy, start, end, ink, 0, width)
200 def ellipse(
201 self,
202 xy: Coords,
203 fill: _Ink | None = None,
204 outline: _Ink | None = None,
205 width: int = 1,
206 ) -> None:
207 """Draw an ellipse."""
208 ink, fill_ink = self._getink(outline, fill)
209 if fill_ink is not None:
210 self.draw.draw_ellipse(xy, fill_ink, 1)
211 if ink is not None and ink != fill_ink and width != 0:
212 self.draw.draw_ellipse(xy, ink, 0, width)
214 def circle(
215 self,
216 xy: Sequence[float],
217 radius: float,
218 fill: _Ink | None = None,
219 outline: _Ink | None = None,
220 width: int = 1,
221 ) -> None:
222 """Draw a circle given center coordinates and a radius."""
223 ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius)
224 self.ellipse(ellipse_xy, fill, outline, width)
226 def line(
227 self,
228 xy: Coords,
229 fill: _Ink | None = None,
230 width: int = 1,
231 joint: str | None = None,
232 ) -> None:
233 """Draw a line, or a connected sequence of line segments."""
234 ink = self._getink(fill)[0]
235 if ink is not None and width != 0:
236 self.draw.draw_lines(xy, ink, width)
237 if joint == "curve" and width > 4:
238 points: Sequence[Sequence[float]]
239 if isinstance(xy[0], (list, tuple)):
240 points = cast(Sequence[Sequence[float]], xy)
241 else:
242 points = [
243 cast(Sequence[float], tuple(xy[i : i + 2]))
244 for i in range(0, len(xy), 2)
245 ]
246 for i in range(1, len(points) - 1):
247 point = points[i]
248 angles = [
249 math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
250 % 360
251 for start, end in (
252 (points[i - 1], point),
253 (point, points[i + 1]),
254 )
255 ]
256 if angles[0] == angles[1]:
257 # This is a straight line, so no joint is required
258 continue
260 def coord_at_angle(
261 coord: Sequence[float], angle: float
262 ) -> tuple[float, ...]:
263 x, y = coord
264 angle -= 90
265 distance = width / 2 - 1
266 return tuple(
267 p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d))
268 for p, p_d in (
269 (x, distance * math.cos(math.radians(angle))),
270 (y, distance * math.sin(math.radians(angle))),
271 )
272 )
274 flipped = (
275 angles[1] > angles[0] and angles[1] - 180 > angles[0]
276 ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0])
277 coords = [
278 (point[0] - width / 2 + 1, point[1] - width / 2 + 1),
279 (point[0] + width / 2 - 1, point[1] + width / 2 - 1),
280 ]
281 if flipped:
282 start, end = (angles[1] + 90, angles[0] + 90)
283 else:
284 start, end = (angles[0] - 90, angles[1] - 90)
285 self.pieslice(coords, start - 90, end - 90, fill)
287 if width > 8:
288 # Cover potential gaps between the line and the joint
289 if flipped:
290 gap_coords = [
291 coord_at_angle(point, angles[0] + 90),
292 point,
293 coord_at_angle(point, angles[1] + 90),
294 ]
295 else:
296 gap_coords = [
297 coord_at_angle(point, angles[0] - 90),
298 point,
299 coord_at_angle(point, angles[1] - 90),
300 ]
301 self.line(gap_coords, fill, width=3)
303 def shape(
304 self,
305 shape: Image.core._Outline,
306 fill: _Ink | None = None,
307 outline: _Ink | None = None,
308 ) -> None:
309 """(Experimental) Draw a shape."""
310 shape.close()
311 ink, fill_ink = self._getink(outline, fill)
312 if fill_ink is not None:
313 self.draw.draw_outline(shape, fill_ink, 1)
314 if ink is not None and ink != fill_ink:
315 self.draw.draw_outline(shape, ink, 0)
317 def pieslice(
318 self,
319 xy: Coords,
320 start: float,
321 end: float,
322 fill: _Ink | None = None,
323 outline: _Ink | None = None,
324 width: int = 1,
325 ) -> None:
326 """Draw a pieslice."""
327 ink, fill_ink = self._getink(outline, fill)
328 if fill_ink is not None:
329 self.draw.draw_pieslice(xy, start, end, fill_ink, 1)
330 if ink is not None and ink != fill_ink and width != 0:
331 self.draw.draw_pieslice(xy, start, end, ink, 0, width)
333 def point(self, xy: Coords, fill: _Ink | None = None) -> None:
334 """Draw one or more individual pixels."""
335 ink, fill = self._getink(fill)
336 if ink is not None:
337 self.draw.draw_points(xy, ink)
339 def polygon(
340 self,
341 xy: Coords,
342 fill: _Ink | None = None,
343 outline: _Ink | None = None,
344 width: int = 1,
345 ) -> None:
346 """Draw a polygon."""
347 ink, fill_ink = self._getink(outline, fill)
348 if fill_ink is not None:
349 self.draw.draw_polygon(xy, fill_ink, 1)
350 if ink is not None and ink != fill_ink and width != 0:
351 if width == 1:
352 self.draw.draw_polygon(xy, ink, 0, width)
353 elif self.im is not None:
354 # To avoid expanding the polygon outwards,
355 # use the fill as a mask
356 mask = Image.new("1", self.im.size)
357 mask_ink = self._getink(1)[0]
358 draw = Draw(mask)
359 draw.draw.draw_polygon(xy, mask_ink, 1)
361 self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im)
363 def regular_polygon(
364 self,
365 bounding_circle: Sequence[Sequence[float] | float],
366 n_sides: int,
367 rotation: float = 0,
368 fill: _Ink | None = None,
369 outline: _Ink | None = None,
370 width: int = 1,
371 ) -> None:
372 """Draw a regular polygon."""
373 xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
374 self.polygon(xy, fill, outline, width)
376 def rectangle(
377 self,
378 xy: Coords,
379 fill: _Ink | None = None,
380 outline: _Ink | None = None,
381 width: int = 1,
382 ) -> None:
383 """Draw a rectangle."""
384 ink, fill_ink = self._getink(outline, fill)
385 if fill_ink is not None:
386 self.draw.draw_rectangle(xy, fill_ink, 1)
387 if ink is not None and ink != fill_ink and width != 0:
388 self.draw.draw_rectangle(xy, ink, 0, width)
390 def rounded_rectangle(
391 self,
392 xy: Coords,
393 radius: float = 0,
394 fill: _Ink | None = None,
395 outline: _Ink | None = None,
396 width: int = 1,
397 *,
398 corners: tuple[bool, bool, bool, bool] | None = None,
399 ) -> None:
400 """Draw a rounded rectangle."""
401 if isinstance(xy[0], (list, tuple)):
402 (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy)
403 else:
404 x0, y0, x1, y1 = cast(Sequence[float], xy)
405 if x1 < x0:
406 msg = "x1 must be greater than or equal to x0"
407 raise ValueError(msg)
408 if y1 < y0:
409 msg = "y1 must be greater than or equal to y0"
410 raise ValueError(msg)
411 if corners is None:
412 corners = (True, True, True, True)
414 d = min(x1 - x0, y1 - y0, radius * 2)
416 x0 = round(x0)
417 y0 = round(y0)
418 x1 = round(x1)
419 y1 = round(y1)
420 full_x, full_y = False, False
421 if all(corners):
422 full_x = d >= x1 - x0 - 1
423 if full_x:
424 # The two left and two right corners are joined
425 d = x1 - x0
426 full_y = d >= y1 - y0 - 1
427 if full_y:
428 # The two top and two bottom corners are joined
429 d = y1 - y0
430 if full_x and full_y:
431 # If all corners are joined, that is a circle
432 return self.ellipse(xy, fill, outline, width)
434 if d == 0 or not any(corners):
435 # If the corners have no curve,
436 # or there are no corners,
437 # that is a rectangle
438 return self.rectangle(xy, fill, outline, width)
440 r = int(d // 2)
441 ink, fill_ink = self._getink(outline, fill)
443 def draw_corners(pieslice: bool) -> None:
444 parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
445 if full_x:
446 # Draw top and bottom halves
447 parts = (
448 ((x0, y0, x0 + d, y0 + d), 180, 360),
449 ((x0, y1 - d, x0 + d, y1), 0, 180),
450 )
451 elif full_y:
452 # Draw left and right halves
453 parts = (
454 ((x0, y0, x0 + d, y0 + d), 90, 270),
455 ((x1 - d, y0, x1, y0 + d), 270, 90),
456 )
457 else:
458 # Draw four separate corners
459 parts = tuple(
460 part
461 for i, part in enumerate(
462 (
463 ((x0, y0, x0 + d, y0 + d), 180, 270),
464 ((x1 - d, y0, x1, y0 + d), 270, 360),
465 ((x1 - d, y1 - d, x1, y1), 0, 90),
466 ((x0, y1 - d, x0 + d, y1), 90, 180),
467 )
468 )
469 if corners[i]
470 )
471 for part in parts:
472 if pieslice:
473 self.draw.draw_pieslice(*(part + (fill_ink, 1)))
474 else:
475 self.draw.draw_arc(*(part + (ink, width)))
477 if fill_ink is not None:
478 draw_corners(True)
480 if full_x:
481 self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
482 elif x1 - r - 1 >= x0 + r + 1:
483 self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
484 if not full_x and not full_y:
485 left = [x0, y0, x0 + r, y1]
486 if corners[0]:
487 left[1] += r + 1
488 if corners[3]:
489 left[3] -= r + 1
490 self.draw.draw_rectangle(left, fill_ink, 1)
492 right = [x1 - r, y0, x1, y1]
493 if corners[1]:
494 right[1] += r + 1
495 if corners[2]:
496 right[3] -= r + 1
497 self.draw.draw_rectangle(right, fill_ink, 1)
498 if ink is not None and ink != fill_ink and width != 0:
499 draw_corners(False)
501 if not full_x:
502 top = [x0, y0, x1, y0 + width - 1]
503 if corners[0]:
504 top[0] += r + 1
505 if corners[1]:
506 top[2] -= r + 1
507 self.draw.draw_rectangle(top, ink, 1)
509 bottom = [x0, y1 - width + 1, x1, y1]
510 if corners[3]:
511 bottom[0] += r + 1
512 if corners[2]:
513 bottom[2] -= r + 1
514 self.draw.draw_rectangle(bottom, ink, 1)
515 if not full_y:
516 left = [x0, y0, x0 + width - 1, y1]
517 if corners[0]:
518 left[1] += r + 1
519 if corners[3]:
520 left[3] -= r + 1
521 self.draw.draw_rectangle(left, ink, 1)
523 right = [x1 - width + 1, y0, x1, y1]
524 if corners[1]:
525 right[1] += r + 1
526 if corners[2]:
527 right[3] -= r + 1
528 self.draw.draw_rectangle(right, ink, 1)
530 def text(
531 self,
532 xy: tuple[float, float],
533 text: AnyStr | ImageText.Text[AnyStr],
534 fill: _Ink | None = None,
535 font: ImageFont.BaseImageFont | None = None,
536 anchor: str | None = None,
537 spacing: float = 4,
538 align: str = "left",
539 direction: str | None = None,
540 features: list[str] | None = None,
541 language: str | None = None,
542 stroke_width: float = 0,
543 stroke_fill: _Ink | None = None,
544 embedded_color: bool = False,
545 *args: Any,
546 **kwargs: Any,
547 ) -> None:
548 """Draw text."""
549 if isinstance(text, ImageText.Text):
550 image_text = text
551 else:
552 if font is None:
553 font = self._getfont(kwargs.get("font_size"))
554 image_text = ImageText.Text(
555 text, font, self.mode, spacing, direction, features, language
556 )
557 if embedded_color:
558 image_text.embed_color()
559 if stroke_width:
560 image_text.stroke(stroke_width, stroke_fill)
562 def getink(fill: _Ink | None) -> int:
563 ink, fill_ink = self._getink(fill)
564 if ink is None:
565 assert fill_ink is not None
566 return fill_ink
567 return ink
569 ink = getink(fill)
570 if ink is None:
571 return
573 stroke_ink = None
574 if image_text.stroke_width:
575 stroke_ink = (
576 getink(image_text.stroke_fill)
577 if image_text.stroke_fill is not None
578 else ink
579 )
581 for line in image_text._split(xy, anchor, align):
583 def draw_text(ink: int, stroke_width: float = 0) -> None:
584 mode = self.fontmode
585 if stroke_width == 0 and embedded_color:
586 mode = "RGBA"
587 x = int(line.x)
588 y = int(line.y)
589 start = (math.modf(line.x)[0], math.modf(line.y)[0])
590 if isinstance(image_text.font, ImageFont.FreeTypeFont):
591 mask, offset = image_text.font.getmask2(
592 line.text,
593 mode,
594 direction,
595 features,
596 language,
597 stroke_width,
598 line.anchor,
599 ink,
600 start,
601 stroke_filled=True,
602 *args,
603 **kwargs,
604 )
605 x += offset[0]
606 y += offset[1]
607 else:
608 try:
609 mask = image_text.font.getmask(
610 line.text,
611 mode,
612 direction,
613 features,
614 language,
615 stroke_width,
616 line.anchor,
617 ink,
618 start=start,
619 *args,
620 **kwargs,
621 )
622 except TypeError:
623 mask = image_text.font.getmask(line.text)
624 if mode == "RGBA":
625 # image_text.font.getmask2(mode="RGBA")
626 # returns color in RGB bands and mask in A
627 # extract mask and set text alpha
628 color, mask = mask, mask.getband(3)
629 ink_alpha = struct.pack("i", ink)[3]
630 color.fillband(3, ink_alpha)
631 if self.im is not None:
632 self.im.paste(
633 color, (x, y, x + mask.size[0], y + mask.size[1]), mask
634 )
635 else:
636 self.draw.draw_bitmap((x, y), mask, ink)
638 if stroke_ink is not None:
639 # Draw stroked text
640 draw_text(stroke_ink, image_text.stroke_width)
642 # Draw normal text
643 if ink != stroke_ink:
644 draw_text(ink)
645 else:
646 # Only draw normal text
647 draw_text(ink)
649 def multiline_text(
650 self,
651 xy: tuple[float, float],
652 text: AnyStr,
653 fill: _Ink | None = None,
654 font: ImageFont.BaseImageFont | None = None,
655 anchor: str | None = None,
656 spacing: float = 4,
657 align: str = "left",
658 direction: str | None = None,
659 features: list[str] | None = None,
660 language: str | None = None,
661 stroke_width: float = 0,
662 stroke_fill: _Ink | None = None,
663 embedded_color: bool = False,
664 *,
665 font_size: float | None = None,
666 ) -> None:
667 return self.text(
668 xy,
669 text,
670 fill,
671 font,
672 anchor,
673 spacing,
674 align,
675 direction,
676 features,
677 language,
678 stroke_width,
679 stroke_fill,
680 embedded_color,
681 font_size=font_size,
682 )
684 def textlength(
685 self,
686 text: AnyStr,
687 font: ImageFont.BaseImageFont | None = None,
688 direction: str | None = None,
689 features: list[str] | None = None,
690 language: str | None = None,
691 embedded_color: bool = False,
692 *,
693 font_size: float | None = None,
694 ) -> float:
695 """Get the length of a given string, in pixels with 1/64 precision."""
696 if font is None:
697 font = self._getfont(font_size)
698 image_text = ImageText.Text(
699 text,
700 font,
701 self.mode,
702 direction=direction,
703 features=features,
704 language=language,
705 )
706 if embedded_color:
707 image_text.embed_color()
708 return image_text.get_length()
710 def textbbox(
711 self,
712 xy: tuple[float, float],
713 text: AnyStr,
714 font: ImageFont.BaseImageFont | None = None,
715 anchor: str | None = None,
716 spacing: float = 4,
717 align: str = "left",
718 direction: str | None = None,
719 features: list[str] | None = None,
720 language: str | None = None,
721 stroke_width: float = 0,
722 embedded_color: bool = False,
723 *,
724 font_size: float | None = None,
725 ) -> tuple[float, float, float, float]:
726 """Get the bounding box of a given string, in pixels."""
727 if font is None:
728 font = self._getfont(font_size)
729 image_text = ImageText.Text(
730 text, font, self.mode, spacing, direction, features, language
731 )
732 if embedded_color:
733 image_text.embed_color()
734 if stroke_width:
735 image_text.stroke(stroke_width)
736 return image_text.get_bbox(xy, anchor, align)
738 def multiline_textbbox(
739 self,
740 xy: tuple[float, float],
741 text: AnyStr,
742 font: ImageFont.BaseImageFont | None = None,
743 anchor: str | None = None,
744 spacing: float = 4,
745 align: str = "left",
746 direction: str | None = None,
747 features: list[str] | None = None,
748 language: str | None = None,
749 stroke_width: float = 0,
750 embedded_color: bool = False,
751 *,
752 font_size: float | None = None,
753 ) -> tuple[float, float, float, float]:
754 return self.textbbox(
755 xy,
756 text,
757 font,
758 anchor,
759 spacing,
760 align,
761 direction,
762 features,
763 language,
764 stroke_width,
765 embedded_color,
766 font_size=font_size,
767 )
770def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
771 """
772 A simple 2D drawing interface for PIL images.
774 :param im: The image to draw in.
775 :param mode: Optional mode to use for color values. For RGB
776 images, this argument can be RGB or RGBA (to blend the
777 drawing into the image). For all other modes, this argument
778 must be the same as the image mode. If omitted, the mode
779 defaults to the mode of the image.
780 """
781 try:
782 return getattr(im, "getdraw")(mode)
783 except AttributeError:
784 return ImageDraw(im, mode)
787def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]:
788 """
789 :param im: The image to draw in.
790 :returns: A (drawing context, drawing resource factory) tuple.
791 """
792 from . import ImageDraw2
794 draw = ImageDraw2.Draw(im) if im is not None else None
795 return draw, ImageDraw2
798def floodfill(
799 image: Image.Image,
800 xy: tuple[int, int],
801 value: float | tuple[int, ...],
802 border: float | tuple[int, ...] | None = None,
803 thresh: float = 0,
804) -> None:
805 """
806 .. warning:: This method is experimental.
808 Fills a bounded region with a given color.
810 :param image: Target image.
811 :param xy: Seed position (a 2-item coordinate tuple). See
812 :ref:`coordinate-system`.
813 :param value: Fill color.
814 :param border: Optional border value. If given, the region consists of
815 pixels with a color different from the border color. If not given,
816 the region consists of pixels having the same color as the seed
817 pixel.
818 :param thresh: Optional threshold value which specifies a maximum
819 tolerable difference of a pixel value from the 'background' in
820 order for it to be replaced. Useful for filling regions of
821 non-homogeneous, but similar, colors.
822 """
823 # based on an implementation by Eric S. Raymond
824 # amended by yo1995 @20180806
825 pixel = image.load()
826 assert pixel is not None
827 x, y = xy
828 try:
829 background = pixel[x, y]
830 if _color_diff(value, background) <= thresh:
831 return # seed point already has fill color
832 pixel[x, y] = value
833 except (ValueError, IndexError):
834 return # seed point outside image
835 edge = {(x, y)}
836 # use a set to keep record of current and previous edge pixels
837 # to reduce memory consumption
838 full_edge = set()
839 while edge:
840 new_edge = set()
841 for x, y in edge: # 4 adjacent method
842 for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
843 # If already processed, or if a coordinate is negative, skip
844 if (s, t) in full_edge or s < 0 or t < 0:
845 continue
846 try:
847 p = pixel[s, t]
848 except (ValueError, IndexError):
849 pass
850 else:
851 full_edge.add((s, t))
852 if border is None:
853 fill = _color_diff(p, background) <= thresh
854 else:
855 fill = p not in (value, border)
856 if fill:
857 pixel[s, t] = value
858 new_edge.add((s, t))
859 full_edge = edge # discard pixels processed
860 edge = new_edge
863def _compute_regular_polygon_vertices(
864 bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float
865) -> list[tuple[float, float]]:
866 """
867 Generate a list of vertices for a 2D regular polygon.
869 :param bounding_circle: The bounding circle is a sequence defined
870 by a point and radius. The polygon is inscribed in this circle.
871 (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
872 :param n_sides: Number of sides
873 (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon)
874 :param rotation: Apply an arbitrary rotation to the polygon
875 (e.g. ``rotation=90``, applies a 90 degree rotation)
876 :return: List of regular polygon vertices
877 (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``)
879 How are the vertices computed?
880 1. Compute the following variables
881 - theta: Angle between the apothem & the nearest polygon vertex
882 - side_length: Length of each polygon edge
883 - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle)
884 - polygon_radius: Polygon radius (last element of bounding_circle)
885 - angles: Location of each polygon vertex in polar grid
886 (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0])
888 2. For each angle in angles, get the polygon vertex at that angle
889 The vertex is computed using the equation below.
890 X= xcos(φ) + ysin(φ)
891 Y= −xsin(φ) + ycos(φ)
893 Note:
894 φ = angle in degrees
895 x = 0
896 y = polygon_radius
898 The formula above assumes rotation around the origin.
899 In our case, we are rotating around the centroid.
900 To account for this, we use the formula below
901 X = xcos(φ) + ysin(φ) + centroid_x
902 Y = −xsin(φ) + ycos(φ) + centroid_y
903 """
904 # 1. Error Handling
905 # 1.1 Check `n_sides` has an appropriate value
906 if not isinstance(n_sides, int):
907 msg = "n_sides should be an int" # type: ignore[unreachable]
908 raise TypeError(msg)
909 if n_sides < 3:
910 msg = "n_sides should be an int > 2"
911 raise ValueError(msg)
913 # 1.2 Check `bounding_circle` has an appropriate value
914 if not isinstance(bounding_circle, (list, tuple)):
915 msg = "bounding_circle should be a sequence"
916 raise TypeError(msg)
918 if len(bounding_circle) == 3:
919 if not all(isinstance(i, (int, float)) for i in bounding_circle):
920 msg = "bounding_circle should only contain numeric data"
921 raise ValueError(msg)
923 *centroid, polygon_radius = cast(list[float], list(bounding_circle))
924 elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
925 if not all(
926 isinstance(i, (int, float)) for i in bounding_circle[0]
927 ) or not isinstance(bounding_circle[1], (int, float)):
928 msg = "bounding_circle should only contain numeric data"
929 raise ValueError(msg)
931 if len(bounding_circle[0]) != 2:
932 msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
933 raise ValueError(msg)
935 centroid = cast(list[float], list(bounding_circle[0]))
936 polygon_radius = cast(float, bounding_circle[1])
937 else:
938 msg = (
939 "bounding_circle should contain 2D coordinates "
940 "and a radius (e.g. (x, y, r) or ((x, y), r) )"
941 )
942 raise ValueError(msg)
944 if polygon_radius <= 0:
945 msg = "bounding_circle radius should be > 0"
946 raise ValueError(msg)
948 # 1.3 Check `rotation` has an appropriate value
949 if not isinstance(rotation, (int, float)):
950 msg = "rotation should be an int or float" # type: ignore[unreachable]
951 raise ValueError(msg)
953 # 2. Define Helper Functions
954 def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
955 return (
956 round(
957 point[0] * math.cos(math.radians(360 - degrees))
958 - point[1] * math.sin(math.radians(360 - degrees))
959 + centroid[0],
960 2,
961 ),
962 round(
963 point[1] * math.cos(math.radians(360 - degrees))
964 + point[0] * math.sin(math.radians(360 - degrees))
965 + centroid[1],
966 2,
967 ),
968 )
970 def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
971 start_point = [polygon_radius, 0]
972 return _apply_rotation(start_point, angle)
974 def _get_angles(n_sides: int, rotation: float) -> list[float]:
975 angles = []
976 degrees = 360 / n_sides
977 # Start with the bottom left polygon vertex
978 current_angle = (270 - 0.5 * degrees) + rotation
979 for _ in range(n_sides):
980 angles.append(current_angle)
981 current_angle += degrees
982 if current_angle > 360:
983 current_angle -= 360
984 return angles
986 # 3. Variable Declarations
987 angles = _get_angles(n_sides, rotation)
989 # 4. Compute Vertices
990 return [_compute_polygon_vertex(angle) for angle in angles]
993def _color_diff(
994 color1: float | tuple[int, ...], color2: float | tuple[int, ...]
995) -> float:
996 """
997 Uses 1-norm distance to calculate difference between two values.
998 """
999 first = color1 if isinstance(color1, tuple) else (color1,)
1000 second = color2 if isinstance(color2, tuple) else (color2,)
1002 return sum(abs(first[i] - second[i]) for i in range(len(second)))