From 96c7e4a6d36e868a67382241547e18489c1b09ee Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 21:31:40 +0800 Subject: [PATCH] feat: screenshot capture tool and screenshots of the real window Task: MIL-003#1 --- docs/images/add-confirmation.png | Bin 0 -> 15194 bytes docs/images/main-window.png | Bin 0 -> 7279 bytes docs/images/search-found.png | Bin 0 -> 12823 bytes docs/images/search-not-found.png | Bin 0 -> 11601 bytes tools/capture_screenshots.py | 266 +++++++++++++++++++++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 docs/images/add-confirmation.png create mode 100644 docs/images/main-window.png create mode 100644 docs/images/search-found.png create mode 100644 docs/images/search-not-found.png create mode 100644 tools/capture_screenshots.py diff --git a/docs/images/add-confirmation.png b/docs/images/add-confirmation.png new file mode 100644 index 0000000000000000000000000000000000000000..538a3f1111ce6e35eb4b8dfc3281ee5cdf2ee005 GIT binary patch literal 15194 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 Kmh?>rPX8an`4fi# literal 0 HcmV?d00001 diff --git a/docs/images/search-not-found.png b/docs/images/search-not-found.png new file mode 100644 index 0000000000000000000000000000000000000000..099e88ffb28acff9c26242e12179e758689c37c3 GIT binary patch literal 11601 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 L>e=Koh7bQ2NW=po literal 0 HcmV?d00001 diff --git a/tools/capture_screenshots.py b/tools/capture_screenshots.py new file mode 100644 index 0000000..d8e7692 --- /dev/null +++ b/tools/capture_screenshots.py @@ -0,0 +1,266 @@ +"""@brief Capture the README and wiki screenshots of the real Tkinter window. + +@file capture_screenshots.py +@details Windows only (GDI through ctypes), standard library only. Each state +runs in its own process, uses a temporary data file and never touches the +user's `data.json`. Only the windows of the program itself are rendered +(`PrintWindow`) onto a neutral canvas, so nothing from the desktop appears. +Usage, from the repository root with the venv active: + + python tools/capture_screenshots.py # writes docs/images/*.png + python tools/capture_screenshots.py --out DIR # another output folder +""" + +import argparse +import ctypes +import os +import struct +import subprocess +import sys +import tempfile +import tkinter as tk +import zlib +from ctypes import wintypes +from pathlib import Path + +STATES = { + "main-window": "Window after Generate Password", + "add-confirmation": "Confirmation before an entry is saved", + "search-found": "Search finds a saved website", + "search-not-found": "Search for an unknown website", +} +SAMPLE_WEBSITE = "example.com" +SAMPLE_EMAIL = "jens@example.com" +SAMPLE_PASSWORD = "Sample#Pass123" +UNKNOWN_WEBSITE = "unknown.org" +PADDING = 24 +CANVAS_PIXEL = bytes([0xEE, 0xEE, 0xEE, 0xFF]) +CAPTURE_DELAY_MS = 900 +PW_RENDERFULLCONTENT = 2 +DWMWA_EXTENDED_FRAME_BOUNDS = 9 + + +class BitmapInfoHeader(ctypes.Structure): + """@brief Win32 BITMAPINFOHEADER.""" + + _fields_ = [ + ("biSize", wintypes.DWORD), + ("biWidth", wintypes.LONG), + ("biHeight", wintypes.LONG), + ("biPlanes", wintypes.WORD), + ("biBitCount", wintypes.WORD), + ("biCompression", wintypes.DWORD), + ("biSizeImage", wintypes.DWORD), + ("biXPelsPerMeter", wintypes.LONG), + ("biYPelsPerMeter", wintypes.LONG), + ("biClrUsed", wintypes.DWORD), + ("biClrImportant", wintypes.DWORD), + ] + + +def write_png(path: Path, width: int, height: int, bgra: bytes) -> None: + """@brief Write a BGRA pixel buffer as an RGB PNG file. + + @param path Output file. + @param width Image width in pixels. + @param height Image height in pixels. + @param bgra Raw pixels, 4 bytes each, top row first. + """ + stride = width * 4 + raw = bytearray() + for y in range(height): + row = bgra[y * stride : (y + 1) * stride] + raw.append(0) + raw += bytes( + channel + for x in range(width) + for channel in (row[x * 4 + 2], row[x * 4 + 1], row[x * 4]) + ) + + def chunk(kind: bytes, data: bytes) -> bytes: + body = kind + data + checksum = zlib.crc32(body) & 0xFFFFFFFF + return struct.pack(">I", len(data)) + body + struct.pack(">I", checksum) + + header = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0) + png = ( + b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", header) + + chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + + chunk(b"IEND", b"") + ) + path.write_bytes(png) + + +def render_window(hwnd: int, width: int, height: int) -> bytes: + """@brief Render one window's own pixels (never the desktop behind it). + + @param hwnd Window handle. + @param width Window width in pixels. + @param height Window height in pixels. + @return Raw BGRA pixels, top row first. + """ + user32 = ctypes.windll.user32 + gdi32 = ctypes.windll.gdi32 + screen_dc = user32.GetDC(0) + memory_dc = gdi32.CreateCompatibleDC(screen_dc) + bitmap = gdi32.CreateCompatibleBitmap(screen_dc, width, height) + gdi32.SelectObject(memory_dc, bitmap) + user32.PrintWindow(hwnd, memory_dc, PW_RENDERFULLCONTENT) + info = BitmapInfoHeader( + ctypes.sizeof(BitmapInfoHeader), width, -height, 1, 32, 0, 0, 0, 0, 0, 0 + ) + buffer = ctypes.create_string_buffer(width * height * 4) + gdi32.GetDIBits(memory_dc, bitmap, 0, height, buffer, ctypes.byref(info), 0) + gdi32.DeleteObject(bitmap) + gdi32.DeleteDC(memory_dc) + user32.ReleaseDC(0, screen_dc) + return buffer.raw + + +def own_windows() -> list[int]: + """@brief Visible top-level windows of this process, topmost first. + + @return Window handles. + """ + user32 = ctypes.windll.user32 + found: list[int] = [] + callback_type = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) + + def visit(hwnd: int, _lparam: int) -> bool: + pid = wintypes.DWORD() + user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid)) + if pid.value == os.getpid() and user32.IsWindowVisible(hwnd): + found.append(hwnd) + return True + + user32.EnumWindows(callback_type(visit), 0) + return found + + +def frame_and_window(hwnd: int) -> tuple[wintypes.RECT, wintypes.RECT]: + """@brief Visible frame and full window rectangle of a window. + + @param hwnd Window handle. + @return The extended frame bounds and the GetWindowRect rectangle. + """ + frame, outer = wintypes.RECT(), wintypes.RECT() + ctypes.windll.dwmapi.DwmGetWindowAttribute( + hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, ctypes.byref(frame), ctypes.sizeof(frame) + ) + ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(outer)) + return frame, outer + + +def compose_windows() -> tuple[int, int, bytes]: + """@brief Render this process's windows side by side on a neutral canvas. + + @return Canvas width, height and BGRA pixels. + """ + pieces = [] + for hwnd in own_windows(): + frame, outer = frame_and_window(hwnd) + width, height = outer.right - outer.left, outer.bottom - outer.top + pixels = render_window(hwnd, width, height) + offset_x, offset_y = frame.left - outer.left, frame.top - outer.top + piece_width = frame.right - frame.left + piece_height = frame.bottom - frame.top + rows = [ + pixels[((offset_y + y) * width + offset_x) * 4 :][: piece_width * 4] + for y in range(piece_height) + ] + pieces.append((frame.left, frame.top, piece_width, piece_height, rows)) + left = min(piece[0] for piece in pieces) - PADDING + top = min(piece[1] for piece in pieces) - PADDING + right = max(piece[0] + piece[2] for piece in pieces) + PADDING + bottom = max(piece[1] + piece[3] for piece in pieces) + PADDING + width, height = right - left, bottom - top + canvas = [bytearray(CANVAS_PIXEL * width) for _ in range(height)] + for x, y, piece_width, _piece_height, rows in reversed(pieces): + for index, row in enumerate(rows): + start = (x - left) * 4 + canvas[y - top + index][start : start + piece_width * 4] = row + return width, height, b"".join(bytes(row) for row in canvas) + + +def run_state(state: str, out_file: Path) -> None: + """@brief Show the window in one state, capture it and exit the process. + + @param state One of the keys of STATES. + @param out_file PNG file to write. + """ + ctypes.windll.user32.SetProcessDPIAware() + from password_manager.application.use_cases import ( + FindPasswordEntry, + GeneratePassword, + SavePasswordEntry, + ) + from password_manager.domain.password_entry import PasswordEntry + from password_manager.infrastructure.persistence.json_password_repository import ( + JsonPasswordRepository, + ) + from password_manager.presentation.features.password_manager import ( + password_manager_view as view_module, + ) + from password_manager.presentation.features.password_manager import ( + password_manager_view_model as view_model_module, + ) + + with tempfile.TemporaryDirectory() as folder: + repository = JsonPasswordRepository(Path(folder) / "data.json") + repository.add(PasswordEntry(SAMPLE_WEBSITE, SAMPLE_EMAIL, SAMPLE_PASSWORD)) + view_model = view_model_module.PasswordManagerViewModel( + SavePasswordEntry(repository), + FindPasswordEntry(repository), + GeneratePassword(), + ) + root = tk.Tk() + view = view_module.PasswordManagerView(root, view_model) + root.geometry("+120+120") + + def capture() -> None: + width, height, pixels = compose_windows() + write_png(out_file, width, height, pixels) + os._exit(0) + + def act() -> None: + website = UNKNOWN_WEBSITE if state == "search-not-found" else SAMPLE_WEBSITE + view.website_entry.insert(0, website) + if state == "main-window": + view.generate_password() + root.after(CAPTURE_DELAY_MS, capture) + return + root.after(CAPTURE_DELAY_MS, capture) + if state == "add-confirmation": + view.password_entry.insert(0, SAMPLE_PASSWORD) + view.save() + else: + view.find_password() + + root.after(400, act) + root.mainloop() + + +def main() -> None: + """@brief Capture every state, each in a child process.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=Path("docs/images")) + parser.add_argument("--state", choices=sorted(STATES), help="internal use") + parser.add_argument("--file", type=Path, help="internal use") + args = parser.parse_args() + if args.state: + run_state(args.state, args.file) + return + args.out.mkdir(parents=True, exist_ok=True) + for state in STATES: + target = args.out / f"{state}.png" + subprocess.run( + [sys.executable, __file__, "--state", state, "--file", str(target)], + check=True, + timeout=60, + ) + print(f"wrote {target}") + + +if __name__ == "__main__": + main()