2424from argparse import ArgumentParser
2525import base64
2626from collections import OrderedDict
27+ from collections .abc import Callable
2728from io import BytesIO
2829import os
2930from threading import Lock
31+ from typing import TYPE_CHECKING , Any , Literal
3032import zlib
3133
32- from PIL import ImageCms
33- from flask import Flask , abort , make_response , render_template , url_for
34+ from PIL import Image , ImageCms
35+ from flask import Flask , Response , abort , make_response , render_template , url_for
36+
37+ if TYPE_CHECKING :
38+ # Python 3.10+
39+ from typing import TypeAlias
3440
3541if os .name == 'nt' :
3642 _dll_path = os .getenv ('OPENSLIDE_PATH' )
3743 if _dll_path is not None :
38- with os .add_dll_directory (_dll_path ):
44+ with os .add_dll_directory (_dll_path ): # type: ignore[attr-defined]
3945 import openslide
4046 else :
4147 import openslide
6268)
6369SRGB_PROFILE = ImageCms .getOpenProfile (BytesIO (SRGB_PROFILE_BYTES ))
6470
71+ if TYPE_CHECKING :
72+ ColorMode : TypeAlias = Literal [
73+ 'default' ,
74+ 'absolute-colorimetric' ,
75+ 'perceptual' ,
76+ 'relative-colorimetric' ,
77+ 'saturation' ,
78+ 'embed' ,
79+ 'ignore' ,
80+ ]
81+ Transform : TypeAlias = Callable [[Image .Image ], None ]
82+
83+
84+ class DeepZoomMultiServer (Flask ):
85+ basedir : str
86+ cache : _SlideCache
87+
88+
89+ class AnnotatedDeepZoomGenerator (DeepZoomGenerator ):
90+ filename : str
91+ mpp : float
92+ transform : Transform
93+
6594
66- def create_app (config = None , config_file = None ):
95+ def create_app (
96+ config : dict [str , Any ] | None = None ,
97+ config_file : str | None = None ,
98+ ) -> Flask :
6799 # Create and configure app
68- app = Flask (__name__ )
100+ app = DeepZoomMultiServer (__name__ )
69101 app .config .from_mapping (
70102 SLIDE_DIR = '.' ,
71103 SLIDE_CACHE_SIZE = 10 ,
@@ -99,7 +131,7 @@ def create_app(config=None, config_file=None):
99131 )
100132
101133 # Helper functions
102- def get_slide (path ) :
134+ def get_slide (path : str ) -> AnnotatedDeepZoomGenerator :
103135 path = os .path .abspath (os .path .join (app .basedir , path ))
104136 if not path .startswith (app .basedir + os .path .sep ):
105137 # Directory traversal
@@ -115,11 +147,11 @@ def get_slide(path):
115147
116148 # Set up routes
117149 @app .route ('/' )
118- def index ():
150+ def index () -> str :
119151 return render_template ('files.html' , root_dir = _Directory (app .basedir ))
120152
121153 @app .route ('/<path:path>' )
122- def slide (path ) :
154+ def slide (path : str ) -> str :
123155 slide = get_slide (path )
124156 slide_url = url_for ('dzi' , path = path )
125157 return render_template (
@@ -130,15 +162,15 @@ def slide(path):
130162 )
131163
132164 @app .route ('/<path:path>.dzi' )
133- def dzi (path ) :
165+ def dzi (path : str ) -> Response :
134166 slide = get_slide (path )
135167 format = app .config ['DEEPZOOM_FORMAT' ]
136168 resp = make_response (slide .get_dzi (format ))
137169 resp .mimetype = 'application/xml'
138170 return resp
139171
140172 @app .route ('/<path:path>_files/<int:level>/<int:col>_<int:row>.<format>' )
141- def tile (path , level , col , row , format ) :
173+ def tile (path : str , level : int , col : int , row : int , format : str ) -> Response :
142174 slide = get_slide (path )
143175 format = format .lower ()
144176 if format != 'jpeg' and format != 'png' :
@@ -165,19 +197,27 @@ def tile(path, level, col, row, format):
165197
166198
167199class _SlideCache :
168- def __init__ (self , cache_size , tile_cache_mb , dz_opts , color_mode ):
200+ def __init__ (
201+ self ,
202+ cache_size : int ,
203+ tile_cache_mb : int ,
204+ dz_opts : dict [str , Any ],
205+ color_mode : ColorMode ,
206+ ):
169207 self .cache_size = cache_size
170208 self .dz_opts = dz_opts
171209 self .color_mode = color_mode
172210 self ._lock = Lock ()
173- self ._cache = OrderedDict ()
211+ self ._cache : OrderedDict [ str , AnnotatedDeepZoomGenerator ] = OrderedDict ()
174212 # Share a single tile cache among all slide handles, if supported
175213 try :
176- self ._tile_cache = OpenSlideCache (tile_cache_mb * 1024 * 1024 )
214+ self ._tile_cache : OpenSlideCache | None = OpenSlideCache (
215+ tile_cache_mb * 1024 * 1024
216+ )
177217 except OpenSlideVersionError :
178218 self ._tile_cache = None
179219
180- def get (self , path ) :
220+ def get (self , path : str ) -> AnnotatedDeepZoomGenerator :
181221 with self ._lock :
182222 if path in self ._cache :
183223 # Move to end of LRU
@@ -188,7 +228,7 @@ def get(self, path):
188228 osr = OpenSlide (path )
189229 if self ._tile_cache is not None :
190230 osr .set_cache (self ._tile_cache )
191- slide = DeepZoomGenerator (osr , ** self .dz_opts )
231+ slide = AnnotatedDeepZoomGenerator (osr , ** self .dz_opts )
192232 try :
193233 mpp_x = osr .properties [openslide .PROPERTY_NAME_MPP_X ]
194234 mpp_y = osr .properties [openslide .PROPERTY_NAME_MPP_Y ]
@@ -204,7 +244,7 @@ def get(self, path):
204244 self ._cache [path ] = slide
205245 return slide
206246
207- def _get_transform (self , image ) :
247+ def _get_transform (self , image : OpenSlide ) -> Transform :
208248 if image .color_profile is None :
209249 return lambda img : None
210250 mode = self .color_mode
@@ -215,7 +255,7 @@ def _get_transform(self, image):
215255 # embed ICC profile in tiles
216256 return lambda img : None
217257 elif mode == 'default' :
218- intent = ImageCms .getDefaultIntent (image .color_profile )
258+ intent = ImageCms .Intent ( ImageCms . getDefaultIntent (image .color_profile ) )
219259 elif mode == 'absolute-colorimetric' :
220260 intent = ImageCms .Intent .ABSOLUTE_COLORIMETRIC
221261 elif mode == 'relative-colorimetric' :
@@ -232,10 +272,10 @@ def _get_transform(self, image):
232272 'RGB' ,
233273 'RGB' ,
234274 intent ,
235- 0 ,
275+ ImageCms . Flags ( 0 ) ,
236276 )
237277
238- def xfrm (img ) :
278+ def xfrm (img : Image . Image ) -> None :
239279 ImageCms .applyTransform (img , transform , True )
240280 # Some browsers assume we intend the display's color space if we
241281 # don't embed the profile. Pillow's serialization is larger, so
@@ -246,9 +286,9 @@ def xfrm(img):
246286
247287
248288class _Directory :
249- def __init__ (self , basedir , relpath = '' ):
289+ def __init__ (self , basedir : str , relpath : str = '' ):
250290 self .name = os .path .basename (relpath )
251- self .children = []
291+ self .children : list [ _Directory | _SlideFile ] = []
252292 for name in sorted (os .listdir (os .path .join (basedir , relpath ))):
253293 cur_relpath = os .path .join (relpath , name )
254294 cur_path = os .path .join (basedir , cur_relpath )
@@ -261,7 +301,7 @@ def __init__(self, basedir, relpath=''):
261301
262302
263303class _SlideFile :
264- def __init__ (self , relpath ):
304+ def __init__ (self , relpath : str ):
265305 self .name = os .path .basename (relpath )
266306 self .url_path = relpath
267307
0 commit comments