Skip to content

Make colorspace detection async - #283

Open
rflamand wants to merge 2 commits into
ImagingDataCommons:masterfrom
rflamand:feature/rflamand/make_colorspace_detection_async
Open

Make colorspace detection async#283
rflamand wants to merge 2 commits into
ImagingDataCommons:masterfrom
rflamand:feature/rflamand/make_colorspace_detection_async

Conversation

@rflamand

Copy link
Copy Markdown
Collaborator

Colorspace adaptation was already provided in #235. However, as noted in #239, this was inherently limited as it only considered the colorspace when loading. If the colorspace changed afterward (e.g. by moving the viewer from one monitor to another), it would not update the viewer. This PR introduces changes such that it does react to such changes.

It makes use of the observable introduced in #260. The colorspace is emitted as an observable that changes value depending on the matchmedia query. The viewer is subscribed to this values and reconfigures the datatile loaders if such a change were to occur.

I have also implemented some small refactoring in the viewer file. More specifically, I have seperated the reload logic in the toggleICCProfiles function and reused it for when the colorspace changes.

@igoroctaviano igoroctaviano left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I've left some inline comments on specific lines that need attention.

Comment thread src/utils.js
mediaQuery.addListener?.(updateColorSpace)
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory Leak: Event listeners are added to media queries but never removed. Since the Observable is returned without a cleanup mechanism, these listeners persist indefinitely.

Suggestion: Add cleanup support to the Observable. For example:

function detectDisplayColorSpace() {
  const colorSpace = new Observable('srgb')
  const cleanupFns = []
  
  if (typeof window !== 'undefined' && window.matchMedia) {
    const p3MediaQuery = window.matchMedia('(color-gamut: p3)')
    const srgbMediaQuery = window.matchMedia('(color-gamut: srgb)')
    const updateColorSpace = () => {
      colorSpace.setValue(p3MediaQuery.matches ? 'display-p3' : 'srgb')
    }

    updateColorSpace()
    for (const mediaQuery of [p3MediaQuery, srgbMediaQuery]) {
      if (mediaQuery.addEventListener) {
        mediaQuery.addEventListener('change', updateColorSpace)
        cleanupFns.push(() => mediaQuery.removeEventListener('change', updateColorSpace))
      } else {
        mediaQuery.addListener?.(updateColorSpace)
        cleanupFns.push(() => mediaQuery.removeListener?.(updateColorSpace))
      }
    }
  }
  
  colorSpace.cleanup = () => cleanupFns.forEach(fn => fn())
  return colorSpace
}

Then in viewer.js cleanup method, call this[_iccOutputType].cleanup?.() before unsubscribing.

Comment thread src/viewer.js
*/
toggleICCProfiles() {
console.debug('toggle ICC profiles:', this[_isICCProfilesEnabled])
this.configureDataLoaders(!this[_isICCProfilesEnabled]).then(() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Error Handling: If configureDataLoaders rejects, the error is silently swallowed. Consider adding a .catch() handler:

toggleICCProfiles() {
  console.debug('toggle ICC profiles:', this[_isICCProfilesEnabled])
  this.configureDataLoaders(!this[_isICCProfilesEnabled])
    .then(() => {
      this[_isICCProfilesEnabled] = !this[_isICCProfilesEnabled]
    })
    .catch((error) => {
      console.error('Failed to toggle ICC profiles:', error)
      const customError = new CustomError(
        errorTypes.VISUALIZATION,
        'Failed to toggle ICC profiles'
      )
      this[_options].errorInterceptor(customError)
    })
}

Comment thread src/viewer.js
Enums.SOPClassUIDs.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE,
)
this[_iccProfiles] = await _getIccProfiles({
metadata,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race Condition: this[_iccProfiles] is assigned inside the Promise.all map callback. If multiple items exist, this gets overwritten multiple times concurrently, which is redundant and could lead to inconsistent state if profiles differ per item.

Suggestion: Fetch ICC profiles once before the loop, or deduplicate the fetch logic.

Comment thread src/viewer.js
: loaderWithoutICCProfiles
source.setLoader(loader)
source.refresh()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Segments and Mappings lose error handlers: Only opticalPath items have onTileLoadError saved (line 1484), but segments and mappings also need tile load error handlers. After reconfiguration, they will lose their error handling.

Suggestion: Ensure onTileLoadError is also set for segments and mappings when they are created, similar to how it is done for optical paths.

Comment thread src/viewer.js
tileGrid: source.getTileGrid(),
projection: source.getProjection(),
wrapX: source.getWrapX(),
bandCount: source.bandCount,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shared Source Instance: Both item.layer and item.overviewLayer are set to the same replacementSource instance. Previously these appeared to have separate sources. Sharing the same DataTileSource might cause unintended side effects (e.g., tile caching behavior, refresh issues).

Is this intentional? If not, consider creating separate sources for each layer.

Comment thread src/viewer.js
@@ -2422,72 +2434,87 @@ class VolumeImageViewer {
getICCProfiles() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed Method: getICCOutputType() was removed but this appears to be unintentional. Please restore it:

/**
 * Get ICC output type.
 *
 * @returns {string} ICC output type
 */
getICCOutputType() {
  return this[_iccOutputType].getValue()
}

Comment thread src/viewer.js
*/
cleanup() {
console.info('cleanup memory')
this[_unsubscribeDisplayColorSpace]?.()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observable Cleanup: The observable from detectDisplayColorSpace() adds event listeners that are never cleaned up. Add cleanup for the observable itself here:

cleanup() {
  console.info('cleanup memory')
  this[_iccOutputType].cleanup?.()  // Add this line
  this[_unsubscribeDisplayColorSpace]?.()
  // ... rest of cleanup
}

This requires implementing the cleanup method on the observable as suggested in the utils.js comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants