File: RealDisplaySizeMM.py

package info (click to toggle)
displaycal-py3 3.9.16-1
  • links: PTS
  • area: main
  • in suites: forky, sid, trixie
  • size: 29,120 kB
  • sloc: python: 115,777; javascript: 11,540; xml: 598; sh: 257; makefile: 173
file content (371 lines) | stat: -rw-r--r-- 11,330 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# -*- coding: utf-8 -*-

import os
import re
import subprocess
import sys
from typing import Dict, List, Union

from DisplayCAL import argyll
from DisplayCAL import localization as lang
from DisplayCAL.util_dbus import BUSTYPE_SESSION, DBusException, DBusObject
from DisplayCAL.util_x import get_display as _get_x_display


_displays = None


def GetXRandROutputXID(display_no=0):
    """Return the XRandR output X11 ID of a given display.

    Args:
        display_no (int): Display number.

    Returns:
        dict:
    """
    display = get_display(display_no)
    if display:
        return display.get("output", 0)
    return 0


def RealDisplaySizeMM(display_no=0):
    """Return the size (in mm) of a given display.

    Args:
        display_no (int): Display number.

    Returns:
        (int, int): The display size in mm.
    """
    if display := get_display(display_no):
        return display.get("size_mm", (0, 0))
    return 0, 0


class Display:
    """Store information about display."""

    def __init__(self):
        self.name = None
        """Display name."""
        self.description = None  # USED
        """Description of display or URL."""

        self.xrandr_name = None  # Generated from self.description

        self.pos = (0, 0)  # USED
        """Displays offset in pixel."""
        # self.sx = None
        # """Displays offset in pixels (X)."""
        # self.sy = None
        # """Displays offset in pixels (Y)."""

        self.size = (0, 0)  # USED
        """Displays width and height in pixels."""

        # WINDOWS / NT
        self.monid = None
        """Monitor ID."""
        self.prim = None
        """ NZ if primary display monitor."""

        # APPLE
        self.ddid = None

        # UNIX
        self.screen = None
        """X11 (possibly virtual) Screen."""
        self.uscreen = None
        """Underlying Xinerama/XRandr screen."""
        self.rscreen = None
        """Underlying RAMDAC screen (user override)."""
        self.icc_atom = None
        """ICC profile root/output atom for this display."""
        self.edid = None
        """128, 256 or 384 bytes of monitor EDID, NULL if none."""
        self.edid_len = None
        """128, 256 or 384."""

        # Xrandr stuff - output is connected 1:1 to a display
        self.crtc = None
        """Associated crtc."""
        self.output = None
        """Associated output."""
        self.icc_out_atom = None
        """ICC profile atom for this output."""

    def from_dispwin_data(self, display_info_line):
        """Parse from dispwin display list data.

        Args:
            display_info_line (str): The dispwin data line.
        """
        display_info_line = display_info_line.strip()
        description_data = re.findall(
            rb"[\s\d]+= '(?P<description>.*)'", display_info_line
        )
        dispwin_error_message = lang.getstr(
            "error.generic",
            (-1, "dispwin returns no usable data while enumerating displays."),
        )
        if not description_data:
            raise ValueError(dispwin_error_message)
        self.description = description_data[0]
        match = re.match(
            rb"[\s]*(?P<id>\d) = '(?P<name>.*) at (?P<x>[-\d]+), (?P<y>[-\d]+), "
            rb"width (?P<width>\d+), height (?P<height>\d+).*'",
            display_info_line,
        )
        if not match:
            raise ValueError(dispwin_error_message)
        groups_dict = match.groupdict()
        self.monid = int(groups_dict["id"])
        self.name = groups_dict["name"]
        # fix the name ending with "," for ArgyllCMS<3.3.0
        if self.name.endswith(b","):
            self.name = self.name[:-1]
        x = int(groups_dict["x"])
        y = int(groups_dict["y"])
        self.pos = (x, y)
        width = int(groups_dict["width"])
        height = int(groups_dict["height"])
        self.size = (width, height)

    def to_dict(self):
        """Return a dictionary.

        Returns:
            dict: The display data as dictionary, matching the previous implementation.
        """
        display_dict = {}
        if self.monid is not None:
            display_dict["monid"] = self.monid
        if self.description is not None:
            display_dict["description"] = self.description
        if self.name is not None:
            display_dict["name"] = self.name
        if self.pos is not None:
            display_dict["pos"] = self.pos
        if self.size is not None:
            display_dict["size"] = self.size

        return display_dict


def get_dispwin_output() -> bytes:
    """Return Argyll dispwin output.

    Returns:
        bytes: The dispwin output.
    """
    dispwin_path = argyll.get_argyll_util("dispwin")
    if dispwin_path is None:
        return b""

    if sys.platform == "win32":
        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE
    else:
        startupinfo = None

    # the invalid value of "-d0" is intentional,
    # we just want to get the information we want and close the dispwin,
    # if we don't supply "-d0" it will start showing color patches.
    p = subprocess.Popen(
        [dispwin_path, "-v", "-d0"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        startupinfo=startupinfo,
    )
    output, _ = p.communicate()
    return output


def _enumerate_displays() -> List[dict]:
    """Generate display information data from ArgyllCMS's dispwin.

    Returns:
        List[dict]: A list of dictionary containing display data.
    """
    displays = []
    has_display = False
    dispwin_output = get_dispwin_output()
    for line in dispwin_output.split(b"\n"):
        if has_display and b"-dweb[:port]" in line:
            break
        if has_display and b"=" in line:
            display = Display()
            display.from_dispwin_data(line)
            displays.append(display.to_dict())
        if not has_display and b"-d n" in line:
            has_display = True

    return displays


def enumerate_displays():
    """Enumerate and return a list of displays."""
    global _displays
    _displays = _enumerate_displays()

    if _displays is None:
        _displays = []

    for display in _displays:
        desc = display.get("description")
        if not desc:
            continue
        match = re.findall(
            rb"(.+?),? at (-?\d+), (-?\d+), width (\d+), height (\d+)", desc
        )
        if not len(match):
            continue

        # update xrandr_name from description
        if sys.platform not in ("darwin", "win32"):
            if (
                os.getenv("XDG_SESSION_TYPE") == "wayland"
                and "pos" in display
                and "size" in display
            ):
                x, y, w, h = display["pos"] + display["size"]
                wayland_display = get_wayland_display(x, y, w, h)
                if wayland_display:
                    display.update(wayland_display)
            else:
                xrandr_name = re.search(rb", Output (.+)", match[0][0])
                if xrandr_name:
                    display["xrandr_name"] = xrandr_name.group(1)
        desc = b"%s @ %s, %s, %sx%s" % match[0]
        display["description"] = desc
    return _displays


def get_display(display_no: int = 0) -> Union[None, Dict]:
    """Return display data for a given display number.

    Args:
        display_no (int): Display number.

    Returns:
        Dict: The display data.
    """
    if _displays is None:
        enumerate_displays()

    # Ensure _displays is not None after calling enumerate_displays
    if _displays is None:
        return

    # Translate from Argyll display index to enumerated display index using the
    # coordinates and dimensions
    from DisplayCAL.config import getcfg, is_virtual_display

    if is_virtual_display(display_no):
        return

    getcfg_displays = getcfg("displays")
    if len(getcfg_displays) < display_no:
        return

    argyll_display = getcfg_displays[display_no]

    if argyll_display.endswith(" [PRIMARY]"):
        argyll_display = " ".join(argyll_display.split(" ")[:-1])

    for display in _displays:
        desc = display.get("description")
        if not desc:
            continue
        geometry = b"".join(desc.split(b"@ ")[-1:])
        if argyll_display.endswith((b"@ " + geometry).decode("utf-8")):
            return display


def get_wayland_display(x, y, w, h):
    """Find matching Wayland display.

    Given x, y, width and height of display geometry, find matching Wayland display.
    """
    # Note that we apparently CANNOT use width and height because the reported
    # values from Argyll code and Mutter can be slightly different,
    # e.g. 3660x1941 from Mutter vs 3656x1941 from Argyll when HiDPI is enabled.
    # The xrandr output is also interesting in that case:
    # $ xrandr
    # Screen 0: minimum 320 x 200, current 3660 x 1941, maximum 8192 x 8192
    # XWAYLAND0 connected 3656x1941+0+0 (normal left inverted right x axis y axis) 0mm x 0mm,B950
    #   3656x1941     59.96*+
    # Note the apparent mismatch between first and 2nd/3rd line.
    # Look for active display at x, y instead.
    # Currently, only support for GNOME 3 / Mutter
    try:
        iface = DBusObject(
            BUSTYPE_SESSION,
            "org.gnome.Mutter.DisplayConfig",
            "/org/gnome/Mutter/DisplayConfig",
        )
        res = iface.get_resources()
    except DBusException:
        return

    if not res or len(res) < 2:
        return

    # See
    # https://github.com/GNOME/mutter/blob/master/src/org.gnome.Mutter.DisplayConfig.xml
    output_storage = None
    found = False
    crtcs = res[1]
    # Look for matching CRTC
    for crtc in crtcs:
        if len(crtc) < 7 or crtc[2:4] != (x, y) or crtc[6] == -1:
            continue

        # Found our CRTC
        crtc_id = crtc[0]
        # Look for matching output
        outputs = res[2]
        for output in outputs:
            if len(output) < 2:
                continue
            if output[2] == crtc_id:
                # Found our output
                found = True
                output_storage = output
                break
        if found:
            break

    if found and output_storage is not None and len(output_storage) > 7:
        properties = output_storage[7]
        wayland_display = {"xrandr_name": output_storage[4]}
        raw_edid = properties.get("edid", ())
        edid = b"".join(v.to_bytes(1, "big") for v in raw_edid)

        if edid:
            wayland_display["edid"] = edid
        w_mm = properties.get("width-mm")
        h_mm = properties.get("height-mm")
        if w_mm and h_mm:
            wayland_display["size_mm"] = (w_mm, h_mm)
        return wayland_display


def get_x_display(display_no=0):
    if display := get_display(display_no):
        if name := display.get("name"):
            return _get_x_display(name)


def get_x_icc_profile_atom_id(display_no=0):
    if display := get_display(display_no):
        return display.get("icc_profile_atom_id")


def get_x_icc_profile_output_atom_id(display_no=0):
    if display := get_display(display_no):
        return display.get("icc_profile_output_atom_id")