ImageDraw2.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. #
  2. # The Python Imaging Library
  3. # $Id$
  4. #
  5. # WCK-style drawing interface operations
  6. #
  7. # History:
  8. # 2003-12-07 fl created
  9. # 2005-05-15 fl updated; added to PIL as ImageDraw2
  10. # 2005-05-15 fl added text support
  11. # 2005-05-20 fl added arc/chord/pieslice support
  12. #
  13. # Copyright (c) 2003-2005 by Secret Labs AB
  14. # Copyright (c) 2003-2005 by Fredrik Lundh
  15. #
  16. # See the README file for information on usage and redistribution.
  17. #
  18. """
  19. (Experimental) WCK-style drawing interface operations
  20. .. seealso:: :py:mod:`PIL.ImageDraw`
  21. """
  22. from __future__ import annotations
  23. from typing import Any, AnyStr, BinaryIO
  24. from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
  25. from ._typing import Coords, StrOrBytesPath
  26. class Pen:
  27. """Stores an outline color and width."""
  28. def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None:
  29. self.color = ImageColor.getrgb(color)
  30. self.width = width
  31. class Brush:
  32. """Stores a fill color"""
  33. def __init__(self, color: str, opacity: int = 255) -> None:
  34. self.color = ImageColor.getrgb(color)
  35. class Font:
  36. """Stores a TrueType font and color"""
  37. def __init__(
  38. self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12
  39. ) -> None:
  40. # FIXME: add support for bitmap fonts
  41. self.color = ImageColor.getrgb(color)
  42. self.font = ImageFont.truetype(file, size)
  43. class Draw:
  44. """
  45. (Experimental) WCK-style drawing interface
  46. """
  47. def __init__(
  48. self,
  49. image: Image.Image | str,
  50. size: tuple[int, int] | list[int] | None = None,
  51. color: float | tuple[float, ...] | str | None = None,
  52. ) -> None:
  53. if isinstance(image, str):
  54. if size is None:
  55. msg = "If image argument is mode string, size must be a list or tuple"
  56. raise ValueError(msg)
  57. image = Image.new(image, size, color)
  58. self.draw = ImageDraw.Draw(image)
  59. self.image = image
  60. self.transform: tuple[float, float, float, float, float, float] | None = None
  61. def flush(self) -> Image.Image:
  62. return self.image
  63. def render(
  64. self,
  65. op: str,
  66. xy: Coords,
  67. pen: Pen | Brush | None,
  68. brush: Brush | Pen | None = None,
  69. **kwargs: Any,
  70. ) -> None:
  71. # handle color arguments
  72. outline = fill = None
  73. width = 1
  74. if isinstance(pen, Pen):
  75. outline = pen.color
  76. width = pen.width
  77. elif isinstance(brush, Pen):
  78. outline = brush.color
  79. width = brush.width
  80. if isinstance(brush, Brush):
  81. fill = brush.color
  82. elif isinstance(pen, Brush):
  83. fill = pen.color
  84. # handle transformation
  85. if self.transform:
  86. path = ImagePath.Path(xy)
  87. path.transform(self.transform)
  88. xy = path
  89. # render the item
  90. if op in ("arc", "line"):
  91. kwargs.setdefault("fill", outline)
  92. else:
  93. kwargs.setdefault("fill", fill)
  94. kwargs.setdefault("outline", outline)
  95. if op == "line":
  96. kwargs.setdefault("width", width)
  97. getattr(self.draw, op)(xy, **kwargs)
  98. def settransform(self, offset: tuple[float, float]) -> None:
  99. """Sets a transformation offset."""
  100. (xoffset, yoffset) = offset
  101. self.transform = (1, 0, xoffset, 0, 1, yoffset)
  102. def arc(
  103. self,
  104. xy: Coords,
  105. pen: Pen | Brush | None,
  106. start: float,
  107. end: float,
  108. *options: Any,
  109. ) -> None:
  110. """
  111. Draws an arc (a portion of a circle outline) between the start and end
  112. angles, inside the given bounding box.
  113. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc`
  114. """
  115. self.render("arc", xy, pen, *options, start=start, end=end)
  116. def chord(
  117. self,
  118. xy: Coords,
  119. pen: Pen | Brush | None,
  120. start: float,
  121. end: float,
  122. *options: Any,
  123. ) -> None:
  124. """
  125. Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points
  126. with a straight line.
  127. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord`
  128. """
  129. self.render("chord", xy, pen, *options, start=start, end=end)
  130. def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
  131. """
  132. Draws an ellipse inside the given bounding box.
  133. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse`
  134. """
  135. self.render("ellipse", xy, pen, *options)
  136. def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
  137. """
  138. Draws a line between the coordinates in the ``xy`` list.
  139. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line`
  140. """
  141. self.render("line", xy, pen, *options)
  142. def pieslice(
  143. self,
  144. xy: Coords,
  145. pen: Pen | Brush | None,
  146. start: float,
  147. end: float,
  148. *options: Any,
  149. ) -> None:
  150. """
  151. Same as arc, but also draws straight lines between the end points and the
  152. center of the bounding box.
  153. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice`
  154. """
  155. self.render("pieslice", xy, pen, *options, start=start, end=end)
  156. def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
  157. """
  158. Draws a polygon.
  159. The polygon outline consists of straight lines between the given
  160. coordinates, plus a straight line between the last and the first
  161. coordinate.
  162. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon`
  163. """
  164. self.render("polygon", xy, pen, *options)
  165. def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
  166. """
  167. Draws a rectangle.
  168. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle`
  169. """
  170. self.render("rectangle", xy, pen, *options)
  171. def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None:
  172. """
  173. Draws the string at the given position.
  174. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text`
  175. """
  176. if self.transform:
  177. path = ImagePath.Path(xy)
  178. path.transform(self.transform)
  179. xy = path
  180. self.draw.text(xy, text, font=font.font, fill=font.color)
  181. def textbbox(
  182. self, xy: tuple[float, float], text: AnyStr, font: Font
  183. ) -> tuple[float, float, float, float]:
  184. """
  185. Returns bounding box (in pixels) of given text.
  186. :return: ``(left, top, right, bottom)`` bounding box
  187. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox`
  188. """
  189. if self.transform:
  190. path = ImagePath.Path(xy)
  191. path.transform(self.transform)
  192. xy = path
  193. return self.draw.textbbox(xy, text, font=font.font)
  194. def textlength(self, text: AnyStr, font: Font) -> float:
  195. """
  196. Returns length (in pixels) of given text.
  197. This is the amount by which following text should be offset.
  198. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength`
  199. """
  200. return self.draw.textlength(text, font=font.font)