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]]