120 lines
4.6 KiB
Bash
Executable file
120 lines
4.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
# See LICENSE.md for license information.
|
|
# SPDX-License-Identifier: MIT
|
|
#
|
|
# verify-win16-icon.sh - a Win16 program's icon as Windows 3.1 shows it.
|
|
#
|
|
# make binds every Win16 program's icon into its executable (make/win16.mk:
|
|
# tools/iconBake.py ico16, 32x32 in the 16 VGA colours, resource
|
|
# HOSTED_ICON_ID), and the backend gives its window class that icon. Here
|
|
# Program Manager is Windows' shell and WIN.INI's load= starts DRAW
|
|
# (examples/draw) minimised, so Windows draws the class's icon on the
|
|
# desktop -- and DRAW runs on, minimised, presenting to a window with no
|
|
# room. DOSBox's own screenshot of the 800x600 screen must hold the baked
|
|
# icon: every pixel of it the icon's mask leaves opaque, in its colour.
|
|
#
|
|
# Usage: scripts/verify-win16-icon.sh
|
|
# Env: WIN16_ICON_KEEP keep the screenshot as build/win16/win16-icon.png
|
|
# Exit: 0 PASS, 1 FAIL, 2 SKIP (Windows or DOSBox not installed, or DRAW
|
|
# not built)
|
|
# Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL + numpy, the win311
|
|
# component; DRAW.EXE and its icon (make win16).
|
|
set -euo pipefail
|
|
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
source "$repo/scripts/lib/joeyEnv.sh"
|
|
source "$repo/scripts/lib/win16Gate.sh"
|
|
|
|
label=verify-win16-icon
|
|
win16Need
|
|
program=$(joeyExampleProgram draw win16)
|
|
icon=$repo/build/win16/icon/DRAW/icon.ico
|
|
[ -f "$program" ] && [ -f "$icon" ] || { echo "$label: build DRAW.EXE first (make win16)" >&2; exit 2; }
|
|
# Seconds to wait for the icon (Windows' boot is most of it), and between
|
|
# screenshots.
|
|
startSeconds=120
|
|
shotSeconds=10
|
|
|
|
# iconShown PNG: true when PNG holds the icon; says where, or that it does
|
|
# not.
|
|
iconShown() {
|
|
python3 - "$1" "$icon" "$label" <<'PY'
|
|
import sys
|
|
|
|
import numpy
|
|
from PIL import Image
|
|
|
|
png, ico, label = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
# The 6-bit DAC behind DOSBox's capture moves a colour by a few levels.
|
|
TOLERANCE = 8
|
|
screen = numpy.asarray(Image.open(png).convert("RGB"), dtype=numpy.int16)
|
|
icon = numpy.asarray(Image.open(ico).convert("RGBA"), dtype=numpy.int16)
|
|
opaque = numpy.argwhere(icon[:, :, 3] >= 128)
|
|
if len(opaque) == 0:
|
|
print(f"{label}: FAIL - the icon file has no opaque pixel")
|
|
sys.exit(1)
|
|
colours = icon[opaque[:, 0], opaque[:, 1], :3]
|
|
ih, iw = icon.shape[:2]
|
|
sh, sw = screen.shape[:2]
|
|
|
|
|
|
def matches(colour):
|
|
return numpy.all(numpy.abs(screen - colour) <= TOLERANCE, axis=2)
|
|
|
|
|
|
# Anchor on the opaque pixel whose colour the screen holds least, then try
|
|
# the icon at every place that pixel's colour is.
|
|
counts = [int(matches(c).sum()) for c in colours]
|
|
anchor = min(range(len(opaque)), key=lambda i: counts[i])
|
|
ay, ax = opaque[anchor]
|
|
found = None
|
|
for y, x in numpy.argwhere(matches(colours[anchor])):
|
|
top, left = y - ay, x - ax
|
|
if top < 0 or left < 0 or top + ih > sh or left + iw > sw:
|
|
continue
|
|
shown = screen[top + opaque[:, 0], left + opaque[:, 1]]
|
|
if numpy.all(numpy.abs(shown - colours) <= TOLERANCE):
|
|
found = (left, top)
|
|
break
|
|
if found is None:
|
|
print(f"{label}: FAIL - the program's icon is not on the screen ({len(opaque)} opaque pixels looked for)")
|
|
sys.exit(1)
|
|
print(f"{label}: PASS (DRAW minimised shows its bound icon at {found[0]},{found[1]}: all {len(opaque)} opaque pixels of the baked 32x32 in place)")
|
|
PY
|
|
}
|
|
|
|
work=$(mktemp -d -t joeylib-win16-icon.XXXXXX)
|
|
cleanup() {
|
|
win16Stop
|
|
rm -rf "$work"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
mkdir -p "$work/program" "$work/shots"
|
|
cp "$program" "$work/program/DRAW.EXE"
|
|
win16Stage "$work" "$work/program" DRAW.EXE
|
|
python3 "$repo/tools/winIni.py" "$work/c/WINDOWS/SYSTEM.INI" set boot shell progman.exe
|
|
python3 "$repo/tools/winIni.py" "$work/c/WINDOWS/WIN.INI" set windows load 'D:\DRAW.EXE'
|
|
WIN16_DOSBOX_ARGS=(--set "capture_dir=$work/shots" --set "default_image_capture_formats=raw")
|
|
win16Start "$work"
|
|
win=""
|
|
for ((waited = 0; waited < startSeconds; waited += shotSeconds)); do
|
|
sleep "$shotSeconds"
|
|
[ -n "$win" ] || win=$(win16Window) || win=""
|
|
[ -n "$win" ] || continue
|
|
# Ctrl+F5: DOSBox's screenshot hotkey (the Ctrl reaches Program Manager,
|
|
# which ignores it).
|
|
rm -f "$work/shots/"*.png
|
|
DISPLAY="$WIN16_DISPLAY" xdotool key --window "$win" ctrl+F5
|
|
sleep 2
|
|
png=$(find "$work/shots" -name '*.png' -print -quit)
|
|
[ -n "$png" ] || continue
|
|
[ -n "${WIN16_ICON_KEEP:-}" ] && cp "$png" "$repo/build/win16/win16-icon.png"
|
|
if iconShown "$png" > "$work/verdict"; then
|
|
cat "$work/verdict"
|
|
exit 0
|
|
fi
|
|
done
|
|
cat "$work/verdict" 2> /dev/null || echo "$label: FAIL - no screenshot of Windows within ${startSeconds}s"
|
|
exit 1
|