Skip to content

feat(display): per-output ICC color profiles and color temperature - #3388

Open
KIDult2226 wants to merge 6 commits into
AvengeMedia:masterfrom
KIDult2226:icc-per-output
Open

feat(display): per-output ICC color profiles and color temperature#3388
KIDult2226 wants to merge 6 commits into
AvengeMedia:masterfrom
KIDult2226:icc-per-output

Conversation

@KIDult2226

@KIDult2226 KIDult2226 commented Sep 10, 2026

Copy link
Copy Markdown

Description

Adds per-output ICC color profile support and per-output color temperature to the night light / gamma stack.

ICC profiles

  • New core/internal/icc package: ICC v2/v4 parser (TRC curves, LUT8/LUT16 and parametric curves, vcgt, description/version/color space metadata) with unit tests.
  • The wayland manager loads iccProfiles and outputTemps from the DMS config directory of the running compositor (<config>/<compositor>/dms/wayland.json, for example ~/.config/niri/dms/wayland.json on niri), builds gamma ramps from the parsed profile per output, falls back to temperature ramps when a profile is missing or unparsable, and re-applies after resume.
  • ICC ramps are applied even when the global night light is off (outputs without a profile keep identity/neutral), so a calibrated profile is not silently dropped when the schedule is disabled.
  • The configuration is attached independently of the gamma control lifecycle, and a hotplugged output picks up its profile and temperature as soon as its name is known (or when its control is created).

Per-output temperature

  • outputTemps in the same config; SetOutputTemp accepts 1000-10000K and the Display Config slider exposes the full 3000-10000K range.
  • Per-output temperature is an independent target: it applies whether or not the night light schedule runs, and a per-output value wins over the schedule, so each display can keep its own white point and its own on/off state (0 = no override). It is composed on top of an ICC profile ramp, which is treated as measured at 6500K (D65); outputs without a profile are driven with the same value directly. dms icc set-temp <output> <kelvin> sets it from the CLI and dms icc status reports it.

IPC / CLI

  • wayland.icc.getStatus, wayland.icc.apply, wayland.icc.remove, wayland.icc.listOutputs, wayland.icc.setTemp, wayland.icc.getTemps.
  • dms icc list | info <file> | apply <output> <file> | remove <output> | status | set-temp <output> <kelvin>.

UI

  • Display Config output card gains a Color Profile row (browse/apply/remove with description, version, color space and active state) plus a per-output color temperature slider, backed by quickshell/Services/ICCService.qml.

Included fix

  • LoadConfig() now takes its fallback values from DefaultConfig() and covers Contrast. Validate() rejects Contrast == 0, so night light configs written before that field existed aborted manager construction and took gamma/ICC down entirely (wayland manager not initialized).

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behavior)
  • Refactor / internal cleanup
  • Documentation
  • Other

Related issues

Screenshots / video

Display Config output cards with the new Color Profile row (browse / apply / remove, showing an applied profile with version, color space and active state) and the per-output Color Temp slider set to 7000K on the calibrated display:

Display Config: Color Profile row and per-output color temperature

Checklist

  • My code follows the conventions in CONTRIBUTING.md
  • I have tested my changes locally
  • New user-facing strings are wrapped in I18n.tr() with translator context, reusing existing terms where possible
  • Go changes: ran make fmt, added/updated tests, make test passes (TZ=UTC go test ./... -> 51 ok, 0 fail), and go mod tidy is clean
  • QML changes: ran make lint-qml with no new warnings

make lint-qml could not run in my worktree: it needs the Quickshell tooling VFS and the dank-qml-common submodule, which is not checked out there; it fails identically on unmodified master and lints only 3 entrypoints. i18n checks (check_term_variants.py, check_term_freeze.py) and extract_settings_index.py were run instead.

@bbedward

Copy link
Copy Markdown
Collaborator

/claude review

Comment thread core/internal/server/wayland/manager.go Outdated
log.Info("gamma: output returned, re-establishing controls")
m.controlsInitialized = true
})
if m.controlsInitialized {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This drops the recovery path for monitor sleep/disconnect. When the last output goes away, the remove handler sets m.controlsInitialized = false (line 270). With the old code the returning output ran addOutputControl and then re-marked controlsInitialized = true so the following gamma_size drove the reapply. Now the if m.controlsInitialized guard is false in exactly that case, so nothing is created and gamma/ICC stays dead until the user manually toggles night light.

Keep the previous behaviour: always post addOutputControl, and set controlsInitialized = true when it was cleared.

Comment thread core/internal/server/wayland/manager.go Outdated
case !m.controlsInitialized:
log.Debugf("applyGamma skipped: controls not initialized")
return
case m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This manager-level dedupe doesn't include contrast, and it short-circuits before the per-output loop, so two existing paths break:

  • SetAdjustments(gamma, contrast) with only contrast changed → syncControlstriggerUpdateapplyCurrentTempapplyGamma(temp) with the same temp and gamma → early return. The contrast change is never written to any output.
  • handleResume (line 1119) resets every out.lastTemp = 0 to force a resend after suspend (Gamma settings not applied after wake from sleep [with patch] #1235), then calls applyCurrentTemp. Temp and gamma are unchanged across suspend, so this guard returns before the outputs are touched and the forced resend never happens.

Either include contrast in the comparison and have those paths reset m.lastAppliedTemp, or drop this guard and rely on the existing per-output rampCurrent dedupe, which already covers temp/gamma/contrast.

Comment thread core/internal/server/wayland/manager.go Outdated
} else {
log.Debugf("gamma_size: output %d not found in m.outputs", outputID)
}
m.lastAppliedTemp = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dropping out.lastTemp = 0 here (and the matching one in the send-failure path below, line ~936) breaks recovery after a gamma control failure. recreateOutputControl reuses the same outputState, so lastTemp/lastGamma/lastContrast survive the failure. When the new control's gamma_size arrives, failed and rampSize are reset but rampCurrent(temp, gamma, contrast) still returns true, so the output is skipped and the recreated control never receives a ramp — the display stays uncorrected.

m.lastAppliedTemp = 0 only bypasses the manager-level guard; it doesn't clear the per-output state. Keep out.lastTemp = 0 in both places.

sunrise := time.Date(now.Year(), now.Month(), now.Day(),
config.ManualSunrise.Hour(), config.ManualSunrise.Minute(), config.ManualSunrise.Second(), 0, now.Location())
sunset := time.Date(now.Year(), now.Month(), now.Day(),
config.ManualSunset.Hour(), config.ManualSunset.Minute(), config.ManualSunset.Second(), 0, now.Location())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR reverts three unrelated scheduler fixes that exist on the base commit:

  1. The if !sunset.After(sunrise) { sunset = sunset.Add(24 * time.Hour) } adjustment right below this line is gone, so a manual schedule whose night start is past midnight (e.g. sunrise 07:00, sunset 00:30) now produces a Sunset before Sunrise on the same day.
  2. activeCycle/shiftTimes were deleted along with their call sites in getSunPositionNormal, getDeadlineNormal and updateStateFromSchedule, so early-morning hours no longer map back to yesterday's cycle — the temperature and isDay are wrong between midnight and dawn.
  3. In the location-missing branch below (line 553), m.schedule = sunSchedule{} was removed, so stale times from a previous config keep driving applies.

None of these are related to ICC; they should be restored.

Comment thread core/internal/server/wayland/manager.go Outdated
// non-ICC outputs get identity (no color shift).
if !enabled {
m.applyGamma(neutralTemp)
m.applyGamma(high)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high is the user-configurable HighTemp (SetTemperature, valid down to 1000K), not a neutral point — the removed neutralTemp was a fixed 6500. With night light disabled and HighTemp set to e.g. 5000, every output now gets a permanent 5000K ramp written to it instead of neutral. Use a fixed 6500 here (or DefaultConfig().HighTemp).

Related: syncControls (line 1292) still calls destroyControls() whenever needsControls() is false, so toggling night light off tears the controls down and drops the ICC ramps anyway — which defeats the "ICC applies while night light is off" goal of this PR. needsControls() probably needs to account for configured ICC profiles / per-output temps.

Comment thread core/internal/server/wayland/types.go Outdated
if err != nil {
return "", err
}
return filepath.Join(configDir, "niri", "dms", "wayland.json"), nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

niri is hardcoded, but this dir is compositor-specific in the rest of the codebase — commands_setup.go:275 picks niri/dms, hypr/dms or mango/dms based on the running compositor. On Hyprland/mango/sway this writes ICC and per-output temp config into a niri directory that the rest of DMS never looks at. commands_icc.go:70 has the same hardcoding for the ICC profile dir.

Also worth using utils.XDGConfigHome() here rather than os.UserConfigDir(), so it matches the path resolution used everywhere else in core.

Comment thread core/internal/icc/parser.go Outdated
strOff := binary.BigEndian.Uint32(data[recordStart+8 : recordStart+12])
// String offset is absolute from profile start
strStart := entry.offset + strOff
strEnd := strStart + strLen

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Both desc branches can panic on a malformed profile, which takes down the dms daemon (ApplyICC parses a user-picked file in-process, and dms icc list parses every file in the config dir).

  • strOff and strLen come straight from the file. strStart is never bounds-checked, and strEnd := strStart + strLen is unchecked uint32 arithmetic. If strStart > len(data) the clamp sets strEnd = len(data), leaving strStart > strEnddata[strStart:strEnd] panics. Same for the desc path at line 266-267 if strLen is large enough to wrap strEnd below strStart.
  • recordStart+12 (line 297-299) is only guarded by entry.size < 16, so a tag with size == 16 at the end of the buffer reads past the slice.

Validate strStart <= strEnd <= len(data) (and recordStart+12 <= len(data)) before slicing, and return an error instead.


onSliderValueChanged: function(newValue) {
colorTempRow.editing = true
tempLabel.text = newValue + "K"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Assigning to tempLabel.text imperatively destroys its binding permanently. After the first drag the label is a static string: colorTempRow.editing = false no longer restores it, and it stops tracking ICCService.outputTemps[outputName] for any later change (including one made from the CLI or another surface).

The binding on line 537 already handles both cases via colorTempRow.editing/tempSlider.value, so this line just needs to go.

Suggested change
tempLabel.text = newValue + "K"
colorTempRow.editing = true

Comment on lines +570 to +571
colorTempRow.editing = true
tempLabel.text = newValue + "K"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Correcting the suggestion in my comment above — it should replace both lines, not just line 571 (otherwise editing = true ends up duplicated):

Suggested change
colorTempRow.editing = true
tempLabel.text = newValue + "K"
colorTempRow.editing = true

}
}

DisplayConfirmationModal {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR deletes the identifyConfigured property, the MonitorIdentifyOverlay Loader that followed this block, and the id: monitorCanvas it depends on (line 560). That removes the working "identify monitors" overlay, which is unrelated to ICC — looks like an accidental revert. Same for the Theme.spacingXXS2 changes here and in OutputCard.qml, which swap Theme tokens back for hardcoded values.

@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Claude review

Solid feature work, but it reverts several unrelated fixes in the gamma stack and the display settings UI, and the new dedupe breaks two existing paths — those need to be sorted before merge.

  • Monitor sleep/disconnect no longer re-establishes gamma controls, core/internal/server/wayland/manager.go:229
  • New manager-level dedupe ignores contrast and defeats the post-resume forced resend (Gamma settings not applied after wake from sleep [with patch] #1235), core/internal/server/wayland/manager.go:851
  • out.lastTemp = 0 removed, so a recreated gamma control never gets its ramp resent, core/internal/server/wayland/manager.go:341
  • Scheduler fixes reverted: midnight-crossing manual sunset, activeCycle, stale-schedule reset, core/internal/server/wayland/manager.go:542
  • Night light off now applies user HighTemp instead of neutral 6500, and syncControls still destroys controls when disabled (drops ICC ramps), core/internal/server/wayland/manager.go:815
  • Config path hardcodes niri/ for all compositors, core/internal/server/wayland/types.go:167
  • desc/mluc parsing can panic on a malformed profile and take down the daemon, core/internal/icc/parser.go:301
  • tempLabel.text assignment destroys the binding, label goes stale after the first drag, quickshell/Modules/Settings/DisplayConfig/OutputCard.qml:571
  • MonitorIdentifyOverlay and identifyConfigured deleted, quickshell/Modules/Settings/DisplayConfigTab.qml:672

Checked: full diff against the merge base, gamma manager lifecycle/dedupe paths, ICC parser bounds handling, IPC handlers, and the new QML service and Display Config rows. Model: claude-opus-5.

Rebuild of AvengeMedia#3388 on current master: the previous branch was cut from an
older base and reverted unrelated fixes in the gamma stack and the display
settings UI.

ICC profiles:
- core/internal/icc: ICC v2/v4 parser (TRC, LUT8/16 and parametric curves,
  vcgt, description/version/color space) plus gamma ramp generation, with
  synthetic coverage and optional vendor-profile tests (ICC_TEST_DIR).
- wayland manager: load iccProfiles from the compositor's DMS config dir,
  build the ramp from the profile when one is set (falling back to the
  temperature ramp when it is missing or unparsable), and re-apply on
  output hotplug and after resume.
- ICC ramps apply while the night light schedule is disabled; outputs
  without a profile keep an identity ramp.
- desc/mluc offsets are validated before slicing, so a malformed profile
  returns an error instead of panicking the daemon.

Per-output temperature:
- outputTemps in the same config; SetOutputTemp accepts 1000-10000K and the
  Display Config slider exposes 3000-10000K, which matters for displays
  calibrated at a higher white point (e.g. a 7000K profile).

Gamma control lifecycle:
- needsControls() now keeps the controls alive for configured ICC profiles
  and per-output temperatures, so toggling the night light off no longer
  destroys the controls and drops the ramps.
- Re-apply paths clear the per-output dedup state instead of adding a
  manager-level guard, which would have suppressed contrast-only writes and
  the post-resume forced resend (AvengeMedia#1235), and left a recreated control
  without a ramp.
- Monitor sleep/disconnect still re-establishes controls when an output
  comes back.

IPC / CLI / UI:
- wayland.icc.{getStatus,apply,remove,listOutputs,setTemp,getTemps}
- dms icc list | info <file> | apply <output> <file> | remove <output> | status
- Display Config output card gains a Color Profile row (browse/apply/remove
  with description, version, color space and active state) and a per-output
  color temperature slider, backed by Services/ICCService.qml.

Config path:
- DMSConfigDir() follows the compositor layout (niri/dms, hypr/dms,
  mango/dms) via the shared compositor detection and utils.XDGConfigHome(),
  replacing the hardcoded niri path in both the manager and `dms icc`.

Tests: TZ=UTC go test ./... (60 packages ok), including red/green coverage
for the icc parser bounds checks and needsControls().
@KIDult2226

Copy link
Copy Markdown
Author

Rebased onto current master (6b50696b) and rebuilt so the diff contains only the ICC / per-output-temperature work. Thanks for the review — I reproduced every point before fixing it.

Reverted upstream fixes (restored)

  • manager.go:229 — the "output returned, re-establishing controls" branch is back: addOutputControl is always posted and controlsInitialized is re-marked so the following gamma_size drives the reapply.
  • manager.go:341out.lastTemp = 0 restored in the gamma_size handler and on the send-failure path.
  • manager.go:542 — restored the midnight-crossing sunset adjustment, activeCycle/shiftTimes (plus their call sites in getSunPositionNormal, getDeadlineNormal, updateStateFromSchedule) and the m.schedule = sunSchedule{} reset.
  • manager.go:815applyGamma(neutralTemp) (fixed 6500) again, not HighTemp.
  • DisplayConfigTab.qml:672identifyConfigured, the MonitorIdentifyOverlay loader and id: monitorCanvas are back; Theme.spacingXXS untouched in both files.

New-code fixes

  • manager.go:851 — the manager-level dedupe is gone; applyGamma uses the per-output rampCurrent check as before, so contrast-only SetAdjustments writes and the post-resume forced resend (Gamma settings not applied after wake from sleep [with patch] #1235) work again. ICC/setTemp changes now clear the affected output's state (out.lastTemp = 0), which the schedule-derived comparison cannot see.
  • needsControls() now returns true for configured ICCProfiles/OutputTemps, so disabling the night light no longer calls destroyControls() and drops the ICC ramps. Covered by TestManager_NeedsControlsCoversICCAndOutputTemps (fails on the old one-liner).
  • types.go:167 / commands_icc.go:70 — both go through DMSConfigDir() / ICCProfilesDir(), which map the running compositor to niri/dms, hypr/dms or mango/dms using the same detection the rest of core uses, and resolve via utils.XDGConfigHome(). dms icc reuses that helper instead of its own copy.
  • parser.go:301desc/mluc offsets and lengths are validated in int arithmetic before slicing (strStart <= strEnd <= len(data), recordStart + 12 <= len(data)); malformed profiles now return an error. TestParseDescriptionMalformed panics on the old code (slice bounds out of range [12:4]) and passes on the new one.
  • OutputCard.qml:571 — the tempLabel.text assignment is gone, so the existing binding keeps tracking the slider and ICCService.outputTemps.
  • i18n: new strings carry translator context, existing terms (Browse, Default, Active, Color Temperature) are reused, and no catalog was re-extracted.

One small addition beyond the review: SettingsContent.qml now passes parentModal into DisplayConfigTab, so the ICC file browser stacks over the settings modal the way WallpaperTab's does.

Verification: TZ=UTC go test ./... (all packages ok), check_term_variants.py clean, extract_settings_index.py produces no diff. make lint-qml fails identically on master in my worktree because the dank-qml-common submodule is not checked out; it lints 3 entrypoints only, so it does not cover these files either way.

Diff vs master: 13 files, +2406/−13. The 13 removed lines are DefaultConfig()LoadConfig(), the single GenerateGammaRamp(...) line replaced by the ICC-aware ramp selection, the needsControls() one-liner, the State struct field realignment, and the DisplayConfigTab instantiation.

The per-output temperature override was reachable from the Display Config
slider and from `wayland.icc.setTemp` over IPC, but not from the CLI.

- `dms icc set-temp <output> <kelvin>` (alias `setTemp`) sets a per-output
  override, 1000-10000K, and 0 clears it so the output follows the night
  light schedule again. Range checking matches the daemon so an invalid
  value fails in the CLI instead of over IPC.
- `dms icc status` gains a Temp column (the override in K, or `schedule`),
  so an override can be verified without opening the settings UI.

Verified against a running daemon: `dms icc status` reports 7000K for the
three overridden outputs and `schedule` for the remaining one.
…e white point

The per-output temperature was documented as the white point a profile was
produced at, but an output with a profile ignored it entirely: the ramp was
generated from the profile and the temperature only applied to outputs
without one (or as the fallback when a profile failed to parse). A display
calibrated at 7000K therefore got the profile ramp, and enabling the night
light had no effect on it at all.

- `ProfileRampWithTemp` composes the profile ramp with the ratio between the
  target temperature ramp and the reference ramp, so the temperature is the
  white point the profile describes and the night light shifts relative to
  it.
- `applyGamma` passes the night light temperature as the target, and
  `noTempTarget` when the schedule is disabled, which leaves profiled
  outputs at their reference white point and drives plain outputs with the
  neutral ramp.
- The re-apply sentinel stays 0, so it cannot collide with an applied
  temperature.

Covered by TestProfileRampWithTemp (reference == target is a no-op, warmer
and cooler targets move the expected channels).

Also: `dms icc set-temp` help text describes the reference-white-point
semantics.
Restoring the upstream "output returned, re-establishing controls" branch
made the registry handler establish the gamma controls before the startup
post runs, and that post returned early on `m.controlsInitialized`, so the
configured ICC profiles and per-output temperatures were never attached:
every output reported "(none)" and the night light ran with defaults until
the user re-applied a profile by hand.

- `initializeControlsAndICC` (the startup post) loads the configuration
  first and only creates the controls when they are missing.
- The loading is split into `loadConfiguredICC` / `applyConfiguredICCForOutput`
  so it can also run per output.
- A hotplugged output now gets its configured profile and temperature as soon
  as its name is known (name handler) or when its control is created, which
  also makes the "re-applies on hotplug" claim in the description true.
- After attaching, the output's dedup state is cleared so the ramp is written.

Covered by TestManager_LoadConfiguredICCWhenControlsAlreadyExist and
TestManager_AttachConfiguredICCForNamedOutput.
The per-output value was treated as the white point a profile was produced
at, which meant a display with a profile never changed when the user set a
temperature: the profile ramp was applied as measured and the value only
mattered when a night light target existed. Per-display temperatures were
therefore unusable on profiled outputs while the night light was disabled,
which is the normal state for users who want each monitor at its own value.

- `effectiveTempTarget` decides per output: a per-output override wins over
  the night light schedule, the schedule applies when there is no override,
  and `noTempTarget` means neither. `0` is "no override", not 0K.
- `applyGamma` composes the override on top of the profile ramp, treating the
  profile as measured at 6500K (D65), and drives outputs without a profile
  with the same target so both paths agree.
- `dms icc set-temp` help text and the Display Config slider description go
  back to describing an independent per-output temperature.

Covered by TestEffectiveTempTarget (override wins, schedule fallback, none)
and the updated TestProfileRampWithTemp (7000K cools, 5000K warms relative to
the 6500K reference).
@KIDult2226

Copy link
Copy Markdown
Author

Post-review updates now on the branch (033b96b3), on top of the fixes in the previous comment:

  • dms icc set-temp <output> <kelvin> (alias setTemp) sets a per-output value, and dms icc status reports it, so the override is reachable and checkable without the settings UI.
  • Per-output temperature is an independent target: it applies whether or not the night light schedule runs, and a per-output value wins over the schedule, so every display keeps its own white point and its own on/off state (0 = no override, and the range 3000-10000K is not limited by the night light's). It is composed on top of the profile ramp with the profile treated as measured at 6500K (D65). Decision logic lives in effectiveTempTarget.
  • Config attachment no longer depends on the control lifecycle: restoring the "output returned, re-establishing controls" branch made the registry handler establish the controls before the startup post ran, and that post returned early on controlsInitialized, so the configured profiles/temperatures were never attached (every output reported (none) until re-applied by hand). The startup path now loads the configuration first and only creates the controls when they are missing.
  • Hotplugged outputs pick up their configured profile and temperature as soon as the name is known, or when the control is created — the description claimed this but the code did not do it.

Tests added/updated: TestParseDescriptionMalformed (panics with slice bounds out of range [12:4] before the fix), TestManager_NeedsControlsCoversICCAndOutputTemps, TestProfileRampWithTemp, TestEffectiveTempTarget, TestManager_LoadConfiguredICCWhenControlsAlreadyExist, TestManager_AttachConfiguredICCForNamedOutput.

TZ=UTC go test ./... -> 51 packages ok, 0 fail. Verified on a real niri session with three calibrated outputs and per-output 7000K overrides: the ramps shift relative to 6500K as expected, profiles attach on restart, and no ICC/gamma warnings appear in the journal.

The profile row only showed the description, so there was no way to tell
which profile a display actually has applied (whitepoint, curve type, file
provenance) without leaving the settings page.

- `ICCStatus` carries the descriptive metadata: class, tone-curve kind (plus
  gamma or table size), vcgt channels/entries, white point chromaticity with
  a derived CCT and standard illuminant name, file size and mtime.
- `internal/icc` gains `WhitePointXY`, `WhitePointCCT` (McCamy),
  `WhitePointName` (D50/D65) and `TRCKind`, all covered by tests.
- Display Config's Color Profile row gets an info button that opens
  `ICCProfileInfoModal` with those fields and the profile path, alongside
  the existing browse/remove actions.
- `dms icc info` reports the white point as name/xy/CCT instead of raw XYZ.
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