1from pathlib import Path
2
3import numpy as np
4
5from ..config import known_extensions
6from .request import InitializationError, IOMode
7from .v3_plugin_api import ImageProperties, PluginV3
8
9
10def _legacy_default_index(format):
11 if format._name == "FFMPEG":
12 index = Ellipsis
13 elif format._name == "GIF-PIL":
14 index = Ellipsis
15 else:
16 index = 0
17
18 return index
19
20
21class LegacyPlugin(PluginV3):
22 """A plugin to make old (v2.9) plugins compatible with v3.0
23
24 .. depreciated:: 2.9
25 `legacy_get_reader` will be removed in a future version of imageio.
26 `legacy_get_writer` will be removed in a future version of imageio.
27
28 This plugin is a wrapper around the old FormatManager class and exposes
29 all the old plugins via the new API. On top of this it has
30 ``legacy_get_reader`` and ``legacy_get_writer`` methods to allow using
31 it with the v2.9 API.
32
33 Methods
34 -------
35 read(index=None, **kwargs)
36 Read the image at position ``index``.
37 write(image, **kwargs)
38 Write image to the URI.
39 iter(**kwargs)
40 Iteratively yield images from the given URI.
41 get_meta(index=None)
42 Return the metadata for the image at position ``index``.
43 legacy_get_reader(**kwargs)
44 Returns the v2.9 image reader. (depreciated)
45 legacy_get_writer(**kwargs)
46 Returns the v2.9 image writer. (depreciated)
47
48 Examples
49 --------
50
51 >>> import imageio.v3 as iio
52 >>> with iio.imopen("/path/to/image.tiff", "r", legacy_mode=True) as file:
53 >>> reader = file.legacy_get_reader() # depreciated
54 >>> for im in file.iter():
55 >>> print(im.shape)
56
57 """
58
59 def __init__(self, request, legacy_plugin):
60 """Instantiate a new Legacy Plugin
61
62 Parameters
63 ----------
64 uri : {str, pathlib.Path, bytes, file}
65 The resource to load the image from, e.g. a filename, pathlib.Path,
66 http address or file object, see the docs for more info.
67 legacy_plugin : Format
68 The (legacy) format to use to interface with the URI.
69
70 """
71 self._request = request
72 self._format = legacy_plugin
73
74 source = (
75 "<bytes>"
76 if isinstance(self._request.raw_uri, bytes)
77 else self._request.raw_uri
78 )
79 if self._request.mode.io_mode == IOMode.read:
80 if not self._format.can_read(request):
81 raise InitializationError(
82 f"`{self._format.name}`" f" can not read `{source}`."
83 )
84 else:
85 if not self._format.can_write(request):
86 raise InitializationError(
87 f"`{self._format.name}`" f" can not write to `{source}`."
88 )
89
90 def legacy_get_reader(self, **kwargs):
91 """legacy_get_reader(**kwargs)
92
93 a utility method to provide support vor the V2.9 API
94
95 Parameters
96 ----------
97 kwargs : ...
98 Further keyword arguments are passed to the reader. See :func:`.help`
99 to see what arguments are available for a particular format.
100 """
101
102 # Note: this will break thread-safety
103 self._request._kwargs = kwargs
104
105 # safeguard for DICOM plugin reading from folders
106 try:
107 assert Path(self._request.filename).is_dir()
108 except OSError:
109 pass # not a valid path on this OS
110 except AssertionError:
111 pass # not a folder
112 else:
113 return self._format.get_reader(self._request)
114
115 self._request.get_file().seek(0)
116 return self._format.get_reader(self._request)
117
118 def read(self, *, index=None, **kwargs):
119 """
120 Parses the given URI and creates a ndarray from it.
121
122 Parameters
123 ----------
124 index : {integer, None}
125 If the URI contains a list of ndimages return the index-th
126 image. If None, stack all images into an ndimage along the
127 0-th dimension (equivalent to np.stack(imgs, axis=0)).
128 kwargs : ...
129 Further keyword arguments are passed to the reader. See
130 :func:`.help` to see what arguments are available for a particular
131 format.
132
133 Returns
134 -------
135 ndimage : np.ndarray
136 A numpy array containing the decoded image data.
137
138 """
139
140 if index is None:
141 index = _legacy_default_index(self._format)
142
143 if index is Ellipsis:
144 img = np.stack([im for im in self.iter(**kwargs)])
145 return img
146
147 reader = self.legacy_get_reader(**kwargs)
148 return reader.get_data(index)
149
150 def legacy_get_writer(self, **kwargs):
151 """legacy_get_writer(**kwargs)
152
153 Returns a :class:`.Writer` object which can be used to write data
154 and meta data to the specified file.
155
156 Parameters
157 ----------
158 kwargs : ...
159 Further keyword arguments are passed to the writer. See :func:`.help`
160 to see what arguments are available for a particular format.
161 """
162
163 # Note: this will break thread-safety
164 self._request._kwargs = kwargs
165 return self._format.get_writer(self._request)
166
167 def write(self, ndimage, *, is_batch=None, metadata=None, **kwargs):
168 """
169 Write an ndimage to the URI specified in path.
170
171 If the URI points to a file on the current host and the file does not
172 yet exist it will be created. If the file exists already, it will be
173 appended if possible; otherwise, it will be replaced.
174
175 Parameters
176 ----------
177 ndimage : numpy.ndarray
178 The ndimage or list of ndimages to write.
179 is_batch : bool
180 If True, treat the supplied ndimage as a batch of images. If False,
181 treat the supplied ndimage as a single image. If None, try to
182 determine ``is_batch`` from the ndimage's shape and ndim.
183 metadata : dict
184 The metadata passed to write alongside the image.
185 kwargs : ...
186 Further keyword arguments are passed to the writer. See
187 :func:`.help` to see what arguments are available for a
188 particular format.
189
190
191 Returns
192 -------
193 buffer : bytes
194 When writing to the special target "<bytes>", this function will
195 return the encoded image data as a bytes string. Otherwise it
196 returns None.
197
198 Notes
199 -----
200 Automatically determining ``is_batch`` may fail for some images due to
201 shape aliasing. For example, it may classify a channel-first color image
202 as a batch of gray images. In most cases this automatic deduction works
203 fine (it has for almost a decade), but if you do have one of those edge
204 cases (or are worried that you might) consider explicitly setting
205 ``is_batch``.
206
207 """
208
209 if is_batch or isinstance(ndimage, (list, tuple)):
210 pass # ndimage is list of images
211 elif is_batch is False:
212 ndimage = [ndimage]
213 else:
214 # Write the largest possible block by guessing the meaning of each
215 # dimension from the shape/ndim and then checking if any batch
216 # dimensions are left.
217 ndimage = np.asanyarray(ndimage)
218 batch_dims = ndimage.ndim
219
220 # two spatial dimensions
221 batch_dims = max(batch_dims - 2, 0)
222
223 # packed (channel-last) image
224 if ndimage.ndim >= 3 and ndimage.shape[-1] < 5:
225 batch_dims = max(batch_dims - 1, 0)
226
227 # format supports volumetric images
228 ext_infos = known_extensions.get(self._request.extension, list())
229 for ext_info in ext_infos:
230 if self._format.name in ext_info.priority and ext_info.volume_support:
231 batch_dims = max(batch_dims - 1, 0)
232 break
233
234 if batch_dims == 0:
235 ndimage = [ndimage]
236
237 with self.legacy_get_writer(**kwargs) as writer:
238 for image in ndimage:
239 image = np.asanyarray(image)
240
241 if image.ndim < 2:
242 raise ValueError(
243 "The image must have at least two spatial dimensions."
244 )
245
246 if not np.issubdtype(image.dtype, np.number) and not np.issubdtype(
247 image.dtype, bool
248 ):
249 raise ValueError(
250 f"All images have to be numeric, and not `{image.dtype}`."
251 )
252
253 writer.append_data(image, metadata)
254
255 return writer.request.get_result()
256
257 def iter(self, **kwargs):
258 """Iterate over a list of ndimages given by the URI
259
260 Parameters
261 ----------
262 kwargs : ...
263 Further keyword arguments are passed to the reader. See
264 :func:`.help` to see what arguments are available for a particular
265 format.
266 """
267
268 reader = self.legacy_get_reader(**kwargs)
269 for image in reader:
270 yield image
271
272 def properties(self, index=None):
273 """Standardized ndimage metadata.
274
275 Parameters
276 ----------
277 index : int
278 The index of the ndimage for which to return properties. If the
279 index is out of bounds a ``ValueError`` is raised. If ``None``,
280 return the properties for the ndimage stack. If this is impossible,
281 e.g., due to shape mismatch, an exception will be raised.
282
283 Returns
284 -------
285 properties : ImageProperties
286 A dataclass filled with standardized image metadata.
287
288 """
289
290 if index is None:
291 index = _legacy_default_index(self._format)
292
293 # for backwards compatibility ... actually reads pixel data :(
294 if index is Ellipsis:
295 image = self.read(index=0)
296 n_images = self.legacy_get_reader().get_length()
297 return ImageProperties(
298 shape=(n_images, *image.shape),
299 dtype=image.dtype,
300 n_images=n_images,
301 is_batch=True,
302 )
303
304 image = self.read(index=index)
305 return ImageProperties(
306 shape=image.shape,
307 dtype=image.dtype,
308 is_batch=False,
309 )
310
311 def get_meta(self, *, index=None):
312 """Read ndimage metadata from the URI
313
314 Parameters
315 ----------
316 index : {integer, None}
317 If the URI contains a list of ndimages return the metadata
318 corresponding to the index-th image. If None, behavior depends on
319 the used api
320
321 Legacy-style API: return metadata of the first element (index=0)
322 New-style API: Behavior depends on the used Plugin.
323
324 Returns
325 -------
326 metadata : dict
327 A dictionary of metadata.
328
329 """
330
331 return self.metadata(index=index, exclude_applied=False)
332
333 def metadata(self, index=None, exclude_applied: bool = True):
334 """Format-Specific ndimage metadata.
335
336 Parameters
337 ----------
338 index : int
339 The index of the ndimage to read. If the index is out of bounds a
340 ``ValueError`` is raised. If ``None``, global metadata is returned.
341 exclude_applied : bool
342 This parameter exists for compatibility and has no effect. Legacy
343 plugins always report all metadata they find.
344
345 Returns
346 -------
347 metadata : dict
348 A dictionary filled with format-specific metadata fields and their
349 values.
350
351 """
352
353 if index is None:
354 index = _legacy_default_index(self._format)
355
356 return self.legacy_get_reader().get_meta_data(index=index)
357
358 def __del__(self) -> None:
359 pass
360 # turns out we can't close the file here for LegacyPlugin
361 # because it would break backwards compatibility
362 # with legacy_get_writer and legacy_get_reader
363 # self._request.finish()