3
3
# deepzoom_multiserver - Example web application for viewing multiple slides
4
4
#
5
5
# Copyright (c) 2010-2015 Carnegie Mellon University
6
- # Copyright (c) 2021-2022 Benjamin Gilbert
6
+ # Copyright (c) 2021-2023 Benjamin Gilbert
7
7
#
8
8
# This library is free software; you can redistribute it and/or modify it
9
9
# under the terms of version 2.1 of the GNU Lesser General Public License
20
20
#
21
21
22
22
from argparse import ArgumentParser
23
+ import base64
23
24
from collections import OrderedDict
24
25
from io import BytesIO
25
26
import os
26
27
from threading import Lock
28
+ import zlib
27
29
30
+ from PIL import ImageCms
28
31
from flask import Flask , abort , make_response , render_template , url_for
29
32
30
33
if os .name == 'nt' :
40
43
from openslide import OpenSlide , OpenSlideCache , OpenSlideError , OpenSlideVersionError
41
44
from openslide .deepzoom import DeepZoomGenerator
42
45
46
+ # Optimized sRGB v2 profile, CC0-1.0 license
47
+ # https://github.com/saucecontrol/Compact-ICC-Profiles/blob/bdd84663/profiles/sRGB-v2-micro.icc
48
+ # ImageCms.createProfile() generates a v4 profile and Firefox has problems
49
+ # with those: https://littlecms.com/blog/2020/09/09/browser-check/
50
+ SRGB_PROFILE_BYTES = zlib .decompress (
51
+ base64 .b64decode (
52
+ 'eNpjYGA8kZOcW8wkwMCQm1dSFOTupBARGaXA/oiBmUGEgZOBj0E2Mbm4wDfYLYQBCIoT'
53
+ 'y4uTS4pyGFDAt2sMjCD6sm5GYl7K3IkMtg4NG2wdSnQa5y1V6mPADzhTUouTgfQHII5P'
54
+ 'LigqYWBg5AGyecpLCkBsCSBbpAjoKCBbB8ROh7AdQOwkCDsErCYkyBnIzgCyE9KR2ElI'
55
+ 'bKhdIMBaCvQsskNKUitKQLSzswEDKAwgop9DwH5jFDuJEMtfwMBg8YmBgbkfIZY0jYFh'
56
+ 'eycDg8QthJgKUB1/KwPDtiPJpUVlUGu0gLiG4QfjHKZS5maWk2x+HEJcEjxJfF8Ez4t8'
57
+ 'k8iS0VNwVlmjmaVXZ/zacrP9NbdwX7OQshjxFNmcttKwut4OnUlmc1Yv79l0e9/MU8ev'
58
+ 'pz4p//jz/38AR4Nk5Q=='
59
+ )
60
+ )
61
+ SRGB_PROFILE = ImageCms .getOpenProfile (BytesIO (SRGB_PROFILE_BYTES ))
62
+
43
63
44
64
def create_app (config = None , config_file = None ):
45
65
# Create and configure app
@@ -53,6 +73,7 @@ def create_app(config=None, config_file=None):
53
73
DEEPZOOM_OVERLAP = 1 ,
54
74
DEEPZOOM_LIMIT_BOUNDS = True ,
55
75
DEEPZOOM_TILE_QUALITY = 75 ,
76
+ DEEPZOOM_COLOR_MODE = 'absolute-colorimetric' ,
56
77
)
57
78
app .config .from_envvar ('DEEPZOOM_MULTISERVER_SETTINGS' , silent = True )
58
79
if config_file is not None :
@@ -69,7 +90,10 @@ def create_app(config=None, config_file=None):
69
90
}
70
91
opts = {v : app .config [k ] for k , v in config_map .items ()}
71
92
app .cache = _SlideCache (
72
- app .config ['SLIDE_CACHE_SIZE' ], app .config ['SLIDE_TILE_CACHE_MB' ], opts
93
+ app .config ['SLIDE_CACHE_SIZE' ],
94
+ app .config ['SLIDE_TILE_CACHE_MB' ],
95
+ opts ,
96
+ app .config ['DEEPZOOM_COLOR_MODE' ],
73
97
)
74
98
75
99
# Helper functions
@@ -123,8 +147,14 @@ def tile(path, level, col, row, format):
123
147
except ValueError :
124
148
# Invalid level or coordinates
125
149
abort (404 )
150
+ slide .transform (tile )
126
151
buf = BytesIO ()
127
- tile .save (buf , format , quality = app .config ['DEEPZOOM_TILE_QUALITY' ])
152
+ tile .save (
153
+ buf ,
154
+ format ,
155
+ quality = app .config ['DEEPZOOM_TILE_QUALITY' ],
156
+ icc_profile = tile .info .get ('icc_profile' ),
157
+ )
128
158
resp = make_response (buf .getvalue ())
129
159
resp .mimetype = 'image/%s' % format
130
160
return resp
@@ -133,9 +163,10 @@ def tile(path, level, col, row, format):
133
163
134
164
135
165
class _SlideCache :
136
- def __init__ (self , cache_size , tile_cache_mb , dz_opts ):
166
+ def __init__ (self , cache_size , tile_cache_mb , dz_opts , color_mode ):
137
167
self .cache_size = cache_size
138
168
self .dz_opts = dz_opts
169
+ self .color_mode = color_mode
139
170
self ._lock = Lock ()
140
171
self ._cache = OrderedDict ()
141
172
# Share a single tile cache among all slide handles, if supported
@@ -162,6 +193,7 @@ def get(self, path):
162
193
slide .mpp = (float (mpp_x ) + float (mpp_y )) / 2
163
194
except (KeyError , ValueError ):
164
195
slide .mpp = 0
196
+ slide .transform = self ._get_transform (osr )
165
197
166
198
with self ._lock :
167
199
if path not in self ._cache :
@@ -170,6 +202,44 @@ def get(self, path):
170
202
self ._cache [path ] = slide
171
203
return slide
172
204
205
+ def _get_transform (self , image ):
206
+ if image .color_profile is None :
207
+ return lambda img : None
208
+ mode = self .color_mode
209
+ if mode == 'ignore' :
210
+ # drop ICC profile from tiles
211
+ return lambda img : img .info .pop ('icc_profile' )
212
+ elif mode == 'embed' :
213
+ # embed ICC profile in tiles
214
+ return lambda img : None
215
+ elif mode == 'absolute-colorimetric' :
216
+ intent = ImageCms .Intent .ABSOLUTE_COLORIMETRIC
217
+ elif mode == 'relative-colorimetric' :
218
+ intent = ImageCms .Intent .RELATIVE_COLORIMETRIC
219
+ elif mode == 'perceptual' :
220
+ intent = ImageCms .Intent .PERCEPTUAL
221
+ elif mode == 'saturation' :
222
+ intent = ImageCms .Intent .SATURATION
223
+ else :
224
+ raise ValueError (f'Unknown color mode { mode } ' )
225
+ transform = ImageCms .buildTransform (
226
+ image .color_profile ,
227
+ SRGB_PROFILE ,
228
+ 'RGB' ,
229
+ 'RGB' ,
230
+ intent ,
231
+ 0 ,
232
+ )
233
+
234
+ def xfrm (img ):
235
+ ImageCms .applyTransform (img , transform , True )
236
+ # Some browsers assume we intend the display's color space if we
237
+ # don't embed the profile. Pillow's serialization is larger, so
238
+ # use ours.
239
+ img .info ['icc_profile' ] = SRGB_PROFILE_BYTES
240
+
241
+ return xfrm
242
+
173
243
174
244
class _Directory :
175
245
def __init__ (self , basedir , relpath = '' ):
@@ -202,6 +272,24 @@ def __init__(self, relpath):
202
272
action = 'store_false' ,
203
273
help = 'display entire scan area' ,
204
274
)
275
+ parser .add_argument (
276
+ '--color-mode' ,
277
+ dest = 'DEEPZOOM_COLOR_MODE' ,
278
+ choices = [
279
+ 'absolute-colorimetric' ,
280
+ 'perceptual' ,
281
+ 'relative-colorimetric' ,
282
+ 'saturation' ,
283
+ 'embed' ,
284
+ 'ignore' ,
285
+ ],
286
+ default = 'absolute-colorimetric' ,
287
+ help = (
288
+ 'convert tiles to sRGB using specified rendering intent, or '
289
+ 'embed original ICC profile, or ignore ICC profile (compat) '
290
+ '[absolute-colorimetric]'
291
+ ),
292
+ )
205
293
parser .add_argument (
206
294
'-c' , '--config' , metavar = 'FILE' , dest = 'config' , help = 'config file'
207
295
)
0 commit comments