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:
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 = 0,
239 joint: str | None = None,
240 ) -> None:
241 """Draw a line, or a connected sequence of line segments."""
242 ink = self._getink(fill)[0]
243 if ink is not None:
244 self.draw.draw_lines(xy, ink, width)
245 if joint == "curve" and width > 4:
246 points: Sequence[Sequence[float]]
247 if isinstance(xy[0], (list, tuple)):
248 points = cast(Sequence[Sequence[float]], xy)
249 else:
250 points = [
251 cast(Sequence[float], tuple(xy[i : i + 2]))
252 for i in range(0, len(xy), 2)
253 ]
254 for i in range(1, len(points) - 1):
255 point = points[i]
256 angles = [
257 math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
258 % 360
259 for start, end in (
260 (points[i - 1], point),
261 (point, points[i + 1]),
262 )
263 ]
264 if angles[0] == angles[1]:
265 # This is a straight line, so no joint is required
266 continue
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,
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 xy, anchor, 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 coord = []
601 for i in range(2):
602 coord.append(int(xy[i]))
603 start = (math.modf(xy[0])[0], math.modf(xy[1])[0])
604 try:
605 mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc]
606 line,
607 mode,
608 direction=direction,
609 features=features,
610 language=language,
611 stroke_width=stroke_width,
612 stroke_filled=True,
613 anchor=anchor,
614 ink=ink,
615 start=start,
616 *args,
617 **kwargs,
618 )
619 coord = [coord[0] + offset[0], coord[1] + offset[1]]
620 except AttributeError:
621 try:
622 mask = image_text.font.getmask( # type: ignore[misc]
623 line,
624 mode,
625 direction,
626 features,
627 language,
628 stroke_width,
629 anchor,
630 ink,
631 start=start,
632 *args,
633 **kwargs,
634 )
635 except TypeError:
636 mask = image_text.font.getmask(line)
637 if mode == "RGBA":
638 # image_text.font.getmask2(mode="RGBA")
639 # returns color in RGB bands and mask in A
640 # extract mask and set text alpha
641 color, mask = mask, mask.getband(3)
642 ink_alpha = struct.pack("i", ink)[3]
643 color.fillband(3, ink_alpha)
644 x, y = coord
645 if self.im is not None:
646 self.im.paste(
647 color, (x, y, x + mask.size[0], y + mask.size[1]), mask
648 )
649 else:
650 self.draw.draw_bitmap(coord, mask, ink)
652 if stroke_ink is not None:
653 # Draw stroked text
654 draw_text(stroke_ink, image_text.stroke_width)
656 # Draw normal text
657 if ink != stroke_ink:
658 draw_text(ink)
659 else:
660 # Only draw normal text
661 draw_text(ink)
663 def multiline_text(
664 self,
665 xy: tuple[float, float],
666 text: AnyStr,
667 fill: _Ink | None = None,
668 font: (
669 ImageFont.ImageFont
670 | ImageFont.FreeTypeFont
671 | ImageFont.TransposedFont
672 | None
673 ) = None,
674 anchor: str | None = None,
675 spacing: float = 4,
676 align: str = "left",
677 direction: str | None = None,
678 features: list[str] | None = None,
679 language: str | None = None,
680 stroke_width: float = 0,
681 stroke_fill: _Ink | None = None,
682 embedded_color: bool = False,
683 *,
684 font_size: float | None = None,
685 ) -> None:
686 return self.text(
687 xy,
688 text,
689 fill,
690 font,
691 anchor,
692 spacing,
693 align,
694 direction,
695 features,
696 language,
697 stroke_width,
698 stroke_fill,
699 embedded_color,
700 font_size=font_size,
701 )
703 def textlength(
704 self,
705 text: AnyStr,
706 font: (
707 ImageFont.ImageFont
708 | ImageFont.FreeTypeFont
709 | ImageFont.TransposedFont
710 | None
711 ) = None,
712 direction: str | None = None,
713 features: list[str] | None = None,
714 language: str | None = None,
715 embedded_color: bool = False,
716 *,
717 font_size: float | None = None,
718 ) -> float:
719 """Get the length of a given string, in pixels with 1/64 precision."""
720 if font is None:
721 font = self._getfont(font_size)
722 image_text = ImageText.Text(
723 text,
724 font,
725 self.mode,
726 direction=direction,
727 features=features,
728 language=language,
729 )
730 if embedded_color:
731 image_text.embed_color()
732 return image_text.get_length()
734 def textbbox(
735 self,
736 xy: tuple[float, float],
737 text: AnyStr,
738 font: (
739 ImageFont.ImageFont
740 | ImageFont.FreeTypeFont
741 | ImageFont.TransposedFont
742 | None
743 ) = None,
744 anchor: str | None = None,
745 spacing: float = 4,
746 align: str = "left",
747 direction: str | None = None,
748 features: list[str] | None = None,
749 language: str | None = None,
750 stroke_width: float = 0,
751 embedded_color: bool = False,
752 *,
753 font_size: float | None = None,
754 ) -> tuple[float, float, float, float]:
755 """Get the bounding box of a given string, in pixels."""
756 if font is None:
757 font = self._getfont(font_size)
758 image_text = ImageText.Text(
759 text, font, self.mode, spacing, direction, features, language
760 )
761 if embedded_color:
762 image_text.embed_color()
763 if stroke_width:
764 image_text.stroke(stroke_width)
765 return image_text.get_bbox(xy, anchor, align)
767 def multiline_textbbox(
768 self,
769 xy: tuple[float, float],
770 text: AnyStr,
771 font: (
772 ImageFont.ImageFont
773 | ImageFont.FreeTypeFont
774 | ImageFont.TransposedFont
775 | None
776 ) = None,
777 anchor: str | None = None,
778 spacing: float = 4,
779 align: str = "left",
780 direction: str | None = None,
781 features: list[str] | None = None,
782 language: str | None = None,
783 stroke_width: float = 0,
784 embedded_color: bool = False,
785 *,
786 font_size: float | None = None,
787 ) -> tuple[float, float, float, float]:
788 return self.textbbox(
789 xy,
790 text,
791 font,
792 anchor,
793 spacing,
794 align,
795 direction,
796 features,
797 language,
798 stroke_width,
799 embedded_color,
800 font_size=font_size,
801 )
804def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
805 """
806 A simple 2D drawing interface for PIL images.
808 :param im: The image to draw in.
809 :param mode: Optional mode to use for color values. For RGB
810 images, this argument can be RGB or RGBA (to blend the
811 drawing into the image). For all other modes, this argument
812 must be the same as the image mode. If omitted, the mode
813 defaults to the mode of the image.
814 """
815 try:
816 return getattr(im, "getdraw")(mode)
817 except AttributeError:
818 return ImageDraw(im, mode)
821def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]:
822 """
823 :param im: The image to draw in.
824 :returns: A (drawing context, drawing resource factory) tuple.
825 """
826 from . import ImageDraw2
828 draw = ImageDraw2.Draw(im) if im is not None else None
829 return draw, ImageDraw2
832def floodfill(
833 image: Image.Image,
834 xy: tuple[int, int],
835 value: float | tuple[int, ...],
836 border: float | tuple[int, ...] | None = None,
837 thresh: float = 0,
838) -> None:
839 """
840 .. warning:: This method is experimental.
842 Fills a bounded region with a given color.
844 :param image: Target image.
845 :param xy: Seed position (a 2-item coordinate tuple). See
846 :ref:`coordinate-system`.
847 :param value: Fill color.
848 :param border: Optional border value. If given, the region consists of
849 pixels with a color different from the border color. If not given,
850 the region consists of pixels having the same color as the seed
851 pixel.
852 :param thresh: Optional threshold value which specifies a maximum
853 tolerable difference of a pixel value from the 'background' in
854 order for it to be replaced. Useful for filling regions of
855 non-homogeneous, but similar, colors.
856 """
857 # based on an implementation by Eric S. Raymond
858 # amended by yo1995 @20180806
859 pixel = image.load()
860 assert pixel is not None
861 x, y = xy
862 try:
863 background = pixel[x, y]
864 if _color_diff(value, background) <= thresh:
865 return # seed point already has fill color
866 pixel[x, y] = value
867 except (ValueError, IndexError):
868 return # seed point outside image
869 edge = {(x, y)}
870 # use a set to keep record of current and previous edge pixels
871 # to reduce memory consumption
872 full_edge = set()
873 while edge:
874 new_edge = set()
875 for x, y in edge: # 4 adjacent method
876 for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
877 # If already processed, or if a coordinate is negative, skip
878 if (s, t) in full_edge or s < 0 or t < 0:
879 continue
880 try:
881 p = pixel[s, t]
882 except (ValueError, IndexError):
883 pass
884 else:
885 full_edge.add((s, t))
886 if border is None:
887 fill = _color_diff(p, background) <= thresh
888 else:
889 fill = p not in (value, border)
890 if fill:
891 pixel[s, t] = value
892 new_edge.add((s, t))
893 full_edge = edge # discard pixels processed
894 edge = new_edge
897def _compute_regular_polygon_vertices(
898 bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float
899) -> list[tuple[float, float]]:
900 """
901 Generate a list of vertices for a 2D regular polygon.
903 :param bounding_circle: The bounding circle is a sequence defined
904 by a point and radius. The polygon is inscribed in this circle.
905 (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
906 :param n_sides: Number of sides
907 (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon)
908 :param rotation: Apply an arbitrary rotation to the polygon
909 (e.g. ``rotation=90``, applies a 90 degree rotation)
910 :return: List of regular polygon vertices
911 (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``)
913 How are the vertices computed?
914 1. Compute the following variables
915 - theta: Angle between the apothem & the nearest polygon vertex
916 - side_length: Length of each polygon edge
917 - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle)
918 - polygon_radius: Polygon radius (last element of bounding_circle)
919 - angles: Location of each polygon vertex in polar grid
920 (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0])
922 2. For each angle in angles, get the polygon vertex at that angle
923 The vertex is computed using the equation below.
924 X= xcos(φ) + ysin(φ)
925 Y= −xsin(φ) + ycos(φ)
927 Note:
928 φ = angle in degrees
929 x = 0
930 y = polygon_radius
932 The formula above assumes rotation around the origin.
933 In our case, we are rotating around the centroid.
934 To account for this, we use the formula below
935 X = xcos(φ) + ysin(φ) + centroid_x
936 Y = −xsin(φ) + ycos(φ) + centroid_y
937 """
938 # 1. Error Handling
939 # 1.1 Check `n_sides` has an appropriate value
940 if not isinstance(n_sides, int):
941 msg = "n_sides should be an int" # type: ignore[unreachable]
942 raise TypeError(msg)
943 if n_sides < 3:
944 msg = "n_sides should be an int > 2"
945 raise ValueError(msg)
947 # 1.2 Check `bounding_circle` has an appropriate value
948 if not isinstance(bounding_circle, (list, tuple)):
949 msg = "bounding_circle should be a sequence"
950 raise TypeError(msg)
952 if len(bounding_circle) == 3:
953 if not all(isinstance(i, (int, float)) for i in bounding_circle):
954 msg = "bounding_circle should only contain numeric data"
955 raise ValueError(msg)
957 *centroid, polygon_radius = cast(list[float], list(bounding_circle))
958 elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
959 if not all(
960 isinstance(i, (int, float)) for i in bounding_circle[0]
961 ) or not isinstance(bounding_circle[1], (int, float)):
962 msg = "bounding_circle should only contain numeric data"
963 raise ValueError(msg)
965 if len(bounding_circle[0]) != 2:
966 msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
967 raise ValueError(msg)
969 centroid = cast(list[float], list(bounding_circle[0]))
970 polygon_radius = cast(float, bounding_circle[1])
971 else:
972 msg = (
973 "bounding_circle should contain 2D coordinates "
974 "and a radius (e.g. (x, y, r) or ((x, y), r) )"
975 )
976 raise ValueError(msg)
978 if polygon_radius <= 0:
979 msg = "bounding_circle radius should be > 0"
980 raise ValueError(msg)
982 # 1.3 Check `rotation` has an appropriate value
983 if not isinstance(rotation, (int, float)):
984 msg = "rotation should be an int or float" # type: ignore[unreachable]
985 raise ValueError(msg)
987 # 2. Define Helper Functions
988 def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
989 return (
990 round(
991 point[0] * math.cos(math.radians(360 - degrees))
992 - point[1] * math.sin(math.radians(360 - degrees))
993 + centroid[0],
994 2,
995 ),
996 round(
997 point[1] * math.cos(math.radians(360 - degrees))
998 + point[0] * math.sin(math.radians(360 - degrees))
999 + centroid[1],
1000 2,
1001 ),
1002 )
1004 def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
1005 start_point = [polygon_radius, 0]
1006 return _apply_rotation(start_point, angle)
1008 def _get_angles(n_sides: int, rotation: float) -> list[float]:
1009 angles = []
1010 degrees = 360 / n_sides
1011 # Start with the bottom left polygon vertex
1012 current_angle = (270 - 0.5 * degrees) + rotation
1013 for _ in range(n_sides):
1014 angles.append(current_angle)
1015 current_angle += degrees
1016 if current_angle > 360:
1017 current_angle -= 360
1018 return angles
1020 # 3. Variable Declarations
1021 angles = _get_angles(n_sides, rotation)
1023 # 4. Compute Vertices
1024 return [_compute_polygon_vertex(angle) for angle in angles]
1027def _color_diff(
1028 color1: float | tuple[int, ...], color2: float | tuple[int, ...]
1029) -> float:
1030 """
1031 Uses 1-norm distance to calculate difference between two values.
1032 """
1033 first = color1 if isinstance(color1, tuple) else (color1,)
1034 second = color2 if isinstance(color2, tuple) else (color2,)
1036 return sum(abs(first[i] - second[i]) for i in range(len(second)))