"""Rebuild the two gpe-buttonbox assets that came from Psion's CVS. The gpe-buttonbox source used here is upstream GPE's, because Psion's tag GPE_BUTTONBOX_50 lived only on cvs.psiondev.com. Upstream ships a green hand for the Desk button and a vivid yellow button face; Psion's photographs show the blockletter glyph and a warmer gold. Both are rebuilt here. show-desktop.png the first Psion blockletter glyph, cut out of Psion's own splash_screen.png -- the same source the desktop watermark uses, and the photographs confirm the Desk button carries exactly that glyph. yellow_button_*.png a gold gradient measured off the fss_88 photograph, #e9c96b, replacing upstream's #ffe910. The geometry (64x48, 1px border #354052) is upstream's and kept. Run: python3 scripts/mkicons.py """ import math from PIL import Image import os import sys SPLASH = os.path.expanduser( '~/Downloads/PsionLinux-netBookPro/PsionLX-UI-photos/psion-splash_screen.png') # The five dark blocks of the PSION logo; the first one carries the glyph # that appears on the Desk button. The white glyph sits in this band, above # the yellow letter. BLOCK_1 = (215, 265) GLYPH_TOP, GLYPH_BOT = 204, 237 # The glyph is not a flat silhouette: the photographs show it bevelled, with # a pale highlight along the top-left of each stroke and a darker edge along # the bottom-right, giving a raised, almost tubular look against the gold. # Measured off the fss_88 photograph by isolating the glyph's own pixels # from the gold around them (gold has red well above blue; the glyph is # neutral). Over 1,587 glyph pixels: average #bbc1bb, darkest tenth #899299, # lightest tenth #e8dfd0, against a white reference of #e8e8e4 -- so the # photo sits about 9% under and the glyph is a LIGHT grey, not a dark one. INK = (198, 205, 209) # body HILIT = (244, 248, 250) # top-left edge SHADE = (150, 160, 168) # bottom-right edge GOLD_TOP = (245, 216, 126) # measured: #e9c96b average, lighter at the top GOLD_BOT = (217, 178, 72) BORDER = (53, 64, 82) # upstream's border colour, unchanged def desk_icon(width, height, scale=6): """The Psion blockletter glyph, shaded as a rounded stroke lit top-left. This is measured, not guessed. Cross-sections through the glyph in the fss_88 photograph give: the horizontal bar, top to bottom : luminance 224 -> 141 over 12 rows a vertical arm of the U, L to R : luminance 217 -> 178 so the shading rolls smoothly across the full thickness of each stroke in both axes -- a rounded, tubular surface lit from the top-left, not a flat body with a rim highlight. Reproduced by building a distance field inside the glyph, raising it to a circular cap so the surface normal turns continuously from one edge to the other, and lighting that. Luminances are corrected for the photograph sitting about 9% under (its white reference reads #e8e8e4), giving the LIGHT..DARK range below. """ splash = Image.open(SPLASH).convert('RGB') tile = splash.crop((BLOCK_1[0], GLYPH_TOP, BLOCK_1[1] + 1, GLYPH_BOT)) mask = tile.convert('L').point(lambda v: 255 if v > 170 else 0) bbox = mask.getbbox() if bbox: mask = mask.crop(bbox) # Pad with background before measuring distances. Without this the glyph # sits flush against the image edge after getbbox(), the transform never # finds a boundary along those sides, and the distance runs away -- which # flattens the shading completely. PAD = 2 * scale W, H = width * scale, height * scale core = mask.resize((W, H), Image.LANCZOS).point(lambda v: 255 if v > 110 else 0) big = Image.new('L', (W + 2 * PAD, H + 2 * PAD), 0) big.paste(core, (PAD, PAD)) W, H = big.size m = big.load() # chamfer distance transform: distance from each interior pixel to the # nearest edge, in units of 1 orthogonal / 1.414 diagonal INF = 10 ** 9 d = [[0 if not m[x, y] else INF for x in range(W)] for y in range(H)] for y in range(H): for x in range(W): if d[y][x] == 0: continue v = d[y][x] if y > 0: v = min(v, d[y - 1][x] + 10) if x > 0: v = min(v, d[y - 1][x - 1] + 14) if x < W - 1: v = min(v, d[y - 1][x + 1] + 14) if x > 0: v = min(v, d[y][x - 1] + 10) d[y][x] = v for y in range(H - 1, -1, -1): for x in range(W - 1, -1, -1): if d[y][x] == 0: continue v = d[y][x] if y < H - 1: v = min(v, d[y + 1][x] + 10) if x > 0: v = min(v, d[y + 1][x - 1] + 14) if x < W - 1: v = min(v, d[y + 1][x + 1] + 14) if x < W - 1: v = min(v, d[y][x + 1] + 10) d[y][x] = v # Normalise by the deepest point found, which is the half-thickness of # the thickest stroke. Thinner strokes then roll over a shorter range, # which is what the photograph shows: the bar's gradient spans a wider # luminance range than the U's arms. R = max(1, max(max(r) for r in d)) / 10.0 def hgt(x, y): """Rounded cap: rises from the edge and flattens at the crown.""" if not m[x, y]: return 0.0 t = (d[y][x] / 10.0) / R # chamfer units are tenths of a pixel if t > 1.0: t = 1.0 return math.sin(t * math.pi * 0.5) LIGHT = (242, 238, 230) # 224 lum in the photo, exposure-corrected DARK = (140, 150, 154) # 141 lum in the photo, exposure-corrected Z = 0.9 # how domed the stroke reads out = Image.new('RGBA', (W, H), (0, 0, 0, 0)) po = out.load() lx, ly, lz = -0.62, -0.62, 0.48 ln = math.sqrt(lx * lx + ly * ly + lz * lz) lx, ly, lz = lx / ln, ly / ln, lz / ln # First pass: the raw lambert term for every glyph pixel. inten = {} for y in range(H): for x in range(W): if not m[x, y]: continue gx = hgt(min(W - 1, x + 1), y) - hgt(max(0, x - 1), y) gy = hgt(x, min(H - 1, y + 1)) - hgt(x, max(0, y - 1)) # the height rises over R pixels, so a one-pixel difference is # tiny; rescale into units where the stroke half-width is 1, # otherwise the normal barely tilts and the glyph reads flat nx, ny, nz = -gx * R, -gy * R, Z nn = math.sqrt(nx * nx + ny * ny + nz * nz) or 1.0 inten[(x, y)] = (nx * lx + ny * ly + nz * lz) / nn # Second pass: stretch that range onto the luminances measured off the # photograph, so the reproduction spans the same light-to-dark sweep # rather than a compressed part of it. vals = list(inten.values()) lo, hi = min(vals), max(vals) span = (hi - lo) or 1.0 for (x, y), v in inten.items(): i = (v - lo) / span col = tuple(int(DARK[k] + (LIGHT[k] - DARK[k]) * i) for k in range(3)) po[x, y] = col + (255,) out = out.crop((PAD, PAD, W - PAD, H - PAD)) return out.resize((width, height), Image.LANCZOS) def button(width, height, top, bottom, pressed=False): """A vertical gradient button face with upstream's 1px border.""" if pressed: top, bottom = bottom, top im = Image.new('RGBA', (width, height), BORDER + (255,)) px = im.load() for y in range(1, height - 1): t = (y - 1) / max(1, height - 3) c = tuple(int(top[i] + (bottom[i] - top[i]) * t) for i in range(3)) for x in range(1, width - 1): px[x, y] = c + (255,) return im def main(): root = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser( '~/Downloads/PsionLinux-netBookPro/workspace/card/root') # gpe-buttonbox loads its own icons through gpe_find_icon() with # MY_PIXMAPS_DIR = "gpe-buttonbox/", which resolves under # /usr/share/gpe/pixmaps/default -- NOT /usr/share/pixmaps, where a # second unused copy of the same files also sits. Write both so they # do not drift apart, but the gpe path is the one that is read. pix = os.path.join(root, 'usr/share/gpe/pixmaps/default/gpe-buttonbox') alt = os.path.join(root, 'usr/share/pixmaps') # gpe-buttonbox scales this to cfg.icon_size in HEIGHT, preserving the # aspect ratio (do_build_icon). With icon_size=24 the upstream 32x21 icon # is scaled *up* to about 37x24, which smears the shading. Emitting it at # three times that size means the dock scales down instead, and the # rounded shading survives. ref = Image.open(os.path.join(pix, 'show-desktop.png')) w, h = ref.size if h <= 24: w, h = w * 3, h * 3 icon = desk_icon(w, h) for d in (pix, alt): icon.save(os.path.join(d, 'show-desktop.png')) print(f' show-desktop.png {w}x{h} Psion blockletter glyph, shaded') bref = Image.open(os.path.join(pix, 'yellow_button_up.png')) w, h = bref.size up = button(w, h, GOLD_TOP, GOLD_BOT) down = button(w, h, GOLD_TOP, GOLD_BOT, pressed=True) for d in (pix, alt): up.save(os.path.join(d, 'yellow_button_up.png')) down.save(os.path.join(d, 'yellow_button_down.png')) print(f' yellow_button_*.png {w}x{h} gold #e9c96b (was #ffe910)') if __name__ == '__main__': main()