From cb281f15358ca1695629cbeeddb31d148f0bd495 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 7 Feb 2023 12:21:40 -0500 Subject: [PATCH] Allow turning on or off ICC correction from the plugin settings. --- CHANGELOG.md | 2 +- docs/config_options.rst | 2 ++ girder/girder_large_image/__init__.py | 27 +++++++++++++++++++ girder/girder_large_image/constants.py | 21 ++++++++------- .../web_client/templates/largeImageConfig.pug | 14 ++++++++++ .../web_client/views/configView.js | 3 +++ girder/test_girder/test_tiles_rest.py | 12 ++++++--- .../web_client_specs/largeImageSpec.js | 7 +++++ large_image/config.py | 3 +++ large_image/tilesource/base.py | 10 ++++--- test/test_source_pil.py | 8 +++--- 11 files changed, 86 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aef060b6..18805f17a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 1.20.0 ### Features -- ICC color profile support ([#1037](../../pull/1037), [#1043](../../pull/1043), [#1046](../../pull/1046), [#1048](../../pull/1048)) +- ICC color profile support ([#1037](../../pull/1037), [#1043](../../pull/1043), [#1046](../../pull/1046), [#1048](../../pull/1048), [#1052](../../pull/1052)) ### Improvements - Speed up generating tiles for some multi source files ([#1035](../../pull/1035), [#1047](../../pull/1047)) diff --git a/docs/config_options.rst b/docs/config_options.rst index 88cc544ad..fe9f05557 100644 --- a/docs/config_options.rst +++ b/docs/config_options.rst @@ -27,6 +27,8 @@ Configuration parameters: - ``source_bioformats_ignored_names``, ``source_pil_ignored_names``, ``source_vips_ignored_names``: Some tile sources can read some files that are better read by other tilesources. Since reading these files is suboptimal, these tile sources have a setting that, by default, ignores files without extensions or with particular extensions. This setting is a Python regular expressions. For bioformats this defaults to ``r'(^[!.]*|\.(jpg|jpeg|jpe|png|tif|tiff|ndpi))$'``. +- ``icc_correction``: If this is True or undefined, ICC color correction will be applied for tile sources that have ICC profile information. If False, correction will not be applied. If the style used to open a tilesource specifies ICC correction explicitly (on or off), then this setting is not used. + Configuration from Python ------------------------- diff --git a/girder/girder_large_image/__init__.py b/girder/girder_large_image/__init__.py index b94f0a5a7..4773a2937 100644 --- a/girder/girder_large_image/__init__.py +++ b/girder/girder_large_image/__init__.py @@ -251,6 +251,28 @@ def handleFileSave(event): fileObj['mimeType'] = alt +def handleSettingSave(event): + """ + When certain settings are changed, clear the caches. + """ + if event.info.get('key') == constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION: + if event.info['value'] == Setting().get( + constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION): + return + import gc + + from girder.api.rest import setResponseHeader + + large_image.config.setConfig('icc_correction', bool(event.info['value'])) + large_image.cache_util.cachesClear() + gc.collect() + try: + # ask the browser to clear the cache; it probably won't be honored + setResponseHeader('Clear-Site-Data', '"cache"') + except Exception: + pass + + def metadataSearchHandler( # noqa query, types, user=None, level=None, limit=0, offset=0, models=None, searchModels=None, metakey='meta'): @@ -351,6 +373,7 @@ def metadataSearchHandler( # noqa constants.PluginSettings.LARGE_IMAGE_SHOW_THUMBNAILS, constants.PluginSettings.LARGE_IMAGE_SHOW_VIEWER, constants.PluginSettings.LARGE_IMAGE_NOTIFICATION_STREAM_FALLBACK, + constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION, }) def validateBoolean(doc): val = doc['value'] @@ -435,6 +458,7 @@ def validateFolder(doc): constants.PluginSettings.LARGE_IMAGE_MAX_THUMBNAIL_FILES: 10, constants.PluginSettings.LARGE_IMAGE_MAX_SMALL_IMAGE_SIZE: 4096, constants.PluginSettings.LARGE_IMAGE_NOTIFICATION_STREAM_FALLBACK: True, + constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION: True, }) @@ -462,6 +486,8 @@ def load(self, info): curConfig = config.getConfig().get('large_image') for key, value in (curConfig or {}).items(): large_image.config.setConfig(key, value) + large_image.config.setConfig('icc_correction', bool(Setting().get( + constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION))) addSystemEndpoints(info['apiRoot']) girder_tilesource.loadGirderTileSources() @@ -487,6 +513,7 @@ def load(self, info): events.bind('server_fuse.unmount', 'large_image', large_image.cache_util.cachesClear) events.bind('model.file.remove', 'large_image', handleRemoveFile) events.bind('model.file.save', 'large_image', handleFileSave) + events.bind('model.setting.save', 'large_image', handleSettingSave) search._allowedSearchMode.pop('li_metadata', None) search.addSearchMode('li_metadata', metadataSearchHandler) diff --git a/girder/girder_large_image/constants.py b/girder/girder_large_image/constants.py index 093904894..27a3b9d4e 100644 --- a/girder/girder_large_image/constants.py +++ b/girder/girder_large_image/constants.py @@ -17,18 +17,19 @@ # Constants representing the setting keys for this plugin class PluginSettings: - LARGE_IMAGE_SHOW_THUMBNAILS = 'large_image.show_thumbnails' - LARGE_IMAGE_SHOW_EXTRA_PUBLIC = 'large_image.show_extra_public' + LARGE_IMAGE_AUTO_SET = 'large_image.auto_set' + LARGE_IMAGE_AUTO_USE_ALL_FILES = 'large_image.auto_use_all_files' + LARGE_IMAGE_CONFIG_FOLDER = 'large_image.config_folder' + LARGE_IMAGE_DEFAULT_VIEWER = 'large_image.default_viewer' + LARGE_IMAGE_ICC_CORRECTION = 'large_image.icc_correction' + LARGE_IMAGE_MAX_SMALL_IMAGE_SIZE = 'large_image.max_small_image_size' + LARGE_IMAGE_MAX_THUMBNAIL_FILES = 'large_image.max_thumbnail_files' + LARGE_IMAGE_NOTIFICATION_STREAM_FALLBACK = 'large_image.notification_stream_fallback' LARGE_IMAGE_SHOW_EXTRA = 'large_image.show_extra' LARGE_IMAGE_SHOW_EXTRA_ADMIN = 'large_image.show_extra_admin' - LARGE_IMAGE_SHOW_ITEM_EXTRA_PUBLIC = 'large_image.show_item_extra_public' + LARGE_IMAGE_SHOW_EXTRA_PUBLIC = 'large_image.show_extra_public' LARGE_IMAGE_SHOW_ITEM_EXTRA = 'large_image.show_item_extra' LARGE_IMAGE_SHOW_ITEM_EXTRA_ADMIN = 'large_image.show_item_extra_admin' + LARGE_IMAGE_SHOW_ITEM_EXTRA_PUBLIC = 'large_image.show_item_extra_public' + LARGE_IMAGE_SHOW_THUMBNAILS = 'large_image.show_thumbnails' LARGE_IMAGE_SHOW_VIEWER = 'large_image.show_viewer' - LARGE_IMAGE_DEFAULT_VIEWER = 'large_image.default_viewer' - LARGE_IMAGE_AUTO_SET = 'large_image.auto_set' - LARGE_IMAGE_MAX_THUMBNAIL_FILES = 'large_image.max_thumbnail_files' - LARGE_IMAGE_MAX_SMALL_IMAGE_SIZE = 'large_image.max_small_image_size' - LARGE_IMAGE_AUTO_USE_ALL_FILES = 'large_image.auto_use_all_files' - LARGE_IMAGE_CONFIG_FOLDER = 'large_image.config_folder' - LARGE_IMAGE_NOTIFICATION_STREAM_FALLBACK = 'large_image.notification_stream_fallback' diff --git a/girder/girder_large_image/web_client/templates/largeImageConfig.pug b/girder/girder_large_image/web_client/templates/largeImageConfig.pug index a58539cf5..65f3d404d 100644 --- a/girder/girder_large_image/web_client/templates/largeImageConfig.pug +++ b/girder/girder_large_image/web_client/templates/largeImageConfig.pug @@ -49,6 +49,20 @@ form#g-large-image-form(role="form") select.form-control.input-sm.g-large-image-default-viewer each viewer in viewers option(value=viewer.name, selected=(settings['large_image.default_viewer'] === viewer.name)) #{viewer.label} + .form-group + label ICC Profile Color Correction + p.g-large-image-description + | Some images have ICC Profile information. If present, this can be used + | to adjust to the sRGB color space. Note: if you change this setting, + | you may need to clear your browser cache to see changes. Some caches + | may take an hour or longer to clear on their own. + .g-large-image-viewer-container + label.radio-inline + input.g-large-image-icc-correction(type="radio", name="g-large-image-icc-correction", checked=settings['large_image.icc_correction'] !== false ? 'checked': undefined) + | Apply ICC Profile adjustments + label.radio-inline + input.g-large-image-icc-correction-off(type="radio", name="g-large-image-icc-correction", checked=settings['large_image.icc_correction'] !== false ? undefined : 'checked') + | Do not apply ICC Profile adjustments .form-group - var detailplaceholder = 'A JSON object listing extra details to show. For example: {"metadata": ["tile", "internal"], "images": ["label", "macro", "*"]}' - var detailtitle = 'This can be specified images and metadata as a JSON object such as {"metadata": ["tile", "internal"], "images": ["label", "macro", "*"]}' diff --git a/girder/girder_large_image/web_client/views/configView.js b/girder/girder_large_image/web_client/views/configView.js index 879eca3a0..9c6bd7225 100644 --- a/girder/girder_large_image/web_client/views/configView.js +++ b/girder/girder_large_image/web_client/views/configView.js @@ -60,6 +60,9 @@ var ConfigView = View.extend({ }, { key: 'large_image.notification_stream_fallback', value: this.$('.g-large-image-stream-fallback').prop('checked') + }, { + key: 'large_image.icc_correction', + value: this.$('.g-large-image-icc-correction').prop('checked') }]); }, 'click .g-open-browser': '_openBrowser' diff --git a/girder/test_girder/test_tiles_rest.py b/girder/test_girder/test_tiles_rest.py index 134ab904f..763264ee8 100644 --- a/girder/test_girder/test_tiles_rest.py +++ b/girder/test_girder/test_tiles_rest.py @@ -263,20 +263,22 @@ def testTilesFromPTIF(server, admin, fsAssetstore): # Check that we conditionally get JFIF headers resp = server.request(path='/item/%s/tiles/zxy/0/0/0' % itemId, - user=admin, isJson=False) + user=admin, isJson=False, + params={'style': '{"icc": false}'}) assert utilities.respStatus(resp) == 200 image = utilities.getBody(resp, text=False) assert image[:len(utilities.JFIFHeader)] != utilities.JFIFHeader resp = server.request(path='/item/%s/tiles/zxy/0/0/0' % itemId, user=admin, isJson=False, - params={'encoding': 'JFIF'}) + params={'style': '{"icc": false}', 'encoding': 'JFIF'}) assert utilities.respStatus(resp) == 200 image = utilities.getBody(resp, text=False) assert image[:len(utilities.JFIFHeader)] == utilities.JFIFHeader resp = server.request(path='/item/%s/tiles/zxy/0/0/0' % itemId, user=admin, isJson=False, + params={'style': '{"icc": false}'}, additionalHeaders=[('User-Agent', 'iPad')]) assert utilities.respStatus(resp) == 200 image = utilities.getBody(resp, text=False) @@ -284,7 +286,8 @@ def testTilesFromPTIF(server, admin, fsAssetstore): resp = server.request( path='/item/%s/tiles/zxy/0/0/0' % itemId, user=admin, - isJson=False, additionalHeaders=[( + isJson=False, params={'style': '{"icc": false}'}, + additionalHeaders=[( 'User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X ' '10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) ' 'Version/10.0.3 Safari/602.4.8')]) @@ -294,7 +297,8 @@ def testTilesFromPTIF(server, admin, fsAssetstore): resp = server.request( path='/item/%s/tiles/zxy/0/0/0' % itemId, user=admin, - isJson=False, additionalHeaders=[( + isJson=False, params={'style': '{"icc": false}'}, + additionalHeaders=[( 'User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 ' 'Safari/537.36')]) diff --git a/girder/test_girder/web_client_specs/largeImageSpec.js b/girder/test_girder/web_client_specs/largeImageSpec.js index ec8b4c0f4..138fcf51d 100644 --- a/girder/test_girder/web_client_specs/largeImageSpec.js +++ b/girder/test_girder/web_client_specs/largeImageSpec.js @@ -49,6 +49,7 @@ describe('Test the large image plugin', function () { $('.g-large-image-show-item-extra-public').val('{}'); $('.g-large-image-show-item-extra').val('{}'); $('.g-large-image-show-item-extra-admin').val('{"metadata": ["tile", "internal"], "images": ["label", "macro", "*"]}'); + $('.g-large-image-icc-correction-off').trigger('click'); $('#g-large-image-form input.btn-primary').click(); }); girderTest.waitForLoad(); @@ -74,6 +75,12 @@ describe('Test the large image plugin', function () { expect(settings['large_image.show_item_extra_public']).toBe('{}'); expect(settings['large_image.show_item_extra']).toBe('{}'); expect(JSON.parse(settings['large_image.show_item_extra_admin'])).toEqual({'metadata': ['tile', 'internal'], 'images': ['label', 'macro', '*']}); + expect(settings['large_image.icc_correction']).toBe(false); + }); + girderTest.waitForLoad(); + runs(function () { + $('.g-large-image-icc-correction').trigger('click'); + $('#g-large-image-form input.btn-primary').click(); }); girderTest.waitForLoad(); }); diff --git a/large_image/config.py b/large_image/config.py index 6573f9d1c..57550c6d7 100644 --- a/large_image/config.py +++ b/large_image/config.py @@ -30,6 +30,9 @@ 'cache_tilesource_maximum': 0, 'max_small_image_size': 4096, + + # Should ICC color correction be applied by default + 'icc_correction': True, } diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index 23d1b6c75..f68f11a12 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -1281,12 +1281,14 @@ def _applyStyle(self, image, style, x, y, z, frame=None): # noqa image=image, originalStyle=style, x=x, y=y, z=z, frame=frame, mainImage=image, mainFrame=frame, dtype=None, axis=None) if style is None or ('icc' in style and len(style) == 1): - sc.style = {'icc': (style or {}).get('icc', True), 'bands': []} + sc.style = {'icc': (style or {}).get( + 'icc', config.getConfig('icc_correction', True)), 'bands': []} else: sc.style = style if 'bands' in style else {'bands': [style]} sc.dtype = style.get('dtype') sc.axis = style.get('axis') - if hasattr(self, '_iccprofiles') and sc.style.get('icc', True): + if hasattr(self, '_iccprofiles') and sc.style.get( + 'icc', config.getConfig('icc_correction', True)): image = self._applyICCProfile(sc, frame) if style is None or ('icc' in style and len(style) == 1): sc.output = image @@ -1464,8 +1466,8 @@ def _outputTile(self, tile, tileEncoding, x, y, z, pilImageAllowed=False, maxY = (y + 1) * self.tileHeight isEdge = maxX > sizeX or maxY > sizeY hasStyle = ( - (getattr(self, 'style', None) or hasattr(self, '_iccprofiles')) and - getattr(self, 'style', None) != {'icc': False}) + len(set(getattr(self, 'style', {})) - {'icc'}) or + getattr(self, 'style', {}).get('icc', config.getConfig('icc_correction', True))) if (tileEncoding not in (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY) and numpyAllowed != 'always' and tileEncoding == self.encoding and not isEdge and (not applyStyle or not hasStyle)): diff --git a/test/test_source_pil.py b/test/test_source_pil.py index 9bb706b3e..69eb2a811 100644 --- a/test/test_source_pil.py +++ b/test/test_source_pil.py @@ -39,20 +39,20 @@ def testTileRedirects(): # Test redirects, use a JPEG imagePath = datastore.fetch('sample_Easy1.jpeg') rawimage = open(imagePath, 'rb').read() - source = large_image_source_pil.open(imagePath) + source = large_image_source_pil.open(imagePath, style={'icc': False}) # No encoding or redirect should just get a JPEG image = source.getTile(0, 0, 0) assert image == rawimage # quality 75 should work - source = large_image_source_pil.open(imagePath, jpegQuality=95) + source = large_image_source_pil.open(imagePath, style={'icc': False}, jpegQuality=95) image = source.getTile(0, 0, 0) assert image == rawimage # redirect with a different quality shouldn't - source = large_image_source_pil.open(imagePath, jpegQuality=75) + source = large_image_source_pil.open(imagePath, style={'icc': False}, jpegQuality=75) image = source.getTile(0, 0, 0) assert image != rawimage # redirect with a different encoding shouldn't - source = large_image_source_pil.open(imagePath, encoding='PNG') + source = large_image_source_pil.open(imagePath, style={'icc': False}, encoding='PNG') image = source.getTile(0, 0, 0) assert image != rawimage