171 lines
6.1 KiB
Python
171 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate the sample images shipped with the ImageSee demo.
|
|
|
|
Run from the ViperOS root:
|
|
python Scripts/gen_imagesee_assets.py
|
|
|
|
The generated files live in Assets/images/ and are copied to /IMAGES on the
|
|
ViperOS disk by build.py. Requires Pillow (only for generating the assets;
|
|
the OS itself does not need it).
|
|
"""
|
|
|
|
import os
|
|
import struct
|
|
import zlib
|
|
from PIL import Image, ImageDraw, ImageFilter
|
|
|
|
|
|
W, H = 320, 220
|
|
OUT = os.path.join("Assets", "images")
|
|
BG_OUT = os.path.join("Assets", "background.jpg")
|
|
|
|
|
|
def test_pattern():
|
|
img = Image.new("RGBA", (W, H), (0, 0, 0, 255))
|
|
px = img.load()
|
|
for y in range(H):
|
|
for x in range(W):
|
|
r = (x * 255) // (W - 1)
|
|
g = (y * 255) // (H - 1)
|
|
b = 255 - ((x + y) * 255) // (W + H - 2)
|
|
px[x, y] = (r, g, b, 255)
|
|
d = ImageDraw.Draw(img)
|
|
d.rectangle([12, 12, 92, 72], fill=(255, 255, 255, 255))
|
|
d.rectangle([22, 22, 82, 62], fill=(0, 0, 0, 255))
|
|
d.rectangle([108, 12, 188, 72], fill=(230, 40, 40, 255))
|
|
d.rectangle([198, 12, 278, 72], fill=(40, 200, 80, 255))
|
|
d.rectangle([288, 12, 308, 72], fill=(40, 80, 230, 255))
|
|
d.ellipse([40, 100, 160, 210], fill=(255, 220, 40, 255))
|
|
d.ellipse([150, 90, 250, 190], outline=(255, 255, 255, 255), width=5)
|
|
d.line([0, H - 1, W - 1, 80], fill=(255, 0, 255, 255), width=3)
|
|
d.line([0, 80, W - 1, H - 1], fill=(0, 255, 255, 255), width=3)
|
|
for i in range(0, 40):
|
|
d.rectangle([280 - i, 160 + i, 286 - i, 166 + i], fill=(i * 6, i * 6, i * 6, 255))
|
|
return img
|
|
|
|
|
|
def png_chunk(typ: bytes, data: bytes) -> bytes:
|
|
c = struct.pack(">I", len(data)) + typ + data
|
|
c += struct.pack(">I", zlib.crc32(typ + data) & 0xFFFFFFFF)
|
|
return c
|
|
|
|
|
|
def write_interlaced_png(path, img):
|
|
"""Adam7 interlaced RGBA PNG (filter 0), to exercise the pass decoder."""
|
|
w, h = img.size
|
|
px = img.load()
|
|
passes = [(0, 0, 8, 8), (4, 0, 8, 8), (0, 4, 4, 8), (2, 0, 4, 4),
|
|
(0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2)]
|
|
raw = bytearray()
|
|
for x0, y0, dx, dy in passes:
|
|
pw = (w - x0 + dx - 1) // dx if w > x0 else 0
|
|
ph = (h - y0 + dy - 1) // dy if h > y0 else 0
|
|
if pw <= 0 or ph <= 0:
|
|
continue
|
|
for ry in range(ph):
|
|
raw.append(0) # filter: None
|
|
y = y0 + ry * dy
|
|
for rx in range(pw):
|
|
x = x0 + rx * dx
|
|
r, g, b, a = px[x, y]
|
|
raw += bytes((r, g, b, a))
|
|
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 1)
|
|
out = b"\x89PNG\r\n\x1a\n" + png_chunk(b"IHDR", ihdr)
|
|
out += png_chunk(b"IDAT", zlib.compress(bytes(raw), 9))
|
|
out += png_chunk(b"IEND", b"")
|
|
with open(path, "wb") as f:
|
|
f.write(out)
|
|
|
|
|
|
def make_background():
|
|
"""960x540 desktop wallpaper shipped as /sys/images/screen/background.jpg."""
|
|
w, h = 960, 540
|
|
base = Image.new("RGB", (w, h))
|
|
px = base.load()
|
|
for y in range(h):
|
|
t = y / (h - 1)
|
|
r = int(12 + 34 * t)
|
|
g = int(16 + 20 * t)
|
|
b = int(34 + 44 * t)
|
|
for x in range(w):
|
|
# subtle diagonal tint
|
|
u = (x / (w - 1) + t) * 0.5
|
|
px[x, y] = (min(255, r + int(10 * u)),
|
|
min(255, g + int(6 * u)),
|
|
min(255, b + int(18 * u)))
|
|
overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(overlay)
|
|
blobs = [
|
|
(140, 420, 210, (30, 120, 190, 120)),
|
|
(330, 140, 170, (150, 40, 180, 110)),
|
|
(700, 360, 260, (20, 90, 160, 120)),
|
|
(820, 110, 150, (220, 60, 130, 90)),
|
|
(520, 470, 190, (40, 170, 170, 90)),
|
|
]
|
|
for cx, cy, rr, col in blobs:
|
|
d.ellipse([cx - rr, cy - rr, cx + rr, cy + rr], fill=col)
|
|
overlay = overlay.filter(ImageFilter.GaussianBlur(30))
|
|
base = Image.alpha_composite(base.convert("RGBA"), overlay).convert("RGB")
|
|
# vignette keeps the corners dark so white window chrome stays readable
|
|
mask = Image.new("L", (w, h))
|
|
mp = mask.load()
|
|
for y in range(h):
|
|
for x in range(w):
|
|
dx = (x - w / 2) / (w / 2)
|
|
dy = (y - h / 2) / (h / 2)
|
|
d2 = dx * dx + dy * dy
|
|
v = 255 - int(min(150, d2 * 110))
|
|
mp[x, y] = max(0, v)
|
|
base = Image.composite(base, Image.new("RGB", (w, h), (0, 0, 0)), mask)
|
|
base.save(BG_OUT, "JPEG", quality=88, subsampling=2, optimize=False)
|
|
print("[assets] wrote", BG_OUT)
|
|
|
|
|
|
def main():
|
|
os.makedirs(OUT, exist_ok=True)
|
|
img = test_pattern()
|
|
|
|
# Opaque RGBA PNG (exercises color type 6, non-interlaced).
|
|
img.save(os.path.join(OUT, "sample.png"), "PNG")
|
|
|
|
# Palette PNG with one transparent color (color type 3 + tRNS).
|
|
pal = img.convert("RGB").quantize(colors=64, method=Image.MEDIANCUT)
|
|
pal.info["transparency"] = 0
|
|
pal.save(os.path.join(OUT, "pal.png"), "PNG", transparency=0)
|
|
|
|
# Baseline 4:2:0 JPEG.
|
|
img.convert("RGB").save(os.path.join(OUT, "photo.jpg"), "JPEG",
|
|
quality=90, subsampling=2, optimize=False)
|
|
|
|
# Grayscale JPEG (single component path).
|
|
img.convert("L").save(os.path.join(OUT, "gray.jpg"), "JPEG", quality=90)
|
|
|
|
# Interlaced RGBA PNG with a vertical alpha ramp (Adam7 + alpha).
|
|
inter = img.copy()
|
|
ip = inter.load()
|
|
for y in range(H):
|
|
for x in range(W):
|
|
r, g, b, a = ip[x, y]
|
|
ip[x, y] = (r, g, b, (x * 255) // (W - 1))
|
|
write_interlaced_png(os.path.join(OUT, "inter.png"), inter)
|
|
|
|
make_background()
|
|
|
|
# Reference samples for the app's serial output.
|
|
print("[assets] reference samples (PNG/JPEG decode with PIL):")
|
|
for name in ("sample.png", "photo.jpg", "gray.jpg",
|
|
"pal.png", "inter.png"):
|
|
path = os.path.join(OUT, name)
|
|
ref = Image.open(path).convert("RGBA")
|
|
pts = [(ref.width // 4, ref.height // 3),
|
|
(ref.width // 2, ref.height // 2),
|
|
((ref.width * 3) // 4, (ref.height * 2) // 3)]
|
|
vals = [ref.getpixel(p) for p in pts]
|
|
print(f"[assets] {name:12s} {ref.width}x{ref.height} " +
|
|
" ".join(f"{p[0]},{p[1]},{p[2]}" for p in vals))
|
|
print("[assets] wrote", OUT)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|