ImageDraw.py 35 KB

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