1from . import Request
2from ..typing import ArrayLike
3import numpy as np
4from typing import Optional, Dict, Any, Tuple, Union, List, Iterator
5from dataclasses import dataclass
6
7
8@dataclass
9class ImageProperties:
10 """Standardized Metadata
11
12 ImageProperties represent a set of standardized metadata that is available
13 under the same name for every supported format. If the ImageResource (or
14 format) does not specify the value, a sensible default value is chosen
15 instead.
16
17 Attributes
18 ----------
19 shape : Tuple[int, ...]
20 The shape of the loaded ndimage.
21 dtype : np.dtype
22 The dtype of the loaded ndimage.
23 n_images : int
24 Number of images in the file if ``index=...``, `None` for single images.
25 is_batch : bool
26 If True, the first dimension of the ndimage represents a batch dimension
27 along which several images are stacked.
28 spacing : Tuple
29 A tuple describing the spacing between pixels along each axis of the
30 ndimage. If the spacing is uniform along an axis the value corresponding
31 to that axis is a single float. If the spacing is non-uniform, the value
32 corresponding to that axis is a tuple in which the i-th element
33 indicates the spacing between the i-th and (i+1)-th pixel along that
34 axis.
35
36 """
37
38 shape: Tuple[int, ...]
39 dtype: np.dtype
40 n_images: Optional[int] = None
41 is_batch: bool = False
42 spacing: Optional[tuple] = None
43
44
45class PluginV3:
46 """A ImageIO Plugin.
47
48 This is an abstract plugin that documents the v3 plugin API interface. A
49 plugin is an adapter/wrapper around a backend that converts a request from
50 iio.core (e.g., read an image from file) into a sequence of instructions for
51 the backend that fulfill the request.
52
53 Plugin authors may choose to subclass this class when implementing a new
54 plugin, but aren't obliged to do so. As long as the plugin class implements
55 the interface (methods) described below the ImageIO core will treat it just
56 like any other plugin.
57
58
59 Parameters
60 ----------
61 request : iio.Request
62 A request object that represents the users intent. It provides a
63 standard interface to access the various ImageResources and serves them
64 to the plugin as a file object (or file). Check the docs for details.
65 **kwargs : Any
66 Additional configuration arguments for the plugin or backend. Usually
67 these match the configuration arguments available on the backend and
68 are forwarded to it.
69
70
71 Raises
72 ------
73 InitializationError
74 During ``__init__`` the plugin tests if it can fulfill the request. If
75 it can't, e.g., because the request points to a file in the wrong
76 format, then it should raise an ``InitializationError`` and provide a
77 reason for failure. This reason may be reported to the user.
78 ImportError
79 Plugins will be imported dynamically when listed in
80 ``iio.config.known_plugins`` to fulfill requests. This way, users only
81 have to load plugins/backends they actually use. If this plugin's backend
82 is not installed, it should raise an ``ImportError`` either during
83 module import or during class construction.
84
85 Notes
86 -----
87 Upon successful construction the plugin takes ownership of the provided
88 request. This means that it is the plugin's responsibility to call
89 request.finish() to close the resource when it is no longer needed.
90
91 Plugins _must_ implement a context manager that closes and cleans any
92 resources held by the plugin upon exit.
93
94 """
95
96 def __init__(self, request: Request) -> None:
97 """Initialize a new Plugin Instance.
98
99 See Plugin's docstring for detailed documentation.
100
101 Notes
102 -----
103 The implementation here stores the request as a local variable that is
104 exposed using a @property below. If you inherit from PluginV3, remember
105 to call ``super().__init__(request)``.
106
107 """
108
109 self._request = request
110
111 def read(self, *, index: int = 0) -> np.ndarray:
112 """Read a ndimage.
113
114 The ``read`` method loads a (single) ndimage, located at ``index`` from
115 the requested ImageResource.
116
117 It is at the plugin's descretion to decide (and document) what
118 constitutes a single ndimage. A sensible way to make this decision is to
119 choose based on the ImageResource's format and on what users will expect
120 from such a format. For example, a sensible choice for a TIFF file
121 produced by an ImageJ hyperstack is to read it as a volumetric ndimage
122 (1 color dimension followed by 3 spatial dimensions). On the other hand,
123 a sensible choice for a MP4 file produced by Davinci Resolve is to treat
124 each frame as a ndimage (2 spatial dimensions followed by 1 color
125 dimension).
126
127 The value ``index=None`` is special. It requests the plugin to load all
128 ndimages in the file and stack them along a new first axis. For example,
129 if a MP4 file is read with ``index=None`` and the plugin identifies
130 single frames as ndimages, then the plugin should read all frames and
131 stack them into a new ndimage which now contains a time axis as its
132 first axis. If a PNG file (single image format) is read with
133 ``index=None`` the plugin does a very similar thing: It loads all
134 ndimages in the file (here it's just one) and stacks them along a new
135 first axis, effectively prepending an axis with size 1 to the image. If
136 a plugin does not wish to support ``index=None`` it should set a more
137 sensible default and raise a ``ValueError`` when requested to read using
138 ``index=None``.
139
140 Parameters
141 ----------
142 index : int
143 If the ImageResource contains multiple ndimages, and index is an
144 integer, select the index-th ndimage from among them and return it.
145 If index is an ellipsis (...), read all ndimages in the file and
146 stack them along a new batch dimension. If index is None, let the
147 plugin decide. If the index is out of bounds a ``ValueError`` is
148 raised.
149 **kwargs : Any
150 The read method may accept any number of plugin-specific keyword
151 arguments to further customize the read behavior. Usually these
152 match the arguments available on the backend and are forwarded to
153 it.
154
155 Returns
156 -------
157 ndimage : np.ndarray
158 A ndimage containing decoded pixel data (sometimes called bitmap).
159
160 Notes
161 -----
162 The ImageResource from which the plugin should read is managed by the
163 provided request object. Directly accessing the managed ImageResource is
164 _not_ permitted. Instead, you can get FileLike access to the
165 ImageResource via request.get_file().
166
167 If the backend doesn't support reading from FileLike objects, you can
168 request a temporary file to pass to the backend via
169 ``request.get_local_filename()``. This is, however, not very performant
170 (involves copying the Request's content into a temporary file), so you
171 should avoid doing this whenever possible. Consider it a fallback method
172 in case all else fails.
173
174 """
175 raise NotImplementedError()
176
177 def write(self, ndimage: Union[ArrayLike, List[ArrayLike]]) -> Optional[bytes]:
178 """Write a ndimage to a ImageResource.
179
180 The ``write`` method encodes the given ndimage into the format handled
181 by the backend and writes it to the ImageResource. It overwrites
182 any content that may have been previously stored in the file.
183
184 If the backend supports only a single format then it must check if
185 the ImageResource matches that format and raise an exception if not.
186 Typically, this should be done during initialization in the form of a
187 ``InitializationError``.
188
189 If the backend supports more than one format it must determine the
190 requested/desired format. Usually this can be done by inspecting the
191 ImageResource (e.g., by checking ``request.extension``), or by providing
192 a mechanism to explicitly set the format (perhaps with a - sensible -
193 default value). If the plugin can not determine the desired format, it
194 **must not** write to the ImageResource, but raise an exception instead.
195
196 If the backend supports at least one format that can hold multiple
197 ndimages it should be capable of handling ndimage batches and lists of
198 ndimages. If the ``ndimage`` input is a list of ndimages, the plugin
199 should not assume that the ndimages are not stackable, i.e., ndimages
200 may have different shapes. Otherwise, the ``ndimage`` may be a batch of
201 multiple ndimages stacked along the first axis of the array. The plugin
202 must be able to discover this, either automatically or via additional
203 `kwargs`. If there is ambiguity in the process, the plugin must clearly
204 document what happens in such cases and, if possible, describe how to
205 resolve this ambiguity.
206
207 Parameters
208 ----------
209 ndimage : ArrayLike
210 The ndimage to encode and write to the current ImageResource.
211 **kwargs : Any
212 The write method may accept any number of plugin-specific keyword
213 arguments to customize the writing behavior. Usually these match the
214 arguments available on the backend and are forwarded to it.
215
216 Returns
217 -------
218 encoded_image : bytes or None
219 If the chosen ImageResource is the special target ``"<bytes>"`` then
220 write should return a byte string containing the encoded image data.
221 Otherwise, it returns None.
222
223 Notes
224 -----
225 The ImageResource to which the plugin should write to is managed by the
226 provided request object. Directly accessing the managed ImageResource is
227 _not_ permitted. Instead, you can get FileLike access to the
228 ImageResource via request.get_file().
229
230 If the backend doesn't support writing to FileLike objects, you can
231 request a temporary file to pass to the backend via
232 ``request.get_local_filename()``. This is, however, not very performant
233 (involves copying the Request's content from a temporary file), so you
234 should avoid doing this whenever possible. Consider it a fallback method
235 in case all else fails.
236
237 """
238 raise NotImplementedError()
239
240 def iter(self) -> Iterator[np.ndarray]:
241 """Iterate the ImageResource.
242
243 This method returns a generator that yields ndimages in the order in which
244 they appear in the file. This is roughly equivalent to::
245
246 idx = 0
247 while True:
248 try:
249 yield self.read(index=idx)
250 except ValueError:
251 break
252
253 It works very similar to ``read``, and you can consult the documentation
254 of that method for additional information on desired behavior.
255
256 Parameters
257 ----------
258 **kwargs : Any
259 The iter method may accept any number of plugin-specific keyword
260 arguments to further customize the reading/iteration behavior.
261 Usually these match the arguments available on the backend and are
262 forwarded to it.
263
264 Yields
265 ------
266 ndimage : np.ndarray
267 A ndimage containing decoded pixel data (sometimes called bitmap).
268
269 See Also
270 --------
271 PluginV3.read
272
273 """
274 raise NotImplementedError()
275
276 def properties(self, index: int = 0) -> ImageProperties:
277 """Standardized ndimage metadata.
278
279 Parameters
280 ----------
281 index : int
282 If the ImageResource contains multiple ndimages, and index is an
283 integer, select the index-th ndimage from among them and return its
284 properties. If index is an ellipsis (...), read all ndimages in the file
285 and stack them along a new batch dimension and return their properties.
286 If index is None, the plugin decides the default.
287
288 Returns
289 -------
290 properties : ImageProperties
291 A dataclass filled with standardized image metadata.
292
293 """
294 raise NotImplementedError()
295
296 def metadata(self, index: int = 0, exclude_applied: bool = True) -> Dict[str, Any]:
297 """Format-Specific ndimage metadata.
298
299 The method reads metadata stored in the ImageResource and returns it as
300 a python dict. The plugin is free to choose which name to give a piece
301 of metadata; however, if possible, it should match the name given by the
302 format. There is no requirement regarding the fields a plugin must
303 expose; however, if a plugin does expose any,``exclude_applied`` applies
304 to these fields.
305
306 If the plugin does return metadata items, it must check the value of
307 ``exclude_applied`` before returning them. If ``exclude applied`` is
308 True, then any metadata item that would be applied to an ndimage
309 returned by ``read`` (or ``iter``) must not be returned. This is done to
310 avoid confusion; for example, if an ImageResource defines the ExIF
311 rotation tag, and the plugin applies the rotation to the data before
312 returning it, then ``exclude_applied`` prevents confusion on whether the
313 tag was already applied or not.
314
315 The `kwarg` ``index`` behaves similar to its counterpart in ``read``
316 with one exception: If the ``index`` is None, then global metadata is
317 returned instead of returning a combination of all metadata items. If
318 there is no global metadata, the Plugin should return an empty dict or
319 raise an exception.
320
321 Parameters
322 ----------
323 index : int
324 If the ImageResource contains multiple ndimages, and index is an
325 integer, select the index-th ndimage from among them and return its
326 metadata. If index is an ellipsis (...), return global metadata. If
327 index is None, the plugin decides the default.
328 exclude_applied : bool
329 If True (default), do not report metadata fields that the plugin
330 would apply/consume while reading the image.
331
332 Returns
333 -------
334 metadata : dict
335 A dictionary filled with format-specific metadata fields and their
336 values.
337
338 """
339 raise NotImplementedError()
340
341 def close(self) -> None:
342 """Close the ImageResource.
343
344 This method allows a plugin to behave similar to the python built-in ``open``::
345
346 image_file = my_plugin(Request, "r")
347 ...
348 image_file.close()
349
350 It is used by the context manager and deconstructor below to avoid leaking
351 ImageResources. If the plugin has no other cleanup to do it doesn't have
352 to overwrite this method itself and can rely on the implementation
353 below.
354
355 """
356
357 self.request.finish()
358
359 @property
360 def request(self) -> Request:
361 return self._request
362
363 def __enter__(self) -> "PluginV3":
364 return self
365
366 def __exit__(self, type, value, traceback) -> None:
367 self.close()
368
369 def __del__(self) -> None:
370 self.close()