# -*- coding: utf-8 -*- # imageio is distributed under the terms of the (new) BSD License. """ Read/Write BSDF files. Backend Library: internal The BSDF format enables reading and writing of image data in the BSDF serialization format. This format allows storage of images, volumes, and series thereof. Data can be of any numeric data type, and can optionally be compressed. Each image/volume can have associated meta data, which can consist of any data type supported by BSDF. By default, image data is lazily loaded; the actual image data is not read until it is requested. This allows storing multiple images in a single file and still have fast access to individual images. Alternatively, a series of images can be read in streaming mode, reading images as they are read (e.g. from http). BSDF is a simple generic binary format. It is easy to extend and there are standard extension definitions for 2D and 3D image data. Read more at http://bsdf.io. Parameters ---------- random_access : bool Whether individual images in the file can be read in random order. Defaults to True for normal files, and to False when reading from HTTP. If False, the file is read in "streaming mode", allowing reading files as they are read, but without support for "rewinding". Note that setting this to True when reading from HTTP, the whole file is read upon opening it (since lazy loading is not possible over HTTP). compression : int Use ``0`` or "no" for no compression, ``1`` or "zlib" for Zlib compression (same as zip files and PNG), and ``2`` or "bz2" for Bz2 compression (more compact but slower). Default 1 (zlib). Note that some BSDF implementations may not support compression (e.g. JavaScript). """ import numpy as np from ..core import Format def get_bsdf_serializer(options): from . import _bsdf as bsdf class NDArrayExtension(bsdf.Extension): """Copy of BSDF's NDArrayExtension but deal with lazy blobs.""" name = "ndarray" cls = np.ndarray def encode(self, s, v): return dict(shape=v.shape, dtype=str(v.dtype), data=v.tobytes()) def decode(self, s, v): return v # return as dict, because of lazy blobs, decode in Image class ImageExtension(bsdf.Extension): """We implement two extensions that trigger on the Image classes.""" def encode(self, s, v): return dict(array=v.array, meta=v.meta) def decode(self, s, v): return Image(v["array"], v["meta"]) class Image2DExtension(ImageExtension): name = "image2d" cls = Image2D class Image3DExtension(ImageExtension): name = "image3d" cls = Image3D exts = [NDArrayExtension, Image2DExtension, Image3DExtension] serializer = bsdf.BsdfSerializer(exts, **options) return bsdf, serializer class Image: """Class in which we wrap the array and meta data. By using an extension we can make BSDF trigger on these classes and thus encode the images. as actual images. """ def __init__(self, array, meta): self.array = array self.meta = meta def get_array(self): if not isinstance(self.array, np.ndarray): v = self.array blob = v["data"] if not isinstance(blob, bytes): # then it's a lazy bsdf.Blob blob = blob.get_bytes() self.array = np.frombuffer(blob, dtype=v["dtype"]) self.array.shape = v["shape"] return self.array def get_meta(self): return self.meta class Image2D(Image): pass class Image3D(Image): pass class BsdfFormat(Format): """The BSDF format enables reading and writing of image data in the BSDF serialization format. This format allows storage of images, volumes, and series thereof. Data can be of any numeric data type, and can optionally be compressed. Each image/volume can have associated meta data, which can consist of any data type supported by BSDF. By default, image data is lazily loaded; the actual image data is not read until it is requested. This allows storing multiple images in a single file and still have fast access to individual images. Alternatively, a series of images can be read in streaming mode, reading images as they are read (e.g. from http). BSDF is a simple generic binary format. It is easy to extend and there are standard extension definitions for 2D and 3D image data. Read more at http://bsdf.io. Parameters for reading ---------------------- random_access : bool Whether individual images in the file can be read in random order. Defaults to True for normal files, and to False when reading from HTTP. If False, the file is read in "streaming mode", allowing reading files as they are read, but without support for "rewinding". Note that setting this to True when reading from HTTP, the whole file is read upon opening it (since lazy loading is not possible over HTTP). Parameters for saving --------------------- compression : {0, 1, 2} Use ``0`` or "no" for no compression, ``1`` or "zlib" for Zlib compression (same as zip files and PNG), and ``2`` or "bz2" for Bz2 compression (more compact but slower). Default 1 (zlib). Note that some BSDF implementations may not support compression (e.g. JavaScript). """ def _can_read(self, request): if request.mode[1] in (self.modes + "?"): # if request.extension in self.extensions: # return True if request.firstbytes.startswith(b"BSDF"): return True def _can_write(self, request): if request.mode[1] in (self.modes + "?"): if request.extension in self.extensions: return True # -- reader class Reader(Format.Reader): def _open(self, random_access=None): # Validate - we need a BSDF file consisting of a list of images # The list is typically a stream, but does not have to be. assert self.request.firstbytes[:4] == b"BSDF", "Not a BSDF file" # self.request.firstbytes[5:6] == major and minor version if not ( self.request.firstbytes[6:15] == b"M\x07image2D" or self.request.firstbytes[6:15] == b"M\x07image3D" or self.request.firstbytes[6:7] == b"l" ): pass # Actually, follow a more duck-type approach ... # raise RuntimeError('BSDF file does not look like an ' # 'image container.') # Set options. If we think that seeking is allowed, we lazily load # blobs, and set streaming to False (i.e. the whole file is read, # but we skip over binary blobs), so that we subsequently allow # random access to the images. # If seeking is not allowed (e.g. with a http request), we cannot # lazily load blobs, but we can still load streaming from the web. options = {} if self.request.filename.startswith(("http://", "https://")): ra = False if random_access is None else bool(random_access) options["lazy_blob"] = False # Because we cannot seek now options["load_streaming"] = not ra # Load as a stream? else: ra = True if random_access is None else bool(random_access) options["lazy_blob"] = ra # Don't read data until needed options["load_streaming"] = not ra file = self.request.get_file() bsdf, self._serializer = get_bsdf_serializer(options) self._stream = self._serializer.load(file) # Another validation if ( isinstance(self._stream, dict) and "meta" in self._stream and "array" in self._stream ): self._stream = Image(self._stream["array"], self._stream["meta"]) if not isinstance(self._stream, (Image, list, bsdf.ListStream)): raise RuntimeError( "BSDF file does not look seem to have an " "image container." ) def _close(self): pass def _get_length(self): if isinstance(self._stream, Image): return 1 elif isinstance(self._stream, list): return len(self._stream) elif self._stream.count < 0: return np.inf return self._stream.count def _get_data(self, index): # Validate if index < 0 or index >= self.get_length(): raise IndexError( "Image index %i not in [0 %i]." % (index, self.get_length()) ) # Get Image object if isinstance(self._stream, Image): image_ob = self._stream # singleton elif isinstance(self._stream, list): # Easy when we have random access image_ob = self._stream[index] else: # For streaming, we need to skip over frames if index < self._stream.index: raise IndexError( "BSDF file is being read in streaming " "mode, thus does not allow rewinding." ) while index > self._stream.index: self._stream.next() image_ob = self._stream.next() # Can raise StopIteration # Is this an image? if ( isinstance(image_ob, dict) and "meta" in image_ob and "array" in image_ob ): image_ob = Image(image_ob["array"], image_ob["meta"]) if isinstance(image_ob, Image): # Return as array (if we have lazy blobs, they are read now) return image_ob.get_array(), image_ob.get_meta() else: r = repr(image_ob) r = r if len(r) < 200 else r[:197] + "..." raise RuntimeError("BSDF file contains non-image " + r) def _get_meta_data(self, index): # pragma: no cover return {} # This format does not support global meta data # -- writer class Writer(Format.Writer): def _open(self, compression=1): options = {"compression": compression} bsdf, self._serializer = get_bsdf_serializer(options) if self.request.mode[1] in "iv": self._stream = None # Singleton image self._written = False else: # Series (stream) of images file = self.request.get_file() self._stream = bsdf.ListStream() self._serializer.save(file, self._stream) def _close(self): # We close the stream here, which will mark the number of written # elements. If we would not close it, the file would be fine, it's # just that upon reading it would not be known how many items are # in there. if self._stream is not None: self._stream.close(False) # False says "keep this a stream" def _append_data(self, im, meta): # Determine dimension ndim = None if self.request.mode[1] in "iI": ndim = 2 elif self.request.mode[1] in "vV": ndim = 3 else: ndim = 3 # Make an educated guess if im.ndim == 2 or (im.ndim == 3 and im.shape[-1] <= 4): ndim = 2 # Validate shape assert ndim in (2, 3) if ndim == 2: assert im.ndim == 2 or (im.ndim == 3 and im.shape[-1] <= 4) else: assert im.ndim == 3 or (im.ndim == 4 and im.shape[-1] <= 4) # Wrap data and meta data in our special class that will trigger # the BSDF image2D or image3D extension. if ndim == 2: ob = Image2D(im, meta) else: ob = Image3D(im, meta) # Write directly or to stream if self._stream is None: assert not self._written, "Cannot write singleton image twice" self._written = True file = self.request.get_file() self._serializer.save(file, ob) else: self._stream.append(ob) def set_meta_data(self, meta): # pragma: no cover raise RuntimeError("The BSDF format only supports " "per-image meta data.")