Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/imageio-2.35.1-py3.8.egg/imageio/plugins/pillow.py: 23%

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

203 statements  

1# -*- coding: utf-8 -*- 

2# imageio is distributed under the terms of the (new) BSD License. 

3 

4""" Read/Write images using Pillow/PIL. 

5 

6Backend Library: `Pillow <https://pillow.readthedocs.io/en/stable/>`_ 

7 

8Plugin that wraps the the Pillow library. Pillow is a friendly fork of PIL 

9(Python Image Library) and supports reading and writing of common formats (jpg, 

10png, gif, tiff, ...). For, the complete list of features and supported formats 

11please refer to pillows official docs (see the Backend Library link). 

12 

13Parameters 

14---------- 

15request : Request 

16 A request object representing the resource to be operated on. 

17 

18Methods 

19------- 

20 

21.. autosummary:: 

22 :toctree: _plugins/pillow 

23 

24 PillowPlugin.read 

25 PillowPlugin.write 

26 PillowPlugin.iter 

27 PillowPlugin.get_meta 

28 

29""" 

30 

31import sys 

32import warnings 

33from io import BytesIO 

34from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union, cast 

35 

36import numpy as np 

37from PIL import ExifTags, GifImagePlugin, Image, ImageSequence, UnidentifiedImageError 

38from PIL import __version__ as pil_version # type: ignore 

39 

40from ..core.request import URI_BYTES, InitializationError, IOMode, Request 

41from ..core.v3_plugin_api import ImageProperties, PluginV3 

42from ..typing import ArrayLike 

43 

44 

45def pillow_version() -> Tuple[int]: 

46 return tuple(int(x) for x in pil_version.split(".")) 

47 

48 

49def _exif_orientation_transform(orientation: int, mode: str) -> Callable: 

50 # get transformation that transforms an image from a 

51 # given EXIF orientation into the standard orientation 

52 

53 # -1 if the mode has color channel, 0 otherwise 

54 axis = -2 if Image.getmodebands(mode) > 1 else -1 

55 

56 EXIF_ORIENTATION = { 

57 1: lambda x: x, 

58 2: lambda x: np.flip(x, axis=axis), 

59 3: lambda x: np.rot90(x, k=2), 

60 4: lambda x: np.flip(x, axis=axis - 1), 

61 5: lambda x: np.flip(np.rot90(x, k=3), axis=axis), 

62 6: lambda x: np.rot90(x, k=3), 

63 7: lambda x: np.flip(np.rot90(x, k=1), axis=axis), 

64 8: lambda x: np.rot90(x, k=1), 

65 } 

66 

67 return EXIF_ORIENTATION[orientation] 

68 

69 

70class PillowPlugin(PluginV3): 

71 def __init__(self, request: Request) -> None: 

72 """Instantiate a new Pillow Plugin Object 

73 

74 Parameters 

75 ---------- 

76 request : {Request} 

77 A request object representing the resource to be operated on. 

78 

79 """ 

80 

81 super().__init__(request) 

82 

83 # Register HEIF opener for Pillow 

84 try: 

85 from pillow_heif import register_heif_opener 

86 except ImportError: 

87 pass 

88 else: 

89 register_heif_opener() 

90 

91 # Register AVIF opener for Pillow 

92 try: 

93 from pillow_heif import register_avif_opener 

94 except ImportError: 

95 pass 

96 else: 

97 register_avif_opener() 

98 

99 self._image: Image = None 

100 self.images_to_write = [] 

101 

102 if request.mode.io_mode == IOMode.read: 

103 try: 

104 with Image.open(request.get_file()): 

105 # Check if it is generally possible to read the image. 

106 # This will not read any data and merely try to find a 

107 # compatible pillow plugin (ref: the pillow docs). 

108 pass 

109 except UnidentifiedImageError: 

110 if request._uri_type == URI_BYTES: 

111 raise InitializationError( 

112 "Pillow can not read the provided bytes." 

113 ) from None 

114 else: 

115 raise InitializationError( 

116 f"Pillow can not read {request.raw_uri}." 

117 ) from None 

118 

119 self._image = Image.open(self._request.get_file()) 

120 else: 

121 self.save_args = {} 

122 

123 extension = self.request.extension or self.request.format_hint 

124 if extension is None: 

125 warnings.warn( 

126 "Can't determine file format to write as. You _must_" 

127 " set `format` during write or the call will fail. Use " 

128 "`extension` to supress this warning. ", 

129 UserWarning, 

130 ) 

131 return 

132 

133 tirage = [Image.preinit, Image.init] 

134 for format_loader in tirage: 

135 format_loader() 

136 if extension in Image.registered_extensions().keys(): 

137 return 

138 

139 raise InitializationError( 

140 f"Pillow can not write `{extension}` files." 

141 ) from None 

142 

143 def close(self) -> None: 

144 self._flush_writer() 

145 

146 if self._image: 

147 self._image.close() 

148 

149 self._request.finish() 

150 

151 def read( 

152 self, 

153 *, 

154 index: int = None, 

155 mode: str = None, 

156 rotate: bool = False, 

157 apply_gamma: bool = False, 

158 writeable_output: bool = True, 

159 pilmode: str = None, 

160 exifrotate: bool = None, 

161 as_gray: bool = None, 

162 ) -> np.ndarray: 

163 """ 

164 Parses the given URI and creates a ndarray from it. 

165 

166 Parameters 

167 ---------- 

168 index : int 

169 If the ImageResource contains multiple ndimages, and index is an 

170 integer, select the index-th ndimage from among them and return it. 

171 If index is an ellipsis (...), read all ndimages in the file and 

172 stack them along a new batch dimension and return them. If index is 

173 None, this plugin reads the first image of the file (index=0) unless 

174 the image is a GIF or APNG, in which case all images are read 

175 (index=...). 

176 mode : str 

177 Convert the image to the given mode before returning it. If None, 

178 the mode will be left unchanged. Possible modes can be found at: 

179 https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes 

180 rotate : bool 

181 If True and the image contains an EXIF orientation tag, 

182 apply the orientation before returning the ndimage. 

183 apply_gamma : bool 

184 If True and the image contains metadata about gamma, apply gamma 

185 correction to the image. 

186 writable_output : bool 

187 If True, ensure that the image is writable before returning it to 

188 the user. This incurs a full copy of the pixel data if the data 

189 served by pillow is read-only. Consequentially, setting this flag to 

190 False improves performance for some images. 

191 pilmode : str 

192 Deprecated, use `mode` instead. 

193 exifrotate : bool 

194 Deprecated, use `rotate` instead. 

195 as_gray : bool 

196 Deprecated. Exists to raise a constructive error message. 

197 

198 Returns 

199 ------- 

200 ndimage : ndarray 

201 A numpy array containing the loaded image data 

202 

203 Notes 

204 ----- 

205 If you read a paletted image (e.g. GIF) then the plugin will apply the 

206 palette by default. Should you wish to read the palette indices of each 

207 pixel use ``mode="P"``. The coresponding color pallete can be found in 

208 the image's metadata using the ``palette`` key when metadata is 

209 extracted using the ``exclude_applied=False`` kwarg. The latter is 

210 needed, as palettes are applied by default and hence excluded by default 

211 to keep metadata and pixel data consistent. 

212 

213 """ 

214 

215 if pilmode is not None: 

216 warnings.warn( 

217 "`pilmode` is deprecated. Use `mode` instead.", DeprecationWarning 

218 ) 

219 mode = pilmode 

220 

221 if exifrotate is not None: 

222 warnings.warn( 

223 "`exifrotate` is deprecated. Use `rotate` instead.", DeprecationWarning 

224 ) 

225 rotate = exifrotate 

226 

227 if as_gray is not None: 

228 raise TypeError( 

229 "The keyword `as_gray` is no longer supported." 

230 "Use `mode='F'` for a backward-compatible result, or " 

231 " `mode='L'` for an integer-valued result." 

232 ) 

233 

234 if self._image.format == "GIF": 

235 # Converting GIF P frames to RGB 

236 # https://github.com/python-pillow/Pillow/pull/6150 

237 GifImagePlugin.LOADING_STRATEGY = ( 

238 GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY 

239 ) 

240 

241 if index is None: 

242 if self._image.format == "GIF": 

243 index = Ellipsis 

244 elif self._image.custom_mimetype == "image/apng": 

245 index = Ellipsis 

246 else: 

247 index = 0 

248 

249 if isinstance(index, int): 

250 # will raise IO error if index >= number of frames in image 

251 self._image.seek(index) 

252 image = self._apply_transforms( 

253 self._image, mode, rotate, apply_gamma, writeable_output 

254 ) 

255 else: 

256 iterator = self.iter( 

257 mode=mode, 

258 rotate=rotate, 

259 apply_gamma=apply_gamma, 

260 writeable_output=writeable_output, 

261 ) 

262 image = np.stack([im for im in iterator], axis=0) 

263 

264 return image 

265 

266 def iter( 

267 self, 

268 *, 

269 mode: str = None, 

270 rotate: bool = False, 

271 apply_gamma: bool = False, 

272 writeable_output: bool = True, 

273 ) -> Iterator[np.ndarray]: 

274 """ 

275 Iterate over all ndimages/frames in the URI 

276 

277 Parameters 

278 ---------- 

279 mode : {str, None} 

280 Convert the image to the given mode before returning it. If None, 

281 the mode will be left unchanged. Possible modes can be found at: 

282 https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes 

283 rotate : {bool} 

284 If set to ``True`` and the image contains an EXIF orientation tag, 

285 apply the orientation before returning the ndimage. 

286 apply_gamma : {bool} 

287 If ``True`` and the image contains metadata about gamma, apply gamma 

288 correction to the image. 

289 writable_output : bool 

290 If True, ensure that the image is writable before returning it to 

291 the user. This incurs a full copy of the pixel data if the data 

292 served by pillow is read-only. Consequentially, setting this flag to 

293 False improves performance for some images. 

294 """ 

295 

296 for im in ImageSequence.Iterator(self._image): 

297 yield self._apply_transforms( 

298 im, mode, rotate, apply_gamma, writeable_output 

299 ) 

300 

301 def _apply_transforms( 

302 self, image, mode, rotate, apply_gamma, writeable_output 

303 ) -> np.ndarray: 

304 if mode is not None: 

305 image = image.convert(mode) 

306 elif image.mode == "P": 

307 # adjust for pillow9 changes 

308 # see: https://github.com/python-pillow/Pillow/issues/5929 

309 image = image.convert(image.palette.mode) 

310 elif image.format == "PNG" and image.mode == "I": 

311 major, minor, patch = pillow_version() 

312 

313 if sys.byteorder == "little": 

314 desired_mode = "I;16" 

315 else: # pragma: no cover 

316 # can't test big-endian in GH-Actions 

317 desired_mode = "I;16B" 

318 

319 if major < 10: # pragma: no cover 

320 warnings.warn( 

321 "Loading 16-bit (uint16) PNG as int32 due to limitations " 

322 "in pillow's PNG decoder. This will be fixed in a future " 

323 "version of pillow which will make this warning dissapear.", 

324 UserWarning, 

325 ) 

326 elif minor < 1: # pragma: no cover 

327 # pillow<10.1.0 can directly decode into 16-bit grayscale 

328 image.mode = desired_mode 

329 else: 

330 # pillow >= 10.1.0 

331 image = image.convert(desired_mode) 

332 

333 image = np.asarray(image) 

334 

335 meta = self.metadata(index=self._image.tell(), exclude_applied=False) 

336 if rotate and "Orientation" in meta: 

337 transformation = _exif_orientation_transform( 

338 meta["Orientation"], self._image.mode 

339 ) 

340 image = transformation(image) 

341 

342 if apply_gamma and "gamma" in meta: 

343 gamma = float(meta["gamma"]) 

344 scale = float(65536 if image.dtype == np.uint16 else 255) 

345 gain = 1.0 

346 image = ((image / scale) ** gamma) * scale * gain + 0.4999 

347 image = np.round(image).astype(np.uint8) 

348 

349 if writeable_output and not image.flags["WRITEABLE"]: 

350 image = np.array(image) 

351 

352 return image 

353 

354 def write( 

355 self, 

356 ndimage: Union[ArrayLike, List[ArrayLike]], 

357 *, 

358 mode: str = None, 

359 format: str = None, 

360 is_batch: bool = None, 

361 **kwargs, 

362 ) -> Optional[bytes]: 

363 """ 

364 Write an ndimage to the URI specified in path. 

365 

366 If the URI points to a file on the current host and the file does not 

367 yet exist it will be created. If the file exists already, it will be 

368 appended if possible; otherwise, it will be replaced. 

369 

370 If necessary, the image is broken down along the leading dimension to 

371 fit into individual frames of the chosen format. If the format doesn't 

372 support multiple frames, and IOError is raised. 

373 

374 Parameters 

375 ---------- 

376 image : ndarray or list 

377 The ndimage to write. If a list is given each element is expected to 

378 be an ndimage. 

379 mode : str 

380 Specify the image's color format. If None (default), the mode is 

381 inferred from the array's shape and dtype. Possible modes can be 

382 found at: 

383 https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes 

384 format : str 

385 Optional format override. If omitted, the format to use is 

386 determined from the filename extension. If a file object was used 

387 instead of a filename, this parameter must always be used. 

388 is_batch : bool 

389 Explicitly tell the writer that ``image`` is a batch of images 

390 (True) or not (False). If None, the writer will guess this from the 

391 provided ``mode`` or ``image.shape``. While the latter often works, 

392 it may cause problems for small images due to aliasing of spatial 

393 and color-channel axes. 

394 kwargs : ... 

395 Extra arguments to pass to pillow. If a writer doesn't recognise an 

396 option, it is silently ignored. The available options are described 

397 in pillow's `image format documentation 

398 <https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html>`_ 

399 for each writer. 

400 

401 Notes 

402 ----- 

403 When writing batches of very narrow (2-4 pixels wide) gray images set 

404 the ``mode`` explicitly to avoid the batch being identified as a colored 

405 image. 

406 

407 """ 

408 if "fps" in kwargs: 

409 warnings.warn( 

410 "The keyword `fps` is no longer supported. Use `duration`" 

411 "(in ms) instead, e.g. `fps=50` == `duration=20` (1000 * 1/50).", 

412 DeprecationWarning, 

413 ) 

414 kwargs["duration"] = 1000 * 1 / kwargs.get("fps") 

415 

416 if isinstance(ndimage, list): 

417 ndimage = np.stack(ndimage, axis=0) 

418 is_batch = True 

419 else: 

420 ndimage = np.asarray(ndimage) 

421 

422 # check if ndimage is a batch of frames/pages (e.g. for writing GIF) 

423 # if mode is given, use it; otherwise fall back to image.ndim only 

424 if is_batch is not None: 

425 pass 

426 elif mode is not None: 

427 is_batch = ( 

428 ndimage.ndim > 3 if Image.getmodebands(mode) > 1 else ndimage.ndim > 2 

429 ) 

430 elif ndimage.ndim == 2: 

431 is_batch = False 

432 elif ndimage.ndim == 3 and ndimage.shape[-1] == 1: 

433 raise ValueError("Can't write images with one color channel.") 

434 elif ndimage.ndim == 3 and ndimage.shape[-1] in [2, 3, 4]: 

435 # Note: this makes a channel-last assumption 

436 is_batch = False 

437 else: 

438 is_batch = True 

439 

440 if not is_batch: 

441 ndimage = ndimage[None, ...] 

442 

443 for frame in ndimage: 

444 pil_frame = Image.fromarray(frame, mode=mode) 

445 if "bits" in kwargs: 

446 pil_frame = pil_frame.quantize(colors=2 ** kwargs["bits"]) 

447 self.images_to_write.append(pil_frame) 

448 

449 if ( 

450 format is not None 

451 and "format" in self.save_args 

452 and self.save_args["format"] != format 

453 ): 

454 old_format = self.save_args["format"] 

455 warnings.warn( 

456 "Changing the output format during incremental" 

457 " writes is strongly discouraged." 

458 f" Was `{old_format}`, is now `{format}`.", 

459 UserWarning, 

460 ) 

461 

462 extension = self.request.extension or self.request.format_hint 

463 self.save_args["format"] = format or Image.registered_extensions()[extension] 

464 self.save_args.update(kwargs) 

465 

466 # when writing to `bytes` we flush instantly 

467 result = None 

468 if self._request._uri_type == URI_BYTES: 

469 self._flush_writer() 

470 file = cast(BytesIO, self._request.get_file()) 

471 result = file.getvalue() 

472 

473 return result 

474 

475 def _flush_writer(self): 

476 if len(self.images_to_write) == 0: 

477 return 

478 

479 primary_image = self.images_to_write.pop(0) 

480 

481 if len(self.images_to_write) > 0: 

482 self.save_args["save_all"] = True 

483 self.save_args["append_images"] = self.images_to_write 

484 

485 primary_image.save(self._request.get_file(), **self.save_args) 

486 self.images_to_write.clear() 

487 self.save_args.clear() 

488 

489 def get_meta(self, *, index=0) -> Dict[str, Any]: 

490 return self.metadata(index=index, exclude_applied=False) 

491 

492 def metadata( 

493 self, index: int = None, exclude_applied: bool = True 

494 ) -> Dict[str, Any]: 

495 """Read ndimage metadata. 

496 

497 Parameters 

498 ---------- 

499 index : {integer, None} 

500 If the ImageResource contains multiple ndimages, and index is an 

501 integer, select the index-th ndimage from among them and return its 

502 metadata. If index is an ellipsis (...), read and return global 

503 metadata. If index is None, this plugin reads metadata from the 

504 first image of the file (index=0) unless the image is a GIF or APNG, 

505 in which case global metadata is read (index=...). 

506 exclude_applied : bool 

507 If True, exclude metadata fields that are applied to the image while 

508 reading. For example, if the binary data contains a rotation flag, 

509 the image is rotated by default and the rotation flag is excluded 

510 from the metadata to avoid confusion. 

511 

512 Returns 

513 ------- 

514 metadata : dict 

515 A dictionary of format-specific metadata. 

516 

517 """ 

518 

519 if index is None: 

520 if self._image.format == "GIF": 

521 index = Ellipsis 

522 elif self._image.custom_mimetype == "image/apng": 

523 index = Ellipsis 

524 else: 

525 index = 0 

526 

527 if isinstance(index, int) and self._image.tell() != index: 

528 self._image.seek(index) 

529 

530 metadata = self._image.info.copy() 

531 metadata["mode"] = self._image.mode 

532 metadata["shape"] = self._image.size 

533 

534 if self._image.mode == "P" and not exclude_applied: 

535 metadata["palette"] = np.asarray(tuple(self._image.palette.colors.keys())) 

536 

537 if self._image.getexif(): 

538 exif_data = { 

539 ExifTags.TAGS.get(key, "unknown"): value 

540 for key, value in dict(self._image.getexif()).items() 

541 } 

542 exif_data.pop("unknown", None) 

543 metadata.update(exif_data) 

544 

545 if exclude_applied: 

546 metadata.pop("Orientation", None) 

547 

548 return metadata 

549 

550 def properties(self, index: int = None) -> ImageProperties: 

551 """Standardized ndimage metadata 

552 Parameters 

553 ---------- 

554 index : int 

555 If the ImageResource contains multiple ndimages, and index is an 

556 integer, select the index-th ndimage from among them and return its 

557 properties. If index is an ellipsis (...), read and return the 

558 properties of all ndimages in the file stacked along a new batch 

559 dimension. If index is None, this plugin reads and returns the 

560 properties of the first image (index=0) unless the image is a GIF or 

561 APNG, in which case it reads and returns the properties all images 

562 (index=...). 

563 

564 Returns 

565 ------- 

566 properties : ImageProperties 

567 A dataclass filled with standardized image metadata. 

568 

569 Notes 

570 ----- 

571 This does not decode pixel data and is fast for large images. 

572 

573 """ 

574 

575 if index is None: 

576 if self._image.format == "GIF": 

577 index = Ellipsis 

578 elif self._image.custom_mimetype == "image/apng": 

579 index = Ellipsis 

580 else: 

581 index = 0 

582 

583 if index is Ellipsis: 

584 self._image.seek(0) 

585 else: 

586 self._image.seek(index) 

587 

588 if self._image.mode == "P": 

589 # mode of palette images is determined by their palette 

590 mode = self._image.palette.mode 

591 else: 

592 mode = self._image.mode 

593 

594 width: int = self._image.width 

595 height: int = self._image.height 

596 shape: Tuple[int, ...] = (height, width) 

597 

598 n_frames: Optional[int] = None 

599 if index is ...: 

600 n_frames = getattr(self._image, "n_frames", 1) 

601 shape = (n_frames, *shape) 

602 

603 dummy = np.asarray(Image.new(mode, (1, 1))) 

604 pil_shape: Tuple[int, ...] = dummy.shape 

605 if len(pil_shape) > 2: 

606 shape = (*shape, *pil_shape[2:]) 

607 

608 return ImageProperties( 

609 shape=shape, 

610 dtype=dummy.dtype, 

611 n_images=n_frames, 

612 is_batch=index is Ellipsis, 

613 )