ImageDraw.py 42 KB

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