joeylib2/scripts/lib/netWmIcon.py

95 lines
3.3 KiB
Python
Executable file

#!/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))