From caaa97fa36b2bf10148a6f821b4ba9ef11eb956e Mon Sep 17 00:00:00 2001 From: banesullivan Date: Sat, 30 Jul 2022 13:54:53 -0600 Subject: [PATCH 1/9] add ImageBytes for better repr --- large_image/tilesource/utilities.py | 14 +++++++++++++- test/test_source_gdal.py | 3 +++ test/test_source_tiff.py | 6 ++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/large_image/tilesource/utilities.py b/large_image/tilesource/utilities.py index dfba6acdc..f09ae862f 100644 --- a/large_image/tilesource/utilities.py +++ b/large_image/tilesource/utilities.py @@ -34,6 +34,15 @@ } +class ImageBytes(bytes): + """Wrapper class to make repr of image bytes better in ipython.""" + def _repr_png_(self): + return self + + def _repr_jpeg_(self): + return self + + def _encodeImageBinary(image, encoding, jpegQuality, jpegSubsampling, tiffCompression): """ Encode a PIL Image to a binary representation of the image (a jpeg, png, or @@ -74,7 +83,10 @@ def _encodeImageBinary(image, encoding, jpegQuality, jpegSubsampling, tiffCompre params['compress_level'] = 2 output = io.BytesIO() image.save(output, encoding, **params) - return output.getvalue() + btes = output.getvalue() + if encoding in ['PNG', 'JPEG']: + return ImageBytes(btes) + return btes def _encodeImage(image, encoding='JPEG', jpegQuality=95, jpegSubsampling=0, diff --git a/test/test_source_gdal.py b/test/test_source_gdal.py index 69bce9c30..d589030f0 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,12 @@ 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 # 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..f2a94a299 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, bytes) 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 From d2f489acafead7d72c59ff5a0aec7dab7acf53e7 Mon Sep 17 00:00:00 2001 From: banesullivan Date: Sat, 30 Jul 2022 14:09:16 -0600 Subject: [PATCH 2/9] Improve repr --- large_image/tilesource/utilities.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/large_image/tilesource/utilities.py b/large_image/tilesource/utilities.py index f09ae862f..5275e1ec6 100644 --- a/large_image/tilesource/utilities.py +++ b/large_image/tilesource/utilities.py @@ -36,12 +36,26 @@ 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): return self def _repr_jpeg_(self): 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): """ @@ -85,7 +99,7 @@ def _encodeImageBinary(image, encoding, jpegQuality, jpegSubsampling, tiffCompre image.save(output, encoding, **params) btes = output.getvalue() if encoding in ['PNG', 'JPEG']: - return ImageBytes(btes) + return ImageBytes(btes, mimetype=f'image/{encoding.lower()}') return btes From 21192bd1197265fa5694ac8a2090b4c520a76974 Mon Sep 17 00:00:00 2001 From: banesullivan Date: Sat, 30 Jul 2022 14:18:15 -0600 Subject: [PATCH 3/9] Linting --- large_image/tilesource/utilities.py | 1 + 1 file changed, 1 insertion(+) diff --git a/large_image/tilesource/utilities.py b/large_image/tilesource/utilities.py index 5275e1ec6..9c4c2b63b 100644 --- a/large_image/tilesource/utilities.py +++ b/large_image/tilesource/utilities.py @@ -36,6 +36,7 @@ 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 From 1def889472f00f53b1c7d68ad7da0f79ec7024e2 Mon Sep 17 00:00:00 2001 From: banesullivan Date: Sat, 30 Jul 2022 14:26:05 -0600 Subject: [PATCH 4/9] Support TIFF in ImageBytes --- large_image/tilesource/utilities.py | 14 ++++++++------ test/test_source_tiff.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/large_image/tilesource/utilities.py b/large_image/tilesource/utilities.py index 9c4c2b63b..bbbd2a79f 100644 --- a/large_image/tilesource/utilities.py +++ b/large_image/tilesource/utilities.py @@ -47,10 +47,12 @@ def mimetype(self): return self._mime_type def _repr_png_(self): - return self + if self.mimetype == 'image/png': + return self def _repr_jpeg_(self): - return self + if self.mimetype == 'image/jpeg': + return self def __repr__(self): if self.mimetype: @@ -98,10 +100,10 @@ def _encodeImageBinary(image, encoding, jpegQuality, jpegSubsampling, tiffCompre params['compress_level'] = 2 output = io.BytesIO() image.save(output, encoding, **params) - btes = output.getvalue() - if encoding in ['PNG', 'JPEG']: - return ImageBytes(btes, mimetype=f'image/{encoding.lower()}') - return btes + 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_tiff.py b/test/test_source_tiff.py index f2a94a299..7bc9f6539 100644 --- a/test/test_source_tiff.py +++ b/test/test_source_tiff.py @@ -275,7 +275,7 @@ def testThumbnails(): assert isinstance(image, ImageBytes) assert image[:len(utilities.PNGHeader)] == utilities.PNGHeader image, mimeType = source.getThumbnail(encoding='TIFF') - assert isinstance(image, bytes) + assert isinstance(image, ImageBytes) assert image[:len(utilities.TIFFHeader)] == utilities.TIFFHeader image, mimeType = source.getThumbnail(jpegQuality=10) assert isinstance(image, ImageBytes) From d2d50e297b8b63ea5aef9d821775f2ab3eb2b226 Mon Sep 17 00:00:00 2001 From: banesullivan Date: Sat, 30 Jul 2022 14:30:52 -0600 Subject: [PATCH 5/9] Handle with simplejpeg --- large_image/tilesource/utilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/large_image/tilesource/utilities.py b/large_image/tilesource/utilities.py index bbbd2a79f..ac0a7b1c4 100644 --- a/large_image/tilesource/utilities.py +++ b/large_image/tilesource/utilities.py @@ -81,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'}: From 0347a078c42edc6f000eaf30289e8ac4c0b5cf45 Mon Sep 17 00:00:00 2001 From: banesullivan Date: Sat, 30 Jul 2022 14:39:19 -0600 Subject: [PATCH 6/9] Test thumbnail with JPEG --- test/test_source_gdal.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_source_gdal.py b/test/test_source_gdal.py index d589030f0..588bc1da2 100644 --- a/test/test_source_gdal.py +++ b/test/test_source_gdal.py @@ -152,6 +152,9 @@ def testThumbnailFromGeotiffs(): 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.PNGHeader)] == 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') From 8f87435bd283d4e3c67414bd3b9cac7efe7de3e9 Mon Sep 17 00:00:00 2001 From: banesullivan Date: Sat, 30 Jul 2022 15:19:23 -0600 Subject: [PATCH 7/9] Fix typo --- test/test_source_gdal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_source_gdal.py b/test/test_source_gdal.py index 588bc1da2..f0f84f50a 100644 --- a/test/test_source_gdal.py +++ b/test/test_source_gdal.py @@ -154,7 +154,7 @@ def testThumbnailFromGeotiffs(): assert image[:len(utilities.PNGHeader)] == utilities.PNGHeader image, mimeType = source.getThumbnail(encoding='JPEG') assert isinstance(image, ImageBytes) - assert image[:len(utilities.PNGHeader)] == utilities.JPEGHeader + 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') From 41baa108952cb43a32b034f7d5b594a27f02e3c9 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 1 Aug 2022 12:48:28 -0400 Subject: [PATCH 8/9] Use isinstance not type. --- utilities/converter/large_image_converter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 33b1208da2374130072a447fed0bee1b091c0b08 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 1 Aug 2022 13:35:16 -0400 Subject: [PATCH 9/9] Add some tests. --- test/test_source_base.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) 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