ImageDraw.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  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. from ._util import isStringType
  36. """
  37. A simple 2D drawing interface for PIL images.
  38. <p>
  39. Application code should use the <b>Draw</b> factory, instead of
  40. directly.
  41. """
  42. class ImageDraw(object):
  43. def __init__(self, im, mode=None):
  44. """
  45. Create a drawing instance.
  46. :param im: The image to draw in.
  47. :param mode: Optional mode to use for color values. For RGB
  48. images, this argument can be RGB or RGBA (to blend the
  49. drawing into the image). For all other modes, this argument
  50. must be the same as the image mode. If omitted, the mode
  51. defaults to the mode of the image.
  52. """
  53. im.load()
  54. if im.readonly:
  55. im._copy() # make it writeable
  56. blend = 0
  57. if mode is None:
  58. mode = im.mode
  59. if mode != im.mode:
  60. if mode == "RGBA" and im.mode == "RGB":
  61. blend = 1
  62. else:
  63. raise ValueError("mode mismatch")
  64. if mode == "P":
  65. self.palette = im.palette
  66. else:
  67. self.palette = None
  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 isStringType(ink):
  100. ink = ImageColor.getcolor(ink, self.mode)
  101. if self.palette and not isinstance(ink, numbers.Number):
  102. ink = self.palette.getcolor(ink)
  103. ink = self.draw.draw_ink(ink)
  104. if fill is not None:
  105. if isStringType(fill):
  106. fill = ImageColor.getcolor(fill, self.mode)
  107. if self.palette and not isinstance(fill, numbers.Number):
  108. fill = self.palette.getcolor(fill)
  109. fill = self.draw.draw_ink(fill)
  110. return ink, fill
  111. def arc(self, xy, start, end, fill=None, width=0):
  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=0):
  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:
  130. self.draw.draw_chord(xy, start, end, ink, 0, width)
  131. def ellipse(self, xy, fill=None, outline=None, width=0):
  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:
  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. for i in range(1, len(xy) - 1):
  145. point = xy[i]
  146. angles = [
  147. math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
  148. % 360
  149. for start, end in ((xy[i - 1], point), (point, xy[i + 1]))
  150. ]
  151. if angles[0] == angles[1]:
  152. # This is a straight line, so no joint is required
  153. continue
  154. def coord_at_angle(coord, angle):
  155. x, y = coord
  156. angle -= 90
  157. distance = width / 2 - 1
  158. return tuple(
  159. [
  160. p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d))
  161. for p, p_d in (
  162. (x, distance * math.cos(math.radians(angle))),
  163. (y, distance * math.sin(math.radians(angle))),
  164. )
  165. ]
  166. )
  167. flipped = (
  168. angles[1] > angles[0] and angles[1] - 180 > angles[0]
  169. ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0])
  170. coords = [
  171. (point[0] - width / 2 + 1, point[1] - width / 2 + 1),
  172. (point[0] + width / 2 - 1, point[1] + width / 2 - 1),
  173. ]
  174. if flipped:
  175. start, end = (angles[1] + 90, angles[0] + 90)
  176. else:
  177. start, end = (angles[0] - 90, angles[1] - 90)
  178. self.pieslice(coords, start - 90, end - 90, fill)
  179. if width > 8:
  180. # Cover potential gaps between the line and the joint
  181. if flipped:
  182. gapCoords = [
  183. coord_at_angle(point, angles[0] + 90),
  184. point,
  185. coord_at_angle(point, angles[1] + 90),
  186. ]
  187. else:
  188. gapCoords = [
  189. coord_at_angle(point, angles[0] - 90),
  190. point,
  191. coord_at_angle(point, angles[1] - 90),
  192. ]
  193. self.line(gapCoords, fill, width=3)
  194. def shape(self, shape, fill=None, outline=None):
  195. """(Experimental) Draw a shape."""
  196. shape.close()
  197. ink, fill = self._getink(outline, fill)
  198. if fill is not None:
  199. self.draw.draw_outline(shape, fill, 1)
  200. if ink is not None and ink != fill:
  201. self.draw.draw_outline(shape, ink, 0)
  202. def pieslice(self, xy, start, end, fill=None, outline=None, width=0):
  203. """Draw a pieslice."""
  204. ink, fill = self._getink(outline, fill)
  205. if fill is not None:
  206. self.draw.draw_pieslice(xy, start, end, fill, 1)
  207. if ink is not None and ink != fill:
  208. self.draw.draw_pieslice(xy, start, end, ink, 0, width)
  209. def point(self, xy, fill=None):
  210. """Draw one or more individual pixels."""
  211. ink, fill = self._getink(fill)
  212. if ink is not None:
  213. self.draw.draw_points(xy, ink)
  214. def polygon(self, xy, fill=None, outline=None):
  215. """Draw a polygon."""
  216. ink, fill = self._getink(outline, fill)
  217. if fill is not None:
  218. self.draw.draw_polygon(xy, fill, 1)
  219. if ink is not None and ink != fill:
  220. self.draw.draw_polygon(xy, ink, 0)
  221. def rectangle(self, xy, fill=None, outline=None, width=0):
  222. """Draw a rectangle."""
  223. ink, fill = self._getink(outline, fill)
  224. if fill is not None:
  225. self.draw.draw_rectangle(xy, fill, 1)
  226. if ink is not None and ink != fill:
  227. self.draw.draw_rectangle(xy, ink, 0, width)
  228. def _multiline_check(self, text):
  229. """Draw text."""
  230. split_character = "\n" if isinstance(text, str) else b"\n"
  231. return split_character in text
  232. def _multiline_split(self, text):
  233. split_character = "\n" if isinstance(text, str) else b"\n"
  234. return text.split(split_character)
  235. def text(
  236. self,
  237. xy,
  238. text,
  239. fill=None,
  240. font=None,
  241. anchor=None,
  242. spacing=4,
  243. align="left",
  244. direction=None,
  245. features=None,
  246. language=None,
  247. stroke_width=0,
  248. stroke_fill=None,
  249. *args,
  250. **kwargs
  251. ):
  252. if self._multiline_check(text):
  253. return self.multiline_text(
  254. xy,
  255. text,
  256. fill,
  257. font,
  258. anchor,
  259. spacing,
  260. align,
  261. direction,
  262. features,
  263. language,
  264. stroke_width,
  265. stroke_fill,
  266. )
  267. if font is None:
  268. font = self.getfont()
  269. def getink(fill):
  270. ink, fill = self._getink(fill)
  271. if ink is None:
  272. return fill
  273. return ink
  274. def draw_text(ink, stroke_width=0, stroke_offset=None):
  275. coord = xy
  276. try:
  277. mask, offset = font.getmask2(
  278. text,
  279. self.fontmode,
  280. direction=direction,
  281. features=features,
  282. language=language,
  283. stroke_width=stroke_width,
  284. *args,
  285. **kwargs
  286. )
  287. coord = coord[0] + offset[0], coord[1] + offset[1]
  288. except AttributeError:
  289. try:
  290. mask = font.getmask(
  291. text,
  292. self.fontmode,
  293. direction,
  294. features,
  295. language,
  296. stroke_width,
  297. *args,
  298. **kwargs
  299. )
  300. except TypeError:
  301. mask = font.getmask(text)
  302. if stroke_offset:
  303. coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]
  304. self.draw.draw_bitmap(coord, mask, ink)
  305. ink = getink(fill)
  306. if ink is not None:
  307. stroke_ink = None
  308. if stroke_width:
  309. stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink
  310. if stroke_ink is not None:
  311. # Draw stroked text
  312. draw_text(stroke_ink, stroke_width)
  313. # Draw normal text
  314. draw_text(ink, 0, (stroke_width, stroke_width))
  315. else:
  316. # Only draw normal text
  317. draw_text(ink)
  318. def multiline_text(
  319. self,
  320. xy,
  321. text,
  322. fill=None,
  323. font=None,
  324. anchor=None,
  325. spacing=4,
  326. align="left",
  327. direction=None,
  328. features=None,
  329. language=None,
  330. stroke_width=0,
  331. stroke_fill=None,
  332. ):
  333. widths = []
  334. max_width = 0
  335. lines = self._multiline_split(text)
  336. line_spacing = (
  337. self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
  338. )
  339. for line in lines:
  340. line_width, line_height = self.textsize(
  341. line,
  342. font,
  343. direction=direction,
  344. features=features,
  345. language=language,
  346. stroke_width=stroke_width,
  347. )
  348. widths.append(line_width)
  349. max_width = max(max_width, line_width)
  350. left, top = xy
  351. for idx, line in enumerate(lines):
  352. if align == "left":
  353. pass # left = x
  354. elif align == "center":
  355. left += (max_width - widths[idx]) / 2.0
  356. elif align == "right":
  357. left += max_width - widths[idx]
  358. else:
  359. raise ValueError('align must be "left", "center" or "right"')
  360. self.text(
  361. (left, top),
  362. line,
  363. fill,
  364. font,
  365. anchor,
  366. direction=direction,
  367. features=features,
  368. language=language,
  369. stroke_width=stroke_width,
  370. stroke_fill=stroke_fill,
  371. )
  372. top += line_spacing
  373. left = xy[0]
  374. def textsize(
  375. self,
  376. text,
  377. font=None,
  378. spacing=4,
  379. direction=None,
  380. features=None,
  381. language=None,
  382. stroke_width=0,
  383. ):
  384. """Get the size of a given string, in pixels."""
  385. if self._multiline_check(text):
  386. return self.multiline_textsize(
  387. text, font, spacing, direction, features, language, stroke_width
  388. )
  389. if font is None:
  390. font = self.getfont()
  391. return font.getsize(text, direction, features, language, stroke_width)
  392. def multiline_textsize(
  393. self,
  394. text,
  395. font=None,
  396. spacing=4,
  397. direction=None,
  398. features=None,
  399. language=None,
  400. stroke_width=0,
  401. ):
  402. max_width = 0
  403. lines = self._multiline_split(text)
  404. line_spacing = (
  405. self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
  406. )
  407. for line in lines:
  408. line_width, line_height = self.textsize(
  409. line, font, spacing, direction, features, language, stroke_width
  410. )
  411. max_width = max(max_width, line_width)
  412. return max_width, len(lines) * line_spacing
  413. def Draw(im, mode=None):
  414. """
  415. A simple 2D drawing interface for PIL images.
  416. :param im: The image to draw in.
  417. :param mode: Optional mode to use for color values. For RGB
  418. images, this argument can be RGB or RGBA (to blend the
  419. drawing into the image). For all other modes, this argument
  420. must be the same as the image mode. If omitted, the mode
  421. defaults to the mode of the image.
  422. """
  423. try:
  424. return im.getdraw(mode)
  425. except AttributeError:
  426. return ImageDraw(im, mode)
  427. # experimental access to the outline API
  428. try:
  429. Outline = Image.core.outline
  430. except AttributeError:
  431. Outline = None
  432. def getdraw(im=None, hints=None):
  433. """
  434. (Experimental) A more advanced 2D drawing interface for PIL images,
  435. based on the WCK interface.
  436. :param im: The image to draw in.
  437. :param hints: An optional list of hints.
  438. :returns: A (drawing context, drawing resource factory) tuple.
  439. """
  440. # FIXME: this needs more work!
  441. # FIXME: come up with a better 'hints' scheme.
  442. handler = None
  443. if not hints or "nicest" in hints:
  444. try:
  445. from . import _imagingagg as handler
  446. except ImportError:
  447. pass
  448. if handler is None:
  449. from . import ImageDraw2 as handler
  450. if im:
  451. im = handler.Draw(im)
  452. return im, handler
  453. def floodfill(image, xy, value, border=None, thresh=0):
  454. """
  455. (experimental) Fills a bounded region with a given color.
  456. :param image: Target image.
  457. :param xy: Seed position (a 2-item coordinate tuple). See
  458. :ref:`coordinate-system`.
  459. :param value: Fill color.
  460. :param border: Optional border value. If given, the region consists of
  461. pixels with a color different from the border color. If not given,
  462. the region consists of pixels having the same color as the seed
  463. pixel.
  464. :param thresh: Optional threshold value which specifies a maximum
  465. tolerable difference of a pixel value from the 'background' in
  466. order for it to be replaced. Useful for filling regions of
  467. non-homogeneous, but similar, colors.
  468. """
  469. # based on an implementation by Eric S. Raymond
  470. # amended by yo1995 @20180806
  471. pixel = image.load()
  472. x, y = xy
  473. try:
  474. background = pixel[x, y]
  475. if _color_diff(value, background) <= thresh:
  476. return # seed point already has fill color
  477. pixel[x, y] = value
  478. except (ValueError, IndexError):
  479. return # seed point outside image
  480. edge = {(x, y)}
  481. # use a set to keep record of current and previous edge pixels
  482. # to reduce memory consumption
  483. full_edge = set()
  484. while edge:
  485. new_edge = set()
  486. for (x, y) in edge: # 4 adjacent method
  487. for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
  488. # If already processed, or if a coordinate is negative, skip
  489. if (s, t) in full_edge or s < 0 or t < 0:
  490. continue
  491. try:
  492. p = pixel[s, t]
  493. except (ValueError, IndexError):
  494. pass
  495. else:
  496. full_edge.add((s, t))
  497. if border is None:
  498. fill = _color_diff(p, background) <= thresh
  499. else:
  500. fill = p != value and p != border
  501. if fill:
  502. pixel[s, t] = value
  503. new_edge.add((s, t))
  504. full_edge = edge # discard pixels processed
  505. edge = new_edge
  506. def _color_diff(color1, color2):
  507. """
  508. Uses 1-norm distance to calculate difference between two values.
  509. """
  510. if isinstance(color2, tuple):
  511. return sum([abs(color1[i] - color2[i]) for i in range(0, len(color2))])
  512. else:
  513. return abs(color1 - color2)