1# -*- coding: utf-8 -*-
2# imageio is distributed under the terms of the (new) BSD License.
3
4""" Read/Write BSDF files.
5
6Backend Library: internal
7
8The BSDF format enables reading and writing of image data in the
9BSDF serialization format. This format allows storage of images, volumes,
10and series thereof. Data can be of any numeric data type, and can
11optionally be compressed. Each image/volume can have associated
12meta data, which can consist of any data type supported by BSDF.
13
14By default, image data is lazily loaded; the actual image data is
15not read until it is requested. This allows storing multiple images
16in a single file and still have fast access to individual images.
17Alternatively, a series of images can be read in streaming mode, reading
18images as they are read (e.g. from http).
19
20BSDF is a simple generic binary format. It is easy to extend and there
21are standard extension definitions for 2D and 3D image data.
22Read more at http://bsdf.io.
23
24
25Parameters
26----------
27random_access : bool
28 Whether individual images in the file can be read in random order.
29 Defaults to True for normal files, and to False when reading from HTTP.
30 If False, the file is read in "streaming mode", allowing reading
31 files as they are read, but without support for "rewinding".
32 Note that setting this to True when reading from HTTP, the whole file
33 is read upon opening it (since lazy loading is not possible over HTTP).
34
35compression : int
36 Use ``0`` or "no" for no compression, ``1`` or "zlib" for Zlib
37 compression (same as zip files and PNG), and ``2`` or "bz2" for Bz2
38 compression (more compact but slower). Default 1 (zlib).
39 Note that some BSDF implementations may not support compression
40 (e.g. JavaScript).
41
42"""
43
44import numpy as np
45
46from ..core import Format
47
48
49def get_bsdf_serializer(options):
50 from . import _bsdf as bsdf
51
52 class NDArrayExtension(bsdf.Extension):
53 """Copy of BSDF's NDArrayExtension but deal with lazy blobs."""
54
55 name = "ndarray"
56 cls = np.ndarray
57
58 def encode(self, s, v):
59 return dict(shape=v.shape, dtype=str(v.dtype), data=v.tobytes())
60
61 def decode(self, s, v):
62 return v # return as dict, because of lazy blobs, decode in Image
63
64 class ImageExtension(bsdf.Extension):
65 """We implement two extensions that trigger on the Image classes."""
66
67 def encode(self, s, v):
68 return dict(array=v.array, meta=v.meta)
69
70 def decode(self, s, v):
71 return Image(v["array"], v["meta"])
72
73 class Image2DExtension(ImageExtension):
74 name = "image2d"
75 cls = Image2D
76
77 class Image3DExtension(ImageExtension):
78 name = "image3d"
79 cls = Image3D
80
81 exts = [NDArrayExtension, Image2DExtension, Image3DExtension]
82 serializer = bsdf.BsdfSerializer(exts, **options)
83
84 return bsdf, serializer
85
86
87class Image:
88 """Class in which we wrap the array and meta data. By using an extension
89 we can make BSDF trigger on these classes and thus encode the images.
90 as actual images.
91 """
92
93 def __init__(self, array, meta):
94 self.array = array
95 self.meta = meta
96
97 def get_array(self):
98 if not isinstance(self.array, np.ndarray):
99 v = self.array
100 blob = v["data"]
101 if not isinstance(blob, bytes): # then it's a lazy bsdf.Blob
102 blob = blob.get_bytes()
103 self.array = np.frombuffer(blob, dtype=v["dtype"])
104 self.array.shape = v["shape"]
105 return self.array
106
107 def get_meta(self):
108 return self.meta
109
110
111class Image2D(Image):
112 pass
113
114
115class Image3D(Image):
116 pass
117
118
119class BsdfFormat(Format):
120 """The BSDF format enables reading and writing of image data in the
121 BSDF serialization format. This format allows storage of images, volumes,
122 and series thereof. Data can be of any numeric data type, and can
123 optionally be compressed. Each image/volume can have associated
124 meta data, which can consist of any data type supported by BSDF.
125
126 By default, image data is lazily loaded; the actual image data is
127 not read until it is requested. This allows storing multiple images
128 in a single file and still have fast access to individual images.
129 Alternatively, a series of images can be read in streaming mode, reading
130 images as they are read (e.g. from http).
131
132 BSDF is a simple generic binary format. It is easy to extend and there
133 are standard extension definitions for 2D and 3D image data.
134 Read more at http://bsdf.io.
135
136 Parameters for reading
137 ----------------------
138 random_access : bool
139 Whether individual images in the file can be read in random order.
140 Defaults to True for normal files, and to False when reading from HTTP.
141 If False, the file is read in "streaming mode", allowing reading
142 files as they are read, but without support for "rewinding".
143 Note that setting this to True when reading from HTTP, the whole file
144 is read upon opening it (since lazy loading is not possible over HTTP).
145
146 Parameters for saving
147 ---------------------
148 compression : {0, 1, 2}
149 Use ``0`` or "no" for no compression, ``1`` or "zlib" for Zlib
150 compression (same as zip files and PNG), and ``2`` or "bz2" for Bz2
151 compression (more compact but slower). Default 1 (zlib).
152 Note that some BSDF implementations may not support compression
153 (e.g. JavaScript).
154
155 """
156
157 def _can_read(self, request):
158 if request.mode[1] in (self.modes + "?"):
159 # if request.extension in self.extensions:
160 # return True
161 if request.firstbytes.startswith(b"BSDF"):
162 return True
163
164 def _can_write(self, request):
165 if request.mode[1] in (self.modes + "?"):
166 if request.extension in self.extensions:
167 return True
168
169 # -- reader
170
171 class Reader(Format.Reader):
172 def _open(self, random_access=None):
173 # Validate - we need a BSDF file consisting of a list of images
174 # The list is typically a stream, but does not have to be.
175 assert self.request.firstbytes[:4] == b"BSDF", "Not a BSDF file"
176 # self.request.firstbytes[5:6] == major and minor version
177 if not (
178 self.request.firstbytes[6:15] == b"M\x07image2D"
179 or self.request.firstbytes[6:15] == b"M\x07image3D"
180 or self.request.firstbytes[6:7] == b"l"
181 ):
182 pass # Actually, follow a more duck-type approach ...
183 # raise RuntimeError('BSDF file does not look like an '
184 # 'image container.')
185 # Set options. If we think that seeking is allowed, we lazily load
186 # blobs, and set streaming to False (i.e. the whole file is read,
187 # but we skip over binary blobs), so that we subsequently allow
188 # random access to the images.
189 # If seeking is not allowed (e.g. with a http request), we cannot
190 # lazily load blobs, but we can still load streaming from the web.
191 options = {}
192 if self.request.filename.startswith(("http://", "https://")):
193 ra = False if random_access is None else bool(random_access)
194 options["lazy_blob"] = False # Because we cannot seek now
195 options["load_streaming"] = not ra # Load as a stream?
196 else:
197 ra = True if random_access is None else bool(random_access)
198 options["lazy_blob"] = ra # Don't read data until needed
199 options["load_streaming"] = not ra
200
201 file = self.request.get_file()
202 bsdf, self._serializer = get_bsdf_serializer(options)
203 self._stream = self._serializer.load(file)
204 # Another validation
205 if (
206 isinstance(self._stream, dict)
207 and "meta" in self._stream
208 and "array" in self._stream
209 ):
210 self._stream = Image(self._stream["array"], self._stream["meta"])
211 if not isinstance(self._stream, (Image, list, bsdf.ListStream)):
212 raise RuntimeError(
213 "BSDF file does not look seem to have an " "image container."
214 )
215
216 def _close(self):
217 pass
218
219 def _get_length(self):
220 if isinstance(self._stream, Image):
221 return 1
222 elif isinstance(self._stream, list):
223 return len(self._stream)
224 elif self._stream.count < 0:
225 return np.inf
226 return self._stream.count
227
228 def _get_data(self, index):
229 # Validate
230 if index < 0 or index >= self.get_length():
231 raise IndexError(
232 "Image index %i not in [0 %i]." % (index, self.get_length())
233 )
234 # Get Image object
235 if isinstance(self._stream, Image):
236 image_ob = self._stream # singleton
237 elif isinstance(self._stream, list):
238 # Easy when we have random access
239 image_ob = self._stream[index]
240 else:
241 # For streaming, we need to skip over frames
242 if index < self._stream.index:
243 raise IndexError(
244 "BSDF file is being read in streaming "
245 "mode, thus does not allow rewinding."
246 )
247 while index > self._stream.index:
248 self._stream.next()
249 image_ob = self._stream.next() # Can raise StopIteration
250 # Is this an image?
251 if (
252 isinstance(image_ob, dict)
253 and "meta" in image_ob
254 and "array" in image_ob
255 ):
256 image_ob = Image(image_ob["array"], image_ob["meta"])
257 if isinstance(image_ob, Image):
258 # Return as array (if we have lazy blobs, they are read now)
259 return image_ob.get_array(), image_ob.get_meta()
260 else:
261 r = repr(image_ob)
262 r = r if len(r) < 200 else r[:197] + "..."
263 raise RuntimeError("BSDF file contains non-image " + r)
264
265 def _get_meta_data(self, index): # pragma: no cover
266 return {} # This format does not support global meta data
267
268 # -- writer
269
270 class Writer(Format.Writer):
271 def _open(self, compression=1):
272 options = {"compression": compression}
273 bsdf, self._serializer = get_bsdf_serializer(options)
274 if self.request.mode[1] in "iv":
275 self._stream = None # Singleton image
276 self._written = False
277 else:
278 # Series (stream) of images
279 file = self.request.get_file()
280 self._stream = bsdf.ListStream()
281 self._serializer.save(file, self._stream)
282
283 def _close(self):
284 # We close the stream here, which will mark the number of written
285 # elements. If we would not close it, the file would be fine, it's
286 # just that upon reading it would not be known how many items are
287 # in there.
288 if self._stream is not None:
289 self._stream.close(False) # False says "keep this a stream"
290
291 def _append_data(self, im, meta):
292 # Determine dimension
293 ndim = None
294 if self.request.mode[1] in "iI":
295 ndim = 2
296 elif self.request.mode[1] in "vV":
297 ndim = 3
298 else:
299 ndim = 3 # Make an educated guess
300 if im.ndim == 2 or (im.ndim == 3 and im.shape[-1] <= 4):
301 ndim = 2
302 # Validate shape
303 assert ndim in (2, 3)
304 if ndim == 2:
305 assert im.ndim == 2 or (im.ndim == 3 and im.shape[-1] <= 4)
306 else:
307 assert im.ndim == 3 or (im.ndim == 4 and im.shape[-1] <= 4)
308 # Wrap data and meta data in our special class that will trigger
309 # the BSDF image2D or image3D extension.
310 if ndim == 2:
311 ob = Image2D(im, meta)
312 else:
313 ob = Image3D(im, meta)
314 # Write directly or to stream
315 if self._stream is None:
316 assert not self._written, "Cannot write singleton image twice"
317 self._written = True
318 file = self.request.get_file()
319 self._serializer.save(file, ob)
320 else:
321 self._stream.append(ob)
322
323 def set_meta_data(self, meta): # pragma: no cover
324 raise RuntimeError("The BSDF format only supports " "per-image meta data.")