ImageDraw.py 30 KB

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