Skip to content

Commit 9ff7260

Browse files
authored
Merge pull request #961 from eoyilmaz/960-colorimeter-correction-web-check-finds-nothing-for-apple-displays
[#960] Fix colorimeter correction web-check finding nothing on Apple built-in displays
2 parents c16f11c + 832dfde commit 9ff7260

3 files changed

Lines changed: 143 additions & 2 deletions

File tree

DisplayCAL/colorimeter_correction.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ def build_web_check_params(worker: Worker) -> dict:
191191
``MainFrame.colorimeter_correction_web_handler`` (the ``http_request``
192192
call itself, and its progress reporting, stay with the caller).
193193
194+
Uses ``worker.get_display_generic_name()`` (falling back to
195+
``get_display_name()``) for the ``display`` param: the online database is
196+
keyed by generic monitor names ("Color LCD"), and for Apple built-in
197+
displays ``get_display_name()`` substitutes the machine model id
198+
("MacBookPro18,1"), which the database has no entries for.
199+
194200
Args:
195201
worker: The running :class:`DisplayCAL.worker.Worker`, used to derive
196202
the current display/instrument.
@@ -204,7 +210,9 @@ def build_web_check_params(worker: Worker) -> dict:
204210
"get": True,
205211
"type": filetype,
206212
"manufacturer_id": worker.get_display_edid().get("manufacturer_id", ""),
207-
"display": worker.get_display_name(False, True) or "Unknown",
213+
"display": worker.get_display_generic_name()
214+
or worker.get_display_name(False, True)
215+
or "Unknown",
208216
"instrument": worker.get_instrument_name() or "Unknown",
209217
"json": 1,
210218
}
@@ -1570,7 +1578,14 @@ def resolve_colorimeter_correction_selection(
15701578
if ccmx_cfg[0] == "AUTO":
15711579
if len(ccmx_cfg) < 2:
15721580
ccmx_cfg.append("")
1573-
display_name = worker.get_display_name(False, True, False)
1581+
# Local CCMX/CCSS files are keyed by their own generic DISPLAY field
1582+
# ("Color LCD"), never by an Apple machine model id, so matching
1583+
# must use the generic name too (see get_display_generic_name()'s
1584+
# docstring / build_web_check_params, which need the same fix for
1585+
# the same reason).
1586+
display_name = worker.get_display_generic_name() or worker.get_display_name(
1587+
False, True, False
1588+
)
15741589
if worker.instrument_supports_ccss():
15751590
# Prefer CCSS
15761591
ccmx_cfg[1] = catalog.mapping.get(f"\0{display_name}", "")

DisplayCAL/worker.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2613,6 +2613,7 @@ def __init__(self, owner=None):
26132613
self.display_edid = []
26142614
self.display_manufacturers = []
26152615
self.display_names = []
2616+
self.display_generic_names = []
26162617
self.display_rects = []
26172618
self.displays = []
26182619
self.instruments = []
@@ -4569,6 +4570,7 @@ def clear_argyll_info(self):
45694570
self.display_edid = []
45704571
self.display_manufacturers = []
45714572
self.display_names = []
4573+
self.display_generic_names = []
45724574
self.display_rects = []
45734575
self.displays = []
45744576
self.instruments = []
@@ -6456,6 +6458,7 @@ def enumerate_displays_and_ports(
64566458
self.display_edid = []
64576459
self.display_manufacturers = []
64586460
self.display_names = []
6461+
self.display_generic_names = []
64596462
if sys.platform == "win32":
64606463
# The ordering will work as long
64616464
# as Argyll continues using
@@ -6475,10 +6478,19 @@ def enumerate_displays_and_ports(
64756478
display_manufacturer = "Google"
64766479
self.display_manufacturers.append(display_manufacturer)
64776480
self.display_names.append(display.split(":", 1)[1].strip())
6481+
self.display_generic_names.append(
6482+
display.split(":", 1)[1].strip()
6483+
)
64786484
continue
64796485
display_name = split_display_name(display)
64806486
# Make sure we have nice descriptions
64816487
desc = []
6488+
# Generic (non-model-id-overridden) description, e.g. for
6489+
# matching against the online colorimeter correction
6490+
# database, which is keyed by generic monitor names
6491+
# ("Color LCD") rather than the machine model id
6492+
# ("MacBookPro18,1").
6493+
generic_desc = []
64826494
if sys.platform == "win32" and i < len(monitors):
64836495
# Get monitor description using win32api
64846496
device = util_win.get_active_display_device(
@@ -6502,6 +6514,7 @@ def enumerate_displays_and_ports(
65026514
)
65036515
if is_primary:
65046516
display += " [PRIMARY]"
6517+
original_display = display
65056518
# Get monitor descriptions from EDID
65066519
try:
65076520
# Important: display_name must be given for get_edid
@@ -6524,6 +6537,7 @@ def enumerate_displays_and_ports(
65246537
"monitor_name",
65256538
edid.get("ascii", str(edid["product_id"] or "")),
65266539
)
6540+
generic_monitor = monitor
65276541
if (
65286542
monitor in ("Color LCD", "iMac")
65296543
and edid["manufacturer_id"] == "APP"
@@ -6538,6 +6552,10 @@ def enumerate_displays_and_ports(
65386552
edid["monitor_name"] = monitor
65396553
if monitor and monitor not in "".join(desc):
65406554
desc = [monitor]
6555+
if generic_monitor and generic_monitor not in "".join(
6556+
generic_desc
6557+
):
6558+
generic_desc = [generic_monitor]
65416559
else:
65426560
manufacturer = []
65436561
if sys.platform == "darwin" and i < len(self.display_rects):
@@ -6546,6 +6564,7 @@ def enumerate_displays_and_ports(
65466564
rect.width, rect.height
65476565
)
65486566
if sp_name:
6567+
generic_desc = [sp_name]
65496568
if sp_name in ("Color LCD", "iMac"):
65506569
model_id = get_model_id()
65516570
if model_id:
@@ -6555,34 +6574,48 @@ def enumerate_displays_and_ports(
65556574
# Only replace the description if it not already
65566575
# contains the monitor model
65576576
display = " @".join([" ".join(desc), display.split("@")[-1]])
6577+
if generic_desc and generic_desc[-1] not in original_display:
6578+
generic_display = " @".join(
6579+
[" ".join(generic_desc), original_display.split("@")[-1]]
6580+
)
6581+
else:
6582+
generic_display = original_display
65586583
displays[i] = display
65596584
self.display_manufacturers.append(" ".join(manufacturer))
65606585
self.display_names.append(split_display_name(display))
6586+
self.display_generic_names.append(
6587+
split_display_name(generic_display)
6588+
)
65616589
if self.argyll_version >= [1, 4, 0]:
65626590
displays.append("Web @ localhost")
65636591
self.display_edid.append({})
65646592
self.display_manufacturers.append("")
65656593
self.display_names.append("Web")
6594+
self.display_generic_names.append("Web")
65666595
if self.argyll_version >= [1, 6, 0]:
65676596
displays.append("madVR")
65686597
self.display_edid.append({})
65696598
self.display_manufacturers.append("")
65706599
self.display_names.append("madVR")
6600+
self.display_generic_names.append("madVR")
65716601
# Prisma (via DisplayCAL)
65726602
displays.append("Prisma")
65736603
self.display_edid.append({})
65746604
self.display_manufacturers.append("Q, Inc")
65756605
self.display_names.append("Prisma")
6606+
self.display_generic_names.append("Prisma")
65766607
# Resolve
65776608
displays.append("Resolve")
65786609
self.display_edid.append({})
65796610
self.display_manufacturers.append("DaVinci")
65806611
self.display_names.append("Resolve")
6612+
self.display_generic_names.append("Resolve")
65816613
# Untethered
65826614
displays.append("Untethered")
65836615
self.display_edid.append({})
65846616
self.display_manufacturers.append("")
65856617
self.display_names.append("Untethered")
6618+
self.display_generic_names.append("Untethered")
65866619
# -
65876620
self.displays = displays
65886621
setcfg("displays", displays)
@@ -9795,6 +9828,25 @@ def get_display_name(
97959828
return " ".join(display)
97969829
return ""
97979830

9831+
def get_display_generic_name(self):
9832+
"""Return the generic (non-model-id-overridden) name of the current display.
9833+
9834+
Unlike :meth:`get_display_name`, this never substitutes an Apple
9835+
machine model id (e.g. "MacBookPro18,1") for a built-in display's
9836+
generic monitor name (e.g. "Color LCD"), which makes it suitable for
9837+
matching against the online colorimeter correction database (see
9838+
:func:`DisplayCAL.colorimeter_correction.build_web_check_params`) -
9839+
that database is keyed by generic monitor/model names, not by the
9840+
machine-specific model id.
9841+
9842+
Returns:
9843+
str: The display's generic name, or "" if unavailable.
9844+
"""
9845+
n = getcfg("display.number") - 1
9846+
if 0 <= n < len(self.display_generic_names):
9847+
return self.display_generic_names[n]
9848+
return ""
9849+
97989850
def get_display_name_short(self, prepend_manufacturer=False, prefer_edid=False):
97999851
"""Return shortened name of configured display (if possible).
98009852

tests/test_colorimeter_correction.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,22 @@
88
slice). No display or QApplication is needed.
99
"""
1010

11+
import os
1112
from hashlib import md5
1213
from unittest.mock import MagicMock
1314

1415
from DisplayCAL import colorimeter_correction as cc
16+
from DisplayCAL import config
17+
18+
# A CCSS fixture whose DISPLAY field ("DELL UP2516D") matches a display's
19+
# *generic* name, never a machine-specific model id - used to prove Auto
20+
# resolution matches against the generic name, not the raw display name.
21+
_CCSS_FIXTURE = os.path.join(
22+
os.path.dirname(__file__),
23+
"data",
24+
"icc",
25+
"Dell, DELL UP2516D (i1 Pro 2) 08.2020.ccss",
26+
)
1527

1628
# A minimal CCMX-shaped blob with the DISPLAY line the injector anchors on.
1729
BASE = b'CCMX\n\nDESCRIPTOR "test"\nDISPLAY "LCD Monitor"\nCOLOR_REP "XYZ"\n'
@@ -185,6 +197,7 @@ def test_uses_worker_state(self):
185197
worker = MagicMock()
186198
worker.instrument_supports_ccss.return_value = True
187199
worker.get_display_edid.return_value = {"manufacturer_id": "DEL"}
200+
worker.get_display_generic_name.return_value = "Dell U2413"
188201
worker.get_display_name.return_value = "Dell U2413"
189202
worker.get_instrument_name.return_value = "i1 Pro"
190203
params = cc.build_web_check_params(worker)
@@ -197,10 +210,21 @@ def test_uses_worker_state(self):
197210
"json": 1,
198211
}
199212

213+
def test_falls_back_to_display_name_when_no_generic_name(self):
214+
worker = MagicMock()
215+
worker.instrument_supports_ccss.return_value = True
216+
worker.get_display_edid.return_value = {"manufacturer_id": "APP"}
217+
worker.get_display_generic_name.return_value = ""
218+
worker.get_display_name.return_value = "MacBookPro18,1"
219+
worker.get_instrument_name.return_value = "i1 DisplayPro"
220+
params = cc.build_web_check_params(worker)
221+
assert params["display"] == "MacBookPro18,1"
222+
200223
def test_falls_back_to_ccmx_only_and_unknown(self):
201224
worker = MagicMock()
202225
worker.instrument_supports_ccss.return_value = False
203226
worker.get_display_edid.return_value = {}
227+
worker.get_display_generic_name.return_value = ""
204228
worker.get_display_name.return_value = None
205229
worker.get_instrument_name.return_value = None
206230
params = cc.build_web_check_params(worker)
@@ -209,6 +233,56 @@ def test_falls_back_to_ccmx_only_and_unknown(self):
209233
assert params["instrument"] == "Unknown"
210234

211235

236+
class TestResolveColorimeterCorrectionSelectionAuto:
237+
""""Auto" must match local CCMX/CCSS files by generic display name.
238+
239+
Regression test: local corrections are keyed by their own DISPLAY field
240+
(a generic monitor name, e.g. "DELL UP2516D"), never by a machine model
241+
id, so "Auto" resolution has to prefer ``get_display_generic_name()``
242+
the same way ``build_web_check_params`` does - otherwise, on a display
243+
where ``get_display_name()`` diverges from the generic name (e.g. an
244+
Apple built-in display, see ``get_display_generic_name``'s docstring),
245+
"Auto" can never find a matching correction even when one is present on
246+
disk.
247+
"""
248+
249+
def _worker(self):
250+
worker = MagicMock()
251+
worker.instrument_supports_ccss.return_value = True
252+
worker.instrument_can_use_ccxx.return_value = True
253+
worker.get_instrument_name.return_value = "i1 Pro 2"
254+
worker.get_instrument_measurement_modes.return_value = {"auto": None}
255+
# Deliberately wrong/overridden, mirroring get_display_name()'s
256+
# Apple model-id substitution - Auto must not use this value.
257+
worker.get_display_name.return_value = "MacBookPro18,1"
258+
worker.get_display_generic_name.return_value = "DELL UP2516D"
259+
return worker
260+
261+
def _catalog(self):
262+
catalog = cc.ColorimeterCorrectionCatalog()
263+
catalog.cached_paths = [_CCSS_FIXTURE]
264+
return catalog
265+
266+
def test_auto_resolves_using_generic_display_name(self):
267+
config.setcfg("measurement_mode", "auto")
268+
config.setcfg("colorimeter_correction_matrix_file", "AUTO:")
269+
result = cc.resolve_colorimeter_correction_selection(
270+
self._catalog(), self._worker()
271+
)
272+
assert result.use_ccmx
273+
assert result.ccmx[1] == _CCSS_FIXTURE
274+
assert result.items[1] != cc.lang.getstr("auto")
275+
276+
def test_auto_stays_none_when_generic_name_does_not_match(self):
277+
config.setcfg("measurement_mode", "auto")
278+
config.setcfg("colorimeter_correction_matrix_file", "AUTO:")
279+
worker = self._worker()
280+
worker.get_display_generic_name.return_value = "Some Other Display"
281+
result = cc.resolve_colorimeter_correction_selection(self._catalog(), worker)
282+
assert not result.use_ccmx
283+
assert result.ccmx[1] == ""
284+
285+
212286
class TestValidateUploadOriginator:
213287
def test_argyll_originator_accepted(self):
214288
assert cc.validate_upload_originator(

0 commit comments

Comments
 (0)