image_operations.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import inspect
  2. from wagtail.images.exceptions import InvalidFilterSpecError
  3. from wagtail.images.rect import Rect
  4. from wagtail.images.utils import parse_color_string
  5. class Operation:
  6. def __init__(self, method, *args):
  7. self.method = method
  8. self.args = args
  9. # Check arguments
  10. try:
  11. inspect.getcallargs(self.construct, *args)
  12. except TypeError as e:
  13. raise InvalidFilterSpecError(e)
  14. # Call construct
  15. try:
  16. self.construct(*args)
  17. except ValueError as e:
  18. raise InvalidFilterSpecError(e)
  19. def construct(self, *args):
  20. raise NotImplementedError
  21. def run(self, willow, image, env):
  22. raise NotImplementedError
  23. class DoNothingOperation(Operation):
  24. def construct(self):
  25. pass
  26. def run(self, willow, image, env):
  27. pass
  28. class FillOperation(Operation):
  29. vary_fields = ('focal_point_width', 'focal_point_height', 'focal_point_x', 'focal_point_y')
  30. def construct(self, size, *extra):
  31. # Get width and height
  32. width_str, height_str = size.split('x')
  33. self.width = int(width_str)
  34. self.height = int(height_str)
  35. # Crop closeness
  36. self.crop_closeness = 0
  37. for extra_part in extra:
  38. if extra_part.startswith('c'):
  39. self.crop_closeness = int(extra_part[1:])
  40. else:
  41. raise ValueError("Unrecognised filter spec part: %s" % extra_part)
  42. # Divide it by 100 (as it's a percentage)
  43. self.crop_closeness /= 100
  44. # Clamp it
  45. if self.crop_closeness > 1:
  46. self.crop_closeness = 1
  47. def run(self, willow, image, env):
  48. image_width, image_height = willow.get_size()
  49. focal_point = image.get_focal_point()
  50. # Get crop aspect ratio
  51. crop_aspect_ratio = self.width / self.height
  52. # Get crop max
  53. crop_max_scale = min(image_width, image_height * crop_aspect_ratio)
  54. crop_max_width = crop_max_scale
  55. crop_max_height = crop_max_scale / crop_aspect_ratio
  56. # Initialise crop width and height to max
  57. crop_width = crop_max_width
  58. crop_height = crop_max_height
  59. # Use crop closeness to zoom in
  60. if focal_point is not None:
  61. # Get crop min
  62. crop_min_scale = max(focal_point.width, focal_point.height * crop_aspect_ratio)
  63. crop_min_width = crop_min_scale
  64. crop_min_height = crop_min_scale / crop_aspect_ratio
  65. # Sometimes, the focal point may be bigger than the image...
  66. if not crop_min_scale >= crop_max_scale:
  67. # Calculate max crop closeness to prevent upscaling
  68. max_crop_closeness = max(
  69. 1 - (self.width - crop_min_width) / (crop_max_width - crop_min_width),
  70. 1 - (self.height - crop_min_height) / (crop_max_height - crop_min_height)
  71. )
  72. # Apply max crop closeness
  73. crop_closeness = min(self.crop_closeness, max_crop_closeness)
  74. if 1 >= crop_closeness >= 0:
  75. # Get crop width and height
  76. crop_width = crop_max_width + (crop_min_width - crop_max_width) * crop_closeness
  77. crop_height = crop_max_height + (crop_min_height - crop_max_height) * crop_closeness
  78. # Find focal point UV
  79. if focal_point is not None:
  80. fp_x, fp_y = focal_point.centroid
  81. else:
  82. # Fall back to positioning in the centre
  83. fp_x = image_width / 2
  84. fp_y = image_height / 2
  85. fp_u = fp_x / image_width
  86. fp_v = fp_y / image_height
  87. # Position crop box based on focal point UV
  88. crop_x = fp_x - (fp_u - 0.5) * crop_width
  89. crop_y = fp_y - (fp_v - 0.5) * crop_height
  90. # Convert crop box into rect
  91. rect = Rect.from_point(crop_x, crop_y, crop_width, crop_height)
  92. # Make sure the entire focal point is in the crop box
  93. if focal_point is not None:
  94. rect = rect.move_to_cover(focal_point)
  95. # Don't allow the crop box to go over the image boundary
  96. rect = rect.move_to_clamp(Rect(0, 0, image_width, image_height))
  97. # Crop!
  98. willow = willow.crop(rect.round())
  99. # Get scale for resizing
  100. # The scale should be the same for both the horizontal and
  101. # vertical axes
  102. aftercrop_width, aftercrop_height = willow.get_size()
  103. scale = self.width / aftercrop_width
  104. # Only resize if the image is too big
  105. if scale < 1.0:
  106. # Resize!
  107. willow = willow.resize((self.width, self.height))
  108. return willow
  109. class MinMaxOperation(Operation):
  110. def construct(self, size):
  111. # Get width and height
  112. width_str, height_str = size.split('x')
  113. self.width = int(width_str)
  114. self.height = int(height_str)
  115. def run(self, willow, image, env):
  116. image_width, image_height = willow.get_size()
  117. horz_scale = self.width / image_width
  118. vert_scale = self.height / image_height
  119. if self.method == 'min':
  120. if image_width <= self.width or image_height <= self.height:
  121. return
  122. if horz_scale > vert_scale:
  123. width = self.width
  124. height = int(image_height * horz_scale)
  125. else:
  126. width = int(image_width * vert_scale)
  127. height = self.height
  128. elif self.method == 'max':
  129. if image_width <= self.width and image_height <= self.height:
  130. return
  131. if horz_scale < vert_scale:
  132. width = self.width
  133. height = int(image_height * horz_scale)
  134. else:
  135. width = int(image_width * vert_scale)
  136. height = self.height
  137. else:
  138. # Unknown method
  139. return
  140. return willow.resize((width, height))
  141. class WidthHeightOperation(Operation):
  142. def construct(self, size):
  143. self.size = int(size)
  144. def run(self, willow, image, env):
  145. image_width, image_height = willow.get_size()
  146. if self.method == 'width':
  147. if image_width <= self.size:
  148. return
  149. scale = self.size / image_width
  150. width = self.size
  151. height = int(image_height * scale)
  152. elif self.method == 'height':
  153. if image_height <= self.size:
  154. return
  155. scale = self.size / image_height
  156. width = int(image_width * scale)
  157. height = self.size
  158. else:
  159. # Unknown method
  160. return
  161. return willow.resize((width, height))
  162. class JPEGQualityOperation(Operation):
  163. def construct(self, quality):
  164. self.quality = int(quality)
  165. if self.quality > 100:
  166. raise ValueError("JPEG quality must not be higher than 100")
  167. def run(self, willow, image, env):
  168. env['jpeg-quality'] = self.quality
  169. class FormatOperation(Operation):
  170. def construct(self, fmt):
  171. self.format = fmt
  172. if self.format not in ['jpeg', 'png', 'gif']:
  173. raise ValueError("Format must be either 'jpeg', 'png' or 'gif'")
  174. def run(self, willow, image, env):
  175. env['output-format'] = self.format
  176. class BackgroundColorOperation(Operation):
  177. def construct(self, color_string):
  178. self.color = parse_color_string(color_string)
  179. def run(self, willow, image, env):
  180. return willow.set_background_color_rgb(self.color)