large-image 1.27.1.dev40__tar.gz → 1.27.1.dev44__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {large-image-1.27.1.dev40/large_image.egg-info → large-image-1.27.1.dev44}/PKG-INFO +1 -1
  2. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/tilesource/base.py +24 -19
  3. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/tilesource/geo.py +57 -38
  4. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/tilesource/jupyter.py +177 -159
  5. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/tilesource/stylefuncs.py +25 -14
  6. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/tilesource/tiledict.py +14 -10
  7. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/tilesource/utilities.py +126 -91
  8. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44/large_image.egg-info}/PKG-INFO +1 -1
  9. large-image-1.27.1.dev44/large_image.egg-info/requires.txt +145 -0
  10. large-image-1.27.1.dev40/large_image.egg-info/requires.txt +0 -145
  11. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/LICENSE +0 -0
  12. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/NOTICE +0 -0
  13. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/README.rst +0 -0
  14. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/annotations.rst +0 -0
  15. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/caching.rst +0 -0
  16. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/conf.py +0 -0
  17. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/config_options.rst +0 -0
  18. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/development.rst +0 -0
  19. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/example_usage.rst +0 -0
  20. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/girder_annotation_config_options.rst +0 -0
  21. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/girder_config_options.rst +0 -0
  22. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/image_conversion.rst +0 -0
  23. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/index.rst +0 -0
  24. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/large_image_examples.ipynb +0 -0
  25. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/make_docs.sh +0 -0
  26. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/multi_source_specification.rst +0 -0
  27. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/notebooks.rst +0 -0
  28. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/static/custom.css +0 -0
  29. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/tilesource_options.rst +0 -0
  30. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/docs/upgrade.rst +0 -0
  31. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/__init__.py +0 -0
  32. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/cache_util/__init__.py +0 -0
  33. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/cache_util/base.py +0 -0
  34. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/cache_util/cache.py +0 -0
  35. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/cache_util/cachefactory.py +0 -0
  36. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/cache_util/memcache.py +0 -0
  37. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/config.py +0 -0
  38. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/constants.py +0 -0
  39. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/exceptions.py +0 -0
  40. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image/tilesource/__init__.py +0 -0
  41. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image.egg-info/SOURCES.txt +0 -0
  42. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image.egg-info/dependency_links.txt +0 -0
  43. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image.egg-info/not-zip-safe +0 -0
  44. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/large_image.egg-info/top_level.txt +0 -0
  45. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/setup.cfg +0 -0
  46. {large-image-1.27.1.dev40 → large-image-1.27.1.dev44}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image
3
- Version: 1.27.1.dev40
3
+ Version: 1.27.1.dev44
4
4
  Summary: Python modules to work with large, multiresolution images.
5
5
  Home-page: https://github.com/girder/large_image
6
6
  Author: Kitware, Inc.
@@ -56,8 +56,6 @@ class TileSource(IPyLeafletMixin):
56
56
  nameMatches: Dict[str, SourcePriority] = {
57
57
  }
58
58
 
59
- geospatial = False
60
-
61
59
  # When getting tiles for otherwise empty levels (missing powers of two), we
62
60
  # composite the tile from higher resolution levels. This can use excessive
63
61
  # memory if there are too many missing levels. For instance, if there are
@@ -203,6 +201,10 @@ class TileSource(IPyLeafletMixin):
203
201
  def _repr_png_(self):
204
202
  return self.getThumbnail(encoding='PNG')[0]
205
203
 
204
+ @property
205
+ def geospatial(self) -> bool:
206
+ return False
207
+
206
208
  def _setStyle(self, style: Any) -> None:
207
209
  """
208
210
  Check and set the specified style from a json string or a dictionary.
@@ -842,8 +844,9 @@ class TileSource(IPyLeafletMixin):
842
844
 
843
845
  self.logger.debug(
844
846
  'Fetching region of an image with a source size of %d x %d; '
845
- 'getting %d tiles',
846
- regionWidth, regionHeight, (xmax - xmin) * (ymax - ymin))
847
+ 'getting %d tile%s',
848
+ regionWidth, regionHeight, (xmax - xmin) * (ymax - ymin),
849
+ '' if (xmax - xmin) * (ymax - ymin) == 1 else 's')
847
850
 
848
851
  # If tile is specified, return at most one tile
849
852
  if iterInfo.get('tile_position') is not None:
@@ -1604,7 +1607,7 @@ class TileSource(IPyLeafletMixin):
1604
1607
  tile = self._applyStyle(tile, getattr(self, 'style', None), x, y, z, frame)
1605
1608
  if tile.shape[0] != self.tileHeight or tile.shape[1] != self.tileWidth:
1606
1609
  extend = np.zeros(
1607
- (self.tileHeight, self.tileWidth, tile.shape[2]),
1610
+ (self.tileHeight, self.tileWidth, tile.shape[2]), # type: ignore[misc]
1608
1611
  dtype=tile.dtype)
1609
1612
  extend[:min(self.tileHeight, tile.shape[0]),
1610
1613
  :min(self.tileWidth, tile.shape[1])] = tile
@@ -1672,10 +1675,10 @@ class TileSource(IPyLeafletMixin):
1672
1675
  sizeY - (maxY - self.tileHeight))
1673
1676
  tile, mode = _imageToNumpy(tile)
1674
1677
  if self.edge in (True, 'crop'):
1675
- tile = cast(np.ndarray, tile)[:contentHeight, :contentWidth]
1678
+ tile = tile[:contentHeight, :contentWidth]
1676
1679
  else:
1677
- color = PIL.ImageColor.getcolor(self.edge, cast(str, mode))
1678
- tile = cast(np.ndarray, tile).copy()
1680
+ color = PIL.ImageColor.getcolor(self.edge, mode)
1681
+ tile = tile.copy()
1679
1682
  tile[:, contentWidth:] = color
1680
1683
  tile[contentHeight:] = color
1681
1684
  if isinstance(tile, np.ndarray) and numpyAllowed:
@@ -2207,7 +2210,7 @@ class TileSource(IPyLeafletMixin):
2207
2210
  mode = None if TILE_FORMAT_NUMPY in format else iterInfo['mode']
2208
2211
  outWidth = iterInfo['output']['width']
2209
2212
  outHeight = iterInfo['output']['height']
2210
- image: Optional[np.ndarray] = None
2213
+ image: Optional[Union[np.ndarray, PIL.Image.Image, ImageBytes, bytes]] = None
2211
2214
  tiledimage = None
2212
2215
  for tile in self._tileIterator(iterInfo):
2213
2216
  # Add each tile to the image
@@ -2218,7 +2221,7 @@ class TileSource(IPyLeafletMixin):
2218
2221
  tiledimage, subimage, x0, y0, regionWidth, regionHeight, tile, **kwargs)
2219
2222
  else:
2220
2223
  image = utilities._addSubimageToImage(
2221
- image, subimage, x0, y0, regionWidth, regionHeight)
2224
+ cast(Optional[np.ndarray], image), subimage, x0, y0, regionWidth, regionHeight)
2222
2225
  # Somehow discarding the tile here speeds things up.
2223
2226
  del tile
2224
2227
  del subimage
@@ -2230,7 +2233,7 @@ class TileSource(IPyLeafletMixin):
2230
2233
  cast(Dict[str, Any], tiledimage), outWidth, outHeight, iterInfo, **kwargs)
2231
2234
  if outWidth != regionWidth or outHeight != regionHeight:
2232
2235
  dtype = cast(np.ndarray, image).dtype
2233
- image = _imageToPIL(image, mode).resize(
2236
+ image = _imageToPIL(cast(np.ndarray, image), mode).resize(
2234
2237
  (outWidth, outHeight),
2235
2238
  getattr(PIL.Image, 'Resampling', PIL.Image).BICUBIC
2236
2239
  if outWidth > regionWidth else
@@ -2241,8 +2244,8 @@ class TileSource(IPyLeafletMixin):
2241
2244
  maxHeight = kwargs.get('output', {}).get('maxHeight')
2242
2245
  if kwargs.get('fill') and maxWidth and maxHeight:
2243
2246
  image = utilities._letterboxImage(
2244
- _imageToPIL(image, mode), maxWidth, maxHeight, kwargs['fill'])
2245
- return utilities._encodeImage(image, format=format, **kwargs)
2247
+ _imageToPIL(cast(np.ndarray, image), mode), maxWidth, maxHeight, kwargs['fill'])
2248
+ return utilities._encodeImage(cast(np.ndarray, image), format=format, **kwargs)
2246
2249
 
2247
2250
  def _encodeTiledImage(
2248
2251
  self, image: Dict[str, Any], outWidth: int, outHeight: int,
@@ -2435,14 +2438,16 @@ class TileSource(IPyLeafletMixin):
2435
2438
  frame, idx, len(frameList), offsetX, offsetY)
2436
2439
  if tiled:
2437
2440
  tiledimage = utilities._addRegionTileToTiled(
2438
- tiledimage, subimage, offsetX, offsetY, outWidth, outHeight, tile, **kwargs)
2439
- else:
2440
- image = utilities._addSubimageToImage(
2441
- image, subimage, offsetX, offsetY, outWidth, outHeight)
2441
+ tiledimage, cast(np.ndarray, subimage), offsetX,
2442
+ offsetY, outWidth, outHeight, tile, **kwargs)
2443
+ else:
2444
+ image = utilities._addSubimageToImage(
2445
+ image, cast(np.ndarray, subimage), offsetX, offsetY,
2446
+ outWidth, outHeight)
2442
2447
  if tiled:
2443
2448
  return self._encodeTiledImage(
2444
2449
  cast(Dict[str, Any], tiledimage), outWidth, outHeight, iterInfo, **kwargs)
2445
- return utilities._encodeImage(image, format=format, **kwargs)
2450
+ return utilities._encodeImage(cast(np.ndarray, image), format=format, **kwargs)
2446
2451
 
2447
2452
  def getRegionAtAnotherScale(
2448
2453
  self, sourceRegion: Dict[str, Any],
@@ -2851,7 +2856,7 @@ class TileSource(IPyLeafletMixin):
2851
2856
  getattr(PIL.Image, 'Resampling', PIL.Image).BICUBIC
2852
2857
  if width > imageWidth else
2853
2858
  getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS)
2854
- return utilities._encodeImage(image, **kwargs)
2859
+ return cast(Tuple[ImageBytes, str], utilities._encodeImage(image, **kwargs))
2855
2860
 
2856
2861
  def getPixel(self, includeTileRecord: bool = False, **kwargs) -> JSONDict:
2857
2862
  """
@@ -1,11 +1,15 @@
1
+ import pathlib
2
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
1
3
  from urllib.parse import urlencode, urlparse
2
4
 
3
- from large_image.cache_util import CacheProperties, methodcache
4
- from large_image.constants import SourcePriority, TileInputUnits
5
- from large_image.exceptions import TileSourceError
5
+ import numpy as np
6
+ import PIL.Image
6
7
 
8
+ from ..cache_util import CacheProperties, methodcache
9
+ from ..constants import SourcePriority, TileInputUnits
10
+ from ..exceptions import TileSourceError
7
11
  from .base import FileTileSource
8
- from .utilities import JSONDict, getPaletteColors
12
+ from .utilities import ImageBytes, JSONDict, getPaletteColors
9
13
 
10
14
  try:
11
15
  import pyproj
@@ -20,16 +24,16 @@ CacheProperties['tilesource']['itemExpectedSize'] = max(
20
24
  100 * 1024 ** 2)
21
25
 
22
26
  # Used to cache pixel size for projections
23
- ProjUnitsAcrossLevel0 = {}
27
+ ProjUnitsAcrossLevel0: Dict[str, float] = {}
24
28
  ProjUnitsAcrossLevel0_MaxSize = 100
25
29
 
26
30
  InitPrefix = ''
27
31
  NeededInitPrefix = '+init=' if has_pyproj and _pyproj_under_6 else InitPrefix
28
32
 
29
33
 
30
- def make_vsi(url: str, **options):
34
+ def make_vsi(url: Union[str, pathlib.Path, Dict[Any, Any]], **options) -> str:
31
35
  if str(url).startswith('s3://'):
32
- s3_path = url.replace('s3://', '')
36
+ s3_path = str(url).replace('s3://', '')
33
37
  vsi = f'/vsis3/{s3_path}'
34
38
  else:
35
39
  gdal_options = {
@@ -74,7 +78,14 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
74
78
  'image/x-tiff': SourcePriority.LOW,
75
79
  }
76
80
 
77
- def _getDriver(self):
81
+ projection: Union[str, bytes]
82
+ projectionOrigin: Tuple[float, float]
83
+ sourceLevels: int
84
+ sourceSizeX: int
85
+ sourceSizeY: int
86
+ unitsAcrossLevel0: float
87
+
88
+ def _getDriver(self) -> str:
78
89
  """
79
90
  Get the GDAL driver used to read this dataset.
80
91
 
@@ -82,20 +93,20 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
82
93
  """
83
94
  raise NotImplementedError
84
95
 
85
- def _convertProjectionUnits(self, *args, **kwargs):
96
+ def _convertProjectionUnits(self, *args, **kwargs) -> Tuple[float, float, float, float, str]:
86
97
  raise NotImplementedError
87
98
 
88
- def pixelToProjection(self, *args, **kwargs):
99
+ def pixelToProjection(self, *args, **kwargs) -> Tuple[float, float]:
89
100
  raise NotImplementedError
90
101
 
91
- def toNativePixelCoordinates(self, *args, **kwargs):
102
+ def toNativePixelCoordinates(self, *args, **kwargs) -> Tuple[float, float]:
92
103
  raise NotImplementedError
93
104
 
94
- def getBounds(self, *args, **kwargs):
105
+ def getBounds(self, *args, **kwargs) -> Dict[str, Any]:
95
106
  raise NotImplementedError
96
107
 
97
108
  @staticmethod
98
- def isGeospatial(path):
109
+ def isGeospatial(path: Union[str, pathlib.Path]) -> bool:
99
110
  """
100
111
  Check if a path is likely to be a geospatial file.
101
112
 
@@ -105,13 +116,13 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
105
116
  raise NotImplementedError
106
117
 
107
118
  @property
108
- def geospatial(self):
119
+ def geospatial(self) -> bool:
109
120
  """
110
121
  This is true if the source has geospatial information.
111
122
  """
112
123
  return bool(self.projection)
113
124
 
114
- def _getLargeImagePath(self):
125
+ def _getLargeImagePath(self) -> str:
115
126
  """Get GDAL-compatible image path.
116
127
 
117
128
  This will cast the output to a string and can also handle URLs
@@ -122,7 +133,7 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
122
133
  return make_vsi(self.largeImagePath)
123
134
  return str(self.largeImagePath)
124
135
 
125
- def _setStyle(self, style):
136
+ def _setStyle(self, style: Any) -> None:
126
137
  """
127
138
  Check and set the specified style from a json string or a dictionary.
128
139
 
@@ -132,7 +143,7 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
132
143
  if hasattr(self, '_getTileLock'):
133
144
  self._setDefaultStyle()
134
145
 
135
- def _styleBands(self):
146
+ def _styleBands(self) -> List[Dict[str, Any]]:
136
147
  interpColorTable = {
137
148
  'red': ['#000000', '#ff0000'],
138
149
  'green': ['#000000', '#00ff00'],
@@ -178,7 +189,7 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
178
189
  })
179
190
  return style
180
191
 
181
- def _setDefaultStyle(self):
192
+ def _setDefaultStyle(self) -> None:
182
193
  """If no style was specified, create a default style."""
183
194
  self._bandNames = {}
184
195
  for idx, band in self.getBandInformation().items():
@@ -224,16 +235,16 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
224
235
  self._style = JSONDict({'bands': style})
225
236
 
226
237
  @staticmethod
227
- def getHexColors(palette):
238
+ def getHexColors(palette: Union[str, List[Union[str, float, Tuple[float, ...]]]]) -> List[str]:
228
239
  """
229
240
  Returns list of hex colors for a given color palette
230
241
 
231
242
  :returns: List of colors
232
243
  """
233
- palette = getPaletteColors(palette)
234
- return ['#%02X%02X%02X%02X' % tuple(int(val) for val in clr) for clr in palette]
244
+ pal = getPaletteColors(palette)
245
+ return ['#%02X%02X%02X%02X' % tuple(int(val) for val in clr) for clr in pal]
235
246
 
236
- def getPixelSizeInMeters(self):
247
+ def getPixelSizeInMeters(self) -> Optional[float]:
237
248
  """
238
249
  Get the approximate base pixel size in meters. This is calculated as
239
250
  the average scale of the four edges in the WGS84 ellipsoid.
@@ -242,13 +253,13 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
242
253
  """
243
254
  bounds = self.getBounds(NeededInitPrefix + 'epsg:4326')
244
255
  if not bounds:
245
- return
256
+ return None
246
257
  if has_pyproj:
247
258
  geod = pyproj.Geod(ellps='WGS84')
248
- computer = geod.inv
259
+ computer = cast(Callable[[Any, Any, Any, Any], Tuple[Any, Any, float]], geod.inv)
249
260
  else:
250
261
  # Estimate based on great-cirlce distance
251
- def computer(lon1, lat1, lon2, lat2):
262
+ def computer(lon1, lat1, lon2, lat2) -> Tuple[Any, Any, float]:
252
263
  from math import acos, cos, radians, sin
253
264
  lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
254
265
  return None, None, 6.378e+6 * (
@@ -264,7 +275,7 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
264
275
  bounds['ul']['x'], bounds['ul']['y'])
265
276
  return (s1 + s2 + s3 + s4) / (self.sourceSizeX * 2 + self.sourceSizeY * 2)
266
277
 
267
- def getNativeMagnification(self):
278
+ def getNativeMagnification(self) -> Dict[str, Optional[float]]:
268
279
  """
269
280
  Get the magnification at the base level.
270
281
 
@@ -277,7 +288,7 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
277
288
  'mm_y': scale * 100 if scale else None,
278
289
  }
279
290
 
280
- def getTileCorners(self, z, x, y):
291
+ def getTileCorners(self, z: int, x: float, y: float) -> Tuple[float, float, float, float]:
281
292
  """
282
293
  Returns bounds of a tile for a given x,y,z index.
283
294
 
@@ -307,7 +318,7 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
307
318
  ymin, ymax = self.sourceSizeY - ymax, self.sourceSizeY - ymin
308
319
  return xmin, ymin, xmax, ymax
309
320
 
310
- def _bandNumber(self, band, exc=True):
321
+ def _bandNumber(self, band: Optional[Union[str, int]], exc: bool = True) -> Optional[int]:
311
322
  """Given a band number or interpretation name, return a validated band number.
312
323
 
313
324
  :param band: either -1, a positive integer, or the name of a band interpretation
@@ -339,9 +350,13 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
339
350
 
340
351
  return band
341
352
 
342
- def _getRegionBounds(self, metadata, left=None, top=None, right=None,
343
- bottom=None, width=None, height=None, units=None,
344
- **kwargs):
353
+ def _getRegionBounds(
354
+ self, metadata: JSONDict, left: Optional[float] = None,
355
+ top: Optional[float] = None, right: Optional[float] = None,
356
+ bottom: Optional[float] = None, width: Optional[float] = None,
357
+ height: Optional[float] = None, units: Optional[str] = None,
358
+ desiredMagnification: Optional[Dict[str, Optional[float]]] = None,
359
+ cropToImage: bool = True, **kwargs) -> Tuple[float, float, float, float]:
345
360
  """
346
361
  Given a set of arguments that can include left, right, top, bottom,
347
362
  width, height, and units, generate actual pixel values for left, top,
@@ -385,7 +400,7 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
385
400
  if top is None:
386
401
  top = bounds['ymax'] if bottom is None or width is None else bottom - width
387
402
  if bottom is None:
388
- bottom = bounds['ymin'] if width is None else top + height
403
+ bottom = bounds['ymin'] if width is None else cast(float, top) + cast(float, height)
389
404
  if not kwargs.get('unitsWH') or kwargs.get('unitsWH') == units:
390
405
  width = height = None
391
406
  # Convert to [-0.5, 0.5], [-0.5, 0.5] coordinate range
@@ -401,14 +416,18 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
401
416
  top = max(0, min(yScale, (0.5 - top) * yScale))
402
417
  bottom = max(0, min(yScale, (0.5 - bottom) * yScale))
403
418
  # Ensure correct ordering
404
- left, right = min(left, right), max(left, right)
405
- top, bottom = min(top, bottom), max(top, bottom)
419
+ left, right = min(cast(float, left), cast(float, right)), max(left, cast(float, right))
420
+ top, bottom = min(cast(float, top), cast(float, bottom)), max(top, cast(float, bottom))
406
421
  units = 'base_pixels'
407
422
  return super()._getRegionBounds(
408
- metadata, left, top, right, bottom, width, height, units, **kwargs)
423
+ metadata, left, top, right, bottom, width, height, units,
424
+ desiredMagnification, cropToImage, **kwargs)
409
425
 
410
426
  @methodcache()
411
- def getThumbnail(self, width=None, height=None, **kwargs):
427
+ def getThumbnail(
428
+ self, width: Optional[Union[str, int]] = None,
429
+ height: Optional[Union[str, int]] = None, **kwargs) -> Tuple[
430
+ Union[np.ndarray, PIL.Image.Image, ImageBytes, bytes, pathlib.Path], str]:
412
431
  """
413
432
  Get a basic thumbnail from the current tile source. Aspect ratio is
414
433
  preserved. If neither width nor height is given, a default value is
@@ -424,8 +443,8 @@ class GDALBaseFileTileSource(GeoBaseFileTileSource):
424
443
  :returns: thumbData, thumbMime: the image data and the mime type.
425
444
  """
426
445
  if self.projection:
427
- if ((width is not None and width < 2) or
428
- (height is not None and height < 2)):
446
+ if ((width is not None and float(width) < 2) or
447
+ (height is not None and float(height) < 2)):
429
448
  msg = 'Invalid width or height. Minimum value is 2.'
430
449
  raise ValueError(msg)
431
450
  if width is None and height is None: