Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/ImageDraw.py: 12%
474 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
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#
33import math
34import numbers
35import warnings
37from . import Image, ImageColor
38from ._deprecate import deprecate
40"""
41A simple 2D drawing interface for PIL images.
42<p>
43Application code should use the <b>Draw</b> factory, instead of
44directly.
45"""
48class ImageDraw:
49 def __init__(self, im, mode=None):
50 """
51 Create a drawing instance.
53 :param im: The image to draw in.
54 :param mode: Optional mode to use for color values. For RGB
55 images, this argument can be RGB or RGBA (to blend the
56 drawing into the image). For all other modes, this argument
57 must be the same as the image mode. If omitted, the mode
58 defaults to the mode of the image.
59 """
60 im.load()
61 if im.readonly: 61 ↛ 62line 61 didn't jump to line 62, because the condition on line 61 was never true
62 im._copy() # make it writeable
63 blend = 0
64 if mode is None: 64 ↛ 66line 64 didn't jump to line 66, because the condition on line 64 was never false
65 mode = im.mode
66 if mode != im.mode: 66 ↛ 67line 66 didn't jump to line 67, because the condition on line 66 was never true
67 if mode == "RGBA" and im.mode == "RGB":
68 blend = 1
69 else:
70 raise ValueError("mode mismatch")
71 if mode == "P": 71 ↛ 72line 71 didn't jump to line 72, because the condition on line 71 was never true
72 self.palette = im.palette
73 else:
74 self.palette = None
75 self._image = im
76 self.im = im.im
77 self.draw = Image.core.draw(self.im, blend)
78 self.mode = mode
79 if mode in ("I", "F"): 79 ↛ 80line 79 didn't jump to line 80, because the condition on line 79 was never true
80 self.ink = self.draw.draw_ink(1)
81 else:
82 self.ink = self.draw.draw_ink(-1)
83 if mode in ("1", "P", "I", "F"):
84 # FIXME: fix Fill2 to properly support matte for I+F images
85 self.fontmode = "1"
86 else:
87 self.fontmode = "L" # aliasing is okay for other modes
88 self.fill = 0
89 self.font = None
91 def getfont(self):
92 """
93 Get the current default font.
95 :returns: An image font."""
96 if not self.font:
97 # FIXME: should add a font repository
98 from . import ImageFont
100 self.font = ImageFont.load_default()
101 return self.font
103 def _getink(self, ink, fill=None):
104 if ink is None and fill is None: 104 ↛ 105line 104 didn't jump to line 105, because the condition on line 104 was never true
105 if self.fill:
106 fill = self.ink
107 else:
108 ink = self.ink
109 else:
110 if ink is not None: 110 ↛ 111line 110 didn't jump to line 111, because the condition on line 110 was never true
111 if isinstance(ink, str):
112 ink = ImageColor.getcolor(ink, self.mode)
113 if self.palette and not isinstance(ink, numbers.Number):
114 ink = self.palette.getcolor(ink, self._image)
115 ink = self.draw.draw_ink(ink)
116 if fill is not None: 116 ↛ 122line 116 didn't jump to line 122, because the condition on line 116 was never false
117 if isinstance(fill, str): 117 ↛ 118line 117 didn't jump to line 118, because the condition on line 117 was never true
118 fill = ImageColor.getcolor(fill, self.mode)
119 if self.palette and not isinstance(fill, numbers.Number): 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true
120 fill = self.palette.getcolor(fill, self._image)
121 fill = self.draw.draw_ink(fill)
122 return ink, fill
124 def arc(self, xy, start, end, fill=None, width=1):
125 """Draw an arc."""
126 ink, fill = self._getink(fill)
127 if ink is not None:
128 self.draw.draw_arc(xy, start, end, ink, width)
130 def bitmap(self, xy, bitmap, fill=None):
131 """Draw a bitmap."""
132 bitmap.load()
133 ink, fill = self._getink(fill)
134 if ink is None:
135 ink = fill
136 if ink is not None:
137 self.draw.draw_bitmap(xy, bitmap.im, ink)
139 def chord(self, xy, start, end, fill=None, outline=None, width=1):
140 """Draw a chord."""
141 ink, fill = self._getink(outline, fill)
142 if fill is not None:
143 self.draw.draw_chord(xy, start, end, fill, 1)
144 if ink is not None and ink != fill and width != 0:
145 self.draw.draw_chord(xy, start, end, ink, 0, width)
147 def ellipse(self, xy, fill=None, outline=None, width=1):
148 """Draw an ellipse."""
149 ink, fill = self._getink(outline, fill)
150 if fill is not None:
151 self.draw.draw_ellipse(xy, fill, 1)
152 if ink is not None and ink != fill and width != 0:
153 self.draw.draw_ellipse(xy, ink, 0, width)
155 def line(self, xy, fill=None, width=0, joint=None):
156 """Draw a line, or a connected sequence of line segments."""
157 ink = self._getink(fill)[0]
158 if ink is not None:
159 self.draw.draw_lines(xy, ink, width)
160 if joint == "curve" and width > 4:
161 if not isinstance(xy[0], (list, tuple)):
162 xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)]
163 for i in range(1, len(xy) - 1):
164 point = xy[i]
165 angles = [
166 math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
167 % 360
168 for start, end in ((xy[i - 1], point), (point, xy[i + 1]))
169 ]
170 if angles[0] == angles[1]:
171 # This is a straight line, so no joint is required
172 continue
174 def coord_at_angle(coord, angle):
175 x, y = coord
176 angle -= 90
177 distance = width / 2 - 1
178 return tuple(
179 p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d))
180 for p, p_d in (
181 (x, distance * math.cos(math.radians(angle))),
182 (y, distance * math.sin(math.radians(angle))),
183 )
184 )
186 flipped = (
187 angles[1] > angles[0] and angles[1] - 180 > angles[0]
188 ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0])
189 coords = [
190 (point[0] - width / 2 + 1, point[1] - width / 2 + 1),
191 (point[0] + width / 2 - 1, point[1] + width / 2 - 1),
192 ]
193 if flipped:
194 start, end = (angles[1] + 90, angles[0] + 90)
195 else:
196 start, end = (angles[0] - 90, angles[1] - 90)
197 self.pieslice(coords, start - 90, end - 90, fill)
199 if width > 8:
200 # Cover potential gaps between the line and the joint
201 if flipped:
202 gap_coords = [
203 coord_at_angle(point, angles[0] + 90),
204 point,
205 coord_at_angle(point, angles[1] + 90),
206 ]
207 else:
208 gap_coords = [
209 coord_at_angle(point, angles[0] - 90),
210 point,
211 coord_at_angle(point, angles[1] - 90),
212 ]
213 self.line(gap_coords, fill, width=3)
215 def shape(self, shape, fill=None, outline=None):
216 """(Experimental) Draw a shape."""
217 shape.close()
218 ink, fill = self._getink(outline, fill)
219 if fill is not None:
220 self.draw.draw_outline(shape, fill, 1)
221 if ink is not None and ink != fill:
222 self.draw.draw_outline(shape, ink, 0)
224 def pieslice(self, xy, start, end, fill=None, outline=None, width=1):
225 """Draw a pieslice."""
226 ink, fill = self._getink(outline, fill)
227 if fill is not None:
228 self.draw.draw_pieslice(xy, start, end, fill, 1)
229 if ink is not None and ink != fill and width != 0:
230 self.draw.draw_pieslice(xy, start, end, ink, 0, width)
232 def point(self, xy, fill=None):
233 """Draw one or more individual pixels."""
234 ink, fill = self._getink(fill)
235 if ink is not None:
236 self.draw.draw_points(xy, ink)
238 def polygon(self, xy, fill=None, outline=None, width=1):
239 """Draw a polygon."""
240 ink, fill = self._getink(outline, fill)
241 if fill is not None:
242 self.draw.draw_polygon(xy, fill, 1)
243 if ink is not None and ink != fill and width != 0:
244 if width == 1:
245 self.draw.draw_polygon(xy, ink, 0, width)
246 else:
247 # To avoid expanding the polygon outwards,
248 # use the fill as a mask
249 mask = Image.new("1", self.im.size)
250 mask_ink = self._getink(1)[0]
252 fill_im = mask.copy()
253 draw = Draw(fill_im)
254 draw.draw.draw_polygon(xy, mask_ink, 1)
256 ink_im = mask.copy()
257 draw = Draw(ink_im)
258 width = width * 2 - 1
259 draw.draw.draw_polygon(xy, mask_ink, 0, width)
261 mask.paste(ink_im, mask=fill_im)
263 im = Image.new(self.mode, self.im.size)
264 draw = Draw(im)
265 draw.draw.draw_polygon(xy, ink, 0, width)
266 self.im.paste(im.im, (0, 0) + im.size, mask.im)
268 def regular_polygon(
269 self, bounding_circle, n_sides, rotation=0, fill=None, outline=None
270 ):
271 """Draw a regular polygon."""
272 xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
273 self.polygon(xy, fill, outline)
275 def rectangle(self, xy, fill=None, outline=None, width=1):
276 """Draw a rectangle."""
277 ink, fill = self._getink(outline, fill)
278 if fill is not None: 278 ↛ 280line 278 didn't jump to line 280, because the condition on line 278 was never false
279 self.draw.draw_rectangle(xy, fill, 1)
280 if ink is not None and ink != fill and width != 0: 280 ↛ 281line 280 didn't jump to line 281, because the condition on line 280 was never true
281 self.draw.draw_rectangle(xy, ink, 0, width)
283 def rounded_rectangle(self, xy, radius=0, fill=None, outline=None, width=1):
284 """Draw a rounded rectangle."""
285 if isinstance(xy[0], (list, tuple)):
286 (x0, y0), (x1, y1) = xy
287 else:
288 x0, y0, x1, y1 = xy
290 d = radius * 2
292 full_x = d >= x1 - x0
293 if full_x:
294 # The two left and two right corners are joined
295 d = x1 - x0
296 full_y = d >= y1 - y0
297 if full_y:
298 # The two top and two bottom corners are joined
299 d = y1 - y0
300 if full_x and full_y:
301 # If all corners are joined, that is a circle
302 return self.ellipse(xy, fill, outline, width)
304 if d == 0:
305 # If the corners have no curve, that is a rectangle
306 return self.rectangle(xy, fill, outline, width)
308 r = d // 2
309 ink, fill = self._getink(outline, fill)
311 def draw_corners(pieslice):
312 if full_x:
313 # Draw top and bottom halves
314 parts = (
315 ((x0, y0, x0 + d, y0 + d), 180, 360),
316 ((x0, y1 - d, x0 + d, y1), 0, 180),
317 )
318 elif full_y:
319 # Draw left and right halves
320 parts = (
321 ((x0, y0, x0 + d, y0 + d), 90, 270),
322 ((x1 - d, y0, x1, y0 + d), 270, 90),
323 )
324 else:
325 # Draw four separate corners
326 parts = (
327 ((x1 - d, y0, x1, y0 + d), 270, 360),
328 ((x1 - d, y1 - d, x1, y1), 0, 90),
329 ((x0, y1 - d, x0 + d, y1), 90, 180),
330 ((x0, y0, x0 + d, y0 + d), 180, 270),
331 )
332 for part in parts:
333 if pieslice:
334 self.draw.draw_pieslice(*(part + (fill, 1)))
335 else:
336 self.draw.draw_arc(*(part + (ink, width)))
338 if fill is not None:
339 draw_corners(True)
341 if full_x:
342 self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1)
343 else:
344 self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1)
345 if not full_x and not full_y:
346 self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1)
347 self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1)
348 if ink is not None and ink != fill and width != 0:
349 draw_corners(False)
351 if not full_x:
352 self.draw.draw_rectangle(
353 (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1
354 )
355 self.draw.draw_rectangle(
356 (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1
357 )
358 if not full_y:
359 self.draw.draw_rectangle(
360 (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1
361 )
362 self.draw.draw_rectangle(
363 (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1
364 )
366 def _multiline_check(self, text):
367 """Draw text."""
368 split_character = "\n" if isinstance(text, str) else b"\n"
370 return split_character in text
372 def _multiline_split(self, text):
373 split_character = "\n" if isinstance(text, str) else b"\n"
375 return text.split(split_character)
377 def _multiline_spacing(self, font, spacing, stroke_width):
378 # this can be replaced with self.textbbox(...)[3] when textsize is removed
379 with warnings.catch_warnings():
380 warnings.filterwarnings("ignore", category=DeprecationWarning)
381 return (
382 self.textsize(
383 "A",
384 font=font,
385 stroke_width=stroke_width,
386 )[1]
387 + spacing
388 )
390 def text(
391 self,
392 xy,
393 text,
394 fill=None,
395 font=None,
396 anchor=None,
397 spacing=4,
398 align="left",
399 direction=None,
400 features=None,
401 language=None,
402 stroke_width=0,
403 stroke_fill=None,
404 embedded_color=False,
405 *args,
406 **kwargs,
407 ):
408 if self._multiline_check(text):
409 return self.multiline_text(
410 xy,
411 text,
412 fill,
413 font,
414 anchor,
415 spacing,
416 align,
417 direction,
418 features,
419 language,
420 stroke_width,
421 stroke_fill,
422 embedded_color,
423 )
425 if embedded_color and self.mode not in ("RGB", "RGBA"):
426 raise ValueError("Embedded color supported only in RGB and RGBA modes")
428 if font is None:
429 font = self.getfont()
431 def getink(fill):
432 ink, fill = self._getink(fill)
433 if ink is None:
434 return fill
435 return ink
437 def draw_text(ink, stroke_width=0, stroke_offset=None):
438 mode = self.fontmode
439 if stroke_width == 0 and embedded_color:
440 mode = "RGBA"
441 coord = xy
442 try:
443 mask, offset = font.getmask2(
444 text,
445 mode,
446 direction=direction,
447 features=features,
448 language=language,
449 stroke_width=stroke_width,
450 anchor=anchor,
451 ink=ink,
452 *args,
453 **kwargs,
454 )
455 coord = coord[0] + offset[0], coord[1] + offset[1]
456 except AttributeError:
457 try:
458 mask = font.getmask(
459 text,
460 mode,
461 direction,
462 features,
463 language,
464 stroke_width,
465 anchor,
466 ink,
467 *args,
468 **kwargs,
469 )
470 except TypeError:
471 mask = font.getmask(text)
472 if stroke_offset:
473 coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]
474 if mode == "RGBA":
475 # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
476 # extract mask and set text alpha
477 color, mask = mask, mask.getband(3)
478 color.fillband(3, (ink >> 24) & 0xFF)
479 coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1]
480 self.im.paste(color, coord + coord2, mask)
481 else:
482 self.draw.draw_bitmap(coord, mask, ink)
484 ink = getink(fill)
485 if ink is not None:
486 stroke_ink = None
487 if stroke_width:
488 stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink
490 if stroke_ink is not None:
491 # Draw stroked text
492 draw_text(stroke_ink, stroke_width)
494 # Draw normal text
495 draw_text(ink, 0)
496 else:
497 # Only draw normal text
498 draw_text(ink)
500 def multiline_text(
501 self,
502 xy,
503 text,
504 fill=None,
505 font=None,
506 anchor=None,
507 spacing=4,
508 align="left",
509 direction=None,
510 features=None,
511 language=None,
512 stroke_width=0,
513 stroke_fill=None,
514 embedded_color=False,
515 ):
516 if direction == "ttb":
517 raise ValueError("ttb direction is unsupported for multiline text")
519 if anchor is None:
520 anchor = "la"
521 elif len(anchor) != 2:
522 raise ValueError("anchor must be a 2 character string")
523 elif anchor[1] in "tb":
524 raise ValueError("anchor not supported for multiline text")
526 widths = []
527 max_width = 0
528 lines = self._multiline_split(text)
529 line_spacing = self._multiline_spacing(font, spacing, stroke_width)
530 for line in lines:
531 line_width = self.textlength(
532 line, font, direction=direction, features=features, language=language
533 )
534 widths.append(line_width)
535 max_width = max(max_width, line_width)
537 top = xy[1]
538 if anchor[1] == "m":
539 top -= (len(lines) - 1) * line_spacing / 2.0
540 elif anchor[1] == "d":
541 top -= (len(lines) - 1) * line_spacing
543 for idx, line in enumerate(lines):
544 left = xy[0]
545 width_difference = max_width - widths[idx]
547 # first align left by anchor
548 if anchor[0] == "m":
549 left -= width_difference / 2.0
550 elif anchor[0] == "r":
551 left -= width_difference
553 # then align by align parameter
554 if align == "left":
555 pass
556 elif align == "center":
557 left += width_difference / 2.0
558 elif align == "right":
559 left += width_difference
560 else:
561 raise ValueError('align must be "left", "center" or "right"')
563 self.text(
564 (left, top),
565 line,
566 fill,
567 font,
568 anchor,
569 direction=direction,
570 features=features,
571 language=language,
572 stroke_width=stroke_width,
573 stroke_fill=stroke_fill,
574 embedded_color=embedded_color,
575 )
576 top += line_spacing
578 def textsize(
579 self,
580 text,
581 font=None,
582 spacing=4,
583 direction=None,
584 features=None,
585 language=None,
586 stroke_width=0,
587 ):
588 """Get the size of a given string, in pixels."""
589 deprecate("textsize", 10, "textbbox or textlength")
590 if self._multiline_check(text):
591 with warnings.catch_warnings():
592 warnings.filterwarnings("ignore", category=DeprecationWarning)
593 return self.multiline_textsize(
594 text,
595 font,
596 spacing,
597 direction,
598 features,
599 language,
600 stroke_width,
601 )
603 if font is None:
604 font = self.getfont()
605 with warnings.catch_warnings():
606 warnings.filterwarnings("ignore", category=DeprecationWarning)
607 return font.getsize(
608 text,
609 direction,
610 features,
611 language,
612 stroke_width,
613 )
615 def multiline_textsize(
616 self,
617 text,
618 font=None,
619 spacing=4,
620 direction=None,
621 features=None,
622 language=None,
623 stroke_width=0,
624 ):
625 deprecate("multiline_textsize", 10, "multiline_textbbox")
626 max_width = 0
627 lines = self._multiline_split(text)
628 line_spacing = self._multiline_spacing(font, spacing, stroke_width)
629 with warnings.catch_warnings():
630 warnings.filterwarnings("ignore", category=DeprecationWarning)
631 for line in lines:
632 line_width, line_height = self.textsize(
633 line,
634 font,
635 spacing,
636 direction,
637 features,
638 language,
639 stroke_width,
640 )
641 max_width = max(max_width, line_width)
642 return max_width, len(lines) * line_spacing - spacing
644 def textlength(
645 self,
646 text,
647 font=None,
648 direction=None,
649 features=None,
650 language=None,
651 embedded_color=False,
652 ):
653 """Get the length of a given string, in pixels with 1/64 precision."""
654 if self._multiline_check(text):
655 raise ValueError("can't measure length of multiline text")
656 if embedded_color and self.mode not in ("RGB", "RGBA"):
657 raise ValueError("Embedded color supported only in RGB and RGBA modes")
659 if font is None:
660 font = self.getfont()
661 mode = "RGBA" if embedded_color else self.fontmode
662 try:
663 return font.getlength(text, mode, direction, features, language)
664 except AttributeError:
665 deprecate("textlength support for fonts without getlength", 10)
666 with warnings.catch_warnings():
667 warnings.filterwarnings("ignore", category=DeprecationWarning)
668 size = self.textsize(
669 text,
670 font,
671 direction=direction,
672 features=features,
673 language=language,
674 )
675 if direction == "ttb":
676 return size[1]
677 return size[0]
679 def textbbox(
680 self,
681 xy,
682 text,
683 font=None,
684 anchor=None,
685 spacing=4,
686 align="left",
687 direction=None,
688 features=None,
689 language=None,
690 stroke_width=0,
691 embedded_color=False,
692 ):
693 """Get the bounding box of a given string, in pixels."""
694 if embedded_color and self.mode not in ("RGB", "RGBA"):
695 raise ValueError("Embedded color supported only in RGB and RGBA modes")
697 if self._multiline_check(text):
698 return self.multiline_textbbox(
699 xy,
700 text,
701 font,
702 anchor,
703 spacing,
704 align,
705 direction,
706 features,
707 language,
708 stroke_width,
709 embedded_color,
710 )
712 if font is None:
713 font = self.getfont()
714 mode = "RGBA" if embedded_color else self.fontmode
715 bbox = font.getbbox(
716 text, mode, direction, features, language, stroke_width, anchor
717 )
718 return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1]
720 def multiline_textbbox(
721 self,
722 xy,
723 text,
724 font=None,
725 anchor=None,
726 spacing=4,
727 align="left",
728 direction=None,
729 features=None,
730 language=None,
731 stroke_width=0,
732 embedded_color=False,
733 ):
734 if direction == "ttb":
735 raise ValueError("ttb direction is unsupported for multiline text")
737 if anchor is None:
738 anchor = "la"
739 elif len(anchor) != 2:
740 raise ValueError("anchor must be a 2 character string")
741 elif anchor[1] in "tb":
742 raise ValueError("anchor not supported for multiline text")
744 widths = []
745 max_width = 0
746 lines = self._multiline_split(text)
747 line_spacing = self._multiline_spacing(font, spacing, stroke_width)
748 for line in lines:
749 line_width = self.textlength(
750 line,
751 font,
752 direction=direction,
753 features=features,
754 language=language,
755 embedded_color=embedded_color,
756 )
757 widths.append(line_width)
758 max_width = max(max_width, line_width)
760 top = xy[1]
761 if anchor[1] == "m":
762 top -= (len(lines) - 1) * line_spacing / 2.0
763 elif anchor[1] == "d":
764 top -= (len(lines) - 1) * line_spacing
766 bbox = None
768 for idx, line in enumerate(lines):
769 left = xy[0]
770 width_difference = max_width - widths[idx]
772 # first align left by anchor
773 if anchor[0] == "m":
774 left -= width_difference / 2.0
775 elif anchor[0] == "r":
776 left -= width_difference
778 # then align by align parameter
779 if align == "left":
780 pass
781 elif align == "center":
782 left += width_difference / 2.0
783 elif align == "right":
784 left += width_difference
785 else:
786 raise ValueError('align must be "left", "center" or "right"')
788 bbox_line = self.textbbox(
789 (left, top),
790 line,
791 font,
792 anchor,
793 direction=direction,
794 features=features,
795 language=language,
796 stroke_width=stroke_width,
797 embedded_color=embedded_color,
798 )
799 if bbox is None:
800 bbox = bbox_line
801 else:
802 bbox = (
803 min(bbox[0], bbox_line[0]),
804 min(bbox[1], bbox_line[1]),
805 max(bbox[2], bbox_line[2]),
806 max(bbox[3], bbox_line[3]),
807 )
809 top += line_spacing
811 if bbox is None:
812 return xy[0], xy[1], xy[0], xy[1]
813 return bbox
816def Draw(im, mode=None):
817 """
818 A simple 2D drawing interface for PIL images.
820 :param im: The image to draw in.
821 :param mode: Optional mode to use for color values. For RGB
822 images, this argument can be RGB or RGBA (to blend the
823 drawing into the image). For all other modes, this argument
824 must be the same as the image mode. If omitted, the mode
825 defaults to the mode of the image.
826 """
827 try:
828 return im.getdraw(mode)
829 except AttributeError:
830 return ImageDraw(im, mode)
833# experimental access to the outline API
834try:
835 Outline = Image.core.outline
836except AttributeError:
837 Outline = None
840def getdraw(im=None, hints=None):
841 """
842 (Experimental) A more advanced 2D drawing interface for PIL images,
843 based on the WCK interface.
845 :param im: The image to draw in.
846 :param hints: An optional list of hints.
847 :returns: A (drawing context, drawing resource factory) tuple.
848 """
849 # FIXME: this needs more work!
850 # FIXME: come up with a better 'hints' scheme.
851 handler = None
852 if not hints or "nicest" in hints:
853 try:
854 from . import _imagingagg as handler
855 except ImportError:
856 pass
857 if handler is None:
858 from . import ImageDraw2 as handler
859 if im:
860 im = handler.Draw(im)
861 return im, handler
864def floodfill(image, xy, value, border=None, thresh=0):
865 """
866 (experimental) Fills a bounded region with a given color.
868 :param image: Target image.
869 :param xy: Seed position (a 2-item coordinate tuple). See
870 :ref:`coordinate-system`.
871 :param value: Fill color.
872 :param border: Optional border value. If given, the region consists of
873 pixels with a color different from the border color. If not given,
874 the region consists of pixels having the same color as the seed
875 pixel.
876 :param thresh: Optional threshold value which specifies a maximum
877 tolerable difference of a pixel value from the 'background' in
878 order for it to be replaced. Useful for filling regions of
879 non-homogeneous, but similar, colors.
880 """
881 # based on an implementation by Eric S. Raymond
882 # amended by yo1995 @20180806
883 pixel = image.load()
884 x, y = xy
885 try:
886 background = pixel[x, y]
887 if _color_diff(value, background) <= thresh:
888 return # seed point already has fill color
889 pixel[x, y] = value
890 except (ValueError, IndexError):
891 return # seed point outside image
892 edge = {(x, y)}
893 # use a set to keep record of current and previous edge pixels
894 # to reduce memory consumption
895 full_edge = set()
896 while edge:
897 new_edge = set()
898 for (x, y) in edge: # 4 adjacent method
899 for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
900 # If already processed, or if a coordinate is negative, skip
901 if (s, t) in full_edge or s < 0 or t < 0:
902 continue
903 try:
904 p = pixel[s, t]
905 except (ValueError, IndexError):
906 pass
907 else:
908 full_edge.add((s, t))
909 if border is None:
910 fill = _color_diff(p, background) <= thresh
911 else:
912 fill = p != value and p != border
913 if fill:
914 pixel[s, t] = value
915 new_edge.add((s, t))
916 full_edge = edge # discard pixels processed
917 edge = new_edge
920def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
921 """
922 Generate a list of vertices for a 2D regular polygon.
924 :param bounding_circle: The bounding circle is a tuple defined
925 by a point and radius. The polygon is inscribed in this circle.
926 (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
927 :param n_sides: Number of sides
928 (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon)
929 :param rotation: Apply an arbitrary rotation to the polygon
930 (e.g. ``rotation=90``, applies a 90 degree rotation)
931 :return: List of regular polygon vertices
932 (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``)
934 How are the vertices computed?
935 1. Compute the following variables
936 - theta: Angle between the apothem & the nearest polygon vertex
937 - side_length: Length of each polygon edge
938 - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle)
939 - polygon_radius: Polygon radius (last element of bounding_circle)
940 - angles: Location of each polygon vertex in polar grid
941 (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0])
943 2. For each angle in angles, get the polygon vertex at that angle
944 The vertex is computed using the equation below.
945 X= xcos(φ) + ysin(φ)
946 Y= −xsin(φ) + ycos(φ)
948 Note:
949 φ = angle in degrees
950 x = 0
951 y = polygon_radius
953 The formula above assumes rotation around the origin.
954 In our case, we are rotating around the centroid.
955 To account for this, we use the formula below
956 X = xcos(φ) + ysin(φ) + centroid_x
957 Y = −xsin(φ) + ycos(φ) + centroid_y
958 """
959 # 1. Error Handling
960 # 1.1 Check `n_sides` has an appropriate value
961 if not isinstance(n_sides, int):
962 raise TypeError("n_sides should be an int")
963 if n_sides < 3:
964 raise ValueError("n_sides should be an int > 2")
966 # 1.2 Check `bounding_circle` has an appropriate value
967 if not isinstance(bounding_circle, (list, tuple)):
968 raise TypeError("bounding_circle should be a tuple")
970 if len(bounding_circle) == 3:
971 *centroid, polygon_radius = bounding_circle
972 elif len(bounding_circle) == 2:
973 centroid, polygon_radius = bounding_circle
974 else:
975 raise ValueError(
976 "bounding_circle should contain 2D coordinates "
977 "and a radius (e.g. (x, y, r) or ((x, y), r) )"
978 )
980 if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)):
981 raise ValueError("bounding_circle should only contain numeric data")
983 if not len(centroid) == 2:
984 raise ValueError(
985 "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
986 )
988 if polygon_radius <= 0:
989 raise ValueError("bounding_circle radius should be > 0")
991 # 1.3 Check `rotation` has an appropriate value
992 if not isinstance(rotation, (int, float)):
993 raise ValueError("rotation should be an int or float")
995 # 2. Define Helper Functions
996 def _apply_rotation(point, degrees, centroid):
997 return (
998 round(
999 point[0] * math.cos(math.radians(360 - degrees))
1000 - point[1] * math.sin(math.radians(360 - degrees))
1001 + centroid[0],
1002 2,
1003 ),
1004 round(
1005 point[1] * math.cos(math.radians(360 - degrees))
1006 + point[0] * math.sin(math.radians(360 - degrees))
1007 + centroid[1],
1008 2,
1009 ),
1010 )
1012 def _compute_polygon_vertex(centroid, polygon_radius, angle):
1013 start_point = [polygon_radius, 0]
1014 return _apply_rotation(start_point, angle, centroid)
1016 def _get_angles(n_sides, rotation):
1017 angles = []
1018 degrees = 360 / n_sides
1019 # Start with the bottom left polygon vertex
1020 current_angle = (270 - 0.5 * degrees) + rotation
1021 for _ in range(0, n_sides):
1022 angles.append(current_angle)
1023 current_angle += degrees
1024 if current_angle > 360:
1025 current_angle -= 360
1026 return angles
1028 # 3. Variable Declarations
1029 angles = _get_angles(n_sides, rotation)
1031 # 4. Compute Vertices
1032 return [
1033 _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles
1034 ]
1037def _color_diff(color1, color2):
1038 """
1039 Uses 1-norm distance to calculate difference between two values.
1040 """
1041 if isinstance(color2, tuple):
1042 return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2)))
1043 else:
1044 return abs(color1 - color2)