1from pathlib import Path
2import warnings
3
4from ..config import known_plugins
5from ..config.extensions import known_extensions
6from .request import (
7 SPECIAL_READ_URIS,
8 URI_FILENAME,
9 InitializationError,
10 IOMode,
11 Request,
12)
13
14
15def imopen(
16 uri,
17 io_mode,
18 *,
19 plugin=None,
20 extension=None,
21 format_hint=None,
22 legacy_mode=False,
23 **kwargs,
24):
25 """Open an ImageResource.
26
27 .. warning::
28 This warning is for pypy users. If you are not using a context manager,
29 remember to deconstruct the returned plugin to avoid leaking the file
30 handle to an unclosed file.
31
32 Parameters
33 ----------
34 uri : str or pathlib.Path or bytes or file or Request
35 The :doc:`ImageResource <../../user_guide/requests>` to load the
36 image from.
37 io_mode : str
38 The mode in which the file is opened. Possible values are::
39
40 ``r`` - open the file for reading
41 ``w`` - open the file for writing
42
43 Depreciated since v2.9:
44 A second character can be added to give the reader a hint on what
45 the user expects. This will be ignored by new plugins and will
46 only have an effect on legacy plugins. Possible values are::
47
48 ``i`` for a single image,
49 ``I`` for multiple images,
50 ``v`` for a single volume,
51 ``V`` for multiple volumes,
52 ``?`` for don't care
53
54 plugin : str, Plugin, or None
55 The plugin to use. If set to None imopen will perform a
56 search for a matching plugin. If not None, this takes priority over
57 the provided format hint.
58 extension : str
59 If not None, treat the provided ImageResource as if it had the given
60 extension. This affects the order in which backends are considered, and
61 when writing this may also influence the format used when encoding.
62 format_hint : str
63 Deprecated. Use `extension` instead.
64 legacy_mode : bool
65 If true use the v2 behavior when searching for a suitable
66 plugin. This will ignore v3 plugins and will check ``plugin``
67 against known extensions if no plugin with the given name can be found.
68 **kwargs : Any
69 Additional keyword arguments will be passed to the plugin upon
70 construction.
71
72 Notes
73 -----
74 Registered plugins are controlled via the ``known_plugins`` dict in
75 ``imageio.config``.
76
77 Passing a ``Request`` as the uri is only supported if ``legacy_mode``
78 is ``True``. In this case ``io_mode`` is ignored.
79
80 Using the kwarg ``format_hint`` does not enforce the given format. It merely
81 provides a `hint` to the selection process and plugin. The selection
82 processes uses this hint for optimization; however, a plugin's decision how
83 to read a ImageResource will - typically - still be based on the content of
84 the resource.
85
86
87 Examples
88 --------
89
90 >>> import imageio.v3 as iio
91 >>> with iio.imopen("/path/to/image.png", "r") as file:
92 >>> im = file.read()
93
94 >>> with iio.imopen("/path/to/output.jpg", "w") as file:
95 >>> file.write(im)
96
97 """
98
99 if isinstance(uri, Request) and legacy_mode:
100 warnings.warn(
101 "`iio.core.Request` is a low-level object and using it"
102 " directly as input to `imopen` is discouraged. This will raise"
103 " an exception in ImageIO v3.",
104 DeprecationWarning,
105 stacklevel=2,
106 )
107
108 request = uri
109 uri = request.raw_uri
110 io_mode = request.mode.io_mode
111 request.format_hint = format_hint
112 else:
113 request = Request(uri, io_mode, format_hint=format_hint, extension=extension)
114
115 source = "<bytes>" if isinstance(uri, bytes) else uri
116
117 # fast-path based on plugin
118 # (except in legacy mode)
119 if plugin is not None:
120 if isinstance(plugin, str):
121 try:
122 config = known_plugins[plugin]
123 except KeyError:
124 request.finish()
125 raise ValueError(
126 f"`{plugin}` is not a registered plugin name."
127 ) from None
128
129 def loader(request, **kwargs):
130 return config.plugin_class(request, **kwargs)
131
132 else:
133
134 def loader(request, **kwargs):
135 return plugin(request, **kwargs)
136
137 try:
138 return loader(request, **kwargs)
139 except InitializationError as class_specific:
140 err_from = class_specific
141 err_type = RuntimeError if legacy_mode else IOError
142 err_msg = f"`{plugin}` can not handle the given uri."
143 except ImportError:
144 err_from = None
145 err_type = ImportError
146 err_msg = (
147 f"The `{config.name}` plugin is not installed. "
148 f"Use `pip install imageio[{config.install_name}]` to install it."
149 )
150 except Exception as generic_error:
151 err_from = generic_error
152 err_type = IOError
153 err_msg = f"An unknown error occurred while initializing plugin `{plugin}`."
154
155 request.finish()
156 raise err_type(err_msg) from err_from
157
158 # fast-path based on format_hint
159 if request.format_hint is not None:
160 for candidate_format in known_extensions[format_hint]:
161 for plugin_name in candidate_format.priority:
162 config = known_plugins[plugin_name]
163
164 try:
165 candidate_plugin = config.plugin_class
166 except ImportError:
167 # not installed
168 continue
169
170 try:
171 plugin_instance = candidate_plugin(request, **kwargs)
172 except InitializationError:
173 # file extension doesn't match file type
174 continue
175
176 return plugin_instance
177 else:
178 resource = (
179 "<bytes>" if isinstance(request.raw_uri, bytes) else request.raw_uri
180 )
181 warnings.warn(f"`{resource}` can not be opened as a `{format_hint}` file.")
182
183 # fast-path based on file extension
184 if request.extension in known_extensions:
185 for candidate_format in known_extensions[request.extension]:
186 for plugin_name in candidate_format.priority:
187 config = known_plugins[plugin_name]
188
189 try:
190 candidate_plugin = config.plugin_class
191 except ImportError:
192 # not installed
193 continue
194
195 try:
196 plugin_instance = candidate_plugin(request, **kwargs)
197 except InitializationError:
198 # file extension doesn't match file type
199 continue
200
201 return plugin_instance
202
203 # error out for read-only special targets
204 # this is hacky; can we come up with a better solution for this?
205 if request.mode.io_mode == IOMode.write:
206 if isinstance(uri, str) and uri.startswith(SPECIAL_READ_URIS):
207 request.finish()
208 err_type = ValueError if legacy_mode else IOError
209 err_msg = f"`{source}` is read-only."
210 raise err_type(err_msg)
211
212 # error out for directories
213 # this is a bit hacky and should be cleaned once we decide
214 # how to gracefully handle DICOM
215 if request._uri_type == URI_FILENAME and Path(request.raw_uri).is_dir():
216 request.finish()
217 err_type = ValueError if legacy_mode else IOError
218 err_msg = (
219 "ImageIO does not generally support reading folders. "
220 "Limited support may be available via specific plugins. "
221 "Specify the plugin explicitly using the `plugin` kwarg, e.g. `plugin='DICOM'`"
222 )
223 raise err_type(err_msg)
224
225 # close the current request here and use fresh/new ones while trying each
226 # plugin This is slow (means potentially reopening a resource several
227 # times), but should only happen rarely because this is the fallback if all
228 # else fails.
229 request.finish()
230
231 # fallback option: try all plugins
232 for config in known_plugins.values():
233 # each plugin gets its own request
234 request = Request(uri, io_mode, format_hint=format_hint)
235
236 try:
237 plugin_instance = config.plugin_class(request, **kwargs)
238 except InitializationError:
239 continue
240 except ImportError:
241 continue
242 else:
243 return plugin_instance
244
245 err_type = ValueError if legacy_mode else IOError
246 err_msg = f"Could not find a backend to open `{source}`` with iomode `{io_mode}`."
247
248 # check if a missing plugin could help
249 if request.extension in known_extensions:
250 missing_plugins = list()
251
252 formats = known_extensions[request.extension]
253 plugin_names = [
254 plugin for file_format in formats for plugin in file_format.priority
255 ]
256 for name in plugin_names:
257 config = known_plugins[name]
258
259 try:
260 config.plugin_class
261 continue
262 except ImportError:
263 missing_plugins.append(config)
264
265 if len(missing_plugins) > 0:
266 install_candidates = "\n".join(
267 [
268 (
269 f" {config.name}: "
270 f"pip install imageio[{config.install_name}]"
271 )
272 for config in missing_plugins
273 ]
274 )
275 err_msg += (
276 "\nBased on the extension, the following plugins might add capable backends:\n"
277 f"{install_candidates}"
278 )
279
280 request.finish()
281 raise err_type(err_msg)