Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
ImageDraw.py1036 linesDownload Raw Back to PIL
1#
2# The Python Imaging Library
3# $Id$
4#
5# drawing interface operations
6#
7# History:
8# 1996-04-13 fl   Created (experimental)
9# 1996-08-07 fl   Filled polygons, ellipses.
10# 1996-08-13 fl   Added text support
11# 1998-06-28 fl   Handle I and F images
12# 1998-12-29 fl   Added arc; use arc primitive to draw ellipses
13# 1999-01-10 fl   Added shape stuff (experimental)
14# 1999-02-06 fl   Added bitmap support
15# 1999-02-11 fl   Changed all primitives to take options
16# 1999-02-20 fl   Fixed backwards compatibility
17# 2000-10-12 fl   Copy on write, when necessary
18# 2001-02-18 fl   Use default ink for bitmap/text also in fill mode
19# 2002-10-24 fl   Added support for CSS-style color strings
20# 2002-12-10 fl   Added experimental support for RGBA-on-RGB drawing
21# 2002-12-11 fl   Refactored low-level drawing API (work in progress)
22# 2004-08-26 fl   Made Draw() a factory function, added getdraw() support
23# 2004-09-04 fl   Added width support to line primitive
24# 2004-09-10 fl   Added font mode handling
25# 2006-06-19 fl   Added font bearing support (getmask2)
26#
27# Copyright (c) 1997-2006 by Secret Labs AB
28# Copyright (c) 1996-2006 by Fredrik Lundh
29#
30# See the README file for information on usage and redistribution.
31#
32from __future__ import annotations
33
34import math
35import struct
36from collections.abc import Sequence
37from typing import cast
38
39from . import Image, ImageColor, ImageText
40
41TYPE_CHECKING = False
42if TYPE_CHECKING:
43    from collections.abc import Callable
44    from types import ModuleType
45    from typing import Any, AnyStr
46
47    from . import ImageDraw2, ImageFont
48    from ._typing import Coords, _Ink
49
50# experimental access to the outline API
51Outline: Callable[[], Image.core._Outline] = Image.core.outline
52
53"""
54A simple 2D drawing interface for PIL images.
55<p>
56Application code should use the <b>Draw</b> factory, instead of
57directly.
58"""
59
60
61class ImageDraw:
62    font: (
63        ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None
64    ) = None
65
66    def __init__(self, im: Image.Image, mode: str | None = None) -> None:
67        """
68        Create a drawing instance.
69
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
105
106    def getfont(
107        self,
108    ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
109        """
110        Get the current default font.
111
112        To set the default font for this ImageDraw instance::
113
114            from PIL import ImageDraw, ImageFont
115            draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
116
117        To set the default font for all future ImageDraw instances::
118
119            from PIL import ImageDraw, ImageFont
120            ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
121
122        If the current default font is ``None``,
123        it is initialized with ``ImageFont.load_default()``.
124
125        :returns: An image font."""
126        if not self.font:
127            # FIXME: should add a font repository
128            from . import ImageFont
129
130            self.font = ImageFont.load_default()
131        return self.font
132
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
138
139            return ImageFont.load_default(font_size)
140        else:
141            return self.getfont()
142
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
167
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)
180
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)
191
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)
207
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)
221
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)
233
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
267
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                        )
281
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)
294
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)
310
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)
324
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)
340
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)
346
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)
368
369                self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im)
370
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)
383
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)
397
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)
421
422        d = radius * 2
423
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)
441
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)
447
448        r = int(d // 2)
449        ink, fill_ink = self._getink(outline, fill)
450
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)))
484
485        if fill_ink is not None:
486            draw_corners(True)
487
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)
499
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)
508
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)
516
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)
530
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)
537
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)
574
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
581
582        ink = getink(fill)
583        if ink is None:
584            return
585
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            )
593
594        for line in image_text._split(xy, anchor, align):
595
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)
650
651            if stroke_ink is not None:
652                # Draw stroked text
653                draw_text(stroke_ink, image_text.stroke_width)
654
655                # Draw normal text
656                if ink != stroke_ink:
657                    draw_text(ink)
658            else:
659                # Only draw normal text
660                draw_text(ink)
661
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        )
701
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()
732
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)
765
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        )
801
802
803def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
804    """
805    A simple 2D drawing interface for PIL images.
806
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)
818
819
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
826
827    draw = ImageDraw2.Draw(im) if im is not None else None
828    return draw, ImageDraw2
829
830
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.
840
841    Fills a bounded region with a given color.
842
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
894
895
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.
901
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)]``)
911
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])
920
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(φ)
925
926        Note:
927            φ = angle in degrees
928            x = 0
929            y = polygon_radius
930
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)
945
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)
950
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)
955
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)
963
964        if len(bounding_circle[0]) != 2:
965            msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
966            raise ValueError(msg)
967
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)
976
977    if polygon_radius <= 0:
978        msg = "bounding_circle radius should be > 0"
979        raise ValueError(msg)
980
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)
985
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        )
1002
1003    def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
1004        start_point = [polygon_radius, 0]
1005        return _apply_rotation(start_point, angle)
1006
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
1018
1019    # 3. Variable Declarations
1020    angles = _get_angles(n_sides, rotation)
1021
1022    # 4. Compute Vertices
1023    return [_compute_polygon_vertex(angle) for angle in angles]
1024
1025
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,)
1034
1035    return sum(abs(first[i] - second[i]) for i in range(len(second)))
1036 
codekingpro/portable-devtools · Team Ai