diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c200bb9..1022f5159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,10 +16,13 @@ - 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)) +- Better detect multiframe images in PIL ([#1791](../../pull/1791)) ### 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 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/large_image/tilesource/base.py b/large_image/tilesource/base.py index fa63b1a72..d677f485c 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/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 14fad53cc..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,11 +223,20 @@ 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 + 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: @@ -323,6 +333,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/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}', 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): 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]