You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

440 lines
16 KiB

4 years ago
  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, mode)
  73. else:
  74. self.ink = self.draw.draw_ink(-1, mode)
  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, self.mode)
  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, self.mode)
  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(
  148. 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. p +
  161. (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. flipped = ((angles[1] > angles[0] and
  167. angles[1] - 180 > angles[0]) or
  168. (angles[1] < angles[0] and
  169. 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(self, xy, text, fill=None, font=None, anchor=None,
  236. *args, **kwargs):
  237. if self._multiline_check(text):
  238. return self.multiline_text(xy, text, fill, font, anchor,
  239. *args, **kwargs)
  240. ink, fill = self._getink(fill)
  241. if font is None:
  242. font = self.getfont()
  243. if ink is None:
  244. ink = fill
  245. if ink is not None:
  246. try:
  247. mask, offset = font.getmask2(text, self.fontmode,
  248. *args, **kwargs)
  249. xy = xy[0] + offset[0], xy[1] + offset[1]
  250. except AttributeError:
  251. try:
  252. mask = font.getmask(text, self.fontmode, *args, **kwargs)
  253. except TypeError:
  254. mask = font.getmask(text)
  255. self.draw.draw_bitmap(xy, mask, ink)
  256. def multiline_text(self, xy, text, fill=None, font=None, anchor=None,
  257. spacing=4, align="left", direction=None, features=None):
  258. widths = []
  259. max_width = 0
  260. lines = self._multiline_split(text)
  261. line_spacing = self.textsize('A', font=font)[1] + spacing
  262. for line in lines:
  263. line_width, line_height = self.textsize(line, font)
  264. widths.append(line_width)
  265. max_width = max(max_width, line_width)
  266. left, top = xy
  267. for idx, line in enumerate(lines):
  268. if align == "left":
  269. pass # left = x
  270. elif align == "center":
  271. left += (max_width - widths[idx]) / 2.0
  272. elif align == "right":
  273. left += (max_width - widths[idx])
  274. else:
  275. raise ValueError('align must be "left", "center" or "right"')
  276. self.text((left, top), line, fill, font, anchor,
  277. direction=direction, features=features)
  278. top += line_spacing
  279. left = xy[0]
  280. def textsize(self, text, font=None, spacing=4, direction=None,
  281. features=None):
  282. """Get the size of a given string, in pixels."""
  283. if self._multiline_check(text):
  284. return self.multiline_textsize(text, font, spacing,
  285. direction, features)
  286. if font is None:
  287. font = self.getfont()
  288. return font.getsize(text, direction, features)
  289. def multiline_textsize(self, text, font=None, spacing=4, direction=None,
  290. features=None):
  291. max_width = 0
  292. lines = self._multiline_split(text)
  293. line_spacing = self.textsize('A', font=font)[1] + spacing
  294. for line in lines:
  295. line_width, line_height = self.textsize(line, font, spacing,
  296. direction, features)
  297. max_width = max(max_width, line_width)
  298. return max_width, len(lines)*line_spacing - spacing
  299. def Draw(im, mode=None):
  300. """
  301. A simple 2D drawing interface for PIL images.
  302. :param im: The image to draw in.
  303. :param mode: Optional mode to use for color values. For RGB
  304. images, this argument can be RGB or RGBA (to blend the
  305. drawing into the image). For all other modes, this argument
  306. must be the same as the image mode. If omitted, the mode
  307. defaults to the mode of the image.
  308. """
  309. try:
  310. return im.getdraw(mode)
  311. except AttributeError:
  312. return ImageDraw(im, mode)
  313. # experimental access to the outline API
  314. try:
  315. Outline = Image.core.outline
  316. except AttributeError:
  317. Outline = None
  318. def getdraw(im=None, hints=None):
  319. """
  320. (Experimental) A more advanced 2D drawing interface for PIL images,
  321. based on the WCK interface.
  322. :param im: The image to draw in.
  323. :param hints: An optional list of hints.
  324. :returns: A (drawing context, drawing resource factory) tuple.
  325. """
  326. # FIXME: this needs more work!
  327. # FIXME: come up with a better 'hints' scheme.
  328. handler = None
  329. if not hints or "nicest" in hints:
  330. try:
  331. from . import _imagingagg as handler
  332. except ImportError:
  333. pass
  334. if handler is None:
  335. from . import ImageDraw2 as handler
  336. if im:
  337. im = handler.Draw(im)
  338. return im, handler
  339. def floodfill(image, xy, value, border=None, thresh=0):
  340. """
  341. (experimental) Fills a bounded region with a given color.
  342. :param image: Target image.
  343. :param xy: Seed position (a 2-item coordinate tuple). See
  344. :ref:`coordinate-system`.
  345. :param value: Fill color.
  346. :param border: Optional border value. If given, the region consists of
  347. pixels with a color different from the border color. If not given,
  348. the region consists of pixels having the same color as the seed
  349. pixel.
  350. :param thresh: Optional threshold value which specifies a maximum
  351. tolerable difference of a pixel value from the 'background' in
  352. order for it to be replaced. Useful for filling regions of non-
  353. homogeneous, but similar, colors.
  354. """
  355. # based on an implementation by Eric S. Raymond
  356. # amended by yo1995 @20180806
  357. pixel = image.load()
  358. x, y = xy
  359. try:
  360. background = pixel[x, y]
  361. if _color_diff(value, background) <= thresh:
  362. return # seed point already has fill color
  363. pixel[x, y] = value
  364. except (ValueError, IndexError):
  365. return # seed point outside image
  366. edge = {(x, y)}
  367. full_edge = set() # use a set to keep record of current and previous edge pixels to reduce memory consumption
  368. while edge:
  369. new_edge = set()
  370. for (x, y) in edge: # 4 adjacent method
  371. for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)):
  372. if (s, t) in full_edge:
  373. continue # if already processed, skip
  374. try:
  375. p = pixel[s, t]
  376. except (ValueError, IndexError):
  377. pass
  378. else:
  379. full_edge.add((s, t))
  380. if border is None:
  381. fill = _color_diff(p, background) <= thresh
  382. else:
  383. fill = p != value and p != border
  384. if fill:
  385. pixel[s, t] = value
  386. new_edge.add((s, t))
  387. full_edge = edge # discard pixels processed
  388. edge = new_edge
  389. def _color_diff(color1, color2):
  390. """
  391. Uses 1-norm distance to calculate difference between two values.
  392. """
  393. if isinstance(color2, tuple):
  394. return sum([abs(color1[i]-color2[i]) for i in range(0, len(color2))])
  395. else:
  396. return abs(color1-color2)