Render complete Steam artwork for every sideload and fix VR shortcut identity

Add optional SteamGridDB settings, source-first fallbacks rendered with Steam canvas, backfill commands and shared APK/native library details. Verify live artwork and Open Saber Steam Play/Stop; document SuperTux's clipboard crash.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
This commit is contained in:
saphidandGPT-6 Astra committed 2026-09-28 22:20:27 +10:00
1 parent c06b3285f2
commit f695f398de
18 files changed
+1097 -289

No files matched your search

+38 -86
View File
@@ -1,8 +1,5 @@
"""Small stdlib PNG compositor for Steam's Android library artwork."""
import hashlib
"""Bounded artwork inputs for Steam library canvas rendering."""
import struct
import textwrap
import unicodedata
import urllib.parse
import urllib.request
import zlib
@@ -12,18 +9,6 @@ SLOTS = {'grid': (600, 900), 'wide': (920, 430), 'hero': (3840, 1240),
MAX_IMAGE = 12 * 1024 * 1024
MAX_PIXELS = 8_000_000
PNG = b'\x89PNG\r\n\x1a\n'
# Five-column display lettering; labels remain exact in Steam's native text.
GLYPHS = dict(zip('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789- .?', (
'0E11111F111111', '1E11111E11111E', '0F10101010100F', '1E11111111111E',
'1F10101E10101F', '1F10101E101010', '0F10101711110F', '1111111F111111',
'0E04040404040E', '0702020212120C', '11121418141211', '1010101010101F',
'111B1515111111', '11191513111111', '0E11111111110E', '1E11111E101010',
'0E11111115120D', '1E11111E141211', '0F10100E01011E', '1F040404040404',
'1111111111110E', '11111111110A04', '11111115151B11', '11110A040A1111',
'11110A04040404', '1F01020408101F', '0E11131519110E', '040C040404040E',
'0E11010204081F', '1E01010E01011E', '02060A121F0202', '1F10101E01011E',
'0E10101E11110E', '1F010204080808', '0E11110E11110E', '0E11110F01010E',
'0000001F000000', '00000000000000', '00000000000C0C', '0E110102040004')))
def chunk(kind, data):
@@ -150,24 +135,6 @@ def image_type(data):
raise ValueError('artwork must be a supported PNG or JPEG')
def rectangle(pixels, width, x, y, w, h, color):
row = bytes(color) * w
for yy in range(y, y + h):
start = (yy * width + x) * 4
pixels[start:start + len(row)] = row
def lettering(pixels, width, text, y, scale, color):
x = (width - (len(text) * 6 - 1) * scale) // 2
for ch in text:
glyph = bytes.fromhex(GLYPHS.get(ch, GLYPHS['?']))
for yy, bits in enumerate(glyph):
for xx in range(5):
if bits & (1 << (4 - xx)):
rectangle(pixels, width, x + xx * scale, y + yy * scale, scale, scale, color)
x += 6 * scale
def stamp(pixels, width, icon, x, y, size):
iw, ih, source = icon
dw, dh = max(1, size * iw // max(iw, ih)), max(1, size * ih // max(iw, ih))
@@ -182,60 +149,45 @@ def stamp(pixels, width, icon, x, y, size):
pixels[dst + 3] = a + pixels[dst + 3] * (255 - a) // 255
def fallback(label, icon_png=None):
text = unicodedata.normalize('NFKD', label).encode('ascii', 'ignore').decode().upper().strip() or 'ANDROID APP'
text = ' '.join(text.split())[:80]
seed = hashlib.sha256(label.encode()).digest()
accent = tuple(100 + c % 110 for c in seed[:3]) + (255,)
try:
icon = decode(icon_png)
except (ValueError, zlib.error, struct.error):
icon = None
out = {}
for slot, (w, h) in SLOTS.items():
pixels = bytearray(w * h * 4)
if slot != 'logo':
for y in range(h):
color = (14 + 12 * y // h, 22 + 16 * y // h, 34 + 22 * y // h, 255)
rectangle(pixels, w, 0, y, w, 1, color)
rectangle(pixels, w, w // 12, h - max(6, h // 70), w * 5 // 6, max(3, h // 180), accent)
size = {'grid': 320, 'wide': 190, 'hero': 640, 'logo': 0, 'icon': 192}[slot]
if size:
x, y = (w - size) // 2, {'grid': 160, 'wide': 38, 'hero': 200, 'icon': 32}[slot]
if icon:
stamp(pixels, w, icon, x, y, size)
else:
scale = size // 9
lettering(pixels, w, text[0], y + (size - scale * 7) // 2, scale, accent)
if slot != 'icon':
scale = {'grid': 5, 'wide': 5, 'hero': 12, 'logo': 10}[slot]
limit = (w - w // 6) // (scale * 6)
lines = textwrap.wrap(text, limit)[:3 if slot in ('grid', 'logo') else 2]
if len(textwrap.wrap(text, limit)) > len(lines):
lines[-1] = lines[-1][:-3] + '...'
y = {'grid': 580, 'wide': 280, 'hero': 930, 'logo': (h - len(lines) * scale * 9) // 2}[slot]
for line in lines:
lettering(pixels, w, line, y, scale, (240, 245, 250, 255))
y += scale * 9
out[slot] = ('png', png(w, h, pixels))
return out
def fetch(value):
if isinstance(value, str):
if urllib.parse.urlsplit(value).scheme not in ('http', 'https'):
raise ValueError('artwork URLs must use HTTP(S)')
request = urllib.request.Request(value, headers={'User-Agent': 'FrameControl/1.0'})
with urllib.request.urlopen(request, timeout=20) as response:
if urllib.parse.urlsplit(response.geturl()).scheme not in ('http', 'https'):
raise ValueError('artwork redirect must use HTTP(S)')
value = response.read(MAX_IMAGE + 1)
return image_type(value), value
def prepare(label, icon_png=None, artwork=None):
"""Gather inputs; the Frame's Chromium canvas renders every final slot."""
import frame_steamgriddb
artwork = artwork or {}
if not isinstance(artwork, dict) or set(artwork) - set(SLOTS):
raise ValueError('artwork slots are grid, wide, hero, logo and icon')
supplied = {}
allowed = set(SLOTS) | {'banner', 'feature_graphic', 'screenshots', 'screenshot'}
if not isinstance(artwork, dict) or set(artwork) - allowed:
raise ValueError('unknown artwork slot')
supplied, warnings = {}, []
for slot, value in artwork.items():
if isinstance(value, str):
if urllib.parse.urlsplit(value).scheme not in ('http', 'https'):
raise ValueError('artwork URLs must use HTTP(S)')
request = urllib.request.Request(value, headers={'User-Agent': 'FrameControl/1.0'})
with urllib.request.urlopen(request, timeout=20) as response:
if urllib.parse.urlsplit(response.geturl()).scheme not in ('http', 'https'):
raise ValueError('artwork redirect must use HTTP(S)')
value = response.read(MAX_IMAGE + 1)
supplied[slot] = (image_type(value), value)
result = fallback(label, supplied.get('icon', ('', icon_png))[1])
result.update(supplied)
return result
values = value if slot == 'screenshots' and isinstance(value, (list, tuple)) else [value]
for candidate in values[:4]:
try:
supplied['screenshot' if slot == 'screenshots' else slot] = fetch(candidate)
break
except (ValueError, OSError):
warnings.append('Source ' + slot + ' unavailable; using fallback art')
if 'icon' not in supplied and icon_png:
try:
supplied['icon'] = fetch(icon_png)
except (ValueError, OSError):
pass
provider, provider_warnings = frame_steamgriddb.lookup(label)
warnings.extend(provider_warnings)
for slot, value in provider.items():
try:
supplied[slot] = fetch(value)
except (ValueError, OSError):
warnings.append('SteamGridDB ' + slot + ' download failed; using fallback art')
return supplied, warnings