diff --git a/pyproject.toml b/pyproject.toml
index d89958f8..753ca720 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,7 +42,7 @@ dependencies = [
"winrt-Windows.Graphics.Imaging >=3.2.1; sys_platform == 'win32'",
#
# Linux-only dependencies
- "pillow >=12.3.0; sys_platform == 'linux'", # Python 3.15 support # Necessary for ImageGrab.
+ "pillow >=13.0.0; sys_platform == 'linux'",
"python-xlib >=0.33; sys_platform == 'linux'",
]
[dependency-groups]
@@ -66,7 +66,7 @@ dev = [
"types-pywin32 >=306.0.0.20240130; sys_platform == 'win32'",
#
# OS-specific, but installable on other OSes for convenient typing references
- "pillow",
+ "pillow >=13.0.0",
"pygrabber",
"python-xlib",
"typed-D3DShot[numpy]",
@@ -102,6 +102,7 @@ override-dependencies = [
exclude-newer = "1 week"
[tool.uv.exclude-newer-package]
typed-D3DShot = false # I own it
+pillow = "6 day"
###
# Development channels
diff --git a/res/design.ui b/res/design.ui
index 9dd9e2da..22bc9f67 100644
--- a/res/design.ui
+++ b/res/design.ui
@@ -492,6 +492,9 @@
+
+ Qt::TextFormat::PlainText
+
Qt::AlignmentFlag::AlignCenter
@@ -865,6 +868,9 @@
-
+
+ Qt::TextFormat::PlainText
+
Qt::AlignmentFlag::AlignCenter
@@ -921,6 +927,9 @@
+
+ Qt::TextFormat::PlainText
+
Qt::AlignmentFlag::AlignCenter
diff --git a/src/AutoSplitImage.py b/src/AutoSplitImage.py
index db8337f8..1845ca50 100644
--- a/src/AutoSplitImage.py
+++ b/src/AutoSplitImage.py
@@ -11,7 +11,14 @@
import error_messages
from compare import extract_and_compare_text, get_comparison_method_by_index
-from utils import MAXBYTE, TESSERACT_PATH, ColorChannel, imread, is_valid_image
+from utils import (
+ MAXBYTE,
+ TESSERACT_PATH,
+ ColorChannel,
+ imread,
+ is_supported_image_file,
+ is_valid_image,
+)
if TYPE_CHECKING:
from cv2.typing import MatLike
@@ -147,6 +154,9 @@ def __validate_ocr(self):
)
def __read_image_bytes(self, path: str):
+ if not is_supported_image_file(path):
+ error_messages.image_type(path)
+ return
image = imread(path, cv2.IMREAD_UNCHANGED)
if not is_valid_image(image):
self.byte_array = None
diff --git a/src/capture_method/XcbCaptureMethod.py b/src/capture_method/XcbCaptureMethod.py
index a5cfbfb5..0cf3cf65 100644
--- a/src/capture_method/XcbCaptureMethod.py
+++ b/src/capture_method/XcbCaptureMethod.py
@@ -63,6 +63,7 @@ def get_frame(self):
x + selection["width"],
y + selection["height"],
),
+ # Passing a non-None xdisplay also prevents pillow falling back to a screenshot tool
xdisplay=self._xdisplay,
)
image = np.array(image)
diff --git a/src/error_messages.py b/src/error_messages.py
index 34100303..6beaf7e5 100644
--- a/src/error_messages.py
+++ b/src/error_messages.py
@@ -2,10 +2,12 @@
from __future__ import annotations
+import html
import os
import signal
import sys
import traceback
+from collections.abc import Mapping
from types import TracebackType
from typing import TYPE_CHECKING, NoReturn
@@ -40,9 +42,18 @@ def _set_text_message(
details: str = "",
kill_button: str = "",
accept_button: str = "",
+ *,
+ rich_text: bool = False,
):
+ """
+ Plain text by default, so dynamic values (paths, filenames, hotkeys, etc.)
+ can't inject HTML. When `rich_text` is needed (for links),
+ every dynamic value must be escaped with `html.escape`.
+ """
# Also surface the error message in the logs
- plain_message = QtGui.QTextDocumentFragment.fromHtml(message).toPlainText()
+ plain_message = (
+ QtGui.QTextDocumentFragment.fromHtml(message).toPlainText() if rich_text else message
+ )
print(
f"{plain_message}\n{details}\n" if details else f"{plain_message}",
file=sys.stderr if details else None,
@@ -50,7 +61,9 @@ def _set_text_message(
message_box = QtWidgets.QMessageBox()
message_box.setWindowTitle("Error")
- message_box.setTextFormat(QtCore.Qt.TextFormat.RichText)
+ message_box.setTextFormat(
+ QtCore.Qt.TextFormat.RichText if rich_text else QtCore.Qt.TextFormat.PlainText
+ )
message_box.setText(message)
# Button order is important for default focus
if accept_button:
@@ -156,6 +169,26 @@ def invalid_hotkey(hotkey_name: str):
_set_text_message(f"Invalid hotkey {hotkey_name!r}")
+def invalid_settings_hotkeys(hotkeys: Mapping[str, object]):
+ _set_text_message(
+ "The following hotkeys from the settings file are invalid and have been unset:\n"
+ + "\n".join(f"{name} = {value!r}" for name, value in hotkeys.items())
+ + "\nOnly a single key, optionally combined with modifiers (excluding Windows/Super), "
+ + "is allowed."
+ )
+
+
+def invalid_settings_network_directories(directories: Mapping[str, str]):
+ _set_text_message(
+ "The following folders from the settings file are network locations "
+ + "and have been unset:\n"
+ + "\n".join(f"{name} = {value!r}" for name, value in directories.items())
+ + "\nOn Windows, accessing a network location can automatically send "
+ + "your Windows credentials to that server. "
+ + "If you trust this location, map it to a drive letter instead."
+ )
+
+
def no_settings_file_on_open():
print(
"No settings file found. "
@@ -189,7 +222,7 @@ def stdin_lost():
def already_open():
_set_text_message(
"An instance of AutoSplit is already running."
- + "
Are you sure you want to open a another one?",
+ + "\nAre you sure you want to open a another one?",
"",
"Don't open",
"Ignore",
@@ -222,7 +255,7 @@ def linux_uinput():
def linux_wayland():
- _set_text_message(WAYLAND_WARNING)
+ _set_text_message(WAYLAND_WARNING, rich_text=True)
def exception_traceback(exception: BaseException, message: str = ""):
@@ -236,15 +269,17 @@ def exception_traceback(exception: BaseException, message: str = ""):
message,
"\n".join(traceback.format_exception(None, exception, exception.__traceback__)),
"Close AutoSplit",
+ rich_text=True,
)
def tesseract_missing(ocr_split_file_path: str):
_set_text_message(
- f"{ocr_split_file_path!r} is an Optical Character Recognition split file "
+ f"{html.escape(repr(ocr_split_file_path))} is an Optical Character Recognition split file "
+ "but tesseract couldn't be found."
- + f'\nPlease read '
- + f"github.com/{GITHUB_REPOSITORY}#install-tesseract for installation instructions."
+ + f'
Please read '
+ + f"github.com/{GITHUB_REPOSITORY}#install-tesseract for installation instructions.",
+ rich_text=True,
)
@@ -255,7 +290,7 @@ def ocr_missing_key(ocr_split_file_path: str, missing_key: str):
def wrong_ocr_values(ocr_split_file_path: str):
_set_text_message(
f"{ocr_split_file_path!r} has invalid values."
- + "\nPlease make sure that `left < right` and `top < bottom`. "
+ + "\nPlease make sure that `left < right` and `top < bottom`. "
+ "Also check for negative values in the 'methods' or 'fps_limit' settings"
)
diff --git a/src/hotkeys.py b/src/hotkeys.py
index 3d9e0732..36b452ee 100644
--- a/src/hotkeys.py
+++ b/src/hotkeys.py
@@ -2,14 +2,15 @@
import sys
from collections.abc import Callable
-from typing import TYPE_CHECKING, Literal, cast
+from typing import TYPE_CHECKING, Literal, TypeGuard, cast
import keyboard
import pyautogui
+from keyboard._canonical_names import normalize_name # noqa: PLC2701
from PySide6.QtTest import QTest
import error_messages
-from utils import fire_and_forget, is_digit, try_input_device_access
+from utils import fire_and_forget, flatten, is_digit, try_input_device_access
if sys.platform == "linux":
import grp
@@ -282,10 +283,40 @@ def toggle_auto_reset_image():
return getattr(autosplit, f"{hotkey}_signal").emit
-def is_valid_hotkey_name(hotkey_name: str):
- return any(
- key and not keyboard.is_modifier(keyboard.key_to_scan_codes(key)[0])
- for key in hotkey_name.split("+")
+def is_valid_hotkey_name(hotkey_name: object) -> TypeGuard[str]:
+ """
+ A valid hotkey is a single regular key, optionally combined with modifiers.
+ This is the only kind of hotkey `__read_hotkey` can produce.
+
+ Hotkeys are also sent as keystrokes, so anything else must be rejected to prevent
+ a malicious profile from injecting arbitrary keystrokes (see `_send_hotkey`).
+ This includes key sequences ("a, b"), multiple regular keys and the Windows key.
+ """
+ if not isinstance(hotkey_name, str):
+ return False
+ keys = hotkey_name.split("+")
+ if any(
+ not key
+ # keyboard.send treats commas as a separator for a sequence of hotkeys
+ or ("," in key and key != ",")
+ # The Windows key triggers OS shortcuts (ie: Win+R opens the Run dialog).
+ # keyboard normalizes aliases ("win", "cmd", "command", ...) to "[left |right ]windows"
+ or normalize_name(key).endswith("windows")
+ for key in keys
+ ):
+ return False
+
+ if KEYBOARD_GROUPS_ISSUE or KEYBOARD_UINPUT_ISSUE:
+ # Can't map to scan codes without keyboard access, fallback to canonical names
+ return sum(not keyboard.is_modifier(normalize_name(key)) for key in keys) == 1
+
+ # Scan codes also support localized key names
+ keys_scan_codes = [keyboard.key_to_scan_codes(key, error_if_missing=False) for key in keys]
+ windows_scan_codes = set(keyboard.key_to_scan_codes("windows", error_if_missing=False))
+ return (
+ all(keys_scan_codes) # Unknown keys have no scan codes
+ and windows_scan_codes.isdisjoint(flatten(keys_scan_codes))
+ and sum(not keyboard.is_modifier(scan_codes[0]) for scan_codes in keys_scan_codes) == 1
)
diff --git a/src/region_selection.py b/src/region_selection.py
index 04dcec22..2df4fb7e 100644
--- a/src/region_selection.py
+++ b/src/region_selection.py
@@ -15,10 +15,12 @@
from utils import (
ALPHA_CHANNEL_COUNT,
MAXBYTE,
+ SUPPORTED_IMREAD_FORMATS,
ImageShape,
auto_split_directory,
get_window_bounds,
imread,
+ is_supported_image_file,
is_valid_hwnd,
is_valid_image,
)
@@ -37,27 +39,13 @@
GNOME_DESKTOP_ICONS_EXTENSION = "@!0,0;BDHF"
ALIGN_REGION_THRESHOLD = 0.9
BORDER_WIDTH = 2
-SUPPORTED_IMREAD_FORMATS = (
- ("Windows bitmaps", "*.bmp *.dib"),
- ("JPEG files", "*.jpeg *.jpg *.jpe"),
- ("JPEG 2000 files", "*.jp2"),
- ("Portable Network Graphics", "*.png"),
- ("WebP", "*.webp"),
- ("AVIF", "*.avif"),
- ("Portable image format", "*.pbm *.pgm *.ppm *.pxm *.pnm"),
- ("PFM files", "*.pfm"),
- ("Sun rasters", "*.sr *.ras"),
- ("TIFF files", "*.tiff *.tif"),
- ("OpenEXR Image files", "*.exr"),
- ("Radiance HDR", "*.hdr *.pic"),
-)
-"""https://docs.opencv.org/4.8.0/d4/da8/group__imgcodecs.html#imread"""
IMREAD_EXT_FILTER = (
"All Files ("
- + " ".join(f"{extensions}" for _, extensions in SUPPORTED_IMREAD_FORMATS)
+ + " ".join(f"{extensions}" for _, extensions, _ in SUPPORTED_IMREAD_FORMATS)
+ ");;"
+ ";;".join(
- f"{imread_format} ({extensions})" for imread_format, extensions in SUPPORTED_IMREAD_FORMATS
+ f"{imread_format} ({extensions})"
+ for imread_format, extensions, _ in SUPPORTED_IMREAD_FORMATS
)
)
@@ -218,6 +206,9 @@ def align_region(autosplit: AutoSplit):
if not template_filename:
return
+ if not is_supported_image_file(template_filename):
+ error_messages.image_validity()
+ return
template = imread(template_filename, cv2.IMREAD_UNCHANGED)
# Captures are standardized to BGR, so drop the template's alpha channel if present
if template.shape[ImageShape.Channels] == ALPHA_CHANNEL_COUNT:
diff --git a/src/user_profile.py b/src/user_profile.py
index 76d6497f..bd4e33d6 100644
--- a/src/user_profile.py
+++ b/src/user_profile.py
@@ -1,8 +1,10 @@
from __future__ import annotations
import os
+import sys
import tomllib
from copy import deepcopy
+from functools import partial
from typing import TYPE_CHECKING, NoReturn, TypedDict, cast, override
from warnings import deprecated
@@ -11,9 +13,9 @@
import error_messages
from capture_method import CAPTURE_METHODS, CaptureMethodEnum, Region, change_capture_method
-from hotkeys import HOTKEYS, CommandStr, remove_all_hotkeys, set_hotkey
+from hotkeys import HOTKEYS, CommandStr, is_valid_hotkey_name, remove_all_hotkeys, set_hotkey
from menu_bar import open_settings
-from utils import auto_split_directory
+from utils import auto_split_directory, is_network_path
if TYPE_CHECKING:
from AutoSplit import AutoSplit
@@ -125,6 +127,20 @@ def __save_settings_to_file(autosplit: AutoSplit, save_settings_file_path: str):
return save_settings_file_path
+def __unset_network_directories(autosplit: AutoSplit, loaded_settings: UserProfileDict):
+ """Accessing a network path can leak NTLM credentials. Don't trust those from a file."""
+ network_directories: dict[str, str] = {}
+ for setting in ("split_image_directory", "screenshot_directory"):
+ directory = loaded_settings[setting]
+ if is_network_path(directory):
+ network_directories[setting] = directory
+ loaded_settings[setting] = ""
+ if network_directories:
+ autosplit.show_error_signal.emit(
+ partial(error_messages.invalid_settings_network_directories, network_directories)
+ )
+
+
def __load_settings_from_file(autosplit: AutoSplit, load_settings_file_path: str):
if load_settings_file_path.endswith(".pkl"):
autosplit.show_error_signal.emit(error_messages.old_version_settings_file)
@@ -146,6 +162,8 @@ def __load_settings_from_file(autosplit: AutoSplit, load_settings_file_path: str
# TODO: Data Validation / fallbacks ?
loaded_settings["screenshot_on"] = list(set(loaded_settings["screenshot_on"]))
+ if sys.platform == "win32":
+ __unset_network_directories(autosplit, loaded_settings)
autosplit.settings_dict = UserProfileDict(**loaded_settings)
autosplit.last_saved_settings = deepcopy(autosplit.settings_dict)
@@ -159,12 +177,24 @@ def __load_settings_from_file(autosplit: AutoSplit, load_settings_file_path: str
return False
remove_all_hotkeys()
- if not autosplit.is_auto_controlled:
- for hotkey in HOTKEYS:
- hotkey_name = f"{hotkey}_hotkey"
- hotkey_value = autosplit.settings_dict.get(hotkey_name)
- if hotkey_value:
- set_hotkey(autosplit, hotkey, hotkey_value)
+ invalid_hotkeys: dict[str, object] = {}
+ for hotkey in HOTKEYS:
+ hotkey_name = f"{hotkey}_hotkey"
+ hotkey_value = autosplit.settings_dict.get(hotkey_name)
+ if not hotkey_value:
+ continue
+ # Hotkeys are sent as keystrokes. Never keep an invalid one around.
+ if not is_valid_hotkey_name(hotkey_value):
+ invalid_hotkeys[hotkey_name] = hotkey_value
+ autosplit.settings_dict[hotkey_name] = "" # pyright: ignore[reportGeneralTypeIssues]
+ autosplit.last_saved_settings[hotkey_name] = "" # pyright: ignore[reportGeneralTypeIssues]
+ continue
+ if not autosplit.is_auto_controlled:
+ set_hotkey(autosplit, hotkey, hotkey_value)
+ if invalid_hotkeys:
+ autosplit.show_error_signal.emit(
+ partial(error_messages.invalid_settings_hotkeys, invalid_hotkeys)
+ )
change_capture_method(
cast("CaptureMethodEnum", autosplit.settings_dict["capture_method"]),
diff --git a/src/utils.py b/src/utils.py
index 84cbda32..9e671632 100644
--- a/src/utils.py
+++ b/src/utils.py
@@ -2,6 +2,7 @@
import asyncio
import os
+import re
import shutil
import subprocess # noqa: S404
import sys
@@ -83,6 +84,38 @@ def find_tesseract_path():
"""How many channels in a BGR/RGB image"""
ALPHA_CHANNEL_COUNT = 4
"""How many channels in a BGRA/RGBA image"""
+SUPPORTED_IMREAD_FORMATS = (
+ ("Windows bitmaps", "*.bmp *.dib", rb"BM"),
+ ("GIF files", "*.gif", rb"GIF8[79]a"),
+ ("JPEG files", "*.jpeg *.jpg *.jpe", rb"\xFF\xD8\xFF"),
+ ("JPEG 2000 files", "*.jp2", rb"\x00\x00\x00\x0CjP \r\n\x87\n"),
+ ("Portable Network Graphics", "*.png", rb"\x89PNG\r\n\x1A\n"),
+ ("WebP", "*.webp", rb"RIFF.{4}WEBP"),
+ # ISO BMFF "ftyp" box with an AVIF brand. The box size must not collide with other signatures.
+ ("AVIF", "*.avif", rb"\x00\x00.{2}ftyp(?:.{4})*?avi[fs]"),
+ ("Portable image format", "*.pbm *.pgm *.ppm *.pxm *.pnm", rb"P[1-6]\s"),
+ ("PFM files", "*.pfm", rb"P[Ff]\s"),
+ ("Sun rasters", "*.sr *.ras", rb"\x59\xA6\x6A\x95"),
+ ("TIFF files", "*.tiff *.tif", rb"II[*+]\x00|MM\x00[*+]"),
+ ("OpenEXR Image files", "*.exr", rb"\x76\x2F\x31\x01"),
+ ("Radiance HDR", "*.hdr *.pic", rb"#\?(?:RADIANCE|RGBE)"),
+ # GDAL is intentionally excluded: its formats don't share a signature we could validate.
+)
+"""
+Name, extensions and file signature (regex of the first bytes) of each supported image format.
+https://docs.opencv.org/5.0/main_modules/imgcodecs.html#imread
+"""
+IMREAD_SIGNATURES = {
+ extension: re.compile(signature, re.DOTALL)
+ for _, extensions, signature in SUPPORTED_IMREAD_FORMATS
+ for extension in extensions.split()
+}
+"""Compiled `SUPPORTED_IMREAD_FORMATS` signatures by extension (ie: `"*.png"`)"""
+IMAGE_SIGNATURE_MAX_SIZE = 64
+"""How many bytes to read from the start of a file to match its format signature"""
+
+LOCAL_DRIVE_DEVICE_PATH = re.compile(r"\\\\[?.]\\[A-Za-z]:")
+r"""Device path to a local drive letter (ie: `\\?\C:` or `\\.\C:`)"""
class ImageShape(IntEnum):
@@ -125,6 +158,18 @@ def is_valid_image(image: MatLike | None) -> TypeGuard[MatLike]:
return image is not None and bool(image.size)
+def is_network_path(path: str):
+ r"""
+ Whether the path is a UNC path (`\\host\share`), including device paths (`\\?\UNC\...`).
+ Only device paths to a local drive letter (`\\?\C:\`) are excluded.
+
+ On Windows, simply accessing a network path can automatically send
+ the user's NTLM credentials to that host.
+ """
+ normalized_path = path.replace("/", "\\")
+ return normalized_path.startswith("\\\\") and not LOCAL_DRIVE_DEVICE_PATH.match(normalized_path)
+
+
def is_valid_hwnd(hwnd: int):
"""
Validate the hwnd points to a valid window
@@ -207,6 +252,8 @@ def get_input_device_resolution(index: int) -> tuple[int, int] | None:
def open_file(file_path: str | bytes | os.PathLike[str] | os.PathLike[bytes]):
+ # Absolute, so the opener can't mistake a relative path starting with "-" for an option
+ file_path = os.path.abspath(file_path)
if sys.platform == "win32":
os.startfile(file_path) # noqa: S606
else:
@@ -261,6 +308,22 @@ def flatten(nested_iterable: Iterable[Iterable[T]]) -> chain[T]:
return chain.from_iterable(nested_iterable)
+def is_supported_image_file(path: str):
+ """
+ Whether the file's extension is in `SUPPORTED_IMREAD_FORMATS`
+ and its content starts with the signature of that format.
+
+ `cv2.imdecode` picks a decoder based on the file's content, not its extension.
+ This ensures untrusted files can only ever reach the decoder of the format they claim to be.
+ """
+ signature = IMREAD_SIGNATURES.get("*" + os.path.splitext(path)[1].lower())
+ if signature is None:
+ return False
+ with open(path, mode="rb") as file:
+ header = file.read(IMAGE_SIGNATURE_MAX_SIZE)
+ return signature.match(header) is not None
+
+
def imread(filename: str, flags: int = cv2.IMREAD_COLOR):
decoded_image = cv2.imdecode(np.fromfile(filename, dtype=np.uint8), flags)
if decoded_image is None:
diff --git a/uv.lock b/uv.lock
index b4c51a61..46dc5def 100644
--- a/uv.lock
+++ b/uv.lock
@@ -19,6 +19,7 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for
exclude-newer-span = "P1W"
[options.exclude-newer-package]
+pillow = { timestamp = "0001-01-01T00:00:00Z", span = "P6D" }
typed-d3dshot = false
[manifest]
@@ -138,7 +139,7 @@ requires-dist = [
{ name = "opencv-contrib-python-headless", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'", specifier = ">=5.0" },
{ name = "opencv-contrib-python-headless", marker = "platform_machine == 'ARM64' and sys_platform == 'win32'", specifier = "==4.11.*" },
{ name = "packaging", specifier = ">=20.0" },
- { name = "pillow", marker = "sys_platform == 'linux'", specifier = ">=12.3.0" },
+ { name = "pillow", marker = "sys_platform == 'linux'", specifier = ">=13.0.0" },
{ name = "pyautogui", specifier = ">=0.9.52" },
{ name = "pygrabber", marker = "sys_platform == 'win32'", specifier = ">=0.2" },
{ name = "pyinstaller", specifier = ">=6.21.0" },
@@ -163,7 +164,7 @@ dev = [
{ name = "beslogic-ruff-config", git = "https://github.com/Beslogic/Beslogic-Ruff-Config?rev=39c27278054fc428b54205581a0e23caca7ad77a" },
{ name = "dprint-py", specifier = ">=0.50.0.0" },
{ name = "mypy", specifier = ">=2.2" },
- { name = "pillow" },
+ { name = "pillow", specifier = ">=13.0.0" },
{ name = "pygrabber" },
{ name = "pyright", extras = ["nodejs"], specifier = ">=1.1.400" },
{ name = "python-xlib" },
@@ -499,44 +500,44 @@ wheels = [
[[package]]
name = "pillow"
-version = "12.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
- { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
- { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
- { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
- { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
- { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
- { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
- { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
- { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
- { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
- { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
- { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
- { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
- { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
- { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
- { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
- { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
- { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
- { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
- { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
- { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
- { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
- { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
- { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
- { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
- { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
- { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
- { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
- { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
- { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
- { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
- { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
- { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
- { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
+version = "13.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ce/a4/9cc30690a394a8305e8be4c9036aa16d0dfddadd09079860b582f562496d/pillow-13.0.0.tar.gz", hash = "sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510", size = 47058631, upload-time = "2026-10-16T22:18:09.718Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/45/fbad9c7f5e2f1745c373a434d46072bc84eddfefaa28b852425ac957fda6/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:44629d4498c9cb6b2051806155f22141df0d8a929c56134b166655232dfb2f8c", size = 4190721, upload-time = "2026-10-16T22:16:27.524Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/bf/2074902a259a90692c5dbf132885d21671644cbbd3f690047516243a55db/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bf97ba1b5542050e9b0c74e632323b6d8136cc99d64e193abebee22fe6013c76", size = 4290236, upload-time = "2026-10-16T22:16:29.562Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/a5/3d1bb93b8ea38d15d21356c1b8e9f50971ac592aaa248f7fb71739094c0e/pillow-13.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aa694c49787040943570d61c9b8fc01271bd2a2cfdd55f0d896f8aecdce1a981", size = 3723093, upload-time = "2026-10-16T22:16:31.249Z" },
+ { url = "https://files.pythonhosted.org/packages/83/16/b603274617b3e6728da3a0133d4fba1ea8d7fbe0c4ea5fef9cbffa398b8b/pillow-13.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5933fcea5a5ba64c686f55c886a115cbfd92f2900460784a1e79909fdbafd7b6", size = 6516058, upload-time = "2026-10-16T22:16:37.228Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/f2/1371707ce438ba9a43f9d24089828494ec6f9396e28cd7903aa704dd6306/pillow-13.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91c2b1cbeae768c98752e67e717e283d68dae45717b41f2d91eaee648e37b7d", size = 7142574, upload-time = "2026-10-16T22:16:39.172Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/a9/62cf68587f910bc87c5951bcb6e9430c7643a65076adf5f18b4bf350e2bd/pillow-13.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f6299e494e46253d40c34653cac369412f44ca53b5f85d848d3f95b1ed0546c", size = 6614295, upload-time = "2026-10-16T22:16:41.117Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/c9/9e9b48178649730299be0bafbf5f810a5b6977538f5a92d47e4ba5f5d608/pillow-13.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f9bd7d260d986f3553518c5d723544346fa48031f8aef3265d786aeb59845bc0", size = 7224841, upload-time = "2026-10-16T22:16:43.118Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/48/c44e91384b1b6c072a714a63d00608acc16a6a968735e478e4be4384e5ac/pillow-13.0.0-cp314-cp314-win32.whl", hash = "sha256:2e31c4e68cffd7b1f116127626813b82fcba19edf6b940a7b0c266d6484d0055", size = 6508694, upload-time = "2026-10-16T22:16:45.883Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e2/7ad17747517e4c813dca78257d6bb593466f8231baf1384bc98d92745edb/pillow-13.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:f7cbb7a64e7ec664775195e4f81923bc8a93d9451b963f844e59118f63147a7a", size = 7281830, upload-time = "2026-10-16T22:16:47.725Z" },
+ { url = "https://files.pythonhosted.org/packages/41/9f/e5b51f2ce8ef4563215334035403ce61a244f93a2e65c24a940131618f87/pillow-13.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6aaa7871aebca081eb5c45f83e845bbd9712ae99fa9286dbfdcee835e63f4b4", size = 2695468, upload-time = "2026-10-16T22:16:49.515Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/52/43ded0ac08e77e39d17bd6d894162572201b5e8fdc4a022b14c55a1960a4/pillow-13.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ca8f69b7c77fa96dad99f8638c1fc4f1c0bed4acaa1baf7a3c31bdb80a0ab1", size = 6607008, upload-time = "2026-10-16T22:16:55.525Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/2e/4a57ed216d5bf877387cba4d44c02aa87abe51ad2e06404aa6b597c4da39/pillow-13.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bce6a29c3898191f4c9998e4d4e8f69790caeabde46e54da4bc235ce5bde683", size = 7224281, upload-time = "2026-10-16T22:16:57.738Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/3d/2929b248e135752d63aaeabf53f6de797e905b7b6263f9c1e2adade20aba/pillow-13.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:816752a5ef76a80484c8c441c85593b7ea9304f6231fc5776ad1fbff7e773058", size = 6695600, upload-time = "2026-10-16T22:16:59.842Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/33/fb0da992cf084ed1ee6ee05e96b6191e35c351eb707ca2df874feb24ce47/pillow-13.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5bdb38ba220b61b4ec31dd1046dea6b016d3c6850a23f43ed8b58332b2fc668e", size = 7300589, upload-time = "2026-10-16T22:17:01.686Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/af/c30dd9ef6a3dce84ecb1b2393cac629047a32e66279056c6cb46c3ffd65c/pillow-13.0.0-cp314-cp314t-win32.whl", hash = "sha256:06903aac074d00c988e77a1fc435286dc498506d4ca8d6f69df9babddf4ecc15", size = 6517204, upload-time = "2026-10-16T22:17:03.722Z" },
+ { url = "https://files.pythonhosted.org/packages/85/d5/ef3a8ae7609c8ab26570abf18194882534b480d245a9148b895a2262c004/pillow-13.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9a635068d9793c7f0d929f39f18b58f1bd86c236e8e7b64167b86af9232ebca0", size = 7286695, upload-time = "2026-10-16T22:17:05.543Z" },
+ { url = "https://files.pythonhosted.org/packages/84/3c/0861db0c69a03689ebe3e7599d35668a414deb5aaafd8255215e4e62e3d3/pillow-13.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0d1a9cf7bc6917538b20f8d06b9ab2147e542f68849e96fc58eb66bdf6acc692", size = 2698098, upload-time = "2026-10-16T22:17:07.674Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/eb/e86a41962a3a31ad495a3d6a912d9002b01bba4a6fa71c28703abdca7f69/pillow-13.0.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7d5e097eabd5d4915c27ac9f6e9325bb1e01a7c94d7348b5c4451d3fc30e8521", size = 4191213, upload-time = "2026-10-16T22:17:09.449Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/04/7e7445e706afec84ef06dc25b2c77f9cecff242c23730c62928ad1089d4a/pillow-13.0.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:51ebb529fc5e8abdd55302f6c3f8943037348ec2384329a3e55677f69ae2fdfc", size = 4290650, upload-time = "2026-10-16T22:17:11.196Z" },
+ { url = "https://files.pythonhosted.org/packages/46/85/d534089068ac40c0f61f650389a296c812eab745a8a733f81feec39ae955/pillow-13.0.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:c87a04e7f152ee4db5dbe15f0bfa3fe628a1aeedbe66b81c43b87f52c813f9ac", size = 3723078, upload-time = "2026-10-16T22:17:12.772Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/d0/c0ff25bb0427ec70f32aac995453321fc3c4494a46c7292ff02d9d260b04/pillow-13.0.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f8db54177035a2b5f717f94475b74907e12a96dc74f6d487f3966b0a4694071", size = 6527349, upload-time = "2026-10-16T22:17:18.534Z" },
+ { url = "https://files.pythonhosted.org/packages/85/11/e46fa0bcc0fc117f1eacc206b3d51f04d583fab328b4b740672d0438d83f/pillow-13.0.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f36aa77f74b14a2103622b196402c71330e4e7cf4122ee45c854e9a01e0eecf", size = 7152873, upload-time = "2026-10-16T22:17:20.415Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/53/2bdeca705f3a58679fa9201233193c86a30238340ca29b3a152c3fcc719b/pillow-13.0.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:149a1c323ba366f90b48e6f1a6db6582eb50512df374588de56fc909abc0f0f3", size = 6626871, upload-time = "2026-10-16T22:17:22.361Z" },
+ { url = "https://files.pythonhosted.org/packages/14/de/3829030f209722ade3753bb456b805a0e7f885cf885b483407365c82e9ca/pillow-13.0.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e892354a424b1e82a78ba16785822bdd40de06f19446a3d4a28047df1a4f8932", size = 7236439, upload-time = "2026-10-16T22:17:24.352Z" },
+ { url = "https://files.pythonhosted.org/packages/44/b1/67fc37388382bc036a74f04da5bb6fcbf90ec3ac1f45893e06176c373fd3/pillow-13.0.0-cp315-cp315-win32.whl", hash = "sha256:8e459b613cf31f8980f584387b4fe47bf08e076e288d61aa8eba1f7fd4ddbb81", size = 6508688, upload-time = "2026-10-16T22:17:26.314Z" },
+ { url = "https://files.pythonhosted.org/packages/44/05/fdb6a79aee10861165526b41b7b440e36ee1ff58613b2fbefe3acbf06d4f/pillow-13.0.0-cp315-cp315-win_amd64.whl", hash = "sha256:d4737f50d23ddd11d6685faacbb9252887dde0bc7997127757247983d74399d6", size = 7281107, upload-time = "2026-10-16T22:17:28.198Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9c/6c6f7dc2a566467baf121394e83de25ac287cffe34cc030726beb4be0224/pillow-13.0.0-cp315-cp315-win_arm64.whl", hash = "sha256:2310e2adfc13c6d457d7d0f2ffdd5c56a74a317650cf765887df8d515a647ac0", size = 2695513, upload-time = "2026-10-16T22:17:30.013Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/64/0861ea4e03c50b14af9ed9dd8b9f388ab42f445c623a9fcbab19aef7aa55/pillow-13.0.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20822140c9a2ba9c5e42df342b0a892ab2e102fc0e3db967a133bb9ce3f898bb", size = 6612582, upload-time = "2026-10-16T22:17:35.415Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/20/cd1e9b3f60a9f3d5ebe46da36955c26004092a501f7f2c42f9fcc26611bc/pillow-13.0.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffea6b2986a797c84b6d1d27d4f43e0b83c73fc16281a930891cd237cf0d08fe", size = 7230176, upload-time = "2026-10-16T22:17:37.362Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ef/ffb0a2fc85690714fb7ac662956949645d57f15bf4426f520da361e7b6e3/pillow-13.0.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:05cd2cd39055d0b9650060e4513ef4a9a0aea870083950bd046a0d6490f93a22", size = 6703640, upload-time = "2026-10-16T22:17:39.313Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/c2/6402ee3ccb3363aeed8fdb3f9fe56703ac5377a539f0b81b53999d7e121f/pillow-13.0.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c6fd031333dedcad7ce3e03d244093f43504f209e86925ebdb2b868fa4037630", size = 7309231, upload-time = "2026-10-16T22:17:41.021Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/96/d6b82e8b8c06ad143b66b12e0b31051e4bf489b4fabca1f390a14fe1ebde/pillow-13.0.0-cp315-cp315t-win32.whl", hash = "sha256:c50c80e3591e5cf9a8dfa6007d4f10732f02e32263850999286e56ccd650cfb2", size = 6517200, upload-time = "2026-10-16T22:17:42.886Z" },
+ { url = "https://files.pythonhosted.org/packages/60/d4/0062e6005e9b270b165015aafd343364ac378ec5a685431ea42baad33357/pillow-13.0.0-cp315-cp315t-win_amd64.whl", hash = "sha256:adb3c2124c68e480f4632d06cb7caa3b48a51678475f483b4edb1c6dee29ea43", size = 7287488, upload-time = "2026-10-16T22:17:45.56Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/81/b8eb349746be7bce47dd95337c6634e84910af91b59aaa9159c034c614fa/pillow-13.0.0-cp315-cp315t-win_arm64.whl", hash = "sha256:4a25756244bdcce3b50580baadf320fa48297f0eeb8e114ba64139a40d4d0fc8", size = 2698073, upload-time = "2026-10-16T22:17:47.556Z" },
]
[[package]]