File size: 9,807 Bytes
80c945e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | #!/usr/bin/env python3
# Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
"""Render the small fictional image set shipped in tests/images/ (Pillow only, no randomness): a bar chart, a phone
login screen, a slide, an invoice, a flow diagram, a sunset scene (JPEG), a red circle, an RGBA icon, a grayscale
letter, a palette chart, plus two edge-size images (7x5 and 1x300 px) that exercise the resampler's upscale and
extreme-aspect paths. All names, companies and numbers are invented.
python3 make_test_images.py [--out images] -> <out>/*.png|jpg + <out>/manifest.json
The output is repeatable on one machine but NOT byte-identical across machines: text is drawn with the first
system font found in FONT_PATHS (or Pillow's built-in font), and glyph rasterization, PNG compression and JPEG
encoding depend on the font files and on the Pillow / FreeType / zlib / libjpeg versions. The shipped files in
tests/images/ are the reference: image-smoke-reference.json and preprocess-cases.json were computed from them, so
regenerate those two fixtures (make_image_smoke_reference.py, make_preprocess_cases.py) if you regenerate the images.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
FONT_PATHS = ["/System/Library/Fonts/Helvetica.ttc", "/System/Library/Fonts/Supplemental/Arial.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"]
def font(size: int):
for p in FONT_PATHS:
try:
return ImageFont.truetype(p, size)
except OSError:
continue
return ImageFont.load_default()
def chart_bar(w=640, h=400):
im = Image.new("RGB", (w, h), "white")
d = ImageDraw.Draw(im)
d.text((20, 12), "Lumen lamps - units sold per city, Q3", fill="black", font=font(20))
cities = ["Port Avelin", "North Corra", "Westmere", "Lake Tamsin", "Oskaro"]
vals = [412, 263, 538, 190, 347]
cols = ["#4C78A8", "#F58518", "#54A24B", "#E45756", "#72B7B2"]
d.line([(60, h - 50), (w - 20, h - 50)], fill="black", width=2)
d.line([(60, 50), (60, h - 50)], fill="black", width=2)
bw = (w - 100) // len(cities)
for i, (c, v, col) in enumerate(zip(cities, vals, cols)):
x0 = 70 + i * bw
d.rectangle([x0, h - 50 - v * 0.5, x0 + bw - 20, h - 50], fill=col)
d.text((x0, h - 44), c, fill="black", font=font(13))
d.text((x0 + 5, h - 50 - v * 0.5 - 18), str(v), fill="black", font=font(13))
return im
def ui_login(w=390, h=844):
im = Image.new("RGB", (w, h), "#111418")
d = ImageDraw.Draw(im)
d.rounded_rectangle([40, 160, w - 40, 300], 24, fill="#1d2229")
d.text((60, 180), "Welcome back", fill="white", font=font(28))
d.text((60, 230), "Sign in to Tidal Boards", fill="#9aa4b1", font=font(16))
for i, label in enumerate(["Email", "Password"]):
y = 340 + i * 90
d.rounded_rectangle([40, y, w - 40, y + 56], 14, fill="#1d2229", outline="#2f3842")
d.text((56, y + 18), label, fill="#6b7682", font=font(16))
d.rounded_rectangle([40, 540, w - 40, 596], 14, fill="#2f6fed")
d.text((150, 556), "Sign in", fill="white", font=font(18))
d.text((110, 620), "Forgot your password?", fill="#9aa4b1", font=font(14))
return im
def slide(w=1280, h=720):
im = Image.new("RGB", (w, h), "#f6f3ec")
d = ImageDraw.Draw(im)
d.rectangle([0, 0, w, 90], fill="#264653")
d.text((50, 25), "Nimbus tents: 2027 launch plan", fill="white", font=font(36))
bullets = ["Three sizes, one pole system", "Pre-orders open in March", "Retail partners in six cities",
"Target: 18,000 units in the first season"]
for i, b in enumerate(bullets):
d.ellipse([70, 160 + i * 90, 90, 180 + i * 90], fill="#2a9d8f")
d.text((110, 150 + i * 90), b, fill="#222", font=font(30))
d.text((w - 260, h - 50), "slide 4 of 12", fill="#777", font=font(18))
return im
def doc_invoice(w=595, h=842):
im = Image.new("RGB", (w, h), "white")
d = ImageDraw.Draw(im)
d.text((40, 40), "INVOICE No. 2027-0418", fill="black", font=font(22))
d.text((40, 80), "Quill Pens Ltd., 12 Harbour Lane, Port Avelin", fill="black", font=font(13))
d.text((40, 100), "Bill to: Westmere Public Library", fill="black", font=font(13))
y = 160
d.line([(40, y), (w - 40, y)], fill="black")
rows = [("Fountain pen, steel nib", 24, "18.00"), ("Ink cartridges (box of 50)", 6, "9.50"), ("Gift boxes", 24, "2.20"),
("Shipping", 1, "14.00")]
for name, qty, price in rows:
y += 30
d.text((40, y), name, fill="black", font=font(13))
d.text((380, y), str(qty), fill="black", font=font(13))
d.text((460, y), price, fill="black", font=font(13))
y += 50
d.line([(40, y), (w - 40, y)], fill="black")
d.text((380, y + 15), "Total: 555.80", fill="black", font=font(15))
d.text((40, h - 80), "Payment within 30 days. Thank you for your order.", fill="black", font=font(12))
return im
def diagram_flow(w=800, h=500):
im = Image.new("RGB", (w, h), "white")
d = ImageDraw.Draw(im)
boxes = [("Order received", 60, 200), ("Check stock", 300, 200), ("Pack", 540, 120), ("Back-order", 540, 280)]
for t, x, y in boxes:
d.rounded_rectangle([x, y, x + 180, y + 70], 12, fill="#dbeafe", outline="#1e3a8a", width=2)
d.text((x + 15, y + 25), t, fill="#1e3a8a", font=font(17))
for a, b in [((240, 235), (300, 235)), ((480, 220), (540, 155)), ((480, 250), (540, 315))]:
d.line([a, b], fill="#1e3a8a", width=3)
d.polygon([b, (b[0] - 12, b[1] - 6), (b[0] - 12, b[1] + 6)], fill="#1e3a8a")
d.text((320, 60), "Fulfilment flow", fill="black", font=font(24))
return im
def scene_sunset(w=800, h=533):
im = Image.new("RGB", (w, h))
d = ImageDraw.Draw(im)
for y in range(h):
t = y / h
if t < 0.6:
c = (int(40 + 200 * t / 0.6), int(60 + 90 * t / 0.6), int(120 - 80 * t / 0.6))
else:
c = (int(20 + 30 * (t - 0.6) / 0.4), int(50 + 40 * (t - 0.6) / 0.4), int(90 + 60 * (t - 0.6) / 0.4))
d.line([(0, y), (w, y)], fill=c)
d.ellipse([w // 2 - 60, int(h * 0.6) - 60, w // 2 + 60, int(h * 0.6) + 60], fill="#ffb347")
d.rectangle([0, int(h * 0.6), w, h], fill=None)
for i in range(0, w, 40):
d.line([(i, int(h * 0.62) + (i % 80) // 20), (i + 24, int(h * 0.62) + (i % 80) // 20)], fill="#ffd27f", width=2)
return im
def shape_red_circle(w=300, h=300):
im = Image.new("RGB", (w, h), "white")
ImageDraw.Draw(im).ellipse([50, 50, 250, 250], fill="#d62828")
return im
def icon_rgba(w=256, h=256):
im = Image.new("RGBA", (w, h), (0, 0, 0, 0))
d = ImageDraw.Draw(im)
d.rounded_rectangle([20, 20, 236, 236], 48, fill=(47, 111, 237, 255))
d.polygon([(100, 75), (100, 181), (185, 128)], fill=(255, 255, 255, 255))
return im
def gray_letter(w=600, h=800):
im = Image.new("L", (w, h), 245)
d = ImageDraw.Draw(im)
lines = ["Dear neighbours,", "", "The community garden on Fern Street reopens on the first Saturday of May.",
"Plots are assigned in the order of the sign-up sheet at the library.", "Tools stay in the shed; please",
"return them before dusk.", "", "With thanks, the garden committee"]
for i, l in enumerate(lines):
d.text((50, 60 + i * 32), l, fill=20, font=font(16))
return im
def palette_chart(w=500, h=320):
im = Image.new("RGB", (w, h), "white")
d = ImageDraw.Draw(im)
d.text((20, 10), "Sales channel share", fill="black", font=font(18))
start = 0
for frac, col in [(0.45, "#1b9e77"), (0.3, "#d95f02"), (0.25, "#7570b3")]:
d.pieslice([120, 60, 380, 300], start, start + frac * 360, fill=col)
start += frac * 360
return im.quantize(16)
def build(out: Path) -> list[dict]:
out.mkdir(parents=True, exist_ok=True)
items = []
def save(name, im, query, fmt="PNG", **kw):
fn = f"{name}.{'jpg' if fmt == 'JPEG' else 'png'}"
im.save(out / fn, format=fmt, **kw)
with Image.open(out / fn) as chk:
items.append(dict(name=name, file=fn, query=query, width=chk.width, height=chk.height, mode=chk.mode, lossy=fmt == "JPEG"))
save("chart_bar", chart_bar(), "bar chart of lamp units sold per city")
save("ui_login", ui_login(), "dark mobile login screen with email and password fields")
save("slide_launch", slide(), "presentation slide about a tent product launch plan")
save("doc_invoice", doc_invoice(), "invoice for fountain pens and ink cartridges")
save("diagram_flow", diagram_flow(), "flow diagram of order fulfilment steps")
save("scene_sunset", scene_sunset(), "sunset over the sea with an orange sun", fmt="JPEG", quality=90)
save("shape_red_circle", shape_red_circle(), "a red circle on a white background")
save("icon_rgba", icon_rgba(), "blue rounded app icon with a white play symbol")
save("gray_letter", gray_letter(), "letter about a community garden reopening")
save("palette_chart", palette_chart(), "pie chart of sales channel share")
save("edge_tiny_7x5", shape_red_circle().resize((7, 5), Image.BICUBIC), "a red circle on a white background")
save("edge_strip_1x300", ui_login().resize((1, 300), Image.BICUBIC), "a thin vertical strip")
return items
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--out", default=str(Path(__file__).resolve().parent / "images"))
args = ap.parse_args()
items = build(Path(args.out))
(Path(args.out) / "manifest.json").write_text(json.dumps(items, indent=1) + "\n")
print(len(items), "images ->", args.out)
if __name__ == "__main__":
main()
|