Skip to content

Commit

Permalink
Merge pull request #902 from girder/image-bytes
Browse files Browse the repository at this point in the history
Improve repr of image bytes
  • Loading branch information
manthey authored Aug 2, 2022
2 parents 8f14861 + 33b1208 commit 28e31b5
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 4 deletions.
35 changes: 32 additions & 3 deletions large_image/tilesource/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'}:
Expand All @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions test/test_source_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions test/test_source_gdal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions test/test_source_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion utilities/converter/large_image_converter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 28e31b5

Please sign in to comment.