From 25a5d7c250b6a55471d6677d5471c8304defb9c9 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 24 Jan 2025 09:14:00 -0500 Subject: [PATCH 1/4] Better repr of large_image classes Before, the repr of a large_image tile source was something like `OpenslideFileTileSource ('/data/samplefile.svs', 'JPEG', 95, 0, 'raw', False, '__STYLESTART__', {'bands': [{'band': 1, 'palette': 'white'}]}, '__STYLEEND__')`. Now, this is `OpenslideFileTileSource('/data/samplefile.svs', style={'bands': [{'band': 1, 'palette': 'white'}]})`, which could actually be used to open the tile source again. If the class is unpickleable (for instance, a vips tile sink), the repr is surrounded by `<>` to indicate this. As an added feature, `__rich_repr__` has been added to make the results prettier for those using the rich text library. --- CHANGELOG.md | 1 + large_image/tilesource/base.py | 21 ++++++++++++++++++- .../pil/large_image_source_pil/__init__.py | 4 +++- sources/pil/setup.py | 3 ++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c200bb9..1c8520d7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Automatically set the JUPYTER_PROXY value ([#1781](../../pull/1781)) - Add a general channelNames property to tile sources ([#1783](../../pull/1783)) - Speed up compositing styles ([#1784](../../pull/1784)) +- Better repr of large_image classes ([#1787](../../pull/1787)) ### Changes diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index ab89b973c..8ed146b43 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -200,11 +200,30 @@ def __reduce__(self) -> Tuple[functools.partial, Tuple[str]]: return functools.partial(type(self), **self._initValues[1]), self._initValues[0] def __repr__(self) -> str: - return self.getState() + if hasattr(self, '_initValues') and not hasattr(self, '_unpickleable'): + param = [ + f'{k}={v!r}' if k != 'style' or not isinstance(v, dict) or + not getattr(self, '_jsonstyle', None) else + f'style={json.loads(self._jsonstyle)}' + for k, v in self._initValues[1].items()] + return ( + f'{self.__class__.__name__}(' + f'{", ".join(repr(val) for val in self._initValues[0])}' + f'{", " if len(self._initValues[1]) else ""}' + f'{", ".join(param)}' + ')') + return '<' + self.getState() + '>' def _repr_png_(self) -> bytes: return self.getThumbnail(encoding='PNG')[0] + def __rich_repr__(self) -> Iterator[Any]: + if not hasattr(self, '_initValues') or hasattr(self, '_unpickleable'): + yield self.getState() + else: + yield from self._initValues[0] + yield from self._initValues[1].items() + @property def geospatial(self) -> bool: return False diff --git a/sources/pil/large_image_source_pil/__init__.py b/sources/pil/large_image_source_pil/__init__.py index 14fad53cc..b4b1ac5fc 100644 --- a/sources/pil/large_image_source_pil/__init__.py +++ b/sources/pil/large_image_source_pil/__init__.py @@ -224,9 +224,11 @@ def _fromRawpy(self, largeImagePath): """ # if rawpy is present, try reading via that library first try: + import builtins + import rawpy - with contextlib.redirect_stderr(open(os.devnull, 'w')): + with contextlib.redirect_stderr(builtins.open(os.devnull, 'w')): rgb = rawpy.imread(largeImagePath).postprocess() rgb = large_image.tilesource.utilities._imageToNumpy(rgb)[0] if rgb.shape[2] == 2: diff --git a/sources/pil/setup.py b/sources/pil/setup.py index f5fd67aae..522bafaf7 100644 --- a/sources/pil/setup.py +++ b/sources/pil/setup.py @@ -58,7 +58,8 @@ def prerelease_local_scheme(version): 'all': [ 'rawpy', 'pillow-heif', - 'pillow-jxl-plugin', + 'pillow-jxl-plugin < 1.3 ; python_version < "3.8"', + 'pillow-jxl-plugin ; python_version >= "3.9"', 'pillow-jpls', ], 'girder': f'girder-large-image{limit_version}', From c6643ceb11b4453e946775e6dec3f84ea5569418 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 27 Jan 2025 09:10:35 -0500 Subject: [PATCH 2/4] List a few more known extensions for different sources Based on reading the gdal and omero test data sets, list extensions for a few sources that were not listed by the originating libraries. --- large_image/constants.py | 7 ++++--- .../large_image_source_bioformats/__init__.py | 4 ++++ sources/gdal/large_image_source_gdal/__init__.py | 10 ++++++++++ sources/pil/large_image_source_pil/__init__.py | 4 ++++ .../rasterio/large_image_source_rasterio/__init__.py | 9 +++++++++ 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/large_image/constants.py b/large_image/constants.py index fd841bea8..dbe6755fc 100644 --- a/large_image/constants.py +++ b/large_image/constants.py @@ -27,9 +27,10 @@ class SourcePriority(enum.IntEnum): LOWER = 6 IMPLICIT_HIGH = 7 IMPLICIT = 8 - FALLBACK_HIGH = 9 - FALLBACK = 10 - MANUAL = 11 # This and higher values will never be selected automatically + IMPLICIT_LOW = 9 + FALLBACK_HIGH = 10 + FALLBACK = 11 + MANUAL = 12 # This and higher values will never be selected automatically TILE_FORMAT_IMAGE = 'image' diff --git a/sources/bioformats/large_image_source_bioformats/__init__.py b/sources/bioformats/large_image_source_bioformats/__init__.py index a23850f76..40f504b6b 100644 --- a/sources/bioformats/large_image_source_bioformats/__init__.py +++ b/sources/bioformats/large_image_source_bioformats/__init__.py @@ -767,6 +767,10 @@ def addKnownExtensions(cls): 'fake', 'no'}: if ext not in cls.extensions: cls.extensions[ext] = SourcePriority.IMPLICIT + # These were found by reading some OMERO test files + for ext in {'columbusidx', 'dv_vol', 'lifext'}: + if ext.lower() not in cls.extensions: + cls.extensions[ext.lower()] = SourcePriority.IMPLICIT_LOW def open(*args, **kwargs): diff --git a/sources/gdal/large_image_source_gdal/__init__.py b/sources/gdal/large_image_source_gdal/__init__.py index 5f7b2ae3e..3d88cd813 100644 --- a/sources/gdal/large_image_source_gdal/__init__.py +++ b/sources/gdal/large_image_source_gdal/__init__.py @@ -1075,6 +1075,16 @@ def addKnownExtensions(cls): if drvmimes is not None: if drvmimes not in cls.mimeTypes: cls.mimeTypes[drvmimes] = SourcePriority.IMPLICIT + # This list was compiled by trying to read the test files in GDAL's + # repo. + for ext in { + 'adf', 'aux', 'demtif', 'dim', 'doq', 'flt', 'fst', + 'gdbtable', 'gsc', 'h3', 'idf', 'lan', 'los', 'lrc', + 'mapml', 'mint.bin', 'mtw', 'nsf', 'nws', 'on2', 'on9', + 'osm.pbf', 'pjg', 'prj', 'ptf', 'rasterlite', 'rdb', 'rl2', + 'shx', 'sos', 'tif.grd', 'til', 'vic', 'xlb'}: + if ext.lower() not in cls.extensions: + cls.extensions[ext.lower()] = SourcePriority.IMPLICIT_LOW def open(*args, **kwargs): diff --git a/sources/pil/large_image_source_pil/__init__.py b/sources/pil/large_image_source_pil/__init__.py index b4b1ac5fc..6bae96297 100644 --- a/sources/pil/large_image_source_pil/__init__.py +++ b/sources/pil/large_image_source_pil/__init__.py @@ -325,6 +325,10 @@ def addKnownExtensions(cls): for mimeType in PIL.Image.MIME.values(): if mimeType not in cls.mimeTypes: cls.mimeTypes[mimeType] = SourcePriority.IMPLICIT_HIGH + # These were found by reading various test files. + for ext in {'ppg'}: + if ext.lower() not in cls.extensions: + cls.extensions[ext.lower()] = SourcePriority.IMPLICIT_LOW def open(*args, **kwargs): diff --git a/sources/rasterio/large_image_source_rasterio/__init__.py b/sources/rasterio/large_image_source_rasterio/__init__.py index 973769e10..098f561ed 100644 --- a/sources/rasterio/large_image_source_rasterio/__init__.py +++ b/sources/rasterio/large_image_source_rasterio/__init__.py @@ -1077,6 +1077,15 @@ def addKnownExtensions(cls): for ext in rasterio.drivers.raster_driver_extensions(): if ext not in cls.extensions: cls.extensions[ext] = SourcePriority.IMPLICIT + # This list was compiled by trying to read the test files in GDAL's + # repo. + for ext in { + 'adf', 'aux', 'demtif', 'dim', 'doq', 'flt', 'fst', 'gsc', + 'h3', 'lan', 'los', 'lrc', 'mint.bin', 'mtw', 'nsf', 'nws', + 'on9', 'pjg', 'png.ovr', 'prj', 'ptf', 'rasterlite', 'rdb', + 'tif.grd', 'til', 'vic', 'xlb'}: + if ext.lower() not in cls.extensions: + cls.extensions[ext.lower()] = SourcePriority.IMPLICIT_LOW def open(*args, **kwargs): From 109dec74ccbadd4c3f38f502479805b54e84ea24 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 27 Jan 2025 13:54:34 -0500 Subject: [PATCH 3/4] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c8520d7a..3dec87620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### Changes - Allow umap-learn for graphing in girder annotations in python 3.13 ([#1780](../../pull/1780)) +- List a few more known extensions for different sources ([#1790](../../pull/1790)) ### Bug Fixes From 9d7a4060824b8264fb3364362ac9147115e208a3 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 28 Jan 2025 11:41:09 -0500 Subject: [PATCH 4/4] Better detect multiframe images in PIL --- CHANGELOG.md | 1 + sources/pil/large_image_source_pil/__init__.py | 10 +++++++++- test/lisource_compare.py | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dec87620..1022f5159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Add a general channelNames property to tile sources ([#1783](../../pull/1783)) - Speed up compositing styles ([#1784](../../pull/1784)) - Better repr of large_image classes ([#1787](../../pull/1787)) +- Better detect multiframe images in PIL ([#1791](../../pull/1791)) ### Changes diff --git a/sources/pil/large_image_source_pil/__init__.py b/sources/pil/large_image_source_pil/__init__.py index 6bae96297..02037805a 100644 --- a/sources/pil/large_image_source_pil/__init__.py +++ b/sources/pil/large_image_source_pil/__init__.py @@ -101,6 +101,7 @@ class PILFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): 'jpg': SourcePriority.LOW, 'jpeg': SourcePriority.LOW, 'jpe': SourcePriority.LOW, + 'nef': SourcePriority.LOW, } mimeTypes = { None: SourcePriority.FALLBACK_HIGH, @@ -222,7 +223,14 @@ def _fromRawpy(self, largeImagePath): """ Try to use rawpy to read an image. """ - # if rawpy is present, try reading via that library first + # if rawpy is present, try reading via that library first, but only + # if PIL reports a single frame + try: + img = PIL.Image.open(largeImagePath) + if len(list(PIL.ImageSequence.Iterator(img))) > 1: + return + except Exception: + pass try: import builtins diff --git a/test/lisource_compare.py b/test/lisource_compare.py index 19bfe7cce..53071cc53 100755 --- a/test/lisource_compare.py +++ b/test/lisource_compare.py @@ -568,6 +568,7 @@ def command(): if not large_image.tilesource.AvailableTileSources: large_image.tilesource.loadTileSources() if opts.all: + large_image.config.setConfig('max_small_image_size', 16384) for key in list(large_image.config.ConfigValues): if '_ignored_names' in key: del large_image.config.ConfigValues[key]