95 lines
3.3 KiB
Python
Executable file
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))
|