forked from BoboTiG/python-mss
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtools.py
More file actions
201 lines (166 loc) · 7.83 KB
/
Copy pathtools.py
File metadata and controls
201 lines (166 loc) · 7.83 KB
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
# This is part of the MSS Python's module.
# Source: https://github.com/BoboTiG/python-mss.
from __future__ import annotations
import os
import struct
import zlib
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pathlib import Path
_EDID_BLOCK_LEN = 128
_EDID_SERIAL_NUMBER_NOT_SET = 0 # The serial number field is unused
_EDID_MANUFACTURE_WEEK_YEAR_IS_MODEL = 0xFF # The year field is for the model year, not the year of manufacture.
_EDID_MANUFACTURE_WEEK_UNKNOWN = 0 # Only the year of manufacture, not the week, is known.
_EDID_YEAR_BASE = 1990 # The starting year for EDID years
_EDID_DESCR_OFFSETS = [0x48, 0x5A, 0x6C] # Display descriptor definition locations in the base block
_EDID_DESCR_LEN = 18
_EDID_DESCR_ZERO_LOCS = [0, 1, 2, 4] # Locations that must be 0 to mark a display descriptor definition
_EDID_DESCR_TAG_LOC = 3 # Location of display descriptor tag
_EDID_DESCR_TAG_SN = 0xFF # Descriptor is a string serial number
_EDID_DESCR_TAG_NAME = 0xFC # Descriptor is a string model name
_EDID_DESCR_STR_LOC = slice(5, 18) # Location of string in a display descriptor
def parse_edid(edid_data: bytes) -> dict:
"""Parse a monitor's EDID block.
Many fields are currently ignored, but may be added in the future.
If the EDID block cannot be parsed, this returns an empty dict.
The dict defines the following fields. Any of these may be
missing, if the EDID block does not define them.
- id_legacy (str): The legacy monitor ID, used in a number of
APIs. This is simply f"{manufacturer}{product_code:04X}".
Those subfields are not part of the returned dict, but are
nominally described as:
- manufacturer (str): A three-letter, all-uppercase code
specifying the manufacturer's legacy PnP ID. The registry is
managed by UEFI forum.
- product_code (int): A 16-bit product code. This is typically
displayed as four hex digits if rendered to a string.
- serial_number (str | int): Serial number of the monitor. EDID
block may provide this as an int, string, or both; the string
version is preferred.
- manufacture_week (int): The week, 1-54, of manufacture. This
may not be populated, even if the year is. (The way the weeks
are numbered is up to the manufacturer.)
- manufacture_year (int): The year, 1990 or later, of manufacture.
- model_year (int): The year, 1990 or later, that the model was
released. This is used if the manufacturer doesn't want to
update their EDID block each year; the manufacture_year field is
more common.
- display_name (str): The monitor's model. This is the preferred
value for display. If this field is not present, then id_legacy
is a distant second.
Currently, the serial_number and name fields are always in ASCII.
This function doesn't currently try to implement the
internationalization extensions defined in the VESA LS-EXT
standard. However, we may in the future.
We also don't currently inspect the extension blocks. The name
and serial number can be in CTA-861 extension blocks; I'll need to
see how common that is.
"""
# See also https://glenwing.github.io/docs/ for a lot of the relevant specs.
if len(edid_data) < _EDID_BLOCK_LEN:
# Too short
return {}
# Get the basic identification information from the start of the
# header. This has been part of EDID for a very long time.
block0 = edid_data[:_EDID_BLOCK_LEN]
if sum(block0) % 256 != 0:
# Checksum failure
return {}
(
header,
id_manufacturer_msb,
id_manufacturer_lsb,
id_product_code,
id_serial_number,
manufacture_week,
manufacture_year,
_edid_version,
_edid_revision,
_ext_count,
) = struct.unpack("<8s2BHIBBBB106xBx", block0)
if header != b"\x00\xff\xff\xff\xff\xff\xff\x00":
# Header incorrect
return {}
id_manufacturer_packed = id_manufacturer_msb << 8 | id_manufacturer_lsb
id_manufacturer = (
chr(((id_manufacturer_packed >> 10) % 32) + 64)
+ chr(((id_manufacturer_packed >> 5) % 32) + 64)
+ chr((id_manufacturer_packed % 32) + 64)
)
rv: dict[str, int | str] = {
"id_legacy": f"{id_manufacturer}{id_product_code:04X}",
}
if id_serial_number != _EDID_SERIAL_NUMBER_NOT_SET:
rv["serial_number"] = id_serial_number
if manufacture_week == _EDID_MANUFACTURE_WEEK_YEAR_IS_MODEL:
rv["model_year"] = manufacture_year + _EDID_YEAR_BASE
else:
if manufacture_week != _EDID_MANUFACTURE_WEEK_UNKNOWN:
rv["manufacture_week"] = manufacture_week
rv["manufacture_year"] = manufacture_year + _EDID_YEAR_BASE
# Read the display descriptor definitions, which can have more useful information.
for descr_offset in _EDID_DESCR_OFFSETS:
descr = block0[descr_offset : descr_offset + _EDID_DESCR_LEN]
if any(descr[field_offset] != 0 for field_offset in _EDID_DESCR_ZERO_LOCS):
# Not a display descriptor definition
continue
# Check the tag in descr[3].
# These strings are in ASCII, optionally terminated by \x0A then right-padded with \x20. In case a
# manufacturer got it a little wrong, we ignore everything after \x0A, and we also strip trailing \x20. (The
# spec requires the \x0A, but some manufacturers don't follow that.)
if descr[_EDID_DESCR_TAG_LOC] == _EDID_DESCR_TAG_SN: # Serial number
sn = descr[_EDID_DESCR_STR_LOC]
sn, _, _ = sn.partition(b"\x0a")
sn = sn.rstrip(b" ")
rv["serial_number"] = sn.decode("ascii", errors="replace")
elif descr[_EDID_DESCR_TAG_LOC] == _EDID_DESCR_TAG_NAME: # Name
name = descr[_EDID_DESCR_STR_LOC]
name, _, _ = name.partition(b"\x0a")
name = name.rstrip(b" ")
rv["display_name"] = name.decode("ascii", errors="replace")
return rv
def to_png(
data: bytes | memoryview[int], size: tuple[int, int], /, *, level: int = 6, output: Path | str | None = None
) -> bytes | None:
"""Dump data to a PNG file. If `output` is `None`, create no file but return
the whole PNG data.
:param bytes data: RGBRGB...RGB data.
:param tuple size: The (width, height) pair.
:param int level: PNG compression level (see :py:func:`zlib.compress()` for details).
:param str output: Output file name.
.. versionadded:: 3.0.0
.. versionchanged:: 3.2.0
Added the ``level`` keyword argument to control the PNG compression level.
"""
pack = struct.pack
crc32 = zlib.crc32
width, height = size
line = width * 3
png_filter = pack(">B", 0)
scanlines = b"".join([png_filter + data[y * line : y * line + line] for y in range(height)])
magic = pack(">8B", 137, 80, 78, 71, 13, 10, 26, 10)
# Header: size, marker, data, CRC32
ihdr = [b"", b"IHDR", b"", b""]
ihdr[2] = pack(">2I5B", width, height, 8, 2, 0, 0, 0)
ihdr[3] = pack(">I", crc32(b"".join(ihdr[1:3])) & 0xFFFFFFFF)
ihdr[0] = pack(">I", len(ihdr[2]))
# Data: size, marker, data, CRC32
idat = [b"", b"IDAT", zlib.compress(scanlines, level), b""]
idat[3] = pack(">I", crc32(b"".join(idat[1:3])) & 0xFFFFFFFF)
idat[0] = pack(">I", len(idat[2]))
# Footer: size, marker, None, CRC32
iend = [b"", b"IEND", b"", b""]
iend[3] = pack(">I", crc32(iend[1]) & 0xFFFFFFFF)
iend[0] = pack(">I", len(iend[2]))
if not output:
# Returns raw bytes of the whole PNG data
return magic + b"".join(ihdr + idat + iend)
with open(output, "wb") as fileh: # noqa: PTH123
fileh.write(magic)
fileh.write(b"".join(ihdr))
fileh.write(b"".join(idat))
fileh.write(b"".join(iend))
# Force write of file to disk
fileh.flush()
os.fsync(fileh.fileno())
return None