diff --git a/large_image/tilesource/utilities.py b/large_image/tilesource/utilities.py index dfba6acdc..ac0a7b1c4 100644 --- a/large_image/tilesource/utilities.py +++ b/large_image/tilesource/utilities.py @@ -34,6 +34,32 @@ } +class ImageBytes(bytes): + """Wrapper class to make repr of image bytes better in ipython.""" + + def __new__(cls, source: bytes, mimetype: str = None): + self = super().__new__(cls, source) + self._mime_type = mimetype + return self + + @property + def mimetype(self): + return self._mime_type + + def _repr_png_(self): + if self.mimetype == 'image/png': + return self + + def _repr_jpeg_(self): + if self.mimetype == 'image/jpeg': + return self + + def __repr__(self): + if self.mimetype: + return f'ImageBytes<{len(self)}> ({self.mimetype})' + return f'ImageBytes<{len(self)}> (wrapped image bytes)' + + def _encodeImageBinary(image, encoding, jpegQuality, jpegSubsampling, tiffCompression): """ Encode a PIL Image to a binary representation of the image (a jpeg, png, or @@ -55,13 +81,13 @@ def _encodeImageBinary(image, encoding, jpegQuality, jpegSubsampling, tiffCompre if image.mode not in ({'L', 'RGB', 'RGBA'} if simplejpeg else {'L', 'RGB'}): image = image.convert('RGB' if image.mode != 'LA' else 'L') if simplejpeg: - return simplejpeg.encode_jpeg( + return ImageBytes(simplejpeg.encode_jpeg( _imageToNumpy(image)[0], quality=jpegQuality, colorspace=image.mode if image.mode in {'RGB', 'RGBA'} else 'GRAY', colorsubsampling={-1: '444', 0: '444', 1: '422', 2: '420'}.get( jpegSubsampling, str(jpegSubsampling).strip(':')), - ) + ), mimetype='image/jpeg') params['quality'] = jpegQuality params['subsampling'] = jpegSubsampling elif encoding in {'TIFF', 'TILED'}: @@ -74,7 +100,10 @@ def _encodeImageBinary(image, encoding, jpegQuality, jpegSubsampling, tiffCompre params['compress_level'] = 2 output = io.BytesIO() image.save(output, encoding, **params) - return output.getvalue() + return ImageBytes( + output.getvalue(), + mimetype=f'image/{encoding.lower().replace("tiled", "tiff")}' + ) def _encodeImage(image, encoding='JPEG', jpegQuality=95, jpegSubsampling=0, diff --git a/test/test_source_base.py b/test/test_source_base.py index c1e553512..a465f788a 100644 --- a/test/test_source_base.py +++ b/test/test_source_base.py @@ -506,3 +506,28 @@ def testCanReadList(): imagePath = datastore.fetch('sample_image.ptif') assert len(large_image.canReadList(imagePath)) > 1 assert any(canRead for source, canRead in large_image.canReadList(imagePath)) + + +def testImageBytes(): + ib = large_image.tilesource.utilities.ImageBytes(b'abc') + assert ib == b'abc' + assert isinstance(ib, bytes) + assert 'ImageBytes' in repr(ib) + assert ib.mimetype is None + assert ib._repr_jpeg_() is None + assert ib._repr_png_() is None + ib = large_image.tilesource.utilities.ImageBytes(b'abc', 'image/jpeg') + assert ib.mimetype == 'image/jpeg' + assert 'ImageBytes' in repr(ib) + assert ib._repr_jpeg_() == b'abc' + assert ib._repr_png_() is None + ib = large_image.tilesource.utilities.ImageBytes(b'abc', 'image/png') + assert ib.mimetype == 'image/png' + assert 'ImageBytes' in repr(ib) + assert ib._repr_jpeg_() is None + assert ib._repr_png_() == b'abc' + ib = large_image.tilesource.utilities.ImageBytes(b'abc', 'other') + assert ib.mimetype == 'other' + assert 'ImageBytes' in repr(ib) + assert ib._repr_jpeg_() is None + assert ib._repr_png_() is None diff --git a/test/test_source_gdal.py b/test/test_source_gdal.py index 69bce9c30..f0f84f50a 100644 --- a/test/test_source_gdal.py +++ b/test/test_source_gdal.py @@ -11,6 +11,7 @@ from large_image import constants from large_image.exceptions import TileSourceError, TileSourceInefficientError +from large_image.tilesource.utilities import ImageBytes from . import utilities from .datastore import datastore @@ -149,10 +150,15 @@ def testThumbnailFromGeotiffs(): source = large_image_source_gdal.open(imagePath) # We get a thumbnail without a projection image, mimeType = source.getThumbnail(encoding='PNG') + assert isinstance(image, ImageBytes) assert image[:len(utilities.PNGHeader)] == utilities.PNGHeader + image, mimeType = source.getThumbnail(encoding='JPEG') + assert isinstance(image, ImageBytes) + assert image[:len(utilities.JPEGHeader)] == utilities.JPEGHeader # We get a different thumbnail with a projection source = large_image_source_gdal.open(imagePath, projection='EPSG:3857') image2, mimeType = source.getThumbnail(encoding='PNG') + assert isinstance(image2, ImageBytes) assert image2[:len(utilities.PNGHeader)] == utilities.PNGHeader assert image != image2 diff --git a/test/test_source_tiff.py b/test/test_source_tiff.py index 477d3d04c..7bc9f6539 100644 --- a/test/test_source_tiff.py +++ b/test/test_source_tiff.py @@ -9,6 +9,7 @@ import tifftools from large_image import constants +from large_image.tilesource.utilities import ImageBytes from . import utilities from .datastore import datastore @@ -271,19 +272,24 @@ def testThumbnails(): assert image[:len(utilities.JPEGHeader)] == utilities.JPEGHeader defaultLength = len(image) image, mimeType = source.getThumbnail(encoding='PNG') + assert isinstance(image, ImageBytes) assert image[:len(utilities.PNGHeader)] == utilities.PNGHeader image, mimeType = source.getThumbnail(encoding='TIFF') + assert isinstance(image, ImageBytes) assert image[:len(utilities.TIFFHeader)] == utilities.TIFFHeader image, mimeType = source.getThumbnail(jpegQuality=10) + assert isinstance(image, ImageBytes) assert image[:len(utilities.JPEGHeader)] == utilities.JPEGHeader assert len(image) < defaultLength image, mimeType = source.getThumbnail(jpegSubsampling=2) + assert isinstance(image, ImageBytes) assert image[:len(utilities.JPEGHeader)] == utilities.JPEGHeader assert len(image) < defaultLength with pytest.raises(Exception): source.getThumbnail(encoding='unknown') # Test width and height using PNGs image, mimeType = source.getThumbnail(encoding='PNG') + assert isinstance(image, ImageBytes) assert image[:len(utilities.PNGHeader)] == utilities.PNGHeader (width, height) = struct.unpack('!LL', image[16:24]) assert max(width, height) == 256 diff --git a/utilities/converter/large_image_converter/__init__.py b/utilities/converter/large_image_converter/__init__.py index 53f045d30..b9a621af7 100644 --- a/utilities/converter/large_image_converter/__init__.py +++ b/utilities/converter/large_image_converter/__init__.py @@ -272,7 +272,7 @@ def _convert_via_vips(inputPathOrBuffer, outputPath, tempPath, forTiled=True, if type(inputPathOrBuffer) == pyvips.vimage.Image: source = 'vips image' image = inputPathOrBuffer - elif type(inputPathOrBuffer) == bytes: + elif isinstance(inputPathOrBuffer, bytes): source = 'buffer' image = pyvips.Image.new_from_buffer(inputPathOrBuffer, '') else: