Created
June 1, 2026 02:42
-
-
Save onyx-nxt/1462e450c4ea0f7b1ccee8cd302001d2 to your computer and use it in GitHub Desktop.
Adds useful metadata and color information to PNG screenshots. Meant to run right after it was taken.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import struct | |
| import sys | |
| import os | |
| import re | |
| import zlib | |
| from datetime import datetime, timezone | |
| import ctypes | |
| import ctypes.wintypes as wintypes | |
| # Load DLLs | |
| user32 = ctypes.WinDLL("user32", use_last_error=True) | |
| kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) | |
| dwmapi = ctypes.WinDLL("dwmapi") | |
| PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 | |
| DWMWA_CLOAKED = 14 | |
| QDC_ONLY_ACTIVE_PATHS = 0x00000002 | |
| DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME = 1 | |
| DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO = 9 | |
| MONITOR_DEFAULTTONEAREST = 0x00000002 | |
| class GUITHREADINFO(ctypes.Structure): | |
| _fields_ = [ | |
| ("cbSize", wintypes.DWORD), | |
| ("flags", wintypes.DWORD), | |
| ("hwndActive", wintypes.HWND), | |
| ("hwndFocus", wintypes.HWND), | |
| ("hwndCapture", wintypes.HWND), | |
| ("hwndMenuOwner", wintypes.HWND), | |
| ("hwndMoveSize", wintypes.HWND), | |
| ("hwndCaret", wintypes.HWND), | |
| ("rcCaret", wintypes.RECT) | |
| ] | |
| class MONITORINFOEX(ctypes.Structure): | |
| _fields_ = [ | |
| ("cbSize", wintypes.DWORD), | |
| ("rcMonitor", wintypes.RECT), | |
| ("rcWork", wintypes.RECT), | |
| ("dwFlags", wintypes.DWORD), | |
| ("szDevice", wintypes.WCHAR * 32), | |
| ] | |
| class LUID(ctypes.Structure): | |
| _fields_ = [("LowPart", wintypes.DWORD), ("HighPart", wintypes.LONG)] | |
| class DISPLAYCONFIG_RATIONAL(ctypes.Structure): | |
| _fields_ = [("Numerator", ctypes.c_uint32), ("Denominator", ctypes.c_uint32)] | |
| class DISPLAYCONFIG_PATH_SOURCE_INFO(ctypes.Structure): | |
| _fields_ = [ | |
| ("adapterId", LUID), | |
| ("id", ctypes.c_uint32), | |
| ("modeInfoIdx", ctypes.c_uint32), | |
| ("statusFlags", ctypes.c_uint32), | |
| ] | |
| class DISPLAYCONFIG_PATH_TARGET_INFO(ctypes.Structure): | |
| _fields_ = [ | |
| ("adapterId", LUID), | |
| ("id", ctypes.c_uint32), | |
| ("modeInfoIdx", ctypes.c_uint32), | |
| ("outputTechnology", ctypes.c_uint32), | |
| ("rotation", ctypes.c_uint32), | |
| ("scaling", ctypes.c_uint32), | |
| ("refreshRate", DISPLAYCONFIG_RATIONAL), | |
| ("scanLineOrdering", ctypes.c_uint32), | |
| ("targetAvailable", wintypes.BOOL), | |
| ("statusFlags", ctypes.c_uint32), | |
| ] | |
| class DISPLAYCONFIG_PATH_INFO(ctypes.Structure): | |
| _fields_ = [ | |
| ("sourceInfo", DISPLAYCONFIG_PATH_SOURCE_INFO), | |
| ("targetInfo", DISPLAYCONFIG_PATH_TARGET_INFO), | |
| ("flags", ctypes.c_uint32), | |
| ] | |
| class DISPLAYCONFIG_MODE_INFO(ctypes.Structure): | |
| # Union of target/source/desktop modes; largest member is 48 bytes, header is 16 | |
| _fields_ = [("_opaque", ctypes.c_byte * 64)] | |
| class DISPLAYCONFIG_DEVICE_INFO_HEADER(ctypes.Structure): | |
| _fields_ = [ | |
| ("type", ctypes.c_uint32), | |
| ("size", ctypes.c_uint32), | |
| ("adapterId", LUID), | |
| ("id", ctypes.c_uint32), | |
| ] | |
| class DISPLAYCONFIG_ADVANCED_COLOR_INFO(ctypes.Structure): | |
| _fields_ = [ | |
| ("header", DISPLAYCONFIG_DEVICE_INFO_HEADER), | |
| ("value", ctypes.c_uint32), # bitfield: bit0=supported, bit1=enabled | |
| ("colorEncoding", ctypes.c_uint32), | |
| ("bitsPerColorChannel", ctypes.c_uint32), | |
| ] | |
| class DISPLAYCONFIG_SOURCE_DEVICE_NAME(ctypes.Structure): | |
| _fields_ = [ | |
| ("header", DISPLAYCONFIG_DEVICE_INFO_HEADER), | |
| ("viewGdiDeviceName", wintypes.WCHAR * 32), | |
| ] | |
| def is_window_cloaked(hwnd): | |
| cloaked = ctypes.c_int(0) | |
| dwmapi.DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, ctypes.byref(cloaked), ctypes.sizeof(cloaked)) | |
| return cloaked.value != 0 | |
| def get_process_name_from_pid(pid): | |
| h_process = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid) | |
| if not h_process: | |
| return None | |
| try: | |
| exe_path = ctypes.create_unicode_buffer(260) | |
| size = wintypes.DWORD(260) | |
| if kernel32.QueryFullProcessImageNameW(h_process, 0, exe_path, ctypes.byref(size)): | |
| return os.path.basename(exe_path.value) | |
| finally: | |
| kernel32.CloseHandle(h_process) | |
| return None | |
| def get_active_process_name(): | |
| hwnd = user32.GetForegroundWindow() | |
| if not hwnd or not user32.IsWindowVisible(hwnd) or is_window_cloaked(hwnd): | |
| return None | |
| length = user32.GetWindowTextLengthW(hwnd) | |
| if length == 0: | |
| return None | |
| class_name = ctypes.create_unicode_buffer(256) | |
| user32.GetClassNameW(hwnd, class_name, 256) | |
| # Progman/WorkerW = Wallpaper/Icons. Shell_TrayWnd = Taskbar. | |
| if class_name.value in ["Progman", "WorkerW", "Shell_TrayWnd"]: | |
| return None | |
| pid = wintypes.DWORD() | |
| user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid)) | |
| name = get_process_name_from_pid(pid.value) | |
| if not name: | |
| return None | |
| # UWP Handling | |
| if name and name.lower() == "applicationframehost.exe": | |
| gui_info = GUITHREADINFO() | |
| gui_info.cbSize = ctypes.sizeof(GUITHREADINFO) | |
| user32.GetGUIThreadInfo(0, ctypes.byref(gui_info)) | |
| if gui_info.hwndFocus: | |
| child_pid = wintypes.DWORD() | |
| user32.GetWindowThreadProcessId(gui_info.hwndFocus, ctypes.byref(child_pid)) | |
| uwp_name = get_process_name_from_pid(child_pid.value) | |
| if uwp_name: | |
| name = uwp_name | |
| # Don't care about desktop | |
| if name.lower() == "explorer.exe": | |
| if class_name.value == "CabinetWClass": | |
| return "Explorer" | |
| return None | |
| return os.path.splitext(name)[0] | |
| def is_hdr_enabled(): | |
| user32 = ctypes.windll.user32 | |
| # Resolve active monitor device name (e.g. "\\.\DISPLAY2") | |
| hwnd = user32.GetForegroundWindow() | |
| hmonitor = user32.MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) | |
| mon_info = MONITORINFOEX() | |
| mon_info.cbSize = ctypes.sizeof(MONITORINFOEX) | |
| if not user32.GetMonitorInfoW(hmonitor, ctypes.byref(mon_info)): | |
| return False | |
| active_device = mon_info.szDevice | |
| num_paths = ctypes.c_uint32(0) | |
| num_modes = ctypes.c_uint32(0) | |
| ret = user32.GetDisplayConfigBufferSizes( | |
| QDC_ONLY_ACTIVE_PATHS, ctypes.byref(num_paths), ctypes.byref(num_modes) | |
| ) | |
| if ret != 0: | |
| print(f"GetDisplayConfigBufferSizes failed: {ret}") | |
| return False | |
| paths = (DISPLAYCONFIG_PATH_INFO * num_paths.value)() | |
| modes = (DISPLAYCONFIG_MODE_INFO * num_modes.value)() | |
| ret = user32.QueryDisplayConfig( | |
| QDC_ONLY_ACTIVE_PATHS, | |
| ctypes.byref(num_paths), paths, | |
| ctypes.byref(num_modes), modes, | |
| None, | |
| ) | |
| if ret != 0: | |
| print(f"QueryDisplayConfig failed: {ret}") | |
| return False | |
| for i in range(num_paths.value): | |
| # Match path to active monitor via its GDI source device name | |
| src_name = DISPLAYCONFIG_SOURCE_DEVICE_NAME() | |
| src_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME | |
| src_name.header.size = ctypes.sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME) | |
| src_name.header.adapterId = paths[i].sourceInfo.adapterId | |
| src_name.header.id = paths[i].sourceInfo.id | |
| if user32.DisplayConfigGetDeviceInfo(ctypes.byref(src_name.header)) != 0: | |
| continue | |
| if src_name.viewGdiDeviceName != active_device: | |
| continue | |
| target = paths[i].targetInfo | |
| color_info = DISPLAYCONFIG_ADVANCED_COLOR_INFO() | |
| color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO | |
| color_info.header.size = ctypes.sizeof(DISPLAYCONFIG_ADVANCED_COLOR_INFO) | |
| color_info.header.adapterId = target.adapterId | |
| color_info.header.id = target.id | |
| if user32.DisplayConfigGetDeviceInfo(ctypes.byref(color_info.header)) != 0: | |
| return False | |
| enabled = bool(color_info.value & 0x2) | |
| supported = bool(color_info.value & 0x1) | |
| print(f"Display {i} ({active_device}): HDR {'enabled' if enabled else 'disabled'} (supported={supported})") | |
| return enabled | |
| return False | |
| def get_active_icc_path(): | |
| gdi32 = ctypes.windll.gdi32 | |
| user32 = ctypes.windll.user32 | |
| hwnd = user32.GetForegroundWindow() | |
| hmonitor = user32.MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) | |
| info = MONITORINFOEX() | |
| info.cbSize = ctypes.sizeof(MONITORINFOEX) | |
| if not user32.GetMonitorInfoW(hmonitor, ctypes.byref(info)): | |
| return None | |
| device_name = info.szDevice | |
| hdc = gdi32.CreateDCW(device_name, device_name, None, None) | |
| if not hdc: | |
| return None | |
| try: | |
| size = wintypes.DWORD(0) | |
| gdi32.GetICMProfileW(hdc, ctypes.byref(size), None) | |
| if size.value == 0: | |
| return None | |
| buffer = ctypes.create_unicode_buffer(size.value) | |
| if gdi32.GetICMProfileW(hdc, ctypes.byref(size), buffer): | |
| return buffer.value | |
| return None | |
| finally: | |
| gdi32.DeleteDC(hdc) | |
| def read_xyz_tag(file_handle, offset): | |
| file_handle.seek(offset) | |
| data = file_handle.read(20) | |
| if len(data) < 20: | |
| return None | |
| signature, _, x_val, y_val, z_val = struct.unpack('>4sIiii', data) | |
| if signature != b'XYZ ': | |
| return None | |
| return (x_val / 65536.0, y_val / 65536.0, z_val / 65536.0) | |
| def calculate_xy_chromaticity(x_val, y_val, z_val): | |
| total = x_val + y_val + z_val | |
| if total == 0: | |
| return 0.0, 0.0 | |
| return (x_val / total, y_val / total) | |
| def extract_chromaticities(profile_path): | |
| if not profile_path or not os.path.exists(profile_path): | |
| return None | |
| chromaticities = {} | |
| with open(profile_path, 'rb') as file_handle: | |
| file_handle.seek(128) | |
| tag_count_data = file_handle.read(4) | |
| if len(tag_count_data) < 4: | |
| return None | |
| tag_count = struct.unpack('>I', tag_count_data)[0] | |
| tags = {} | |
| for _ in range(tag_count): | |
| tag_data = file_handle.read(12) | |
| if len(tag_data) < 12: | |
| break | |
| signature, offset, _ = struct.unpack('>4sII', tag_data) | |
| tags[signature] = offset | |
| target_tags = { | |
| b'rXYZ': 'red', | |
| b'gXYZ': 'green', | |
| b'bXYZ': 'blue', | |
| b'wtpt': 'white_point' | |
| } | |
| for byte_sig, string_name in target_tags.items(): | |
| if byte_sig in tags: | |
| xyz_values = read_xyz_tag(file_handle, tags[byte_sig]) | |
| if xyz_values: | |
| xy_values = calculate_xy_chromaticity(*xyz_values) | |
| chromaticities[string_name] = { | |
| 'XYZ': xyz_values, | |
| 'xy': xy_values | |
| } | |
| return chromaticities | |
| # Determine captured process name | |
| process_name = get_active_process_name() | |
| print("Active process:", process_name) | |
| profile_path = get_active_icc_path() | |
| if profile_path: | |
| print(f"Active Profile: {profile_path}") | |
| chromaticity_data = extract_chromaticities(profile_path) | |
| if chromaticity_data: | |
| for color_name, data in chromaticity_data.items(): | |
| xyz_vals = data['XYZ'] | |
| xy_vals = data['xy'] | |
| print(f"{color_name.capitalize()}:") | |
| print(f" XYZ: ({xyz_vals[0]:.4f}, {xyz_vals[1]:.4f}, {xyz_vals[2]:.4f})") | |
| print(f" xy: ({xy_vals[0]:.4f}, {xy_vals[1]:.4f})") | |
| else: | |
| print("Could not extract chromaticity data. Ensure the profile is a matrix/TRC type.") | |
| else: | |
| print("No active ICC profile found.") | |
| hdr_enabled = is_hdr_enabled() | |
| def to_fixed(v): return int(round(v * 100000)) | |
| # cHRM values | |
| chrm_payload = struct.pack(">8I", | |
| to_fixed(chromaticity_data['white_point']['xy'][0]), to_fixed(chromaticity_data['white_point']['xy'][1]), | |
| to_fixed(chromaticity_data['red']['xy'][0]), to_fixed(chromaticity_data['red']['xy'][1]), | |
| to_fixed(chromaticity_data['green']['xy'][0]), to_fixed(chromaticity_data['green']['xy'][1]), | |
| to_fixed(chromaticity_data['blue']['xy'][0]), to_fixed(chromaticity_data['blue']['xy'][1]) | |
| ) | |
| def png_crc32(data: bytes) -> int: | |
| crc = 0xFFFFFFFF | |
| for b in data: | |
| crc ^= b | |
| for _ in range(8): | |
| crc = (crc >> 1) ^ (0xEDB88320 if crc & 1 else 0) | |
| return crc ^ 0xFFFFFFFF | |
| def make_chunk(type_: bytes, payload: bytes) -> bytes: | |
| crc = png_crc32(type_ + payload) | |
| return struct.pack(">I", len(payload)) + type_ + payload + struct.pack(">I", crc) | |
| def make_chrm_chunk(): return make_chunk(b"cHRM", chrm_payload) | |
| def make_iccp_chunk(path: str) -> bytes: | |
| with open(path, "rb") as f: | |
| profile_data = f.read() | |
| name = os.path.splitext(os.path.basename(path))[0][:79].encode("latin-1") | |
| payload = name + b"\x00\x00" + zlib.compress(profile_data) | |
| return make_chunk(b"iCCP", payload) | |
| def make_time_chunk(dt: datetime): | |
| dt = dt.astimezone(timezone.utc) | |
| payload = struct.pack(">HBBBBB", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) | |
| return make_chunk(b"tIME", payload) | |
| def make_text_chunk(keyword: str, text: str): | |
| payload = keyword.encode("latin-1") + b"\0" + text.encode("latin-1") | |
| return make_chunk(b"tEXt", payload) | |
| def insert_before_first_idat(png: bytearray, chunk: bytes): | |
| pos = 8 | |
| while pos < len(png) - 12: | |
| length = struct.unpack_from(">I", png, pos)[0] | |
| ctype = png[pos+4:pos+8] | |
| if ctype == b"IDAT": | |
| png[pos:pos] = chunk | |
| return | |
| pos += 12 + length | |
| png[-12:-12] = chunk | |
| # Main Logic | |
| if len(sys.argv) < 2: | |
| print('Usage: python script.py "input.png"') | |
| sys.exit(1) | |
| input_path = sys.argv[1].strip('"').replace("\\\\", "\\") | |
| if not os.path.isfile(input_path): | |
| print(f"File not found: {input_path}") | |
| sys.exit(1) | |
| norm_input = os.path.normcase(os.path.normpath(input_path)) | |
| found_obj = None | |
| # Read PNG Data | |
| with open(input_path, "rb") as f: | |
| png = bytearray(f.read()) | |
| # Strip existing metadata | |
| pos = 8 | |
| while pos < len(png) - 12: | |
| length = struct.unpack_from(">I", png, pos)[0] | |
| ctype = png[pos+4:pos+8] | |
| if ctype in (b"iCCP", b"cHRM", b"tEXt", b"tIME"): | |
| del png[pos:pos + 12 + length] | |
| continue | |
| pos += 12 + length | |
| # Determine DateTime | |
| dt = None | |
| basename = os.path.basename(input_path) | |
| print("Parsing filename for data...") | |
| # Supported formats: Screenshot YYYY-MM-DD HHMMSS.png | |
| pattern = r'^Screenshot\s+(?P<year>\d{4})-(?P<mo>\d{2})-(?P<day>\d{2})\s+(?P<h>\d{2})(?P<mi>\d{2})(?P<s>\d{2})' | |
| match = re.match(pattern, basename) | |
| local_tz = datetime.now().astimezone().tzinfo | |
| if match: | |
| year = int(match.group('year')) | |
| mo = int(match.group('mo')) | |
| day = int(match.group('day')) | |
| hour = int(match.group('h')) | |
| minute = int(match.group('mi')) | |
| second = int(match.group('s')) | |
| # Interpret the filename timestamp as local time, then convert to UTC | |
| dt_local = datetime(year, mo, day, hour, minute, second).replace(tzinfo=local_tz) | |
| dt = dt_local.astimezone(timezone.utc) | |
| print(f"Parsed date from filename (UTC): {dt.isoformat()}") | |
| else: | |
| print("Fallback to file modified time.") | |
| # Use file mtime in local timezone and convert to UTC | |
| dt = datetime.fromtimestamp(os.path.getmtime(input_path), tz=local_tz).astimezone(timezone.utc) | |
| # Injection | |
| if not hdr_enabled: | |
| if profile_path: | |
| insert_before_first_idat(png, make_iccp_chunk(profile_path)) | |
| insert_before_first_idat(png, make_chrm_chunk()) | |
| insert_before_first_idat(png, make_time_chunk(dt)) | |
| if process_name: | |
| insert_before_first_idat(png, make_text_chunk("Source", process_name)) | |
| # Overwrite input file | |
| # Write output safely to temp then replace original | |
| out_abs = os.path.abspath(input_path) | |
| dirn = os.path.dirname(out_abs) or "." | |
| tmp_path = os.path.join(dirn, os.path.basename(out_abs) + ".tmp") | |
| with open(tmp_path, "wb") as f: | |
| f.write(png) | |
| os.replace(tmp_path, out_abs) | |
| print(f"Added metadata for {out_abs}!") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment