ImageDraw.py 36 KB

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