ImageDraw.py 35 KB

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