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

296 statements  

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 

20 

21import functools 

22import operator 

23import re 

24from collections.abc import Sequence 

25from typing import Literal, Protocol, cast, overload 

26 

27from . import ExifTags, Image, ImagePalette 

28 

29# 

30# helpers 

31 

32 

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 

45 

46 

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 

52 

53 color = ImageColor.getcolor(color, mode) 

54 return color 

55 

56 

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) 

69 

70 

71# 

72# actions 

73 

74 

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). 

88 

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. 

98 

99 .. versionadded:: 8.2.0 

100 

101 :return: An image. 

102 """ 

103 if preserve_tone: 

104 histogram = image.convert("L").histogram(mask) 

105 else: 

106 histogram = image.histogram(mask) 

107 

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) 

169 

170 

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). 

192 

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 """ 

202 

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) 

215 

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 

220 

221 # Empty lists for the mapping 

222 red = [] 

223 green = [] 

224 blue = [] 

225 

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]) 

231 

232 # Create the mapping (2-color) 

233 if rgb_mid is None: 

234 range_map = range(whitepoint - blackpoint) 

235 

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 ) 

246 

247 # Create the mapping (3-color) 

248 else: 

249 range_map1 = range(midpoint - blackpoint) 

250 range_map2 = range(whitepoint - midpoint) 

251 

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)) 

268 

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]) 

274 

275 # Return converted image 

276 image = image.convert("RGB") 

277 return _lut(image, red + green + blue) 

278 

279 

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. 

286 

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 """ 

295 

296 im_ratio = image.width / image.height 

297 dest_ratio = size[0] / size[1] 

298 

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) 

309 

310 

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. 

317 

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 """ 

326 

327 im_ratio = image.width / image.height 

328 dest_ratio = size[0] / size[1] 

329 

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) 

340 

341 

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 

357 

358 

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. 

369 

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. 

379 

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 """ 

386 

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 

399 

400 

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. 

405 

406 .. seealso:: :py:meth:`~PIL.Image.Image.crop` 

407 

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)) 

414 

415 

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. 

423 

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) 

439 

440 

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 """ 

449 

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 ]: ... 

455 

456 

457def deform( 

458 image: Image.Image, 

459 deformer: SupportsGetMesh, 

460 resample: int = Image.Resampling.BILINEAR, 

461) -> Image.Image: 

462 """ 

463 Deform the image. 

464 

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 ) 

475 

476 

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. 

482 

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) 

506 

507 

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 

515 

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 

527 

528 

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. 

539 

540 This function was contributed by Kevin Cazabon. 

541 

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 """ 

564 

565 # by Kevin Cazabon, Feb 17/2000 

566 # kevin@cazabon.com 

567 # https://www.cazabon.com 

568 

569 centering_x, centering_y = centering 

570 

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 

575 

576 if not 0.0 <= bleed < 0.5: 

577 bleed = 0.0 

578 

579 # calculate the area to use for resizing and cropping, subtracting 

580 # the 'bleed' around the edges 

581 

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]) 

584 

585 live_size = ( 

586 image.size[0] - bleed_pixels[0] * 2, 

587 image.size[1] - bleed_pixels[1] * 2, 

588 ) 

589 

590 # calculate the aspect ratio of the live_size 

591 live_size_ratio = live_size[0] / live_size[1] 

592 

593 # calculate the aspect ratio of the output image 

594 output_ratio = size[0] / size[1] 

595 

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 

609 

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 

613 

614 crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) 

615 

616 # resize the image and return it 

617 return image.resize(size, method, box=crop) 

618 

619 

620def flip(image: Image.Image) -> Image.Image: 

621 """ 

622 Flip the image vertically (top to bottom). 

623 

624 :param image: The image to flip. 

625 :return: An image. 

626 """ 

627 return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) 

628 

629 

630def grayscale(image: Image.Image) -> Image.Image: 

631 """ 

632 Convert the image to grayscale. 

633 

634 :param image: The image to convert. 

635 :return: An image. 

636 """ 

637 return image.convert("L") 

638 

639 

640def invert(image: Image.Image) -> Image.Image: 

641 """ 

642 Invert (negate) the image. 

643 

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) 

649 

650 

651def mirror(image: Image.Image) -> Image.Image: 

652 """ 

653 Flip image horizontally (left to right). 

654 

655 :param image: The image to mirror. 

656 :return: An image. 

657 """ 

658 return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) 

659 

660 

661def posterize(image: Image.Image, bits: int) -> Image.Image: 

662 """ 

663 Reduce the number of bits for each color channel. 

664 

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) 

672 

673 

674def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: 

675 """ 

676 Invert all pixel values above a threshold. 

677 

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) 

689 

690 

691@overload 

692def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ... 

693 

694 

695@overload 

696def exif_transpose( 

697 image: Image.Image, *, in_place: Literal[False] = False 

698) -> Image.Image: ... 

699 

700 

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. 

705 

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 

732 

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