diff --git a/large_image/exceptions.py b/large_image/exceptions.py index 5a71a5b4f..8fe0683be 100644 --- a/large_image/exceptions.py +++ b/large_image/exceptions.py @@ -17,6 +17,10 @@ class TileSourceXYZRangeError(TileSourceError): pass +class TileSourceInefficientError(TileSourceError): + pass + + class TileSourceFileNotFoundError(TileSourceError, FileNotFoundError): def __init__(self, *args, **kwargs): return super().__init__(errno.ENOENT, *args, **kwargs) diff --git a/sources/gdal/large_image_source_gdal/__init__.py b/sources/gdal/large_image_source_gdal/__init__.py index ffa28ca3b..c7618720d 100644 --- a/sources/gdal/large_image_source_gdal/__init__.py +++ b/sources/gdal/large_image_source_gdal/__init__.py @@ -41,7 +41,9 @@ from large_image.constants import (TILE_FORMAT_IMAGE, TILE_FORMAT_NUMPY, TILE_FORMAT_PIL, SourcePriority, TileInputUnits, TileOutputMimeTypes) -from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError +from large_image.exceptions import (TileSourceError, + TileSourceFileNotFoundError, + TileSourceInefficientError) from large_image.tilesource import FileTileSource from large_image.tilesource.utilities import getPaletteColors @@ -1185,6 +1187,44 @@ def getRegion(self, format=(TILE_FORMAT_IMAGE, ), **kwargs): raise exc return pathlib.Path(outputPath), TileOutputMimeTypes['TILED'] + def validateCOG(self, check_tiled=True, full_check=False, strict=True, warn=True): + """Check if this image is a valid Cloud Optimized GeoTiff. + + This will raise a :class:`large_image.exceptions.TileSourceInefficientError` + if not a valid Cloud Optimized GeoTiff. Otherwise, returns True. + + Requires the ``osgeo_utils`` package. + + Parameters + ---------- + check_tiled : bool + Set to False to ignore missing tiling. + full_check : bool + Set to True to check tile/strip leader/trailer bytes. + Might be slow on remote files + strict : bool + Enforce warnings as exceptions. Set to False to only warn and not + raise exceptions. + warn : bool + Log any warnings + + """ + from osgeo_utils.samples.validate_cloud_optimized_geotiff import validate + + warnings, errors, details = validate( + self._largeImagePath, + check_tiled=check_tiled, + full_check=full_check + ) + if errors: + raise TileSourceInefficientError(errors) + if strict and warnings: + raise TileSourceInefficientError(warnings) + if warn: + for warning in warnings: + self.logger.warning(warning) + return True + def open(*args, **kwargs): """ diff --git a/test/datastore.py b/test/datastore.py index 64ee7c797..8c88e6649 100644 --- a/test/datastore.py +++ b/test/datastore.py @@ -75,6 +75,11 @@ # Multi source file using different sources # Source: manually generated. 'multi_source.yml': 'sha512:81d7768b06eca6903082daa5b91706beaac8557ba4cede7f826524303df69a33478d6bb205c56af7ee2b45cd7d75897cc4b5704f743ddbf71bb3537ed3b9e8a8', # noqa + # Geospatial tiff - not cloud optimized + 'TC_NG_SFBay_US_Geo.tif': 'sha512:da2e66528f77a5e10af5de9e496074b77277c3da81dafc69790189510e5a7e18dba9e966329d36c979f1b547f0d36a82fbc4cfccc65ae9ef9e2747b5a9ee77b0', # noqa + # Geospatial tiff - cloud optimized + 'TC_NG_SFBay_US_Geo_COG.tif': 'sha512:5e56cdb8fb1a02615698a153862c10d5292b1ad42836a6e8bce5627e93a387dc0d3c9b6cfbd539796500bc2d3e23eafd07550f8c214e9348880bbbc6b3b0ea0c', # noqa + } diff --git a/test/test_source_base.py b/test/test_source_base.py index c55c86bf5..f595e6354 100644 --- a/test/test_source_base.py +++ b/test/test_source_base.py @@ -42,16 +42,16 @@ 'openjpeg': {'read': r'\.(jp2)$'}, 'openslide': { 'read': r'\.(ptif|svs|tif.*)$', - 'noread': r'(oahu|DDX58_AXL|huron\.image2_jpeg2k|landcover_sample|d042-353\.crop)', + 'noread': r'(oahu|DDX58_AXL|huron\.image2_jpeg2k|landcover_sample|d042-353\.crop|US_Geo\.)', 'skipTiles': r'one_layer_missing'}, 'pil': { 'read': r'\.(jpeg|png|tif.*)$', - 'noread': r'(G10-3|JK-kidney|d042-353|huron|one_layer_missing)'}, + 'noread': r'(G10-3|JK-kidney|d042-353|huron|one_layer_missing|US_Geo)'}, 'test': {'any': True, 'skipTiles': r''}, 'tiff': { 'read': r'\.(ptif|scn|svs|tif.*)$', 'noread': r'(oahu|DDX58_AXL|G10-3_pelvis_crop|' - r'd042-353\.crop\.small\.float|landcover_sample)', + r'd042-353\.crop\.small\.float|landcover_sample|US_Geo\.)', 'skipTiles': r'(sample_image\.ptif|one_layer_missing_tiles)'}, 'vips': { 'read': r'', @@ -66,7 +66,7 @@ # Python 3.6 has an older version of PIL that won't read some of the # ome.tif files. SourceAndFiles['pil']['noread'] = \ - r'(G10-3|JK-kidney|d042-353|huron|sample.*ome|one_layer_missing)' + r'(G10-3|JK-kidney|d042-353|huron|sample.*ome|one_layer_missing|US_Geo)' def testNearPowerOfTwo(): diff --git a/test/test_source_gdal.py b/test/test_source_gdal.py index 10bcaf8f2..69bce9c30 100644 --- a/test/test_source_gdal.py +++ b/test/test_source_gdal.py @@ -10,7 +10,7 @@ import pytest from large_image import constants -from large_image.exceptions import TileSourceError +from large_image.exceptions import TileSourceError, TileSourceInefficientError from . import utilities from .datastore import datastore @@ -550,3 +550,15 @@ def testHttpVfsPath(): assert tileMetadata['bounds']['ymin'] == pytest.approx(4876273, 1) assert tileMetadata['bounds']['srs'] == 'epsg:3857' assert tileMetadata['geospatial'] + + +def testVfsCogValidation(): + imagePath = datastore.get_url('TC_NG_SFBay_US_Geo_COG.tif') + source = large_image_source_gdal.open( + imagePath, projection='EPSG:3857', encoding='PNG') + assert source.validateCOG() + imagePath = datastore.get_url('TC_NG_SFBay_US_Geo.tif') + source = large_image_source_gdal.open( + imagePath, projection='EPSG:3857', encoding='PNG') + with pytest.raises(TileSourceInefficientError): + source.validateCOG()