1010
1111import io
1212from copy import deepcopy
13+ from urllib import request
1314from .fileholders import FileHolder
1415from .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
489490class 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 (self ._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