10
10
11
11
import io
12
12
from copy import deepcopy
13
+ from urllib import request
13
14
from .fileholders import FileHolder
14
15
from .filename_parser import (types_filenames , TypesFilenamesError ,
15
16
splitext_addext )
@@ -488,7 +489,7 @@ def path_maybe_image(klass, filename, sniff=None, sniff_max=1024):
488
489
489
490
class SerializableImage (FileBasedImage ):
490
491
"""
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.
492
493
493
494
The class doesn't define any image properties.
494
495
@@ -501,6 +502,7 @@ class SerializableImage(FileBasedImage):
501
502
classmethods:
502
503
503
504
* from_bytes(bytestring) - make instance by deserializing a byte string
505
+ * from_url(url) - make instance by fetching and deserializing a URL
504
506
505
507
Loading from byte strings should provide round-trip equivalence:
506
508
@@ -538,7 +540,30 @@ class SerializableImage(FileBasedImage):
538
540
"""
539
541
540
542
@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 ):
542
567
""" Construct image from a byte string
543
568
544
569
Class method
@@ -548,13 +573,9 @@ def from_bytes(klass, bytestring):
548
573
bstring : bytes
549
574
Byte string containing the on-disk representation of an image
550
575
"""
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 ))
556
577
557
- def to_bytes (self , ** kwargs ):
578
+ def to_bytes (self , ** kwargs ) -> bytes :
558
579
r""" Return a ``bytes`` object with the contents of the file that would
559
580
be written if the image were saved.
560
581
@@ -568,9 +589,20 @@ def to_bytes(self, **kwargs):
568
589
bytes
569
590
Serialized image
570
591
"""
571
- if len (self .__class__ .files_types ) > 1 :
572
- raise NotImplementedError ("to_bytes() is undefined for multi-file images" )
573
592
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 )
576
594
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