Skip to content

Commit 0e2297a

Browse files
committed
ENH: Add from_url classmethod to SerializableImage
1 parent 225892a commit 0e2297a

File tree

1 file changed

+44
-12
lines changed

1 file changed

+44
-12
lines changed

nibabel/filebasedimages.py

+44-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import io
1212
from copy import deepcopy
13+
from urllib import request
1314
from .fileholders import FileHolder
1415
from .filename_parser import (types_filenames, TypesFilenamesError,
1516
splitext_addext)
@@ -488,7 +489,7 @@ def path_maybe_image(klass, filename, sniff=None, sniff_max=1024):
488489

489490
class SerializableImage(FileBasedImage):
490491
"""
491-
Abstract image class for (de)serializing images to/from byte strings.
492+
Abstract image class for (de)serializing images to/from byte streams/strings.
492493
493494
The class doesn't define any image properties.
494495
@@ -501,6 +502,7 @@ class SerializableImage(FileBasedImage):
501502
classmethods:
502503
503504
* from_bytes(bytestring) - make instance by deserializing a byte string
505+
* from_url(url) - make instance by fetching and deserializing a URL
504506
505507
Loading from byte strings should provide round-trip equivalence:
506508
@@ -538,7 +540,30 @@ class SerializableImage(FileBasedImage):
538540
"""
539541

540542
@classmethod
541-
def from_bytes(klass, bytestring):
543+
def _filemap_from_iobase(klass, ioobject: io.IOBase):
544+
"""For single-file image types, make a file map with the correct key"""
545+
if len(klass.files_types) > 1:
546+
raise NotImplementedError(
547+
"(de)serialization is undefined for multi-file images"
548+
)
549+
return klass.make_file_map({klass.files_types[0][0]: ioobject})
550+
551+
@classmethod
552+
def _from_iobase(klass, ioobject: io.IOBase):
553+
"""Load image from readable IO stream
554+
555+
Convert to BytesIO to enable seeking, if input stream is not seekable
556+
"""
557+
if not ioobject.seekable():
558+
ioobject = io.BytesIO(ioobject.read())
559+
return klass.from_file_map(klass._filemap_from_iobase(ioobject))
560+
561+
def _to_iobase(self, ioobject: io.IOBase, **kwargs):
562+
"""Save image from writable IO stream"""
563+
self.to_file_map(klass._filemap_from_iobase(ioobject), **kwargs)
564+
565+
@classmethod
566+
def from_bytes(klass, bytestring: bytes):
542567
""" Construct image from a byte string
543568
544569
Class method
@@ -548,13 +573,9 @@ def from_bytes(klass, bytestring):
548573
bstring : bytes
549574
Byte string containing the on-disk representation of an image
550575
"""
551-
if len(klass.files_types) > 1:
552-
raise NotImplementedError("from_bytes is undefined for multi-file images")
553-
bio = io.BytesIO(bytestring)
554-
file_map = klass.make_file_map({'image': bio, 'header': bio})
555-
return klass.from_file_map(file_map)
576+
return klass._from_iobase(io.BytesIO(bytestring))
556577

557-
def to_bytes(self, **kwargs):
578+
def to_bytes(self, **kwargs) -> bytes:
558579
r""" Return a ``bytes`` object with the contents of the file that would
559580
be written if the image were saved.
560581
@@ -568,9 +589,20 @@ def to_bytes(self, **kwargs):
568589
bytes
569590
Serialized image
570591
"""
571-
if len(self.__class__.files_types) > 1:
572-
raise NotImplementedError("to_bytes() is undefined for multi-file images")
573592
bio = io.BytesIO()
574-
file_map = self.make_file_map({'image': bio, 'header': bio})
575-
self.to_file_map(file_map, **kwargs)
593+
self._to_iobase(bio, **kwargs)
576594
return bio.getvalue()
595+
596+
@classmethod
597+
def from_url(klass, url, timeout=5):
598+
"""Retrieve and load an image from a URL
599+
600+
Class method
601+
602+
Parameters
603+
----------
604+
url : str or urllib.request.Request object
605+
URL of file to retrieve
606+
"""
607+
with request.urlopen(url, timeout=timeout) as response:
608+
return klass._from_iobase(response)

0 commit comments

Comments
 (0)