33# deepzoom_multiserver - Example web application for viewing multiple slides
44#
55# Copyright (c) 2010-2015 Carnegie Mellon University
6- # Copyright (c) 2021-2022 Benjamin Gilbert
6+ # Copyright (c) 2021-2023 Benjamin Gilbert
77#
88# This library is free software; you can redistribute it and/or modify it
99# under the terms of version 2.1 of the GNU Lesser General Public License
2020#
2121
2222from argparse import ArgumentParser
23+ import base64
2324from collections import OrderedDict
2425from io import BytesIO
2526import os
2627from threading import Lock
28+ import zlib
2729
30+ from PIL import ImageCms
2831from flask import Flask , abort , make_response , render_template , url_for
2932
3033if os .name == 'nt' :
4043from openslide import OpenSlide , OpenSlideCache , OpenSlideError , OpenSlideVersionError
4144from openslide .deepzoom import DeepZoomGenerator
4245
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+
4363
4464def create_app (config = None , config_file = None ):
4565 # Create and configure app
@@ -53,6 +73,7 @@ def create_app(config=None, config_file=None):
5373 DEEPZOOM_OVERLAP = 1 ,
5474 DEEPZOOM_LIMIT_BOUNDS = True ,
5575 DEEPZOOM_TILE_QUALITY = 75 ,
76+ DEEPZOOM_COLOR_MODE = 'absolute-colorimetric' ,
5677 )
5778 app .config .from_envvar ('DEEPZOOM_MULTISERVER_SETTINGS' , silent = True )
5879 if config_file is not None :
@@ -69,7 +90,10 @@ def create_app(config=None, config_file=None):
6990 }
7091 opts = {v : app .config [k ] for k , v in config_map .items ()}
7192 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' ],
7397 )
7498
7599 # Helper functions
@@ -123,8 +147,14 @@ def tile(path, level, col, row, format):
123147 except ValueError :
124148 # Invalid level or coordinates
125149 abort (404 )
150+ slide .transform (tile )
126151 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+ )
128158 resp = make_response (buf .getvalue ())
129159 resp .mimetype = 'image/%s' % format
130160 return resp
@@ -133,9 +163,10 @@ def tile(path, level, col, row, format):
133163
134164
135165class _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 ):
137167 self .cache_size = cache_size
138168 self .dz_opts = dz_opts
169+ self .color_mode = color_mode
139170 self ._lock = Lock ()
140171 self ._cache = OrderedDict ()
141172 # Share a single tile cache among all slide handles, if supported
@@ -162,6 +193,7 @@ def get(self, path):
162193 slide .mpp = (float (mpp_x ) + float (mpp_y )) / 2
163194 except (KeyError , ValueError ):
164195 slide .mpp = 0
196+ slide .transform = self ._get_transform (osr )
165197
166198 with self ._lock :
167199 if path not in self ._cache :
@@ -170,6 +202,44 @@ def get(self, path):
170202 self ._cache [path ] = slide
171203 return slide
172204
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+
173243
174244class _Directory :
175245 def __init__ (self , basedir , relpath = '' ):
@@ -202,6 +272,24 @@ def __init__(self, relpath):
202272 action = 'store_false' ,
203273 help = 'display entire scan area' ,
204274 )
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+ )
205293 parser .add_argument (
206294 '-c' , '--config' , metavar = 'FILE' , dest = 'config' , help = 'config file'
207295 )
0 commit comments