Files
Tirsvad 0752fbd066
TirSystem/git-workflows: Publish wiki / publish-wiki (push) Skipped
TirSystem/git-workflows: Validate / validate (pull_request) Successful in 36s
TirSystem/git-workflows: OpenGrep / opengrep (pull_request) Successful in 2m37s
TirSystem/git-workflows: Release / release (pull_request) Successful in 18s
TirSystem/git-workflows: Validate / validate (push) Successful in 31s
TirSystem/git-workflows: Sync GitHub mirror metadata / sync-metadata (push) Successful in 12s
TirSystem/git-workflows: Publish accepted milestones / publish (push) Successful in 26s
TirSystem/git-workflows: OpenGrep / opengrep (push) Successful in 2m28s
Render screenshots in-process instead of through a subprocess
The OpenGrep scan flagged subprocess.run with a non-literal command in the
screenshot script. The script now runs each step in the same process.

Task: MIL-002#6
2026-11-06 13:40:28 +08:00

77 lines
2.5 KiB
Python

"""Render the real output of each lecture step to a PNG file.
Dev-only tool (needs Pillow, installed by `pip install -e ".[dev]"`). It runs
each step in-process, captures its output and draws it as a terminal image, so no
screen is captured and the desktop is never touched.
Usage, from the project root: python docs/screenshots/generate_screenshots.py
"""
import contextlib
import io
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from speed_calc.__main__ import main as speed_calc_main
from speed_calc.lecture_steps import main as lecture_steps_main
OUTPUT_DIR = Path(__file__).resolve().parent
ROOT = OUTPUT_DIR.parent.parent
# (file name, command shown in the prompt, lecture step to run; None = the program)
SHOTS = [
("step-1-current-time.png", "python -m speed_calc.lecture_steps 1", ["1"]),
("step-2-decorator-skeleton.png", "python -m speed_calc.lecture_steps 2", ["2"]),
("step-3-timing-wrapper.png", "python -m speed_calc.lecture_steps 3", ["3"]),
("step-4-fast-vs-slow.png", "python -m speed_calc", None),
]
BACKGROUND, FOREGROUND, PROMPT = "#1e1e2e", "#cdd6f4", "#a6e3a1"
FONT_CANDIDATES = [
"C:/Windows/Fonts/consola.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/System/Library/Fonts/Menlo.ttc",
]
def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
for candidate in FONT_CANDIDATES:
if Path(candidate).exists():
return ImageFont.truetype(candidate, size)
return ImageFont.load_default()
def run(step: list[str] | None) -> str:
"""Run a step in this process and return what it printed."""
buffer = io.StringIO()
with contextlib.redirect_stdout(buffer):
if step is None:
speed_calc_main()
else:
lecture_steps_main(step)
return buffer.getvalue().strip()
def render(path: Path, command: str, output: str) -> None:
font = load_font(20)
lines = [f"$ {command}", *output.splitlines()]
width = int(max(font.getlength(line) for line in lines)) + 48
height = 28 * len(lines) + 48
image = Image.new("RGB", (max(width, 640), height), BACKGROUND)
draw = ImageDraw.Draw(image)
for index, line in enumerate(lines):
colour = PROMPT if index == 0 else FOREGROUND
draw.text((24, 24 + 28 * index), line, font=font, fill=colour)
image.save(path)
def main() -> None:
for name, command, step in SHOTS:
render(OUTPUT_DIR / name, command, run(step))
print("wrote", name)
if __name__ == "__main__":
main()