200 lines
7.8 KiB
Python
200 lines
7.8 KiB
Python
# 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, 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
|