Files
031-flash-card/tools/capture_screenshots.py
Tirsvad f45d6036e4 MIL-008: Choose level
Review record RC-041 (Go, autonomous mode, not independent).

Task: MIL-008#1
Task: MIL-008#2
Task: MIL-008#3
Task: MIL-008#4
Task: MIL-008#5
Task: MIL-008#6
Task: MIL-008#7
Task: MIL-008#8
2026-10-14 23:58:31 +08:00

130 lines
4.3 KiB
Python

"""Capture screenshots of the running flash card window.
@file capture_screenshots.py
@brief Starts the program, grabs the level screen, the front of a card and,
three seconds later, the back, and saves them as PNG files.
The tool needs Pillow (`python -m pip install -e ".[tools]"`), which is a tool
dependency only and not a dependency of the program. It grabs the screen area
of the window, so it needs a visible desktop.
"""
from __future__ import annotations
import argparse
import ctypes
import random
import shutil
import sys
import tempfile
import time
from collections.abc import Sequence
from pathlib import Path
from PIL import ImageGrab
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from flash_card.constants import ( # noqa: E402
DEFAULT_DATA_DIR,
FLIP_DELAY_MS,
PROJECT_ROOT,
VOCABULARY_FILE_NAME,
)
from flash_card.domain.value_objects.level import Level # noqa: E402
from flash_card.presentation.app import create_application # noqa: E402
from flash_card.presentation.features.flash_card.flash_card_view import ( # noqa: E402
FlashCardView,
)
## @brief Default folder of the screenshots.
DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "docs" / "images"
## @brief Seed that makes the first card the same on every run.
SCREENSHOT_SEED = 7
## @brief Extra time in seconds to let the window settle before a grab.
SETTLE_SECONDS = 0.5
def _make_dpi_aware() -> None:
"""Make Windows report real pixel coordinates (no effect elsewhere)."""
if sys.platform == "win32":
ctypes.windll.shcore.SetProcessDpiAwareness(2) # type: ignore[attr-defined,unused-ignore]
def _pump(root: object, seconds: float) -> None:
"""Run the Tk event loop for a while.
@param root The Tk root window.
@param seconds How long to run.
"""
end = time.monotonic() + seconds
while time.monotonic() < end:
root.update() # type: ignore[attr-defined]
time.sleep(0.01)
def capture(output_dir: Path) -> list[Path]:
"""Capture the level screen, then the front and the back of one card.
@param output_dir Folder to write `screenshot-level.png`,
`screenshot-front.png` and `screenshot-back.png` into.
@return The three files written: level, front, back.
"""
_make_dpi_aware()
output_dir.mkdir(parents=True, exist_ok=True)
random.seed(SCREENSHOT_SEED)
level = output_dir / "screenshot-level.png"
front = output_dir / "screenshot-front.png"
back = output_dir / "screenshot-back.png"
with tempfile.TemporaryDirectory() as data_dir:
shutil.copy(DEFAULT_DATA_DIR / VOCABULARY_FILE_NAME, data_dir)
application = create_application(Path(data_dir), speech_enabled=False)
root = application.root
root.lift()
root.attributes("-topmost", True)
_pump(root, SETTLE_SECONDS)
window_box = (
root.winfo_rootx(),
root.winfo_rooty(),
root.winfo_rootx() + root.winfo_width(),
root.winfo_rooty() + root.winfo_height(),
)
ImageGrab.grab(bbox=window_box).save(level)
application.start(Level(None))
view = application.view
if not isinstance(view, FlashCardView):
raise RuntimeError("the card window did not open")
_pump(root, SETTLE_SECONDS)
box = (
view.canvas.winfo_rootx() - 20,
view.canvas.winfo_rooty() - 20,
view.canvas.winfo_rootx() + view.canvas.winfo_width() + 20,
view.canvas.winfo_rooty()
+ view.canvas.winfo_height()
+ view.right_button.winfo_height()
+ 20,
)
ImageGrab.grab(bbox=box).save(front)
_pump(root, FLIP_DELAY_MS / 1000 + SETTLE_SECONDS)
ImageGrab.grab(bbox=box).save(back)
root.destroy()
return [level, front, back]
def main(argv: Sequence[str] | None = None) -> int:
"""Run the tool.
@param argv Command line arguments, or None for `sys.argv`.
@return The exit code, 0 on success.
"""
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
args = parser.parse_args(argv)
for path in capture(args.output_dir):
sys.stdout.write(f"{path}\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())