gooderp18绿色标准版
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

596 rindas
23KB

  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import base64
  4. import binascii
  5. import io
  6. from typing import Tuple, Union
  7. from PIL import Image, ImageOps
  8. # We can preload Ico too because it is considered safe
  9. from PIL import IcoImagePlugin
  10. try:
  11. from PIL.Image import Transpose, Palette, Resampling
  12. except ImportError:
  13. Transpose = Palette = Resampling = Image
  14. from random import randrange
  15. from odoo.exceptions import UserError
  16. from odoo.tools.misc import DotDict
  17. from odoo.tools.translate import LazyTranslate
  18. __all__ = ["image_process"]
  19. _lt = LazyTranslate('base')
  20. # Preload PIL with the minimal subset of image formats we need
  21. Image.preinit()
  22. Image._initialized = 2
  23. # Maps only the 6 first bits of the base64 data, accurate enough
  24. # for our purpose and faster than decoding the full blob first
  25. FILETYPE_BASE64_MAGICWORD = {
  26. b'/': 'jpg',
  27. b'R': 'gif',
  28. b'i': 'png',
  29. b'P': 'svg+xml',
  30. b'U': 'webp',
  31. }
  32. EXIF_TAG_ORIENTATION = 0x112
  33. # The target is to have 1st row/col to be top/left
  34. # Note: rotate is counterclockwise
  35. EXIF_TAG_ORIENTATION_TO_TRANSPOSE_METHODS = { # Initial side on 1st row/col:
  36. 0: [], # reserved
  37. 1: [], # top/left
  38. 2: [Transpose.FLIP_LEFT_RIGHT], # top/right
  39. 3: [Transpose.ROTATE_180], # bottom/right
  40. 4: [Transpose.FLIP_TOP_BOTTOM], # bottom/left
  41. 5: [Transpose.FLIP_LEFT_RIGHT, Transpose.ROTATE_90],# left/top
  42. 6: [Transpose.ROTATE_270], # right/top
  43. 7: [Transpose.FLIP_TOP_BOTTOM, Transpose.ROTATE_90],# right/bottom
  44. 8: [Transpose.ROTATE_90], # left/bottom
  45. }
  46. # Arbitrary limit to fit most resolutions, including Samsung Galaxy A22 photo,
  47. # 8K with a ratio up to 16:10, and almost all variants of 4320p
  48. IMAGE_MAX_RESOLUTION = 50e6
  49. class ImageProcess:
  50. def __init__(self, source, verify_resolution=True):
  51. """Initialize the ``source`` image for processing.
  52. :param bytes source: the original image binary
  53. No processing will be done if the `source` is falsy or if
  54. the image is SVG.
  55. :param verify_resolution: if True, make sure the original image size is not
  56. excessive before starting to process it. The max allowed resolution is
  57. defined by `IMAGE_MAX_RESOLUTION`.
  58. :type verify_resolution: bool
  59. :rtype: ImageProcess
  60. :raise: ValueError if `verify_resolution` is True and the image is too large
  61. :raise: UserError if the image can't be identified by PIL
  62. """
  63. self.source = source or False
  64. self.operationsCount = 0
  65. if not source or source[:1] == b'<' or (source[0:4] == b'RIFF' and source[8:15] == b'WEBPVP8'):
  66. # don't process empty source or SVG or WEBP
  67. self.image = False
  68. else:
  69. try:
  70. self.image = Image.open(io.BytesIO(source))
  71. except (OSError, binascii.Error):
  72. raise UserError(_lt("This file could not be decoded as an image file."))
  73. # Original format has to be saved before fixing the orientation or
  74. # doing any other operations because the information will be lost on
  75. # the resulting image.
  76. self.original_format = (self.image.format or '').upper()
  77. self.image = image_fix_orientation(self.image)
  78. w, h = self.image.size
  79. if verify_resolution and w * h > IMAGE_MAX_RESOLUTION:
  80. raise UserError(_lt("Image size excessive, uploaded images must be smaller than %s million pixels.", str(IMAGE_MAX_RESOLUTION / 1e6)))
  81. def image_quality(self, quality=0, output_format=''):
  82. """Return the image resulting of all the image processing
  83. operations that have been applied previously.
  84. The source is returned as-is if it's an SVG, or if no operations have
  85. been applied, the `output_format` is the same as the original format,
  86. and the quality is not specified.
  87. :param int quality: quality setting to apply. Default to 0.
  88. - for JPEG: 1 is worse, 95 is best. Values above 95 should be
  89. avoided. Falsy values will fallback to 95, but only if the image
  90. was changed, otherwise the original image is returned.
  91. - for PNG: set falsy to prevent conversion to a WEB palette.
  92. - for other formats: no effect.
  93. :param str output_format: Can be PNG, JPEG, GIF, or ICO.
  94. Default to the format of the original image if a valid output format,
  95. otherwise BMP is converted to PNG and the rest are converted to JPEG.
  96. :return: the final image, or ``False`` if the original ``source`` was falsy.
  97. :rtype: bytes | False
  98. """
  99. if not self.image:
  100. return self.source
  101. output_image = self.image
  102. output_format = output_format.upper() or self.original_format
  103. if output_format == 'BMP':
  104. output_format = 'PNG'
  105. elif output_format not in ['PNG', 'JPEG', 'GIF', 'ICO']:
  106. output_format = 'JPEG'
  107. if not self.operationsCount and output_format == self.original_format and not quality:
  108. return self.source
  109. opt = {'output_format': output_format}
  110. if output_format == 'PNG':
  111. opt['optimize'] = True
  112. if quality:
  113. if output_image.mode != 'P':
  114. # Floyd Steinberg dithering by default
  115. output_image = output_image.convert('RGBA').convert('P', palette=Palette.WEB, colors=256)
  116. if output_format == 'JPEG':
  117. opt['optimize'] = True
  118. opt['quality'] = quality or 95
  119. if output_format == 'GIF':
  120. opt['optimize'] = True
  121. opt['save_all'] = True
  122. if output_image.mode not in ["1", "L", "P", "RGB", "RGBA"] or (output_format == 'JPEG' and output_image.mode == 'RGBA'):
  123. output_image = output_image.convert("RGB")
  124. output_bytes = image_apply_opt(output_image, **opt)
  125. if len(output_bytes) >= len(self.source) and self.original_format == output_format and not self.operationsCount:
  126. # Format has not changed and image content is unchanged but the
  127. # reached binary is bigger: rather use the original.
  128. return self.source
  129. return output_bytes
  130. def resize(self, max_width=0, max_height=0, expand=False):
  131. """Resize the image.
  132. The image is not resized above the current image size, unless the expand
  133. parameter is True. This method is used by default to create smaller versions
  134. of the image.
  135. The current ratio is preserved. To change the ratio, see `crop_resize`.
  136. If `max_width` or `max_height` is falsy, it will be computed from the
  137. other to keep the current ratio. If both are falsy, no resize is done.
  138. It is currently not supported for GIF because we do not handle all the
  139. frames properly.
  140. :param int max_width: max width
  141. :param int max_height: max height
  142. :param bool expand: whether or not the image size can be increased
  143. :return: self to allow chaining
  144. :rtype: ImageProcess
  145. """
  146. if self.image and self.original_format != 'GIF' and (max_width or max_height):
  147. w, h = self.image.size
  148. asked_width = max_width or (w * max_height) // h
  149. asked_height = max_height or (h * max_width) // w
  150. if expand and (asked_width > w or asked_height > h):
  151. self.image = self.image.resize((asked_width, asked_height))
  152. self.operationsCount += 1
  153. return self
  154. if asked_width != w or asked_height != h:
  155. self.image.thumbnail((asked_width, asked_height), Resampling.LANCZOS)
  156. if self.image.width != w or self.image.height != h:
  157. self.operationsCount += 1
  158. return self
  159. def crop_resize(self, max_width, max_height, center_x=0.5, center_y=0.5):
  160. """Crop and resize the image.
  161. The image is never resized above the current image size. This method is
  162. only to create smaller versions of the image.
  163. Instead of preserving the ratio of the original image like `resize`,
  164. this method will force the output to take the ratio of the given
  165. `max_width` and `max_height`, so both have to be defined.
  166. The crop is done before the resize in order to preserve as much of the
  167. original image as possible. The goal of this method is primarily to
  168. resize to a given ratio, and it is not to crop unwanted parts of the
  169. original image. If the latter is what you want to do, you should create
  170. another method, or directly use the `crop` method from PIL.
  171. It is currently not supported for GIF because we do not handle all the
  172. frames properly.
  173. :param int max_width: max width
  174. :param int max_height: max height
  175. :param float center_x: the center of the crop between 0 (left) and 1
  176. (right). Defaults to 0.5 (center).
  177. :param float center_y: the center of the crop between 0 (top) and 1
  178. (bottom). Defaults to 0.5 (center).
  179. :return: self to allow chaining
  180. :rtype: ImageProcess
  181. """
  182. if self.image and self.original_format != 'GIF' and max_width and max_height:
  183. w, h = self.image.size
  184. # We want to keep as much of the image as possible -> at least one
  185. # of the 2 crop dimensions always has to be the same value as the
  186. # original image.
  187. # The target size will be reached with the final resize.
  188. if w / max_width > h / max_height:
  189. new_w, new_h = w, (max_height * w) // max_width
  190. else:
  191. new_w, new_h = (max_width * h) // max_height, h
  192. # No cropping above image size.
  193. if new_w > w:
  194. new_w, new_h = w, (new_h * w) // new_w
  195. if new_h > h:
  196. new_w, new_h = (new_w * h) // new_h, h
  197. # Dimensions should be at least 1.
  198. new_w, new_h = max(new_w, 1), max(new_h, 1)
  199. # Correctly place the center of the crop.
  200. x_offset = int((w - new_w) * center_x)
  201. h_offset = int((h - new_h) * center_y)
  202. if new_w != w or new_h != h:
  203. self.image = self.image.crop((x_offset, h_offset, x_offset + new_w, h_offset + new_h))
  204. if self.image.width != w or self.image.height != h:
  205. self.operationsCount += 1
  206. return self.resize(max_width, max_height)
  207. def colorize(self, color=None):
  208. """Replace the transparent background by a given color, or by a random one.
  209. :param tuple color: RGB values for the color to use
  210. :return: self to allow chaining
  211. :rtype: ImageProcess
  212. """
  213. if color is None:
  214. color = (randrange(32, 224, 24), randrange(32, 224, 24), randrange(32, 224, 24))
  215. if self.image:
  216. original = self.image
  217. self.image = Image.new('RGB', original.size)
  218. self.image.paste(color, box=(0, 0) + original.size)
  219. self.image.paste(original, mask=original)
  220. self.operationsCount += 1
  221. return self
  222. def add_padding(self, padding):
  223. """Expand the image size by adding padding around the image
  224. :param int padding: thickness of the padding
  225. :return: self to allow chaining
  226. :rtype: ImageProcess
  227. """
  228. if self.image:
  229. img_width, img_height = self.image.size
  230. self.image = self.image.resize((img_width - 2 * padding, img_height - 2 * padding))
  231. self.image = ImageOps.expand(self.image, border=padding)
  232. self.operationsCount += 1
  233. return self
  234. def image_process(source, size=(0, 0), verify_resolution=False, quality=0, expand=False, crop=None, colorize=False, output_format='', padding=False):
  235. """Process the `source` image by executing the given operations and
  236. return the result image.
  237. """
  238. if not source or ((not size or (not size[0] and not size[1])) and not verify_resolution and not quality and not crop and not colorize and not output_format and not padding):
  239. # for performance: don't do anything if the image is falsy or if
  240. # no operations have been requested
  241. return source
  242. image = ImageProcess(source, verify_resolution)
  243. if size:
  244. if crop:
  245. center_x = 0.5
  246. center_y = 0.5
  247. if crop == 'top':
  248. center_y = 0
  249. elif crop == 'bottom':
  250. center_y = 1
  251. image.crop_resize(max_width=size[0], max_height=size[1], center_x=center_x, center_y=center_y)
  252. else:
  253. image.resize(max_width=size[0], max_height=size[1], expand=expand)
  254. if padding:
  255. image.add_padding(padding)
  256. if colorize:
  257. image.colorize(colorize if isinstance(colorize, tuple) else None)
  258. return image.image_quality(quality=quality, output_format=output_format)
  259. # ----------------------------------------
  260. # Misc image tools
  261. # ---------------------------------------
  262. def average_dominant_color(colors, mitigate=175, max_margin=140):
  263. """This function is used to calculate the dominant colors when given a list of colors
  264. There are 5 steps:
  265. 1) Select dominant colors (highest count), isolate its values and remove
  266. it from the current color set.
  267. 2) Set margins according to the prevalence of the dominant color.
  268. 3) Evaluate the colors. Similar colors are grouped in the dominant set
  269. while others are put in the "remaining" list.
  270. 4) Calculate the average color for the dominant set. This is done by
  271. averaging each band and joining them into a tuple.
  272. 5) Mitigate final average and convert it to hex
  273. :param colors: list of tuples having:
  274. 0. color count in the image
  275. 1. actual color: tuple(R, G, B, A)
  276. -> these can be extracted from a PIL image using
  277. :meth:`~PIL.Image.Image.getcolors`
  278. :param mitigate: maximum value a band can reach
  279. :param max_margin: maximum difference from one of the dominant values
  280. :returns: a tuple with two items:
  281. 0. the average color of the dominant set as: tuple(R, G, B)
  282. 1. list of remaining colors, used to evaluate subsequent dominant colors
  283. """
  284. dominant_color = max(colors)
  285. dominant_rgb = dominant_color[1][:3]
  286. dominant_set = [dominant_color]
  287. remaining = []
  288. margins = [max_margin * (1 - dominant_color[0] /
  289. sum([col[0] for col in colors]))] * 3
  290. colors.remove(dominant_color)
  291. for color in colors:
  292. rgb = color[1]
  293. if (rgb[0] < dominant_rgb[0] + margins[0] and rgb[0] > dominant_rgb[0] - margins[0] and
  294. rgb[1] < dominant_rgb[1] + margins[1] and rgb[1] > dominant_rgb[1] - margins[1] and
  295. rgb[2] < dominant_rgb[2] + margins[2] and rgb[2] > dominant_rgb[2] - margins[2]):
  296. dominant_set.append(color)
  297. else:
  298. remaining.append(color)
  299. dominant_avg = []
  300. for band in range(3):
  301. avg = total = 0
  302. for color in dominant_set:
  303. avg += color[0] * color[1][band]
  304. total += color[0]
  305. dominant_avg.append(int(avg / total))
  306. final_dominant = []
  307. brightest = max(dominant_avg)
  308. for color in range(3):
  309. value = dominant_avg[color] / (brightest / mitigate) if brightest > mitigate else dominant_avg[color]
  310. final_dominant.append(int(value))
  311. return tuple(final_dominant), remaining
  312. def image_fix_orientation(image):
  313. """Fix the orientation of the image if it has an EXIF orientation tag.
  314. This typically happens for images taken from a non-standard orientation
  315. by some phones or other devices that are able to report orientation.
  316. The specified transposition is applied to the image before all other
  317. operations, because all of them expect the image to be in its final
  318. orientation, which is the case only when the first row of pixels is the top
  319. of the image and the first column of pixels is the left of the image.
  320. Moreover the EXIF tags will not be kept when the image is later saved, so
  321. the transposition has to be done to ensure the final image is correctly
  322. orientated.
  323. Note: to be completely correct, the resulting image should have its exif
  324. orientation tag removed, since the transpositions have been applied.
  325. However since this tag is not used in the code, it is acceptable to
  326. save the complexity of removing it.
  327. :param image: the source image
  328. :type image: ~PIL.Image.Image
  329. :return: the resulting image, copy of the source, with orientation fixed
  330. or the source image if no operation was applied
  331. :rtype: ~PIL.Image.Image
  332. """
  333. getexif = getattr(image, 'getexif', None) or getattr(image, '_getexif', None) # support PIL < 6.0
  334. if getexif:
  335. exif = getexif()
  336. if exif:
  337. orientation = exif.get(EXIF_TAG_ORIENTATION, 0)
  338. for method in EXIF_TAG_ORIENTATION_TO_TRANSPOSE_METHODS.get(orientation, []):
  339. image = image.transpose(method)
  340. return image
  341. return image
  342. def binary_to_image(source):
  343. try:
  344. return Image.open(io.BytesIO(source))
  345. except (OSError, binascii.Error):
  346. raise UserError(_lt("This file could not be decoded as an image file."))
  347. def base64_to_image(base64_source: Union[str, bytes]) -> Image:
  348. """Return a PIL image from the given `base64_source`.
  349. :param base64_source: the image base64 encoded
  350. :raise: UserError if the base64 is incorrect or the image can't be identified by PIL
  351. """
  352. try:
  353. return Image.open(io.BytesIO(base64.b64decode(base64_source)))
  354. except (OSError, binascii.Error):
  355. raise UserError(_lt("This file could not be decoded as an image file."))
  356. def image_apply_opt(image: Image, output_format: str, **params) -> bytes:
  357. """Return the serialization of the provided `image` to `output_format`
  358. using `params`.
  359. :param image: the image to encode
  360. :param output_format: :meth:`~PIL.Image.Image.save`'s ``format`` parameter
  361. :param dict params: params to expand when calling :meth:`~PIL.Image.Image.save`
  362. :return: the image formatted
  363. """
  364. if output_format == 'JPEG' and image.mode not in ['1', 'L', 'RGB']:
  365. image = image.convert("RGB")
  366. stream = io.BytesIO()
  367. image.save(stream, format=output_format, **params)
  368. return stream.getvalue()
  369. def image_to_base64(image, output_format, **params):
  370. """Return a base64_image from the given PIL `image` using `params`.
  371. :type image: ~PIL.Image.Image
  372. :param str output_format:
  373. :param dict params: params to expand when calling :meth:`~PIL.Image.Image.save`
  374. :return: the image base64 encoded
  375. :rtype: bytes
  376. """
  377. stream = image_apply_opt(image, output_format, **params)
  378. return base64.b64encode(stream)
  379. def get_webp_size(source):
  380. """
  381. Returns the size of the provided webp binary source for VP8, VP8X and
  382. VP8L, otherwise returns None.
  383. See https://developers.google.com/speed/webp/docs/riff_container.
  384. :param source: binary source
  385. :return: (width, height) tuple, or None if not supported
  386. """
  387. if not (source[0:4] == b'RIFF' and source[8:15] == b'WEBPVP8'):
  388. raise UserError(_lt("This file is not a webp file."))
  389. vp8_type = source[15]
  390. if vp8_type == 0x20: # 0x20 = ' '
  391. # Sizes on big-endian 16 bits at offset 26.
  392. width_low, width_high, height_low, height_high = source[26:30]
  393. width = (width_high << 8) + width_low
  394. height = (height_high << 8) + height_low
  395. return (width, height)
  396. elif vp8_type == 0x58: # 0x48 = 'X'
  397. # Sizes (minus one) on big-endian 24 bits at offset 24.
  398. width_low, width_medium, width_high, height_low, height_medium, height_high = source[24:30]
  399. width = 1 + (width_high << 16) + (width_medium << 8) + width_low
  400. height = 1 + (height_high << 16) + (height_medium << 8) + height_low
  401. return (width, height)
  402. elif vp8_type == 0x4C and source[20] == 0x2F: # 0x4C = 'L'
  403. # Sizes (minus one) on big-endian-ish 14 bits at offset 21.
  404. # E.g. [@20] 2F ab cd ef gh
  405. # - width = 1 + (c&0x3)d ab: ignore the two high bits of the second byte
  406. # - height= 1 + hef(c&0xC>>2): used them as the first two bits of the height
  407. ab, cd, ef, gh = source[21:25]
  408. width = 1 + ((cd & 0x3F) << 8) + ab
  409. height = 1 + ((gh & 0xF) << 10) + (ef << 2) + (cd >> 6)
  410. return (width, height)
  411. return None
  412. def is_image_size_above(base64_source_1, base64_source_2):
  413. """Return whether or not the size of the given image `base64_source_1` is
  414. above the size of the given image `base64_source_2`.
  415. """
  416. if not base64_source_1 or not base64_source_2:
  417. return False
  418. if base64_source_1[:1] in (b'P', 'P') or base64_source_2[:1] in (b'P', 'P'):
  419. # False for SVG
  420. return False
  421. def get_image_size(base64_source):
  422. source = base64.b64decode(base64_source)
  423. if (source[0:4] == b'RIFF' and source[8:15] == b'WEBPVP8'):
  424. size = get_webp_size(source)
  425. if size:
  426. return DotDict({'width': size[0], 'height': size[0]})
  427. else:
  428. # False for unknown WEBP format
  429. return False
  430. else:
  431. return image_fix_orientation(binary_to_image(source))
  432. image_source = get_image_size(base64_source_1)
  433. image_target = get_image_size(base64_source_2)
  434. return image_source.width > image_target.width or image_source.height > image_target.height
  435. def image_guess_size_from_field_name(field_name: str) -> Tuple[int, int]:
  436. """Attempt to guess the image size based on `field_name`.
  437. If it can't be guessed or if it is a custom field: return (0, 0) instead.
  438. :param field_name: the name of a field
  439. :return: the guessed size
  440. """
  441. if field_name == 'image':
  442. return (1024, 1024)
  443. if field_name.startswith('x_'):
  444. return (0, 0)
  445. try:
  446. suffix = int(field_name.rsplit('_', 1)[-1])
  447. except ValueError:
  448. return 0, 0
  449. if suffix < 16:
  450. # If the suffix is less than 16, it's probably not the size
  451. return (0, 0)
  452. return (suffix, suffix)
  453. def image_data_uri(base64_source: bytes) -> str:
  454. """This returns data URL scheme according RFC 2397
  455. (https://tools.ietf.org/html/rfc2397) for all kind of supported images
  456. (PNG, GIF, JPG and SVG), defaulting on PNG type if not mimetype detected.
  457. """
  458. return 'data:image/%s;base64,%s' % (
  459. FILETYPE_BASE64_MAGICWORD.get(base64_source[:1], 'png'),
  460. base64_source.decode(),
  461. )
  462. def get_saturation(rgb):
  463. """Returns the saturation (hsl format) of a given rgb color
  464. :param rgb: rgb tuple or list
  465. :return: saturation
  466. """
  467. c_max = max(rgb) / 255
  468. c_min = min(rgb) / 255
  469. d = c_max - c_min
  470. return 0 if d == 0 else d / (1 - abs(c_max + c_min - 1))
  471. def get_lightness(rgb):
  472. """Returns the lightness (hsl format) of a given rgb color
  473. :param rgb: rgb tuple or list
  474. :return: lightness
  475. """
  476. return (max(rgb) + min(rgb)) / 2 / 255
  477. def hex_to_rgb(hx):
  478. """Converts an hexadecimal string (starting with '#') to a RGB tuple"""
  479. return tuple([int(hx[i:i+2], 16) for i in range(1, 6, 2)])
  480. def rgb_to_hex(rgb):
  481. """Converts a RGB tuple or list to an hexadecimal string"""
  482. return '#' + ''.join([(hex(c).split('x')[-1].zfill(2)) for c in rgb])
上海开阖软件有限公司 沪ICP备12045867号-1