imopen.py 9.5 KB

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