[Scummvm-git-logs] scummvm-sites director-buildbot -> c9bbcf1f1fd0bb98ca8b765df1e87fc4d2f4b44b

sev- noreply at scummvm.org
Sat Jun 13 18:00:48 UTC 2026


This automated email contains information about 3 new commits which have been
pushed to the 'scummvm-sites' repo located at https://api.github.com/repos/scummvm/scummvm-sites .

Summary:
881dbb814a BUILDBOT: Add imagediff tool
29eed1586c BUILDBOT: Add flask to pyproject.toml and update uv.lock
c9bbcf1f1f BUILDBOT: Fix imagediff import and move utility functions to imagediff


Commit: 881dbb814a0244e7dd248f778feac95728399711
    https://github.com/scummvm/scummvm-sites/commit/881dbb814a0244e7dd248f778feac95728399711
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-06-13T20:00:44+02:00

Commit Message:
BUILDBOT: Add imagediff tool

Changed paths:
  A director/screenshot_diff.py
    director/env.py
    director/steps.py
    director/targets.py
    pyproject.toml


diff --git a/director/env.py b/director/env.py
index 39abdf8..290a46e 100644
--- a/director/env.py
+++ b/director/env.py
@@ -23,6 +23,10 @@ default_vars = {
     # base directory which contains all the targets
     "TARGETS_BASEDIR": "~/wb1/",
     "UI": False,
+    # Screenshot comparison (ImageDiff integration)
+    "SCREENSHOTS_DIR": "/home/director-buildbot/screenshots",
+    "IMAGEDIFF_URL": "",
+    "SCREENSHOT_DIFF_STRICT": False,
 }
 
 
diff --git a/director/screenshot_diff.py b/director/screenshot_diff.py
new file mode 100644
index 0000000..6173c7e
--- /dev/null
+++ b/director/screenshot_diff.py
@@ -0,0 +1,267 @@
+"""Screenshot comparison for Director buildbot tests.
+
+Uses comparison logic from imagediff/imagediff.py. Invoked on the worker by
+ScreenshotDiffStep after ScummVMTest when screenshot debugflags are enabled.
+"""
+
+from __future__ import annotations
+
+import argparse
+import importlib.util
+import os
+import sys
+import urllib.parse
+from typing import Callable, Optional
+
+EXIT_OK = 0
+EXIT_DIFF = 1
+EXIT_NO_BASELINE = 2
+EXIT_ERROR = 3
+
+_repo_root = os.path.dirname(os.path.dirname(__file__))
+_seen_prefixes_name = ".screenshot_prefixes_seen"
+
+
+def _load_movie_diff(screenshots_dir: str) -> Callable[..., bool]:
+    """Load movie_diff from the vendored imagediff package."""
+    os.environ["SCREENSHOTS_DIR"] = screenshots_dir
+    imagediff_dir = os.path.join(_repo_root, "imagediff")
+
+    config_spec = importlib.util.spec_from_file_location(
+        "imagediff_config", os.path.join(imagediff_dir, "config.py")
+    )
+    if config_spec is None or config_spec.loader is None:
+        raise ImportError("unable to load imagediff config")
+    config_module = importlib.util.module_from_spec(config_spec)
+    sys.modules["config"] = config_module
+    config_spec.loader.exec_module(config_module)
+
+    core_spec = importlib.util.spec_from_file_location(
+        "imagediff_core", os.path.join(imagediff_dir, "imagediff.py")
+    )
+    if core_spec is None or core_spec.loader is None:
+        raise ImportError("unable to load imagediff core")
+    core_module = importlib.util.module_from_spec(core_spec)
+    core_spec.loader.exec_module(core_module)
+    return core_module.movie_diff
+
+
+def list_movie_prefixes(build_path: str) -> list[str]:
+    """Return sorted movie filename prefixes in a build directory."""
+    prefixes: set[str] = set()
+    if not os.path.isdir(build_path):
+        return []
+
+    for name in os.listdir(build_path):
+        if not name.endswith(".png") or "-" not in name:
+            continue
+        prefixes.add(name.rsplit("-", 1)[0])
+
+    return sorted(prefixes)
+
+
+def _seen_prefixes_path(screenshots_dir: str, target: str, build: str) -> str:
+    return os.path.join(screenshots_dir, target, build, _seen_prefixes_name)
+
+
+def _read_seen_prefixes(path: str) -> set[str]:
+    if not os.path.isfile(path):
+        return set()
+    with open(path, encoding="utf-8") as handle:
+        return {line.strip() for line in handle if line.strip()}
+
+
+def _write_seen_prefixes(path: str, prefixes: set[str]) -> None:
+    with open(path, "w", encoding="utf-8") as handle:
+        for prefix in sorted(prefixes):
+            handle.write(f"{prefix}\n")
+
+
+def prefixes_for_step(
+    screenshots_dir: str,
+    target: str,
+    build: str,
+    *,
+    only_new: bool,
+) -> list[str]:
+    """Return prefixes to compare for this step."""
+    build_path = os.path.join(screenshots_dir, target, build)
+    current = set(list_movie_prefixes(build_path))
+    if not only_new:
+        return sorted(current)
+
+    seen_path = _seen_prefixes_path(screenshots_dir, target, build)
+    seen = _read_seen_prefixes(seen_path)
+    new_prefixes = sorted(current - seen)
+    _write_seen_prefixes(seen_path, seen | set(new_prefixes))
+    return new_prefixes
+
+
+def find_baseline_build(
+    screenshots_dir: str, target: str, current_build: str
+) -> Optional[str]:
+    """Return the highest prior build number that has stored screenshots."""
+    target_path = os.path.join(screenshots_dir, target)
+    if not os.path.isdir(target_path):
+        return None
+
+    try:
+        current_number = int(current_build)
+    except ValueError:
+        return None
+
+    candidates: list[int] = []
+    for entry in os.listdir(target_path):
+        path = os.path.join(target_path, entry)
+        if not os.path.isdir(path):
+            continue
+        try:
+            build_number = int(entry)
+        except ValueError:
+            continue
+        if build_number < current_number:
+            candidates.append(build_number)
+
+    if not candidates:
+        return None
+
+    return str(max(candidates))
+
+
+def compare_url(
+    imagediff_url: str,
+    current_build: str,
+    baseline_build: str,
+    target: str,
+    movie_prefix: str,
+) -> str:
+    """Build an ImageDiff compare URL for a movie prefix."""
+    base = imagediff_url.rstrip("/")
+    movie = urllib.parse.quote(movie_prefix, safe="")
+    return f"{base}/compare/{current_build}/{baseline_build}/{target}/{movie}"
+
+
+def check_screenshots(
+    screenshots_dir: str,
+    target: str,
+    current_build: str,
+    baseline_build: Optional[str] = None,
+    movie_prefix: Optional[str] = None,
+    *,
+    only_new: bool = True,
+) -> tuple[int, str]:
+    """Compare screenshots and return an exit code plus a human-readable summary."""
+    current_path = os.path.join(screenshots_dir, target, current_build)
+    if not os.path.isdir(current_path):
+        return EXIT_NO_BASELINE, (
+            f"No screenshots found for target '{target}' build {current_build} "
+            f"under {screenshots_dir}"
+        )
+
+    if baseline_build is None:
+        baseline_build = find_baseline_build(screenshots_dir, target, current_build)
+
+    if baseline_build is None:
+        return EXIT_NO_BASELINE, (
+            f"No baseline screenshots for target '{target}' before build {current_build}"
+        )
+
+    if movie_prefix:
+        prefixes = [movie_prefix]
+    elif only_new:
+        prefixes = prefixes_for_step(
+            screenshots_dir, target, current_build, only_new=True
+        )
+    else:
+        prefixes = list_movie_prefixes(current_path)
+
+    if not prefixes:
+        return EXIT_NO_BASELINE, (
+            f"No new screenshot frames to compare for target '{target}' "
+            f"build {current_build}"
+        )
+
+    try:
+        movie_diff = _load_movie_diff(screenshots_dir)
+    except ImportError as exc:
+        return EXIT_ERROR, f"Failed to load imagediff: {exc}"
+
+    diff_prefixes = [
+        prefix
+        for prefix in prefixes
+        if movie_diff(current_build, baseline_build, target, prefix)
+    ]
+
+    if not diff_prefixes:
+        return EXIT_OK, (
+            f"Screenshots match baseline build {baseline_build} "
+            f"for target '{target}' build {current_build}"
+        )
+
+    lines = [
+        "Screenshot differences detected:",
+        f"  target: {target}",
+        f"  current build: {current_build}",
+        f"  baseline build: {baseline_build}",
+        "  changed movies:",
+    ]
+    lines.extend(f"    - {prefix}" for prefix in diff_prefixes)
+    return EXIT_DIFF, "\n".join(lines)
+
+
+def main(argv: Optional[list[str]] = None) -> int:
+    parser = argparse.ArgumentParser(description="Compare Director buildbot screenshots")
+    parser.add_argument("--screenshots-dir", default=os.environ.get("SCREENSHOTS_DIR", ""))
+    parser.add_argument("--target", required=True, help="ScummVM target name (game_id)")
+    parser.add_argument("--build", required=True, help="Current build number")
+    parser.add_argument("--baseline", help="Baseline build number (default: previous build)")
+    parser.add_argument("--movie-prefix", help="Compare only this ScummVM movie prefix")
+    parser.add_argument(
+        "--all-prefixes",
+        action="store_true",
+        help="Compare every prefix in the build directory, not only new ones",
+    )
+    parser.add_argument("--imagediff-url", default=os.environ.get("IMAGEDIFF_URL", ""))
+    args = parser.parse_args(argv)
+
+    if not args.screenshots_dir:
+        print("SCREENSHOTS_DIR is not configured", file=sys.stderr)
+        return EXIT_ERROR
+
+    exit_code, message = check_screenshots(
+        args.screenshots_dir,
+        args.target,
+        args.build,
+        baseline_build=args.baseline,
+        movie_prefix=args.movie_prefix,
+        only_new=not args.all_prefixes,
+    )
+
+    print(message)
+    if exit_code == EXIT_DIFF and args.imagediff_url:
+        baseline = args.baseline or find_baseline_build(
+            args.screenshots_dir, args.target, args.build
+        )
+        if baseline:
+            diff_prefixes = [
+                line.removeprefix("    - ")
+                for line in message.splitlines()
+                if line.startswith("    - ")
+            ]
+            print("\nCompare in ImageDiff:")
+            for prefix in diff_prefixes:
+                print(
+                    compare_url(
+                        args.imagediff_url,
+                        args.build,
+                        baseline,
+                        args.target,
+                        prefix,
+                    )
+                )
+
+    return exit_code
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/director/steps.py b/director/steps.py
index 95c2110..bbf98c0 100644
--- a/director/steps.py
+++ b/director/steps.py
@@ -1,15 +1,18 @@
+import os
 import re
 from typing import Any, Optional
 
 from buildbot import config
 from buildbot.plugins import steps, util
 from buildbot.process import buildstep, logobserver
-from buildbot.process.properties import Property
-from buildbot.process.results import FAILURE, worst_status
+from buildbot.process.results import FAILURE, SUCCESS, WARNINGS, worst_status
 from twisted.internet import defer
 
 from .build_factory import default_env, master_file, worker_file
 from .env import settings
+from .screenshot_diff import EXIT_DIFF, EXIT_ERROR, EXIT_NO_BASELINE
+
+_master_dir = os.path.dirname(os.path.dirname(__file__))
 
 download_step = steps.FileDownload(
     mastersrc=master_file,
@@ -103,6 +106,70 @@ class ScummVMTest(steps.WarningCountingShellCommand):
         return result
 
 
+class ScreenshotDiffStep(steps.ShellCommand):
+    """Compare screenshots from the current build against the previous build."""
+
+    renderables = ["command", "env"]
+
+    def __init__(
+        self,
+        target_key: str,
+        movie_prefix: Optional[str] = None,
+        strict: Optional[bool] = None,
+        **kwargs: Any,
+    ):
+        if strict is None:
+            strict_value = settings["SCREENSHOT_DIFF_STRICT"]
+            if isinstance(strict_value, bool):
+                strict = strict_value
+            else:
+                strict = str(strict_value).lower() in ("1", "true", "yes")
+        self.strict = strict
+
+        env = default_env.copy()
+        imagediff_dir = os.path.join(_master_dir, "imagediff")
+        env["PYTHONPATH"] = os.pathsep.join([_master_dir, imagediff_dir])
+        env["SCREENSHOTS_DIR"] = settings["SCREENSHOTS_DIR"]
+        if settings["IMAGEDIFF_URL"]:
+            env["IMAGEDIFF_URL"] = settings["IMAGEDIFF_URL"]
+
+        command = [
+            "python3",
+            "-m",
+            "director.screenshot_diff",
+            "--screenshots-dir",
+            settings["SCREENSHOTS_DIR"],
+            "--target",
+            target_key,
+            "--build",
+            util.Interpolate("%(prop:buildnumber)s"),
+        ]
+        if movie_prefix:
+            command.extend(["--movie-prefix", movie_prefix])
+        if settings["IMAGEDIFF_URL"]:
+            command.extend(["--imagediff-url", settings["IMAGEDIFF_URL"]])
+
+        kwargs.setdefault("haltOnFailure", False)
+        kwargs.setdefault("flunkOnFailure", False)
+        kwargs.setdefault("warnOnFailure", False)
+        kwargs["command"] = command
+        kwargs["env"] = env
+        kwargs["logEnviron"] = False
+        super().__init__(**kwargs)
+
+    def evaluateCommand(self, cmd):
+        exit_code = cmd.exitCode
+        if exit_code in (0, None):
+            return SUCCESS
+        if exit_code == EXIT_NO_BASELINE:
+            return SUCCESS
+        if exit_code == EXIT_DIFF:
+            return FAILURE if self.strict else WARNINGS
+        if exit_code == EXIT_ERROR:
+            return FAILURE
+        return super().evaluateCommand(cmd)
+
+
 class GenerateStartMovieCommands(buildstep.ShellMixin, steps.BuildStep):
     """Generate the steps to build all lingo files."""
 
diff --git a/director/targets.py b/director/targets.py
index 1e0535e..8a07925 100644
--- a/director/targets.py
+++ b/director/targets.py
@@ -7,7 +7,7 @@ from buildbot.plugins import steps, util
 
 from .build_factory import default_env
 from .env import settings
-from .steps import ScummVMTest, download_step
+from .steps import ScummVMTest, ScreenshotDiffStep, download_step
 
 
 @dataclass(frozen=True)
@@ -61,7 +61,7 @@ def generate_command(target: TestTarget, moviename: str) -> list[str]:
         "../scummvm",
         "-c",
         "scummvm.conf",
-        "--screenshotpath=/home/director-buildbot/screenshots",
+        f"--screenshotpath={settings['SCREENSHOTS_DIR']}",
         f"--start-movie={moviename}",
     ]
     if target.debugflags:
@@ -96,6 +96,7 @@ def generate_builder(target: TestTarget, workernames: list[str]) -> BuilderConfi
             logEnviron=False,
         )
     )
+    screenshot_enabled = "screenshot" in target.debugflags
     for moviename in target.movienames:
         name = moviename
         env = default_env.copy()
@@ -120,6 +121,18 @@ def generate_builder(target: TestTarget, workernames: list[str]) -> BuilderConfi
                 logEnviron=False,
             )
         )
+        if screenshot_enabled:
+            diff_name = f"{name} screenshot diff"
+            if len(diff_name) > 49:
+                diff_name = diff_name[-49:]
+            factory.addStep(
+                ScreenshotDiffStep(
+                    name=diff_name,
+                    description=f"Compare screenshots for {moviename}",
+                    descriptionDone=f"Compared screenshots for {moviename}",
+                    target_key=target.game_id,
+                )
+            )
 
     return BuilderConfig(
         name=target.builder_name, workernames=workernames, factory=factory
diff --git a/pyproject.toml b/pyproject.toml
index d445571..04a4306 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,6 +10,7 @@ dependencies = [
   "buildbot[bundle]<4.4.0",
   "psycopg2>=2.9.5,<3.0.0",
   "certifi",
+  "Pillow>=10.0.0",
 ]
 
 [dependency-groups]


Commit: 29eed1586cf022ee2b2a903a498a2478d24dd9f8
    https://github.com/scummvm/scummvm-sites/commit/29eed1586cf022ee2b2a903a498a2478d24dd9f8
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-06-13T20:00:44+02:00

Commit Message:
BUILDBOT: Add flask to pyproject.toml and update uv.lock

Changed paths:
    pyproject.toml
    uv.lock


diff --git a/pyproject.toml b/pyproject.toml
index 04a4306..027b0ee 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,6 +11,7 @@ dependencies = [
   "psycopg2>=2.9.5,<3.0.0",
   "certifi",
   "Pillow>=10.0.0",
+  "flask>=3.0.0",
 ]
 
 [dependency-groups]
diff --git a/uv.lock b/uv.lock
index 59ec676..9bc047d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -238,6 +238,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" },
 ]
 
+[[package]]
+name = "blinker"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
+]
+
 [[package]]
 name = "buildbot"
 version = "4.3.0"
@@ -293,6 +302,8 @@ dependencies = [
     { name = "buildbot", extra = ["bundle"] },
     { name = "certifi" },
     { name = "environs" },
+    { name = "flask" },
+    { name = "pillow" },
     { name = "psycopg2" },
     { name = "treq" },
 ]
@@ -311,6 +322,8 @@ requires-dist = [
     { name = "buildbot", extras = ["bundle"], specifier = "<4.4.0" },
     { name = "certifi" },
     { name = "environs" },
+    { name = "flask", specifier = ">=3.0.0" },
+    { name = "pillow", specifier = ">=10.0.0" },
     { name = "psycopg2", specifier = ">=2.9.5,<3.0.0" },
     { name = "treq" },
 ]
@@ -776,6 +789,23 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
 ]
 
+[[package]]
+name = "flask"
+version = "3.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "blinker" },
+    { name = "click" },
+    { name = "itsdangerous" },
+    { name = "jinja2" },
+    { name = "markupsafe" },
+    { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
+]
+
 [[package]]
 name = "greenlet"
 version = "3.5.1"
@@ -969,6 +999,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" },
 ]
 
+[[package]]
+name = "itsdangerous"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
+]
+
 [[package]]
 name = "jedi"
 version = "0.20.0"
@@ -1386,6 +1425,104 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
 ]
 
+[[package]]
+name = "pillow"
+version = "12.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" },
+    { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" },
+    { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" },
+    { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" },
+    { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" },
+    { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" },
+    { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
+    { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
+    { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
+    { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
+    { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
+    { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
+    { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
+    { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
+    { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
+    { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
+    { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
+    { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
+    { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
+    { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
+    { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
+    { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
+    { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
+    { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
+    { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
+    { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
+    { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
+    { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
+    { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
+    { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" },
+    { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
+]
+
 [[package]]
 name = "platformdirs"
 version = "4.9.6"
@@ -2070,6 +2207,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" },
 ]
 
+[[package]]
+name = "werkzeug"
+version = "3.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
+]
+
 [[package]]
 name = "zope-interface"
 version = "8.4"


Commit: c9bbcf1f1fd0bb98ca8b765df1e87fc4d2f4b44b
    https://github.com/scummvm/scummvm-sites/commit/c9bbcf1f1fd0bb98ca8b765df1e87fc4d2f4b44b
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-06-13T20:00:44+02:00

Commit Message:
BUILDBOT: Fix imagediff import and move utility functions to imagediff

imagediff/imagediff.py no uses imagediff/ by inserting into sys.path before importing, replacing the previous importlib workaround.

Also moved list_movie_prefixes and find_baseline_build into imagediff/imagediff.py and added a sceenshots_dir parameter to movie_diff.

Changed paths:
    director/screenshot_diff.py
    imagediff/imagediff.py


diff --git a/director/screenshot_diff.py b/director/screenshot_diff.py
index 6173c7e..4259b5c 100644
--- a/director/screenshot_diff.py
+++ b/director/screenshot_diff.py
@@ -7,11 +7,10 @@ ScreenshotDiffStep after ScummVMTest when screenshot debugflags are enabled.
 from __future__ import annotations
 
 import argparse
-import importlib.util
 import os
 import sys
 import urllib.parse
-from typing import Callable, Optional
+from typing import Optional
 
 EXIT_OK = 0
 EXIT_DIFF = 1
@@ -19,45 +18,15 @@ EXIT_NO_BASELINE = 2
 EXIT_ERROR = 3
 
 _repo_root = os.path.dirname(os.path.dirname(__file__))
+_imagediff_dir = os.path.join(_repo_root, "imagediff")
 _seen_prefixes_name = ".screenshot_prefixes_seen"
 
+# imagediff/imagediff.py does `from config import SCREENSHOTS_DIR` at module
+# level, so imagediff/ must be on sys.path before we import it.
+if _imagediff_dir not in sys.path:
+    sys.path.insert(0, _imagediff_dir)
 
-def _load_movie_diff(screenshots_dir: str) -> Callable[..., bool]:
-    """Load movie_diff from the vendored imagediff package."""
-    os.environ["SCREENSHOTS_DIR"] = screenshots_dir
-    imagediff_dir = os.path.join(_repo_root, "imagediff")
-
-    config_spec = importlib.util.spec_from_file_location(
-        "imagediff_config", os.path.join(imagediff_dir, "config.py")
-    )
-    if config_spec is None or config_spec.loader is None:
-        raise ImportError("unable to load imagediff config")
-    config_module = importlib.util.module_from_spec(config_spec)
-    sys.modules["config"] = config_module
-    config_spec.loader.exec_module(config_module)
-
-    core_spec = importlib.util.spec_from_file_location(
-        "imagediff_core", os.path.join(imagediff_dir, "imagediff.py")
-    )
-    if core_spec is None or core_spec.loader is None:
-        raise ImportError("unable to load imagediff core")
-    core_module = importlib.util.module_from_spec(core_spec)
-    core_spec.loader.exec_module(core_module)
-    return core_module.movie_diff
-
-
-def list_movie_prefixes(build_path: str) -> list[str]:
-    """Return sorted movie filename prefixes in a build directory."""
-    prefixes: set[str] = set()
-    if not os.path.isdir(build_path):
-        return []
-
-    for name in os.listdir(build_path):
-        if not name.endswith(".png") or "-" not in name:
-            continue
-        prefixes.add(name.rsplit("-", 1)[0])
-
-    return sorted(prefixes)
+import imagediff as _imagediff  # imagediff/imagediff.py
 
 
 def _seen_prefixes_path(screenshots_dir: str, target: str, build: str) -> str:
@@ -86,7 +55,7 @@ def prefixes_for_step(
 ) -> list[str]:
     """Return prefixes to compare for this step."""
     build_path = os.path.join(screenshots_dir, target, build)
-    current = set(list_movie_prefixes(build_path))
+    current = set(_imagediff.list_movie_prefixes(build_path))
     if not only_new:
         return sorted(current)
 
@@ -97,37 +66,6 @@ def prefixes_for_step(
     return new_prefixes
 
 
-def find_baseline_build(
-    screenshots_dir: str, target: str, current_build: str
-) -> Optional[str]:
-    """Return the highest prior build number that has stored screenshots."""
-    target_path = os.path.join(screenshots_dir, target)
-    if not os.path.isdir(target_path):
-        return None
-
-    try:
-        current_number = int(current_build)
-    except ValueError:
-        return None
-
-    candidates: list[int] = []
-    for entry in os.listdir(target_path):
-        path = os.path.join(target_path, entry)
-        if not os.path.isdir(path):
-            continue
-        try:
-            build_number = int(entry)
-        except ValueError:
-            continue
-        if build_number < current_number:
-            candidates.append(build_number)
-
-    if not candidates:
-        return None
-
-    return str(max(candidates))
-
-
 def compare_url(
     imagediff_url: str,
     current_build: str,
@@ -159,7 +97,9 @@ def check_screenshots(
         )
 
     if baseline_build is None:
-        baseline_build = find_baseline_build(screenshots_dir, target, current_build)
+        baseline_build = _imagediff.find_baseline_build(
+            screenshots_dir, target, current_build
+        )
 
     if baseline_build is None:
         return EXIT_NO_BASELINE, (
@@ -168,12 +108,10 @@ def check_screenshots(
 
     if movie_prefix:
         prefixes = [movie_prefix]
-    elif only_new:
+    else:
         prefixes = prefixes_for_step(
-            screenshots_dir, target, current_build, only_new=True
+            screenshots_dir, target, current_build, only_new=only_new
         )
-    else:
-        prefixes = list_movie_prefixes(current_path)
 
     if not prefixes:
         return EXIT_NO_BASELINE, (
@@ -181,15 +119,12 @@ def check_screenshots(
             f"build {current_build}"
         )
 
-    try:
-        movie_diff = _load_movie_diff(screenshots_dir)
-    except ImportError as exc:
-        return EXIT_ERROR, f"Failed to load imagediff: {exc}"
-
     diff_prefixes = [
         prefix
         for prefix in prefixes
-        if movie_diff(current_build, baseline_build, target, prefix)
+        if _imagediff.movie_diff(
+            current_build, baseline_build, target, prefix, screenshots_dir
+        )
     ]
 
     if not diff_prefixes:
@@ -239,7 +174,7 @@ def main(argv: Optional[list[str]] = None) -> int:
 
     print(message)
     if exit_code == EXIT_DIFF and args.imagediff_url:
-        baseline = args.baseline or find_baseline_build(
+        baseline = args.baseline or _imagediff.find_baseline_build(
             args.screenshots_dir, args.target, args.build
         )
         if baseline:
diff --git a/imagediff/imagediff.py b/imagediff/imagediff.py
index 8609ab1..40232c3 100644
--- a/imagediff/imagediff.py
+++ b/imagediff/imagediff.py
@@ -1,7 +1,9 @@
-from PIL import Image, ImageChops
 import os
 import base64
 from io import BytesIO
+
+from PIL import Image, ImageChops
+
 from config import SCREENSHOTS_DIR
 
 def encode_image(image):
@@ -30,7 +32,40 @@ def image_diff(src_img_path, cmp_img_path):
     except IOError:
         return {}
 
-def movie_diff(src_build, cmp_build, target, movie):
+def list_movie_prefixes(build_path):
+    """Return sorted movie filename prefixes present in a build directory."""
+    prefixes = set()
+    if not os.path.isdir(build_path):
+        return []
+    for name in os.listdir(build_path):
+        if name.endswith(".png") and "-" in name:
+            prefixes.add(name.rsplit("-", 1)[0])
+    return sorted(prefixes)
+
+
+def find_baseline_build(screenshots_dir, target, current_build):
+    """Return the highest prior build number that has stored screenshots."""
+    target_path = os.path.join(screenshots_dir, target)
+    if not os.path.isdir(target_path):
+        return None
+    try:
+        current_number = int(current_build)
+    except ValueError:
+        return None
+    candidates = []
+    for entry in os.listdir(target_path):
+        if not os.path.isdir(os.path.join(target_path, entry)):
+            continue
+        try:
+            build_number = int(entry)
+        except ValueError:
+            continue
+        if build_number < current_number:
+            candidates.append(build_number)
+    return str(max(candidates)) if candidates else None
+
+
+def movie_diff(src_build, cmp_build, target, movie, screenshots_dir=None):
     """
     Compare all frames of a movie between two builds and determine if there are any differences.
 
@@ -39,12 +74,14 @@ def movie_diff(src_build, cmp_build, target, movie):
         cmp_build (str): The comparison build name
         target (str): The target name
         movie (str): The movie name
+        screenshots_dir (str): Override for SCREENSHOTS_DIR from config
 
     Returns:
         bool: True if any frame has differences, False otherwise
     """
-    src_build_path = os.path.join(SCREENSHOTS_DIR, target, src_build)
-    cmp_build_path = os.path.join(SCREENSHOTS_DIR, target, cmp_build)
+    screenshots_dir = screenshots_dir or SCREENSHOTS_DIR
+    src_build_path = os.path.join(screenshots_dir, target, src_build)
+    cmp_build_path = os.path.join(screenshots_dir, target, cmp_build)
 
     # Ensure both build paths exist
     if not os.path.exists(src_build_path) or not os.path.exists(cmp_build_path):




More information about the Scummvm-git-logs mailing list