Files
031-flash-card/tools/capture_screenshots.py
Tirsvad 92894268c5 MIL-007: Documentation and publication
Review record RC-029 (Go, autonomous mode, not independent).

Task: MIL-007#1
Task: MIL-007#2
Task: MIL-007#3
Task: MIL-007#4
Task: MIL-007#5
Task: MIL-007#6
Task: MIL-007#7
Task: MIL-007#8
2026-10-14 22:42:26 +08:00

112 lines
3.5 KiB
Python

"""Capture screenshots of the running flash card window.
@file capture_screenshots.py
@brief Starts the program, grabs the front of a card and, three seconds later,
the back, and saves both 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.presentation.app import create_application # noqa: E402
## @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 front and the back of one card.
@param output_dir Folder to write `screenshot-front.png` and
`screenshot-back.png` into.
@return The two files written, front first.
"""
_make_dpi_aware()
output_dir.mkdir(parents=True, exist_ok=True)
random.seed(SCREENSHOT_SEED)
with tempfile.TemporaryDirectory() as data_dir:
shutil.copy(DEFAULT_DATA_DIR / VOCABULARY_FILE_NAME, data_dir)
root, view = create_application(Path(data_dir), speech_enabled=False)
root.lift()
root.attributes("-topmost", True)
_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,
)
front = output_dir / "screenshot-front.png"
back = output_dir / "screenshot-back.png"
ImageGrab.grab(bbox=box).save(front)
_pump(root, FLIP_DELAY_MS / 1000 + SETTLE_SECONDS)
ImageGrab.grab(bbox=box).save(back)
root.destroy()
return [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())