ImageDraw.py 33 KB

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