diff --git a/girder/girder_large_image/girder_tilesource.py b/girder/girder_large_image/girder_tilesource.py index 1f49032cd..639004bde 100644 --- a/girder/girder_large_image/girder_tilesource.py +++ b/girder/girder_large_image/girder_tilesource.py @@ -4,7 +4,7 @@ from girder.models.item import Item from large_image import tilesource from large_image.constants import SourcePriority -from large_image.exceptions import TileSourceAssetstoreException, TileSourceException +from large_image.exceptions import TileSourceAssetstoreError, TileSourceError AvailableGirderTileSources = {} KnownMimeTypes = set() @@ -101,13 +101,13 @@ def _getLargeImagePath(self): try: largeImagePath = File().getLocalFilePath(largeImageFile) except AttributeError as e: - raise TileSourceException( + raise TileSourceError( 'No local file path for this file: %s' % e.args[0]) return largeImagePath - except (TileSourceAssetstoreException, FilePathException): + except (TileSourceAssetstoreError, FilePathException): raise - except (KeyError, ValidationException, TileSourceException) as e: - raise TileSourceException( + except (KeyError, ValidationException, TileSourceError) as e: + raise TileSourceError( 'No large image file in this item: %s' % e.args[0]) diff --git a/girder/girder_large_image/models/image_item.py b/girder/girder_large_image/models/image_item.py index 9aa772c06..f3458729f 100644 --- a/girder/girder_large_image/models/image_item.py +++ b/girder/girder_large_image/models/image_item.py @@ -32,7 +32,7 @@ from girder.models.upload import Upload from large_image.cache_util import getTileCache, strhash from large_image.constants import TileOutputMimeTypes -from large_image.exceptions import TileGeneralException, TileSourceException +from large_image.exceptions import TileGeneralError, TileSourceError from .. import constants, girder_tilesource @@ -60,13 +60,13 @@ def createImageItem(self, item, fileObj, user=None, token=None, createJob=True, notify=False, **kwargs): # Using setdefault ensures that 'largeImage' is in the item if 'fileId' in item.setdefault('largeImage', {}): - raise TileGeneralException('Item already has largeImage set.') + raise TileGeneralError('Item already has largeImage set.') if fileObj['itemId'] != item['_id']: - raise TileGeneralException( + raise TileGeneralError( 'The provided file must be in the provided item.') if (item['largeImage'].get('expected') is True and 'jobId' in item['largeImage']): - raise TileGeneralException( + raise TileGeneralError( 'Item is scheduled to generate a largeImage.') item['largeImage'].pop('expected', None) @@ -79,7 +79,7 @@ def createImageItem(self, item, fileObj, user=None, token=None, item['largeImage']['sourceName'] = sourceName if not sourceName or createJob == 'always': if not createJob: - raise TileGeneralException( + raise TileGeneralError( 'A job must be used to generate a largeImage.') # No source was successful del item['largeImage']['fileId'] @@ -120,7 +120,7 @@ def _createLargeImageJob(self, item, fileObj, user, token, **kwargs): def convertImage(self, item, fileObj, user=None, token=None, localJob=True, **kwargs): if fileObj['itemId'] != item['_id']: - raise TileGeneralException( + raise TileGeneralError( 'The provided file must be in the provided item.') if not localJob: return self._convertImageViaWorker(item, fileObj, user, token, **kwargs) @@ -187,7 +187,7 @@ def _tileFromHash(cls, item, x, y, z, mayRedirect=False, **kwargs): sourceName = item['largeImage']['sourceName'] try: sourceClass = girder_tilesource.AvailableGirderTileSources[sourceName] - except TileSourceException: + except TileSourceError: return None classHash = sourceClass.getLRUHash(item, **kwargs) tileHash = sourceClass.__name__ + ' ' + classHash + ' ' + strhash( @@ -206,17 +206,17 @@ def _tileFromHash(cls, item, x, y, z, mayRedirect=False, **kwargs): @classmethod def _loadTileSource(cls, item, **kwargs): if 'largeImage' not in item: - raise TileSourceException('No large image file in this item.') + raise TileSourceError('No large image file in this item.') if item['largeImage'].get('expected'): - raise TileSourceException('The large image file for this item is ' - 'still pending creation.') + raise TileSourceError('The large image file for this item is ' + 'still pending creation.') sourceName = item['largeImage']['sourceName'] try: # First try to use the tilesource we recorded as the preferred one. # This is faster than trying to find the best source each time. tileSource = girder_tilesource.AvailableGirderTileSources[sourceName](item, **kwargs) - except TileSourceException: + except TileSourceError: # We could try any source # tileSource = girder_tilesource.getGirderTileSource(item, **kwargs) # but, instead, log that the original source no longer works are diff --git a/girder/girder_large_image/rest/large_image_resource.py b/girder/girder_large_image/rest/large_image_resource.py index 991546604..b983597c9 100644 --- a/girder/girder_large_image/rest/large_image_resource.py +++ b/girder/girder_large_image/rest/large_image_resource.py @@ -34,7 +34,7 @@ from girder.models.item import Item from girder.models.setting import Setting from large_image import cache_util -from large_image.exceptions import TileGeneralException +from large_image.exceptions import TileGeneralError from .. import constants, girder_tilesource from ..models.image_item import ImageItem @@ -56,7 +56,7 @@ def createThumbnailsJobTask(item, spec): else: result = ImageItem().getThumbnail(item, checkAndCreate=True, **entry) status['checked' if result is True else 'created'] += 1 - except TileGeneralException as exc: + except TileGeneralError as exc: status['failed'] += 1 status['lastFailed'] = str(item['_id']) logger.info('Failed to get thumbnail for item %s: %r' % (item['_id'], exc)) diff --git a/girder/girder_large_image/rest/tiles.py b/girder/girder_large_image/rest/tiles.py index 1d719f12f..9fcde3fbd 100644 --- a/girder/girder_large_image/rest/tiles.py +++ b/girder/girder_large_image/rest/tiles.py @@ -34,7 +34,7 @@ from girder.utility.progress import setResponseTimeLimit from large_image.cache_util import strhash from large_image.constants import TileInputUnits -from large_image.exceptions import TileGeneralException +from large_image.exceptions import TileGeneralError from .. import loadmodelcache from ..models.image_item import ImageItem @@ -188,7 +188,7 @@ def createTiles(self, item, params): createJob='always' if self.boolParam('force', params, default=False) else True, notify=notify, **params) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0]) @describeRoute( @@ -251,7 +251,7 @@ def convertImage(self, item, params): try: return self.imageItemModel.convertImage( item, largeImageFile, user, token, localJob=localJob, **params) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0]) @classmethod @@ -325,7 +325,7 @@ def _getTilesInfo(self, item, imageArgs): """ try: return self.imageItemModel.getMetadata(item, **imageArgs) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0], code=400) def _setContentDisposition(self, item, contentDisposition, mime, subname, fullFilename=None): @@ -383,7 +383,7 @@ def getTilesInfo(self, item, params): def getInternalMetadata(self, item, params): try: return self.imageItemModel.getInternalMetadata(item, **params) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0], code=400) @describeRoute( @@ -462,7 +462,7 @@ def _getTile(self, item, z, x, y, imageArgs, mayRedirect=False): try: tileData, tileMime = self.imageItemModel.getTile( item, x, y, z, mayRedirect=mayRedirect, **imageArgs) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0], code=404) setResponseHeader('Content-Type', tileMime) setRawResponse() @@ -690,7 +690,7 @@ def getTilesThumbnail(self, item, params): _handleETag('getTilesThumbnail', item, params) try: result = self.imageItemModel.getThumbnail(item, **params) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0]) except ValueError as e: raise RestException('Value Error: %s' % e.args[0]) @@ -828,7 +828,7 @@ def getTilesRegion(self, item, params): try: regionData, regionMime = self.imageItemModel.getRegion( item, **params) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0]) except ValueError as e: raise RestException('Value Error: %s' % e.args[0]) @@ -884,7 +884,7 @@ def getTilesPixel(self, item, params): ]) try: pixel = self.imageItemModel.getPixel(item, **params) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0]) except ValueError as e: raise RestException('Value Error: %s' % e.args[0]) @@ -1005,7 +1005,7 @@ def getBandInformation(self, item, params): def getAssociatedImagesList(self, item, params): try: return self.imageItemModel.getAssociatedImagesList(item) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0], code=400) @describeRoute( @@ -1049,7 +1049,7 @@ def getAssociatedImage(self, itemId, image, params): _handleETag('getAssociatedImage', item, image, params) try: result = self.imageItemModel.getAssociatedImage(item, image, **params) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0], code=400) if not isinstance(result, tuple): return result @@ -1217,7 +1217,7 @@ def tileFrames(self, item, params): try: result = self.imageItemModel.tileFrames( item, checkAndCreate=checkAndCreate, **params) - except TileGeneralException as e: + except TileGeneralError as e: raise RestException(e.args[0]) except ValueError as e: raise RestException('Value Error: %s' % e.args[0]) diff --git a/girder/test_girder/test_web_client.py b/girder/test_girder/test_web_client.py index f463869d5..68fe599ff 100644 --- a/girder/test_girder/test_web_client.py +++ b/girder/test_girder/test_web_client.py @@ -9,8 +9,17 @@ @pytest.mark.plugin('large_image') @pytest.mark.parametrize('spec', ( 'imageViewerSpec.js', - 'largeImageSpec.js', )) def testWebClient(boundServer, fsAssetstore, db, spec, girderWorker): spec = os.path.join(os.path.dirname(__file__), 'web_client_specs', spec) runWebClientTest(boundServer, spec, 15000) + + +@pytest.mark.usefixtures('unbindLargeImage') +@pytest.mark.plugin('large_image') +@pytest.mark.parametrize('spec', ( + 'largeImageSpec.js', +)) +def testWebClientNoWorker(boundServer, fsAssetstore, db, spec): + spec = os.path.join(os.path.dirname(__file__), 'web_client_specs', spec) + runWebClientTest(boundServer, spec, 15000) diff --git a/large_image/exceptions.py b/large_image/exceptions.py index 1f8b51359..6586a834b 100644 --- a/large_image/exceptions.py +++ b/large_image/exceptions.py @@ -1,10 +1,23 @@ -class TileGeneralException(Exception): +import errno + + +class TileGeneralError(Exception): pass -class TileSourceException(TileGeneralException): +class TileSourceError(TileGeneralError): pass -class TileSourceAssetstoreException(TileSourceException): +class TileSourceAssetstoreError(TileSourceError): pass + + +class TileSourceFileNotFoundError(TileSourceError, FileNotFoundError): + def __init__(self, *args, **kwargs): + return super().__init__(errno.ENOENT, *args, **kwargs) + + +TileGeneralException = TileGeneralError +TileSourceException = TileSourceError +TileSourceAssetstoreException = TileSourceAssetstoreError diff --git a/large_image/tilesource/__init__.py b/large_image/tilesource/__init__.py index 329d5f09e..310703679 100644 --- a/large_image/tilesource/__init__.py +++ b/large_image/tilesource/__init__.py @@ -4,7 +4,10 @@ from .. import config from ..constants import SourcePriority -from ..exceptions import TileGeneralException, TileSourceAssetstoreException, TileSourceException +from ..exceptions import (TileGeneralError, TileGeneralException, + TileSourceAssetstoreError, + TileSourceAssetstoreException, TileSourceError, + TileSourceException, TileSourceFileNotFoundError) from .base import (TILE_FORMAT_IMAGE, TILE_FORMAT_NUMPY, TILE_FORMAT_PIL, FileTileSource, TileOutputMimeTypes, TileSource, dictToEtree, etreeToDict, nearPowerOfTwo) @@ -108,7 +111,7 @@ def getTileSourceFromDict(availableSources, pathOrUri, *args, **kwargs): sourceName = getSourceNameFromDict(availableSources, pathOrUri, *args, **kwargs) if sourceName: return availableSources[sourceName](pathOrUri, *args, **kwargs) - raise TileSourceException('No available tilesource for %s' % pathOrUri) + raise TileSourceError('No available tilesource for %s' % pathOrUri) def getTileSource(*args, **kwargs): @@ -151,7 +154,9 @@ def canRead(*args, **kwargs): __all__ = [ 'TileSource', 'FileTileSource', - 'exceptions', 'TileGeneralException', 'TileSourceException', 'TileSourceAssetstoreException', + 'exceptions', 'TileGeneralError', 'TileSourceError', + 'TileSourceAssetstoreError', 'TileSourceFileNotFoundError', + 'TileGeneralException', 'TileSourceException', 'TileSourceAssetstoreException', 'TileOutputMimeTypes', 'TILE_FORMAT_IMAGE', 'TILE_FORMAT_PIL', 'TILE_FORMAT_NUMPY', 'AvailableTileSources', 'getTileSource', 'canRead', 'getSourceNameFromDict', 'nearPowerOfTwo', 'etreeToDict', 'dictToEtree', diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index 09bb84843..a5c3724fe 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -126,7 +126,7 @@ def __init__(self, encoding='JPEG', jpegQuality=95, jpegSubsampling=0, if not isinstance(self.style, dict): raise TypeError except TypeError: - raise exceptions.TileSourceException('Style is not a valid json object.') + raise exceptions.TileSourceError('Style is not a valid json object.') @staticmethod def getLRUHash(*args, **kwargs): @@ -1374,17 +1374,17 @@ def _xyzInRange(self, x, y, z, frame=None, numFrames=None): exception if not. """ if z < 0 or z >= self.levels: - raise exceptions.TileSourceException('z layer does not exist') + raise exceptions.TileSourceError('z layer does not exist') scale = 2 ** (self.levels - 1 - z) offsetx = x * self.tileWidth * scale if not (0 <= offsetx < self.sizeX): - raise exceptions.TileSourceException('x is outside layer') + raise exceptions.TileSourceError('x is outside layer') offsety = y * self.tileHeight * scale if not (0 <= offsety < self.sizeY): - raise exceptions.TileSourceException('y is outside layer') + raise exceptions.TileSourceError('y is outside layer') if frame is not None and numFrames is not None: if frame < 0 or frame >= numFrames: - raise exceptions.TileSourceException('Frame does not exist') + raise exceptions.TileSourceError('Frame does not exist') @methodcache() def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, @@ -1639,7 +1639,7 @@ def _addRegionTileToImage( (height, width, subimage.shape[2]), dtype=subimage.dtype) except MemoryError: - raise exceptions.TileSourceException( + raise exceptions.TileSourceError( 'Insufficient memory to get region of %d x %d pixels.' % ( width, height)) if subimage.shape[2] > image.shape[2]: @@ -2342,5 +2342,5 @@ def canRead(cls, path, *args, **kwargs): try: cls(path, *args, **kwargs) return True - except exceptions.TileSourceException: + except exceptions.TileSourceError: return False diff --git a/large_image/tilesource/tiledict.py b/large_image/tilesource/tiledict.py index a383b836b..214b2d81b 100644 --- a/large_image/tilesource/tiledict.py +++ b/large_image/tilesource/tiledict.py @@ -210,7 +210,7 @@ def __getitem__(self, key, *args, **kwargs): tileData, **self.imageKwargs) tileFormat = TILE_FORMAT_IMAGE if tileFormat not in self.format: - raise exceptions.TileSourceException( + raise exceptions.TileSourceError( 'Cannot yield tiles in desired format %r' % ( self.format, )) else: diff --git a/sources/bioformats/large_image_source_bioformats/__init__.py b/sources/bioformats/large_image_source_bioformats/__init__.py index 4f99a2423..923efdc88 100644 --- a/sources/bioformats/large_image_source_bioformats/__init__.py +++ b/sources/bioformats/large_image_source_bioformats/__init__.py @@ -38,7 +38,7 @@ from large_image import config from large_image.cache_util import LruCacheMetaclass, methodcache from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority -from large_image.exceptions import TileSourceException +from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError from large_image.tilesource import FileTileSource, nearPowerOfTwo try: @@ -141,15 +141,17 @@ def __init__(self, path, **kwargs): # noqa ext = os.path.splitext(largeImagePath)[1] if not ext: - raise TileSourceException( + if not os.path.isfile(largeImagePath): + raise TileSourceFileNotFoundError(largeImagePath) from None + raise TileSourceError( 'File cannot be opened via bioformats because it has no ' 'extension to specify the file type (%s).' % largeImagePath) if ext.lower() in (config.getConfig( 'source_bioformats_ignored_extensions') or '.png').split(','): - raise TileSourceException('File will not be opened by bioformats reader') + raise TileSourceError('File will not be opened by bioformats reader') if not _startJavabridge(self.logger): - raise TileSourceException( + raise TileSourceError( 'File cannot be opened by bioformats reader because javabridge failed to start') self._tileLock = threading.RLock() @@ -159,8 +161,10 @@ def __init__(self, path, **kwargs): # noqa try: self._bioimage = bioformats.ImageReader(largeImagePath) except (AttributeError, FileNotFoundError) as exc: + if not os.path.isfile(largeImagePath): + raise TileSourceFileNotFoundError(largeImagePath) from None self.logger.debug('File cannot be opened via Bioformats. (%r)' % exc) - raise TileSourceException('File cannot be opened via Bioformats. (%r)' % exc) + raise TileSourceError('File cannot be opened via Bioformats. (%r)' % exc) _openImages.append(self) rdr = self._bioimage.rdr @@ -237,20 +241,20 @@ def __init__(self, path, **kwargs): # noqa except javabridge.JavaException as exc: es = javabridge.to_string(exc.throwable) self.logger.debug('File cannot be opened via Bioformats. (%s)' % es) - raise TileSourceException('File cannot be opened via Bioformats. (%s)' % es) + raise TileSourceError('File cannot be opened via Bioformats. (%s)' % es) except AttributeError: self.logger.exception('The bioformats reader threw an unhandled exception.') - raise TileSourceException('The bioformats reader threw an unhandled exception.') + raise TileSourceError('The bioformats reader threw an unhandled exception.') finally: if javabridge.get_env(): javabridge.detach() if self.levels < 1: - raise TileSourceException( + raise TileSourceError( 'OpenSlide image must have at least one level.') if self.sizeX <= 0 or self.sizeY <= 0: - raise TileSourceException('Bioformats tile size is invalid.') + raise TileSourceError('Bioformats tile size is invalid.') def __del__(self): if getattr(self, '_bioimage', None) is not None: @@ -455,7 +459,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): fxy = (frame // self._metadata['sizeC'] // self._metadata['sizeZ'] // self._metadata['sizeT']) if frame < 0 or fxy > self._metadata['sizeXY']: - raise TileSourceException('Frame does not exist') + raise TileSourceError('Frame does not exist') fseries = self._metadata['frameSeries'][fxy] seriesLevel = self.levels - 1 - z scale = 1 @@ -491,7 +495,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): format = TILE_FORMAT_NUMPY except javabridge.JavaException as exc: es = javabridge.to_string(exc.throwable) - raise TileSourceException('Failed to get Bioformat region (%s, %r).' % (es, ( + raise TileSourceError('Failed to get Bioformat region (%s, %r).' % (es, ( fc, fz, ft, fseries, self.sizeX, self.sizeY, offsetx, offsety, width, height))) finally: if javabridge.get_env(): @@ -543,7 +547,7 @@ def _getAssociatedImage(self, imageKey): XYWH=(0, 0, info['sizeX'], info['sizeY'])) except javabridge.JavaException as exc: es = javabridge.to_string(exc.throwable) - raise TileSourceException('Failed to get Bioformat series (%s, %r).' % (es, ( + raise TileSourceError('Failed to get Bioformat series (%s, %r).' % (es, ( series, info['sizeX'], info['sizeY']))) finally: if javabridge.get_env(): diff --git a/sources/deepzoom/large_image_source_deepzoom/__init__.py b/sources/deepzoom/large_image_source_deepzoom/__init__.py index 7bff126f2..2f2b5b565 100644 --- a/sources/deepzoom/large_image_source_deepzoom/__init__.py +++ b/sources/deepzoom/large_image_source_deepzoom/__init__.py @@ -10,7 +10,7 @@ from large_image import config from large_image.cache_util import LruCacheMetaclass, methodcache from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority -from large_image.exceptions import TileSourceException +from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError from large_image.tilesource import FileTileSource, etreeToDict @@ -45,12 +45,16 @@ def __init__(self, path, **kwargs): try: with builtins.open(self._largeImagePath) as fptr: if fptr.read(1024).strip()[:5] != ' 1: - raise TileSourceException('OME Tiff references multiple files') + raise TileSourceError('OME Tiff references multiple files') if (len(self._omebase['TiffData']) != int(self._omebase['SizeC']) * int(self._omebase['SizeT']) * int(self._omebase['SizeZ']) or len(self._omebase['TiffData']) != len( self._omebase.get('Plane', self._omebase['TiffData']))): - raise TileSourceException( + raise TileSourceError( 'OME Tiff contains frames that contain multiple planes') except (KeyError, ValueError, IndexError): - raise TileSourceException('OME Tiff does not contain an expected record') + raise TileSourceError('OME Tiff does not contain an expected record') def getMetadata(self): """ @@ -299,7 +302,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs) frame = int(kwargs.get('frame') or 0) if frame < 0 or frame >= len(self._omebase['TiffData']): - raise TileSourceException('Frame does not exist') + raise TileSourceError('Frame does not exist') subdir = None if self._omeLevels[z] is not None: dirnum = int(self._omeLevels[z]['TiffData'][frame].get('IFD', frame)) @@ -328,9 +331,9 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) except InvalidOperationTiffException as e: - raise TileSourceException(e.args[0]) + raise TileSourceError(e.args[0]) except IOTiffException as e: - return self.getTileIOTiffException( + return self.getTileIOTiffError( x, y, z, pilImageAllowed=pilImageAllowed, numpyAllowed=numpyAllowed, sparseFallback=sparseFallback, exception=e, **kwargs) diff --git a/sources/openjpeg/large_image_source_openjpeg/__init__.py b/sources/openjpeg/large_image_source_openjpeg/__init__.py index b11e3d1e3..1cc14c51a 100644 --- a/sources/openjpeg/large_image_source_openjpeg/__init__.py +++ b/sources/openjpeg/large_image_source_openjpeg/__init__.py @@ -18,6 +18,7 @@ import io import math import multiprocessing +import os import queue import struct import warnings @@ -29,7 +30,7 @@ from large_image.cache_util import LruCacheMetaclass, methodcache from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority -from large_image.exceptions import TileSourceException +from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError from large_image.tilesource import FileTileSource, etreeToDict try: @@ -92,7 +93,11 @@ def __init__(self, path, **kwargs): try: self._openjpeg = glymur.Jp2k(largeImagePath) except (glymur.jp2box.InvalidJp2kError, struct.error): - raise TileSourceException('File cannot be opened via Glymur and OpenJPEG.') + raise TileSourceError('File cannot be opened via Glymur and OpenJPEG.') + except FileNotFoundError: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + raise glymur.set_option('lib.num_threads', multiprocessing.cpu_count()) self._openjpegHandles = queue.LifoQueue() for _ in range(self._maxOpenHandles - 1): @@ -101,7 +106,7 @@ def __init__(self, path, **kwargs): try: self.sizeY, self.sizeX = self._openjpeg.shape[:2] except IndexError: - raise TileSourceException('File cannot be opened via Glymur and OpenJPEG.') + raise TileSourceError('File cannot be opened via Glymur and OpenJPEG.') self.levels = int(self._openjpeg.codestream.segment[2].num_res) + 1 self._minlevel = 0 self.tileWidth = self.tileHeight = 2 ** int(math.ceil(max( diff --git a/sources/openslide/large_image_source_openslide/__init__.py b/sources/openslide/large_image_source_openslide/__init__.py index 9c90f795d..0b012165f 100644 --- a/sources/openslide/large_image_source_openslide/__init__.py +++ b/sources/openslide/large_image_source_openslide/__init__.py @@ -15,6 +15,7 @@ ############################################################################## import math +import os import openslide import PIL @@ -23,7 +24,7 @@ from large_image.cache_util import LruCacheMetaclass, methodcache from large_image.constants import TILE_FORMAT_PIL, SourcePriority -from large_image.exceptions import TileSourceException +from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError from large_image.tilesource import FileTileSource, nearPowerOfTwo try: @@ -67,7 +68,7 @@ class OpenslideFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): 'image/x-tiff': SourcePriority.MEDIUM, } - def __init__(self, path, **kwargs): + def __init__(self, path, **kwargs): # noqa """ Initialize the tile class. See the base class for other available parameters. @@ -76,23 +77,25 @@ def __init__(self, path, **kwargs): """ super().__init__(path, **kwargs) - largeImagePath = self._getLargeImagePath() + self._largeImagePath = self._getLargeImagePath() try: - self._openslide = openslide.OpenSlide(largeImagePath) + self._openslide = openslide.OpenSlide(self._largeImagePath) except openslide.lowlevel.OpenSlideUnsupportedFormatError: - raise TileSourceException('File cannot be opened via OpenSlide.') + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + raise TileSourceError('File cannot be opened via OpenSlide.') except openslide.lowlevel.OpenSlideError: - raise TileSourceException('File will not be opened via OpenSlide.') + raise TileSourceError('File will not be opened via OpenSlide.') if libtiff_ctypes: try: - self._tiffinfo = tifftools.read_tiff(largeImagePath) + self._tiffinfo = tifftools.read_tiff(self._largeImagePath) except Exception: pass - svsAvailableLevels = self._getAvailableLevels(largeImagePath) + svsAvailableLevels = self._getAvailableLevels(self._largeImagePath) if not len(svsAvailableLevels): - raise TileSourceException('OpenSlide image size is invalid.') + raise TileSourceError('OpenSlide image size is invalid.') self.sizeX = svsAvailableLevels[0]['width'] self.sizeY = svsAvailableLevels[0]['height'] if (self.sizeX != self._openslide.dimensions[0] or @@ -110,7 +113,7 @@ def __init__(self, path, **kwargs): math.log(float(self.sizeX) / self.tileWidth), math.log(float(self.sizeY) / self.tileHeight)) / math.log(2))) + 1 if self.levels < 1: - raise TileSourceException( + raise TileSourceError( 'OpenSlide image must have at least one level.') self._svslevels = [] # Precompute which SVS level should be used for our tile levels. SVS @@ -144,12 +147,11 @@ def __init__(self, path, **kwargs): msg = ('OpenSlide has no small-scale tiles (level %d is at %d ' 'scale)' % (level, scale)) self.logger.info(msg) - raise TileSourceException(msg) + raise TileSourceError(msg) self._svslevels.append({ 'svslevel': bestlevel, 'scale': scale }) - self._largeImagePath = largeImagePath def _getTileSize(self): """ @@ -286,7 +288,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): (self.tileWidth * svslevel['scale'], self.tileHeight * svslevel['scale'])) except openslide.lowlevel.OpenSlideError as exc: - raise TileSourceException( + raise TileSourceError( 'Failed to get OpenSlide region (%r).' % exc) # Always scale to the svs level 0 tile size. if svslevel['scale'] != 1: diff --git a/sources/pil/large_image_source_pil/__init__.py b/sources/pil/large_image_source_pil/__init__.py index 024a225c5..a4f486c5e 100644 --- a/sources/pil/large_image_source_pil/__init__.py +++ b/sources/pil/large_image_source_pil/__init__.py @@ -25,7 +25,7 @@ from large_image import config from large_image.cache_util import LruCacheMetaclass, methodcache, strhash from large_image.constants import TILE_FORMAT_PIL, SourcePriority -from large_image.exceptions import TileSourceException +from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError from large_image.tilesource import FileTileSource try: @@ -91,7 +91,7 @@ def __init__(self, path, maxSize=None, **kwargs): try: maxSize = json.loads(maxSize) except Exception: - raise TileSourceException( + raise TileSourceError( 'maxSize must be None, an integer, a dictionary, or a ' 'JSON string that converts to one of those.') self.maxSize = maxSize @@ -102,11 +102,13 @@ def __init__(self, path, maxSize=None, **kwargs): # instances, mirax (mrxs) files look like JPEGs, but opening them as # such misses most of the data. if os.path.splitext(largeImagePath)[1] in ('.mrxs', ): - raise TileSourceException('File cannot be opened via PIL.') + raise TileSourceError('File cannot be opened via PIL.') try: self._pilImage = PIL.Image.open(largeImagePath) except OSError: - raise TileSourceException('File cannot be opened via PIL.') + if not os.path.isfile(largeImagePath): + raise TileSourceFileNotFoundError(largeImagePath) from None + raise TileSourceError('File cannot be opened via PIL.') # If this is encoded as a 32-bit integer or a 32-bit float, convert it # to an 8-bit integer. This expects the source value to either have a # maximum of 1, 2^8-1, 2^16-1, 2^24-1, or 2^32-1, and scales it to @@ -125,10 +127,10 @@ def __init__(self, path, maxSize=None, **kwargs): self.levels = 1 # Throw an exception if too big if self.tileWidth <= 0 or self.tileHeight <= 0: - raise TileSourceException('PIL tile size is invalid.') + raise TileSourceError('PIL tile size is invalid.') maxWidth, maxHeight = getMaxSize(maxSize, self.defaultMaxSize()) if self.tileWidth > maxWidth or self.tileHeight > maxHeight: - raise TileSourceException('PIL tile size is too large.') + raise TileSourceError('PIL tile size is too large.') def defaultMaxSize(self): """ @@ -169,11 +171,11 @@ def getInternalMetadata(self, **kwargs): def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, mayRedirect=False, **kwargs): if z != 0: - raise TileSourceException('z layer does not exist') + raise TileSourceError('z layer does not exist') if x != 0: - raise TileSourceException('x is outside layer') + raise TileSourceError('x is outside layer') if y != 0: - raise TileSourceException('y is outside layer') + raise TileSourceError('y is outside layer') return self._outputTile(self._pilImage, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/pil/large_image_source_pil/girder_source.py b/sources/pil/large_image_source_pil/girder_source.py index 5b8ca7d0f..9706e4ff7 100644 --- a/sources/pil/large_image_source_pil/girder_source.py +++ b/sources/pil/large_image_source_pil/girder_source.py @@ -21,7 +21,7 @@ from girder.models.setting import Setting from large_image.cache_util import methodcache from large_image.constants import TILE_FORMAT_PIL -from large_image.exceptions import TileSourceException +from large_image.exceptions import TileSourceError from . import PILFileTileSource @@ -53,11 +53,11 @@ def getState(self): def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, mayRedirect=False, **kwargs): if z != 0: - raise TileSourceException('z layer does not exist') + raise TileSourceError('z layer does not exist') if x != 0: - raise TileSourceException('x is outside layer') + raise TileSourceError('x is outside layer') if y != 0: - raise TileSourceException('y is outside layer') + raise TileSourceError('y is outside layer') if (mayRedirect and not pilImageAllowed and not numpyAllowed and cherrypy.request and self._pilFormatMatches(self._pilImage, mayRedirect, **kwargs)): diff --git a/sources/test/large_image_source_test/__init__.py b/sources/test/large_image_source_test/__init__.py index c1699fc64..b11f6b34a 100644 --- a/sources/test/large_image_source_test/__init__.py +++ b/sources/test/large_image_source_test/__init__.py @@ -23,7 +23,7 @@ from large_image.cache_util import LruCacheMetaclass, methodcache, strhash from large_image.constants import TILE_FORMAT_PIL, SourcePriority -from large_image.exceptions import TileSourceException +from large_image.exceptions import TileSourceError from large_image.tilesource import TileSource try: @@ -169,7 +169,7 @@ def getTile(self, x, y, z, *args, **kwargs): self._xyzInRange(x, y, z, frame, len(self._frames) if hasattr(self, '_frames') else None) if not (self.minLevel <= z <= self.maxLevel): - raise TileSourceException('z layer does not exist') + raise TileSourceError('z layer does not exist') xFraction = (x + 0.5) * self.tileWidth * 2 ** (self.levels - 1 - z) / self.sizeX yFraction = (y + 0.5) * self.tileHeight * 2 ** (self.levels - 1 - z) / self.sizeY diff --git a/sources/tiff/large_image_source_tiff/__init__.py b/sources/tiff/large_image_source_tiff/__init__.py index b7287d17f..fdb0aba91 100644 --- a/sources/tiff/large_image_source_tiff/__init__.py +++ b/sources/tiff/large_image_source_tiff/__init__.py @@ -19,6 +19,7 @@ import itertools import json import math +import os import numpy import PIL.Image @@ -28,7 +29,7 @@ from large_image import config from large_image.cache_util import LruCacheMetaclass, methodcache from large_image.constants import TILE_FORMAT_NUMPY, TILE_FORMAT_PIL, SourcePriority -from large_image.exceptions import TileSourceException +from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError from large_image.tilesource import FileTileSource, nearPowerOfTwo from .tiff_reader import (InvalidOperationTiffException, IOTiffException, @@ -99,10 +100,12 @@ def __init__(self, path, **kwargs): # If there are no tiled images, raise an exception. if not len(alldir): + if not os.path.isfile(largeImagePath): + raise TileSourceFileNotFoundError(largeImagePath) from None msg = "File %s didn't meet requirements for tile source: %s" % ( largeImagePath, lastException) config.getConfig('logger').debug(msg) - raise TileSourceException(msg) + raise TileSourceError(msg) # Sort the known directories by image area (width * height). Given # equal area, sort by the level. alldir.sort() @@ -128,7 +131,7 @@ def __init__(self, path, **kwargs): continue directories[level] = td if not len(directories) or (len(directories) < 2 and max(directories.keys()) + 1 > 4): - raise TileSourceException( + raise TileSourceError( 'Tiff image must have at least two levels.') # Sort the directories so that the highest resolution is the last one; @@ -221,7 +224,7 @@ def _levelFromIfd(self, ifd, baseifd): (tag not in ifd['tags'] and tag in baseifd['tags']) or (tag in ifd['tags'] and ifd['tags'][tag]['data'] != baseifd['tags'][tag]['data'])): - raise TileSourceException('IFD does not match first IFD.') + raise TileSourceError('IFD does not match first IFD.') sizes = [(self.sizeX, self.sizeY)] for level in range(self.levels - 1, -1, -1): if (sizeX, sizeY) in sizes: @@ -238,7 +241,7 @@ def _levelFromIfd(self, ifd, baseifd): if (w2, h2) not in altsizes: altsizes.append((w2, h2)) sizes = altsizes - raise TileSourceException('IFD size is not a power of two smaller than first IFD.') + raise TileSourceError('IFD size is not a power of two smaller than first IFD.') def _initWithTiffTools(self): # noqa """ @@ -287,18 +290,18 @@ def _initWithTiffTools(self): # noqa idx for idx, frame in enumerate(frames) if frame['dirs'][level] is None )]['dirs'][level] = (idx, 0) else: - raise TileSourceException('Tile layers are in a surprising order') + raise TileSourceError('Tile layers are in a surprising order') # if there are sub ifds, add them if tifftools.Tag.SubIfd.value in ifd['tags']: for subidx, subifds in enumerate(ifd['tags'][tifftools.Tag.SubIfd.value]['ifds']): if len(subifds) != 1: - raise TileSourceException( + raise TileSourceError( 'When stored in subifds, each subifd should be a single ifd.') level = self._levelFromIfd(subifds[0], info['ifds'][0]) if level < self.levels - 1 and frames[-1]['dirs'][level] is None: frames[-1]['dirs'][level] = (idx, subidx + 1) else: - raise TileSourceException('Tile layers are in a surprising order') + raise TileSourceError('Tile layers are in a surprising order') self._associatedImages = {} for dirNum in associated: self._addAssociatedImage(self._largeImagePath, dirNum) @@ -556,9 +559,9 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, applyStyle=allowStyle, **kwargs) except InvalidOperationTiffException as e: - raise TileSourceException(e.args[0]) + raise TileSourceError(e.args[0]) except IOTiffException as e: - return self.getTileIOTiffException( + return self.getTileIOTiffError( x, y, z, pilImageAllowed=pilImageAllowed, numpyAllowed=numpyAllowed, sparseFallback=sparseFallback, exception=e, **kwargs) @@ -580,9 +583,9 @@ def _getDirFromCache(self, dirnum, subdir=None): self._directoryCache[key] = result return result - def getTileIOTiffException(self, x, y, z, pilImageAllowed=False, - numpyAllowed=False, sparseFallback=False, - exception=None, **kwargs): + def getTileIOTiffError(self, x, y, z, pilImageAllowed=False, + numpyAllowed=False, sparseFallback=False, + exception=None, **kwargs): if sparseFallback: if z: noedge = kwargs.copy() @@ -604,7 +607,7 @@ def getTileIOTiffException(self, x, y, z, pilImageAllowed=False, image = PIL.Image.new('RGBA', (self.tileWidth, self.tileHeight)) return self._outputTile(image, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, applyStyle=False, **kwargs) - raise TileSourceException('Internal I/O failure: %s' % exception.args[0]) + raise TileSourceError('Internal I/O failure: %s' % exception.args[0]) def getTileFromEmptyDirectory(self, x, y, z, **kwargs): """ diff --git a/test/test_source_base.py b/test/test_source_base.py index aeb343c3c..545553460 100644 --- a/test/test_source_base.py +++ b/test/test_source_base.py @@ -1,5 +1,7 @@ import os +import pytest + import large_image from large_image.tilesource import nearPowerOfTwo @@ -23,3 +25,25 @@ def testCanRead(): imagePath = datastore.fetch('sample_image.ptif') assert large_image.canRead(imagePath) is True + + +@pytest.mark.parametrize('source', [ + 'bioformats', + 'deepzoom', + # 'dummy', # exclude - no files + 'gdal', + 'mapnik', + 'nd2', + 'ometiff', + 'openjpeg', + 'openslide', + 'pil', + # 'test', # exclude - no files + 'tiff', +]) +def testSourcesFileNotFound(source): + large_image.tilesource.loadTileSources() + with pytest.raises(large_image.exceptions.TileSourceFileNotFoundError): + large_image.tilesource.AvailableTileSources[source]('nosuchfile') + with pytest.raises(large_image.exceptions.TileSourceFileNotFoundError): + large_image.tilesource.AvailableTileSources[source]('nosuchfile.ext') diff --git a/test/test_source_gdal.py b/test/test_source_gdal.py index 4ea772982..bcdb4e532 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 TileSourceException +from large_image.exceptions import TileSourceError from . import utilities from .datastore import datastore @@ -106,7 +106,7 @@ def testTileLinearStyleFromGeotiffs(): def testTileStyleBadInput(): def _assertStyleResponse(imagePath, style, message): - with pytest.raises(TileSourceException, match=message): + with pytest.raises(TileSourceError, match=message): source = large_image_source_gdal.open( imagePath, projection='EPSG:3857', style=json.dumps(style), encoding='PNG') source.getTile(22, 51, 7) @@ -200,13 +200,13 @@ def testPixel(): def testSourceErrors(): testDir = os.path.dirname(os.path.realpath(__file__)) imagePath = os.path.join(testDir, 'test_files', 'rgb_geotiff.tiff') - with pytest.raises(TileSourceException, match='must not be geographic'): + with pytest.raises(TileSourceError, match='must not be geographic'): large_image_source_gdal.open(imagePath, 'EPSG:4326') imagePath = os.path.join(testDir, 'test_files', 'zero_gi.tif') - with pytest.raises(TileSourceException, match='cannot be opened via'): + with pytest.raises(TileSourceError, match='cannot be opened via'): large_image_source_gdal.open(imagePath) imagePath = os.path.join(testDir, 'test_files', 'yb10kx5k.png') - with pytest.raises(TileSourceException, match='does not have a projected scale'): + with pytest.raises(TileSourceError, match='does not have a projected scale'): large_image_source_gdal.open(imagePath) @@ -215,7 +215,7 @@ def testStereographicProjection(): imagePath = os.path.join(testDir, 'test_files', 'rgb_geotiff.tiff') # We will fail if we ask for a stereographic projection and don't # specify unitsPerPixel - with pytest.raises(TileSourceException, match='unitsPerPixel must be specified'): + with pytest.raises(TileSourceError, match='unitsPerPixel must be specified'): large_image_source_gdal.open(imagePath, 'EPSG:3411') # But will pass if unitsPerPixel is specified large_image_source_gdal.open(imagePath, 'EPSG:3411', unitsPerPixel=150000) @@ -265,7 +265,7 @@ def testConvertProjectionUnits(): assert result[1] == pytest.approx(149, 1) assert result[2:] == (None, None, 'base_pixels') - with pytest.raises(TileSourceException, match='Cannot convert'): + with pytest.raises(TileSourceError, match='Cannot convert'): tsNoProj._convertProjectionUnits( -117.5, None, -117, None, None, None, 'EPSG:4326') diff --git a/test/test_source_mapnik.py b/test/test_source_mapnik.py index 495983fb5..326c348d0 100644 --- a/test/test_source_mapnik.py +++ b/test/test_source_mapnik.py @@ -9,7 +9,7 @@ import pytest import large_image -from large_image.exceptions import TileSourceException +from large_image.exceptions import TileSourceError from . import utilities from .datastore import datastore @@ -117,7 +117,7 @@ def testTileLinearStyleFromGeotiffs(): def testTileStyleBadInput(): def _assertStyleResponse(imagePath, style, message): - with pytest.raises(TileSourceException, match=message): + with pytest.raises(TileSourceError, match=message): source = large_image_source_mapnik.open( imagePath, projection='EPSG:3857', style=json.dumps(style)) source.getTile(22, 51, 7, encoding='PNG') @@ -245,13 +245,13 @@ def testPixel(): def testSourceErrors(): testDir = os.path.dirname(os.path.realpath(__file__)) imagePath = os.path.join(testDir, 'test_files', 'rgb_geotiff.tiff') - with pytest.raises(TileSourceException, match='must not be geographic'): + with pytest.raises(TileSourceError, match='must not be geographic'): large_image_source_mapnik.open(imagePath, 'EPSG:4326') imagePath = os.path.join(testDir, 'test_files', 'zero_gi.tif') - with pytest.raises(TileSourceException, match='cannot be opened via'): + with pytest.raises(TileSourceError, match='cannot be opened via'): large_image_source_mapnik.open(imagePath) imagePath = os.path.join(testDir, 'test_files', 'yb10kx5k.png') - with pytest.raises(TileSourceException, match='does not have a projected scale'): + with pytest.raises(TileSourceError, match='does not have a projected scale'): large_image_source_mapnik.open(imagePath) @@ -260,7 +260,7 @@ def testStereographicProjection(): imagePath = os.path.join(testDir, 'test_files', 'rgb_geotiff.tiff') # We will fail if we ask for a stereographic projection and don't # specify unitsPerPixel - with pytest.raises(TileSourceException, match='unitsPerPixel must be specified'): + with pytest.raises(TileSourceError, match='unitsPerPixel must be specified'): large_image_source_mapnik.open(imagePath, 'EPSG:3411') # But will pass if unitsPerPixel is specified large_image_source_mapnik.open(imagePath, 'EPSG:3411', unitsPerPixel=150000) @@ -310,7 +310,7 @@ def testConvertProjectionUnits(): assert result[1] == pytest.approx(149, 1) assert result[2:] == (None, None, 'base_pixels') - with pytest.raises(TileSourceException, match='Cannot convert'): + with pytest.raises(TileSourceError, match='Cannot convert'): tsNoProj._convertProjectionUnits( -117.5, None, -117, None, None, None, 'EPSG:4326')