24
24
from argparse import ArgumentParser
25
25
import base64
26
26
from collections import OrderedDict
27
+ from collections .abc import Callable
27
28
from io import BytesIO
28
29
import os
29
30
from threading import Lock
31
+ from typing import TYPE_CHECKING , Any , Literal
30
32
import zlib
31
33
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
34
40
35
41
if os .name == 'nt' :
36
42
_dll_path = os .getenv ('OPENSLIDE_PATH' )
37
43
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]
39
45
import openslide
40
46
else :
41
47
import openslide
62
68
)
63
69
SRGB_PROFILE = ImageCms .getOpenProfile (BytesIO (SRGB_PROFILE_BYTES ))
64
70
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
+
65
94
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 :
67
99
# Create and configure app
68
- app = Flask (__name__ )
100
+ app = DeepZoomMultiServer (__name__ )
69
101
app .config .from_mapping (
70
102
SLIDE_DIR = '.' ,
71
103
SLIDE_CACHE_SIZE = 10 ,
@@ -99,7 +131,7 @@ def create_app(config=None, config_file=None):
99
131
)
100
132
101
133
# Helper functions
102
- def get_slide (path ) :
134
+ def get_slide (path : str ) -> AnnotatedDeepZoomGenerator :
103
135
path = os .path .abspath (os .path .join (app .basedir , path ))
104
136
if not path .startswith (app .basedir + os .path .sep ):
105
137
# Directory traversal
@@ -115,11 +147,11 @@ def get_slide(path):
115
147
116
148
# Set up routes
117
149
@app .route ('/' )
118
- def index ():
150
+ def index () -> str :
119
151
return render_template ('files.html' , root_dir = _Directory (app .basedir ))
120
152
121
153
@app .route ('/<path:path>' )
122
- def slide (path ) :
154
+ def slide (path : str ) -> str :
123
155
slide = get_slide (path )
124
156
slide_url = url_for ('dzi' , path = path )
125
157
return render_template (
@@ -130,15 +162,15 @@ def slide(path):
130
162
)
131
163
132
164
@app .route ('/<path:path>.dzi' )
133
- def dzi (path ) :
165
+ def dzi (path : str ) -> Response :
134
166
slide = get_slide (path )
135
167
format = app .config ['DEEPZOOM_FORMAT' ]
136
168
resp = make_response (slide .get_dzi (format ))
137
169
resp .mimetype = 'application/xml'
138
170
return resp
139
171
140
172
@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 :
142
174
slide = get_slide (path )
143
175
format = format .lower ()
144
176
if format != 'jpeg' and format != 'png' :
@@ -165,19 +197,27 @@ def tile(path, level, col, row, format):
165
197
166
198
167
199
class _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
+ ):
169
207
self .cache_size = cache_size
170
208
self .dz_opts = dz_opts
171
209
self .color_mode = color_mode
172
210
self ._lock = Lock ()
173
- self ._cache = OrderedDict ()
211
+ self ._cache : OrderedDict [ str , AnnotatedDeepZoomGenerator ] = OrderedDict ()
174
212
# Share a single tile cache among all slide handles, if supported
175
213
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
+ )
177
217
except OpenSlideVersionError :
178
218
self ._tile_cache = None
179
219
180
- def get (self , path ) :
220
+ def get (self , path : str ) -> AnnotatedDeepZoomGenerator :
181
221
with self ._lock :
182
222
if path in self ._cache :
183
223
# Move to end of LRU
@@ -188,7 +228,7 @@ def get(self, path):
188
228
osr = OpenSlide (path )
189
229
if self ._tile_cache is not None :
190
230
osr .set_cache (self ._tile_cache )
191
- slide = DeepZoomGenerator (osr , ** self .dz_opts )
231
+ slide = AnnotatedDeepZoomGenerator (osr , ** self .dz_opts )
192
232
try :
193
233
mpp_x = osr .properties [openslide .PROPERTY_NAME_MPP_X ]
194
234
mpp_y = osr .properties [openslide .PROPERTY_NAME_MPP_Y ]
@@ -204,7 +244,7 @@ def get(self, path):
204
244
self ._cache [path ] = slide
205
245
return slide
206
246
207
- def _get_transform (self , image ) :
247
+ def _get_transform (self , image : OpenSlide ) -> Transform :
208
248
if image .color_profile is None :
209
249
return lambda img : None
210
250
mode = self .color_mode
@@ -215,7 +255,7 @@ def _get_transform(self, image):
215
255
# embed ICC profile in tiles
216
256
return lambda img : None
217
257
elif mode == 'default' :
218
- intent = ImageCms .getDefaultIntent (image .color_profile )
258
+ intent = ImageCms .Intent ( ImageCms . getDefaultIntent (image .color_profile ) )
219
259
elif mode == 'absolute-colorimetric' :
220
260
intent = ImageCms .Intent .ABSOLUTE_COLORIMETRIC
221
261
elif mode == 'relative-colorimetric' :
@@ -232,10 +272,10 @@ def _get_transform(self, image):
232
272
'RGB' ,
233
273
'RGB' ,
234
274
intent ,
235
- 0 ,
275
+ ImageCms . Flags ( 0 ) ,
236
276
)
237
277
238
- def xfrm (img ) :
278
+ def xfrm (img : Image . Image ) -> None :
239
279
ImageCms .applyTransform (img , transform , True )
240
280
# Some browsers assume we intend the display's color space if we
241
281
# don't embed the profile. Pillow's serialization is larger, so
@@ -246,9 +286,9 @@ def xfrm(img):
246
286
247
287
248
288
class _Directory :
249
- def __init__ (self , basedir , relpath = '' ):
289
+ def __init__ (self , basedir : str , relpath : str = '' ):
250
290
self .name = os .path .basename (relpath )
251
- self .children = []
291
+ self .children : list [ _Directory | _SlideFile ] = []
252
292
for name in sorted (os .listdir (os .path .join (basedir , relpath ))):
253
293
cur_relpath = os .path .join (relpath , name )
254
294
cur_path = os .path .join (basedir , cur_relpath )
@@ -261,7 +301,7 @@ def __init__(self, basedir, relpath=''):
261
301
262
302
263
303
class _SlideFile :
264
- def __init__ (self , relpath ):
304
+ def __init__ (self , relpath : str ):
265
305
self .name = os .path .basename (relpath )
266
306
self .url_path = relpath
267
307
0 commit comments