Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/ImageOps.py: 18%
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
21__lazy_modules__ = {"re"}
23import re
24from typing import Protocol, cast, overload
26from . import ExifTags, Image, ImageFilter, ImagePalette
28TYPE_CHECKING = False
29if TYPE_CHECKING:
30 from collections.abc import Sequence
31 from typing import Literal
33#
34# helpers
37def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]:
38 if isinstance(border, tuple):
39 if len(border) == 2:
40 left, top = right, bottom = border
41 elif len(border) == 4:
42 left, top, right, bottom = border
43 else:
44 msg = "border must be an integer, or a tuple of two or four elements"
45 raise ValueError(msg)
46 else:
47 left = top = right = bottom = border
48 return left, top, right, bottom
51def _color(
52 color: str | int | tuple[int, ...] | None, mode: str
53) -> int | tuple[int, ...] | None:
54 if isinstance(color, str):
55 from . import ImageColor
57 color = ImageColor.getcolor(color, mode)
58 return color
61def _lut(image: Image.Image, lut: list[int], palette: bool = False) -> Image.Image:
62 if image.mode == "P":
63 if not palette:
64 # The lookup table was built from the palette indexes, not colors
65 msg = "mode P support coming soon"
66 raise NotImplementedError(msg)
68 # The lookup table can be applied to the palette colors
69 im = image.copy()
70 assert im.palette is not None
71 palette_mode = im.palette.mode
72 channels = len(palette_mode)
73 colors = im.getpalette(None)
74 assert colors is not None
75 im.putpalette(
76 [
77 v if palette_mode[i % channels] == "A" else lut[v]
78 for i, v in enumerate(colors)
79 ],
80 palette_mode,
81 )
82 return im
83 elif image.mode in ("L", "RGB"):
84 if image.mode == "RGB" and len(lut) == 256:
85 lut = lut + lut + lut
86 return image.point(lut)
87 else:
88 msg = f"not supported for mode {image.mode}"
89 raise OSError(msg)
92#
93# actions
96def autocontrast(
97 image: Image.Image,
98 cutoff: float | tuple[float, float] = 0,
99 ignore: int | Sequence[int] | None = None,
100 mask: Image.Image | None = None,
101 preserve_tone: bool = False,
102) -> Image.Image:
103 """
104 Maximize (normalize) image contrast. This function calculates a
105 histogram of the input image (or mask region), removes ``cutoff`` percent of the
106 lightest and darkest pixels from the histogram, and remaps the image
107 so that the darkest pixel becomes black (0), and the lightest
108 becomes white (255).
110 :param image: The image to process.
111 :param cutoff: The percent to cut off from the histogram on the low and
112 high ends. Either a tuple of (low, high), or a single
113 number for both.
114 :param ignore: The background pixel value (use None for no background).
115 :param mask: Histogram used in contrast operation is computed using pixels
116 within the mask. If no mask is given the entire image is used
117 for histogram computation.
118 :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast.
120 .. versionadded:: 8.2.0
122 :return: An image.
123 """
124 if ignore is not None:
125 if isinstance(ignore, int):
126 ignore = (ignore,)
127 if not all(0 <= ix <= 255 for ix in ignore):
128 msg = "ignore must be between or equal to 0 and 255"
129 raise ValueError(msg)
131 if preserve_tone:
132 histogram = image.convert("L").histogram(mask)
133 else:
134 histogram = image.histogram(mask)
136 lut = []
137 for layer in range(0, len(histogram), 256):
138 h = histogram[layer : layer + 256]
139 if ignore is not None:
140 # get rid of outliers
141 for ix in ignore:
142 h[ix] = 0
143 if cutoff:
144 # cut off pixels from both ends of the histogram
145 if not isinstance(cutoff, tuple):
146 cutoff = (cutoff, cutoff)
147 # get number of pixels
148 n = sum(h)
149 # remove cutoff% pixels from the low end
150 cut = int(n * cutoff[0] // 100)
151 for lo in range(256):
152 if cut > h[lo]:
153 cut = cut - h[lo]
154 h[lo] = 0
155 else:
156 h[lo] -= cut
157 cut = 0
158 if cut <= 0:
159 break
160 # remove cutoff% samples from the high end
161 cut = int(n * cutoff[1] // 100)
162 for hi in range(255, -1, -1):
163 if cut > h[hi]:
164 cut = cut - h[hi]
165 h[hi] = 0
166 else:
167 h[hi] -= cut
168 cut = 0
169 if cut <= 0:
170 break
171 # find lowest/highest samples after preprocessing
172 for lo in range(256):
173 if h[lo]:
174 break
175 for hi in range(255, -1, -1):
176 if h[hi]:
177 break
178 if hi <= lo:
179 # don't bother
180 lut.extend(list(range(256)))
181 else:
182 for ix in range(256):
183 ix = (ix - lo) * 255 // (hi - lo)
184 if ix < 0:
185 ix = 0
186 elif ix > 255:
187 ix = 255
188 lut.append(ix)
189 return _lut(image, lut)
192def colorize(
193 image: Image.Image,
194 black: str | tuple[int, ...],
195 white: str | tuple[int, ...],
196 mid: str | int | tuple[int, ...] | None = None,
197 blackpoint: int = 0,
198 whitepoint: int = 255,
199 midpoint: int = 127,
200) -> Image.Image:
201 """
202 Colorize grayscale image.
203 This function calculates a color wedge which maps all black pixels in
204 the source image to the first color and all white pixels to the
205 second color. If ``mid`` is specified, it uses three-color mapping.
206 The ``black`` and ``white`` arguments should be RGB tuples or color names;
207 optionally you can use three-color mapping by also specifying ``mid``.
208 Mapping positions for any of the colors can be specified
209 (e.g. ``blackpoint``), where these parameters are the integer
210 value corresponding to where the corresponding color should be mapped.
211 These parameters must have logical order, such that
212 ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified).
214 :param image: The image to colorize.
215 :param black: The color to use for black input pixels.
216 :param white: The color to use for white input pixels.
217 :param mid: The color to use for midtone input pixels.
218 :param blackpoint: an int value [0, 255] for the black mapping.
219 :param whitepoint: an int value [0, 255] for the white mapping.
220 :param midpoint: an int value [0, 255] for the midtone mapping.
221 :return: An image.
222 """
224 if image.mode != "L":
225 msg = f"mode must be L, not {image.mode}"
226 raise ValueError(msg)
227 if not 0 <= blackpoint <= whitepoint <= 255:
228 msg = (
229 "blackpoint and whitepoint must each be between or equal to 0 and 255, "
230 "with blackpoint less than or equal to whitepoint"
231 )
232 raise ValueError(msg)
233 if mid is not None and not blackpoint <= midpoint <= whitepoint:
234 msg = "midpoint must be between or equal to blackpoint and whitepoint"
235 raise ValueError(msg)
237 # Define colors from arguments
238 rgb_black = cast("Sequence[int]", _color(black, "RGB"))
239 rgb_white = cast("Sequence[int]", _color(white, "RGB"))
240 rgb_mid = cast("Sequence[int]", _color(mid, "RGB")) if mid is not None else None
242 # Empty lists for the mapping
243 red = []
244 green = []
245 blue = []
247 # Create the low-end values
248 for i in range(blackpoint):
249 red.append(rgb_black[0])
250 green.append(rgb_black[1])
251 blue.append(rgb_black[2])
253 # Create the mapping (2-color)
254 if rgb_mid is None:
255 range_map = range(whitepoint - blackpoint)
257 for i in range_map:
258 red.append(
259 rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map)
260 )
261 green.append(
262 rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map)
263 )
264 blue.append(
265 rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map)
266 )
268 # Create the mapping (3-color)
269 else:
270 range_map1 = range(midpoint - blackpoint)
271 range_map2 = range(whitepoint - midpoint)
273 for i in range_map1:
274 red.append(
275 rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1)
276 )
277 green.append(
278 rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1)
279 )
280 blue.append(
281 rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1)
282 )
283 for i in range_map2:
284 red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2))
285 green.append(
286 rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2)
287 )
288 blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
290 # Create the high-end values
291 for i in range(256 - whitepoint):
292 red.append(rgb_white[0])
293 green.append(rgb_white[1])
294 blue.append(rgb_white[2])
296 # Return converted image
297 image = image.convert("RGB")
298 return _lut(image, red + green + blue)
301def contain(
302 image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
303) -> Image.Image:
304 """
305 Returns a resized version of the image, set to the maximum width and height
306 within the requested size, while maintaining the original aspect ratio.
308 :param image: The image to resize.
309 :param size: The requested output size in pixels, given as a
310 (width, height) tuple.
311 :param method: Resampling method to use. Default is
312 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
313 See :ref:`concept-filters`.
314 :return: An image.
315 """
317 if image.height == 0:
318 return image.crop((0, 0, size[0], 0))
319 im_ratio = image.width / image.height
320 if size[1] == 0:
321 return image.crop((0, 0, 0, 0))
322 dest_ratio = size[0] / size[1]
324 if im_ratio != dest_ratio:
325 if im_ratio > dest_ratio:
326 new_height = round(image.height / image.width * size[0])
327 if new_height != size[1]:
328 size = (size[0], new_height)
329 else:
330 new_width = round(image.width / image.height * size[1])
331 if new_width != size[0]:
332 size = (new_width, size[1])
333 if min(size) == 0:
334 return image.crop((0, 0, *size))
335 return image.resize(size, resample=method)
338def cover(
339 image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
340) -> Image.Image:
341 """
342 Returns a resized version of the image, so that the requested size is
343 covered, while maintaining the original aspect ratio.
345 :param image: The image to resize.
346 :param size: The requested output size in pixels, given as a
347 (width, height) tuple.
348 :param method: Resampling method to use. Default is
349 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
350 See :ref:`concept-filters`.
351 :return: An image.
352 """
354 if image.width == 0:
355 return image.crop((0, 0, 0, size[1]))
356 if image.height == 0:
357 return image.crop((0, 0, size[0], 0))
358 im_ratio = image.width / image.height
359 if size[1] == 0:
360 return image.crop((0, 0, 0, 0))
361 dest_ratio = size[0] / size[1]
363 if im_ratio != dest_ratio:
364 if im_ratio < dest_ratio:
365 new_height = round(image.height / image.width * size[0])
366 if new_height != size[1]:
367 size = (size[0], new_height)
368 else:
369 new_width = round(image.width / image.height * size[1])
370 if new_width != size[0]:
371 size = (new_width, size[1])
372 return image.resize(size, resample=method)
375def _new_with_fill(
376 image: Image.Image, size: tuple[int, int], fill: str | int | tuple[int, ...] | None
377) -> Image.Image:
378 color = _color(fill, image.mode)
379 if image.palette:
380 mode = image.palette.mode
381 palette = ImagePalette.ImagePalette(mode, image.getpalette(mode))
382 if isinstance(color, tuple) and len(color) in (3, 4):
383 if image.mode == "PA":
384 alpha = color[3] if len(color) == 4 else 255
385 color = (palette.getcolor(color[:3], image), alpha)
386 else:
387 color = palette.getcolor(color, image)
388 else:
389 palette = None
390 out = Image.new(image.mode, size, color)
391 if palette:
392 out.putpalette(palette.palette, mode)
393 if "transparency" in image.info:
394 out.info["transparency"] = image.info["transparency"]
395 return out
398def pad(
399 image: Image.Image,
400 size: tuple[int, int],
401 method: int = Image.Resampling.BICUBIC,
402 color: str | int | tuple[int, ...] | None = None,
403 centering: tuple[float, float] = (0.5, 0.5),
404) -> Image.Image:
405 """
406 Returns a resized and padded version of the image, expanded to fill the
407 requested aspect ratio and size.
409 :param image: The image to resize and crop.
410 :param size: The requested output size in pixels, given as a
411 (width, height) tuple.
412 :param method: Resampling method to use. Default is
413 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
414 See :ref:`concept-filters`.
415 :param color: The background color of the padded image.
416 :param centering: Control the position of the original image within the
417 padded version.
419 (0.5, 0.5) will keep the image centered
420 (0, 0) will keep the image aligned to the top left
421 (1, 1) will keep the image aligned to the bottom
422 right
423 :return: An image.
424 """
426 resized = contain(image, size, method)
427 if resized.size == size:
428 out = resized
429 else:
430 out = _new_with_fill(resized, size, color)
431 if resized.width != size[0]:
432 x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
433 out.paste(resized, (x, 0))
434 else:
435 y = round((size[1] - resized.height) * max(0, min(centering[1], 1)))
436 out.paste(resized, (0, y))
437 return out
440def crop(image: Image.Image, border: int | tuple[int, ...] = 0) -> Image.Image:
441 """
442 Remove border from image. This function works on all image modes.
444 .. seealso:: :py:meth:`~PIL.Image.Image.crop`
446 :param image: The image to crop.
447 :param border: The number of pixels to remove. An integer removes the same
448 number of pixels from all four sides. A 2-tuple specifies
449 the number of pixels to remove horizontally and vertically.
450 A 4-tuple specifies the number of pixels to remove from the
451 left, top, right and bottom of the image.
452 :return: An image.
453 """
454 left, top, right, bottom = _border(border)
455 return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
458def scale(
459 image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC
460) -> Image.Image:
461 """
462 Returns a rescaled image by a specific factor given in parameter.
463 A factor greater than 1 expands the image, between 0 and 1 contracts the
464 image.
466 :param image: The image to rescale.
467 :param factor: The expansion factor, as a float.
468 :param resample: Resampling method to use. Default is
469 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
470 See :ref:`concept-filters`.
471 :returns: An :py:class:`~PIL.Image.Image` object.
472 """
473 if factor == 1:
474 return image.copy()
475 elif factor <= 0:
476 msg = "the factor must be greater than 0"
477 raise ValueError(msg)
478 else:
479 size = (round(factor * image.width), round(factor * image.height))
480 if min(size) == 0:
481 return image.crop((0, 0, *size))
482 return image.resize(size, resample)
485class SupportsGetMesh(Protocol):
486 """
487 An object that supports the ``getmesh`` method, taking an image as an
488 argument, and returning a list of tuples. Each tuple contains two tuples,
489 the source box as a tuple of 4 integers, and a tuple of 8 integers for the
490 final quadrilateral, in order of top left, bottom left, bottom right, top
491 right.
492 """
494 def getmesh(
495 self, image: Image.Image
496 ) -> list[
497 tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
498 ]: ...
501def deform(
502 image: Image.Image,
503 deformer: SupportsGetMesh,
504 resample: int = Image.Resampling.BILINEAR,
505) -> Image.Image:
506 """
507 Deform the image.
509 :param image: The image to deform.
510 :param deformer: A deformer object. Any object that implements a
511 ``getmesh`` method can be used.
512 :param resample: An optional resampling filter. Same values possible as
513 in the PIL.Image.transform function.
514 :return: An image.
515 """
516 return image.transform(
517 image.size, Image.Transform.MESH, deformer.getmesh(image), resample
518 )
521def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image:
522 """
523 Equalize the image histogram. This function applies a non-linear
524 mapping to the input image, in order to create a uniform
525 distribution of grayscale values in the output image.
527 :param image: The image to equalize.
528 :param mask: An optional mask. If given, only the pixels selected by
529 the mask are included in the analysis.
530 :return: An image.
531 """
532 if image.mode == "P":
533 image = image.convert("RGB")
534 h = image.histogram(mask)
535 lut = []
536 for b in range(0, len(h), 256):
537 histo = [_f for _f in h[b : b + 256] if _f]
538 if len(histo) <= 1:
539 lut.extend(list(range(256)))
540 else:
541 step = (sum(histo) - histo[-1]) // 255
542 if not step:
543 lut.extend(list(range(256)))
544 else:
545 n = step // 2
546 for i in range(256):
547 lut.append(n // step)
548 n = n + h[i + b]
549 return _lut(image, lut)
552def expand(
553 image: Image.Image,
554 border: int | tuple[int, ...] = 0,
555 fill: str | int | tuple[int, ...] = 0,
556) -> Image.Image:
557 """
558 Add border to the image
560 :param image: The image to expand.
561 :param border: Border width, in pixels. An integer adds the same width to
562 all four sides. A 2-tuple specifies the horizontal and
563 vertical border widths. A 4-tuple specifies the widths of
564 the left, top, right and bottom borders.
565 :param fill: Pixel fill value (a color value). Default is 0 (black).
566 :return: An image.
567 """
568 left, top, right, bottom = _border(border)
569 width = left + image.size[0] + right
570 height = top + image.size[1] + bottom
571 out = _new_with_fill(image, (width, height), fill)
572 out.paste(image, (left, top))
573 return out
576def fit(
577 image: Image.Image,
578 size: tuple[int, int],
579 method: int = Image.Resampling.BICUBIC,
580 bleed: float = 0.0,
581 centering: tuple[float, float] = (0.5, 0.5),
582) -> Image.Image:
583 """
584 Returns a resized and cropped version of the image, cropped to the
585 requested aspect ratio and size.
587 This function was contributed by Kevin Cazabon.
589 :param image: The image to resize and crop.
590 :param size: The requested output size in pixels, given as a
591 (width, height) tuple.
592 :param method: Resampling method to use. Default is
593 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
594 See :ref:`concept-filters`.
595 :param bleed: Remove a border around the outside of the image from all
596 four edges. The value is a decimal percentage (use 0.01 for
597 one percent). The default value is 0 (no border).
598 Cannot be greater than or equal to 0.5.
599 :param centering: Control the cropping position. Use (0.5, 0.5) for
600 center cropping (e.g. if cropping the width, take 50% off
601 of the left side, and therefore 50% off the right side).
602 (0.0, 0.0) will crop from the top left corner (i.e. if
603 cropping the width, take all of the crop off of the right
604 side, and if cropping the height, take all of it off the
605 bottom). (1.0, 1.0) will crop from the bottom right
606 corner, etc. (i.e. if cropping the width, take all of the
607 crop off the left side, and if cropping the height take
608 none from the bottom, and therefore all off the top).
609 :return: An image.
610 """
612 # by Kevin Cazabon, Feb 17/2000
613 # kevin@cazabon.com
614 # https://www.cazabon.com
616 if min(size) == 0:
617 return image.crop((0, 0, *size))
618 centering_x, centering_y = centering
620 if not 0.0 <= centering_x <= 1.0:
621 centering_x = 0.5
622 if not 0.0 <= centering_y <= 1.0:
623 centering_y = 0.5
625 if not 0.0 <= bleed < 0.5:
626 bleed = 0.0
628 # calculate the area to use for resizing and cropping, subtracting
629 # the 'bleed' around the edges
631 # number of pixels to trim off on Top and Bottom, Left and Right
632 bleed_pixels = (bleed * image.size[0], bleed * image.size[1])
634 live_size = (
635 image.size[0] - bleed_pixels[0] * 2,
636 image.size[1] - bleed_pixels[1] * 2,
637 )
639 # calculate the aspect ratio of the live_size
640 if live_size[1] == 0:
641 return image.crop((0, 0, *size))
642 live_size_ratio = live_size[0] / live_size[1]
644 # calculate the aspect ratio of the output image
645 output_ratio = size[0] / size[1]
647 # figure out if the sides or top/bottom will be cropped off
648 if live_size_ratio == output_ratio:
649 # live_size is already the needed ratio
650 crop_width = live_size[0]
651 crop_height = live_size[1]
652 elif live_size_ratio >= output_ratio:
653 # live_size is wider than what's needed, crop the sides
654 crop_width = output_ratio * live_size[1]
655 crop_height = live_size[1]
656 else:
657 # live_size is taller than what's needed, crop the top and bottom
658 crop_width = live_size[0]
659 crop_height = live_size[0] / output_ratio
661 # make the crop
662 crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
663 crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
665 crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
667 # resize the image and return it
668 return image.resize(size, method, box=crop)
671def flip(image: Image.Image) -> Image.Image:
672 """
673 Flip the image vertically (top to bottom).
675 :param image: The image to flip.
676 :return: An image.
677 """
678 return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
681def grayscale(image: Image.Image) -> Image.Image:
682 """
683 Convert the image to grayscale.
685 :param image: The image to convert.
686 :return: An image.
687 """
688 return image.convert("L")
691def sepia(image: Image.Image) -> Image.Image:
692 """
693 Apply a sepia tone to the image.
695 :param image: The image to modify.
696 :return: An image.
698 .. versionadded:: 13.1.0
699 """
700 if image.mode != "RGB":
701 image = image.convert("RGB")
703 return image.convert(
704 "RGB",
705 matrix=(
706 0.393,
707 0.769,
708 0.189,
709 0,
710 0.349,
711 0.686,
712 0.168,
713 0,
714 0.272,
715 0.534,
716 0.131,
717 0,
718 ),
719 )
722def _sobel(image: Image.Image) -> Image.Image:
723 if image.mode != "L":
724 image = image.convert("L")
726 Kx = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]
727 Ky = [[1, 2, 1], [0, 0, 0], [-1, -2, -1]]
729 out = Image.new("L", image.size)
731 for x in range(1, image.width - 1):
732 for y in range(1, image.height - 1):
734 gx = gy = 0.0
736 for dy in (-1, 0, 1):
737 for dx in (-1, 0, 1):
738 v = image.getpixel((x + dx, y + dy))
739 assert isinstance(v, (int, float))
741 gx += v * Kx[dy + 1][dx + 1]
742 gy += v * Ky[dy + 1][dx + 1]
744 # Approximate gradient magnitude and clamp to [0, 255]
745 mag = int(min(255, abs(gx) + abs(gy)))
746 out.putpixel((x, y), mag)
748 return out
751def neon(
752 image: Image.Image, color: tuple[int, int, int] = (255, 0, 255), alpha: float = 0.2
753) -> Image.Image:
754 """
755 Apply a neon glow effect to an image using edge detection,
756 blur-based glow generation, colorization, and alpha blending.
758 :param image: The image to be used for the effect
759 :param color: RGB color to be used for the neon effect
760 :param alpha: The intensity of the neon effect. If alpha is 0.0, a copy of the
761 image is returned unaltered.
762 :return: An image.
764 .. versionadded:: 13.1.0
765 """
766 edges = _sobel(image).filter(ImageFilter.GaussianBlur(2))
768 # Apply a glow-enhancing mask transformation
769 glow = edges.point(lambda value: 255 - ((255 - value) ** 2 // 255))
771 # Apply a color tint to the intensity mask
772 neon = Image.merge(
773 "RGB",
774 tuple(glow.point(lambda value: min(255, int(value * c / 255))) for c in color),
775 )
777 return Image.blend(image.convert("RGB"), neon, alpha)
780def invert(image: Image.Image) -> Image.Image:
781 """
782 Invert (negate) the image.
784 :param image: The image to invert.
785 :return: An image.
786 """
787 lut = list(range(255, -1, -1))
788 return image.point(lut) if image.mode == "1" else _lut(image, lut, palette=True)
791def mirror(image: Image.Image) -> Image.Image:
792 """
793 Flip image horizontally (left to right).
795 :param image: The image to mirror.
796 :return: An image.
797 """
798 return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
801def posterize(image: Image.Image, bits: int) -> Image.Image:
802 """
803 Reduce the number of bits for each color channel.
805 :param image: The image to posterize.
806 :param bits: The number of bits to keep for each channel (1-8).
807 :return: An image.
808 """
809 if not 1 <= bits <= 8:
810 msg = "bits must be between or equal to 1 and 8"
811 raise ValueError(msg)
812 mask = ~(2 ** (8 - bits) - 1)
813 lut = [i & mask for i in range(256)]
814 return _lut(image, lut, palette=True)
817def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
818 """
819 Invert all pixel values above a threshold.
821 :param image: The image to solarize.
822 :param threshold: All pixels above this grayscale level are inverted.
823 :return: An image.
824 """
825 lut = []
826 for i in range(256):
827 if i < threshold:
828 lut.append(i)
829 else:
830 lut.append(255 - i)
831 return _lut(image, lut, palette=True)
834@overload
835def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
838@overload
839def exif_transpose(
840 image: Image.Image, *, in_place: Literal[False] = False
841) -> Image.Image: ...
844def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
845 """
846 If an image has an EXIF Orientation tag, other than 1, transpose the image
847 accordingly, and remove the orientation data.
849 :param image: The image to transpose.
850 :param in_place: Boolean. Keyword-only argument.
851 If ``True``, the original image is modified in-place, and ``None`` is returned.
852 If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
853 with the transposition applied. If there is no transposition, a copy of the
854 image will be returned.
855 """
856 image.load()
857 image_exif = image.getexif()
858 orientation = image_exif.get(ExifTags.Base.Orientation, 1)
859 method = {
860 2: Image.Transpose.FLIP_LEFT_RIGHT,
861 3: Image.Transpose.ROTATE_180,
862 4: Image.Transpose.FLIP_TOP_BOTTOM,
863 5: Image.Transpose.TRANSPOSE,
864 6: Image.Transpose.ROTATE_270,
865 7: Image.Transpose.TRANSVERSE,
866 8: Image.Transpose.ROTATE_90,
867 }.get(orientation)
868 if method is not None:
869 if in_place:
870 image.im = image.im.transpose(method)
871 image._size = image.im.size
872 else:
873 transposed_image = image.transpose(method)
874 exif_image = image if in_place else transposed_image
876 exif = exif_image.getexif()
877 if ExifTags.Base.Orientation in exif:
878 del exif[ExifTags.Base.Orientation]
879 if "exif" in exif_image.info:
880 exif_image.info["exif"] = exif.tobytes()
881 elif "Raw profile type exif" in exif_image.info:
882 data = exif.tobytes()
883 exif_image.info["Raw profile type exif"] = (
884 f"\nexif\n{len(data):8}\n{data.hex()}\n"
885 )
886 for key in ("XML:com.adobe.xmp", "xmp"):
887 if key in exif_image.info:
888 for pattern in (
889 r'tiff:Orientation="([0-9])"',
890 r"<tiff:Orientation>([0-9])</tiff:Orientation>",
891 ):
892 value = exif_image.info[key]
893 exif_image.info[key] = (
894 re.sub(pattern, "", value)
895 if isinstance(value, str)
896 else re.sub(pattern.encode(), b"", value)
897 )
898 if not in_place:
899 return transposed_image
900 elif not in_place:
901 return image.copy()
902 return None