"""Regression test for the gray-strip artifact caused by near-white pixels in asset PNGs. Compares the number of near-white (RGB 240-254, alpha>200) pixels in `lose.png` after running through `GameWindow.load_image` with and without the normalization step. The fix in load_image clamps those pixels to pure (255,255,255), which is what removes the visible artifact on a white panel. Does not require a running display, only a SpriteFactory that can create textures headless. The texture is queried via `SDL_QueryTexture` to confirm the pixel data, or alternatively we fall back to inspecting the PIL image post-normalization directly. """ import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import ctypes import sdl2 import sdl2.ext import sdl2.render from PIL import Image def make_headless_renderer(): """Create a SDL renderer without requiring a visible display. Uses an offscreen SDL window which works with the dummy driver on headless systems. """ os.environ.setdefault("SDL_VIDEODRIVER", "dummy") sdl2.ext.init() window = sdl2.ext.Window("gray_strip_test", size=(320, 200)) window.show() renderer = sdl2.ext.Renderer(window) return renderer, window def count_near_white(image, lo=240, hi=254): """Count opaque near-white pixels in a PIL image.""" if image.mode != "RGBA": image = image.convert("RGBA") count = 0 for r, g, b, a in image.getdata(): if a > 200 and r == g == b and lo <= r <= hi: count += 1 return count def main(): image_path = "Rat/lose.png" # Create the SpriteFactory / renderer without a real display renderer, window = make_headless_renderer() factory = sdl2.ext.SpriteFactory(renderer=renderer) # Build a minimal GameWindow instance for load_image from runtime_paths import resolve_bundle_path from engine.sdl2 import GameWindow font_path = str(resolve_bundle_path("assets/decterm.ttf")) gw = GameWindow.__new__(GameWindow) gw.cell_size = 40 gw.target_size = (320, 200) gw.fonts = {10: sdl2.ext.FontManager(font_path=font_path, size=10)} gw.w_offset = 0 gw.h_offset = 0 gw.window = window gw.renderer = renderer gw.factory = factory gw.width = 320 gw.height = 200 # Patch load_image to bypass the normalization step original_load = gw.load_image def buggy_load(path, transparent_color=None, surface=False): from runtime_paths import resolve_bundle_path from PIL import Image import sdl2.ext image_path_full = resolve_bundle_path(os.path.join("assets", path)) image = Image.open(image_path_full) if transparent_color: image = image.convert("RGBA") if isinstance(transparent_color[0], int): color_set = {transparent_color} else: color_set = set(transparent_color) datas = image.getdata() new_data = [ (255, 255, 255, 0) if item[:3] in color_set else item for item in datas ] image.putdata(new_data) image = image.resize((image.width * 5 // 8, image.height * 5 // 8), Image.NEAREST) temp_surface = sdl2.ext.pillow_to_surface(image) texture = factory.from_surface(temp_surface) sdl2.SDL_FreeSurface(temp_surface) return texture # Inspect the RAW PIL image to compare normalization outcomes. # This avoids relying on the SDL renderer which may fail in some # headless environments. raw_image = Image.open(resolve_bundle_path(os.path.join("assets", image_path))) raw_image_rgba = raw_image.convert("RGBA") # Reproduce the bug pipeline: transparent_color applied, NO normalization buggy_img = raw_image_rgba.copy() color_set = {(125, 125, 125), (128, 128, 128)} datas = buggy_img.getdata() new_data = [ (255, 255, 255, 0) if item[:3] in color_set else item for item in datas ] buggy_img.putdata(new_data) buggy_img = buggy_img.resize((buggy_img.width * 5 // 8, buggy_img.height * 5 // 8), Image.NEAREST) # Apply normalization (mirror the fix) fixed_img = raw_image_rgba.copy() datas = fixed_img.getdata() normalized = [ (255, 255, 255, item[3]) if item[3] > 0 and item[0] >= 250 and item[1] >= 250 and item[2] >= 250 and item[0] == item[1] == item[2] else item for item in datas ] fixed_img.putdata(normalized) datas = fixed_img.getdata() normalized = [ (255, 255, 255, 0) if item[:3] in color_set else item for item in datas ] fixed_img.putdata(normalized) fixed_img = fixed_img.resize((fixed_img.width * 5 // 8, fixed_img.height * 5 // 8), Image.NEAREST) # Save for visual inspection buggy_img.save("/tmp/test_loss_before.png") fixed_img.save("/tmp/test_loss_after.png") before = count_near_white(buggy_img) after = count_near_white(fixed_img) print(f"\n=== RESULTS ===") print(f"Pixel quasi-bianchi (240-254) PRIMA della normalizzazione: {before}") print(f"Pixel quasi-bianchi (240-254) DOPO la normalizzazione: {after}") print(f"Immagini salvate in: /tmp/test_loss_before.png, /tmp/test_loss_after.png") if after < before: print("PASS: la normalizzazione riduce l'artefatto grigio") return 0 elif before == 0 and after == 0: print("INCONCLUSIVE: nessun pixel artefatto rilevato in entrambe le versioni") return 0 else: print("FAIL: la normalizzazione non riduce l'artefatto") return 1 if __name__ == "__main__": sys.exit(main())