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
130 lines
4.3 KiB
Python
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())
|