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, 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, ImageFont
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: (
63 ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None
64 ) = None
66 def __init__(self, im: Image.Image, mode: str | None = None) -> None:
67 """
68 Create a drawing instance.
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
106 def getfont(
107 self,
108 ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
109 """
110 Get the current default font.
112 To set the default font for this ImageDraw instance::
114 from PIL import ImageDraw, ImageFont
115 draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
117 To set the default font for all future ImageDraw instances::
119 from PIL import ImageDraw, ImageFont
120 ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
122 If the current default font is ``None``,
123 it is initialized with ``ImageFont.load_default()``.
125 :returns: An image font."""
126 if not self.font:
127 # FIXME: should add a font repository
128 from . import ImageFont
130 self.font = ImageFont.load_default()
131 return self.font
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
139 return ImageFont.load_default(font_size)
140 else:
141 return self.getfont()
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
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 and width != 0:
179 self.draw.draw_arc(xy, start, end, ink, width)
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)
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)
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)
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)
234 def line(
235 self,
236 xy: Coords,
237 fill: _Ink | None = None,
238 width: int = 1,
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 and width != 0:
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
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 )
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)
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)
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)
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)
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)
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)
369 self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im)
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)
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)
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)
422 d = radius * 2
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)
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)
448 r = int(d // 2)
449 ink, fill_ink = self._getink(outline, fill)
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)))
485 if fill_ink is not None:
486 draw_corners(True)
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)
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)
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)
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)
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)
538 def text(
539 self,
540 xy: tuple[float, float],
541 text: AnyStr | ImageText.Text[AnyStr],
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)
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
582 ink = getink(fill)
583 if ink is None:
584 return
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 )
594 for line in image_text._split(xy, anchor, align):
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 x = int(line.x)
601 y = int(line.y)
602 start = (math.modf(line.x)[0], math.modf(line.y)[0])
603 try:
604 mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc]
605 line.text,
606 mode,
607 direction=direction,
608 features=features,
609 language=language,
610 stroke_width=stroke_width,
611 stroke_filled=True,
612 anchor=line.anchor,
613 ink=ink,
614 start=start,
615 *args,
616 **kwargs,
617 )
618 x += offset[0]
619 y += offset[1]
620 except AttributeError:
621 try:
622 mask = image_text.font.getmask( # type: ignore[misc]
623 line.text,
624 mode,
625 direction,
626 features,
627 language,
628 stroke_width,
629 line.anchor,
630 ink,
631 start=start,
632 *args,
633 **kwargs,
634 )
635 except TypeError:
636 mask = image_text.font.getmask(line.text)
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 if self.im is not None:
645 self.im.paste(
646 color, (x, y, x + mask.size[0], y + mask.size[1]), mask
647 )
648 else:
649 self.draw.draw_bitmap((x, y), mask, ink)
651 if stroke_ink is not None:
652 # Draw stroked text
653 draw_text(stroke_ink, image_text.stroke_width)
655 # Draw normal text
656 if ink != stroke_ink:
657 draw_text(ink)
658 else:
659 # Only draw normal text
660 draw_text(ink)
662 def multiline_text(
663 self,
664 xy: tuple[float, float],
665 text: AnyStr,
666 fill: _Ink | None = None,
667 font: (
668 ImageFont.ImageFont
669 | ImageFont.FreeTypeFont
670 | ImageFont.TransposedFont
671 | None
672 ) = None,
673 anchor: str | None = None,
674 spacing: float = 4,
675 align: str = "left",
676 direction: str | None = None,
677 features: list[str] | None = None,
678 language: str | None = None,
679 stroke_width: float = 0,
680 stroke_fill: _Ink | None = None,
681 embedded_color: bool = False,
682 *,
683 font_size: float | None = None,
684 ) -> None:
685 return self.text(
686 xy,
687 text,
688 fill,
689 font,
690 anchor,
691 spacing,
692 align,
693 direction,
694 features,
695 language,
696 stroke_width,
697 stroke_fill,
698 embedded_color,
699 font_size=font_size,
700 )
702 def textlength(
703 self,
704 text: AnyStr,
705 font: (
706 ImageFont.ImageFont
707 | ImageFont.FreeTypeFont
708 | ImageFont.TransposedFont
709 | None
710 ) = None,
711 direction: str | None = None,
712 features: list[str] | None = None,
713 language: str | None = None,
714 embedded_color: bool = False,
715 *,
716 font_size: float | None = None,
717 ) -> float:
718 """Get the length of a given string, in pixels with 1/64 precision."""
719 if font is None:
720 font = self._getfont(font_size)
721 image_text = ImageText.Text(
722 text,
723 font,
724 self.mode,
725 direction=direction,
726 features=features,
727 language=language,
728 )
729 if embedded_color:
730 image_text.embed_color()
731 return image_text.get_length()
733 def textbbox(
734 self,
735 xy: tuple[float, float],
736 text: AnyStr,
737 font: (
738 ImageFont.ImageFont
739 | ImageFont.FreeTypeFont
740 | ImageFont.TransposedFont
741 | None
742 ) = 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 """Get the bounding box of a given string, in pixels."""
755 if font is None:
756 font = self._getfont(font_size)
757 image_text = ImageText.Text(
758 text, font, self.mode, spacing, direction, features, language
759 )
760 if embedded_color:
761 image_text.embed_color()
762 if stroke_width:
763 image_text.stroke(stroke_width)
764 return image_text.get_bbox(xy, anchor, align)
766 def multiline_textbbox(
767 self,
768 xy: tuple[float, float],
769 text: AnyStr,
770 font: (
771 ImageFont.ImageFont
772 | ImageFont.FreeTypeFont
773 | ImageFont.TransposedFont
774 | None
775 ) = None,
776 anchor: str | None = None,
777 spacing: float = 4,
778 align: str = "left",
779 direction: str | None = None,
780 features: list[str] | None = None,
781 language: str | None = None,
782 stroke_width: float = 0,
783 embedded_color: bool = False,
784 *,
785 font_size: float | None = None,
786 ) -> tuple[float, float, float, float]:
787 return self.textbbox(
788 xy,
789 text,
790 font,
791 anchor,
792 spacing,
793 align,
794 direction,
795 features,
796 language,
797 stroke_width,
798 embedded_color,
799 font_size=font_size,
800 )
803def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
804 """
805 A simple 2D drawing interface for PIL images.
807 :param im: The image to draw in.
808 :param mode: Optional mode to use for color values. For RGB
809 images, this argument can be RGB or RGBA (to blend the
810 drawing into the image). For all other modes, this argument
811 must be the same as the image mode. If omitted, the mode
812 defaults to the mode of the image.
813 """
814 try:
815 return getattr(im, "getdraw")(mode)
816 except AttributeError:
817 return ImageDraw(im, mode)
820def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]:
821 """
822 :param im: The image to draw in.
823 :returns: A (drawing context, drawing resource factory) tuple.
824 """
825 from . import ImageDraw2
827 draw = ImageDraw2.Draw(im) if im is not None else None
828 return draw, ImageDraw2
831def floodfill(
832 image: Image.Image,
833 xy: tuple[int, int],
834 value: float | tuple[int, ...],
835 border: float | tuple[int, ...] | None = None,
836 thresh: float = 0,
837) -> None:
838 """
839 .. warning:: This method is experimental.
841 Fills a bounded region with a given color.
843 :param image: Target image.
844 :param xy: Seed position (a 2-item coordinate tuple). See
845 :ref:`coordinate-system`.
846 :param value: Fill color.
847 :param border: Optional border value. If given, the region consists of
848 pixels with a color different from the border color. If not given,
849 the region consists of pixels having the same color as the seed
850 pixel.
851 :param thresh: Optional threshold value which specifies a maximum
852 tolerable difference of a pixel value from the 'background' in
853 order for it to be replaced. Useful for filling regions of
854 non-homogeneous, but similar, colors.
855 """
856 # based on an implementation by Eric S. Raymond
857 # amended by yo1995 @20180806
858 pixel = image.load()
859 assert pixel is not None
860 x, y = xy
861 try:
862 background = pixel[x, y]
863 if _color_diff(value, background) <= thresh:
864 return # seed point already has fill color
865 pixel[x, y] = value
866 except (ValueError, IndexError):
867 return # seed point outside image
868 edge = {(x, y)}
869 # use a set to keep record of current and previous edge pixels
870 # to reduce memory consumption
871 full_edge = set()
872 while edge:
873 new_edge = set()
874 for x, y in edge: # 4 adjacent method
875 for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
876 # If already processed, or if a coordinate is negative, skip
877 if (s, t) in full_edge or s < 0 or t < 0:
878 continue
879 try:
880 p = pixel[s, t]
881 except (ValueError, IndexError):
882 pass
883 else:
884 full_edge.add((s, t))
885 if border is None:
886 fill = _color_diff(p, background) <= thresh
887 else:
888 fill = p not in (value, border)
889 if fill:
890 pixel[s, t] = value
891 new_edge.add((s, t))
892 full_edge = edge # discard pixels processed
893 edge = new_edge
896def _compute_regular_polygon_vertices(
897 bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float
898) -> list[tuple[float, float]]:
899 """
900 Generate a list of vertices for a 2D regular polygon.
902 :param bounding_circle: The bounding circle is a sequence defined
903 by a point and radius. The polygon is inscribed in this circle.
904 (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
905 :param n_sides: Number of sides
906 (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon)
907 :param rotation: Apply an arbitrary rotation to the polygon
908 (e.g. ``rotation=90``, applies a 90 degree rotation)
909 :return: List of regular polygon vertices
910 (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``)
912 How are the vertices computed?
913 1. Compute the following variables
914 - theta: Angle between the apothem & the nearest polygon vertex
915 - side_length: Length of each polygon edge
916 - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle)
917 - polygon_radius: Polygon radius (last element of bounding_circle)
918 - angles: Location of each polygon vertex in polar grid
919 (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0])
921 2. For each angle in angles, get the polygon vertex at that angle
922 The vertex is computed using the equation below.
923 X= xcos(φ) + ysin(φ)
924 Y= −xsin(φ) + ycos(φ)
926 Note:
927 φ = angle in degrees
928 x = 0
929 y = polygon_radius
931 The formula above assumes rotation around the origin.
932 In our case, we are rotating around the centroid.
933 To account for this, we use the formula below
934 X = xcos(φ) + ysin(φ) + centroid_x
935 Y = −xsin(φ) + ycos(φ) + centroid_y
936 """
937 # 1. Error Handling
938 # 1.1 Check `n_sides` has an appropriate value
939 if not isinstance(n_sides, int):
940 msg = "n_sides should be an int" # type: ignore[unreachable]
941 raise TypeError(msg)
942 if n_sides < 3:
943 msg = "n_sides should be an int > 2"
944 raise ValueError(msg)
946 # 1.2 Check `bounding_circle` has an appropriate value
947 if not isinstance(bounding_circle, (list, tuple)):
948 msg = "bounding_circle should be a sequence"
949 raise TypeError(msg)
951 if len(bounding_circle) == 3:
952 if not all(isinstance(i, (int, float)) for i in bounding_circle):
953 msg = "bounding_circle should only contain numeric data"
954 raise ValueError(msg)
956 *centroid, polygon_radius = cast(list[float], list(bounding_circle))
957 elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
958 if not all(
959 isinstance(i, (int, float)) for i in bounding_circle[0]
960 ) or not isinstance(bounding_circle[1], (int, float)):
961 msg = "bounding_circle should only contain numeric data"
962 raise ValueError(msg)
964 if len(bounding_circle[0]) != 2:
965 msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
966 raise ValueError(msg)
968 centroid = cast(list[float], list(bounding_circle[0]))
969 polygon_radius = cast(float, bounding_circle[1])
970 else:
971 msg = (
972 "bounding_circle should contain 2D coordinates "
973 "and a radius (e.g. (x, y, r) or ((x, y), r) )"
974 )
975 raise ValueError(msg)
977 if polygon_radius <= 0:
978 msg = "bounding_circle radius should be > 0"
979 raise ValueError(msg)
981 # 1.3 Check `rotation` has an appropriate value
982 if not isinstance(rotation, (int, float)):
983 msg = "rotation should be an int or float" # type: ignore[unreachable]
984 raise ValueError(msg)
986 # 2. Define Helper Functions
987 def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
988 return (
989 round(
990 point[0] * math.cos(math.radians(360 - degrees))
991 - point[1] * math.sin(math.radians(360 - degrees))
992 + centroid[0],
993 2,
994 ),
995 round(
996 point[1] * math.cos(math.radians(360 - degrees))
997 + point[0] * math.sin(math.radians(360 - degrees))
998 + centroid[1],
999 2,
1000 ),
1001 )
1003 def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
1004 start_point = [polygon_radius, 0]
1005 return _apply_rotation(start_point, angle)
1007 def _get_angles(n_sides: int, rotation: float) -> list[float]:
1008 angles = []
1009 degrees = 360 / n_sides
1010 # Start with the bottom left polygon vertex
1011 current_angle = (270 - 0.5 * degrees) + rotation
1012 for _ in range(n_sides):
1013 angles.append(current_angle)
1014 current_angle += degrees
1015 if current_angle > 360:
1016 current_angle -= 360
1017 return angles
1019 # 3. Variable Declarations
1020 angles = _get_angles(n_sides, rotation)
1022 # 4. Compute Vertices
1023 return [_compute_polygon_vertex(angle) for angle in angles]
1026def _color_diff(
1027 color1: float | tuple[int, ...], color2: float | tuple[int, ...]
1028) -> float:
1029 """
1030 Uses 1-norm distance to calculate difference between two values.
1031 """
1032 first = color1 if isinstance(color1, tuple) else (color1,)
1033 second = color2 if isinstance(color2, tuple) else (color2,)
1035 return sum(abs(first[i] - second[i]) for i in range(len(second)))