#!/usr/bin/env python3 # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT """netWmIcon.py - an X window's icon against the picture it should be. A window's icon on X is its _NET_WM_ICON property: one or more pictures, each its width, its height and then its pixels as 32-bit ARGB, row after row. SDL3 sets it from SDL_SetWindowIcon; Wine from the window class's icon. xprop prints it as numbers with xprop -id WINDOW -f _NET_WM_ICON 32c ' = $0+' _NET_WM_ICON > XPROP and this compares the picture of the wanted size in that against WANT (any picture PIL reads; an .ico's 32-bit picture of that size). A pixel at least half opaque must be so in both, and then of the same colour, give or take TOLERANCE a gun (a system may round its own way). netWmIcon.py XPROP WANT SIZE LABEL Prints LABEL: PASS or LABEL: FAIL, and exits 0 or 1. """ import re import sys from PIL import Image TOLERANCE = 2 ALPHA_OPAQUE_MIN = 128 ICO_BPP = 32 def pictures(text): """Every (width, height, pixels) in xprop's numbers.""" match = re.search(r"=\s*([\d,\s]+)", text) if match is None: return [] values = [int(v) for v in match.group(1).replace(",", " ").split()] out = [] at = 0 while at + 2 <= len(values): w, h = values[at], values[at + 1] at += 2 out.append((w, h, values[at:at + w * h])) at += w * h return out def wanted(path, size): """WANT as RGBA at SIZE x SIZE: an .ico's 32-bit picture of that size.""" image = Image.open(path) if image.format == "ICO": image = image.ico.getimage((size, size), ICO_BPP) return image.convert("RGBA") def main(argv): if len(argv) != 5: sys.stderr.write(__doc__) return 2 xpropPath, wantPath, size, label = argv[1], argv[2], int(argv[3]), argv[4] found = pictures(open(xpropPath, encoding="ascii", errors="replace").read()) sizes = ", ".join("%dx%d" % (w, h) for w, h, _ in found) or "none" shown = [pixels for w, h, pixels in found if w == size and h == size] if not shown: print("%s: FAIL - the window has no %dx%d icon (it has: %s)" % (label, size, size, sizes)) return 1 want = wanted(wantPath, size) if want.size != (size, size): print("%s: FAIL - %s is %dx%d, not %dx%d" % (label, wantPath, want.size[0], want.size[1], size, size)) return 1 wrong = 0 opaqueCount = 0 for argb, (r, g, b, a) in zip(shown[0], want.getdata()): shownOpaque = (argb >> 24) >= ALPHA_OPAQUE_MIN if shownOpaque != (a >= ALPHA_OPAQUE_MIN): wrong += 1 continue if not shownOpaque: continue opaqueCount += 1 rgb = ((argb >> 16) & 0xFF, (argb >> 8) & 0xFF, argb & 0xFF) if max(abs(rgb[0] - r), abs(rgb[1] - g), abs(rgb[2] - b)) > TOLERANCE: wrong += 1 if wrong or opaqueCount == 0: print("%s: FAIL - %d of the %dx%d icon's pixels differ from %s (%d opaque)" % (label, wrong, size, size, wantPath, opaqueCount)) return 1 print("%s: PASS (the window's %dx%d icon is the baked one: %d opaque pixels and the transparency round them; icons on the window: %s)" % (label, size, size, opaqueCount, sizes)) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))