Skip to content

Commit

Permalink
Merge pull request #1052 from girder/icc-setting
Browse files Browse the repository at this point in the history
Allow turning on or off ICC correction from the plugin settings.
  • Loading branch information
manthey authored Feb 7, 2023
2 parents cbd9d58 + cb281f1 commit d17469d
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 23 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions docs/config_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------------------
Expand Down
27 changes: 27 additions & 0 deletions girder/girder_large_image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand Down Expand Up @@ -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']
Expand Down Expand Up @@ -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,
})


Expand Down Expand Up @@ -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()
Expand All @@ -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)
21 changes: 11 additions & 10 deletions girder/girder_large_image/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Original file line number Diff line number Diff line change
Expand Up @@ -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", "*"]}'
Expand Down
3 changes: 3 additions & 0 deletions girder/girder_large_image/web_client/views/configView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
12 changes: 8 additions & 4 deletions girder/test_girder/test_tiles_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,28 +263,31 @@ 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)
assert image[:len(utilities.JFIFHeader)] == utilities.JFIFHeader

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')])
Expand All @@ -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')])
Expand Down
7 changes: 7 additions & 0 deletions girder/test_girder/web_client_specs/largeImageSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
});
Expand Down
3 changes: 3 additions & 0 deletions large_image/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
'cache_tilesource_maximum': 0,

'max_small_image_size': 4096,

# Should ICC color correction be applied by default
'icc_correction': True,
}


Expand Down
10 changes: 6 additions & 4 deletions large_image/tilesource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)):
Expand Down
8 changes: 4 additions & 4 deletions test/test_source_pil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit d17469d

Please sign in to comment.