ImageDraw.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035
  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[AnyStr],
  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 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. x = int(line.x)
  546. y = int(line.y)
  547. start = (math.modf(line.x)[0], math.modf(line.y)[0])
  548. try:
  549. mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc]
  550. line.text,
  551. mode,
  552. direction=direction,
  553. features=features,
  554. language=language,
  555. stroke_width=stroke_width,
  556. stroke_filled=True,
  557. anchor=line.anchor,
  558. ink=ink,
  559. start=start,
  560. *args,
  561. **kwargs,
  562. )
  563. x += offset[0]
  564. y += offset[1]
  565. except AttributeError:
  566. try:
  567. mask = image_text.font.getmask( # type: ignore[misc]
  568. line.text,
  569. mode,
  570. direction,
  571. features,
  572. language,
  573. stroke_width,
  574. line.anchor,
  575. ink,
  576. start=start,
  577. *args,
  578. **kwargs,
  579. )
  580. except TypeError:
  581. mask = image_text.font.getmask(line.text)
  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. if self.im is not None:
  590. self.im.paste(
  591. color, (x, y, x + mask.size[0], y + mask.size[1]), mask
  592. )
  593. else:
  594. self.draw.draw_bitmap((x, y), mask, ink)
  595. if stroke_ink is not None:
  596. # Draw stroked text
  597. draw_text(stroke_ink, image_text.stroke_width)
  598. # Draw normal text
  599. if ink != stroke_ink:
  600. draw_text(ink)
  601. else:
  602. # Only draw normal text
  603. draw_text(ink)
  604. def multiline_text(
  605. self,
  606. xy: tuple[float, float],
  607. text: AnyStr,
  608. fill: _Ink | None = None,
  609. font: (
  610. ImageFont.ImageFont
  611. | ImageFont.FreeTypeFont
  612. | ImageFont.TransposedFont
  613. | None
  614. ) = None,
  615. anchor: str | None = None,
  616. spacing: float = 4,
  617. align: str = "left",
  618. direction: str | None = None,
  619. features: list[str] | None = None,
  620. language: str | None = None,
  621. stroke_width: float = 0,
  622. stroke_fill: _Ink | None = None,
  623. embedded_color: bool = False,
  624. *,
  625. font_size: float | None = None,
  626. ) -> None:
  627. return self.text(
  628. xy,
  629. text,
  630. fill,
  631. font,
  632. anchor,
  633. spacing,
  634. align,
  635. direction,
  636. features,
  637. language,
  638. stroke_width,
  639. stroke_fill,
  640. embedded_color,
  641. font_size=font_size,
  642. )
  643. def textlength(
  644. self,
  645. text: AnyStr,
  646. font: (
  647. ImageFont.ImageFont
  648. | ImageFont.FreeTypeFont
  649. | ImageFont.TransposedFont
  650. | None
  651. ) = None,
  652. direction: str | None = None,
  653. features: list[str] | None = None,
  654. language: str | None = None,
  655. embedded_color: bool = False,
  656. *,
  657. font_size: float | None = None,
  658. ) -> float:
  659. """Get the length of a given string, in pixels with 1/64 precision."""
  660. if font is None:
  661. font = self._getfont(font_size)
  662. image_text = ImageText.Text(
  663. text,
  664. font,
  665. self.mode,
  666. direction=direction,
  667. features=features,
  668. language=language,
  669. )
  670. if embedded_color:
  671. image_text.embed_color()
  672. return image_text.get_length()
  673. def textbbox(
  674. self,
  675. xy: tuple[float, float],
  676. text: AnyStr,
  677. font: (
  678. ImageFont.ImageFont
  679. | ImageFont.FreeTypeFont
  680. | ImageFont.TransposedFont
  681. | None
  682. ) = None,
  683. anchor: str | None = None,
  684. spacing: float = 4,
  685. align: str = "left",
  686. direction: str | None = None,
  687. features: list[str] | None = None,
  688. language: str | None = None,
  689. stroke_width: float = 0,
  690. embedded_color: bool = False,
  691. *,
  692. font_size: float | None = None,
  693. ) -> tuple[float, float, float, float]:
  694. """Get the bounding box of a given string, in pixels."""
  695. if font is None:
  696. font = self._getfont(font_size)
  697. image_text = ImageText.Text(
  698. text, font, self.mode, spacing, direction, features, language
  699. )
  700. if embedded_color:
  701. image_text.embed_color()
  702. if stroke_width:
  703. image_text.stroke(stroke_width)
  704. return image_text.get_bbox(xy, anchor, align)
  705. def multiline_textbbox(
  706. self,
  707. xy: tuple[float, float],
  708. text: AnyStr,
  709. font: (
  710. ImageFont.ImageFont
  711. | ImageFont.FreeTypeFont
  712. | ImageFont.TransposedFont
  713. | None
  714. ) = None,
  715. anchor: str | None = None,
  716. spacing: float = 4,
  717. align: str = "left",
  718. direction: str | None = None,
  719. features: list[str] | None = None,
  720. language: str | None = None,
  721. stroke_width: float = 0,
  722. embedded_color: bool = False,
  723. *,
  724. font_size: float | None = None,
  725. ) -> tuple[float, float, float, float]:
  726. return self.textbbox(
  727. xy,
  728. text,
  729. font,
  730. anchor,
  731. spacing,
  732. align,
  733. direction,
  734. features,
  735. language,
  736. stroke_width,
  737. embedded_color,
  738. font_size=font_size,
  739. )
  740. def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
  741. """
  742. A simple 2D drawing interface for PIL images.
  743. :param im: The image to draw in.
  744. :param mode: Optional mode to use for color values. For RGB
  745. images, this argument can be RGB or RGBA (to blend the
  746. drawing into the image). For all other modes, this argument
  747. must be the same as the image mode. If omitted, the mode
  748. defaults to the mode of the image.
  749. """
  750. try:
  751. return getattr(im, "getdraw")(mode)
  752. except AttributeError:
  753. return ImageDraw(im, mode)
  754. def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]:
  755. """
  756. :param im: The image to draw in.
  757. :returns: A (drawing context, drawing resource factory) tuple.
  758. """
  759. from . import ImageDraw2
  760. draw = ImageDraw2.Draw(im) if im is not None else None
  761. return draw, ImageDraw2
  762. def floodfill(
  763. image: Image.Image,
  764. xy: tuple[int, int],
  765. value: float | tuple[int, ...],
  766. border: float | tuple[int, ...] | None = None,
  767. thresh: float = 0,
  768. ) -> None:
  769. """
  770. .. warning:: This method is experimental.
  771. Fills a bounded region with a given color.
  772. :param image: Target image.
  773. :param xy: Seed position (a 2-item coordinate tuple). See
  774. :ref:`coordinate-system`.
  775. :param value: Fill color.
  776. :param border: Optional border value. If given, the region consists of
  777. pixels with a color different from the border color. If not given,
  778. the region consists of pixels having the same color as the seed
  779. pixel.
  780. :param thresh: Optional threshold value which specifies a maximum
  781. tolerable difference of a pixel value from the 'background' in
  782. order for it to be replaced. Useful for filling regions of
  783. non-homogeneous, but similar, colors.
  784. """
  785. # based on an implementation by Eric S. Raymond
  786. # amended by yo1995 @20180806
  787. pixel = image.load()
  788. assert pixel is not None
  789. x, y = xy
  790. try:
  791. background = pixel[x, y]
  792. if _color_diff(value, background) <= thresh:
  793. return # seed point already has fill color
  794. pixel[x, y] = value
  795. except (ValueError, IndexError):
  796. return # seed point outside image
  797. edge = {(x, y)}
  798. # use a set to keep record of current and previous edge pixels
  799. # to reduce memory consumption
  800. full_edge = set()
  801. while edge:
  802. new_edge = set()
  803. for x, y in edge: # 4 adjacent method
  804. for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
  805. # If already processed, or if a coordinate is negative, skip
  806. if (s, t) in full_edge or s < 0 or t < 0:
  807. continue
  808. try:
  809. p = pixel[s, t]
  810. except (ValueError, IndexError):
  811. pass
  812. else:
  813. full_edge.add((s, t))
  814. if border is None:
  815. fill = _color_diff(p, background) <= thresh
  816. else:
  817. fill = p not in (value, border)
  818. if fill:
  819. pixel[s, t] = value
  820. new_edge.add((s, t))
  821. full_edge = edge # discard pixels processed
  822. edge = new_edge
  823. def _compute_regular_polygon_vertices(
  824. bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float
  825. ) -> list[tuple[float, float]]:
  826. """
  827. Generate a list of vertices for a 2D regular polygon.
  828. :param bounding_circle: The bounding circle is a sequence defined
  829. by a point and radius. The polygon is inscribed in this circle.
  830. (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
  831. :param n_sides: Number of sides
  832. (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon)
  833. :param rotation: Apply an arbitrary rotation to the polygon
  834. (e.g. ``rotation=90``, applies a 90 degree rotation)
  835. :return: List of regular polygon vertices
  836. (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``)
  837. How are the vertices computed?
  838. 1. Compute the following variables
  839. - theta: Angle between the apothem & the nearest polygon vertex
  840. - side_length: Length of each polygon edge
  841. - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle)
  842. - polygon_radius: Polygon radius (last element of bounding_circle)
  843. - angles: Location of each polygon vertex in polar grid
  844. (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0])
  845. 2. For each angle in angles, get the polygon vertex at that angle
  846. The vertex is computed using the equation below.
  847. X= xcos(φ) + ysin(φ)
  848. Y= −xsin(φ) + ycos(φ)
  849. Note:
  850. φ = angle in degrees
  851. x = 0
  852. y = polygon_radius
  853. The formula above assumes rotation around the origin.
  854. In our case, we are rotating around the centroid.
  855. To account for this, we use the formula below
  856. X = xcos(φ) + ysin(φ) + centroid_x
  857. Y = −xsin(φ) + ycos(φ) + centroid_y
  858. """
  859. # 1. Error Handling
  860. # 1.1 Check `n_sides` has an appropriate value
  861. if not isinstance(n_sides, int):
  862. msg = "n_sides should be an int" # type: ignore[unreachable]
  863. raise TypeError(msg)
  864. if n_sides < 3:
  865. msg = "n_sides should be an int > 2"
  866. raise ValueError(msg)
  867. # 1.2 Check `bounding_circle` has an appropriate value
  868. if not isinstance(bounding_circle, (list, tuple)):
  869. msg = "bounding_circle should be a sequence"
  870. raise TypeError(msg)
  871. if len(bounding_circle) == 3:
  872. if not all(isinstance(i, (int, float)) for i in bounding_circle):
  873. msg = "bounding_circle should only contain numeric data"
  874. raise ValueError(msg)
  875. *centroid, polygon_radius = cast(list[float], list(bounding_circle))
  876. elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
  877. if not all(
  878. isinstance(i, (int, float)) for i in bounding_circle[0]
  879. ) or not isinstance(bounding_circle[1], (int, float)):
  880. msg = "bounding_circle should only contain numeric data"
  881. raise ValueError(msg)
  882. if len(bounding_circle[0]) != 2:
  883. msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
  884. raise ValueError(msg)
  885. centroid = cast(list[float], list(bounding_circle[0]))
  886. polygon_radius = cast(float, bounding_circle[1])
  887. else:
  888. msg = (
  889. "bounding_circle should contain 2D coordinates "
  890. "and a radius (e.g. (x, y, r) or ((x, y), r) )"
  891. )
  892. raise ValueError(msg)
  893. if polygon_radius <= 0:
  894. msg = "bounding_circle radius should be > 0"
  895. raise ValueError(msg)
  896. # 1.3 Check `rotation` has an appropriate value
  897. if not isinstance(rotation, (int, float)):
  898. msg = "rotation should be an int or float" # type: ignore[unreachable]
  899. raise ValueError(msg)
  900. # 2. Define Helper Functions
  901. def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
  902. return (
  903. round(
  904. point[0] * math.cos(math.radians(360 - degrees))
  905. - point[1] * math.sin(math.radians(360 - degrees))
  906. + centroid[0],
  907. 2,
  908. ),
  909. round(
  910. point[1] * math.cos(math.radians(360 - degrees))
  911. + point[0] * math.sin(math.radians(360 - degrees))
  912. + centroid[1],
  913. 2,
  914. ),
  915. )
  916. def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
  917. start_point = [polygon_radius, 0]
  918. return _apply_rotation(start_point, angle)
  919. def _get_angles(n_sides: int, rotation: float) -> list[float]:
  920. angles = []
  921. degrees = 360 / n_sides
  922. # Start with the bottom left polygon vertex
  923. current_angle = (270 - 0.5 * degrees) + rotation
  924. for _ in range(n_sides):
  925. angles.append(current_angle)
  926. current_angle += degrees
  927. if current_angle > 360:
  928. current_angle -= 360
  929. return angles
  930. # 3. Variable Declarations
  931. angles = _get_angles(n_sides, rotation)
  932. # 4. Compute Vertices
  933. return [_compute_polygon_vertex(angle) for angle in angles]
  934. def _color_diff(
  935. color1: float | tuple[int, ...], color2: float | tuple[int, ...]
  936. ) -> float:
  937. """
  938. Uses 1-norm distance to calculate difference between two values.
  939. """
  940. first = color1 if isinstance(color1, tuple) else (color1,)
  941. second = color2 if isinstance(color2, tuple) else (color2,)
  942. return sum(abs(first[i] - second[i]) for i in range(len(second)))