Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/ImageOps.py: 20%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# The Python Imaging Library.
3# $Id$
4#
5# standard image operations
6#
7# History:
8# 2001-10-20 fl Created
9# 2001-10-23 fl Added autocontrast operator
10# 2001-12-18 fl Added Kevin's fit operator
11# 2004-03-14 fl Fixed potential division by zero in equalize
12# 2005-05-05 fl Fixed equalize for low number of values
13#
14# Copyright (c) 2001-2004 by Secret Labs AB
15# Copyright (c) 2001-2004 by Fredrik Lundh
16#
17# See the README file for information on usage and redistribution.
18#
19from __future__ import annotations
21import functools
22import operator
23import re
24from collections.abc import Sequence
25from typing import Literal, Protocol, cast, overload
27from . import ExifTags, Image, ImagePalette
29#
30# helpers
33def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]:
34 if isinstance(border, tuple):
35 if len(border) == 2:
36 left, top = right, bottom = border
37 elif len(border) == 4:
38 left, top, right, bottom = border
39 else:
40 msg = "border must be an integer, or a tuple of two or four elements"
41 raise ValueError(msg)
42 else:
43 left = top = right = bottom = border
44 return left, top, right, bottom
47def _color(
48 color: str | int | tuple[int, ...] | None, mode: str
49) -> int | tuple[int, ...] | None:
50 if isinstance(color, str):
51 from . import ImageColor
53 color = ImageColor.getcolor(color, mode)
54 return color
57def _lut(image: Image.Image, lut: list[int]) -> Image.Image:
58 if image.mode == "P":
59 # FIXME: apply to lookup table, not image data
60 msg = "mode P support coming soon"
61 raise NotImplementedError(msg)
62 elif image.mode in ("L", "RGB"):
63 if image.mode == "RGB" and len(lut) == 256:
64 lut = lut + lut + lut
65 return image.point(lut)
66 else:
67 msg = f"not supported for mode {image.mode}"
68 raise OSError(msg)
71#
72# actions
75def autocontrast(
76 image: Image.Image,
77 cutoff: float | tuple[float, float] = 0,
78 ignore: int | Sequence[int] | None = None,
79 mask: Image.Image | None = None,
80 preserve_tone: bool = False,
81) -> Image.Image:
82 """
83 Maximize (normalize) image contrast. This function calculates a
84 histogram of the input image (or mask region), removes ``cutoff`` percent of the
85 lightest and darkest pixels from the histogram, and remaps the image
86 so that the darkest pixel becomes black (0), and the lightest
87 becomes white (255).
89 :param image: The image to process.
90 :param cutoff: The percent to cut off from the histogram on the low and
91 high ends. Either a tuple of (low, high), or a single
92 number for both.
93 :param ignore: The background pixel value (use None for no background).
94 :param mask: Histogram used in contrast operation is computed using pixels
95 within the mask. If no mask is given the entire image is used
96 for histogram computation.
97 :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast.
99 .. versionadded:: 8.2.0
101 :return: An image.
102 """
103 if preserve_tone:
104 histogram = image.convert("L").histogram(mask)
105 else:
106 histogram = image.histogram(mask)
108 lut = []
109 for layer in range(0, len(histogram), 256):
110 h = histogram[layer : layer + 256]
111 if ignore is not None:
112 # get rid of outliers
113 if isinstance(ignore, int):
114 h[ignore] = 0
115 else:
116 for ix in ignore:
117 h[ix] = 0
118 if cutoff:
119 # cut off pixels from both ends of the histogram
120 if not isinstance(cutoff, tuple):
121 cutoff = (cutoff, cutoff)
122 # get number of pixels
123 n = 0
124 for ix in range(256):
125 n = n + h[ix]
126 # remove cutoff% pixels from the low end
127 cut = int(n * cutoff[0] // 100)
128 for lo in range(256):
129 if cut > h[lo]:
130 cut = cut - h[lo]
131 h[lo] = 0
132 else:
133 h[lo] -= cut
134 cut = 0
135 if cut <= 0:
136 break
137 # remove cutoff% samples from the high end
138 cut = int(n * cutoff[1] // 100)
139 for hi in range(255, -1, -1):
140 if cut > h[hi]:
141 cut = cut - h[hi]
142 h[hi] = 0
143 else:
144 h[hi] -= cut
145 cut = 0
146 if cut <= 0:
147 break
148 # find lowest/highest samples after preprocessing
149 for lo in range(256):
150 if h[lo]:
151 break
152 for hi in range(255, -1, -1):
153 if h[hi]:
154 break
155 if hi <= lo:
156 # don't bother
157 lut.extend(list(range(256)))
158 else:
159 scale = 255.0 / (hi - lo)
160 offset = -lo * scale
161 for ix in range(256):
162 ix = int(ix * scale + offset)
163 if ix < 0:
164 ix = 0
165 elif ix > 255:
166 ix = 255
167 lut.append(ix)
168 return _lut(image, lut)
171def colorize(
172 image: Image.Image,
173 black: str | tuple[int, ...],
174 white: str | tuple[int, ...],
175 mid: str | int | tuple[int, ...] | None = None,
176 blackpoint: int = 0,
177 whitepoint: int = 255,
178 midpoint: int = 127,
179) -> Image.Image:
180 """
181 Colorize grayscale image.
182 This function calculates a color wedge which maps all black pixels in
183 the source image to the first color and all white pixels to the
184 second color. If ``mid`` is specified, it uses three-color mapping.
185 The ``black`` and ``white`` arguments should be RGB tuples or color names;
186 optionally you can use three-color mapping by also specifying ``mid``.
187 Mapping positions for any of the colors can be specified
188 (e.g. ``blackpoint``), where these parameters are the integer
189 value corresponding to where the corresponding color should be mapped.
190 These parameters must have logical order, such that
191 ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified).
193 :param image: The image to colorize.
194 :param black: The color to use for black input pixels.
195 :param white: The color to use for white input pixels.
196 :param mid: The color to use for midtone input pixels.
197 :param blackpoint: an int value [0, 255] for the black mapping.
198 :param whitepoint: an int value [0, 255] for the white mapping.
199 :param midpoint: an int value [0, 255] for the midtone mapping.
200 :return: An image.
201 """
203 if image.mode != "L":
204 msg = f"mode must be L, not {image.mode}"
205 raise ValueError(msg)
206 if not 0 <= blackpoint <= whitepoint <= 255:
207 msg = (
208 "blackpoint and whitepoint must each be between or equal to 0 and 255, "
209 "with blackpoint less than or equal to whitepoint"
210 )
211 raise ValueError(msg)
212 if mid is not None and not blackpoint <= midpoint <= whitepoint:
213 msg = "midpoint must be between or equal to blackpoint and whitepoint"
214 raise ValueError(msg)
216 # Define colors from arguments
217 rgb_black = cast(Sequence[int], _color(black, "RGB"))
218 rgb_white = cast(Sequence[int], _color(white, "RGB"))
219 rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None
221 # Empty lists for the mapping
222 red = []
223 green = []
224 blue = []
226 # Create the low-end values
227 for i in range(blackpoint):
228 red.append(rgb_black[0])
229 green.append(rgb_black[1])
230 blue.append(rgb_black[2])
232 # Create the mapping (2-color)
233 if rgb_mid is None:
234 range_map = range(whitepoint - blackpoint)
236 for i in range_map:
237 red.append(
238 rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map)
239 )
240 green.append(
241 rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map)
242 )
243 blue.append(
244 rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map)
245 )
247 # Create the mapping (3-color)
248 else:
249 range_map1 = range(midpoint - blackpoint)
250 range_map2 = range(whitepoint - midpoint)
252 for i in range_map1:
253 red.append(
254 rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1)
255 )
256 green.append(
257 rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1)
258 )
259 blue.append(
260 rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1)
261 )
262 for i in range_map2:
263 red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2))
264 green.append(
265 rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2)
266 )
267 blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
269 # Create the high-end values
270 for i in range(256 - whitepoint):
271 red.append(rgb_white[0])
272 green.append(rgb_white[1])
273 blue.append(rgb_white[2])
275 # Return converted image
276 image = image.convert("RGB")
277 return _lut(image, red + green + blue)
280def contain(
281 image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
282) -> Image.Image:
283 """
284 Returns a resized version of the image, set to the maximum width and height
285 within the requested size, while maintaining the original aspect ratio.
287 :param image: The image to resize.
288 :param size: The requested output size in pixels, given as a
289 (width, height) tuple.
290 :param method: Resampling method to use. Default is
291 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
292 See :ref:`concept-filters`.
293 :return: An image.
294 """
296 im_ratio = image.width / image.height
297 dest_ratio = size[0] / size[1]
299 if im_ratio != dest_ratio:
300 if im_ratio > dest_ratio:
301 new_height = round(image.height / image.width * size[0])
302 if new_height != size[1]:
303 size = (size[0], new_height)
304 else:
305 new_width = round(image.width / image.height * size[1])
306 if new_width != size[0]:
307 size = (new_width, size[1])
308 return image.resize(size, resample=method)
311def cover(
312 image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
313) -> Image.Image:
314 """
315 Returns a resized version of the image, so that the requested size is
316 covered, while maintaining the original aspect ratio.
318 :param image: The image to resize.
319 :param size: The requested output size in pixels, given as a
320 (width, height) tuple.
321 :param method: Resampling method to use. Default is
322 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
323 See :ref:`concept-filters`.
324 :return: An image.
325 """
327 im_ratio = image.width / image.height
328 dest_ratio = size[0] / size[1]
330 if im_ratio != dest_ratio:
331 if im_ratio < dest_ratio:
332 new_height = round(image.height / image.width * size[0])
333 if new_height != size[1]:
334 size = (size[0], new_height)
335 else:
336 new_width = round(image.width / image.height * size[1])
337 if new_width != size[0]:
338 size = (new_width, size[1])
339 return image.resize(size, resample=method)
342def _new_with_fill(
343 image: Image.Image, size: tuple[int, int], fill: str | int | tuple[int, ...] | None
344) -> Image.Image:
345 color = _color(fill, image.mode)
346 if image.palette:
347 mode = image.palette.mode
348 palette = ImagePalette.ImagePalette(mode, image.getpalette(mode))
349 if isinstance(color, tuple) and len(color) in (3, 4):
350 color = palette.getcolor(color)
351 else:
352 palette = None
353 out = Image.new(image.mode, size, color)
354 if palette:
355 out.putpalette(palette.palette, mode)
356 return out
359def pad(
360 image: Image.Image,
361 size: tuple[int, int],
362 method: int = Image.Resampling.BICUBIC,
363 color: str | int | tuple[int, ...] | None = None,
364 centering: tuple[float, float] = (0.5, 0.5),
365) -> Image.Image:
366 """
367 Returns a resized and padded version of the image, expanded to fill the
368 requested aspect ratio and size.
370 :param image: The image to resize and crop.
371 :param size: The requested output size in pixels, given as a
372 (width, height) tuple.
373 :param method: Resampling method to use. Default is
374 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
375 See :ref:`concept-filters`.
376 :param color: The background color of the padded image.
377 :param centering: Control the position of the original image within the
378 padded version.
380 (0.5, 0.5) will keep the image centered
381 (0, 0) will keep the image aligned to the top left
382 (1, 1) will keep the image aligned to the bottom
383 right
384 :return: An image.
385 """
387 resized = contain(image, size, method)
388 if resized.size == size:
389 out = resized
390 else:
391 out = _new_with_fill(resized, size, color)
392 if resized.width != size[0]:
393 x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
394 out.paste(resized, (x, 0))
395 else:
396 y = round((size[1] - resized.height) * max(0, min(centering[1], 1)))
397 out.paste(resized, (0, y))
398 return out
401def crop(image: Image.Image, border: int = 0) -> Image.Image:
402 """
403 Remove border from image. The same amount of pixels are removed
404 from all four sides. This function works on all image modes.
406 .. seealso:: :py:meth:`~PIL.Image.Image.crop`
408 :param image: The image to crop.
409 :param border: The number of pixels to remove.
410 :return: An image.
411 """
412 left, top, right, bottom = _border(border)
413 return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
416def scale(
417 image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC
418) -> Image.Image:
419 """
420 Returns a rescaled image by a specific factor given in parameter.
421 A factor greater than 1 expands the image, between 0 and 1 contracts the
422 image.
424 :param image: The image to rescale.
425 :param factor: The expansion factor, as a float.
426 :param resample: Resampling method to use. Default is
427 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
428 See :ref:`concept-filters`.
429 :returns: An :py:class:`~PIL.Image.Image` object.
430 """
431 if factor == 1:
432 return image.copy()
433 elif factor <= 0:
434 msg = "the factor must be greater than 0"
435 raise ValueError(msg)
436 else:
437 size = (round(factor * image.width), round(factor * image.height))
438 return image.resize(size, resample)
441class SupportsGetMesh(Protocol):
442 """
443 An object that supports the ``getmesh`` method, taking an image as an
444 argument, and returning a list of tuples. Each tuple contains two tuples,
445 the source box as a tuple of 4 integers, and a tuple of 8 integers for the
446 final quadrilateral, in order of top left, bottom left, bottom right, top
447 right.
448 """
450 def getmesh(
451 self, image: Image.Image
452 ) -> list[
453 tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
454 ]: ...
457def deform(
458 image: Image.Image,
459 deformer: SupportsGetMesh,
460 resample: int = Image.Resampling.BILINEAR,
461) -> Image.Image:
462 """
463 Deform the image.
465 :param image: The image to deform.
466 :param deformer: A deformer object. Any object that implements a
467 ``getmesh`` method can be used.
468 :param resample: An optional resampling filter. Same values possible as
469 in the PIL.Image.transform function.
470 :return: An image.
471 """
472 return image.transform(
473 image.size, Image.Transform.MESH, deformer.getmesh(image), resample
474 )
477def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image:
478 """
479 Equalize the image histogram. This function applies a non-linear
480 mapping to the input image, in order to create a uniform
481 distribution of grayscale values in the output image.
483 :param image: The image to equalize.
484 :param mask: An optional mask. If given, only the pixels selected by
485 the mask are included in the analysis.
486 :return: An image.
487 """
488 if image.mode == "P":
489 image = image.convert("RGB")
490 h = image.histogram(mask)
491 lut = []
492 for b in range(0, len(h), 256):
493 histo = [_f for _f in h[b : b + 256] if _f]
494 if len(histo) <= 1:
495 lut.extend(list(range(256)))
496 else:
497 step = (functools.reduce(operator.add, histo) - histo[-1]) // 255
498 if not step:
499 lut.extend(list(range(256)))
500 else:
501 n = step // 2
502 for i in range(256):
503 lut.append(n // step)
504 n = n + h[i + b]
505 return _lut(image, lut)
508def expand(
509 image: Image.Image,
510 border: int | tuple[int, ...] = 0,
511 fill: str | int | tuple[int, ...] = 0,
512) -> Image.Image:
513 """
514 Add border to the image
516 :param image: The image to expand.
517 :param border: Border width, in pixels.
518 :param fill: Pixel fill value (a color value). Default is 0 (black).
519 :return: An image.
520 """
521 left, top, right, bottom = _border(border)
522 width = left + image.size[0] + right
523 height = top + image.size[1] + bottom
524 out = _new_with_fill(image, (width, height), fill)
525 out.paste(image, (left, top))
526 return out
529def fit(
530 image: Image.Image,
531 size: tuple[int, int],
532 method: int = Image.Resampling.BICUBIC,
533 bleed: float = 0.0,
534 centering: tuple[float, float] = (0.5, 0.5),
535) -> Image.Image:
536 """
537 Returns a resized and cropped version of the image, cropped to the
538 requested aspect ratio and size.
540 This function was contributed by Kevin Cazabon.
542 :param image: The image to resize and crop.
543 :param size: The requested output size in pixels, given as a
544 (width, height) tuple.
545 :param method: Resampling method to use. Default is
546 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
547 See :ref:`concept-filters`.
548 :param bleed: Remove a border around the outside of the image from all
549 four edges. The value is a decimal percentage (use 0.01 for
550 one percent). The default value is 0 (no border).
551 Cannot be greater than or equal to 0.5.
552 :param centering: Control the cropping position. Use (0.5, 0.5) for
553 center cropping (e.g. if cropping the width, take 50% off
554 of the left side, and therefore 50% off the right side).
555 (0.0, 0.0) will crop from the top left corner (i.e. if
556 cropping the width, take all of the crop off of the right
557 side, and if cropping the height, take all of it off the
558 bottom). (1.0, 0.0) will crop from the bottom left
559 corner, etc. (i.e. if cropping the width, take all of the
560 crop off the left side, and if cropping the height take
561 none from the top, and therefore all off the bottom).
562 :return: An image.
563 """
565 # by Kevin Cazabon, Feb 17/2000
566 # kevin@cazabon.com
567 # https://www.cazabon.com
569 centering_x, centering_y = centering
571 if not 0.0 <= centering_x <= 1.0:
572 centering_x = 0.5
573 if not 0.0 <= centering_y <= 1.0:
574 centering_y = 0.5
576 if not 0.0 <= bleed < 0.5:
577 bleed = 0.0
579 # calculate the area to use for resizing and cropping, subtracting
580 # the 'bleed' around the edges
582 # number of pixels to trim off on Top and Bottom, Left and Right
583 bleed_pixels = (bleed * image.size[0], bleed * image.size[1])
585 live_size = (
586 image.size[0] - bleed_pixels[0] * 2,
587 image.size[1] - bleed_pixels[1] * 2,
588 )
590 # calculate the aspect ratio of the live_size
591 live_size_ratio = live_size[0] / live_size[1]
593 # calculate the aspect ratio of the output image
594 output_ratio = size[0] / size[1]
596 # figure out if the sides or top/bottom will be cropped off
597 if live_size_ratio == output_ratio:
598 # live_size is already the needed ratio
599 crop_width = live_size[0]
600 crop_height = live_size[1]
601 elif live_size_ratio >= output_ratio:
602 # live_size is wider than what's needed, crop the sides
603 crop_width = output_ratio * live_size[1]
604 crop_height = live_size[1]
605 else:
606 # live_size is taller than what's needed, crop the top and bottom
607 crop_width = live_size[0]
608 crop_height = live_size[0] / output_ratio
610 # make the crop
611 crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
612 crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
614 crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
616 # resize the image and return it
617 return image.resize(size, method, box=crop)
620def flip(image: Image.Image) -> Image.Image:
621 """
622 Flip the image vertically (top to bottom).
624 :param image: The image to flip.
625 :return: An image.
626 """
627 return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
630def grayscale(image: Image.Image) -> Image.Image:
631 """
632 Convert the image to grayscale.
634 :param image: The image to convert.
635 :return: An image.
636 """
637 return image.convert("L")
640def invert(image: Image.Image) -> Image.Image:
641 """
642 Invert (negate) the image.
644 :param image: The image to invert.
645 :return: An image.
646 """
647 lut = list(range(255, -1, -1))
648 return image.point(lut) if image.mode == "1" else _lut(image, lut)
651def mirror(image: Image.Image) -> Image.Image:
652 """
653 Flip image horizontally (left to right).
655 :param image: The image to mirror.
656 :return: An image.
657 """
658 return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
661def posterize(image: Image.Image, bits: int) -> Image.Image:
662 """
663 Reduce the number of bits for each color channel.
665 :param image: The image to posterize.
666 :param bits: The number of bits to keep for each channel (1-8).
667 :return: An image.
668 """
669 mask = ~(2 ** (8 - bits) - 1)
670 lut = [i & mask for i in range(256)]
671 return _lut(image, lut)
674def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
675 """
676 Invert all pixel values above a threshold.
678 :param image: The image to solarize.
679 :param threshold: All pixels above this grayscale level are inverted.
680 :return: An image.
681 """
682 lut = []
683 for i in range(256):
684 if i < threshold:
685 lut.append(i)
686 else:
687 lut.append(255 - i)
688 return _lut(image, lut)
691@overload
692def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
695@overload
696def exif_transpose(
697 image: Image.Image, *, in_place: Literal[False] = False
698) -> Image.Image: ...
701def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
702 """
703 If an image has an EXIF Orientation tag, other than 1, transpose the image
704 accordingly, and remove the orientation data.
706 :param image: The image to transpose.
707 :param in_place: Boolean. Keyword-only argument.
708 If ``True``, the original image is modified in-place, and ``None`` is returned.
709 If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
710 with the transposition applied. If there is no transposition, a copy of the
711 image will be returned.
712 """
713 image.load()
714 image_exif = image.getexif()
715 orientation = image_exif.get(ExifTags.Base.Orientation, 1)
716 method = {
717 2: Image.Transpose.FLIP_LEFT_RIGHT,
718 3: Image.Transpose.ROTATE_180,
719 4: Image.Transpose.FLIP_TOP_BOTTOM,
720 5: Image.Transpose.TRANSPOSE,
721 6: Image.Transpose.ROTATE_270,
722 7: Image.Transpose.TRANSVERSE,
723 8: Image.Transpose.ROTATE_90,
724 }.get(orientation)
725 if method is not None:
726 if in_place:
727 image.im = image.im.transpose(method)
728 image._size = image.im.size
729 else:
730 transposed_image = image.transpose(method)
731 exif_image = image if in_place else transposed_image
733 exif = exif_image.getexif()
734 if ExifTags.Base.Orientation in exif:
735 del exif[ExifTags.Base.Orientation]
736 if "exif" in exif_image.info:
737 exif_image.info["exif"] = exif.tobytes()
738 elif "Raw profile type exif" in exif_image.info:
739 exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
740 for key in ("XML:com.adobe.xmp", "xmp"):
741 if key in exif_image.info:
742 for pattern in (
743 r'tiff:Orientation="([0-9])"',
744 r"<tiff:Orientation>([0-9])</tiff:Orientation>",
745 ):
746 value = exif_image.info[key]
747 if isinstance(value, str):
748 value = re.sub(pattern, "", value)
749 elif isinstance(value, tuple):
750 value = tuple(
751 re.sub(pattern.encode(), b"", v) for v in value
752 )
753 else:
754 value = re.sub(pattern.encode(), b"", value)
755 exif_image.info[key] = value
756 if not in_place:
757 return transposed_image
758 elif not in_place:
759 return image.copy()
760 return None