From d651c8d5be025c76ad354c830dadd36919f583b2 Mon Sep 17 00:00:00 2001 From: Mrinank-Bhowmick <77621953+Mrinank-Bhowmick@users.noreply.github.com> Date: Thu, 21 May 2026 01:44:35 +0530 Subject: [PATCH 01/14] updated whole repo with frontend and backend --- .wrangler/cache/pages.json | 4 + .wrangler/cache/wrangler-account.json | 6 + pyproject.toml | 9 + python_modules/.synced | 1 + python_modules/PIL/AvifImagePlugin.py | 291 ++ python_modules/PIL/BdfFontFile.py | 122 + python_modules/PIL/BlpImagePlugin.py | 497 ++ python_modules/PIL/BmpImagePlugin.py | 515 ++ python_modules/PIL/BufrStubImagePlugin.py | 75 + python_modules/PIL/ContainerIO.py | 173 + python_modules/PIL/CurImagePlugin.py | 75 + python_modules/PIL/DcxImagePlugin.py | 83 + python_modules/PIL/DdsImagePlugin.py | 624 +++ python_modules/PIL/EpsImagePlugin.py | 476 ++ python_modules/PIL/ExifTags.py | 382 ++ python_modules/PIL/FitsImagePlugin.py | 152 + python_modules/PIL/FliImagePlugin.py | 178 + python_modules/PIL/FontFile.py | 134 + python_modules/PIL/FpxImagePlugin.py | 257 + python_modules/PIL/FtexImagePlugin.py | 114 + python_modules/PIL/GbrImagePlugin.py | 103 + python_modules/PIL/GdImageFile.py | 102 + python_modules/PIL/GifImagePlugin.py | 1213 +++++ python_modules/PIL/GimpGradientFile.py | 149 + python_modules/PIL/GimpPaletteFile.py | 72 + python_modules/PIL/GribStubImagePlugin.py | 75 + python_modules/PIL/Hdf5StubImagePlugin.py | 75 + python_modules/PIL/IcnsImagePlugin.py | 411 ++ python_modules/PIL/IcoImagePlugin.py | 381 ++ python_modules/PIL/ImImagePlugin.py | 389 ++ python_modules/PIL/Image.py | 4245 +++++++++++++++++ python_modules/PIL/ImageChops.py | 311 ++ python_modules/PIL/ImageCms.py | 1123 +++++ python_modules/PIL/ImageColor.py | 320 ++ python_modules/PIL/ImageDraw.py | 1232 +++++ python_modules/PIL/ImageDraw2.py | 243 + python_modules/PIL/ImageEnhance.py | 113 + python_modules/PIL/ImageFile.py | 922 ++++ python_modules/PIL/ImageFilter.py | 604 +++ python_modules/PIL/ImageFont.py | 1339 ++++++ python_modules/PIL/ImageGrab.py | 196 + python_modules/PIL/ImageMath.py | 368 ++ python_modules/PIL/ImageMode.py | 92 + python_modules/PIL/ImageMorph.py | 265 + python_modules/PIL/ImageOps.py | 745 +++ python_modules/PIL/ImagePalette.py | 286 ++ python_modules/PIL/ImagePath.py | 20 + python_modules/PIL/ImageQt.py | 220 + python_modules/PIL/ImageSequence.py | 86 + python_modules/PIL/ImageShow.py | 362 ++ python_modules/PIL/ImageStat.py | 160 + python_modules/PIL/ImageTk.py | 266 ++ python_modules/PIL/ImageTransform.py | 136 + python_modules/PIL/ImageWin.py | 247 + python_modules/PIL/ImtImagePlugin.py | 103 + python_modules/PIL/IptcImagePlugin.py | 250 + python_modules/PIL/Jpeg2KImagePlugin.py | 442 ++ python_modules/PIL/JpegImagePlugin.py | 902 ++++ python_modules/PIL/JpegPresets.py | 242 + python_modules/PIL/McIdasImagePlugin.py | 78 + python_modules/PIL/MicImagePlugin.py | 102 + python_modules/PIL/MpegImagePlugin.py | 84 + python_modules/PIL/MpoImagePlugin.py | 202 + python_modules/PIL/MspImagePlugin.py | 200 + python_modules/PIL/PSDraw.py | 237 + python_modules/PIL/PaletteFile.py | 54 + python_modules/PIL/PalmImagePlugin.py | 217 + python_modules/PIL/PcdImagePlugin.py | 64 + python_modules/PIL/PcfFontFile.py | 254 + python_modules/PIL/PcxImagePlugin.py | 228 + python_modules/PIL/PdfImagePlugin.py | 311 ++ python_modules/PIL/PdfParser.py | 1074 +++++ python_modules/PIL/PixarImagePlugin.py | 72 + python_modules/PIL/PngImagePlugin.py | 1551 ++++++ python_modules/PIL/PpmImagePlugin.py | 375 ++ python_modules/PIL/PsdImagePlugin.py | 333 ++ python_modules/PIL/QoiImagePlugin.py | 234 + python_modules/PIL/SgiImagePlugin.py | 231 + python_modules/PIL/SpiderImagePlugin.py | 331 ++ python_modules/PIL/SunImagePlugin.py | 145 + python_modules/PIL/TarIO.py | 61 + python_modules/PIL/TgaImagePlugin.py | 264 + python_modules/PIL/TiffImagePlugin.py | 2339 +++++++++ python_modules/PIL/TiffTags.py | 562 +++ python_modules/PIL/WalImageFile.py | 127 + python_modules/PIL/WebPImagePlugin.py | 320 ++ python_modules/PIL/WmfImagePlugin.py | 186 + python_modules/PIL/XVThumbImagePlugin.py | 83 + python_modules/PIL/XbmImagePlugin.py | 98 + python_modules/PIL/XpmImagePlugin.py | 157 + python_modules/PIL/__init__.py | 87 + python_modules/PIL/__main__.py | 7 + python_modules/PIL/_avif.pyi | 3 + python_modules/PIL/_binary.py | 112 + python_modules/PIL/_deprecate.py | 72 + python_modules/PIL/_imaging.pyi | 31 + python_modules/PIL/_imagingcms.pyi | 143 + python_modules/PIL/_imagingft.pyi | 69 + python_modules/PIL/_imagingmath.pyi | 3 + python_modules/PIL/_imagingmorph.pyi | 3 + python_modules/PIL/_imagingtk.pyi | 3 + python_modules/PIL/_tkinter_finder.py | 20 + python_modules/PIL/_typing.py | 54 + python_modules/PIL/_util.py | 26 + python_modules/PIL/_version.py | 4 + python_modules/PIL/_webp.pyi | 3 + python_modules/PIL/features.py | 361 ++ python_modules/PIL/py.typed | 0 python_modules/PIL/report.py | 5 + python_modules/_cloudflare_compat_flags.pyi | 4 + python_modules/_pyodide_entrypoint_helper.pyi | 9 + .../_workers_sdk_entropy_import_context.pth | 1 + .../_workers_sdk_entropy_import_context.py | 183 + ...rkers_sdk_entropy_import_context_loader.py | 13 + .../pillow-11.3.0.dist-info/INSTALLER | 1 + .../pillow-11.3.0.dist-info/METADATA | 177 + python_modules/pillow-11.3.0.dist-info/RECORD | 119 + .../pillow-11.3.0.dist-info/REQUESTED | 0 python_modules/pillow-11.3.0.dist-info/WHEEL | 5 + .../pillow-11.3.0.dist-info/licenses/LICENSE | 30 + .../pillow-11.3.0.dist-info/top_level.txt | 1 + .../pillow-11.3.0.dist-info/zip-safe | 1 + python_modules/pyvenv.cfg | 0 python_modules/qrcode-8.2.dist-info/INSTALLER | 1 + python_modules/qrcode-8.2.dist-info/LICENSE | 48 + python_modules/qrcode-8.2.dist-info/METADATA | 668 +++ python_modules/qrcode-8.2.dist-info/RECORD | 42 + python_modules/qrcode-8.2.dist-info/REQUESTED | 0 python_modules/qrcode-8.2.dist-info/WHEEL | 4 + .../qrcode-8.2.dist-info/entry_points.txt | 3 + python_modules/qrcode/LUT.py | 223 + python_modules/qrcode/__init__.py | 30 + python_modules/qrcode/base.py | 313 ++ python_modules/qrcode/compat/__init__.py | 0 python_modules/qrcode/compat/etree.py | 4 + python_modules/qrcode/compat/png.py | 7 + python_modules/qrcode/console_scripts.py | 181 + python_modules/qrcode/constants.py | 5 + python_modules/qrcode/exceptions.py | 2 + python_modules/qrcode/image/__init__.py | 0 python_modules/qrcode/image/base.py | 164 + python_modules/qrcode/image/pil.py | 57 + python_modules/qrcode/image/pure.py | 56 + python_modules/qrcode/image/styledpil.py | 120 + .../qrcode/image/styles/__init__.py | 0 .../qrcode/image/styles/colormasks.py | 226 + .../image/styles/moduledrawers/__init__.py | 10 + .../qrcode/image/styles/moduledrawers/base.py | 33 + .../qrcode/image/styles/moduledrawers/pil.py | 265 + .../qrcode/image/styles/moduledrawers/svg.py | 139 + python_modules/qrcode/image/svg.py | 175 + python_modules/qrcode/main.py | 541 +++ python_modules/qrcode/release.py | 42 + python_modules/qrcode/tests/__init__.py | 0 python_modules/qrcode/tests/consts.py | 4 + python_modules/qrcode/tests/test_example.py | 13 + python_modules/qrcode/tests/test_qrcode.py | 271 ++ .../qrcode/tests/test_qrcode_pil.py | 157 + .../qrcode/tests/test_qrcode_pypng.py | 35 + .../qrcode/tests/test_qrcode_svg.py | 54 + python_modules/qrcode/tests/test_release.py | 43 + python_modules/qrcode/tests/test_script.py | 97 + python_modules/qrcode/tests/test_util.py | 11 + python_modules/qrcode/util.py | 584 +++ python_modules/workers/__init__.py | 66 + python_modules/workers/_workers.py | 1454 ++++++ python_modules/workers/py.typed | 0 python_modules/workers/workflows.py | 12 + .../INSTALLER | 1 + .../METADATA | 15 + .../RECORD | 14 + .../REQUESTED | 0 .../workers_runtime_sdk-1.1.5.dist-info/WHEEL | 4 + src/handlers/__init__.py | 0 src/handlers/_shared.py | 38 + src/handlers/bmi.py | 35 + src/handlers/hangman.py | 78 + src/handlers/madlibs.py | 60 + src/handlers/qr.py | 28 + src/handlers/rps.py | 32 + src/handlers/tictactoe.py | 95 + src/worker.py | 27 + uv.lock | 108 + website/api.js | 81 + website/data.js | 70 + website/index.html | 47 + website/sticker-app-2.css | 133 + website/sticker-app-3.css | 417 ++ website/sticker-app.css | 100 + website/sticker-app.jsx | 299 ++ website/sticker-components.jsx | 361 ++ website/tweaks-panel.jsx | 568 +++ wrangler.jsonc | 11 + 193 files changed, 43884 insertions(+) create mode 100644 .wrangler/cache/pages.json create mode 100644 .wrangler/cache/wrangler-account.json create mode 100644 pyproject.toml create mode 100644 python_modules/.synced create mode 100644 python_modules/PIL/AvifImagePlugin.py create mode 100644 python_modules/PIL/BdfFontFile.py create mode 100644 python_modules/PIL/BlpImagePlugin.py create mode 100644 python_modules/PIL/BmpImagePlugin.py create mode 100644 python_modules/PIL/BufrStubImagePlugin.py create mode 100644 python_modules/PIL/ContainerIO.py create mode 100644 python_modules/PIL/CurImagePlugin.py create mode 100644 python_modules/PIL/DcxImagePlugin.py create mode 100644 python_modules/PIL/DdsImagePlugin.py create mode 100644 python_modules/PIL/EpsImagePlugin.py create mode 100644 python_modules/PIL/ExifTags.py create mode 100644 python_modules/PIL/FitsImagePlugin.py create mode 100644 python_modules/PIL/FliImagePlugin.py create mode 100644 python_modules/PIL/FontFile.py create mode 100644 python_modules/PIL/FpxImagePlugin.py create mode 100644 python_modules/PIL/FtexImagePlugin.py create mode 100644 python_modules/PIL/GbrImagePlugin.py create mode 100644 python_modules/PIL/GdImageFile.py create mode 100644 python_modules/PIL/GifImagePlugin.py create mode 100644 python_modules/PIL/GimpGradientFile.py create mode 100644 python_modules/PIL/GimpPaletteFile.py create mode 100644 python_modules/PIL/GribStubImagePlugin.py create mode 100644 python_modules/PIL/Hdf5StubImagePlugin.py create mode 100644 python_modules/PIL/IcnsImagePlugin.py create mode 100644 python_modules/PIL/IcoImagePlugin.py create mode 100644 python_modules/PIL/ImImagePlugin.py create mode 100644 python_modules/PIL/Image.py create mode 100644 python_modules/PIL/ImageChops.py create mode 100644 python_modules/PIL/ImageCms.py create mode 100644 python_modules/PIL/ImageColor.py create mode 100644 python_modules/PIL/ImageDraw.py create mode 100644 python_modules/PIL/ImageDraw2.py create mode 100644 python_modules/PIL/ImageEnhance.py create mode 100644 python_modules/PIL/ImageFile.py create mode 100644 python_modules/PIL/ImageFilter.py create mode 100644 python_modules/PIL/ImageFont.py create mode 100644 python_modules/PIL/ImageGrab.py create mode 100644 python_modules/PIL/ImageMath.py create mode 100644 python_modules/PIL/ImageMode.py create mode 100644 python_modules/PIL/ImageMorph.py create mode 100644 python_modules/PIL/ImageOps.py create mode 100644 python_modules/PIL/ImagePalette.py create mode 100644 python_modules/PIL/ImagePath.py create mode 100644 python_modules/PIL/ImageQt.py create mode 100644 python_modules/PIL/ImageSequence.py create mode 100644 python_modules/PIL/ImageShow.py create mode 100644 python_modules/PIL/ImageStat.py create mode 100644 python_modules/PIL/ImageTk.py create mode 100644 python_modules/PIL/ImageTransform.py create mode 100644 python_modules/PIL/ImageWin.py create mode 100644 python_modules/PIL/ImtImagePlugin.py create mode 100644 python_modules/PIL/IptcImagePlugin.py create mode 100644 python_modules/PIL/Jpeg2KImagePlugin.py create mode 100644 python_modules/PIL/JpegImagePlugin.py create mode 100644 python_modules/PIL/JpegPresets.py create mode 100644 python_modules/PIL/McIdasImagePlugin.py create mode 100644 python_modules/PIL/MicImagePlugin.py create mode 100644 python_modules/PIL/MpegImagePlugin.py create mode 100644 python_modules/PIL/MpoImagePlugin.py create mode 100644 python_modules/PIL/MspImagePlugin.py create mode 100644 python_modules/PIL/PSDraw.py create mode 100644 python_modules/PIL/PaletteFile.py create mode 100644 python_modules/PIL/PalmImagePlugin.py create mode 100644 python_modules/PIL/PcdImagePlugin.py create mode 100644 python_modules/PIL/PcfFontFile.py create mode 100644 python_modules/PIL/PcxImagePlugin.py create mode 100644 python_modules/PIL/PdfImagePlugin.py create mode 100644 python_modules/PIL/PdfParser.py create mode 100644 python_modules/PIL/PixarImagePlugin.py create mode 100644 python_modules/PIL/PngImagePlugin.py create mode 100644 python_modules/PIL/PpmImagePlugin.py create mode 100644 python_modules/PIL/PsdImagePlugin.py create mode 100644 python_modules/PIL/QoiImagePlugin.py create mode 100644 python_modules/PIL/SgiImagePlugin.py create mode 100644 python_modules/PIL/SpiderImagePlugin.py create mode 100644 python_modules/PIL/SunImagePlugin.py create mode 100644 python_modules/PIL/TarIO.py create mode 100644 python_modules/PIL/TgaImagePlugin.py create mode 100644 python_modules/PIL/TiffImagePlugin.py create mode 100644 python_modules/PIL/TiffTags.py create mode 100644 python_modules/PIL/WalImageFile.py create mode 100644 python_modules/PIL/WebPImagePlugin.py create mode 100644 python_modules/PIL/WmfImagePlugin.py create mode 100644 python_modules/PIL/XVThumbImagePlugin.py create mode 100644 python_modules/PIL/XbmImagePlugin.py create mode 100644 python_modules/PIL/XpmImagePlugin.py create mode 100644 python_modules/PIL/__init__.py create mode 100644 python_modules/PIL/__main__.py create mode 100644 python_modules/PIL/_avif.pyi create mode 100644 python_modules/PIL/_binary.py create mode 100644 python_modules/PIL/_deprecate.py create mode 100644 python_modules/PIL/_imaging.pyi create mode 100644 python_modules/PIL/_imagingcms.pyi create mode 100644 python_modules/PIL/_imagingft.pyi create mode 100644 python_modules/PIL/_imagingmath.pyi create mode 100644 python_modules/PIL/_imagingmorph.pyi create mode 100644 python_modules/PIL/_imagingtk.pyi create mode 100644 python_modules/PIL/_tkinter_finder.py create mode 100644 python_modules/PIL/_typing.py create mode 100644 python_modules/PIL/_util.py create mode 100644 python_modules/PIL/_version.py create mode 100644 python_modules/PIL/_webp.pyi create mode 100644 python_modules/PIL/features.py create mode 100644 python_modules/PIL/py.typed create mode 100644 python_modules/PIL/report.py create mode 100644 python_modules/_cloudflare_compat_flags.pyi create mode 100644 python_modules/_pyodide_entrypoint_helper.pyi create mode 100644 python_modules/_workers_sdk_entropy_import_context.pth create mode 100644 python_modules/_workers_sdk_entropy_import_context.py create mode 100644 python_modules/_workers_sdk_entropy_import_context_loader.py create mode 100644 python_modules/pillow-11.3.0.dist-info/INSTALLER create mode 100644 python_modules/pillow-11.3.0.dist-info/METADATA create mode 100644 python_modules/pillow-11.3.0.dist-info/RECORD create mode 100644 python_modules/pillow-11.3.0.dist-info/REQUESTED create mode 100644 python_modules/pillow-11.3.0.dist-info/WHEEL create mode 100644 python_modules/pillow-11.3.0.dist-info/licenses/LICENSE create mode 100644 python_modules/pillow-11.3.0.dist-info/top_level.txt create mode 100644 python_modules/pillow-11.3.0.dist-info/zip-safe create mode 100644 python_modules/pyvenv.cfg create mode 100644 python_modules/qrcode-8.2.dist-info/INSTALLER create mode 100644 python_modules/qrcode-8.2.dist-info/LICENSE create mode 100644 python_modules/qrcode-8.2.dist-info/METADATA create mode 100644 python_modules/qrcode-8.2.dist-info/RECORD create mode 100644 python_modules/qrcode-8.2.dist-info/REQUESTED create mode 100644 python_modules/qrcode-8.2.dist-info/WHEEL create mode 100644 python_modules/qrcode-8.2.dist-info/entry_points.txt create mode 100644 python_modules/qrcode/LUT.py create mode 100644 python_modules/qrcode/__init__.py create mode 100644 python_modules/qrcode/base.py create mode 100644 python_modules/qrcode/compat/__init__.py create mode 100644 python_modules/qrcode/compat/etree.py create mode 100644 python_modules/qrcode/compat/png.py create mode 100644 python_modules/qrcode/console_scripts.py create mode 100644 python_modules/qrcode/constants.py create mode 100644 python_modules/qrcode/exceptions.py create mode 100644 python_modules/qrcode/image/__init__.py create mode 100644 python_modules/qrcode/image/base.py create mode 100644 python_modules/qrcode/image/pil.py create mode 100644 python_modules/qrcode/image/pure.py create mode 100644 python_modules/qrcode/image/styledpil.py create mode 100644 python_modules/qrcode/image/styles/__init__.py create mode 100644 python_modules/qrcode/image/styles/colormasks.py create mode 100644 python_modules/qrcode/image/styles/moduledrawers/__init__.py create mode 100644 python_modules/qrcode/image/styles/moduledrawers/base.py create mode 100644 python_modules/qrcode/image/styles/moduledrawers/pil.py create mode 100644 python_modules/qrcode/image/styles/moduledrawers/svg.py create mode 100644 python_modules/qrcode/image/svg.py create mode 100644 python_modules/qrcode/main.py create mode 100644 python_modules/qrcode/release.py create mode 100644 python_modules/qrcode/tests/__init__.py create mode 100644 python_modules/qrcode/tests/consts.py create mode 100644 python_modules/qrcode/tests/test_example.py create mode 100644 python_modules/qrcode/tests/test_qrcode.py create mode 100644 python_modules/qrcode/tests/test_qrcode_pil.py create mode 100644 python_modules/qrcode/tests/test_qrcode_pypng.py create mode 100644 python_modules/qrcode/tests/test_qrcode_svg.py create mode 100644 python_modules/qrcode/tests/test_release.py create mode 100644 python_modules/qrcode/tests/test_script.py create mode 100644 python_modules/qrcode/tests/test_util.py create mode 100644 python_modules/qrcode/util.py create mode 100644 python_modules/workers/__init__.py create mode 100644 python_modules/workers/_workers.py create mode 100644 python_modules/workers/py.typed create mode 100644 python_modules/workers/workflows.py create mode 100644 python_modules/workers_runtime_sdk-1.1.5.dist-info/INSTALLER create mode 100644 python_modules/workers_runtime_sdk-1.1.5.dist-info/METADATA create mode 100644 python_modules/workers_runtime_sdk-1.1.5.dist-info/RECORD create mode 100644 python_modules/workers_runtime_sdk-1.1.5.dist-info/REQUESTED create mode 100644 python_modules/workers_runtime_sdk-1.1.5.dist-info/WHEEL create mode 100644 src/handlers/__init__.py create mode 100644 src/handlers/_shared.py create mode 100644 src/handlers/bmi.py create mode 100644 src/handlers/hangman.py create mode 100644 src/handlers/madlibs.py create mode 100644 src/handlers/qr.py create mode 100644 src/handlers/rps.py create mode 100644 src/handlers/tictactoe.py create mode 100644 src/worker.py create mode 100644 uv.lock create mode 100644 website/api.js create mode 100644 website/data.js create mode 100644 website/index.html create mode 100644 website/sticker-app-2.css create mode 100644 website/sticker-app-3.css create mode 100644 website/sticker-app.css create mode 100644 website/sticker-app.jsx create mode 100644 website/sticker-components.jsx create mode 100644 website/tweaks-panel.jsx create mode 100644 wrangler.jsonc diff --git a/.wrangler/cache/pages.json b/.wrangler/cache/pages.json new file mode 100644 index 000000000..348673cfc --- /dev/null +++ b/.wrangler/cache/pages.json @@ -0,0 +1,4 @@ +{ + "account_id": "51b5d4b1862c5d3379a7b244c5593875", + "project_name": "pybegin" +} \ No newline at end of file diff --git a/.wrangler/cache/wrangler-account.json b/.wrangler/cache/wrangler-account.json new file mode 100644 index 000000000..e6c4434aa --- /dev/null +++ b/.wrangler/cache/wrangler-account.json @@ -0,0 +1,6 @@ +{ + "account": { + "id": "51b5d4b1862c5d3379a7b244c5593875", + "name": "Bhowmickmrinank@gmail.com's Account" + } +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..c0156247c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "pybegin-worker" +version = "0.1.0" +description = "Cloudflare Worker backend for the python-beginner-projects sticker site" +requires-python = ">=3.12" +dependencies = [ + "qrcode>=7.4", + "pillow", +] diff --git a/python_modules/.synced b/python_modules/.synced new file mode 100644 index 000000000..70b02ffc1 --- /dev/null +++ b/python_modules/.synced @@ -0,0 +1 @@ +1.9.4 \ No newline at end of file diff --git a/python_modules/PIL/AvifImagePlugin.py b/python_modules/PIL/AvifImagePlugin.py new file mode 100644 index 000000000..366e0c864 --- /dev/null +++ b/python_modules/PIL/AvifImagePlugin.py @@ -0,0 +1,291 @@ +from __future__ import annotations + +import os +from io import BytesIO +from typing import IO + +from . import ExifTags, Image, ImageFile + +try: + from . import _avif + + SUPPORTED = True +except ImportError: + SUPPORTED = False + +# Decoder options as module globals, until there is a way to pass parameters +# to Image.open (see https://github.com/python-pillow/Pillow/issues/569) +DECODE_CODEC_CHOICE = "auto" +DEFAULT_MAX_THREADS = 0 + + +def get_codec_version(codec_name: str) -> str | None: + versions = _avif.codec_versions() + for version in versions.split(", "): + if version.split(" [")[0] == codec_name: + return version.split(":")[-1].split(" ")[0] + return None + + +def _accept(prefix: bytes) -> bool | str: + if prefix[4:8] != b"ftyp": + return False + major_brand = prefix[8:12] + if major_brand in ( + # coding brands + b"avif", + b"avis", + # We accept files with AVIF container brands; we can't yet know if + # the ftyp box has the correct compatible brands, but if it doesn't + # then the plugin will raise a SyntaxError which Pillow will catch + # before moving on to the next plugin that accepts the file. + # + # Also, because this file might not actually be an AVIF file, we + # don't raise an error if AVIF support isn't properly compiled. + b"mif1", + b"msf1", + ): + if not SUPPORTED: + return ( + "image file could not be identified because AVIF support not installed" + ) + return True + return False + + +def _get_default_max_threads() -> int: + if DEFAULT_MAX_THREADS: + return DEFAULT_MAX_THREADS + if hasattr(os, "sched_getaffinity"): + return len(os.sched_getaffinity(0)) + else: + return os.cpu_count() or 1 + + +class AvifImageFile(ImageFile.ImageFile): + format = "AVIF" + format_description = "AVIF image" + __frame = -1 + + def _open(self) -> None: + if not SUPPORTED: + msg = "image file could not be opened because AVIF support not installed" + raise SyntaxError(msg) + + if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available( + DECODE_CODEC_CHOICE + ): + msg = "Invalid opening codec" + raise ValueError(msg) + self._decoder = _avif.AvifDecoder( + self.fp.read(), + DECODE_CODEC_CHOICE, + _get_default_max_threads(), + ) + + # Get info from decoder + self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = ( + self._decoder.get_info() + ) + self.is_animated = self.n_frames > 1 + + if icc: + self.info["icc_profile"] = icc + if xmp: + self.info["xmp"] = xmp + + if exif_orientation != 1 or exif: + exif_data = Image.Exif() + if exif: + exif_data.load(exif) + original_orientation = exif_data.get(ExifTags.Base.Orientation, 1) + else: + original_orientation = 1 + if exif_orientation != original_orientation: + exif_data[ExifTags.Base.Orientation] = exif_orientation + exif = exif_data.tobytes() + if exif: + self.info["exif"] = exif + self.seek(0) + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + + # Set tile + self.__frame = frame + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] + + def load(self) -> Image.core.PixelAccess | None: + if self.tile: + # We need to load the image data for this frame + data, timescale, pts_in_timescales, duration_in_timescales = ( + self._decoder.get_frame(self.__frame) + ) + self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale)) + self.info["duration"] = round(1000 * (duration_in_timescales / timescale)) + + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + + return super().load() + + def load_seek(self, pos: int) -> None: + pass + + def tell(self) -> int: + return self.__frame + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: + info = im.encoderinfo.copy() + if save_all: + append_images = list(info.get("append_images", [])) + else: + append_images = [] + + total = 0 + for ims in [im] + append_images: + total += getattr(ims, "n_frames", 1) + + quality = info.get("quality", 75) + if not isinstance(quality, int) or quality < 0 or quality > 100: + msg = "Invalid quality setting" + raise ValueError(msg) + + duration = info.get("duration", 0) + subsampling = info.get("subsampling", "4:2:0") + speed = info.get("speed", 6) + max_threads = info.get("max_threads", _get_default_max_threads()) + codec = info.get("codec", "auto") + if codec != "auto" and not _avif.encoder_codec_available(codec): + msg = "Invalid saving codec" + raise ValueError(msg) + range_ = info.get("range", "full") + tile_rows_log2 = info.get("tile_rows", 0) + tile_cols_log2 = info.get("tile_cols", 0) + alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) + autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) + + icc_profile = info.get("icc_profile", im.info.get("icc_profile")) + exif_orientation = 1 + if exif := info.get("exif"): + if isinstance(exif, Image.Exif): + exif_data = exif + else: + exif_data = Image.Exif() + exif_data.load(exif) + if ExifTags.Base.Orientation in exif_data: + exif_orientation = exif_data.pop(ExifTags.Base.Orientation) + exif = exif_data.tobytes() if exif_data else b"" + elif isinstance(exif, Image.Exif): + exif = exif_data.tobytes() + + xmp = info.get("xmp") + + if isinstance(xmp, str): + xmp = xmp.encode("utf-8") + + advanced = info.get("advanced") + if advanced is not None: + if isinstance(advanced, dict): + advanced = advanced.items() + try: + advanced = tuple(advanced) + except TypeError: + invalid = True + else: + invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced) + if invalid: + msg = ( + "advanced codec options must be a dict of key-value string " + "pairs or a series of key-value two-tuples" + ) + raise ValueError(msg) + + # Setup the AVIF encoder + enc = _avif.AvifEncoder( + im.size, + subsampling, + quality, + speed, + max_threads, + codec, + range_, + tile_rows_log2, + tile_cols_log2, + alpha_premultiplied, + autotiling, + icc_profile or b"", + exif or b"", + exif_orientation, + xmp or b"", + advanced, + ) + + # Add each frame + frame_idx = 0 + frame_duration = 0 + cur_idx = im.tell() + is_single_frame = total == 1 + try: + for ims in [im] + append_images: + # Get number of frames in this image + nfr = getattr(ims, "n_frames", 1) + + for idx in range(nfr): + ims.seek(idx) + + # Make sure image mode is supported + frame = ims + rawmode = ims.mode + if ims.mode not in {"RGB", "RGBA"}: + rawmode = "RGBA" if ims.has_transparency_data else "RGB" + frame = ims.convert(rawmode) + + # Update frame duration + if isinstance(duration, (list, tuple)): + frame_duration = duration[frame_idx] + else: + frame_duration = duration + + # Append the frame to the animation encoder + enc.add( + frame.tobytes("raw", rawmode), + frame_duration, + frame.size, + rawmode, + is_single_frame, + ) + + # Update frame index + frame_idx += 1 + + if not save_all: + break + + finally: + im.seek(cur_idx) + + # Get the final output from the encoder + data = enc.finish() + if data is None: + msg = "cannot write file as AVIF (encoder returned None)" + raise OSError(msg) + + fp.write(data) + + +Image.register_open(AvifImageFile.format, AvifImageFile, _accept) +if SUPPORTED: + Image.register_save(AvifImageFile.format, _save) + Image.register_save_all(AvifImageFile.format, _save_all) + Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"]) + Image.register_mime(AvifImageFile.format, "image/avif") diff --git a/python_modules/PIL/BdfFontFile.py b/python_modules/PIL/BdfFontFile.py new file mode 100644 index 000000000..f175e2f4f --- /dev/null +++ b/python_modules/PIL/BdfFontFile.py @@ -0,0 +1,122 @@ +# +# The Python Imaging Library +# $Id$ +# +# bitmap distribution font (bdf) file parser +# +# history: +# 1996-05-16 fl created (as bdf2pil) +# 1997-08-25 fl converted to FontFile driver +# 2001-05-25 fl removed bogus __init__ call +# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev) +# 2003-04-22 fl more robustification (from Graham Dumpleton) +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1997-2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +""" +Parse X Bitmap Distribution Format (BDF) +""" +from __future__ import annotations + +from typing import BinaryIO + +from . import FontFile, Image + + +def bdf_char( + f: BinaryIO, +) -> ( + tuple[ + str, + int, + tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]], + Image.Image, + ] + | None +): + # skip to STARTCHAR + while True: + s = f.readline() + if not s: + return None + if s.startswith(b"STARTCHAR"): + break + id = s[9:].strip().decode("ascii") + + # load symbol properties + props = {} + while True: + s = f.readline() + if not s or s.startswith(b"BITMAP"): + break + i = s.find(b" ") + props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") + + # load bitmap + bitmap = bytearray() + while True: + s = f.readline() + if not s or s.startswith(b"ENDCHAR"): + break + bitmap += s[:-1] + + # The word BBX + # followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBxoff0, BByoff0) + # of the lower left corner from the origin of the character. + width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split()) + + # The word DWIDTH + # followed by the width in x and y of the character in device pixels. + dwx, dwy = (int(p) for p in props["DWIDTH"].split()) + + bbox = ( + (dwx, dwy), + (x_disp, -y_disp - height, width + x_disp, -y_disp), + (0, 0, width, height), + ) + + try: + im = Image.frombytes("1", (width, height), bitmap, "hex", "1") + except ValueError: + # deal with zero-width characters + im = Image.new("1", (width, height)) + + return id, int(props["ENCODING"]), bbox, im + + +class BdfFontFile(FontFile.FontFile): + """Font file plugin for the X11 BDF format.""" + + def __init__(self, fp: BinaryIO) -> None: + super().__init__() + + s = fp.readline() + if not s.startswith(b"STARTFONT 2.1"): + msg = "not a valid BDF file" + raise SyntaxError(msg) + + props = {} + comments = [] + + while True: + s = fp.readline() + if not s or s.startswith(b"ENDPROPERTIES"): + break + i = s.find(b" ") + props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") + if s[:i] in [b"COMMENT", b"COPYRIGHT"]: + if s.find(b"LogicalFontDescription") < 0: + comments.append(s[i + 1 : -1].decode("ascii")) + + while True: + c = bdf_char(fp) + if not c: + break + id, ch, (xy, dst, src), im = c + if 0 <= ch < len(self.glyph): + self.glyph[ch] = xy, dst, src, im diff --git a/python_modules/PIL/BlpImagePlugin.py b/python_modules/PIL/BlpImagePlugin.py new file mode 100644 index 000000000..f7be7746d --- /dev/null +++ b/python_modules/PIL/BlpImagePlugin.py @@ -0,0 +1,497 @@ +""" +Blizzard Mipmap Format (.blp) +Jerome Leclanche + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ + +BLP1 files, used mostly in Warcraft III, are not fully supported. +All types of BLP2 files used in World of Warcraft are supported. + +The BLP file structure consists of a header, up to 16 mipmaps of the +texture + +Texture sizes must be powers of two, though the two dimensions do +not have to be equal; 512x256 is valid, but 512x200 is not. +The first mipmap (mipmap #0) is the full size image; each subsequent +mipmap halves both dimensions. The final mipmap should be 1x1. + +BLP files come in many different flavours: +* JPEG-compressed (type == 0) - only supported for BLP1. +* RAW images (type == 1, encoding == 1). Each mipmap is stored as an + array of 8-bit values, one per pixel, left to right, top to bottom. + Each value is an index to the palette. +* DXT-compressed (type == 1, encoding == 2): +- DXT1 compression is used if alpha_encoding == 0. + - An additional alpha bit is used if alpha_depth == 1. + - DXT3 compression is used if alpha_encoding == 1. + - DXT5 compression is used if alpha_encoding == 7. +""" + +from __future__ import annotations + +import abc +import os +import struct +from enum import IntEnum +from io import BytesIO +from typing import IO + +from . import Image, ImageFile + + +class Format(IntEnum): + JPEG = 0 + + +class Encoding(IntEnum): + UNCOMPRESSED = 1 + DXT = 2 + UNCOMPRESSED_RAW_BGRA = 3 + + +class AlphaEncoding(IntEnum): + DXT1 = 0 + DXT3 = 1 + DXT5 = 7 + + +def unpack_565(i: int) -> tuple[int, int, int]: + return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 + + +def decode_dxt1( + data: bytes, alpha: bool = False +) -> tuple[bytearray, bytearray, bytearray, bytearray]: + """ + input: one "row" of data (i.e. will produce 4*width pixels) + """ + + blocks = len(data) // 8 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block_index in range(blocks): + # Decode next 8-byte block. + idx = block_index * 8 + color0, color1, bits = struct.unpack_from("> 2 + + a = 0xFF + if control == 0: + r, g, b = r0, g0, b0 + elif control == 1: + r, g, b = r1, g1, b1 + elif control == 2: + if color0 > color1: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + else: + r = (r0 + r1) // 2 + g = (g0 + g1) // 2 + b = (b0 + b1) // 2 + elif control == 3: + if color0 > color1: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + else: + r, g, b, a = 0, 0, 0, 0 + + if alpha: + ret[j].extend([r, g, b, a]) + else: + ret[j].extend([r, g, b]) + + return ret + + +def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: + """ + input: one "row" of data (i.e. will produce 4*width pixels) + """ + + blocks = len(data) // 16 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block_index in range(blocks): + idx = block_index * 16 + block = data[idx : idx + 16] + # Decode next 16-byte block. + bits = struct.unpack_from("<8B", block) + color0, color1 = struct.unpack_from(">= 4 + else: + high = True + a &= 0xF + a *= 17 # We get a value between 0 and 15 + + color_code = (code >> 2 * (4 * j + i)) & 0x03 + + if color_code == 0: + r, g, b = r0, g0, b0 + elif color_code == 1: + r, g, b = r1, g1, b1 + elif color_code == 2: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + elif color_code == 3: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + + ret[j].extend([r, g, b, a]) + + return ret + + +def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: + """ + input: one "row" of data (i.e. will produce 4 * width pixels) + """ + + blocks = len(data) // 16 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block_index in range(blocks): + idx = block_index * 16 + block = data[idx : idx + 16] + # Decode next 16-byte block. + a0, a1 = struct.unpack_from("> alphacode_index) & 0x07 + elif alphacode_index == 15: + alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06) + else: # alphacode_index >= 18 and alphacode_index <= 45 + alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07 + + if alphacode == 0: + a = a0 + elif alphacode == 1: + a = a1 + elif a0 > a1: + a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7 + elif alphacode == 6: + a = 0 + elif alphacode == 7: + a = 255 + else: + a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5 + + color_code = (code >> 2 * (4 * j + i)) & 0x03 + + if color_code == 0: + r, g, b = r0, g0, b0 + elif color_code == 1: + r, g, b = r1, g1, b1 + elif color_code == 2: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + elif color_code == 3: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + + ret[j].extend([r, g, b, a]) + + return ret + + +class BLPFormatError(NotImplementedError): + pass + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith((b"BLP1", b"BLP2")) + + +class BlpImageFile(ImageFile.ImageFile): + """ + Blizzard Mipmap Format + """ + + format = "BLP" + format_description = "Blizzard Mipmap Format" + + def _open(self) -> None: + self.magic = self.fp.read(4) + if not _accept(self.magic): + msg = f"Bad BLP magic {repr(self.magic)}" + raise BLPFormatError(msg) + + compression = struct.unpack(" tuple[int, int]: + try: + self._read_header() + self._load() + except struct.error as e: + msg = "Truncated BLP file" + raise OSError(msg) from e + return -1, 0 + + @abc.abstractmethod + def _load(self) -> None: + pass + + def _read_header(self) -> None: + self._offsets = struct.unpack("<16I", self._safe_read(16 * 4)) + self._lengths = struct.unpack("<16I", self._safe_read(16 * 4)) + + def _safe_read(self, length: int) -> bytes: + assert self.fd is not None + return ImageFile._safe_read(self.fd, length) + + def _read_palette(self) -> list[tuple[int, int, int, int]]: + ret = [] + for i in range(256): + try: + b, g, r, a = struct.unpack("<4B", self._safe_read(4)) + except struct.error: + break + ret.append((b, g, r, a)) + return ret + + def _read_bgra( + self, palette: list[tuple[int, int, int, int]], alpha: bool + ) -> bytearray: + data = bytearray() + _data = BytesIO(self._safe_read(self._lengths[0])) + while True: + try: + (offset,) = struct.unpack(" None: + self._compression, self._encoding, alpha = self.args + + if self._compression == Format.JPEG: + self._decode_jpeg_stream() + + elif self._compression == 1: + if self._encoding in (4, 5): + palette = self._read_palette() + data = self._read_bgra(palette, alpha) + self.set_as_raw(data) + else: + msg = f"Unsupported BLP encoding {repr(self._encoding)}" + raise BLPFormatError(msg) + else: + msg = f"Unsupported BLP compression {repr(self._encoding)}" + raise BLPFormatError(msg) + + def _decode_jpeg_stream(self) -> None: + from .JpegImagePlugin import JpegImageFile + + (jpeg_header_size,) = struct.unpack(" None: + self._compression, self._encoding, alpha, self._alpha_encoding = self.args + + palette = self._read_palette() + + assert self.fd is not None + self.fd.seek(self._offsets[0]) + + if self._compression == 1: + # Uncompressed or DirectX compression + + if self._encoding == Encoding.UNCOMPRESSED: + data = self._read_bgra(palette, alpha) + + elif self._encoding == Encoding.DXT: + data = bytearray() + if self._alpha_encoding == AlphaEncoding.DXT1: + linesize = (self.state.xsize + 3) // 4 * 8 + for yb in range((self.state.ysize + 3) // 4): + for d in decode_dxt1(self._safe_read(linesize), alpha): + data += d + + elif self._alpha_encoding == AlphaEncoding.DXT3: + linesize = (self.state.xsize + 3) // 4 * 16 + for yb in range((self.state.ysize + 3) // 4): + for d in decode_dxt3(self._safe_read(linesize)): + data += d + + elif self._alpha_encoding == AlphaEncoding.DXT5: + linesize = (self.state.xsize + 3) // 4 * 16 + for yb in range((self.state.ysize + 3) // 4): + for d in decode_dxt5(self._safe_read(linesize)): + data += d + else: + msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}" + raise BLPFormatError(msg) + else: + msg = f"Unknown BLP encoding {repr(self._encoding)}" + raise BLPFormatError(msg) + + else: + msg = f"Unknown BLP compression {repr(self._compression)}" + raise BLPFormatError(msg) + + self.set_as_raw(data) + + +class BLPEncoder(ImageFile.PyEncoder): + _pushes_fd = True + + def _write_palette(self) -> bytes: + data = b"" + assert self.im is not None + palette = self.im.getpalette("RGBA", "RGBA") + for i in range(len(palette) // 4): + r, g, b, a = palette[i * 4 : (i + 1) * 4] + data += struct.pack("<4B", b, g, r, a) + while len(data) < 256 * 4: + data += b"\x00" * 4 + return data + + def encode(self, bufsize: int) -> tuple[int, int, bytes]: + palette_data = self._write_palette() + + offset = 20 + 16 * 4 * 2 + len(palette_data) + data = struct.pack("<16I", offset, *((0,) * 15)) + + assert self.im is not None + w, h = self.im.size + data += struct.pack("<16I", w * h, *((0,) * 15)) + + data += palette_data + + for y in range(h): + for x in range(w): + data += struct.pack(" None: + if im.mode != "P": + msg = "Unsupported BLP image mode" + raise ValueError(msg) + + magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2" + fp.write(magic) + + assert im.palette is not None + fp.write(struct.pack(" mode, rawmode + 1: ("P", "P;1"), + 4: ("P", "P;4"), + 8: ("P", "P"), + 16: ("RGB", "BGR;15"), + 24: ("RGB", "BGR"), + 32: ("RGB", "BGRX"), +} + +USE_RAW_ALPHA = False + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"BM") + + +def _dib_accept(prefix: bytes) -> bool: + return i32(prefix) in [12, 40, 52, 56, 64, 108, 124] + + +# ============================================================================= +# Image plugin for the Windows BMP format. +# ============================================================================= +class BmpImageFile(ImageFile.ImageFile): + """Image plugin for the Windows Bitmap format (BMP)""" + + # ------------------------------------------------------------- Description + format_description = "Windows Bitmap" + format = "BMP" + + # -------------------------------------------------- BMP Compression values + COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5} + for k, v in COMPRESSIONS.items(): + vars()[k] = v + + def _bitmap(self, header: int = 0, offset: int = 0) -> None: + """Read relevant info about the BMP""" + read, seek = self.fp.read, self.fp.seek + if header: + seek(header) + # read bmp header size @offset 14 (this is part of the header size) + file_info: dict[str, bool | int | tuple[int, ...]] = { + "header_size": i32(read(4)), + "direction": -1, + } + + # -------------------- If requested, read header at a specific position + # read the rest of the bmp header, without its size + assert isinstance(file_info["header_size"], int) + header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) + + # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1 + # ----- This format has different offsets because of width/height types + # 12: BITMAPCOREHEADER/OS21XBITMAPHEADER + if file_info["header_size"] == 12: + file_info["width"] = i16(header_data, 0) + file_info["height"] = i16(header_data, 2) + file_info["planes"] = i16(header_data, 4) + file_info["bits"] = i16(header_data, 6) + file_info["compression"] = self.COMPRESSIONS["RAW"] + file_info["palette_padding"] = 3 + + # --------------------------------------------- Windows Bitmap v3 to v5 + # 40: BITMAPINFOHEADER + # 52: BITMAPV2HEADER + # 56: BITMAPV3HEADER + # 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER + # 108: BITMAPV4HEADER + # 124: BITMAPV5HEADER + elif file_info["header_size"] in (40, 52, 56, 64, 108, 124): + file_info["y_flip"] = header_data[7] == 0xFF + file_info["direction"] = 1 if file_info["y_flip"] else -1 + file_info["width"] = i32(header_data, 0) + file_info["height"] = ( + i32(header_data, 4) + if not file_info["y_flip"] + else 2**32 - i32(header_data, 4) + ) + file_info["planes"] = i16(header_data, 8) + file_info["bits"] = i16(header_data, 10) + file_info["compression"] = i32(header_data, 12) + # byte size of pixel data + file_info["data_size"] = i32(header_data, 16) + file_info["pixels_per_meter"] = ( + i32(header_data, 20), + i32(header_data, 24), + ) + file_info["colors"] = i32(header_data, 28) + file_info["palette_padding"] = 4 + assert isinstance(file_info["pixels_per_meter"], tuple) + self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) + if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: + masks = ["r_mask", "g_mask", "b_mask"] + if len(header_data) >= 48: + if len(header_data) >= 52: + masks.append("a_mask") + else: + file_info["a_mask"] = 0x0 + for idx, mask in enumerate(masks): + file_info[mask] = i32(header_data, 36 + idx * 4) + else: + # 40 byte headers only have the three components in the + # bitfields masks, ref: + # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx + # See also + # https://github.com/python-pillow/Pillow/issues/1293 + # There is a 4th component in the RGBQuad, in the alpha + # location, but it is listed as a reserved component, + # and it is not generally an alpha channel + file_info["a_mask"] = 0x0 + for mask in masks: + file_info[mask] = i32(read(4)) + assert isinstance(file_info["r_mask"], int) + assert isinstance(file_info["g_mask"], int) + assert isinstance(file_info["b_mask"], int) + assert isinstance(file_info["a_mask"], int) + file_info["rgb_mask"] = ( + file_info["r_mask"], + file_info["g_mask"], + file_info["b_mask"], + ) + file_info["rgba_mask"] = ( + file_info["r_mask"], + file_info["g_mask"], + file_info["b_mask"], + file_info["a_mask"], + ) + else: + msg = f"Unsupported BMP header type ({file_info['header_size']})" + raise OSError(msg) + + # ------------------ Special case : header is reported 40, which + # ---------------------- is shorter than real size for bpp >= 16 + assert isinstance(file_info["width"], int) + assert isinstance(file_info["height"], int) + self._size = file_info["width"], file_info["height"] + + # ------- If color count was not found in the header, compute from bits + assert isinstance(file_info["bits"], int) + file_info["colors"] = ( + file_info["colors"] + if file_info.get("colors", 0) + else (1 << file_info["bits"]) + ) + assert isinstance(file_info["colors"], int) + if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: + offset += 4 * file_info["colors"] + + # ---------------------- Check bit depth for unusual unsupported values + self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", "")) + if not self.mode: + msg = f"Unsupported BMP pixel depth ({file_info['bits']})" + raise OSError(msg) + + # ---------------- Process BMP with Bitfields compression (not palette) + decoder_name = "raw" + if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: + SUPPORTED: dict[int, list[tuple[int, ...]]] = { + 32: [ + (0xFF0000, 0xFF00, 0xFF, 0x0), + (0xFF000000, 0xFF0000, 0xFF00, 0x0), + (0xFF000000, 0xFF00, 0xFF, 0x0), + (0xFF000000, 0xFF0000, 0xFF00, 0xFF), + (0xFF, 0xFF00, 0xFF0000, 0xFF000000), + (0xFF0000, 0xFF00, 0xFF, 0xFF000000), + (0xFF000000, 0xFF00, 0xFF, 0xFF0000), + (0x0, 0x0, 0x0, 0x0), + ], + 24: [(0xFF0000, 0xFF00, 0xFF)], + 16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)], + } + MASK_MODES = { + (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", + (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", + (32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR", + (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", + (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", + (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", + (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR", + (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", + (24, (0xFF0000, 0xFF00, 0xFF)): "BGR", + (16, (0xF800, 0x7E0, 0x1F)): "BGR;16", + (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15", + } + if file_info["bits"] in SUPPORTED: + if ( + file_info["bits"] == 32 + and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] + ): + assert isinstance(file_info["rgba_mask"], tuple) + raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] + self._mode = "RGBA" if "A" in raw_mode else self.mode + elif ( + file_info["bits"] in (24, 16) + and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] + ): + assert isinstance(file_info["rgb_mask"], tuple) + raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] + else: + msg = "Unsupported BMP bitfields layout" + raise OSError(msg) + else: + msg = "Unsupported BMP bitfields layout" + raise OSError(msg) + elif file_info["compression"] == self.COMPRESSIONS["RAW"]: + if file_info["bits"] == 32 and ( + header == 22 or USE_RAW_ALPHA # 32-bit .cur offset + ): + raw_mode, self._mode = "BGRA", "RGBA" + elif file_info["compression"] in ( + self.COMPRESSIONS["RLE8"], + self.COMPRESSIONS["RLE4"], + ): + decoder_name = "bmp_rle" + else: + msg = f"Unsupported BMP compression ({file_info['compression']})" + raise OSError(msg) + + # --------------- Once the header is processed, process the palette/LUT + if self.mode == "P": # Paletted for 1, 4 and 8 bit images + # ---------------------------------------------------- 1-bit images + if not (0 < file_info["colors"] <= 65536): + msg = f"Unsupported BMP Palette size ({file_info['colors']})" + raise OSError(msg) + else: + assert isinstance(file_info["palette_padding"], int) + padding = file_info["palette_padding"] + palette = read(padding * file_info["colors"]) + grayscale = True + indices = ( + (0, 255) + if file_info["colors"] == 2 + else list(range(file_info["colors"])) + ) + + # ----------------- Check if grayscale and ignore palette if so + for ind, val in enumerate(indices): + rgb = palette[ind * padding : ind * padding + 3] + if rgb != o8(val) * 3: + grayscale = False + + # ------- If all colors are gray, white or black, ditch palette + if grayscale: + self._mode = "1" if file_info["colors"] == 2 else "L" + raw_mode = self.mode + else: + self._mode = "P" + self.palette = ImagePalette.raw( + "BGRX" if padding == 4 else "BGR", palette + ) + + # ---------------------------- Finally set the tile data for the plugin + self.info["compression"] = file_info["compression"] + args: list[Any] = [raw_mode] + if decoder_name == "bmp_rle": + args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"]) + else: + assert isinstance(file_info["width"], int) + args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) + args.append(file_info["direction"]) + self.tile = [ + ImageFile._Tile( + decoder_name, + (0, 0, file_info["width"], file_info["height"]), + offset or self.fp.tell(), + tuple(args), + ) + ] + + def _open(self) -> None: + """Open file, check magic number and read header""" + # read 14 bytes: magic number, filesize, reserved, header final offset + head_data = self.fp.read(14) + # choke if the file does not have the required magic bytes + if not _accept(head_data): + msg = "Not a BMP file" + raise SyntaxError(msg) + # read the start position of the BMP image data (u32) + offset = i32(head_data, 10) + # load bitmap information (offset=raster info) + self._bitmap(offset=offset) + + +class BmpRleDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + rle4 = self.args[1] + data = bytearray() + x = 0 + dest_length = self.state.xsize * self.state.ysize + while len(data) < dest_length: + pixels = self.fd.read(1) + byte = self.fd.read(1) + if not pixels or not byte: + break + num_pixels = pixels[0] + if num_pixels: + # encoded mode + if x + num_pixels > self.state.xsize: + # Too much data for row + num_pixels = max(0, self.state.xsize - x) + if rle4: + first_pixel = o8(byte[0] >> 4) + second_pixel = o8(byte[0] & 0x0F) + for index in range(num_pixels): + if index % 2 == 0: + data += first_pixel + else: + data += second_pixel + else: + data += byte * num_pixels + x += num_pixels + else: + if byte[0] == 0: + # end of line + while len(data) % self.state.xsize != 0: + data += b"\x00" + x = 0 + elif byte[0] == 1: + # end of bitmap + break + elif byte[0] == 2: + # delta + bytes_read = self.fd.read(2) + if len(bytes_read) < 2: + break + right, up = self.fd.read(2) + data += b"\x00" * (right + up * self.state.xsize) + x = len(data) % self.state.xsize + else: + # absolute mode + if rle4: + # 2 pixels per byte + byte_count = byte[0] // 2 + bytes_read = self.fd.read(byte_count) + for byte_read in bytes_read: + data += o8(byte_read >> 4) + data += o8(byte_read & 0x0F) + else: + byte_count = byte[0] + bytes_read = self.fd.read(byte_count) + data += bytes_read + if len(bytes_read) < byte_count: + break + x += byte[0] + + # align to 16-bit word boundary + if self.fd.tell() % 2 != 0: + self.fd.seek(1, os.SEEK_CUR) + rawmode = "L" if self.mode == "L" else "P" + self.set_as_raw(bytes(data), rawmode, (0, self.args[-1])) + return -1, 0 + + +# ============================================================================= +# Image plugin for the DIB format (BMP alias) +# ============================================================================= +class DibImageFile(BmpImageFile): + format = "DIB" + format_description = "Windows Bitmap" + + def _open(self) -> None: + self._bitmap() + + +# +# -------------------------------------------------------------------- +# Write BMP file + + +SAVE = { + "1": ("1", 1, 2), + "L": ("L", 8, 256), + "P": ("P", 8, 256), + "RGB": ("BGR", 24, 0), + "RGBA": ("BGRA", 32, 0), +} + + +def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, False) + + +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True +) -> None: + try: + rawmode, bits, colors = SAVE[im.mode] + except KeyError as e: + msg = f"cannot write mode {im.mode} as BMP" + raise OSError(msg) from e + + info = im.encoderinfo + + dpi = info.get("dpi", (96, 96)) + + # 1 meter == 39.3701 inches + ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi) + + stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) + header = 40 # or 64 for OS/2 version 2 + image = stride * im.size[1] + + if im.mode == "1": + palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255)) + elif im.mode == "L": + palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256)) + elif im.mode == "P": + palette = im.im.getpalette("RGB", "BGRX") + colors = len(palette) // 4 + else: + palette = None + + # bitmap header + if bitmap_header: + offset = 14 + header + colors * 4 + file_size = offset + image + if file_size > 2**32 - 1: + msg = "File size is too large for the BMP format" + raise ValueError(msg) + fp.write( + b"BM" # file type (magic) + + o32(file_size) # file size + + o32(0) # reserved + + o32(offset) # image data offset + ) + + # bitmap info header + fp.write( + o32(header) # info header size + + o32(im.size[0]) # width + + o32(im.size[1]) # height + + o16(1) # planes + + o16(bits) # depth + + o32(0) # compression (0=uncompressed) + + o32(image) # size of bitmap + + o32(ppm[0]) # resolution + + o32(ppm[1]) # resolution + + o32(colors) # colors used + + o32(colors) # colors important + ) + + fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) + + if palette: + fp.write(palette) + + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))] + ) + + +# +# -------------------------------------------------------------------- +# Registry + + +Image.register_open(BmpImageFile.format, BmpImageFile, _accept) +Image.register_save(BmpImageFile.format, _save) + +Image.register_extension(BmpImageFile.format, ".bmp") + +Image.register_mime(BmpImageFile.format, "image/bmp") + +Image.register_decoder("bmp_rle", BmpRleDecoder) + +Image.register_open(DibImageFile.format, DibImageFile, _dib_accept) +Image.register_save(DibImageFile.format, _dib_save) + +Image.register_extension(DibImageFile.format, ".dib") + +Image.register_mime(DibImageFile.format, "image/bmp") diff --git a/python_modules/PIL/BufrStubImagePlugin.py b/python_modules/PIL/BufrStubImagePlugin.py new file mode 100644 index 000000000..8c5da14f5 --- /dev/null +++ b/python_modules/PIL/BufrStubImagePlugin.py @@ -0,0 +1,75 @@ +# +# The Python Imaging Library +# $Id$ +# +# BUFR stub adapter +# +# Copyright (c) 1996-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +from typing import IO + +from . import Image, ImageFile + +_handler = None + + +def register_handler(handler: ImageFile.StubHandler | None) -> None: + """ + Install application-specific BUFR image handler. + + :param handler: Handler object. + """ + global _handler + _handler = handler + + +# -------------------------------------------------------------------- +# Image adapter + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith((b"BUFR", b"ZCZC")) + + +class BufrStubImageFile(ImageFile.StubImageFile): + format = "BUFR" + format_description = "BUFR" + + def _open(self) -> None: + if not _accept(self.fp.read(4)): + msg = "Not a BUFR file" + raise SyntaxError(msg) + + self.fp.seek(-4, os.SEEK_CUR) + + # make something up + self._mode = "F" + self._size = 1, 1 + + loader = self._load() + if loader: + loader.open(self) + + def _load(self) -> ImageFile.StubHandler | None: + return _handler + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if _handler is None or not hasattr(_handler, "save"): + msg = "BUFR save handler not installed" + raise OSError(msg) + _handler.save(im, fp, filename) + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept) +Image.register_save(BufrStubImageFile.format, _save) + +Image.register_extension(BufrStubImageFile.format, ".bufr") diff --git a/python_modules/PIL/ContainerIO.py b/python_modules/PIL/ContainerIO.py new file mode 100644 index 000000000..ec9e66c71 --- /dev/null +++ b/python_modules/PIL/ContainerIO.py @@ -0,0 +1,173 @@ +# +# The Python Imaging Library. +# $Id$ +# +# a class to read from a container file +# +# History: +# 1995-06-18 fl Created +# 1995-09-07 fl Added readline(), readlines() +# +# Copyright (c) 1997-2001 by Secret Labs AB +# Copyright (c) 1995 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +from collections.abc import Iterable +from typing import IO, AnyStr, NoReturn + + +class ContainerIO(IO[AnyStr]): + """ + A file object that provides read access to a part of an existing + file (for example a TAR file). + """ + + def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None: + """ + Create file object. + + :param file: Existing file. + :param offset: Start of region, in bytes. + :param length: Size of region, in bytes. + """ + self.fh: IO[AnyStr] = file + self.pos = 0 + self.offset = offset + self.length = length + self.fh.seek(offset) + + ## + # Always false. + + def isatty(self) -> bool: + return False + + def seekable(self) -> bool: + return True + + def seek(self, offset: int, mode: int = io.SEEK_SET) -> int: + """ + Move file pointer. + + :param offset: Offset in bytes. + :param mode: Starting position. Use 0 for beginning of region, 1 + for current offset, and 2 for end of region. You cannot move + the pointer outside the defined region. + :returns: Offset from start of region, in bytes. + """ + if mode == 1: + self.pos = self.pos + offset + elif mode == 2: + self.pos = self.length + offset + else: + self.pos = offset + # clamp + self.pos = max(0, min(self.pos, self.length)) + self.fh.seek(self.offset + self.pos) + return self.pos + + def tell(self) -> int: + """ + Get current file pointer. + + :returns: Offset from start of region, in bytes. + """ + return self.pos + + def readable(self) -> bool: + return True + + def read(self, n: int = -1) -> AnyStr: + """ + Read data. + + :param n: Number of bytes to read. If omitted, zero or negative, + read until end of region. + :returns: An 8-bit string. + """ + if n > 0: + n = min(n, self.length - self.pos) + else: + n = self.length - self.pos + if n <= 0: # EOF + return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] + self.pos = self.pos + n + return self.fh.read(n) + + def readline(self, n: int = -1) -> AnyStr: + """ + Read a line of text. + + :param n: Number of bytes to read. If omitted, zero or negative, + read until end of line. + :returns: An 8-bit string. + """ + s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] + newline_character = b"\n" if "b" in self.fh.mode else "\n" + while True: + c = self.read(1) + if not c: + break + s = s + c + if c == newline_character or len(s) == n: + break + return s + + def readlines(self, n: int | None = -1) -> list[AnyStr]: + """ + Read multiple lines of text. + + :param n: Number of lines to read. If omitted, zero, negative or None, + read until end of region. + :returns: A list of 8-bit strings. + """ + lines = [] + while True: + s = self.readline() + if not s: + break + lines.append(s) + if len(lines) == n: + break + return lines + + def writable(self) -> bool: + return False + + def write(self, b: AnyStr) -> NoReturn: + raise NotImplementedError() + + def writelines(self, lines: Iterable[AnyStr]) -> NoReturn: + raise NotImplementedError() + + def truncate(self, size: int | None = None) -> int: + raise NotImplementedError() + + def __enter__(self) -> ContainerIO[AnyStr]: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def __iter__(self) -> ContainerIO[AnyStr]: + return self + + def __next__(self) -> AnyStr: + line = self.readline() + if not line: + msg = "end of region" + raise StopIteration(msg) + return line + + def fileno(self) -> int: + return self.fh.fileno() + + def flush(self) -> None: + self.fh.flush() + + def close(self) -> None: + self.fh.close() diff --git a/python_modules/PIL/CurImagePlugin.py b/python_modules/PIL/CurImagePlugin.py new file mode 100644 index 000000000..b817dbc87 --- /dev/null +++ b/python_modules/PIL/CurImagePlugin.py @@ -0,0 +1,75 @@ +# +# The Python Imaging Library. +# $Id$ +# +# Windows Cursor support for PIL +# +# notes: +# uses BmpImagePlugin.py to read the bitmap data. +# +# history: +# 96-05-27 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import BmpImagePlugin, Image, ImageFile +from ._binary import i16le as i16 +from ._binary import i32le as i32 + +# +# -------------------------------------------------------------------- + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"\0\0\2\0") + + +## +# Image plugin for Windows Cursor files. + + +class CurImageFile(BmpImagePlugin.BmpImageFile): + format = "CUR" + format_description = "Windows Cursor" + + def _open(self) -> None: + offset = self.fp.tell() + + # check magic + s = self.fp.read(6) + if not _accept(s): + msg = "not a CUR file" + raise SyntaxError(msg) + + # pick the largest cursor in the file + m = b"" + for i in range(i16(s, 4)): + s = self.fp.read(16) + if not m: + m = s + elif s[0] > m[0] and s[1] > m[1]: + m = s + if not m: + msg = "No cursors were found" + raise TypeError(msg) + + # load as bitmap + self._bitmap(i32(m, 12) + offset) + + # patch up the bitmap height + self._size = self.size[0], self.size[1] // 2 + d, e, o, a = self.tile[0] + self.tile[0] = ImageFile._Tile(d, (0, 0) + self.size, o, a) + + +# +# -------------------------------------------------------------------- + +Image.register_open(CurImageFile.format, CurImageFile, _accept) + +Image.register_extension(CurImageFile.format, ".cur") diff --git a/python_modules/PIL/DcxImagePlugin.py b/python_modules/PIL/DcxImagePlugin.py new file mode 100644 index 000000000..aea661b9c --- /dev/null +++ b/python_modules/PIL/DcxImagePlugin.py @@ -0,0 +1,83 @@ +# +# The Python Imaging Library. +# $Id$ +# +# DCX file handling +# +# DCX is a container file format defined by Intel, commonly used +# for fax applications. Each DCX file consists of a directory +# (a list of file offsets) followed by a set of (usually 1-bit) +# PCX files. +# +# History: +# 1995-09-09 fl Created +# 1996-03-20 fl Properly derived from PcxImageFile. +# 1998-07-15 fl Renamed offset attribute to avoid name clash +# 2002-07-30 fl Fixed file handling +# +# Copyright (c) 1997-98 by Secret Labs AB. +# Copyright (c) 1995-96 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image +from ._binary import i32le as i32 +from ._util import DeferredError +from .PcxImagePlugin import PcxImageFile + +MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? + + +def _accept(prefix: bytes) -> bool: + return len(prefix) >= 4 and i32(prefix) == MAGIC + + +## +# Image plugin for the Intel DCX format. + + +class DcxImageFile(PcxImageFile): + format = "DCX" + format_description = "Intel DCX" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + # Header + s = self.fp.read(4) + if not _accept(s): + msg = "not a DCX file" + raise SyntaxError(msg) + + # Component directory + self._offset = [] + for i in range(1024): + offset = i32(self.fp.read(4)) + if not offset: + break + self._offset.append(offset) + + self._fp = self.fp + self.frame = -1 + self.n_frames = len(self._offset) + self.is_animated = self.n_frames > 1 + self.seek(0) + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + if isinstance(self._fp, DeferredError): + raise self._fp.ex + self.frame = frame + self.fp = self._fp + self.fp.seek(self._offset[frame]) + PcxImageFile._open(self) + + def tell(self) -> int: + return self.frame + + +Image.register_open(DcxImageFile.format, DcxImageFile, _accept) + +Image.register_extension(DcxImageFile.format, ".dcx") diff --git a/python_modules/PIL/DdsImagePlugin.py b/python_modules/PIL/DdsImagePlugin.py new file mode 100644 index 000000000..f9ade18f9 --- /dev/null +++ b/python_modules/PIL/DdsImagePlugin.py @@ -0,0 +1,624 @@ +""" +A Pillow plugin for .dds files (S3TC-compressed aka DXTC) +Jerome Leclanche + +Documentation: +https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: +https://creativecommons.org/publicdomain/zero/1.0/ +""" + +from __future__ import annotations + +import io +import struct +import sys +from enum import IntEnum, IntFlag +from typing import IO + +from . import Image, ImageFile, ImagePalette +from ._binary import i32le as i32 +from ._binary import o8 +from ._binary import o32le as o32 + +# Magic ("DDS ") +DDS_MAGIC = 0x20534444 + + +# DDS flags +class DDSD(IntFlag): + CAPS = 0x1 + HEIGHT = 0x2 + WIDTH = 0x4 + PITCH = 0x8 + PIXELFORMAT = 0x1000 + MIPMAPCOUNT = 0x20000 + LINEARSIZE = 0x80000 + DEPTH = 0x800000 + + +# DDS caps +class DDSCAPS(IntFlag): + COMPLEX = 0x8 + TEXTURE = 0x1000 + MIPMAP = 0x400000 + + +class DDSCAPS2(IntFlag): + CUBEMAP = 0x200 + CUBEMAP_POSITIVEX = 0x400 + CUBEMAP_NEGATIVEX = 0x800 + CUBEMAP_POSITIVEY = 0x1000 + CUBEMAP_NEGATIVEY = 0x2000 + CUBEMAP_POSITIVEZ = 0x4000 + CUBEMAP_NEGATIVEZ = 0x8000 + VOLUME = 0x200000 + + +# Pixel Format +class DDPF(IntFlag): + ALPHAPIXELS = 0x1 + ALPHA = 0x2 + FOURCC = 0x4 + PALETTEINDEXED8 = 0x20 + RGB = 0x40 + LUMINANCE = 0x20000 + + +# dxgiformat.h +class DXGI_FORMAT(IntEnum): + UNKNOWN = 0 + R32G32B32A32_TYPELESS = 1 + R32G32B32A32_FLOAT = 2 + R32G32B32A32_UINT = 3 + R32G32B32A32_SINT = 4 + R32G32B32_TYPELESS = 5 + R32G32B32_FLOAT = 6 + R32G32B32_UINT = 7 + R32G32B32_SINT = 8 + R16G16B16A16_TYPELESS = 9 + R16G16B16A16_FLOAT = 10 + R16G16B16A16_UNORM = 11 + R16G16B16A16_UINT = 12 + R16G16B16A16_SNORM = 13 + R16G16B16A16_SINT = 14 + R32G32_TYPELESS = 15 + R32G32_FLOAT = 16 + R32G32_UINT = 17 + R32G32_SINT = 18 + R32G8X24_TYPELESS = 19 + D32_FLOAT_S8X24_UINT = 20 + R32_FLOAT_X8X24_TYPELESS = 21 + X32_TYPELESS_G8X24_UINT = 22 + R10G10B10A2_TYPELESS = 23 + R10G10B10A2_UNORM = 24 + R10G10B10A2_UINT = 25 + R11G11B10_FLOAT = 26 + R8G8B8A8_TYPELESS = 27 + R8G8B8A8_UNORM = 28 + R8G8B8A8_UNORM_SRGB = 29 + R8G8B8A8_UINT = 30 + R8G8B8A8_SNORM = 31 + R8G8B8A8_SINT = 32 + R16G16_TYPELESS = 33 + R16G16_FLOAT = 34 + R16G16_UNORM = 35 + R16G16_UINT = 36 + R16G16_SNORM = 37 + R16G16_SINT = 38 + R32_TYPELESS = 39 + D32_FLOAT = 40 + R32_FLOAT = 41 + R32_UINT = 42 + R32_SINT = 43 + R24G8_TYPELESS = 44 + D24_UNORM_S8_UINT = 45 + R24_UNORM_X8_TYPELESS = 46 + X24_TYPELESS_G8_UINT = 47 + R8G8_TYPELESS = 48 + R8G8_UNORM = 49 + R8G8_UINT = 50 + R8G8_SNORM = 51 + R8G8_SINT = 52 + R16_TYPELESS = 53 + R16_FLOAT = 54 + D16_UNORM = 55 + R16_UNORM = 56 + R16_UINT = 57 + R16_SNORM = 58 + R16_SINT = 59 + R8_TYPELESS = 60 + R8_UNORM = 61 + R8_UINT = 62 + R8_SNORM = 63 + R8_SINT = 64 + A8_UNORM = 65 + R1_UNORM = 66 + R9G9B9E5_SHAREDEXP = 67 + R8G8_B8G8_UNORM = 68 + G8R8_G8B8_UNORM = 69 + BC1_TYPELESS = 70 + BC1_UNORM = 71 + BC1_UNORM_SRGB = 72 + BC2_TYPELESS = 73 + BC2_UNORM = 74 + BC2_UNORM_SRGB = 75 + BC3_TYPELESS = 76 + BC3_UNORM = 77 + BC3_UNORM_SRGB = 78 + BC4_TYPELESS = 79 + BC4_UNORM = 80 + BC4_SNORM = 81 + BC5_TYPELESS = 82 + BC5_UNORM = 83 + BC5_SNORM = 84 + B5G6R5_UNORM = 85 + B5G5R5A1_UNORM = 86 + B8G8R8A8_UNORM = 87 + B8G8R8X8_UNORM = 88 + R10G10B10_XR_BIAS_A2_UNORM = 89 + B8G8R8A8_TYPELESS = 90 + B8G8R8A8_UNORM_SRGB = 91 + B8G8R8X8_TYPELESS = 92 + B8G8R8X8_UNORM_SRGB = 93 + BC6H_TYPELESS = 94 + BC6H_UF16 = 95 + BC6H_SF16 = 96 + BC7_TYPELESS = 97 + BC7_UNORM = 98 + BC7_UNORM_SRGB = 99 + AYUV = 100 + Y410 = 101 + Y416 = 102 + NV12 = 103 + P010 = 104 + P016 = 105 + OPAQUE_420 = 106 + YUY2 = 107 + Y210 = 108 + Y216 = 109 + NV11 = 110 + AI44 = 111 + IA44 = 112 + P8 = 113 + A8P8 = 114 + B4G4R4A4_UNORM = 115 + P208 = 130 + V208 = 131 + V408 = 132 + SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189 + SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190 + + +class D3DFMT(IntEnum): + UNKNOWN = 0 + R8G8B8 = 20 + A8R8G8B8 = 21 + X8R8G8B8 = 22 + R5G6B5 = 23 + X1R5G5B5 = 24 + A1R5G5B5 = 25 + A4R4G4B4 = 26 + R3G3B2 = 27 + A8 = 28 + A8R3G3B2 = 29 + X4R4G4B4 = 30 + A2B10G10R10 = 31 + A8B8G8R8 = 32 + X8B8G8R8 = 33 + G16R16 = 34 + A2R10G10B10 = 35 + A16B16G16R16 = 36 + A8P8 = 40 + P8 = 41 + L8 = 50 + A8L8 = 51 + A4L4 = 52 + V8U8 = 60 + L6V5U5 = 61 + X8L8V8U8 = 62 + Q8W8V8U8 = 63 + V16U16 = 64 + A2W10V10U10 = 67 + D16_LOCKABLE = 70 + D32 = 71 + D15S1 = 73 + D24S8 = 75 + D24X8 = 77 + D24X4S4 = 79 + D16 = 80 + D32F_LOCKABLE = 82 + D24FS8 = 83 + D32_LOCKABLE = 84 + S8_LOCKABLE = 85 + L16 = 81 + VERTEXDATA = 100 + INDEX16 = 101 + INDEX32 = 102 + Q16W16V16U16 = 110 + R16F = 111 + G16R16F = 112 + A16B16G16R16F = 113 + R32F = 114 + G32R32F = 115 + A32B32G32R32F = 116 + CxV8U8 = 117 + A1 = 118 + A2B10G10R10_XR_BIAS = 119 + BINARYBUFFER = 199 + + UYVY = i32(b"UYVY") + R8G8_B8G8 = i32(b"RGBG") + YUY2 = i32(b"YUY2") + G8R8_G8B8 = i32(b"GRGB") + DXT1 = i32(b"DXT1") + DXT2 = i32(b"DXT2") + DXT3 = i32(b"DXT3") + DXT4 = i32(b"DXT4") + DXT5 = i32(b"DXT5") + DX10 = i32(b"DX10") + BC4S = i32(b"BC4S") + BC4U = i32(b"BC4U") + BC5S = i32(b"BC5S") + BC5U = i32(b"BC5U") + ATI1 = i32(b"ATI1") + ATI2 = i32(b"ATI2") + MULTI2_ARGB8 = i32(b"MET1") + + +# Backward compatibility layer +module = sys.modules[__name__] +for item in DDSD: + assert item.name is not None + setattr(module, f"DDSD_{item.name}", item.value) +for item1 in DDSCAPS: + assert item1.name is not None + setattr(module, f"DDSCAPS_{item1.name}", item1.value) +for item2 in DDSCAPS2: + assert item2.name is not None + setattr(module, f"DDSCAPS2_{item2.name}", item2.value) +for item3 in DDPF: + assert item3.name is not None + setattr(module, f"DDPF_{item3.name}", item3.value) + +DDS_FOURCC = DDPF.FOURCC +DDS_RGB = DDPF.RGB +DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS +DDS_LUMINANCE = DDPF.LUMINANCE +DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS +DDS_ALPHA = DDPF.ALPHA +DDS_PAL8 = DDPF.PALETTEINDEXED8 + +DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT +DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT +DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH +DDS_HEADER_FLAGS_PITCH = DDSD.PITCH +DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE + +DDS_HEIGHT = DDSD.HEIGHT +DDS_WIDTH = DDSD.WIDTH + +DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE +DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP +DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX + +DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX +DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX +DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY +DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY +DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ +DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ + +DXT1_FOURCC = D3DFMT.DXT1 +DXT3_FOURCC = D3DFMT.DXT3 +DXT5_FOURCC = D3DFMT.DXT5 + +DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS +DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM +DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB +DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS +DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM +DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM +DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16 +DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16 +DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS +DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM +DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB + + +class DdsImageFile(ImageFile.ImageFile): + format = "DDS" + format_description = "DirectDraw Surface" + + def _open(self) -> None: + if not _accept(self.fp.read(4)): + msg = "not a DDS file" + raise SyntaxError(msg) + (header_size,) = struct.unpack(" None: + pass + + +class DdsRgbDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + bitcount, masks = self.args + + # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100 + # Calculate how many zeros each mask is padded with + mask_offsets = [] + # And the maximum value of each channel without the padding + mask_totals = [] + for mask in masks: + offset = 0 + if mask != 0: + while mask >> (offset + 1) << (offset + 1) == mask: + offset += 1 + mask_offsets.append(offset) + mask_totals.append(mask >> offset) + + data = bytearray() + bytecount = bitcount // 8 + dest_length = self.state.xsize * self.state.ysize * len(masks) + while len(data) < dest_length: + value = int.from_bytes(self.fd.read(bytecount), "little") + for i, mask in enumerate(masks): + masked_value = value & mask + # Remove the zero padding, and scale it to 8 bits + data += o8( + int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) + ) + self.set_as_raw(data) + return -1, 0 + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode not in ("RGB", "RGBA", "L", "LA"): + msg = f"cannot write mode {im.mode} as DDS" + raise OSError(msg) + + flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT + bitcount = len(im.getbands()) * 8 + pixel_format = im.encoderinfo.get("pixel_format") + args: tuple[int] | str + if pixel_format: + codec_name = "bcn" + flags |= DDSD.LINEARSIZE + pitch = (im.width + 3) * 4 + rgba_mask = [0, 0, 0, 0] + pixel_flags = DDPF.FOURCC + if pixel_format == "DXT1": + fourcc = D3DFMT.DXT1 + args = (1,) + elif pixel_format == "DXT3": + fourcc = D3DFMT.DXT3 + args = (2,) + elif pixel_format == "DXT5": + fourcc = D3DFMT.DXT5 + args = (3,) + else: + fourcc = D3DFMT.DX10 + if pixel_format == "BC2": + args = (2,) + dxgi_format = DXGI_FORMAT.BC2_TYPELESS + elif pixel_format == "BC3": + args = (3,) + dxgi_format = DXGI_FORMAT.BC3_TYPELESS + elif pixel_format == "BC5": + args = (5,) + dxgi_format = DXGI_FORMAT.BC5_TYPELESS + if im.mode != "RGB": + msg = "only RGB mode can be written as BC5" + raise OSError(msg) + else: + msg = f"cannot write pixel format {pixel_format}" + raise OSError(msg) + else: + codec_name = "raw" + flags |= DDSD.PITCH + pitch = (im.width * bitcount + 7) // 8 + + alpha = im.mode[-1] == "A" + if im.mode[0] == "L": + pixel_flags = DDPF.LUMINANCE + args = im.mode + if alpha: + rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] + else: + rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] + else: + pixel_flags = DDPF.RGB + args = im.mode[::-1] + rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] + + if alpha: + r, g, b, a = im.split() + im = Image.merge("RGBA", (a, r, g, b)) + if alpha: + pixel_flags |= DDPF.ALPHAPIXELS + rgba_mask.append(0xFF000000 if alpha else 0) + + fourcc = D3DFMT.UNKNOWN + fp.write( + o32(DDS_MAGIC) + + struct.pack( + "<7I", + 124, # header size + flags, # flags + im.height, + im.width, + pitch, + 0, # depth + 0, # mipmaps + ) + + struct.pack("11I", *((0,) * 11)) # reserved + # pfsize, pfflags, fourcc, bitcount + + struct.pack("<4I", 32, pixel_flags, fourcc, bitcount) + + struct.pack("<4I", *rgba_mask) # dwRGBABitMask + + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) + ) + if fourcc == D3DFMT.DX10: + fp.write( + # dxgi_format, 2D resource, misc, array size, straight alpha + struct.pack("<5I", dxgi_format, 3, 0, 0, 1) + ) + ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)]) + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"DDS ") + + +Image.register_open(DdsImageFile.format, DdsImageFile, _accept) +Image.register_decoder("dds_rgb", DdsRgbDecoder) +Image.register_save(DdsImageFile.format, _save) +Image.register_extension(DdsImageFile.format, ".dds") diff --git a/python_modules/PIL/EpsImagePlugin.py b/python_modules/PIL/EpsImagePlugin.py new file mode 100644 index 000000000..5e2ddad99 --- /dev/null +++ b/python_modules/PIL/EpsImagePlugin.py @@ -0,0 +1,476 @@ +# +# The Python Imaging Library. +# $Id$ +# +# EPS file handling +# +# History: +# 1995-09-01 fl Created (0.1) +# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2) +# 1996-08-22 fl Don't choke on floating point BoundingBox values +# 1996-08-23 fl Handle files from Macintosh (0.3) +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) +# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5) +# 2014-05-07 e Handling of EPS with binary preview and fixed resolution +# resizing +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +import os +import re +import subprocess +import sys +import tempfile +from typing import IO + +from . import Image, ImageFile +from ._binary import i32le as i32 + +# -------------------------------------------------------------------- + + +split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") +field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") + +gs_binary: str | bool | None = None +gs_windows_binary = None + + +def has_ghostscript() -> bool: + global gs_binary, gs_windows_binary + if gs_binary is None: + if sys.platform.startswith("win"): + if gs_windows_binary is None: + import shutil + + for binary in ("gswin32c", "gswin64c", "gs"): + if shutil.which(binary) is not None: + gs_windows_binary = binary + break + else: + gs_windows_binary = False + gs_binary = gs_windows_binary + else: + try: + subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) + gs_binary = "gs" + except OSError: + gs_binary = False + return gs_binary is not False + + +def Ghostscript( + tile: list[ImageFile._Tile], + size: tuple[int, int], + fp: IO[bytes], + scale: int = 1, + transparency: bool = False, +) -> Image.core.ImagingCore: + """Render an image using Ghostscript""" + global gs_binary + if not has_ghostscript(): + msg = "Unable to locate Ghostscript on paths" + raise OSError(msg) + assert isinstance(gs_binary, str) + + # Unpack decoder tile + args = tile[0].args + assert isinstance(args, tuple) + length, bbox = args + + # Hack to support hi-res rendering + scale = int(scale) or 1 + width = size[0] * scale + height = size[1] * scale + # resolution is dependent on bbox and size + res_x = 72.0 * width / (bbox[2] - bbox[0]) + res_y = 72.0 * height / (bbox[3] - bbox[1]) + + out_fd, outfile = tempfile.mkstemp() + os.close(out_fd) + + infile_temp = None + if hasattr(fp, "name") and os.path.exists(fp.name): + infile = fp.name + else: + in_fd, infile_temp = tempfile.mkstemp() + os.close(in_fd) + infile = infile_temp + + # Ignore length and offset! + # Ghostscript can read it + # Copy whole file to read in Ghostscript + with open(infile_temp, "wb") as f: + # fetch length of fp + fp.seek(0, io.SEEK_END) + fsize = fp.tell() + # ensure start position + # go back + fp.seek(0) + lengthfile = fsize + while lengthfile > 0: + s = fp.read(min(lengthfile, 100 * 1024)) + if not s: + break + lengthfile -= len(s) + f.write(s) + + if transparency: + # "RGBA" + device = "pngalpha" + else: + # "pnmraw" automatically chooses between + # PBM ("1"), PGM ("L"), and PPM ("RGB"). + device = "pnmraw" + + # Build Ghostscript command + command = [ + gs_binary, + "-q", # quiet mode + f"-g{width:d}x{height:d}", # set output geometry (pixels) + f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch) + "-dBATCH", # exit after processing + "-dNOPAUSE", # don't pause between pages + "-dSAFER", # safe mode + f"-sDEVICE={device}", + f"-sOutputFile={outfile}", # output file + # adjust for image origin + "-c", + f"{-bbox[0]} {-bbox[1]} translate", + "-f", + infile, # input file + # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272) + "-c", + "showpage", + ] + + # push data through Ghostscript + try: + startupinfo = None + if sys.platform.startswith("win"): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + subprocess.check_call(command, startupinfo=startupinfo) + with Image.open(outfile) as out_im: + out_im.load() + return out_im.im.copy() + finally: + try: + os.unlink(outfile) + if infile_temp: + os.unlink(infile_temp) + except OSError: + pass + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"%!PS") or ( + len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5 + ) + + +## +# Image plugin for Encapsulated PostScript. This plugin supports only +# a few variants of this format. + + +class EpsImageFile(ImageFile.ImageFile): + """EPS File Parser for the Python Imaging Library""" + + format = "EPS" + format_description = "Encapsulated Postscript" + + mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} + + def _open(self) -> None: + (length, offset) = self._find_offset(self.fp) + + # go to offset - start of "%!PS" + self.fp.seek(offset) + + self._mode = "RGB" + + # When reading header comments, the first comment is used. + # When reading trailer comments, the last comment is used. + bounding_box: list[int] | None = None + imagedata_size: tuple[int, int] | None = None + + byte_arr = bytearray(255) + bytes_mv = memoryview(byte_arr) + bytes_read = 0 + reading_header_comments = True + reading_trailer_comments = False + trailer_reached = False + + def check_required_header_comments() -> None: + """ + The EPS specification requires that some headers exist. + This should be checked when the header comments formally end, + when image data starts, or when the file ends, whichever comes first. + """ + if "PS-Adobe" not in self.info: + msg = 'EPS header missing "%!PS-Adobe" comment' + raise SyntaxError(msg) + if "BoundingBox" not in self.info: + msg = 'EPS header missing "%%BoundingBox" comment' + raise SyntaxError(msg) + + def read_comment(s: str) -> bool: + nonlocal bounding_box, reading_trailer_comments + try: + m = split.match(s) + except re.error as e: + msg = "not an EPS file" + raise SyntaxError(msg) from e + + if not m: + return False + + k, v = m.group(1, 2) + self.info[k] = v + if k == "BoundingBox": + if v == "(atend)": + reading_trailer_comments = True + elif not bounding_box or (trailer_reached and reading_trailer_comments): + try: + # Note: The DSC spec says that BoundingBox + # fields should be integers, but some drivers + # put floating point values there anyway. + bounding_box = [int(float(i)) for i in v.split()] + except Exception: + pass + return True + + while True: + byte = self.fp.read(1) + if byte == b"": + # if we didn't read a byte we must be at the end of the file + if bytes_read == 0: + if reading_header_comments: + check_required_header_comments() + break + elif byte in b"\r\n": + # if we read a line ending character, ignore it and parse what + # we have already read. if we haven't read any other characters, + # continue reading + if bytes_read == 0: + continue + else: + # ASCII/hexadecimal lines in an EPS file must not exceed + # 255 characters, not including line ending characters + if bytes_read >= 255: + # only enforce this for lines starting with a "%", + # otherwise assume it's binary data + if byte_arr[0] == ord("%"): + msg = "not an EPS file" + raise SyntaxError(msg) + else: + if reading_header_comments: + check_required_header_comments() + reading_header_comments = False + # reset bytes_read so we can keep reading + # data until the end of the line + bytes_read = 0 + byte_arr[bytes_read] = byte[0] + bytes_read += 1 + continue + + if reading_header_comments: + # Load EPS header + + # if this line doesn't start with a "%", + # or does start with "%%EndComments", + # then we've reached the end of the header/comments + if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": + check_required_header_comments() + reading_header_comments = False + continue + + s = str(bytes_mv[:bytes_read], "latin-1") + if not read_comment(s): + m = field.match(s) + if m: + k = m.group(1) + if k.startswith("PS-Adobe"): + self.info["PS-Adobe"] = k[9:] + else: + self.info[k] = "" + elif s[0] == "%": + # handle non-DSC PostScript comments that some + # tools mistakenly put in the Comments section + pass + else: + msg = "bad EPS header" + raise OSError(msg) + elif bytes_mv[:11] == b"%ImageData:": + # Check for an "ImageData" descriptor + # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 + + # If we've already read an "ImageData" descriptor, + # don't read another one. + if imagedata_size: + bytes_read = 0 + continue + + # Values: + # columns + # rows + # bit depth (1 or 8) + # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) + # number of padding channels + # block size (number of bytes per row per channel) + # binary/ascii (1: binary, 2: ascii) + # data start identifier (the image data follows after a single line + # consisting only of this quoted value) + image_data_values = byte_arr[11:bytes_read].split(None, 7) + columns, rows, bit_depth, mode_id = ( + int(value) for value in image_data_values[:4] + ) + + if bit_depth == 1: + self._mode = "1" + elif bit_depth == 8: + try: + self._mode = self.mode_map[mode_id] + except ValueError: + break + else: + break + + # Parse the columns and rows after checking the bit depth and mode + # in case the bit depth and/or mode are invalid. + imagedata_size = columns, rows + elif bytes_mv[:5] == b"%%EOF": + break + elif trailer_reached and reading_trailer_comments: + # Load EPS trailer + s = str(bytes_mv[:bytes_read], "latin-1") + read_comment(s) + elif bytes_mv[:9] == b"%%Trailer": + trailer_reached = True + bytes_read = 0 + + # A "BoundingBox" is always required, + # even if an "ImageData" descriptor size exists. + if not bounding_box: + msg = "cannot determine EPS bounding box" + raise OSError(msg) + + # An "ImageData" size takes precedence over the "BoundingBox". + self._size = imagedata_size or ( + bounding_box[2] - bounding_box[0], + bounding_box[3] - bounding_box[1], + ) + + self.tile = [ + ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box)) + ] + + def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: + s = fp.read(4) + + if s == b"%!PS": + # for HEAD without binary preview + fp.seek(0, io.SEEK_END) + length = fp.tell() + offset = 0 + elif i32(s) == 0xC6D3D0C5: + # FIX for: Some EPS file not handled correctly / issue #302 + # EPS can contain binary data + # or start directly with latin coding + # more info see: + # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf + s = fp.read(8) + offset = i32(s) + length = i32(s, 4) + else: + msg = "not an EPS file" + raise SyntaxError(msg) + + return length, offset + + def load( + self, scale: int = 1, transparency: bool = False + ) -> Image.core.PixelAccess | None: + # Load EPS via Ghostscript + if self.tile: + self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) + self._mode = self.im.mode + self._size = self.im.size + self.tile = [] + return Image.Image.load(self) + + def load_seek(self, pos: int) -> None: + # we can't incrementally load, so force ImageFile.parser to + # use our custom load method by defining this method. + pass + + +# -------------------------------------------------------------------- + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None: + """EPS Writer for the Python Imaging Library.""" + + # make sure image data is available + im.load() + + # determine PostScript image mode + if im.mode == "L": + operator = (8, 1, b"image") + elif im.mode == "RGB": + operator = (8, 3, b"false 3 colorimage") + elif im.mode == "CMYK": + operator = (8, 4, b"false 4 colorimage") + else: + msg = "image mode is not supported" + raise ValueError(msg) + + if eps: + # write EPS header + fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") + fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") + # fp.write("%%CreationDate: %s"...) + fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size) + fp.write(b"%%Pages: 1\n") + fp.write(b"%%EndComments\n") + fp.write(b"%%Page: 1 1\n") + fp.write(b"%%ImageData: %d %d " % im.size) + fp.write(b'%d %d 0 1 1 "%s"\n' % operator) + + # image header + fp.write(b"gsave\n") + fp.write(b"10 dict begin\n") + fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1])) + fp.write(b"%d %d scale\n" % im.size) + fp.write(b"%d %d 8\n" % im.size) # <= bits + fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) + fp.write(b"{ currentfile buf readhexstring pop } bind\n") + fp.write(operator[2] + b"\n") + if hasattr(fp, "flush"): + fp.flush() + + ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)]) + + fp.write(b"\n%%%%EndBinary\n") + fp.write(b"grestore end\n") + if hasattr(fp, "flush"): + fp.flush() + + +# -------------------------------------------------------------------- + + +Image.register_open(EpsImageFile.format, EpsImageFile, _accept) + +Image.register_save(EpsImageFile.format, _save) + +Image.register_extensions(EpsImageFile.format, [".ps", ".eps"]) + +Image.register_mime(EpsImageFile.format, "application/postscript") diff --git a/python_modules/PIL/ExifTags.py b/python_modules/PIL/ExifTags.py new file mode 100644 index 000000000..2280d5ce8 --- /dev/null +++ b/python_modules/PIL/ExifTags.py @@ -0,0 +1,382 @@ +# +# The Python Imaging Library. +# $Id$ +# +# EXIF tags +# +# Copyright (c) 2003 by Secret Labs AB +# +# See the README file for information on usage and redistribution. +# + +""" +This module provides constants and clear-text names for various +well-known EXIF tags. +""" +from __future__ import annotations + +from enum import IntEnum + + +class Base(IntEnum): + # possibly incomplete + InteropIndex = 0x0001 + ProcessingSoftware = 0x000B + NewSubfileType = 0x00FE + SubfileType = 0x00FF + ImageWidth = 0x0100 + ImageLength = 0x0101 + BitsPerSample = 0x0102 + Compression = 0x0103 + PhotometricInterpretation = 0x0106 + Thresholding = 0x0107 + CellWidth = 0x0108 + CellLength = 0x0109 + FillOrder = 0x010A + DocumentName = 0x010D + ImageDescription = 0x010E + Make = 0x010F + Model = 0x0110 + StripOffsets = 0x0111 + Orientation = 0x0112 + SamplesPerPixel = 0x0115 + RowsPerStrip = 0x0116 + StripByteCounts = 0x0117 + MinSampleValue = 0x0118 + MaxSampleValue = 0x0119 + XResolution = 0x011A + YResolution = 0x011B + PlanarConfiguration = 0x011C + PageName = 0x011D + FreeOffsets = 0x0120 + FreeByteCounts = 0x0121 + GrayResponseUnit = 0x0122 + GrayResponseCurve = 0x0123 + T4Options = 0x0124 + T6Options = 0x0125 + ResolutionUnit = 0x0128 + PageNumber = 0x0129 + TransferFunction = 0x012D + Software = 0x0131 + DateTime = 0x0132 + Artist = 0x013B + HostComputer = 0x013C + Predictor = 0x013D + WhitePoint = 0x013E + PrimaryChromaticities = 0x013F + ColorMap = 0x0140 + HalftoneHints = 0x0141 + TileWidth = 0x0142 + TileLength = 0x0143 + TileOffsets = 0x0144 + TileByteCounts = 0x0145 + SubIFDs = 0x014A + InkSet = 0x014C + InkNames = 0x014D + NumberOfInks = 0x014E + DotRange = 0x0150 + TargetPrinter = 0x0151 + ExtraSamples = 0x0152 + SampleFormat = 0x0153 + SMinSampleValue = 0x0154 + SMaxSampleValue = 0x0155 + TransferRange = 0x0156 + ClipPath = 0x0157 + XClipPathUnits = 0x0158 + YClipPathUnits = 0x0159 + Indexed = 0x015A + JPEGTables = 0x015B + OPIProxy = 0x015F + JPEGProc = 0x0200 + JpegIFOffset = 0x0201 + JpegIFByteCount = 0x0202 + JpegRestartInterval = 0x0203 + JpegLosslessPredictors = 0x0205 + JpegPointTransforms = 0x0206 + JpegQTables = 0x0207 + JpegDCTables = 0x0208 + JpegACTables = 0x0209 + YCbCrCoefficients = 0x0211 + YCbCrSubSampling = 0x0212 + YCbCrPositioning = 0x0213 + ReferenceBlackWhite = 0x0214 + XMLPacket = 0x02BC + RelatedImageFileFormat = 0x1000 + RelatedImageWidth = 0x1001 + RelatedImageLength = 0x1002 + Rating = 0x4746 + RatingPercent = 0x4749 + ImageID = 0x800D + CFARepeatPatternDim = 0x828D + BatteryLevel = 0x828F + Copyright = 0x8298 + ExposureTime = 0x829A + FNumber = 0x829D + IPTCNAA = 0x83BB + ImageResources = 0x8649 + ExifOffset = 0x8769 + InterColorProfile = 0x8773 + ExposureProgram = 0x8822 + SpectralSensitivity = 0x8824 + GPSInfo = 0x8825 + ISOSpeedRatings = 0x8827 + OECF = 0x8828 + Interlace = 0x8829 + TimeZoneOffset = 0x882A + SelfTimerMode = 0x882B + SensitivityType = 0x8830 + StandardOutputSensitivity = 0x8831 + RecommendedExposureIndex = 0x8832 + ISOSpeed = 0x8833 + ISOSpeedLatitudeyyy = 0x8834 + ISOSpeedLatitudezzz = 0x8835 + ExifVersion = 0x9000 + DateTimeOriginal = 0x9003 + DateTimeDigitized = 0x9004 + OffsetTime = 0x9010 + OffsetTimeOriginal = 0x9011 + OffsetTimeDigitized = 0x9012 + ComponentsConfiguration = 0x9101 + CompressedBitsPerPixel = 0x9102 + ShutterSpeedValue = 0x9201 + ApertureValue = 0x9202 + BrightnessValue = 0x9203 + ExposureBiasValue = 0x9204 + MaxApertureValue = 0x9205 + SubjectDistance = 0x9206 + MeteringMode = 0x9207 + LightSource = 0x9208 + Flash = 0x9209 + FocalLength = 0x920A + Noise = 0x920D + ImageNumber = 0x9211 + SecurityClassification = 0x9212 + ImageHistory = 0x9213 + TIFFEPStandardID = 0x9216 + MakerNote = 0x927C + UserComment = 0x9286 + SubsecTime = 0x9290 + SubsecTimeOriginal = 0x9291 + SubsecTimeDigitized = 0x9292 + AmbientTemperature = 0x9400 + Humidity = 0x9401 + Pressure = 0x9402 + WaterDepth = 0x9403 + Acceleration = 0x9404 + CameraElevationAngle = 0x9405 + XPTitle = 0x9C9B + XPComment = 0x9C9C + XPAuthor = 0x9C9D + XPKeywords = 0x9C9E + XPSubject = 0x9C9F + FlashPixVersion = 0xA000 + ColorSpace = 0xA001 + ExifImageWidth = 0xA002 + ExifImageHeight = 0xA003 + RelatedSoundFile = 0xA004 + ExifInteroperabilityOffset = 0xA005 + FlashEnergy = 0xA20B + SpatialFrequencyResponse = 0xA20C + FocalPlaneXResolution = 0xA20E + FocalPlaneYResolution = 0xA20F + FocalPlaneResolutionUnit = 0xA210 + SubjectLocation = 0xA214 + ExposureIndex = 0xA215 + SensingMethod = 0xA217 + FileSource = 0xA300 + SceneType = 0xA301 + CFAPattern = 0xA302 + CustomRendered = 0xA401 + ExposureMode = 0xA402 + WhiteBalance = 0xA403 + DigitalZoomRatio = 0xA404 + FocalLengthIn35mmFilm = 0xA405 + SceneCaptureType = 0xA406 + GainControl = 0xA407 + Contrast = 0xA408 + Saturation = 0xA409 + Sharpness = 0xA40A + DeviceSettingDescription = 0xA40B + SubjectDistanceRange = 0xA40C + ImageUniqueID = 0xA420 + CameraOwnerName = 0xA430 + BodySerialNumber = 0xA431 + LensSpecification = 0xA432 + LensMake = 0xA433 + LensModel = 0xA434 + LensSerialNumber = 0xA435 + CompositeImage = 0xA460 + CompositeImageCount = 0xA461 + CompositeImageExposureTimes = 0xA462 + Gamma = 0xA500 + PrintImageMatching = 0xC4A5 + DNGVersion = 0xC612 + DNGBackwardVersion = 0xC613 + UniqueCameraModel = 0xC614 + LocalizedCameraModel = 0xC615 + CFAPlaneColor = 0xC616 + CFALayout = 0xC617 + LinearizationTable = 0xC618 + BlackLevelRepeatDim = 0xC619 + BlackLevel = 0xC61A + BlackLevelDeltaH = 0xC61B + BlackLevelDeltaV = 0xC61C + WhiteLevel = 0xC61D + DefaultScale = 0xC61E + DefaultCropOrigin = 0xC61F + DefaultCropSize = 0xC620 + ColorMatrix1 = 0xC621 + ColorMatrix2 = 0xC622 + CameraCalibration1 = 0xC623 + CameraCalibration2 = 0xC624 + ReductionMatrix1 = 0xC625 + ReductionMatrix2 = 0xC626 + AnalogBalance = 0xC627 + AsShotNeutral = 0xC628 + AsShotWhiteXY = 0xC629 + BaselineExposure = 0xC62A + BaselineNoise = 0xC62B + BaselineSharpness = 0xC62C + BayerGreenSplit = 0xC62D + LinearResponseLimit = 0xC62E + CameraSerialNumber = 0xC62F + LensInfo = 0xC630 + ChromaBlurRadius = 0xC631 + AntiAliasStrength = 0xC632 + ShadowScale = 0xC633 + DNGPrivateData = 0xC634 + MakerNoteSafety = 0xC635 + CalibrationIlluminant1 = 0xC65A + CalibrationIlluminant2 = 0xC65B + BestQualityScale = 0xC65C + RawDataUniqueID = 0xC65D + OriginalRawFileName = 0xC68B + OriginalRawFileData = 0xC68C + ActiveArea = 0xC68D + MaskedAreas = 0xC68E + AsShotICCProfile = 0xC68F + AsShotPreProfileMatrix = 0xC690 + CurrentICCProfile = 0xC691 + CurrentPreProfileMatrix = 0xC692 + ColorimetricReference = 0xC6BF + CameraCalibrationSignature = 0xC6F3 + ProfileCalibrationSignature = 0xC6F4 + AsShotProfileName = 0xC6F6 + NoiseReductionApplied = 0xC6F7 + ProfileName = 0xC6F8 + ProfileHueSatMapDims = 0xC6F9 + ProfileHueSatMapData1 = 0xC6FA + ProfileHueSatMapData2 = 0xC6FB + ProfileToneCurve = 0xC6FC + ProfileEmbedPolicy = 0xC6FD + ProfileCopyright = 0xC6FE + ForwardMatrix1 = 0xC714 + ForwardMatrix2 = 0xC715 + PreviewApplicationName = 0xC716 + PreviewApplicationVersion = 0xC717 + PreviewSettingsName = 0xC718 + PreviewSettingsDigest = 0xC719 + PreviewColorSpace = 0xC71A + PreviewDateTime = 0xC71B + RawImageDigest = 0xC71C + OriginalRawFileDigest = 0xC71D + SubTileBlockSize = 0xC71E + RowInterleaveFactor = 0xC71F + ProfileLookTableDims = 0xC725 + ProfileLookTableData = 0xC726 + OpcodeList1 = 0xC740 + OpcodeList2 = 0xC741 + OpcodeList3 = 0xC74E + NoiseProfile = 0xC761 + + +"""Maps EXIF tags to tag names.""" +TAGS = { + **{i.value: i.name for i in Base}, + 0x920C: "SpatialFrequencyResponse", + 0x9214: "SubjectLocation", + 0x9215: "ExposureIndex", + 0x828E: "CFAPattern", + 0x920B: "FlashEnergy", + 0x9216: "TIFF/EPStandardID", +} + + +class GPS(IntEnum): + GPSVersionID = 0x00 + GPSLatitudeRef = 0x01 + GPSLatitude = 0x02 + GPSLongitudeRef = 0x03 + GPSLongitude = 0x04 + GPSAltitudeRef = 0x05 + GPSAltitude = 0x06 + GPSTimeStamp = 0x07 + GPSSatellites = 0x08 + GPSStatus = 0x09 + GPSMeasureMode = 0x0A + GPSDOP = 0x0B + GPSSpeedRef = 0x0C + GPSSpeed = 0x0D + GPSTrackRef = 0x0E + GPSTrack = 0x0F + GPSImgDirectionRef = 0x10 + GPSImgDirection = 0x11 + GPSMapDatum = 0x12 + GPSDestLatitudeRef = 0x13 + GPSDestLatitude = 0x14 + GPSDestLongitudeRef = 0x15 + GPSDestLongitude = 0x16 + GPSDestBearingRef = 0x17 + GPSDestBearing = 0x18 + GPSDestDistanceRef = 0x19 + GPSDestDistance = 0x1A + GPSProcessingMethod = 0x1B + GPSAreaInformation = 0x1C + GPSDateStamp = 0x1D + GPSDifferential = 0x1E + GPSHPositioningError = 0x1F + + +"""Maps EXIF GPS tags to tag names.""" +GPSTAGS = {i.value: i.name for i in GPS} + + +class Interop(IntEnum): + InteropIndex = 0x0001 + InteropVersion = 0x0002 + RelatedImageFileFormat = 0x1000 + RelatedImageWidth = 0x1001 + RelatedImageHeight = 0x1002 + + +class IFD(IntEnum): + Exif = 0x8769 + GPSInfo = 0x8825 + MakerNote = 0x927C + Makernote = 0x927C # Deprecated + Interop = 0xA005 + IFD1 = -1 + + +class LightSource(IntEnum): + Unknown = 0x00 + Daylight = 0x01 + Fluorescent = 0x02 + Tungsten = 0x03 + Flash = 0x04 + Fine = 0x09 + Cloudy = 0x0A + Shade = 0x0B + DaylightFluorescent = 0x0C + DayWhiteFluorescent = 0x0D + CoolWhiteFluorescent = 0x0E + WhiteFluorescent = 0x0F + StandardLightA = 0x11 + StandardLightB = 0x12 + StandardLightC = 0x13 + D55 = 0x14 + D65 = 0x15 + D75 = 0x16 + D50 = 0x17 + ISO = 0x18 + Other = 0xFF diff --git a/python_modules/PIL/FitsImagePlugin.py b/python_modules/PIL/FitsImagePlugin.py new file mode 100644 index 000000000..a3fdc0efe --- /dev/null +++ b/python_modules/PIL/FitsImagePlugin.py @@ -0,0 +1,152 @@ +# +# The Python Imaging Library +# $Id$ +# +# FITS file handling +# +# Copyright (c) 1998-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import gzip +import math + +from . import Image, ImageFile + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"SIMPLE") + + +class FitsImageFile(ImageFile.ImageFile): + format = "FITS" + format_description = "FITS" + + def _open(self) -> None: + assert self.fp is not None + + headers: dict[bytes, bytes] = {} + header_in_progress = False + decoder_name = "" + while True: + header = self.fp.read(80) + if not header: + msg = "Truncated FITS file" + raise OSError(msg) + keyword = header[:8].strip() + if keyword in (b"SIMPLE", b"XTENSION"): + header_in_progress = True + elif headers and not header_in_progress: + # This is now a data unit + break + elif keyword == b"END": + # Seek to the end of the header unit + self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880) + if not decoder_name: + decoder_name, offset, args = self._parse_headers(headers) + + header_in_progress = False + continue + + if decoder_name: + # Keep going to read past the headers + continue + + value = header[8:].split(b"/")[0].strip() + if value.startswith(b"="): + value = value[1:].strip() + if not headers and (not _accept(keyword) or value != b"T"): + msg = "Not a FITS file" + raise SyntaxError(msg) + headers[keyword] = value + + if not decoder_name: + msg = "No image data" + raise ValueError(msg) + + offset += self.fp.tell() - 80 + self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)] + + def _get_size( + self, headers: dict[bytes, bytes], prefix: bytes + ) -> tuple[int, int] | None: + naxis = int(headers[prefix + b"NAXIS"]) + if naxis == 0: + return None + + if naxis == 1: + return 1, int(headers[prefix + b"NAXIS1"]) + else: + return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"]) + + def _parse_headers( + self, headers: dict[bytes, bytes] + ) -> tuple[str, int, tuple[str | int, ...]]: + prefix = b"" + decoder_name = "raw" + offset = 0 + if ( + headers.get(b"XTENSION") == b"'BINTABLE'" + and headers.get(b"ZIMAGE") == b"T" + and headers[b"ZCMPTYPE"] == b"'GZIP_1 '" + ): + no_prefix_size = self._get_size(headers, prefix) or (0, 0) + number_of_bits = int(headers[b"BITPIX"]) + offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8) + + prefix = b"Z" + decoder_name = "fits_gzip" + + size = self._get_size(headers, prefix) + if not size: + return "", 0, () + + self._size = size + + number_of_bits = int(headers[prefix + b"BITPIX"]) + if number_of_bits == 8: + self._mode = "L" + elif number_of_bits == 16: + self._mode = "I;16" + elif number_of_bits == 32: + self._mode = "I" + elif number_of_bits in (-32, -64): + self._mode = "F" + + args: tuple[str | int, ...] + if decoder_name == "raw": + args = (self.mode, 0, -1) + else: + args = (number_of_bits,) + return decoder_name, offset, args + + +class FitsGzipDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + value = gzip.decompress(self.fd.read()) + + rows = [] + offset = 0 + number_of_bits = min(self.args[0] // 8, 4) + for y in range(self.state.ysize): + row = bytearray() + for x in range(self.state.xsize): + row += value[offset + (4 - number_of_bits) : offset + 4] + offset += 4 + rows.append(row) + self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row])) + return -1, 0 + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(FitsImageFile.format, FitsImageFile, _accept) +Image.register_decoder("fits_gzip", FitsGzipDecoder) + +Image.register_extensions(FitsImageFile.format, [".fit", ".fits"]) diff --git a/python_modules/PIL/FliImagePlugin.py b/python_modules/PIL/FliImagePlugin.py new file mode 100644 index 000000000..7c5bfeefa --- /dev/null +++ b/python_modules/PIL/FliImagePlugin.py @@ -0,0 +1,178 @@ +# +# The Python Imaging Library. +# $Id$ +# +# FLI/FLC file handling. +# +# History: +# 95-09-01 fl Created +# 97-01-03 fl Fixed parser, setup decoder tile +# 98-07-15 fl Renamed offset attribute to avoid name clash +# +# Copyright (c) Secret Labs AB 1997-98. +# Copyright (c) Fredrik Lundh 1995-97. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os + +from . import Image, ImageFile, ImagePalette +from ._binary import i16le as i16 +from ._binary import i32le as i32 +from ._binary import o8 +from ._util import DeferredError + +# +# decoder + + +def _accept(prefix: bytes) -> bool: + return ( + len(prefix) >= 6 + and i16(prefix, 4) in [0xAF11, 0xAF12] + and i16(prefix, 14) in [0, 3] # flags + ) + + +## +# Image plugin for the FLI/FLC animation format. Use the seek +# method to load individual frames. + + +class FliImageFile(ImageFile.ImageFile): + format = "FLI" + format_description = "Autodesk FLI/FLC Animation" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + # HEAD + s = self.fp.read(128) + if not (_accept(s) and s[20:22] == b"\x00\x00"): + msg = "not an FLI/FLC file" + raise SyntaxError(msg) + + # frames + self.n_frames = i16(s, 6) + self.is_animated = self.n_frames > 1 + + # image characteristics + self._mode = "P" + self._size = i16(s, 8), i16(s, 10) + + # animation speed + duration = i32(s, 16) + magic = i16(s, 4) + if magic == 0xAF11: + duration = (duration * 1000) // 70 + self.info["duration"] = duration + + # look for palette + palette = [(a, a, a) for a in range(256)] + + s = self.fp.read(16) + + self.__offset = 128 + + if i16(s, 4) == 0xF100: + # prefix chunk; ignore it + self.__offset = self.__offset + i32(s) + self.fp.seek(self.__offset) + s = self.fp.read(16) + + if i16(s, 4) == 0xF1FA: + # look for palette chunk + number_of_subchunks = i16(s, 6) + chunk_size: int | None = None + for _ in range(number_of_subchunks): + if chunk_size is not None: + self.fp.seek(chunk_size - 6, os.SEEK_CUR) + s = self.fp.read(6) + chunk_type = i16(s, 4) + if chunk_type in (4, 11): + self._palette(palette, 2 if chunk_type == 11 else 0) + break + chunk_size = i32(s) + if not chunk_size: + break + + self.palette = ImagePalette.raw( + "RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette) + ) + + # set things up to decode first frame + self.__frame = -1 + self._fp = self.fp + self.__rewind = self.fp.tell() + self.seek(0) + + def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None: + # load palette + + i = 0 + for e in range(i16(self.fp.read(2))): + s = self.fp.read(2) + i = i + s[0] + n = s[1] + if n == 0: + n = 256 + s = self.fp.read(n * 3) + for n in range(0, len(s), 3): + r = s[n] << shift + g = s[n + 1] << shift + b = s[n + 2] << shift + palette[i] = (r, g, b) + i += 1 + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + if frame < self.__frame: + self._seek(0) + + for f in range(self.__frame + 1, frame + 1): + self._seek(f) + + def _seek(self, frame: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex + if frame == 0: + self.__frame = -1 + self._fp.seek(self.__rewind) + self.__offset = 128 + else: + # ensure that the previous frame was loaded + self.load() + + if frame != self.__frame + 1: + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) + self.__frame = frame + + # move to next frame + self.fp = self._fp + self.fp.seek(self.__offset) + + s = self.fp.read(4) + if not s: + msg = "missing frame size" + raise EOFError(msg) + + framesize = i32(s) + + self.decodermaxblock = framesize + self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)] + + self.__offset += framesize + + def tell(self) -> int: + return self.__frame + + +# +# registry + +Image.register_open(FliImageFile.format, FliImageFile, _accept) + +Image.register_extensions(FliImageFile.format, [".fli", ".flc"]) diff --git a/python_modules/PIL/FontFile.py b/python_modules/PIL/FontFile.py new file mode 100644 index 000000000..1e0c1c166 --- /dev/null +++ b/python_modules/PIL/FontFile.py @@ -0,0 +1,134 @@ +# +# The Python Imaging Library +# $Id$ +# +# base class for raster font file parsers +# +# history: +# 1997-06-05 fl created +# 1997-08-19 fl restrict image width +# +# Copyright (c) 1997-1998 by Secret Labs AB +# Copyright (c) 1997-1998 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +from typing import BinaryIO + +from . import Image, _binary + +WIDTH = 800 + + +def puti16( + fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int] +) -> None: + """Write network order (big-endian) 16-bit sequence""" + for v in values: + if v < 0: + v += 65536 + fp.write(_binary.o16be(v)) + + +class FontFile: + """Base class for raster font file handlers.""" + + bitmap: Image.Image | None = None + + def __init__(self) -> None: + self.info: dict[bytes, bytes | int] = {} + self.glyph: list[ + tuple[ + tuple[int, int], + tuple[int, int, int, int], + tuple[int, int, int, int], + Image.Image, + ] + | None + ] = [None] * 256 + + def __getitem__(self, ix: int) -> ( + tuple[ + tuple[int, int], + tuple[int, int, int, int], + tuple[int, int, int, int], + Image.Image, + ] + | None + ): + return self.glyph[ix] + + def compile(self) -> None: + """Create metrics and bitmap""" + + if self.bitmap: + return + + # create bitmap large enough to hold all data + h = w = maxwidth = 0 + lines = 1 + for glyph in self.glyph: + if glyph: + d, dst, src, im = glyph + h = max(h, src[3] - src[1]) + w = w + (src[2] - src[0]) + if w > WIDTH: + lines += 1 + w = src[2] - src[0] + maxwidth = max(maxwidth, w) + + xsize = maxwidth + ysize = lines * h + + if xsize == 0 and ysize == 0: + return + + self.ysize = h + + # paste glyphs into bitmap + self.bitmap = Image.new("1", (xsize, ysize)) + self.metrics: list[ + tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]] + | None + ] = [None] * 256 + x = y = 0 + for i in range(256): + glyph = self[i] + if glyph: + d, dst, src, im = glyph + xx = src[2] - src[0] + x0, y0 = x, y + x = x + xx + if x > WIDTH: + x, y = 0, y + h + x0, y0 = x, y + x = xx + s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0 + self.bitmap.paste(im.crop(src), s) + self.metrics[i] = d, dst, s + + def save(self, filename: str) -> None: + """Save font""" + + self.compile() + + # font data + if not self.bitmap: + msg = "No bitmap created" + raise ValueError(msg) + self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG") + + # font metrics + with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: + fp.write(b"PILfont\n") + fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! + fp.write(b"DATA\n") + for id in range(256): + m = self.metrics[id] + if not m: + puti16(fp, (0,) * 10) + else: + puti16(fp, m[0] + m[1] + m[2]) diff --git a/python_modules/PIL/FpxImagePlugin.py b/python_modules/PIL/FpxImagePlugin.py new file mode 100644 index 000000000..fd992cd9e --- /dev/null +++ b/python_modules/PIL/FpxImagePlugin.py @@ -0,0 +1,257 @@ +# +# THIS IS WORK IN PROGRESS +# +# The Python Imaging Library. +# $Id$ +# +# FlashPix support for PIL +# +# History: +# 97-01-25 fl Created (reads uncompressed RGB images only) +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import olefile + +from . import Image, ImageFile +from ._binary import i32le as i32 + +# we map from colour field tuples to (mode, rawmode) descriptors +MODES = { + # opacity + (0x00007FFE,): ("A", "L"), + # monochrome + (0x00010000,): ("L", "L"), + (0x00018000, 0x00017FFE): ("RGBA", "LA"), + # photo YCC + (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"), + (0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"), + # standard RGB (NIFRGB) + (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"), + (0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"), +} + + +# +# -------------------------------------------------------------------- + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(olefile.MAGIC) + + +## +# Image plugin for the FlashPix images. + + +class FpxImageFile(ImageFile.ImageFile): + format = "FPX" + format_description = "FlashPix" + + def _open(self) -> None: + # + # read the OLE directory and see if this is a likely + # to be a FlashPix file + + try: + self.ole = olefile.OleFileIO(self.fp) + except OSError as e: + msg = "not an FPX file; invalid OLE file" + raise SyntaxError(msg) from e + + root = self.ole.root + if not root or root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": + msg = "not an FPX file; bad root CLSID" + raise SyntaxError(msg) + + self._open_index(1) + + def _open_index(self, index: int = 1) -> None: + # + # get the Image Contents Property Set + + prop = self.ole.getproperties( + [f"Data Object Store {index:06d}", "\005Image Contents"] + ) + + # size (highest resolution) + + assert isinstance(prop[0x1000002], int) + assert isinstance(prop[0x1000003], int) + self._size = prop[0x1000002], prop[0x1000003] + + size = max(self.size) + i = 1 + while size > 64: + size = size // 2 + i += 1 + self.maxid = i - 1 + + # mode. instead of using a single field for this, flashpix + # requires you to specify the mode for each channel in each + # resolution subimage, and leaves it to the decoder to make + # sure that they all match. for now, we'll cheat and assume + # that this is always the case. + + id = self.maxid << 16 + + s = prop[0x2000002 | id] + + if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4: + msg = "Invalid number of bands" + raise OSError(msg) + + # note: for now, we ignore the "uncalibrated" flag + colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands)) + + self._mode, self.rawmode = MODES[colors] + + # load JPEG tables, if any + self.jpeg = {} + for i in range(256): + id = 0x3000001 | (i << 16) + if id in prop: + self.jpeg[i] = prop[id] + + self._open_subimage(1, self.maxid) + + def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: + # + # setup tile descriptors for a given subimage + + stream = [ + f"Data Object Store {index:06d}", + f"Resolution {subimage:04d}", + "Subimage 0000 Header", + ] + + fp = self.ole.openstream(stream) + + # skip prefix + fp.read(28) + + # header stream + s = fp.read(36) + + size = i32(s, 4), i32(s, 8) + # tilecount = i32(s, 12) + tilesize = i32(s, 16), i32(s, 20) + # channels = i32(s, 24) + offset = i32(s, 28) + length = i32(s, 32) + + if size != self.size: + msg = "subimage mismatch" + raise OSError(msg) + + # get tile descriptors + fp.seek(28 + offset) + s = fp.read(i32(s, 12) * length) + + x = y = 0 + xsize, ysize = size + xtile, ytile = tilesize + self.tile = [] + + for i in range(0, len(s), length): + x1 = min(xsize, x + xtile) + y1 = min(ysize, y + ytile) + + compression = i32(s, i + 8) + + if compression == 0: + self.tile.append( + ImageFile._Tile( + "raw", + (x, y, x1, y1), + i32(s, i) + 28, + self.rawmode, + ) + ) + + elif compression == 1: + # FIXME: the fill decoder is not implemented + self.tile.append( + ImageFile._Tile( + "fill", + (x, y, x1, y1), + i32(s, i) + 28, + (self.rawmode, s[12:16]), + ) + ) + + elif compression == 2: + internal_color_conversion = s[14] + jpeg_tables = s[15] + rawmode = self.rawmode + + if internal_color_conversion: + # The image is stored as usual (usually YCbCr). + if rawmode == "RGBA": + # For "RGBA", data is stored as YCbCrA based on + # negative RGB. The following trick works around + # this problem : + jpegmode, rawmode = "YCbCrK", "CMYK" + else: + jpegmode = None # let the decoder decide + + else: + # The image is stored as defined by rawmode + jpegmode = rawmode + + self.tile.append( + ImageFile._Tile( + "jpeg", + (x, y, x1, y1), + i32(s, i) + 28, + (rawmode, jpegmode), + ) + ) + + # FIXME: jpeg tables are tile dependent; the prefix + # data must be placed in the tile descriptor itself! + + if jpeg_tables: + self.tile_prefix = self.jpeg[jpeg_tables] + + else: + msg = "unknown/invalid compression" + raise OSError(msg) + + x = x + xtile + if x >= xsize: + x, y = 0, y + ytile + if y >= ysize: + break # isn't really required + + self.stream = stream + self._fp = self.fp + self.fp = None + + def load(self) -> Image.core.PixelAccess | None: + if not self.fp: + self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) + + return ImageFile.ImageFile.load(self) + + def close(self) -> None: + self.ole.close() + super().close() + + def __exit__(self, *args: object) -> None: + self.ole.close() + super().__exit__() + + +# +# -------------------------------------------------------------------- + + +Image.register_open(FpxImageFile.format, FpxImageFile, _accept) + +Image.register_extension(FpxImageFile.format, ".fpx") diff --git a/python_modules/PIL/FtexImagePlugin.py b/python_modules/PIL/FtexImagePlugin.py new file mode 100644 index 000000000..d60e75bb6 --- /dev/null +++ b/python_modules/PIL/FtexImagePlugin.py @@ -0,0 +1,114 @@ +""" +A Pillow loader for .ftc and .ftu files (FTEX) +Jerome Leclanche + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ + +Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001 + +The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a +packed custom format called FTEX. This file format uses file extensions FTC +and FTU. +* FTC files are compressed textures (using standard texture compression). +* FTU files are not compressed. +Texture File Format +The FTC and FTU texture files both use the same format. This +has the following structure: +{header} +{format_directory} +{data} +Where: +{header} = { + u32:magic, + u32:version, + u32:width, + u32:height, + u32:mipmap_count, + u32:format_count +} + +* The "magic" number is "FTEX". +* "width" and "height" are the dimensions of the texture. +* "mipmap_count" is the number of mipmaps in the texture. +* "format_count" is the number of texture formats (different versions of the +same texture) in this file. + +{format_directory} = format_count * { u32:format, u32:where } + +The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB +uncompressed textures. +The texture data for a format starts at the position "where" in the file. + +Each set of texture data in the file has the following structure: +{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } } +* "mipmap_size" is the number of bytes in that mip level. For compressed +textures this is the size of the texture data compressed with DXT1. For 24 bit +uncompressed textures, this is 3 * width * height. Following this are the image +bytes for that mipmap level. + +Note: All data is stored in little-Endian (Intel) byte order. +""" + +from __future__ import annotations + +import struct +from enum import IntEnum +from io import BytesIO + +from . import Image, ImageFile + +MAGIC = b"FTEX" + + +class Format(IntEnum): + DXT1 = 0 + UNCOMPRESSED = 1 + + +class FtexImageFile(ImageFile.ImageFile): + format = "FTEX" + format_description = "Texture File Format (IW2:EOC)" + + def _open(self) -> None: + if not _accept(self.fp.read(4)): + msg = "not an FTEX file" + raise SyntaxError(msg) + struct.unpack(" None: + pass + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(MAGIC) + + +Image.register_open(FtexImageFile.format, FtexImageFile, _accept) +Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"]) diff --git a/python_modules/PIL/GbrImagePlugin.py b/python_modules/PIL/GbrImagePlugin.py new file mode 100644 index 000000000..f319d7e84 --- /dev/null +++ b/python_modules/PIL/GbrImagePlugin.py @@ -0,0 +1,103 @@ +# +# The Python Imaging Library +# +# load a GIMP brush file +# +# History: +# 96-03-14 fl Created +# 16-01-08 es Version 2 +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# Copyright (c) Eric Soroos 2016. +# +# See the README file for information on usage and redistribution. +# +# +# See https://github.com/GNOME/gimp/blob/mainline/devel-docs/gbr.txt for +# format documentation. +# +# This code Interprets version 1 and 2 .gbr files. +# Version 1 files are obsolete, and should not be used for new +# brushes. +# Version 2 files are saved by GIMP v2.8 (at least) +# Version 3 files have a format specifier of 18 for 16bit floats in +# the color depth field. This is currently unsupported by Pillow. +from __future__ import annotations + +from . import Image, ImageFile +from ._binary import i32be as i32 + + +def _accept(prefix: bytes) -> bool: + return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2) + + +## +# Image plugin for the GIMP brush format. + + +class GbrImageFile(ImageFile.ImageFile): + format = "GBR" + format_description = "GIMP brush file" + + def _open(self) -> None: + header_size = i32(self.fp.read(4)) + if header_size < 20: + msg = "not a GIMP brush" + raise SyntaxError(msg) + version = i32(self.fp.read(4)) + if version not in (1, 2): + msg = f"Unsupported GIMP brush version: {version}" + raise SyntaxError(msg) + + width = i32(self.fp.read(4)) + height = i32(self.fp.read(4)) + color_depth = i32(self.fp.read(4)) + if width <= 0 or height <= 0: + msg = "not a GIMP brush" + raise SyntaxError(msg) + if color_depth not in (1, 4): + msg = f"Unsupported GIMP brush color depth: {color_depth}" + raise SyntaxError(msg) + + if version == 1: + comment_length = header_size - 20 + else: + comment_length = header_size - 28 + magic_number = self.fp.read(4) + if magic_number != b"GIMP": + msg = "not a GIMP brush, bad magic number" + raise SyntaxError(msg) + self.info["spacing"] = i32(self.fp.read(4)) + + comment = self.fp.read(comment_length)[:-1] + + if color_depth == 1: + self._mode = "L" + else: + self._mode = "RGBA" + + self._size = width, height + + self.info["comment"] = comment + + # Image might not be small + Image._decompression_bomb_check(self.size) + + # Data is an uncompressed block of w * h * bytes/pixel + self._data_size = width * height * color_depth + + def load(self) -> Image.core.PixelAccess | None: + if self._im is None: + self.im = Image.core.new(self.mode, self.size) + self.frombytes(self.fp.read(self._data_size)) + return Image.Image.load(self) + + +# +# registry + + +Image.register_open(GbrImageFile.format, GbrImageFile, _accept) +Image.register_extension(GbrImageFile.format, ".gbr") diff --git a/python_modules/PIL/GdImageFile.py b/python_modules/PIL/GdImageFile.py new file mode 100644 index 000000000..891225ce2 --- /dev/null +++ b/python_modules/PIL/GdImageFile.py @@ -0,0 +1,102 @@ +# +# The Python Imaging Library. +# $Id$ +# +# GD file handling +# +# History: +# 1996-04-12 fl Created +# +# Copyright (c) 1997 by Secret Labs AB. +# Copyright (c) 1996 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + + +""" +.. note:: + This format cannot be automatically recognized, so the + class is not registered for use with :py:func:`PIL.Image.open()`. To open a + gd file, use the :py:func:`PIL.GdImageFile.open()` function instead. + +.. warning:: + THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This + implementation is provided for convenience and demonstrational + purposes only. +""" +from __future__ import annotations + +from typing import IO + +from . import ImageFile, ImagePalette, UnidentifiedImageError +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._typing import StrOrBytesPath + + +class GdImageFile(ImageFile.ImageFile): + """ + Image plugin for the GD uncompressed format. Note that this format + is not supported by the standard :py:func:`PIL.Image.open()` function. To use + this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and + use the :py:func:`PIL.GdImageFile.open()` function. + """ + + format = "GD" + format_description = "GD uncompressed images" + + def _open(self) -> None: + # Header + assert self.fp is not None + + s = self.fp.read(1037) + + if i16(s) not in [65534, 65535]: + msg = "Not a valid GD 2.x .gd file" + raise SyntaxError(msg) + + self._mode = "P" + self._size = i16(s, 2), i16(s, 4) + + true_color = s[6] + true_color_offset = 2 if true_color else 0 + + # transparency index + tindex = i32(s, 7 + true_color_offset) + if tindex < 256: + self.info["transparency"] = tindex + + self.palette = ImagePalette.raw( + "RGBX", s[7 + true_color_offset + 6 : 7 + true_color_offset + 6 + 256 * 4] + ) + + self.tile = [ + ImageFile._Tile( + "raw", + (0, 0) + self.size, + 7 + true_color_offset + 6 + 256 * 4, + "L", + ) + ] + + +def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile: + """ + Load texture from a GD image file. + + :param fp: GD file name, or an opened file handle. + :param mode: Optional mode. In this version, if the mode argument + is given, it must be "r". + :returns: An image instance. + :raises OSError: If the image could not be read. + """ + if mode != "r": + msg = "bad mode" + raise ValueError(msg) + + try: + return GdImageFile(fp) + except SyntaxError as e: + msg = "cannot identify this image file" + raise UnidentifiedImageError(msg) from e diff --git a/python_modules/PIL/GifImagePlugin.py b/python_modules/PIL/GifImagePlugin.py new file mode 100644 index 000000000..b03aa7f15 --- /dev/null +++ b/python_modules/PIL/GifImagePlugin.py @@ -0,0 +1,1213 @@ +# +# The Python Imaging Library. +# $Id$ +# +# GIF file handling +# +# History: +# 1995-09-01 fl Created +# 1996-12-14 fl Added interlace support +# 1996-12-30 fl Added animation support +# 1997-01-05 fl Added write support, fixed local colour map bug +# 1997-02-23 fl Make sure to load raster data in getdata() +# 1997-07-05 fl Support external decoder (0.4) +# 1998-07-09 fl Handle all modes when saving (0.5) +# 1998-07-15 fl Renamed offset attribute to avoid name clash +# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6) +# 2001-04-17 fl Added palette optimization (0.7) +# 2002-06-06 fl Added transparency support for save (0.8) +# 2004-02-24 fl Disable interlacing for small images +# +# Copyright (c) 1997-2004 by Secret Labs AB +# Copyright (c) 1995-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import itertools +import math +import os +import subprocess +from enum import IntEnum +from functools import cached_property +from typing import IO, Any, Literal, NamedTuple, Union, cast + +from . import ( + Image, + ImageChops, + ImageFile, + ImageMath, + ImageOps, + ImagePalette, + ImageSequence, +) +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 +from ._util import DeferredError + +TYPE_CHECKING = False +if TYPE_CHECKING: + from . import _imaging + from ._typing import Buffer + + +class LoadingStrategy(IntEnum): + """.. versionadded:: 9.1.0""" + + RGB_AFTER_FIRST = 0 + RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 + RGB_ALWAYS = 2 + + +#: .. versionadded:: 9.1.0 +LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST + +# -------------------------------------------------------------------- +# Identify/read GIF files + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith((b"GIF87a", b"GIF89a")) + + +## +# Image plugin for GIF images. This plugin supports both GIF87 and +# GIF89 images. + + +class GifImageFile(ImageFile.ImageFile): + format = "GIF" + format_description = "Compuserve GIF" + _close_exclusive_fp_after_loading = False + + global_palette = None + + def data(self) -> bytes | None: + s = self.fp.read(1) + if s and s[0]: + return self.fp.read(s[0]) + return None + + def _is_palette_needed(self, p: bytes) -> bool: + for i in range(0, len(p), 3): + if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): + return True + return False + + def _open(self) -> None: + # Screen + s = self.fp.read(13) + if not _accept(s): + msg = "not a GIF file" + raise SyntaxError(msg) + + self.info["version"] = s[:6] + self._size = i16(s, 6), i16(s, 8) + flags = s[10] + bits = (flags & 7) + 1 + + if flags & 128: + # get global palette + self.info["background"] = s[11] + # check if palette contains colour indices + p = self.fp.read(3 << bits) + if self._is_palette_needed(p): + p = ImagePalette.raw("RGB", p) + self.global_palette = self.palette = p + + self._fp = self.fp # FIXME: hack + self.__rewind = self.fp.tell() + self._n_frames: int | None = None + self._seek(0) # get ready to read first frame + + @property + def n_frames(self) -> int: + if self._n_frames is None: + current = self.tell() + try: + while True: + self._seek(self.tell() + 1, False) + except EOFError: + self._n_frames = self.tell() + 1 + self.seek(current) + return self._n_frames + + @cached_property + def is_animated(self) -> bool: + if self._n_frames is not None: + return self._n_frames != 1 + + current = self.tell() + if current: + return True + + try: + self._seek(1, False) + is_animated = True + except EOFError: + is_animated = False + + self.seek(current) + return is_animated + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + if frame < self.__frame: + self._im = None + self._seek(0) + + last_frame = self.__frame + for f in range(self.__frame + 1, frame + 1): + try: + self._seek(f) + except EOFError as e: + self.seek(last_frame) + msg = "no more images in GIF file" + raise EOFError(msg) from e + + def _seek(self, frame: int, update_image: bool = True) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex + if frame == 0: + # rewind + self.__offset = 0 + self.dispose: _imaging.ImagingCore | None = None + self.__frame = -1 + self._fp.seek(self.__rewind) + self.disposal_method = 0 + if "comment" in self.info: + del self.info["comment"] + else: + # ensure that the previous frame was loaded + if self.tile and update_image: + self.load() + + if frame != self.__frame + 1: + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) + + self.fp = self._fp + if self.__offset: + # backup to last frame + self.fp.seek(self.__offset) + while self.data(): + pass + self.__offset = 0 + + s = self.fp.read(1) + if not s or s == b";": + msg = "no more images in GIF file" + raise EOFError(msg) + + palette: ImagePalette.ImagePalette | Literal[False] | None = None + + info: dict[str, Any] = {} + frame_transparency = None + interlace = None + frame_dispose_extent = None + while True: + if not s: + s = self.fp.read(1) + if not s or s == b";": + break + + elif s == b"!": + # + # extensions + # + s = self.fp.read(1) + block = self.data() + if s[0] == 249 and block is not None: + # + # graphic control extension + # + flags = block[0] + if flags & 1: + frame_transparency = block[3] + info["duration"] = i16(block, 1) * 10 + + # disposal method - find the value of bits 4 - 6 + dispose_bits = 0b00011100 & flags + dispose_bits = dispose_bits >> 2 + if dispose_bits: + # only set the dispose if it is not + # unspecified. I'm not sure if this is + # correct, but it seems to prevent the last + # frame from looking odd for some animations + self.disposal_method = dispose_bits + elif s[0] == 254: + # + # comment extension + # + comment = b"" + + # Read this comment block + while block: + comment += block + block = self.data() + + if "comment" in info: + # If multiple comment blocks in frame, separate with \n + info["comment"] += b"\n" + comment + else: + info["comment"] = comment + s = None + continue + elif s[0] == 255 and frame == 0 and block is not None: + # + # application extension + # + info["extension"] = block, self.fp.tell() + if block.startswith(b"NETSCAPE2.0"): + block = self.data() + if block and len(block) >= 3 and block[0] == 1: + self.info["loop"] = i16(block, 1) + while self.data(): + pass + + elif s == b",": + # + # local image + # + s = self.fp.read(9) + + # extent + x0, y0 = i16(s, 0), i16(s, 2) + x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6) + if (x1 > self.size[0] or y1 > self.size[1]) and update_image: + self._size = max(x1, self.size[0]), max(y1, self.size[1]) + Image._decompression_bomb_check(self._size) + frame_dispose_extent = x0, y0, x1, y1 + flags = s[8] + + interlace = (flags & 64) != 0 + + if flags & 128: + bits = (flags & 7) + 1 + p = self.fp.read(3 << bits) + if self._is_palette_needed(p): + palette = ImagePalette.raw("RGB", p) + else: + palette = False + + # image data + bits = self.fp.read(1)[0] + self.__offset = self.fp.tell() + break + s = None + + if interlace is None: + msg = "image not found in GIF frame" + raise EOFError(msg) + + self.__frame = frame + if not update_image: + return + + self.tile = [] + + if self.dispose: + self.im.paste(self.dispose, self.dispose_extent) + + self._frame_palette = palette if palette is not None else self.global_palette + self._frame_transparency = frame_transparency + if frame == 0: + if self._frame_palette: + if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: + self._mode = "RGBA" if frame_transparency is not None else "RGB" + else: + self._mode = "P" + else: + self._mode = "L" + + if palette: + self.palette = palette + elif self.global_palette: + from copy import copy + + self.palette = copy(self.global_palette) + else: + self.palette = None + else: + if self.mode == "P": + if ( + LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY + or palette + ): + if "transparency" in self.info: + self.im.putpalettealpha(self.info["transparency"], 0) + self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) + self._mode = "RGBA" + del self.info["transparency"] + else: + self._mode = "RGB" + self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) + + def _rgb(color: int) -> tuple[int, int, int]: + if self._frame_palette: + if color * 3 + 3 > len(self._frame_palette.palette): + color = 0 + return cast( + tuple[int, int, int], + tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]), + ) + else: + return (color, color, color) + + self.dispose = None + self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent + if self.dispose_extent and self.disposal_method >= 2: + try: + if self.disposal_method == 2: + # replace with background colour + + # only dispose the extent in this frame + x0, y0, x1, y1 = self.dispose_extent + dispose_size = (x1 - x0, y1 - y0) + + Image._decompression_bomb_check(dispose_size) + + # by convention, attempt to use transparency first + dispose_mode = "P" + color = self.info.get("transparency", frame_transparency) + if color is not None: + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGBA" + color = _rgb(color) + (0,) + else: + color = self.info.get("background", 0) + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGB" + color = _rgb(color) + self.dispose = Image.core.fill(dispose_mode, dispose_size, color) + else: + # replace with previous contents + if self._im is not None: + # only dispose the extent in this frame + self.dispose = self._crop(self.im, self.dispose_extent) + elif frame_transparency is not None: + x0, y0, x1, y1 = self.dispose_extent + dispose_size = (x1 - x0, y1 - y0) + + Image._decompression_bomb_check(dispose_size) + dispose_mode = "P" + color = frame_transparency + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGBA" + color = _rgb(frame_transparency) + (0,) + self.dispose = Image.core.fill( + dispose_mode, dispose_size, color + ) + except AttributeError: + pass + + if interlace is not None: + transparency = -1 + if frame_transparency is not None: + if frame == 0: + if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: + self.info["transparency"] = frame_transparency + elif self.mode not in ("RGB", "RGBA"): + transparency = frame_transparency + self.tile = [ + ImageFile._Tile( + "gif", + (x0, y0, x1, y1), + self.__offset, + (bits, interlace, transparency), + ) + ] + + if info.get("comment"): + self.info["comment"] = info["comment"] + for k in ["duration", "extension"]: + if k in info: + self.info[k] = info[k] + elif k in self.info: + del self.info[k] + + def load_prepare(self) -> None: + temp_mode = "P" if self._frame_palette else "L" + self._prev_im = None + if self.__frame == 0: + if self._frame_transparency is not None: + self.im = Image.core.fill( + temp_mode, self.size, self._frame_transparency + ) + elif self.mode in ("RGB", "RGBA"): + self._prev_im = self.im + if self._frame_palette: + self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) + self.im.putpalette("RGB", *self._frame_palette.getdata()) + else: + self._im = None + if not self._prev_im and self._im is not None and self.size != self.im.size: + expanded_im = Image.core.fill(self.im.mode, self.size) + if self._frame_palette: + expanded_im.putpalette("RGB", *self._frame_palette.getdata()) + expanded_im.paste(self.im, (0, 0) + self.im.size) + + self.im = expanded_im + self._mode = temp_mode + self._frame_palette = None + + super().load_prepare() + + def load_end(self) -> None: + if self.__frame == 0: + if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: + if self._frame_transparency is not None: + self.im.putpalettealpha(self._frame_transparency, 0) + self._mode = "RGBA" + else: + self._mode = "RGB" + self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) + return + if not self._prev_im: + return + if self.size != self._prev_im.size: + if self._frame_transparency is not None: + expanded_im = Image.core.fill("RGBA", self.size) + else: + expanded_im = Image.core.fill("P", self.size) + expanded_im.putpalette("RGB", "RGB", self.im.getpalette()) + expanded_im = expanded_im.convert("RGB") + expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size) + + self._prev_im = expanded_im + assert self._prev_im is not None + if self._frame_transparency is not None: + if self.mode == "L": + frame_im = self.im.convert_transparent("LA", self._frame_transparency) + else: + self.im.putpalettealpha(self._frame_transparency, 0) + frame_im = self.im.convert("RGBA") + else: + frame_im = self.im.convert("RGB") + + assert self.dispose_extent is not None + frame_im = self._crop(frame_im, self.dispose_extent) + + self.im = self._prev_im + self._mode = self.im.mode + if frame_im.mode in ("LA", "RGBA"): + self.im.paste(frame_im, self.dispose_extent, frame_im) + else: + self.im.paste(frame_im, self.dispose_extent) + + def tell(self) -> int: + return self.__frame + + +# -------------------------------------------------------------------- +# Write GIF files + + +RAWMODE = {"1": "L", "L": "L", "P": "P"} + + +def _normalize_mode(im: Image.Image) -> Image.Image: + """ + Takes an image (or frame), returns an image in a mode that is appropriate + for saving in a Gif. + + It may return the original image, or it may return an image converted to + palette or 'L' mode. + + :param im: Image object + :returns: Image object + """ + if im.mode in RAWMODE: + im.load() + return im + if Image.getmodebase(im.mode) == "RGB": + im = im.convert("P", palette=Image.Palette.ADAPTIVE) + assert im.palette is not None + if im.palette.mode == "RGBA": + for rgba in im.palette.colors: + if rgba[3] == 0: + im.info["transparency"] = im.palette.colors[rgba] + break + return im + return im.convert("L") + + +_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette] + + +def _normalize_palette( + im: Image.Image, palette: _Palette | None, info: dict[str, Any] +) -> Image.Image: + """ + Normalizes the palette for image. + - Sets the palette to the incoming palette, if provided. + - Ensures that there's a palette for L mode images + - Optimizes the palette if necessary/desired. + + :param im: Image object + :param palette: bytes object containing the source palette, or .... + :param info: encoderinfo + :returns: Image object + """ + source_palette = None + if palette: + # a bytes palette + if isinstance(palette, (bytes, bytearray, list)): + source_palette = bytearray(palette[:768]) + if isinstance(palette, ImagePalette.ImagePalette): + source_palette = bytearray(palette.palette) + + if im.mode == "P": + if not source_palette: + im_palette = im.getpalette(None) + assert im_palette is not None + source_palette = bytearray(im_palette) + else: # L-mode + if not source_palette: + source_palette = bytearray(i // 3 for i in range(768)) + im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) + assert source_palette is not None + + if palette: + used_palette_colors: list[int | None] = [] + assert im.palette is not None + for i in range(0, len(source_palette), 3): + source_color = tuple(source_palette[i : i + 3]) + index = im.palette.colors.get(source_color) + if index in used_palette_colors: + index = None + used_palette_colors.append(index) + for i, index in enumerate(used_palette_colors): + if index is None: + for j in range(len(used_palette_colors)): + if j not in used_palette_colors: + used_palette_colors[i] = j + break + dest_map: list[int] = [] + for index in used_palette_colors: + assert index is not None + dest_map.append(index) + im = im.remap_palette(dest_map) + else: + optimized_palette_colors = _get_optimize(im, info) + if optimized_palette_colors is not None: + im = im.remap_palette(optimized_palette_colors, source_palette) + if "transparency" in info: + try: + info["transparency"] = optimized_palette_colors.index( + info["transparency"] + ) + except ValueError: + del info["transparency"] + return im + + assert im.palette is not None + im.palette.palette = source_palette + return im + + +def _write_single_frame( + im: Image.Image, + fp: IO[bytes], + palette: _Palette | None, +) -> None: + im_out = _normalize_mode(im) + for k, v in im_out.info.items(): + if isinstance(k, str): + im.encoderinfo.setdefault(k, v) + im_out = _normalize_palette(im_out, palette, im.encoderinfo) + + for s in _get_global_header(im_out, im.encoderinfo): + fp.write(s) + + # local image header + flags = 0 + if get_interlace(im): + flags = flags | 64 + _write_local_header(fp, im, (0, 0), flags) + + im_out.encoderconfig = (8, get_interlace(im)) + ImageFile._save( + im_out, fp, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])] + ) + + fp.write(b"\0") # end of image data + + +def _getbbox( + base_im: Image.Image, im_frame: Image.Image +) -> tuple[Image.Image, tuple[int, int, int, int] | None]: + palette_bytes = [ + bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame) + ] + if palette_bytes[0] != palette_bytes[1]: + im_frame = im_frame.convert("RGBA") + base_im = base_im.convert("RGBA") + delta = ImageChops.subtract_modulo(im_frame, base_im) + return delta, delta.getbbox(alpha_only=False) + + +class _Frame(NamedTuple): + im: Image.Image + bbox: tuple[int, int, int, int] | None + encoderinfo: dict[str, Any] + + +def _write_multiple_frames( + im: Image.Image, fp: IO[bytes], palette: _Palette | None +) -> bool: + duration = im.encoderinfo.get("duration") + disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) + + im_frames: list[_Frame] = [] + previous_im: Image.Image | None = None + frame_count = 0 + background_im = None + for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): + for im_frame in ImageSequence.Iterator(imSequence): + # a copy is required here since seek can still mutate the image + im_frame = _normalize_mode(im_frame.copy()) + if frame_count == 0: + for k, v in im_frame.info.items(): + if k == "transparency": + continue + if isinstance(k, str): + im.encoderinfo.setdefault(k, v) + + encoderinfo = im.encoderinfo.copy() + if "transparency" in im_frame.info: + encoderinfo.setdefault("transparency", im_frame.info["transparency"]) + im_frame = _normalize_palette(im_frame, palette, encoderinfo) + if isinstance(duration, (list, tuple)): + encoderinfo["duration"] = duration[frame_count] + elif duration is None and "duration" in im_frame.info: + encoderinfo["duration"] = im_frame.info["duration"] + if isinstance(disposal, (list, tuple)): + encoderinfo["disposal"] = disposal[frame_count] + frame_count += 1 + + diff_frame = None + if im_frames and previous_im: + # delta frame + delta, bbox = _getbbox(previous_im, im_frame) + if not bbox: + # This frame is identical to the previous frame + if encoderinfo.get("duration"): + im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] + continue + if im_frames[-1].encoderinfo.get("disposal") == 2: + # To appear correctly in viewers using a convention, + # only consider transparency, and not background color + color = im.encoderinfo.get( + "transparency", im.info.get("transparency") + ) + if color is not None: + if background_im is None: + background = _get_background(im_frame, color) + background_im = Image.new("P", im_frame.size, background) + first_palette = im_frames[0].im.palette + assert first_palette is not None + background_im.putpalette(first_palette, first_palette.mode) + bbox = _getbbox(background_im, im_frame)[1] + else: + bbox = (0, 0) + im_frame.size + elif encoderinfo.get("optimize") and im_frame.mode != "1": + if "transparency" not in encoderinfo: + assert im_frame.palette is not None + try: + encoderinfo["transparency"] = ( + im_frame.palette._new_color_index(im_frame) + ) + except ValueError: + pass + if "transparency" in encoderinfo: + # When the delta is zero, fill the image with transparency + diff_frame = im_frame.copy() + fill = Image.new("P", delta.size, encoderinfo["transparency"]) + if delta.mode == "RGBA": + r, g, b, a = delta.split() + mask = ImageMath.lambda_eval( + lambda args: args["convert"]( + args["max"]( + args["max"]( + args["max"](args["r"], args["g"]), args["b"] + ), + args["a"], + ) + * 255, + "1", + ), + r=r, + g=g, + b=b, + a=a, + ) + else: + if delta.mode == "P": + # Convert to L without considering palette + delta_l = Image.new("L", delta.size) + delta_l.putdata(delta.getdata()) + delta = delta_l + mask = ImageMath.lambda_eval( + lambda args: args["convert"](args["im"] * 255, "1"), + im=delta, + ) + diff_frame.paste(fill, mask=ImageOps.invert(mask)) + else: + bbox = None + previous_im = im_frame + im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo)) + + if len(im_frames) == 1: + if "duration" in im.encoderinfo: + # Since multiple frames will not be written, use the combined duration + im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"] + return False + + for frame_data in im_frames: + im_frame = frame_data.im + if not frame_data.bbox: + # global header + for s in _get_global_header(im_frame, frame_data.encoderinfo): + fp.write(s) + offset = (0, 0) + else: + # compress difference + if not palette: + frame_data.encoderinfo["include_color_table"] = True + + if frame_data.bbox != (0, 0) + im_frame.size: + im_frame = im_frame.crop(frame_data.bbox) + offset = frame_data.bbox[:2] + _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo) + return True + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: + # header + if "palette" in im.encoderinfo or "palette" in im.info: + palette = im.encoderinfo.get("palette", im.info.get("palette")) + else: + palette = None + im.encoderinfo.setdefault("optimize", True) + + if not save_all or not _write_multiple_frames(im, fp, palette): + _write_single_frame(im, fp, palette) + + fp.write(b";") # end of file + + if hasattr(fp, "flush"): + fp.flush() + + +def get_interlace(im: Image.Image) -> int: + interlace = im.encoderinfo.get("interlace", 1) + + # workaround for @PIL153 + if min(im.size) < 16: + interlace = 0 + + return interlace + + +def _write_local_header( + fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int +) -> None: + try: + transparency = im.encoderinfo["transparency"] + except KeyError: + transparency = None + + if "duration" in im.encoderinfo: + duration = int(im.encoderinfo["duration"] / 10) + else: + duration = 0 + + disposal = int(im.encoderinfo.get("disposal", 0)) + + if transparency is not None or duration != 0 or disposal: + packed_flag = 1 if transparency is not None else 0 + packed_flag |= disposal << 2 + + fp.write( + b"!" + + o8(249) # extension intro + + o8(4) # length + + o8(packed_flag) # packed fields + + o16(duration) # duration + + o8(transparency or 0) # transparency index + + o8(0) + ) + + include_color_table = im.encoderinfo.get("include_color_table") + if include_color_table: + palette_bytes = _get_palette_bytes(im) + color_table_size = _get_color_table_size(palette_bytes) + if color_table_size: + flags = flags | 128 # local color table flag + flags = flags | color_table_size + + fp.write( + b"," + + o16(offset[0]) # offset + + o16(offset[1]) + + o16(im.size[0]) # size + + o16(im.size[1]) + + o8(flags) # flags + ) + if include_color_table and color_table_size: + fp.write(_get_header_palette(palette_bytes)) + fp.write(o8(8)) # bits + + +def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + # Unused by default. + # To use, uncomment the register_save call at the end of the file. + # + # If you need real GIF compression and/or RGB quantization, you + # can use the external NETPBM/PBMPLUS utilities. See comments + # below for information on how to enable this. + tempfile = im._dump() + + try: + with open(filename, "wb") as f: + if im.mode != "RGB": + subprocess.check_call( + ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL + ) + else: + # Pipe ppmquant output into ppmtogif + # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) + quant_cmd = ["ppmquant", "256", tempfile] + togif_cmd = ["ppmtogif"] + quant_proc = subprocess.Popen( + quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) + togif_proc = subprocess.Popen( + togif_cmd, + stdin=quant_proc.stdout, + stdout=f, + stderr=subprocess.DEVNULL, + ) + + # Allow ppmquant to receive SIGPIPE if ppmtogif exits + assert quant_proc.stdout is not None + quant_proc.stdout.close() + + retcode = quant_proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, quant_cmd) + + retcode = togif_proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, togif_cmd) + finally: + try: + os.unlink(tempfile) + except OSError: + pass + + +# Force optimization so that we can test performance against +# cases where it took lots of memory and time previously. +_FORCE_OPTIMIZE = False + + +def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: + """ + Palette optimization is a potentially expensive operation. + + This function determines if the palette should be optimized using + some heuristics, then returns the list of palette entries in use. + + :param im: Image object + :param info: encoderinfo + :returns: list of indexes of palette entries in use, or None + """ + if im.mode in ("P", "L") and info and info.get("optimize"): + # Potentially expensive operation. + + # The palette saves 3 bytes per color not used, but palette + # lengths are restricted to 3*(2**N) bytes. Max saving would + # be 768 -> 6 bytes if we went all the way down to 2 colors. + # * If we're over 128 colors, we can't save any space. + # * If there aren't any holes, it's not worth collapsing. + # * If we have a 'large' image, the palette is in the noise. + + # create the new palette if not every color is used + optimise = _FORCE_OPTIMIZE or im.mode == "L" + if optimise or im.width * im.height < 512 * 512: + # check which colors are used + used_palette_colors = [] + for i, count in enumerate(im.histogram()): + if count: + used_palette_colors.append(i) + + if optimise or max(used_palette_colors) >= len(used_palette_colors): + return used_palette_colors + + assert im.palette is not None + num_palette_colors = len(im.palette.palette) // Image.getmodebands( + im.palette.mode + ) + current_palette_size = 1 << (num_palette_colors - 1).bit_length() + if ( + # check that the palette would become smaller when saved + len(used_palette_colors) <= current_palette_size // 2 + # check that the palette is not already the smallest possible size + and current_palette_size > 2 + ): + return used_palette_colors + return None + + +def _get_color_table_size(palette_bytes: bytes) -> int: + # calculate the palette size for the header + if not palette_bytes: + return 0 + elif len(palette_bytes) < 9: + return 1 + else: + return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 + + +def _get_header_palette(palette_bytes: bytes) -> bytes: + """ + Returns the palette, null padded to the next power of 2 (*3) bytes + suitable for direct inclusion in the GIF header + + :param palette_bytes: Unpadded palette bytes, in RGBRGB form + :returns: Null padded palette + """ + color_table_size = _get_color_table_size(palette_bytes) + + # add the missing amount of bytes + # the palette has to be 2< 0: + palette_bytes += o8(0) * 3 * actual_target_size_diff + return palette_bytes + + +def _get_palette_bytes(im: Image.Image) -> bytes: + """ + Gets the palette for inclusion in the gif header + + :param im: Image object + :returns: Bytes, len<=768 suitable for inclusion in gif header + """ + if not im.palette: + return b"" + + palette = bytes(im.palette.palette) + if im.palette.mode == "RGBA": + palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3)) + return palette + + +def _get_background( + im: Image.Image, + info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None, +) -> int: + background = 0 + if info_background: + if isinstance(info_background, tuple): + # WebPImagePlugin stores an RGBA value in info["background"] + # So it must be converted to the same format as GifImagePlugin's + # info["background"] - a global color table index + assert im.palette is not None + try: + background = im.palette.getcolor(info_background, im) + except ValueError as e: + if str(e) not in ( + # If all 256 colors are in use, + # then there is no need for the background color + "cannot allocate more than 256 colors", + # Ignore non-opaque WebP background + "cannot add non-opaque RGBA color to RGB palette", + ): + raise + else: + background = info_background + return background + + +def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]: + """Return a list of strings representing a GIF header""" + + # Header Block + # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp + + version = b"87a" + if im.info.get("version") == b"89a" or ( + info + and ( + "transparency" in info + or info.get("loop") is not None + or info.get("duration") + or info.get("comment") + ) + ): + version = b"89a" + + background = _get_background(im, info.get("background")) + + palette_bytes = _get_palette_bytes(im) + color_table_size = _get_color_table_size(palette_bytes) + + header = [ + b"GIF" # signature + + version # version + + o16(im.size[0]) # canvas width + + o16(im.size[1]), # canvas height + # Logical Screen Descriptor + # size of global color table + global color table flag + o8(color_table_size + 128), # packed fields + # background + reserved/aspect + o8(background) + o8(0), + # Global Color Table + _get_header_palette(palette_bytes), + ] + if info.get("loop") is not None: + header.append( + b"!" + + o8(255) # extension intro + + o8(11) + + b"NETSCAPE2.0" + + o8(3) + + o8(1) + + o16(info["loop"]) # number of loops + + o8(0) + ) + if info.get("comment"): + comment_block = b"!" + o8(254) # extension intro + + comment = info["comment"] + if isinstance(comment, str): + comment = comment.encode() + for i in range(0, len(comment), 255): + subblock = comment[i : i + 255] + comment_block += o8(len(subblock)) + subblock + + comment_block += o8(0) + header.append(comment_block) + return header + + +def _write_frame_data( + fp: IO[bytes], + im_frame: Image.Image, + offset: tuple[int, int], + params: dict[str, Any], +) -> None: + try: + im_frame.encoderinfo = params + + # local image header + _write_local_header(fp, im_frame, offset, 0) + + ImageFile._save( + im_frame, + fp, + [ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])], + ) + + fp.write(b"\0") # end of image data + finally: + del im_frame.encoderinfo + + +# -------------------------------------------------------------------- +# Legacy GIF utilities + + +def getheader( + im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None +) -> tuple[list[bytes], list[int] | None]: + """ + Legacy Method to get Gif data from image. + + Warning:: May modify image data. + + :param im: Image object + :param palette: bytes object containing the source palette, or .... + :param info: encoderinfo + :returns: tuple of(list of header items, optimized palette) + + """ + if info is None: + info = {} + + used_palette_colors = _get_optimize(im, info) + + if "background" not in info and "background" in im.info: + info["background"] = im.info["background"] + + im_mod = _normalize_palette(im, palette, info) + im.palette = im_mod.palette + im.im = im_mod.im + header = _get_global_header(im, info) + + return header, used_palette_colors + + +def getdata( + im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any +) -> list[bytes]: + """ + Legacy Method + + Return a list of strings representing this image. + The first string is a local image header, the rest contains + encoded image data. + + To specify duration, add the time in milliseconds, + e.g. ``getdata(im_frame, duration=1000)`` + + :param im: Image object + :param offset: Tuple of (x, y) pixels. Defaults to (0, 0) + :param \\**params: e.g. duration or other encoder info parameters + :returns: List of bytes containing GIF encoded frame data + + """ + from io import BytesIO + + class Collector(BytesIO): + data = [] + + def write(self, data: Buffer) -> int: + self.data.append(data) + return len(data) + + im.load() # make sure raster data is available + + fp = Collector() + + _write_frame_data(fp, im, offset, params) + + return fp.data + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(GifImageFile.format, GifImageFile, _accept) +Image.register_save(GifImageFile.format, _save) +Image.register_save_all(GifImageFile.format, _save_all) +Image.register_extension(GifImageFile.format, ".gif") +Image.register_mime(GifImageFile.format, "image/gif") + +# +# Uncomment the following line if you wish to use NETPBM/PBMPLUS +# instead of the built-in "uncompressed" GIF encoder + +# Image.register_save(GifImageFile.format, _save_netpbm) diff --git a/python_modules/PIL/GimpGradientFile.py b/python_modules/PIL/GimpGradientFile.py new file mode 100644 index 000000000..ec62f8e4e --- /dev/null +++ b/python_modules/PIL/GimpGradientFile.py @@ -0,0 +1,149 @@ +# +# Python Imaging Library +# $Id$ +# +# stuff to read (and render) GIMP gradient files +# +# History: +# 97-08-23 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# + +""" +Stuff to translate curve segments to palette values (derived from +the corresponding code in GIMP, written by Federico Mena Quintero. +See the GIMP distribution for more information.) +""" +from __future__ import annotations + +from math import log, pi, sin, sqrt +from typing import IO, Callable + +from ._binary import o8 + +EPSILON = 1e-10 +"""""" # Enable auto-doc for data member + + +def linear(middle: float, pos: float) -> float: + if pos <= middle: + if middle < EPSILON: + return 0.0 + else: + return 0.5 * pos / middle + else: + pos = pos - middle + middle = 1.0 - middle + if middle < EPSILON: + return 1.0 + else: + return 0.5 + 0.5 * pos / middle + + +def curved(middle: float, pos: float) -> float: + return pos ** (log(0.5) / log(max(middle, EPSILON))) + + +def sine(middle: float, pos: float) -> float: + return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 + + +def sphere_increasing(middle: float, pos: float) -> float: + return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) + + +def sphere_decreasing(middle: float, pos: float) -> float: + return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) + + +SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] +"""""" # Enable auto-doc for data member + + +class GradientFile: + gradient: ( + list[ + tuple[ + float, + float, + float, + list[float], + list[float], + Callable[[float, float], float], + ] + ] + | None + ) = None + + def getpalette(self, entries: int = 256) -> tuple[bytes, str]: + assert self.gradient is not None + palette = [] + + ix = 0 + x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] + + for i in range(entries): + x = i / (entries - 1) + + while x1 < x: + ix += 1 + x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] + + w = x1 - x0 + + if w < EPSILON: + scale = segment(0.5, 0.5) + else: + scale = segment((xm - x0) / w, (x - x0) / w) + + # expand to RGBA + r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5)) + g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5)) + b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5)) + a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5)) + + # add to palette + palette.append(r + g + b + a) + + return b"".join(palette), "RGBA" + + +class GimpGradientFile(GradientFile): + """File handler for GIMP's gradient format.""" + + def __init__(self, fp: IO[bytes]) -> None: + if not fp.readline().startswith(b"GIMP Gradient"): + msg = "not a GIMP gradient file" + raise SyntaxError(msg) + + line = fp.readline() + + # GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do + if line.startswith(b"Name: "): + line = fp.readline().strip() + + count = int(line) + + self.gradient = [] + + for i in range(count): + s = fp.readline().split() + w = [float(x) for x in s[:11]] + + x0, x1 = w[0], w[2] + xm = w[1] + rgb0 = w[3:7] + rgb1 = w[7:11] + + segment = SEGMENTS[int(s[11])] + cspace = int(s[12]) + + if cspace != 0: + msg = "cannot handle HSV colour space" + raise OSError(msg) + + self.gradient.append((x0, x1, xm, rgb0, rgb1, segment)) diff --git a/python_modules/PIL/GimpPaletteFile.py b/python_modules/PIL/GimpPaletteFile.py new file mode 100644 index 000000000..379ffd739 --- /dev/null +++ b/python_modules/PIL/GimpPaletteFile.py @@ -0,0 +1,72 @@ +# +# Python Imaging Library +# $Id$ +# +# stuff to read GIMP palette files +# +# History: +# 1997-08-23 fl Created +# 2004-09-07 fl Support GIMP 2.0 palette files. +# +# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. +# Copyright (c) Fredrik Lundh 1997-2004. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import re +from io import BytesIO +from typing import IO + + +class GimpPaletteFile: + """File handler for GIMP's palette format.""" + + rawmode = "RGB" + + def _read(self, fp: IO[bytes], limit: bool = True) -> None: + if not fp.readline().startswith(b"GIMP Palette"): + msg = "not a GIMP palette file" + raise SyntaxError(msg) + + palette: list[int] = [] + i = 0 + while True: + if limit and i == 256 + 3: + break + + i += 1 + s = fp.readline() + if not s: + break + + # skip fields and comment lines + if re.match(rb"\w+:|#", s): + continue + if limit and len(s) > 100: + msg = "bad palette file" + raise SyntaxError(msg) + + v = s.split(maxsplit=3) + if len(v) < 3: + msg = "bad palette entry" + raise ValueError(msg) + + palette += (int(v[i]) for i in range(3)) + if limit and len(palette) == 768: + break + + self.palette = bytes(palette) + + def __init__(self, fp: IO[bytes]) -> None: + self._read(fp) + + @classmethod + def frombytes(cls, data: bytes) -> GimpPaletteFile: + self = cls.__new__(cls) + self._read(BytesIO(data), False) + return self + + def getpalette(self) -> tuple[bytes, str]: + return self.palette, self.rawmode diff --git a/python_modules/PIL/GribStubImagePlugin.py b/python_modules/PIL/GribStubImagePlugin.py new file mode 100644 index 000000000..439fc5a3e --- /dev/null +++ b/python_modules/PIL/GribStubImagePlugin.py @@ -0,0 +1,75 @@ +# +# The Python Imaging Library +# $Id$ +# +# GRIB stub adapter +# +# Copyright (c) 1996-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +from typing import IO + +from . import Image, ImageFile + +_handler = None + + +def register_handler(handler: ImageFile.StubHandler | None) -> None: + """ + Install application-specific GRIB image handler. + + :param handler: Handler object. + """ + global _handler + _handler = handler + + +# -------------------------------------------------------------------- +# Image adapter + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"GRIB") and prefix[7] == 1 + + +class GribStubImageFile(ImageFile.StubImageFile): + format = "GRIB" + format_description = "GRIB" + + def _open(self) -> None: + if not _accept(self.fp.read(8)): + msg = "Not a GRIB file" + raise SyntaxError(msg) + + self.fp.seek(-8, os.SEEK_CUR) + + # make something up + self._mode = "F" + self._size = 1, 1 + + loader = self._load() + if loader: + loader.open(self) + + def _load(self) -> ImageFile.StubHandler | None: + return _handler + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if _handler is None or not hasattr(_handler, "save"): + msg = "GRIB save handler not installed" + raise OSError(msg) + _handler.save(im, fp, filename) + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept) +Image.register_save(GribStubImageFile.format, _save) + +Image.register_extension(GribStubImageFile.format, ".grib") diff --git a/python_modules/PIL/Hdf5StubImagePlugin.py b/python_modules/PIL/Hdf5StubImagePlugin.py new file mode 100644 index 000000000..76e640f15 --- /dev/null +++ b/python_modules/PIL/Hdf5StubImagePlugin.py @@ -0,0 +1,75 @@ +# +# The Python Imaging Library +# $Id$ +# +# HDF5 stub adapter +# +# Copyright (c) 2000-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +from typing import IO + +from . import Image, ImageFile + +_handler = None + + +def register_handler(handler: ImageFile.StubHandler | None) -> None: + """ + Install application-specific HDF5 image handler. + + :param handler: Handler object. + """ + global _handler + _handler = handler + + +# -------------------------------------------------------------------- +# Image adapter + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"\x89HDF\r\n\x1a\n") + + +class HDF5StubImageFile(ImageFile.StubImageFile): + format = "HDF5" + format_description = "HDF5" + + def _open(self) -> None: + if not _accept(self.fp.read(8)): + msg = "Not an HDF file" + raise SyntaxError(msg) + + self.fp.seek(-8, os.SEEK_CUR) + + # make something up + self._mode = "F" + self._size = 1, 1 + + loader = self._load() + if loader: + loader.open(self) + + def _load(self) -> ImageFile.StubHandler | None: + return _handler + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if _handler is None or not hasattr(_handler, "save"): + msg = "HDF5 save handler not installed" + raise OSError(msg) + _handler.save(im, fp, filename) + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept) +Image.register_save(HDF5StubImageFile.format, _save) + +Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"]) diff --git a/python_modules/PIL/IcnsImagePlugin.py b/python_modules/PIL/IcnsImagePlugin.py new file mode 100644 index 000000000..5a88429e5 --- /dev/null +++ b/python_modules/PIL/IcnsImagePlugin.py @@ -0,0 +1,411 @@ +# +# The Python Imaging Library. +# $Id$ +# +# macOS icns file decoder, based on icns.py by Bob Ippolito. +# +# history: +# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies. +# 2020-04-04 Allow saving on all operating systems. +# +# Copyright (c) 2004 by Bob Ippolito. +# Copyright (c) 2004 by Secret Labs. +# Copyright (c) 2004 by Fredrik Lundh. +# Copyright (c) 2014 by Alastair Houghton. +# Copyright (c) 2020 by Pan Jing. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +import os +import struct +import sys +from typing import IO + +from . import Image, ImageFile, PngImagePlugin, features +from ._deprecate import deprecate + +enable_jpeg2k = features.check_codec("jpg_2000") +if enable_jpeg2k: + from . import Jpeg2KImagePlugin + +MAGIC = b"icns" +HEADERSIZE = 8 + + +def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]: + return struct.unpack(">4sI", fobj.read(HEADERSIZE)) + + +def read_32t( + fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] +) -> dict[str, Image.Image]: + # The 128x128 icon seems to have an extra header for some reason. + (start, length) = start_length + fobj.seek(start) + sig = fobj.read(4) + if sig != b"\x00\x00\x00\x00": + msg = "Unknown signature, expecting 0x00000000" + raise SyntaxError(msg) + return read_32(fobj, (start + 4, length - 4), size) + + +def read_32( + fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] +) -> dict[str, Image.Image]: + """ + Read a 32bit RGB icon resource. Seems to be either uncompressed or + an RLE packbits-like scheme. + """ + (start, length) = start_length + fobj.seek(start) + pixel_size = (size[0] * size[2], size[1] * size[2]) + sizesq = pixel_size[0] * pixel_size[1] + if length == sizesq * 3: + # uncompressed ("RGBRGBGB") + indata = fobj.read(length) + im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1) + else: + # decode image + im = Image.new("RGB", pixel_size, None) + for band_ix in range(3): + data = [] + bytesleft = sizesq + while bytesleft > 0: + byte = fobj.read(1) + if not byte: + break + byte_int = byte[0] + if byte_int & 0x80: + blocksize = byte_int - 125 + byte = fobj.read(1) + for i in range(blocksize): + data.append(byte) + else: + blocksize = byte_int + 1 + data.append(fobj.read(blocksize)) + bytesleft -= blocksize + if bytesleft <= 0: + break + if bytesleft != 0: + msg = f"Error reading channel [{repr(bytesleft)} left]" + raise SyntaxError(msg) + band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) + im.im.putband(band.im, band_ix) + return {"RGB": im} + + +def read_mk( + fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] +) -> dict[str, Image.Image]: + # Alpha masks seem to be uncompressed + start = start_length[0] + fobj.seek(start) + pixel_size = (size[0] * size[2], size[1] * size[2]) + sizesq = pixel_size[0] * pixel_size[1] + band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1) + return {"A": band} + + +def read_png_or_jpeg2000( + fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] +) -> dict[str, Image.Image]: + (start, length) = start_length + fobj.seek(start) + sig = fobj.read(12) + + im: Image.Image + if sig.startswith(b"\x89PNG\x0d\x0a\x1a\x0a"): + fobj.seek(start) + im = PngImagePlugin.PngImageFile(fobj) + Image._decompression_bomb_check(im.size) + return {"RGBA": im} + elif ( + sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a")) + or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" + ): + if not enable_jpeg2k: + msg = ( + "Unsupported icon subimage format (rebuild PIL " + "with JPEG 2000 support to fix this)" + ) + raise ValueError(msg) + # j2k, jpc or j2c + fobj.seek(start) + jp2kstream = fobj.read(length) + f = io.BytesIO(jp2kstream) + im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) + Image._decompression_bomb_check(im.size) + if im.mode != "RGBA": + im = im.convert("RGBA") + return {"RGBA": im} + else: + msg = "Unsupported icon subimage format" + raise ValueError(msg) + + +class IcnsFile: + SIZES = { + (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], + (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], + (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)], + (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)], + (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)], + (128, 128, 1): [ + (b"ic07", read_png_or_jpeg2000), + (b"it32", read_32t), + (b"t8mk", read_mk), + ], + (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)], + (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)], + (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)], + (32, 32, 1): [ + (b"icp5", read_png_or_jpeg2000), + (b"il32", read_32), + (b"l8mk", read_mk), + ], + (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)], + (16, 16, 1): [ + (b"icp4", read_png_or_jpeg2000), + (b"is32", read_32), + (b"s8mk", read_mk), + ], + } + + def __init__(self, fobj: IO[bytes]) -> None: + """ + fobj is a file-like object as an icns resource + """ + # signature : (start, length) + self.dct = {} + self.fobj = fobj + sig, filesize = nextheader(fobj) + if not _accept(sig): + msg = "not an icns file" + raise SyntaxError(msg) + i = HEADERSIZE + while i < filesize: + sig, blocksize = nextheader(fobj) + if blocksize <= 0: + msg = "invalid block header" + raise SyntaxError(msg) + i += HEADERSIZE + blocksize -= HEADERSIZE + self.dct[sig] = (i, blocksize) + fobj.seek(blocksize, io.SEEK_CUR) + i += blocksize + + def itersizes(self) -> list[tuple[int, int, int]]: + sizes = [] + for size, fmts in self.SIZES.items(): + for fmt, reader in fmts: + if fmt in self.dct: + sizes.append(size) + break + return sizes + + def bestsize(self) -> tuple[int, int, int]: + sizes = self.itersizes() + if not sizes: + msg = "No 32bit icon resources found" + raise SyntaxError(msg) + return max(sizes) + + def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]: + """ + Get an icon resource as {channel: array}. Note that + the arrays are bottom-up like windows bitmaps and will likely + need to be flipped or transposed in some way. + """ + dct = {} + for code, reader in self.SIZES[size]: + desc = self.dct.get(code) + if desc is not None: + dct.update(reader(self.fobj, desc, size)) + return dct + + def getimage( + self, size: tuple[int, int] | tuple[int, int, int] | None = None + ) -> Image.Image: + if size is None: + size = self.bestsize() + elif len(size) == 2: + size = (size[0], size[1], 1) + channels = self.dataforsize(size) + + im = channels.get("RGBA") + if im: + return im + + im = channels["RGB"].copy() + try: + im.putalpha(channels["A"]) + except KeyError: + pass + return im + + +## +# Image plugin for Mac OS icons. + + +class IcnsImageFile(ImageFile.ImageFile): + """ + PIL image support for Mac OS .icns files. + Chooses the best resolution, but will possibly load + a different size image if you mutate the size attribute + before calling 'load'. + + The info dictionary has a key 'sizes' that is a list + of sizes that the icns file has. + """ + + format = "ICNS" + format_description = "Mac OS icns resource" + + def _open(self) -> None: + self.icns = IcnsFile(self.fp) + self._mode = "RGBA" + self.info["sizes"] = self.icns.itersizes() + self.best_size = self.icns.bestsize() + self.size = ( + self.best_size[0] * self.best_size[2], + self.best_size[1] * self.best_size[2], + ) + + @property # type: ignore[override] + def size(self) -> tuple[int, int] | tuple[int, int, int]: + return self._size + + @size.setter + def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None: + if len(value) == 3: + deprecate("Setting size to (width, height, scale)", 12, "load(scale)") + if value in self.info["sizes"]: + self._size = value # type: ignore[assignment] + return + else: + # Check that a matching size exists, + # or that there is a scale that would create a size that matches + for size in self.info["sizes"]: + simple_size = size[0] * size[2], size[1] * size[2] + scale = simple_size[0] // value[0] + if simple_size[1] / value[1] == scale: + self._size = value + return + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) + + def load(self, scale: int | None = None) -> Image.core.PixelAccess | None: + if scale is not None or len(self.size) == 3: + if scale is None and len(self.size) == 3: + scale = self.size[2] + assert scale is not None + width, height = self.size[:2] + self.size = width * scale, height * scale + self.best_size = width, height, scale + + px = Image.Image.load(self) + if self._im is not None and self.im.size == self.size: + # Already loaded + return px + self.load_prepare() + # This is likely NOT the best way to do it, but whatever. + im = self.icns.getimage(self.best_size) + + # If this is a PNG or JPEG 2000, it won't be loaded yet + px = im.load() + + self.im = im.im + self._mode = im.mode + self.size = im.size + + return px + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + """ + Saves the image as a series of PNG files, + that are then combined into a .icns file. + """ + if hasattr(fp, "flush"): + fp.flush() + + sizes = { + b"ic07": 128, + b"ic08": 256, + b"ic09": 512, + b"ic10": 1024, + b"ic11": 32, + b"ic12": 64, + b"ic13": 256, + b"ic14": 512, + } + provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])} + size_streams = {} + for size in set(sizes.values()): + image = ( + provided_images[size] + if size in provided_images + else im.resize((size, size)) + ) + + temp = io.BytesIO() + image.save(temp, "png") + size_streams[size] = temp.getvalue() + + entries = [] + for type, size in sizes.items(): + stream = size_streams[size] + entries.append((type, HEADERSIZE + len(stream), stream)) + + # Header + fp.write(MAGIC) + file_length = HEADERSIZE # Header + file_length += HEADERSIZE + 8 * len(entries) # TOC + file_length += sum(entry[1] for entry in entries) + fp.write(struct.pack(">i", file_length)) + + # TOC + fp.write(b"TOC ") + fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) + for entry in entries: + fp.write(entry[0]) + fp.write(struct.pack(">i", entry[1])) + + # Data + for entry in entries: + fp.write(entry[0]) + fp.write(struct.pack(">i", entry[1])) + fp.write(entry[2]) + + if hasattr(fp, "flush"): + fp.flush() + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(MAGIC) + + +Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) +Image.register_extension(IcnsImageFile.format, ".icns") + +Image.register_save(IcnsImageFile.format, _save) +Image.register_mime(IcnsImageFile.format, "image/icns") + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Syntax: python3 IcnsImagePlugin.py [file]") + sys.exit() + + with open(sys.argv[1], "rb") as fp: + imf = IcnsImageFile(fp) + for size in imf.info["sizes"]: + width, height, scale = imf.size = size + imf.save(f"out-{width}-{height}-{scale}.png") + with Image.open(sys.argv[1]) as im: + im.save("out.png") + if sys.platform == "windows": + os.startfile("out.png") diff --git a/python_modules/PIL/IcoImagePlugin.py b/python_modules/PIL/IcoImagePlugin.py new file mode 100644 index 000000000..bd35ac890 --- /dev/null +++ b/python_modules/PIL/IcoImagePlugin.py @@ -0,0 +1,381 @@ +# +# The Python Imaging Library. +# $Id$ +# +# Windows Icon support for PIL +# +# History: +# 96-05-27 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + +# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis +# . +# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki +# +# Icon format references: +# * https://en.wikipedia.org/wiki/ICO_(file_format) +# * https://msdn.microsoft.com/en-us/library/ms997538.aspx +from __future__ import annotations + +import warnings +from io import BytesIO +from math import ceil, log +from typing import IO, NamedTuple + +from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin +from ._binary import i16le as i16 +from ._binary import i32le as i32 +from ._binary import o8 +from ._binary import o16le as o16 +from ._binary import o32le as o32 + +# +# -------------------------------------------------------------------- + +_MAGIC = b"\0\0\1\0" + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + fp.write(_MAGIC) # (2+2) + bmp = im.encoderinfo.get("bitmap_format") == "bmp" + sizes = im.encoderinfo.get( + "sizes", + [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], + ) + frames = [] + provided_ims = [im] + im.encoderinfo.get("append_images", []) + width, height = im.size + for size in sorted(set(sizes)): + if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256: + continue + + for provided_im in provided_ims: + if provided_im.size != size: + continue + frames.append(provided_im) + if bmp: + bits = BmpImagePlugin.SAVE[provided_im.mode][1] + bits_used = [bits] + for other_im in provided_ims: + if other_im.size != size: + continue + bits = BmpImagePlugin.SAVE[other_im.mode][1] + if bits not in bits_used: + # Another image has been supplied for this size + # with a different bit depth + frames.append(other_im) + bits_used.append(bits) + break + else: + # TODO: invent a more convenient method for proportional scalings + frame = provided_im.copy() + frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) + frames.append(frame) + fp.write(o16(len(frames))) # idCount(2) + offset = fp.tell() + len(frames) * 16 + for frame in frames: + width, height = frame.size + # 0 means 256 + fp.write(o8(width if width < 256 else 0)) # bWidth(1) + fp.write(o8(height if height < 256 else 0)) # bHeight(1) + + bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0) + fp.write(o8(colors)) # bColorCount(1) + fp.write(b"\0") # bReserved(1) + fp.write(b"\0\0") # wPlanes(2) + fp.write(o16(bits)) # wBitCount(2) + + image_io = BytesIO() + if bmp: + frame.save(image_io, "dib") + + if bits != 32: + and_mask = Image.new("1", size) + ImageFile._save( + and_mask, + image_io, + [ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))], + ) + else: + frame.save(image_io, "png") + image_io.seek(0) + image_bytes = image_io.read() + if bmp: + image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:] + bytes_len = len(image_bytes) + fp.write(o32(bytes_len)) # dwBytesInRes(4) + fp.write(o32(offset)) # dwImageOffset(4) + current = fp.tell() + fp.seek(offset) + fp.write(image_bytes) + offset = offset + bytes_len + fp.seek(current) + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(_MAGIC) + + +class IconHeader(NamedTuple): + width: int + height: int + nb_color: int + reserved: int + planes: int + bpp: int + size: int + offset: int + dim: tuple[int, int] + square: int + color_depth: int + + +class IcoFile: + def __init__(self, buf: IO[bytes]) -> None: + """ + Parse image from file-like object containing ico file data + """ + + # check magic + s = buf.read(6) + if not _accept(s): + msg = "not an ICO file" + raise SyntaxError(msg) + + self.buf = buf + self.entry = [] + + # Number of items in file + self.nb_items = i16(s, 4) + + # Get headers for each item + for i in range(self.nb_items): + s = buf.read(16) + + # See Wikipedia + width = s[0] or 256 + height = s[1] or 256 + + # No. of colors in image (0 if >=8bpp) + nb_color = s[2] + bpp = i16(s, 6) + icon_header = IconHeader( + width=width, + height=height, + nb_color=nb_color, + reserved=s[3], + planes=i16(s, 4), + bpp=i16(s, 6), + size=i32(s, 8), + offset=i32(s, 12), + dim=(width, height), + square=width * height, + # See Wikipedia notes about color depth. + # We need this just to differ images with equal sizes + color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256, + ) + + self.entry.append(icon_header) + + self.entry = sorted(self.entry, key=lambda x: x.color_depth) + # ICO images are usually squares + self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True) + + def sizes(self) -> set[tuple[int, int]]: + """ + Get a set of all available icon sizes and color depths. + """ + return {(h.width, h.height) for h in self.entry} + + def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int: + for i, h in enumerate(self.entry): + if size == h.dim and (bpp is False or bpp == h.color_depth): + return i + return 0 + + def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image: + """ + Get an image from the icon + """ + return self.frame(self.getentryindex(size, bpp)) + + def frame(self, idx: int) -> Image.Image: + """ + Get an image from frame idx + """ + + header = self.entry[idx] + + self.buf.seek(header.offset) + data = self.buf.read(8) + self.buf.seek(header.offset) + + im: Image.Image + if data[:8] == PngImagePlugin._MAGIC: + # png frame + im = PngImagePlugin.PngImageFile(self.buf) + Image._decompression_bomb_check(im.size) + else: + # XOR + AND mask bmp frame + im = BmpImagePlugin.DibImageFile(self.buf) + Image._decompression_bomb_check(im.size) + + # change tile dimension to only encompass XOR image + im._size = (im.size[0], int(im.size[1] / 2)) + d, e, o, a = im.tile[0] + im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a) + + # figure out where AND mask image starts + if header.bpp == 32: + # 32-bit color depth icon image allows semitransparent areas + # PIL's DIB format ignores transparency bits, recover them. + # The DIB is packed in BGRX byte order where X is the alpha + # channel. + + # Back up to start of bmp data + self.buf.seek(o) + # extract every 4th byte (eg. 3,7,11,15,...) + alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4] + + # convert to an 8bpp grayscale image + try: + mask = Image.frombuffer( + "L", # 8bpp + im.size, # (w, h) + alpha_bytes, # source chars + "raw", # raw decoder + ("L", 0, -1), # 8bpp inverted, unpadded, reversed + ) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + mask = None + else: + raise + else: + # get AND image from end of bitmap + w = im.size[0] + if (w % 32) > 0: + # bitmap row data is aligned to word boundaries + w += 32 - (im.size[0] % 32) + + # the total mask data is + # padded row size * height / bits per char + + total_bytes = int((w * im.size[1]) / 8) + and_mask_offset = header.offset + header.size - total_bytes + + self.buf.seek(and_mask_offset) + mask_data = self.buf.read(total_bytes) + + # convert raw data to image + try: + mask = Image.frombuffer( + "1", # 1 bpp + im.size, # (w, h) + mask_data, # source chars + "raw", # raw decoder + ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed + ) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + mask = None + else: + raise + + # now we have two images, im is XOR image and mask is AND image + + # apply mask image as alpha channel + if mask: + im = im.convert("RGBA") + im.putalpha(mask) + + return im + + +## +# Image plugin for Windows Icon files. + + +class IcoImageFile(ImageFile.ImageFile): + """ + PIL read-only image support for Microsoft Windows .ico files. + + By default the largest resolution image in the file will be loaded. This + can be changed by altering the 'size' attribute before calling 'load'. + + The info dictionary has a key 'sizes' that is a list of the sizes available + in the icon file. + + Handles classic, XP and Vista icon formats. + + When saving, PNG compression is used. Support for this was only added in + Windows Vista. If you are unable to view the icon in Windows, convert the + image to "RGBA" mode before saving. + + This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis + . + https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki + """ + + format = "ICO" + format_description = "Windows Icon" + + def _open(self) -> None: + self.ico = IcoFile(self.fp) + self.info["sizes"] = self.ico.sizes() + self.size = self.ico.entry[0].dim + self.load() + + @property + def size(self) -> tuple[int, int]: + return self._size + + @size.setter + def size(self, value: tuple[int, int]) -> None: + if value not in self.info["sizes"]: + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) + self._size = value + + def load(self) -> Image.core.PixelAccess | None: + if self._im is not None and self.im.size == self.size: + # Already loaded + return Image.Image.load(self) + im = self.ico.getimage(self.size) + # if tile is PNG, it won't really be loaded yet + im.load() + self.im = im.im + self._mode = im.mode + if im.palette: + self.palette = im.palette + if im.size != self.size: + warnings.warn("Image was not the expected size") + + index = self.ico.getentryindex(self.size) + sizes = list(self.info["sizes"]) + sizes[index] = im.size + self.info["sizes"] = set(sizes) + + self.size = im.size + return Image.Image.load(self) + + def load_seek(self, pos: int) -> None: + # Flag the ImageFile.Parser so that it + # just does all the decode at the end. + pass + + +# +# -------------------------------------------------------------------- + + +Image.register_open(IcoImageFile.format, IcoImageFile, _accept) +Image.register_save(IcoImageFile.format, _save) +Image.register_extension(IcoImageFile.format, ".ico") + +Image.register_mime(IcoImageFile.format, "image/x-icon") diff --git a/python_modules/PIL/ImImagePlugin.py b/python_modules/PIL/ImImagePlugin.py new file mode 100644 index 000000000..71b999678 --- /dev/null +++ b/python_modules/PIL/ImImagePlugin.py @@ -0,0 +1,389 @@ +# +# The Python Imaging Library. +# $Id$ +# +# IFUNC IM file handling for PIL +# +# history: +# 1995-09-01 fl Created. +# 1997-01-03 fl Save palette images +# 1997-01-08 fl Added sequence support +# 1997-01-23 fl Added P and RGB save support +# 1997-05-31 fl Read floating point images +# 1997-06-22 fl Save floating point images +# 1997-08-27 fl Read and save 1-bit images +# 1998-06-25 fl Added support for RGB+LUT images +# 1998-07-02 fl Added support for YCC images +# 1998-07-15 fl Renamed offset attribute to avoid name clash +# 1998-12-29 fl Added I;16 support +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) +# 2003-09-26 fl Added LA/PA support +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-2001 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +import re +from typing import IO, Any + +from . import Image, ImageFile, ImagePalette +from ._util import DeferredError + +# -------------------------------------------------------------------- +# Standard tags + +COMMENT = "Comment" +DATE = "Date" +EQUIPMENT = "Digitalization equipment" +FRAMES = "File size (no of images)" +LUT = "Lut" +NAME = "Name" +SCALE = "Scale (x,y)" +SIZE = "Image size (x*y)" +MODE = "Image type" + +TAGS = { + COMMENT: 0, + DATE: 0, + EQUIPMENT: 0, + FRAMES: 0, + LUT: 0, + NAME: 0, + SCALE: 0, + SIZE: 0, + MODE: 0, +} + +OPEN = { + # ifunc93/p3cfunc formats + "0 1 image": ("1", "1"), + "L 1 image": ("1", "1"), + "Greyscale image": ("L", "L"), + "Grayscale image": ("L", "L"), + "RGB image": ("RGB", "RGB;L"), + "RLB image": ("RGB", "RLB"), + "RYB image": ("RGB", "RLB"), + "B1 image": ("1", "1"), + "B2 image": ("P", "P;2"), + "B4 image": ("P", "P;4"), + "X 24 image": ("RGB", "RGB"), + "L 32 S image": ("I", "I;32"), + "L 32 F image": ("F", "F;32"), + # old p3cfunc formats + "RGB3 image": ("RGB", "RGB;T"), + "RYB3 image": ("RGB", "RYB;T"), + # extensions + "LA image": ("LA", "LA;L"), + "PA image": ("LA", "PA;L"), + "RGBA image": ("RGBA", "RGBA;L"), + "RGBX image": ("RGB", "RGBX;L"), + "CMYK image": ("CMYK", "CMYK;L"), + "YCC image": ("YCbCr", "YCbCr;L"), +} + +# ifunc95 extensions +for i in ["8", "8S", "16", "16S", "32", "32F"]: + OPEN[f"L {i} image"] = ("F", f"F;{i}") + OPEN[f"L*{i} image"] = ("F", f"F;{i}") +for i in ["16", "16L", "16B"]: + OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}") + OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}") +for i in ["32S"]: + OPEN[f"L {i} image"] = ("I", f"I;{i}") + OPEN[f"L*{i} image"] = ("I", f"I;{i}") +for j in range(2, 33): + OPEN[f"L*{j} image"] = ("F", f"F;{j}") + + +# -------------------------------------------------------------------- +# Read IM directory + +split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") + + +def number(s: Any) -> float: + try: + return int(s) + except ValueError: + return float(s) + + +## +# Image plugin for the IFUNC IM file format. + + +class ImImageFile(ImageFile.ImageFile): + format = "IM" + format_description = "IFUNC Image Memory" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + # Quick rejection: if there's not an LF among the first + # 100 bytes, this is (probably) not a text header. + + if b"\n" not in self.fp.read(100): + msg = "not an IM file" + raise SyntaxError(msg) + self.fp.seek(0) + + n = 0 + + # Default values + self.info[MODE] = "L" + self.info[SIZE] = (512, 512) + self.info[FRAMES] = 1 + + self.rawmode = "L" + + while True: + s = self.fp.read(1) + + # Some versions of IFUNC uses \n\r instead of \r\n... + if s == b"\r": + continue + + if not s or s == b"\0" or s == b"\x1a": + break + + # FIXME: this may read whole file if not a text file + s = s + self.fp.readline() + + if len(s) > 100: + msg = "not an IM file" + raise SyntaxError(msg) + + if s.endswith(b"\r\n"): + s = s[:-2] + elif s.endswith(b"\n"): + s = s[:-1] + + try: + m = split.match(s) + except re.error as e: + msg = "not an IM file" + raise SyntaxError(msg) from e + + if m: + k, v = m.group(1, 2) + + # Don't know if this is the correct encoding, + # but a decent guess (I guess) + k = k.decode("latin-1", "replace") + v = v.decode("latin-1", "replace") + + # Convert value as appropriate + if k in [FRAMES, SCALE, SIZE]: + v = v.replace("*", ",") + v = tuple(map(number, v.split(","))) + if len(v) == 1: + v = v[0] + elif k == MODE and v in OPEN: + v, self.rawmode = OPEN[v] + + # Add to dictionary. Note that COMMENT tags are + # combined into a list of strings. + if k == COMMENT: + if k in self.info: + self.info[k].append(v) + else: + self.info[k] = [v] + else: + self.info[k] = v + + if k in TAGS: + n += 1 + + else: + msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}" + raise SyntaxError(msg) + + if not n: + msg = "Not an IM file" + raise SyntaxError(msg) + + # Basic attributes + self._size = self.info[SIZE] + self._mode = self.info[MODE] + + # Skip forward to start of image data + while s and not s.startswith(b"\x1a"): + s = self.fp.read(1) + if not s: + msg = "File truncated" + raise SyntaxError(msg) + + if LUT in self.info: + # convert lookup table to palette or lut attribute + palette = self.fp.read(768) + greyscale = 1 # greyscale palette + linear = 1 # linear greyscale palette + for i in range(256): + if palette[i] == palette[i + 256] == palette[i + 512]: + if palette[i] != i: + linear = 0 + else: + greyscale = 0 + if self.mode in ["L", "LA", "P", "PA"]: + if greyscale: + if not linear: + self.lut = list(palette[:256]) + else: + if self.mode in ["L", "P"]: + self._mode = self.rawmode = "P" + elif self.mode in ["LA", "PA"]: + self._mode = "PA" + self.rawmode = "PA;L" + self.palette = ImagePalette.raw("RGB;L", palette) + elif self.mode == "RGB": + if not greyscale or not linear: + self.lut = list(palette) + + self.frame = 0 + + self.__offset = offs = self.fp.tell() + + self._fp = self.fp # FIXME: hack + + if self.rawmode.startswith("F;"): + # ifunc95 formats + try: + # use bit decoder (if necessary) + bits = int(self.rawmode[2:]) + if bits not in [8, 16, 32]: + self.tile = [ + ImageFile._Tile( + "bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1) + ) + ] + return + except ValueError: + pass + + if self.rawmode in ["RGB;T", "RYB;T"]: + # Old LabEye/3PC files. Would be very surprised if anyone + # ever stumbled upon such a file ;-) + size = self.size[0] * self.size[1] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)), + ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), + ImageFile._Tile( + "raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1) + ), + ] + else: + # LabEye/IFUNC files + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1)) + ] + + @property + def n_frames(self) -> int: + return self.info[FRAMES] + + @property + def is_animated(self) -> bool: + return self.info[FRAMES] > 1 + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + if isinstance(self._fp, DeferredError): + raise self._fp.ex + + self.frame = frame + + if self.mode == "1": + bits = 1 + else: + bits = 8 * len(self.mode) + + size = ((self.size[0] * bits + 7) // 8) * self.size[1] + offs = self.__offset + frame * size + + self.fp = self._fp + + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1)) + ] + + def tell(self) -> int: + return self.frame + + +# +# -------------------------------------------------------------------- +# Save IM files + + +SAVE = { + # mode: (im type, raw mode) + "1": ("0 1", "1"), + "L": ("Greyscale", "L"), + "LA": ("LA", "LA;L"), + "P": ("Greyscale", "P"), + "PA": ("LA", "PA;L"), + "I": ("L 32S", "I;32S"), + "I;16": ("L 16", "I;16"), + "I;16L": ("L 16L", "I;16L"), + "I;16B": ("L 16B", "I;16B"), + "F": ("L 32F", "F;32F"), + "RGB": ("RGB", "RGB;L"), + "RGBA": ("RGBA", "RGBA;L"), + "RGBX": ("RGBX", "RGBX;L"), + "CMYK": ("CMYK", "CMYK;L"), + "YCbCr": ("YCC", "YCbCr;L"), +} + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + try: + image_type, rawmode = SAVE[im.mode] + except KeyError as e: + msg = f"Cannot save {im.mode} images as IM" + raise ValueError(msg) from e + + frames = im.encoderinfo.get("frames", 1) + + fp.write(f"Image type: {image_type} image\r\n".encode("ascii")) + if filename: + # Each line must be 100 characters or less, + # or: SyntaxError("not an IM file") + # 8 characters are used for "Name: " and "\r\n" + # Keep just the filename, ditch the potentially overlong path + if isinstance(filename, bytes): + filename = filename.decode("ascii") + name, ext = os.path.splitext(os.path.basename(filename)) + name = "".join([name[: 92 - len(ext)], ext]) + + fp.write(f"Name: {name}\r\n".encode("ascii")) + fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii")) + fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) + if im.mode in ["P", "PA"]: + fp.write(b"Lut: 1\r\n") + fp.write(b"\000" * (511 - fp.tell()) + b"\032") + if im.mode in ["P", "PA"]: + im_palette = im.im.getpalette("RGB", "RGB;L") + colors = len(im_palette) // 3 + palette = b"" + for i in range(3): + palette += im_palette[colors * i : colors * (i + 1)] + palette += b"\x00" * (256 - colors) + fp.write(palette) # 768 bytes + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))] + ) + + +# +# -------------------------------------------------------------------- +# Registry + + +Image.register_open(ImImageFile.format, ImImageFile) +Image.register_save(ImImageFile.format, _save) + +Image.register_extension(ImImageFile.format, ".im") diff --git a/python_modules/PIL/Image.py b/python_modules/PIL/Image.py new file mode 100644 index 000000000..d209405c4 --- /dev/null +++ b/python_modules/PIL/Image.py @@ -0,0 +1,4245 @@ +# +# The Python Imaging Library. +# $Id$ +# +# the Image class wrapper +# +# partial release history: +# 1995-09-09 fl Created +# 1996-03-11 fl PIL release 0.0 (proof of concept) +# 1996-04-30 fl PIL release 0.1b1 +# 1999-07-28 fl PIL release 1.0 final +# 2000-06-07 fl PIL release 1.1 +# 2000-10-20 fl PIL release 1.1.1 +# 2001-05-07 fl PIL release 1.1.2 +# 2002-03-15 fl PIL release 1.1.3 +# 2003-05-10 fl PIL release 1.1.4 +# 2005-03-28 fl PIL release 1.1.5 +# 2006-12-02 fl PIL release 1.1.6 +# 2009-11-15 fl PIL release 1.1.7 +# +# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. +# Copyright (c) 1995-2009 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +from __future__ import annotations + +import abc +import atexit +import builtins +import io +import logging +import math +import os +import re +import struct +import sys +import tempfile +import warnings +from collections.abc import Callable, Iterator, MutableMapping, Sequence +from enum import IntEnum +from types import ModuleType +from typing import IO, Any, Literal, Protocol, cast + +# VERSION was removed in Pillow 6.0.0. +# PILLOW_VERSION was removed in Pillow 9.0.0. +# Use __version__ instead. +from . import ( + ExifTags, + ImageMode, + TiffTags, + UnidentifiedImageError, + __version__, + _plugins, +) +from ._binary import i32le, o32be, o32le +from ._deprecate import deprecate +from ._util import DeferredError, is_path + +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + +logger = logging.getLogger(__name__) + + +class DecompressionBombWarning(RuntimeWarning): + pass + + +class DecompressionBombError(Exception): + pass + + +WARN_POSSIBLE_FORMATS: bool = False + +# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image +MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3) + + +try: + # If the _imaging C module is not present, Pillow will not load. + # Note that other modules should not refer to _imaging directly; + # import Image and use the Image.core variable instead. + # Also note that Image.core is not a publicly documented interface, + # and should be considered private and subject to change. + from . import _imaging as core + + if __version__ != getattr(core, "PILLOW_VERSION", None): + msg = ( + "The _imaging extension was built for another version of Pillow or PIL:\n" + f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n" + f"Pillow version: {__version__}" + ) + raise ImportError(msg) + +except ImportError as v: + core = DeferredError.new(ImportError("The _imaging C module is not installed.")) + # Explanations for ways that we know we might have an import error + if str(v).startswith("Module use of python"): + # The _imaging C module is present, but not compiled for + # the right version (windows only). Print a warning, if + # possible. + warnings.warn( + "The _imaging extension was built for another version of Python.", + RuntimeWarning, + ) + elif str(v).startswith("The _imaging extension"): + warnings.warn(str(v), RuntimeWarning) + # Fail here anyway. Don't let people run with a mostly broken Pillow. + # see docs/porting.rst + raise + + +def isImageType(t: Any) -> TypeGuard[Image]: + """ + Checks if an object is an image object. + + .. warning:: + + This function is for internal use only. + + :param t: object to check if it's an image + :returns: True if the object is an image + """ + deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)") + return hasattr(t, "im") + + +# +# Constants + + +# transpose +class Transpose(IntEnum): + FLIP_LEFT_RIGHT = 0 + FLIP_TOP_BOTTOM = 1 + ROTATE_90 = 2 + ROTATE_180 = 3 + ROTATE_270 = 4 + TRANSPOSE = 5 + TRANSVERSE = 6 + + +# transforms (also defined in Imaging.h) +class Transform(IntEnum): + AFFINE = 0 + EXTENT = 1 + PERSPECTIVE = 2 + QUAD = 3 + MESH = 4 + + +# resampling filters (also defined in Imaging.h) +class Resampling(IntEnum): + NEAREST = 0 + BOX = 4 + BILINEAR = 2 + HAMMING = 5 + BICUBIC = 3 + LANCZOS = 1 + + +_filters_support = { + Resampling.BOX: 0.5, + Resampling.BILINEAR: 1.0, + Resampling.HAMMING: 1.0, + Resampling.BICUBIC: 2.0, + Resampling.LANCZOS: 3.0, +} + + +# dithers +class Dither(IntEnum): + NONE = 0 + ORDERED = 1 # Not yet implemented + RASTERIZE = 2 # Not yet implemented + FLOYDSTEINBERG = 3 # default + + +# palettes/quantizers +class Palette(IntEnum): + WEB = 0 + ADAPTIVE = 1 + + +class Quantize(IntEnum): + MEDIANCUT = 0 + MAXCOVERAGE = 1 + FASTOCTREE = 2 + LIBIMAGEQUANT = 3 + + +module = sys.modules[__name__] +for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): + for item in enum: + setattr(module, item.name, item.value) + + +if hasattr(core, "DEFAULT_STRATEGY"): + DEFAULT_STRATEGY = core.DEFAULT_STRATEGY + FILTERED = core.FILTERED + HUFFMAN_ONLY = core.HUFFMAN_ONLY + RLE = core.RLE + FIXED = core.FIXED + + +# -------------------------------------------------------------------- +# Registries + +TYPE_CHECKING = False +if TYPE_CHECKING: + import mmap + from xml.etree.ElementTree import Element + + from IPython.lib.pretty import PrettyPrinter + + from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin + from ._typing import CapsuleType, NumpyArray, StrOrBytesPath, TypeGuard +ID: list[str] = [] +OPEN: dict[ + str, + tuple[ + Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + Callable[[bytes], bool | str] | None, + ], +] = {} +MIME: dict[str, str] = {} +SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +EXTENSION: dict[str, str] = {} +DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} +ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} + +# -------------------------------------------------------------------- +# Modes + +_ENDIAN = "<" if sys.byteorder == "little" else ">" + + +def _conv_type_shape(im: Image) -> tuple[tuple[int, ...], str]: + m = ImageMode.getmode(im.mode) + shape: tuple[int, ...] = (im.height, im.width) + extra = len(m.bands) + if extra != 1: + shape += (extra,) + return shape, m.typestr + + +MODES = [ + "1", + "CMYK", + "F", + "HSV", + "I", + "I;16", + "I;16B", + "I;16L", + "I;16N", + "L", + "LA", + "La", + "LAB", + "P", + "PA", + "RGB", + "RGBA", + "RGBa", + "RGBX", + "YCbCr", +] + +# raw modes that may be memory mapped. NOTE: if you change this, you +# may have to modify the stride calculation in map.c too! +_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") + + +def getmodebase(mode: str) -> str: + """ + Gets the "base" mode for given mode. This function returns "L" for + images that contain grayscale data, and "RGB" for images that + contain color data. + + :param mode: Input mode. + :returns: "L" or "RGB". + :exception KeyError: If the input mode was not a standard mode. + """ + return ImageMode.getmode(mode).basemode + + +def getmodetype(mode: str) -> str: + """ + Gets the storage type mode. Given a mode, this function returns a + single-layer mode suitable for storing individual bands. + + :param mode: Input mode. + :returns: "L", "I", or "F". + :exception KeyError: If the input mode was not a standard mode. + """ + return ImageMode.getmode(mode).basetype + + +def getmodebandnames(mode: str) -> tuple[str, ...]: + """ + Gets a list of individual band names. Given a mode, this function returns + a tuple containing the names of individual bands (use + :py:method:`~PIL.Image.getmodetype` to get the mode used to store each + individual band. + + :param mode: Input mode. + :returns: A tuple containing band names. The length of the tuple + gives the number of bands in an image of the given mode. + :exception KeyError: If the input mode was not a standard mode. + """ + return ImageMode.getmode(mode).bands + + +def getmodebands(mode: str) -> int: + """ + Gets the number of individual bands for this mode. + + :param mode: Input mode. + :returns: The number of bands in this mode. + :exception KeyError: If the input mode was not a standard mode. + """ + return len(ImageMode.getmode(mode).bands) + + +# -------------------------------------------------------------------- +# Helpers + +_initialized = 0 + + +def preinit() -> None: + """ + Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers. + + It is called when opening or saving images. + """ + + global _initialized + if _initialized >= 1: + return + + try: + from . import BmpImagePlugin + + assert BmpImagePlugin + except ImportError: + pass + try: + from . import GifImagePlugin + + assert GifImagePlugin + except ImportError: + pass + try: + from . import JpegImagePlugin + + assert JpegImagePlugin + except ImportError: + pass + try: + from . import PpmImagePlugin + + assert PpmImagePlugin + except ImportError: + pass + try: + from . import PngImagePlugin + + assert PngImagePlugin + except ImportError: + pass + + _initialized = 1 + + +def init() -> bool: + """ + Explicitly initializes the Python Imaging Library. This function + loads all available file format drivers. + + It is called when opening or saving images if :py:meth:`~preinit()` is + insufficient, and by :py:meth:`~PIL.features.pilinfo`. + """ + + global _initialized + if _initialized >= 2: + return False + + parent_name = __name__.rpartition(".")[0] + for plugin in _plugins: + try: + logger.debug("Importing %s", plugin) + __import__(f"{parent_name}.{plugin}", globals(), locals(), []) + except ImportError as e: + logger.debug("Image: failed to import %s: %s", plugin, e) + + if OPEN or SAVE: + _initialized = 2 + return True + return False + + +# -------------------------------------------------------------------- +# Codec factories (used by tobytes/frombytes and ImageFile.load) + + +def _getdecoder( + mode: str, decoder_name: str, args: Any, extra: tuple[Any, ...] = () +) -> core.ImagingDecoder | ImageFile.PyDecoder: + # tweak arguments + if args is None: + args = () + elif not isinstance(args, tuple): + args = (args,) + + try: + decoder = DECODERS[decoder_name] + except KeyError: + pass + else: + return decoder(mode, *args + extra) + + try: + # get decoder + decoder = getattr(core, f"{decoder_name}_decoder") + except AttributeError as e: + msg = f"decoder {decoder_name} not available" + raise OSError(msg) from e + return decoder(mode, *args + extra) + + +def _getencoder( + mode: str, encoder_name: str, args: Any, extra: tuple[Any, ...] = () +) -> core.ImagingEncoder | ImageFile.PyEncoder: + # tweak arguments + if args is None: + args = () + elif not isinstance(args, tuple): + args = (args,) + + try: + encoder = ENCODERS[encoder_name] + except KeyError: + pass + else: + return encoder(mode, *args + extra) + + try: + # get encoder + encoder = getattr(core, f"{encoder_name}_encoder") + except AttributeError as e: + msg = f"encoder {encoder_name} not available" + raise OSError(msg) from e + return encoder(mode, *args + extra) + + +# -------------------------------------------------------------------- +# Simple expression analyzer + + +class ImagePointTransform: + """ + Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than + 8 bits, this represents an affine transformation, where the value is multiplied by + ``scale`` and ``offset`` is added. + """ + + def __init__(self, scale: float, offset: float) -> None: + self.scale = scale + self.offset = offset + + def __neg__(self) -> ImagePointTransform: + return ImagePointTransform(-self.scale, -self.offset) + + def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): + return ImagePointTransform( + self.scale + other.scale, self.offset + other.offset + ) + return ImagePointTransform(self.scale, self.offset + other) + + __radd__ = __add__ + + def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform: + return self + -other + + def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform: + return other + -self + + def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): + return NotImplemented + return ImagePointTransform(self.scale * other, self.offset * other) + + __rmul__ = __mul__ + + def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): + return NotImplemented + return ImagePointTransform(self.scale / other, self.offset / other) + + +def _getscaleoffset( + expr: Callable[[ImagePointTransform], ImagePointTransform | float], +) -> tuple[float, float]: + a = expr(ImagePointTransform(1, 0)) + return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) + + +# -------------------------------------------------------------------- +# Implementation wrapper + + +class SupportsGetData(Protocol): + def getdata( + self, + ) -> tuple[Transform, Sequence[int]]: ... + + +class Image: + """ + This class represents an image object. To create + :py:class:`~PIL.Image.Image` objects, use the appropriate factory + functions. There's hardly ever any reason to call the Image constructor + directly. + + * :py:func:`~PIL.Image.open` + * :py:func:`~PIL.Image.new` + * :py:func:`~PIL.Image.frombytes` + """ + + format: str | None = None + format_description: str | None = None + _close_exclusive_fp_after_loading = True + + def __init__(self) -> None: + # FIXME: take "new" parameters / other image? + self._im: core.ImagingCore | DeferredError | None = None + self._mode = "" + self._size = (0, 0) + self.palette: ImagePalette.ImagePalette | None = None + self.info: dict[str | tuple[int, int], Any] = {} + self.readonly = 0 + self._exif: Exif | None = None + + @property + def im(self) -> core.ImagingCore: + if isinstance(self._im, DeferredError): + raise self._im.ex + assert self._im is not None + return self._im + + @im.setter + def im(self, im: core.ImagingCore) -> None: + self._im = im + + @property + def width(self) -> int: + return self.size[0] + + @property + def height(self) -> int: + return self.size[1] + + @property + def size(self) -> tuple[int, int]: + return self._size + + @property + def mode(self) -> str: + return self._mode + + @property + def readonly(self) -> int: + return (self._im and self._im.readonly) or self._readonly + + @readonly.setter + def readonly(self, readonly: int) -> None: + self._readonly = readonly + + def _new(self, im: core.ImagingCore) -> Image: + new = Image() + new.im = im + new._mode = im.mode + new._size = im.size + if im.mode in ("P", "PA"): + if self.palette: + new.palette = self.palette.copy() + else: + from . import ImagePalette + + new.palette = ImagePalette.ImagePalette() + new.info = self.info.copy() + return new + + # Context manager support + def __enter__(self): + return self + + def __exit__(self, *args): + from . import ImageFile + + if isinstance(self, ImageFile.ImageFile): + if getattr(self, "_exclusive_fp", False): + self._close_fp() + self.fp = None + + def close(self) -> None: + """ + This operation will destroy the image core and release its memory. + The image data will be unusable afterward. + + This function is required to close images that have multiple frames or + have not had their file read and closed by the + :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for + more information. + """ + if getattr(self, "map", None): + if sys.platform == "win32" and hasattr(sys, "pypy_version_info"): + self.map.close() + self.map: mmap.mmap | None = None + + # Instead of simply setting to None, we're setting up a + # deferred error that will better explain that the core image + # object is gone. + self._im = DeferredError(ValueError("Operation on closed image")) + + def _copy(self) -> None: + self.load() + self.im = self.im.copy() + self.readonly = 0 + + def _ensure_mutable(self) -> None: + if self.readonly: + self._copy() + else: + self.load() + + def _dump( + self, file: str | None = None, format: str | None = None, **options: Any + ) -> str: + suffix = "" + if format: + suffix = f".{format}" + + if not file: + f, filename = tempfile.mkstemp(suffix) + os.close(f) + else: + filename = file + if not filename.endswith(suffix): + filename = filename + suffix + + self.load() + + if not format or format == "PPM": + self.im.save_ppm(filename) + else: + self.save(filename, format, **options) + + return filename + + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return False + assert isinstance(other, Image) + return ( + self.mode == other.mode + and self.size == other.size + and self.info == other.info + and self.getpalette() == other.getpalette() + and self.tobytes() == other.tobytes() + ) + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"image mode={self.mode} size={self.size[0]}x{self.size[1]} " + f"at 0x{id(self):X}>" + ) + + def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: + """IPython plain text display support""" + + # Same as __repr__ but without unpredictable id(self), + # to keep Jupyter notebook `text/plain` output stable. + p.text( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>" + ) + + def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: + """Helper function for iPython display hook. + + :param image_format: Image format. + :returns: image as bytes, saved into the given format. + """ + b = io.BytesIO() + try: + self.save(b, image_format, **kwargs) + except Exception: + return None + return b.getvalue() + + def _repr_png_(self) -> bytes | None: + """iPython display hook support for PNG format. + + :returns: PNG version of the image as bytes + """ + return self._repr_image("PNG", compress_level=1) + + def _repr_jpeg_(self) -> bytes | None: + """iPython display hook support for JPEG format. + + :returns: JPEG version of the image as bytes + """ + return self._repr_image("JPEG") + + @property + def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: + # numpy array interface support + new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3} + if self.mode == "1": + # Binary images need to be extended from bits to bytes + # See: https://github.com/python-pillow/Pillow/issues/350 + new["data"] = self.tobytes("raw", "L") + else: + new["data"] = self.tobytes() + new["shape"], new["typestr"] = _conv_type_shape(self) + return new + + def __arrow_c_schema__(self) -> object: + self.load() + return self.im.__arrow_c_schema__() + + def __arrow_c_array__( + self, requested_schema: object | None = None + ) -> tuple[object, object]: + self.load() + return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__()) + + def __getstate__(self) -> list[Any]: + im_data = self.tobytes() # load image first + return [self.info, self.mode, self.size, self.getpalette(), im_data] + + def __setstate__(self, state: list[Any]) -> None: + Image.__init__(self) + info, mode, size, palette, data = state[:5] + self.info = info + self._mode = mode + self._size = size + self.im = core.new(mode, size) + if mode in ("L", "LA", "P", "PA") and palette: + self.putpalette(palette) + self.frombytes(data) + + def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes: + """ + Return image as a bytes object. + + .. warning:: + + This method returns raw image data derived from Pillow's internal + storage. For compressed image data (e.g. PNG, JPEG) use + :meth:`~.save`, with a BytesIO parameter for in-memory data. + + :param encoder_name: What encoder to use. + + The default is to use the standard "raw" encoder. + To see how this packs pixel data into the returned + bytes, see :file:`libImaging/Pack.c`. + + A list of C encoders can be seen under codecs + section of the function array in + :file:`_imaging.c`. Python encoders are registered + within the relevant plugins. + :param args: Extra arguments to the encoder. + :returns: A :py:class:`bytes` object. + """ + + encoder_args: Any = args + if len(encoder_args) == 1 and isinstance(encoder_args[0], tuple): + # may pass tuple instead of argument list + encoder_args = encoder_args[0] + + if encoder_name == "raw" and encoder_args == (): + encoder_args = self.mode + + self.load() + + if self.width == 0 or self.height == 0: + return b"" + + # unpack data + e = _getencoder(self.mode, encoder_name, encoder_args) + e.setimage(self.im) + + from . import ImageFile + + bufsize = max(ImageFile.MAXBLOCK, self.size[0] * 4) # see RawEncode.c + + output = [] + while True: + bytes_consumed, errcode, data = e.encode(bufsize) + output.append(data) + if errcode: + break + if errcode < 0: + msg = f"encoder error {errcode} in tobytes" + raise RuntimeError(msg) + + return b"".join(output) + + def tobitmap(self, name: str = "image") -> bytes: + """ + Returns the image converted to an X11 bitmap. + + .. note:: This method only works for mode "1" images. + + :param name: The name prefix to use for the bitmap variables. + :returns: A string containing an X11 bitmap. + :raises ValueError: If the mode is not "1" + """ + + self.load() + if self.mode != "1": + msg = "not a bitmap" + raise ValueError(msg) + data = self.tobytes("xbm") + return b"".join( + [ + f"#define {name}_width {self.size[0]}\n".encode("ascii"), + f"#define {name}_height {self.size[1]}\n".encode("ascii"), + f"static char {name}_bits[] = {{\n".encode("ascii"), + data, + b"};", + ] + ) + + def frombytes( + self, + data: bytes | bytearray | SupportsArrayInterface, + decoder_name: str = "raw", + *args: Any, + ) -> None: + """ + Loads this image with pixel data from a bytes object. + + This method is similar to the :py:func:`~PIL.Image.frombytes` function, + but loads data into this image instead of creating a new image object. + """ + + if self.width == 0 or self.height == 0: + return + + decoder_args: Any = args + if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple): + # may pass tuple instead of argument list + decoder_args = decoder_args[0] + + # default format + if decoder_name == "raw" and decoder_args == (): + decoder_args = self.mode + + # unpack data + d = _getdecoder(self.mode, decoder_name, decoder_args) + d.setimage(self.im) + s = d.decode(data) + + if s[0] >= 0: + msg = "not enough image data" + raise ValueError(msg) + if s[1] != 0: + msg = "cannot decode image data" + raise ValueError(msg) + + def load(self) -> core.PixelAccess | None: + """ + Allocates storage for the image and loads the pixel data. In + normal cases, you don't need to call this method, since the + Image class automatically loads an opened image when it is + accessed for the first time. + + If the file associated with the image was opened by Pillow, then this + method will close it. The exception to this is if the image has + multiple frames, in which case the file will be left open for seek + operations. See :ref:`file-handling` for more information. + + :returns: An image access object. + :rtype: :py:class:`.PixelAccess` + """ + if self._im is not None and self.palette and self.palette.dirty: + # realize palette + mode, arr = self.palette.getdata() + self.im.putpalette(self.palette.mode, mode, arr) + self.palette.dirty = 0 + self.palette.rawmode = None + if "transparency" in self.info and mode in ("LA", "PA"): + if isinstance(self.info["transparency"], int): + self.im.putpalettealpha(self.info["transparency"], 0) + else: + self.im.putpalettealphas(self.info["transparency"]) + self.palette.mode = "RGBA" + else: + self.palette.palette = self.im.getpalette( + self.palette.mode, self.palette.mode + ) + + if self._im is not None: + return self.im.pixel_access(self.readonly) + return None + + def verify(self) -> None: + """ + Verifies the contents of a file. For data read from a file, this + method attempts to determine if the file is broken, without + actually decoding the image data. If this method finds any + problems, it raises suitable exceptions. If you need to load + the image after using this method, you must reopen the image + file. + """ + pass + + def convert( + self, + mode: str | None = None, + matrix: tuple[float, ...] | None = None, + dither: Dither | None = None, + palette: Palette = Palette.WEB, + colors: int = 256, + ) -> Image: + """ + Returns a converted copy of this image. For the "P" mode, this + method translates pixels through the palette. If mode is + omitted, a mode is chosen so that all information in the image + and the palette can be represented without a palette. + + This supports all possible conversions between "L", "RGB" and "CMYK". The + ``matrix`` argument only supports "L" and "RGB". + + When translating a color image to grayscale (mode "L"), + the library uses the ITU-R 601-2 luma transform:: + + L = R * 299/1000 + G * 587/1000 + B * 114/1000 + + The default method of converting a grayscale ("L") or "RGB" + image into a bilevel (mode "1") image uses Floyd-Steinberg + dither to approximate the original image luminosity levels. If + dither is ``None``, all values larger than 127 are set to 255 (white), + all other values to 0 (black). To use other thresholds, use the + :py:meth:`~PIL.Image.Image.point` method. + + When converting from "RGBA" to "P" without a ``matrix`` argument, + this passes the operation to :py:meth:`~PIL.Image.Image.quantize`, + and ``dither`` and ``palette`` are ignored. + + When converting from "PA", if an "RGBA" palette is present, the alpha + channel from the image will be used instead of the values from the palette. + + :param mode: The requested mode. See: :ref:`concept-modes`. + :param matrix: An optional conversion matrix. If given, this + should be 4- or 12-tuple containing floating point values. + :param dither: Dithering method, used when converting from + mode "RGB" to "P" or from "RGB" or "L" to "1". + Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` + (default). Note that this is not used when ``matrix`` is supplied. + :param palette: Palette to use when converting from mode "RGB" + to "P". Available palettes are :data:`Palette.WEB` or + :data:`Palette.ADAPTIVE`. + :param colors: Number of colors to use for the :data:`Palette.ADAPTIVE` + palette. Defaults to 256. + :rtype: :py:class:`~PIL.Image.Image` + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + + self.load() + + has_transparency = "transparency" in self.info + if not mode and self.mode == "P": + # determine default mode + if self.palette: + mode = self.palette.mode + else: + mode = "RGB" + if mode == "RGB" and has_transparency: + mode = "RGBA" + if not mode or (mode == self.mode and not matrix): + return self.copy() + + if matrix: + # matrix conversion + if mode not in ("L", "RGB"): + msg = "illegal conversion" + raise ValueError(msg) + im = self.im.convert_matrix(mode, matrix) + new_im = self._new(im) + if has_transparency and self.im.bands == 3: + transparency = new_im.info["transparency"] + + def convert_transparency( + m: tuple[float, ...], v: tuple[int, int, int] + ) -> int: + value = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 + return max(0, min(255, int(value))) + + if mode == "L": + transparency = convert_transparency(matrix, transparency) + elif len(mode) == 3: + transparency = tuple( + convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) + for i in range(len(transparency)) + ) + new_im.info["transparency"] = transparency + return new_im + + if mode == "P" and self.mode == "RGBA": + return self.quantize(colors) + + trns = None + delete_trns = False + # transparency handling + if has_transparency: + if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or ( + self.mode == "RGB" and mode in ("La", "LA", "RGBa", "RGBA") + ): + # Use transparent conversion to promote from transparent + # color to an alpha channel. + new_im = self._new( + self.im.convert_transparent(mode, self.info["transparency"]) + ) + del new_im.info["transparency"] + return new_im + elif self.mode in ("L", "RGB", "P") and mode in ("L", "RGB", "P"): + t = self.info["transparency"] + if isinstance(t, bytes): + # Dragons. This can't be represented by a single color + warnings.warn( + "Palette images with Transparency expressed in bytes should be " + "converted to RGBA images" + ) + delete_trns = True + else: + # get the new transparency color. + # use existing conversions + trns_im = new(self.mode, (1, 1)) + if self.mode == "P": + assert self.palette is not None + trns_im.putpalette(self.palette, self.palette.mode) + if isinstance(t, tuple): + err = "Couldn't allocate a palette color for transparency" + assert trns_im.palette is not None + try: + t = trns_im.palette.getcolor(t, self) + except ValueError as e: + if str(e) == "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for transparency + t = None + else: + raise ValueError(err) from e + if t is None: + trns = None + else: + trns_im.putpixel((0, 0), t) + + if mode in ("L", "RGB"): + trns_im = trns_im.convert(mode) + else: + # can't just retrieve the palette number, got to do it + # after quantization. + trns_im = trns_im.convert("RGB") + trns = trns_im.getpixel((0, 0)) + + elif self.mode == "P" and mode in ("LA", "PA", "RGBA"): + t = self.info["transparency"] + delete_trns = True + + if isinstance(t, bytes): + self.im.putpalettealphas(t) + elif isinstance(t, int): + self.im.putpalettealpha(t, 0) + else: + msg = "Transparency for P mode should be bytes or int" + raise ValueError(msg) + + if mode == "P" and palette == Palette.ADAPTIVE: + im = self.im.quantize(colors) + new_im = self._new(im) + from . import ImagePalette + + new_im.palette = ImagePalette.ImagePalette( + "RGB", new_im.im.getpalette("RGB") + ) + if delete_trns: + # This could possibly happen if we requantize to fewer colors. + # The transparency would be totally off in that case. + del new_im.info["transparency"] + if trns is not None: + try: + new_im.info["transparency"] = new_im.palette.getcolor( + cast(tuple[int, ...], trns), # trns was converted to RGB + new_im, + ) + except Exception: + # if we can't make a transparent color, don't leave the old + # transparency hanging around to mess us up. + del new_im.info["transparency"] + warnings.warn("Couldn't allocate palette entry for transparency") + return new_im + + if "LAB" in (self.mode, mode): + im = self + if mode == "LAB": + if im.mode not in ("RGB", "RGBA", "RGBX"): + im = im.convert("RGBA") + other_mode = im.mode + else: + other_mode = mode + if other_mode in ("RGB", "RGBA", "RGBX"): + from . import ImageCms + + srgb = ImageCms.createProfile("sRGB") + lab = ImageCms.createProfile("LAB") + profiles = [lab, srgb] if im.mode == "LAB" else [srgb, lab] + transform = ImageCms.buildTransform( + profiles[0], profiles[1], im.mode, mode + ) + return transform.apply(im) + + # colorspace conversion + if dither is None: + dither = Dither.FLOYDSTEINBERG + + try: + im = self.im.convert(mode, dither) + except ValueError: + try: + # normalize source image and try again + modebase = getmodebase(self.mode) + if modebase == self.mode: + raise + im = self.im.convert(modebase) + im = im.convert(mode, dither) + except KeyError as e: + msg = "illegal conversion" + raise ValueError(msg) from e + + new_im = self._new(im) + if mode == "P" and palette != Palette.ADAPTIVE: + from . import ImagePalette + + new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB")) + if delete_trns: + # crash fail if we leave a bytes transparency in an rgb/l mode. + del new_im.info["transparency"] + if trns is not None: + if new_im.mode == "P" and new_im.palette: + try: + new_im.info["transparency"] = new_im.palette.getcolor( + cast(tuple[int, ...], trns), new_im # trns was converted to RGB + ) + except ValueError as e: + del new_im.info["transparency"] + if str(e) != "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for transparency + warnings.warn( + "Couldn't allocate palette entry for transparency" + ) + else: + new_im.info["transparency"] = trns + return new_im + + def quantize( + self, + colors: int = 256, + method: int | None = None, + kmeans: int = 0, + palette: Image | None = None, + dither: Dither = Dither.FLOYDSTEINBERG, + ) -> Image: + """ + Convert the image to 'P' mode with the specified number + of colors. + + :param colors: The desired number of colors, <= 256 + :param method: :data:`Quantize.MEDIANCUT` (median cut), + :data:`Quantize.MAXCOVERAGE` (maximum coverage), + :data:`Quantize.FASTOCTREE` (fast octree), + :data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support + using :py:func:`PIL.features.check_feature` with + ``feature="libimagequant"``). + + By default, :data:`Quantize.MEDIANCUT` will be used. + + The exception to this is RGBA images. :data:`Quantize.MEDIANCUT` + and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so + :data:`Quantize.FASTOCTREE` is used by default instead. + :param kmeans: Integer greater than or equal to zero. + :param palette: Quantize to the palette of given + :py:class:`PIL.Image.Image`. + :param dither: Dithering method, used when converting from + mode "RGB" to "P" or from "RGB" or "L" to "1". + Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` + (default). + :returns: A new image + """ + + self.load() + + if method is None: + # defaults: + method = Quantize.MEDIANCUT + if self.mode == "RGBA": + method = Quantize.FASTOCTREE + + if self.mode == "RGBA" and method not in ( + Quantize.FASTOCTREE, + Quantize.LIBIMAGEQUANT, + ): + # Caller specified an invalid mode. + msg = ( + "Fast Octree (method == 2) and libimagequant (method == 3) " + "are the only valid methods for quantizing RGBA images" + ) + raise ValueError(msg) + + if palette: + # use palette from reference image + palette.load() + if palette.mode != "P": + msg = "bad mode for palette image" + raise ValueError(msg) + if self.mode not in {"RGB", "L"}: + msg = "only RGB or L mode images can be quantized to a palette" + raise ValueError(msg) + im = self.im.convert("P", dither, palette.im) + new_im = self._new(im) + assert palette.palette is not None + new_im.palette = palette.palette.copy() + return new_im + + if kmeans < 0: + msg = "kmeans must not be negative" + raise ValueError(msg) + + im = self._new(self.im.quantize(colors, method, kmeans)) + + from . import ImagePalette + + mode = im.im.getpalettemode() + palette_data = im.im.getpalette(mode, mode)[: colors * len(mode)] + im.palette = ImagePalette.ImagePalette(mode, palette_data) + + return im + + def copy(self) -> Image: + """ + Copies this image. Use this method if you wish to paste things + into an image, but still retain the original. + + :rtype: :py:class:`~PIL.Image.Image` + :returns: An :py:class:`~PIL.Image.Image` object. + """ + self.load() + return self._new(self.im.copy()) + + __copy__ = copy + + def crop(self, box: tuple[float, float, float, float] | None = None) -> Image: + """ + Returns a rectangular region from this image. The box is a + 4-tuple defining the left, upper, right, and lower pixel + coordinate. See :ref:`coordinate-system`. + + Note: Prior to Pillow 3.4.0, this was a lazy operation. + + :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. + :rtype: :py:class:`~PIL.Image.Image` + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if box is None: + return self.copy() + + if box[2] < box[0]: + msg = "Coordinate 'right' is less than 'left'" + raise ValueError(msg) + elif box[3] < box[1]: + msg = "Coordinate 'lower' is less than 'upper'" + raise ValueError(msg) + + self.load() + return self._new(self._crop(self.im, box)) + + def _crop( + self, im: core.ImagingCore, box: tuple[float, float, float, float] + ) -> core.ImagingCore: + """ + Returns a rectangular region from the core image object im. + + This is equivalent to calling im.crop((x0, y0, x1, y1)), but + includes additional sanity checks. + + :param im: a core image object + :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. + :returns: A core image object. + """ + + x0, y0, x1, y1 = map(int, map(round, box)) + + absolute_values = (abs(x1 - x0), abs(y1 - y0)) + + _decompression_bomb_check(absolute_values) + + return im.crop((x0, y0, x1, y1)) + + def draft( + self, mode: str | None, size: tuple[int, int] | None + ) -> tuple[str, tuple[int, int, float, float]] | None: + """ + Configures the image file loader so it returns a version of the + image that as closely as possible matches the given mode and + size. For example, you can use this method to convert a color + JPEG to grayscale while loading it. + + If any changes are made, returns a tuple with the chosen ``mode`` and + ``box`` with coordinates of the original image within the altered one. + + Note that this method modifies the :py:class:`~PIL.Image.Image` object + in place. If the image has already been loaded, this method has no + effect. + + Note: This method is not implemented for most images. It is + currently implemented only for JPEG and MPO images. + + :param mode: The requested mode. + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + """ + pass + + def _expand(self, xmargin: int, ymargin: int | None = None) -> Image: + if ymargin is None: + ymargin = xmargin + self.load() + return self._new(self.im.expand(xmargin, ymargin)) + + def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: + """ + Filters this image using the given filter. For a list of + available filters, see the :py:mod:`~PIL.ImageFilter` module. + + :param filter: Filter kernel. + :returns: An :py:class:`~PIL.Image.Image` object.""" + + from . import ImageFilter + + self.load() + + if callable(filter): + filter = filter() + if not hasattr(filter, "filter"): + msg = "filter argument should be ImageFilter.Filter instance or class" + raise TypeError(msg) + + multiband = isinstance(filter, ImageFilter.MultibandFilter) + if self.im.bands == 1 or multiband: + return self._new(filter.filter(self.im)) + + ims = [ + self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands) + ] + return merge(self.mode, ims) + + def getbands(self) -> tuple[str, ...]: + """ + Returns a tuple containing the name of each band in this image. + For example, ``getbands`` on an RGB image returns ("R", "G", "B"). + + :returns: A tuple containing band names. + :rtype: tuple + """ + return ImageMode.getmode(self.mode).bands + + def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | None: + """ + Calculates the bounding box of the non-zero regions in the + image. + + :param alpha_only: Optional flag, defaulting to ``True``. + If ``True`` and the image has an alpha channel, trim transparent pixels. + Otherwise, trim pixels when all channels are zero. + Keyword-only argument. + :returns: The bounding box is returned as a 4-tuple defining the + left, upper, right, and lower pixel coordinate. See + :ref:`coordinate-system`. If the image is completely empty, this + method returns None. + + """ + + self.load() + return self.im.getbbox(alpha_only) + + def getcolors( + self, maxcolors: int = 256 + ) -> list[tuple[int, tuple[int, ...]]] | list[tuple[int, float]] | None: + """ + Returns a list of colors used in this image. + + The colors will be in the image's mode. For example, an RGB image will + return a tuple of (red, green, blue) color values, and a P image will + return the index of the color in the palette. + + :param maxcolors: Maximum number of colors. If this number is + exceeded, this method returns None. The default limit is + 256 colors. + :returns: An unsorted list of (count, pixel) values. + """ + + self.load() + if self.mode in ("1", "L", "P"): + h = self.im.histogram() + out: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]] + if len(out) > maxcolors: + return None + return out + return self.im.getcolors(maxcolors) + + def getdata(self, band: int | None = None) -> core.ImagingCore: + """ + Returns the contents of this image as a sequence object + containing pixel values. The sequence object is flattened, so + that values for line one follow directly after the values of + line zero, and so on. + + Note that the sequence object returned by this method is an + internal PIL data type, which only supports certain sequence + operations. To convert it to an ordinary sequence (e.g. for + printing), use ``list(im.getdata())``. + + :param band: What band to return. The default is to return + all bands. To return a single band, pass in the index + value (e.g. 0 to get the "R" band from an "RGB" image). + :returns: A sequence-like object. + """ + + self.load() + if band is not None: + return self.im.getband(band) + return self.im # could be abused + + def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: + """ + Gets the minimum and maximum pixel values for each band in + the image. + + :returns: For a single-band image, a 2-tuple containing the + minimum and maximum pixel value. For a multi-band image, + a tuple containing one 2-tuple for each band. + """ + + self.load() + if self.im.bands > 1: + return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) + return self.im.getextrema() + + def getxmp(self) -> dict[str, Any]: + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ + + def get_name(tag: str) -> str: + return re.sub("^{[^}]+}", "", tag) + + def get_value(element: Element) -> str | dict[str, Any] | None: + value: dict[str, Any] = {get_name(k): v for k, v in element.attrib.items()} + children = list(element) + if children: + for child in children: + name = get_name(child.tag) + child_value = get_value(child) + if name in value: + if not isinstance(value[name], list): + value[name] = [value[name]] + value[name].append(child_value) + else: + value[name] = child_value + elif value: + if element.text: + value["text"] = element.text + else: + return element.text + return value + + if ElementTree is None: + warnings.warn("XMP data cannot be read without defusedxml dependency") + return {} + if "xmp" not in self.info: + return {} + root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 ")) + return {get_name(root.tag): get_value(root)} + + def getexif(self) -> Exif: + """ + Gets EXIF data from the image. + + :returns: an :py:class:`~PIL.Image.Exif` object. + """ + if self._exif is None: + self._exif = Exif() + elif self._exif._loaded: + return self._exif + self._exif._loaded = True + + exif_info = self.info.get("exif") + if exif_info is None: + if "Raw profile type exif" in self.info: + exif_info = bytes.fromhex( + "".join(self.info["Raw profile type exif"].split("\n")[3:]) + ) + elif hasattr(self, "tag_v2"): + self._exif.bigtiff = self.tag_v2._bigtiff + self._exif.endian = self.tag_v2._endian + self._exif.load_from_fp(self.fp, self.tag_v2._offset) + if exif_info is not None: + self._exif.load(exif_info) + + # XMP tags + if ExifTags.Base.Orientation not in self._exif: + xmp_tags = self.info.get("XML:com.adobe.xmp") + pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])' + if not xmp_tags and (xmp_tags := self.info.get("xmp")): + pattern = rb'tiff:Orientation(="|>)([0-9])' + if xmp_tags: + match = re.search(pattern, xmp_tags) + if match: + self._exif[ExifTags.Base.Orientation] = int(match[2]) + + return self._exif + + def _reload_exif(self) -> None: + if self._exif is None or not self._exif._loaded: + return + self._exif._loaded = False + self.getexif() + + def get_child_images(self) -> list[ImageFile.ImageFile]: + from . import ImageFile + + deprecate("Image.Image.get_child_images", 13) + return ImageFile.ImageFile.get_child_images(self) # type: ignore[arg-type] + + def getim(self) -> CapsuleType: + """ + Returns a capsule that points to the internal image memory. + + :returns: A capsule object. + """ + + self.load() + return self.im.ptr + + def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: + """ + Returns the image palette as a list. + + :param rawmode: The mode in which to return the palette. ``None`` will + return the palette in its current mode. + + .. versionadded:: 9.1.0 + + :returns: A list of color values [r, g, b, ...], or None if the + image has no palette. + """ + + self.load() + try: + mode = self.im.getpalettemode() + except ValueError: + return None # no palette + if rawmode is None: + rawmode = mode + return list(self.im.getpalette(mode, rawmode)) + + @property + def has_transparency_data(self) -> bool: + """ + Determine if an image has transparency data, whether in the form of an + alpha channel, a palette with an alpha channel, or a "transparency" key + in the info dictionary. + + Note the image might still appear solid, if all of the values shown + within are opaque. + + :returns: A boolean. + """ + if ( + self.mode in ("LA", "La", "PA", "RGBA", "RGBa") + or "transparency" in self.info + ): + return True + if self.mode == "P": + assert self.palette is not None + return self.palette.mode.endswith("A") + return False + + def apply_transparency(self) -> None: + """ + If a P mode image has a "transparency" key in the info dictionary, + remove the key and instead apply the transparency to the palette. + Otherwise, the image is unchanged. + """ + if self.mode != "P" or "transparency" not in self.info: + return + + from . import ImagePalette + + palette = self.getpalette("RGBA") + assert palette is not None + transparency = self.info["transparency"] + if isinstance(transparency, bytes): + for i, alpha in enumerate(transparency): + palette[i * 4 + 3] = alpha + else: + palette[transparency * 4 + 3] = 0 + self.palette = ImagePalette.ImagePalette("RGBA", bytes(palette)) + self.palette.dirty = 1 + + del self.info["transparency"] + + def getpixel( + self, xy: tuple[int, int] | list[int] + ) -> float | tuple[int, ...] | None: + """ + Returns the pixel value at a given position. + + :param xy: The coordinate, given as (x, y). See + :ref:`coordinate-system`. + :returns: The pixel value. If the image is a multi-layer image, + this method returns a tuple. + """ + + self.load() + return self.im.getpixel(tuple(xy)) + + def getprojection(self) -> tuple[list[int], list[int]]: + """ + Get projection to x and y axes + + :returns: Two sequences, indicating where there are non-zero + pixels along the X-axis and the Y-axis, respectively. + """ + + self.load() + x, y = self.im.getprojection() + return list(x), list(y) + + def histogram( + self, mask: Image | None = None, extrema: tuple[float, float] | None = None + ) -> list[int]: + """ + Returns a histogram for the image. The histogram is returned as a + list of pixel counts, one for each pixel value in the source + image. Counts are grouped into 256 bins for each band, even if + the image has more than 8 bits per band. If the image has more + than one band, the histograms for all bands are concatenated (for + example, the histogram for an "RGB" image contains 768 values). + + A bilevel image (mode "1") is treated as a grayscale ("L") image + by this method. + + If a mask is provided, the method returns a histogram for those + parts of the image where the mask image is non-zero. The mask + image must have the same size as the image, and be either a + bi-level image (mode "1") or a grayscale image ("L"). + + :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. + :returns: A list containing pixel counts. + """ + self.load() + if mask: + mask.load() + return self.im.histogram((0, 0), mask.im) + if self.mode in ("I", "F"): + return self.im.histogram( + extrema if extrema is not None else self.getextrema() + ) + return self.im.histogram() + + def entropy( + self, mask: Image | None = None, extrema: tuple[float, float] | None = None + ) -> float: + """ + Calculates and returns the entropy for the image. + + A bilevel image (mode "1") is treated as a grayscale ("L") + image by this method. + + If a mask is provided, the method employs the histogram for + those parts of the image where the mask image is non-zero. + The mask image must have the same size as the image, and be + either a bi-level image (mode "1") or a grayscale image ("L"). + + :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. + :returns: A float value representing the image entropy + """ + self.load() + if mask: + mask.load() + return self.im.entropy((0, 0), mask.im) + if self.mode in ("I", "F"): + return self.im.entropy( + extrema if extrema is not None else self.getextrema() + ) + return self.im.entropy() + + def paste( + self, + im: Image | str | float | tuple[float, ...], + box: Image | tuple[int, int, int, int] | tuple[int, int] | None = None, + mask: Image | None = None, + ) -> None: + """ + Pastes another image into this image. The box argument is either + a 2-tuple giving the upper left corner, a 4-tuple defining the + left, upper, right, and lower pixel coordinate, or None (same as + (0, 0)). See :ref:`coordinate-system`. If a 4-tuple is given, the size + of the pasted image must match the size of the region. + + If the modes don't match, the pasted image is converted to the mode of + this image (see the :py:meth:`~PIL.Image.Image.convert` method for + details). + + Instead of an image, the source can be a integer or tuple + containing pixel values. The method then fills the region + with the given color. When creating RGB images, you can + also use color strings as supported by the ImageColor module. + + If a mask is given, this method updates only the regions + indicated by the mask. You can use either "1", "L", "LA", "RGBA" + or "RGBa" images (if present, the alpha band is used as mask). + Where the mask is 255, the given image is copied as is. Where + the mask is 0, the current value is preserved. Intermediate + values will mix the two images together, including their alpha + channels if they have them. + + See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to + combine images with respect to their alpha channels. + + :param im: Source image or pixel value (integer, float or tuple). + :param box: An optional 4-tuple giving the region to paste into. + If a 2-tuple is used instead, it's treated as the upper left + corner. If omitted or None, the source is pasted into the + upper left corner. + + If an image is given as the second argument and there is no + third, the box defaults to (0, 0), and the second argument + is interpreted as a mask image. + :param mask: An optional mask image. + """ + + if isinstance(box, Image): + if mask is not None: + msg = "If using second argument as mask, third argument must be None" + raise ValueError(msg) + # abbreviated paste(im, mask) syntax + mask = box + box = None + + if box is None: + box = (0, 0) + + if len(box) == 2: + # upper left corner given; get size from image or mask + if isinstance(im, Image): + size = im.size + elif isinstance(mask, Image): + size = mask.size + else: + # FIXME: use self.size here? + msg = "cannot determine region size; use 4-item box" + raise ValueError(msg) + box += (box[0] + size[0], box[1] + size[1]) + + source: core.ImagingCore | str | float | tuple[float, ...] + if isinstance(im, str): + from . import ImageColor + + source = ImageColor.getcolor(im, self.mode) + elif isinstance(im, Image): + im.load() + if self.mode != im.mode: + if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): + # should use an adapter for this! + im = im.convert(self.mode) + source = im.im + else: + source = im + + self._ensure_mutable() + + if mask: + mask.load() + self.im.paste(source, box, mask.im) + else: + self.im.paste(source, box) + + def alpha_composite( + self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0) + ) -> None: + """'In-place' analog of Image.alpha_composite. Composites an image + onto this image. + + :param im: image to composite over this one + :param dest: Optional 2 tuple (left, top) specifying the upper + left corner in this (destination) image. + :param source: Optional 2 (left, top) tuple for the upper left + corner in the overlay source image, or 4 tuple (left, top, right, + bottom) for the bounds of the source rectangle + + Performance Note: Not currently implemented in-place in the core layer. + """ + + if not isinstance(source, (list, tuple)): + msg = "Source must be a list or tuple" + raise ValueError(msg) + if not isinstance(dest, (list, tuple)): + msg = "Destination must be a list or tuple" + raise ValueError(msg) + + if len(source) == 4: + overlay_crop_box = tuple(source) + elif len(source) == 2: + overlay_crop_box = tuple(source) + im.size + else: + msg = "Source must be a sequence of length 2 or 4" + raise ValueError(msg) + + if not len(dest) == 2: + msg = "Destination must be a sequence of length 2" + raise ValueError(msg) + if min(source) < 0: + msg = "Source must be non-negative" + raise ValueError(msg) + + # over image, crop if it's not the whole image. + if overlay_crop_box == (0, 0) + im.size: + overlay = im + else: + overlay = im.crop(overlay_crop_box) + + # target for the paste + box = tuple(dest) + (dest[0] + overlay.width, dest[1] + overlay.height) + + # destination image. don't copy if we're using the whole image. + if box == (0, 0) + self.size: + background = self + else: + background = self.crop(box) + + result = alpha_composite(background, overlay) + self.paste(result, box) + + def point( + self, + lut: ( + Sequence[float] + | NumpyArray + | Callable[[int], float] + | Callable[[ImagePointTransform], ImagePointTransform | float] + | ImagePointHandler + ), + mode: str | None = None, + ) -> Image: + """ + Maps this image through a lookup table or function. + + :param lut: A lookup table, containing 256 (or 65536 if + self.mode=="I" and mode == "L") values per band in the + image. A function can be used instead, it should take a + single argument. The function is called once for each + possible pixel value, and the resulting table is applied to + all bands of the image. + + It may also be an :py:class:`~PIL.Image.ImagePointHandler` + object:: + + class Example(Image.ImagePointHandler): + def point(self, im: Image) -> Image: + # Return result + :param mode: Output mode (default is same as input). This can only be used if + the source image has mode "L" or "P", and the output has mode "1" or the + source image mode is "I" and the output mode is "L". + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + self.load() + + if isinstance(lut, ImagePointHandler): + return lut.point(self) + + if callable(lut): + # if it isn't a list, it should be a function + if self.mode in ("I", "I;16", "F"): + # check if the function can be used with point_transform + # UNDONE wiredfool -- I think this prevents us from ever doing + # a gamma function point transform on > 8bit images. + scale, offset = _getscaleoffset(lut) # type: ignore[arg-type] + return self._new(self.im.point_transform(scale, offset)) + # for other modes, convert the function to a table + flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type] + else: + flatLut = lut + + if self.mode == "F": + # FIXME: _imaging returns a confusing error message for this case + msg = "point operation not supported for this mode" + raise ValueError(msg) + + if mode != "F": + flatLut = [round(i) for i in flatLut] + return self._new(self.im.point(flatLut, mode)) + + def putalpha(self, alpha: Image | int) -> None: + """ + Adds or replaces the alpha layer in this image. If the image + does not have an alpha layer, it's converted to "LA" or "RGBA". + The new layer must be either "L" or "1". + + :param alpha: The new alpha layer. This can either be an "L" or "1" + image having the same size as this image, or an integer. + """ + + self._ensure_mutable() + + if self.mode not in ("LA", "PA", "RGBA"): + # attempt to promote self to a matching alpha mode + try: + mode = getmodebase(self.mode) + "A" + try: + self.im.setmode(mode) + except (AttributeError, ValueError) as e: + # do things the hard way + im = self.im.convert(mode) + if im.mode not in ("LA", "PA", "RGBA"): + msg = "alpha channel could not be added" + raise ValueError(msg) from e # sanity check + self.im = im + self._mode = self.im.mode + except KeyError as e: + msg = "illegal image mode" + raise ValueError(msg) from e + + if self.mode in ("LA", "PA"): + band = 1 + else: + band = 3 + + if isinstance(alpha, Image): + # alpha layer + if alpha.mode not in ("1", "L"): + msg = "illegal image mode" + raise ValueError(msg) + alpha.load() + if alpha.mode == "1": + alpha = alpha.convert("L") + else: + # constant alpha + try: + self.im.fillband(band, alpha) + except (AttributeError, ValueError): + # do things the hard way + alpha = new("L", self.size, alpha) + else: + return + + self.im.putband(alpha.im, band) + + def putdata( + self, + data: Sequence[float] | Sequence[Sequence[int]] | core.ImagingCore | NumpyArray, + scale: float = 1.0, + offset: float = 0.0, + ) -> None: + """ + Copies pixel data from a flattened sequence object into the image. The + values should start at the upper left corner (0, 0), continue to the + end of the line, followed directly by the first value of the second + line, and so on. Data will be read until either the image or the + sequence ends. The scale and offset values are used to adjust the + sequence values: **pixel = value*scale + offset**. + + :param data: A flattened sequence object. + :param scale: An optional scale value. The default is 1.0. + :param offset: An optional offset value. The default is 0.0. + """ + + self._ensure_mutable() + + self.im.putdata(data, scale, offset) + + def putpalette( + self, + data: ImagePalette.ImagePalette | bytes | Sequence[int], + rawmode: str = "RGB", + ) -> None: + """ + Attaches a palette to this image. The image must be a "P", "PA", "L" + or "LA" image. + + The palette sequence must contain at most 256 colors, made up of one + integer value for each channel in the raw mode. + For example, if the raw mode is "RGB", then it can contain at most 768 + values, made up of red, green and blue values for the corresponding pixel + index in the 256 colors. + If the raw mode is "RGBA", then it can contain at most 1024 values, + containing red, green, blue and alpha values. + + Alternatively, an 8-bit string may be used instead of an integer sequence. + + :param data: A palette sequence (either a list or a string). + :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode + that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). + """ + from . import ImagePalette + + if self.mode not in ("L", "LA", "P", "PA"): + msg = "illegal image mode" + raise ValueError(msg) + if isinstance(data, ImagePalette.ImagePalette): + if data.rawmode is not None: + palette = ImagePalette.raw(data.rawmode, data.palette) + else: + palette = ImagePalette.ImagePalette(palette=data.palette) + palette.dirty = 1 + else: + if not isinstance(data, bytes): + data = bytes(data) + palette = ImagePalette.raw(rawmode, data) + self._mode = "PA" if "A" in self.mode else "P" + self.palette = palette + self.palette.mode = "RGBA" if "A" in rawmode else "RGB" + self.load() # install new palette + + def putpixel( + self, xy: tuple[int, int], value: float | tuple[int, ...] | list[int] + ) -> None: + """ + Modifies the pixel at the given position. The color is given as + a single numerical value for single-band images, and a tuple for + multi-band images. In addition to this, RGB and RGBA tuples are + accepted for P and PA images. + + Note that this method is relatively slow. For more extensive changes, + use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` + module instead. + + See: + + * :py:meth:`~PIL.Image.Image.paste` + * :py:meth:`~PIL.Image.Image.putdata` + * :py:mod:`~PIL.ImageDraw` + + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. + :param value: The pixel value. + """ + + if self.readonly: + self._copy() + self.load() + + if ( + self.mode in ("P", "PA") + and isinstance(value, (list, tuple)) + and len(value) in [3, 4] + ): + # RGB or RGBA value for a P or PA image + if self.mode == "PA": + alpha = value[3] if len(value) == 4 else 255 + value = value[:3] + assert self.palette is not None + palette_index = self.palette.getcolor(tuple(value), self) + value = (palette_index, alpha) if self.mode == "PA" else palette_index + return self.im.putpixel(xy, value) + + def remap_palette( + self, dest_map: list[int], source_palette: bytes | bytearray | None = None + ) -> Image: + """ + Rewrites the image to reorder the palette. + + :param dest_map: A list of indexes into the original palette. + e.g. ``[1,0]`` would swap a two item palette, and ``list(range(256))`` + is the identity transform. + :param source_palette: Bytes or None. + :returns: An :py:class:`~PIL.Image.Image` object. + + """ + from . import ImagePalette + + if self.mode not in ("L", "P"): + msg = "illegal image mode" + raise ValueError(msg) + + bands = 3 + palette_mode = "RGB" + if source_palette is None: + if self.mode == "P": + self.load() + palette_mode = self.im.getpalettemode() + if palette_mode == "RGBA": + bands = 4 + source_palette = self.im.getpalette(palette_mode, palette_mode) + else: # L-mode + source_palette = bytearray(i // 3 for i in range(768)) + elif len(source_palette) > 768: + bands = 4 + palette_mode = "RGBA" + + palette_bytes = b"" + new_positions = [0] * 256 + + # pick only the used colors from the palette + for i, oldPosition in enumerate(dest_map): + palette_bytes += source_palette[ + oldPosition * bands : oldPosition * bands + bands + ] + new_positions[oldPosition] = i + + # replace the palette color id of all pixel with the new id + + # Palette images are [0..255], mapped through a 1 or 3 + # byte/color map. We need to remap the whole image + # from palette 1 to palette 2. New_positions is + # an array of indexes into palette 1. Palette 2 is + # palette 1 with any holes removed. + + # We're going to leverage the convert mechanism to use the + # C code to remap the image from palette 1 to palette 2, + # by forcing the source image into 'L' mode and adding a + # mapping 'L' mode palette, then converting back to 'L' + # sans palette thus converting the image bytes, then + # assigning the optimized RGB palette. + + # perf reference, 9500x4000 gif, w/~135 colors + # 14 sec prepatch, 1 sec postpatch with optimization forced. + + mapping_palette = bytearray(new_positions) + + m_im = self.copy() + m_im._mode = "P" + + m_im.palette = ImagePalette.ImagePalette( + palette_mode, palette=mapping_palette * bands + ) + # possibly set palette dirty, then + # m_im.putpalette(mapping_palette, 'L') # converts to 'P' + # or just force it. + # UNDONE -- this is part of the general issue with palettes + m_im.im.putpalette(palette_mode, palette_mode + ";L", m_im.palette.tobytes()) + + m_im = m_im.convert("L") + + m_im.putpalette(palette_bytes, palette_mode) + m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes) + + if "transparency" in self.info: + try: + m_im.info["transparency"] = dest_map.index(self.info["transparency"]) + except ValueError: + if "transparency" in m_im.info: + del m_im.info["transparency"] + + return m_im + + def _get_safe_box( + self, + size: tuple[int, int], + resample: Resampling, + box: tuple[float, float, float, float], + ) -> tuple[int, int, int, int]: + """Expands the box so it includes adjacent pixels + that may be used by resampling with the given resampling filter. + """ + filter_support = _filters_support[resample] - 0.5 + scale_x = (box[2] - box[0]) / size[0] + scale_y = (box[3] - box[1]) / size[1] + support_x = filter_support * scale_x + support_y = filter_support * scale_y + + return ( + max(0, int(box[0] - support_x)), + max(0, int(box[1] - support_y)), + min(self.size[0], math.ceil(box[2] + support_x)), + min(self.size[1], math.ceil(box[3] + support_y)), + ) + + def resize( + self, + size: tuple[int, int] | list[int] | NumpyArray, + resample: int | None = None, + box: tuple[float, float, float, float] | None = None, + reducing_gap: float | None = None, + ) -> Image: + """ + Returns a resized copy of this image. + + :param size: The requested size in pixels, as a tuple or array: + (width, height). + :param resample: An optional resampling filter. This can be + one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, + :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, + :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. + If the image has mode "1" or "P", it is always set to + :py:data:`Resampling.NEAREST`. If the image mode is "BGR;15", + "BGR;16" or "BGR;24", then the default filter is + :py:data:`Resampling.NEAREST`. Otherwise, the default filter is + :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. + :param box: An optional 4-tuple of floats providing + the source image region to be scaled. + The values must be within (0, 0, width, height) rectangle. + If omitted or None, the entire source is used. + :param reducing_gap: Apply optimization by resizing the image + in two steps. First, reducing the image by integer times + using :py:meth:`~PIL.Image.Image.reduce`. + Second, resizing using regular resampling. The last step + changes size no less than by ``reducing_gap`` times. + ``reducing_gap`` may be None (no first step is performed) + or should be greater than 1.0. The bigger ``reducing_gap``, + the closer the result to the fair resampling. + The smaller ``reducing_gap``, the faster resizing. + With ``reducing_gap`` greater or equal to 3.0, the result is + indistinguishable from fair resampling in most cases. + The default value is None (no optimization). + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if resample is None: + bgr = self.mode.startswith("BGR;") + resample = Resampling.NEAREST if bgr else Resampling.BICUBIC + elif resample not in ( + Resampling.NEAREST, + Resampling.BILINEAR, + Resampling.BICUBIC, + Resampling.LANCZOS, + Resampling.BOX, + Resampling.HAMMING, + ): + msg = f"Unknown resampling filter ({resample})." + + filters = [ + f"{filter[1]} ({filter[0]})" + for filter in ( + (Resampling.NEAREST, "Image.Resampling.NEAREST"), + (Resampling.LANCZOS, "Image.Resampling.LANCZOS"), + (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), + (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), + (Resampling.BOX, "Image.Resampling.BOX"), + (Resampling.HAMMING, "Image.Resampling.HAMMING"), + ) + ] + msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" + raise ValueError(msg) + + if reducing_gap is not None and reducing_gap < 1.0: + msg = "reducing_gap must be 1.0 or greater" + raise ValueError(msg) + + if box is None: + box = (0, 0) + self.size + + size = tuple(size) + if self.size == size and box == (0, 0) + self.size: + return self.copy() + + if self.mode in ("1", "P"): + resample = Resampling.NEAREST + + if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST: + im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) + im = im.resize(size, resample, box) + return im.convert(self.mode) + + self.load() + + if reducing_gap is not None and resample != Resampling.NEAREST: + factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 + factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 + if factor_x > 1 or factor_y > 1: + reduce_box = self._get_safe_box(size, cast(Resampling, resample), box) + factor = (factor_x, factor_y) + self = ( + self.reduce(factor, box=reduce_box) + if callable(self.reduce) + else Image.reduce(self, factor, box=reduce_box) + ) + box = ( + (box[0] - reduce_box[0]) / factor_x, + (box[1] - reduce_box[1]) / factor_y, + (box[2] - reduce_box[0]) / factor_x, + (box[3] - reduce_box[1]) / factor_y, + ) + + return self._new(self.im.resize(size, resample, box)) + + def reduce( + self, + factor: int | tuple[int, int], + box: tuple[int, int, int, int] | None = None, + ) -> Image: + """ + Returns a copy of the image reduced ``factor`` times. + If the size of the image is not dividable by ``factor``, + the resulting size will be rounded up. + + :param factor: A greater than 0 integer or tuple of two integers + for width and height separately. + :param box: An optional 4-tuple of ints providing + the source image region to be reduced. + The values must be within ``(0, 0, width, height)`` rectangle. + If omitted or ``None``, the entire source is used. + """ + if not isinstance(factor, (list, tuple)): + factor = (factor, factor) + + if box is None: + box = (0, 0) + self.size + + if factor == (1, 1) and box == (0, 0) + self.size: + return self.copy() + + if self.mode in ["LA", "RGBA"]: + im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) + im = im.reduce(factor, box) + return im.convert(self.mode) + + self.load() + + return self._new(self.im.reduce(factor, box)) + + def rotate( + self, + angle: float, + resample: Resampling = Resampling.NEAREST, + expand: int | bool = False, + center: tuple[float, float] | None = None, + translate: tuple[int, int] | None = None, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: + """ + Returns a rotated copy of this image. This method returns a + copy of this image, rotated the given number of degrees counter + clockwise around its centre. + + :param angle: In degrees counter clockwise. + :param resample: An optional resampling filter. This can be + one of :py:data:`Resampling.NEAREST` (use nearest neighbour), + :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`Resampling.BICUBIC` (cubic spline + interpolation in a 4x4 environment). If omitted, or if the image has + mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. + See :ref:`concept-filters`. + :param expand: Optional expansion flag. If true, expands the output + image to make it large enough to hold the entire rotated image. + If false or omitted, make the output image the same size as the + input image. Note that the expand flag assumes rotation around + the center and no translation. + :param center: Optional center of rotation (a 2-tuple). Origin is + the upper left corner. Default is the center of the image. + :param translate: An optional post-rotate translation (a 2-tuple). + :param fillcolor: An optional color for area outside the rotated image. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + angle = angle % 360.0 + + # Fast paths regardless of filter, as long as we're not + # translating or changing the center. + if not (center or translate): + if angle == 0: + return self.copy() + if angle == 180: + return self.transpose(Transpose.ROTATE_180) + if angle in (90, 270) and (expand or self.width == self.height): + return self.transpose( + Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270 + ) + + # Calculate the affine matrix. Note that this is the reverse + # transformation (from destination image to source) because we + # want to interpolate the (discrete) destination pixel from + # the local area around the (floating) source pixel. + + # The matrix we actually want (note that it operates from the right): + # (1, 0, tx) (1, 0, cx) ( cos a, sin a, 0) (1, 0, -cx) + # (0, 1, ty) * (0, 1, cy) * (-sin a, cos a, 0) * (0, 1, -cy) + # (0, 0, 1) (0, 0, 1) ( 0, 0, 1) (0, 0, 1) + + # The reverse matrix is thus: + # (1, 0, cx) ( cos -a, sin -a, 0) (1, 0, -cx) (1, 0, -tx) + # (0, 1, cy) * (-sin -a, cos -a, 0) * (0, 1, -cy) * (0, 1, -ty) + # (0, 0, 1) ( 0, 0, 1) (0, 0, 1) (0, 0, 1) + + # In any case, the final translation may be updated at the end to + # compensate for the expand flag. + + w, h = self.size + + if translate is None: + post_trans = (0, 0) + else: + post_trans = translate + if center is None: + center = (w / 2, h / 2) + + angle = -math.radians(angle) + matrix = [ + round(math.cos(angle), 15), + round(math.sin(angle), 15), + 0.0, + round(-math.sin(angle), 15), + round(math.cos(angle), 15), + 0.0, + ] + + def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]: + (a, b, c, d, e, f) = matrix + return a * x + b * y + c, d * x + e * y + f + + matrix[2], matrix[5] = transform( + -center[0] - post_trans[0], -center[1] - post_trans[1], matrix + ) + matrix[2] += center[0] + matrix[5] += center[1] + + if expand: + # calculate output size + xx = [] + yy = [] + for x, y in ((0, 0), (w, 0), (w, h), (0, h)): + transformed_x, transformed_y = transform(x, y, matrix) + xx.append(transformed_x) + yy.append(transformed_y) + nw = math.ceil(max(xx)) - math.floor(min(xx)) + nh = math.ceil(max(yy)) - math.floor(min(yy)) + + # We multiply a translation matrix from the right. Because of its + # special form, this is the same as taking the image of the + # translation vector as new translation vector. + matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix) + w, h = nw, nh + + return self.transform( + (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor + ) + + def save( + self, fp: StrOrBytesPath | IO[bytes], format: str | None = None, **params: Any + ) -> None: + """ + Saves this image under the given filename. If no format is + specified, the format to use is determined from the filename + extension, if possible. + + Keyword options can be used to provide additional instructions + to the writer. If a writer doesn't recognise an option, it is + silently ignored. The available options are described in the + :doc:`image format documentation + <../handbook/image-file-formats>` for each writer. + + You can use a file object instead of a filename. In this case, + you must always specify the format. The file object must + implement the ``seek``, ``tell``, and ``write`` + methods, and be opened in binary mode. + + :param fp: A filename (string), os.PathLike object or file object. + :param format: Optional format override. If omitted, the + format to use is determined from the filename extension. + If a file object was used instead of a filename, this + parameter should always be used. + :param params: Extra parameters to the image writer. These can also be + set on the image itself through ``encoderinfo``. This is useful when + saving multiple images:: + + # Saving XMP data to a single image + from PIL import Image + red = Image.new("RGB", (1, 1), "#f00") + red.save("out.mpo", xmp=b"test") + + # Saving XMP data to the second frame of an image + from PIL import Image + black = Image.new("RGB", (1, 1)) + red = Image.new("RGB", (1, 1), "#f00") + red.encoderinfo = {"xmp": b"test"} + black.save("out.mpo", save_all=True, append_images=[red]) + :returns: None + :exception ValueError: If the output format could not be determined + from the file name. Use the format option to solve this. + :exception OSError: If the file could not be written. The file + may have been created, and may contain partial data. + """ + + filename: str | bytes = "" + open_fp = False + if is_path(fp): + filename = os.fspath(fp) + open_fp = True + elif fp == sys.stdout: + try: + fp = sys.stdout.buffer + except AttributeError: + pass + if not filename and hasattr(fp, "name") and is_path(fp.name): + # only set the name for metadata purposes + filename = os.fspath(fp.name) + + preinit() + + filename_ext = os.path.splitext(filename)[1].lower() + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext + + if not format: + if ext not in EXTENSION: + init() + try: + format = EXTENSION[ext] + except KeyError as e: + msg = f"unknown file extension: {ext}" + raise ValueError(msg) from e + + from . import ImageFile + + # may mutate self! + if isinstance(self, ImageFile.ImageFile) and os.path.abspath( + filename + ) == os.path.abspath(self.filename): + self._ensure_mutable() + else: + self.load() + + save_all = params.pop("save_all", None) + self._default_encoderinfo = params + encoderinfo = getattr(self, "encoderinfo", {}) + self._attach_default_encoderinfo(self) + self.encoderconfig: tuple[Any, ...] = () + + if format.upper() not in SAVE: + init() + if save_all or ( + save_all is None + and params.get("append_images") + and format.upper() in SAVE_ALL + ): + save_handler = SAVE_ALL[format.upper()] + else: + save_handler = SAVE[format.upper()] + + created = False + if open_fp: + created = not os.path.exists(filename) + if params.get("append", False): + # Open also for reading ("+"), because TIFF save_all + # writer needs to go back and edit the written data. + fp = builtins.open(filename, "r+b") + else: + fp = builtins.open(filename, "w+b") + else: + fp = cast(IO[bytes], fp) + + try: + save_handler(self, fp, filename) + except Exception: + if open_fp: + fp.close() + if created: + try: + os.remove(filename) + except PermissionError: + pass + raise + finally: + self.encoderinfo = encoderinfo + if open_fp: + fp.close() + + def _attach_default_encoderinfo(self, im: Image) -> dict[str, Any]: + encoderinfo = getattr(self, "encoderinfo", {}) + self.encoderinfo = {**im._default_encoderinfo, **encoderinfo} + return encoderinfo + + def seek(self, frame: int) -> None: + """ + Seeks to the given frame in this sequence file. If you seek + beyond the end of the sequence, the method raises an + ``EOFError`` exception. When a sequence file is opened, the + library automatically seeks to frame 0. + + See :py:meth:`~PIL.Image.Image.tell`. + + If defined, :attr:`~PIL.Image.Image.n_frames` refers to the + number of available frames. + + :param frame: Frame number, starting at 0. + :exception EOFError: If the call attempts to seek beyond the end + of the sequence. + """ + + # overridden by file handlers + if frame != 0: + msg = "no more images in file" + raise EOFError(msg) + + def show(self, title: str | None = None) -> None: + """ + Displays this image. This method is mainly intended for debugging purposes. + + This method calls :py:func:`PIL.ImageShow.show` internally. You can use + :py:func:`PIL.ImageShow.register` to override its default behaviour. + + The image is first saved to a temporary file. By default, it will be in + PNG format. + + On Unix, the image is then opened using the **xdg-open**, **display**, + **gm**, **eog** or **xv** utility, depending on which one can be found. + + On macOS, the image is opened with the native Preview application. + + On Windows, the image is opened with the standard PNG display utility. + + :param title: Optional title to use for the image window, where possible. + """ + + _show(self, title=title) + + def split(self) -> tuple[Image, ...]: + """ + Split this image into individual bands. This method returns a + tuple of individual image bands from an image. For example, + splitting an "RGB" image creates three new images each + containing a copy of one of the original bands (red, green, + blue). + + If you need only one band, :py:meth:`~PIL.Image.Image.getchannel` + method can be more convenient and faster. + + :returns: A tuple containing bands. + """ + + self.load() + if self.im.bands == 1: + return (self.copy(),) + return tuple(map(self._new, self.im.split())) + + def getchannel(self, channel: int | str) -> Image: + """ + Returns an image containing a single channel of the source image. + + :param channel: What channel to return. Could be index + (0 for "R" channel of "RGB") or channel name + ("A" for alpha channel of "RGBA"). + :returns: An image in "L" mode. + + .. versionadded:: 4.3.0 + """ + self.load() + + if isinstance(channel, str): + try: + channel = self.getbands().index(channel) + except ValueError as e: + msg = f'The image has no channel "{channel}"' + raise ValueError(msg) from e + + return self._new(self.im.getband(channel)) + + def tell(self) -> int: + """ + Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. + + If defined, :attr:`~PIL.Image.Image.n_frames` refers to the + number of available frames. + + :returns: Frame number, starting with 0. + """ + return 0 + + def thumbnail( + self, + size: tuple[float, float], + resample: Resampling = Resampling.BICUBIC, + reducing_gap: float | None = 2.0, + ) -> None: + """ + Make this image into a thumbnail. This method modifies the + image to contain a thumbnail version of itself, no larger than + the given size. This method calculates an appropriate thumbnail + size to preserve the aspect of the image, calls the + :py:meth:`~PIL.Image.Image.draft` method to configure the file reader + (where applicable), and finally resizes the image. + + Note that this function modifies the :py:class:`~PIL.Image.Image` + object in place. If you need to use the full resolution image as well, + apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original + image. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param resample: Optional resampling filter. This can be one + of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, + :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, + :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. + If omitted, it defaults to :py:data:`Resampling.BICUBIC`. + (was :py:data:`Resampling.NEAREST` prior to version 2.5.0). + See: :ref:`concept-filters`. + :param reducing_gap: Apply optimization by resizing the image + in two steps. First, reducing the image by integer times + using :py:meth:`~PIL.Image.Image.reduce` or + :py:meth:`~PIL.Image.Image.draft` for JPEG images. + Second, resizing using regular resampling. The last step + changes size no less than by ``reducing_gap`` times. + ``reducing_gap`` may be None (no first step is performed) + or should be greater than 1.0. The bigger ``reducing_gap``, + the closer the result to the fair resampling. + The smaller ``reducing_gap``, the faster resizing. + With ``reducing_gap`` greater or equal to 3.0, the result is + indistinguishable from fair resampling in most cases. + The default value is 2.0 (very close to fair resampling + while still being faster in many cases). + :returns: None + """ + + provided_size = tuple(map(math.floor, size)) + + def preserve_aspect_ratio() -> tuple[int, int] | None: + def round_aspect(number: float, key: Callable[[int], float]) -> int: + return max(min(math.floor(number), math.ceil(number), key=key), 1) + + x, y = provided_size + if x >= self.width and y >= self.height: + return None + + aspect = self.width / self.height + if x / y >= aspect: + x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) + else: + y = round_aspect( + x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) + ) + return x, y + + preserved_size = preserve_aspect_ratio() + if preserved_size is None: + return + final_size = preserved_size + + box = None + if reducing_gap is not None: + res = self.draft( + None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) + ) + if res is not None: + box = res[1] + + if self.size != final_size: + im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) + + self.im = im.im + self._size = final_size + self._mode = self.im.mode + + self.readonly = 0 + + # FIXME: the different transform methods need further explanation + # instead of bloating the method docs, add a separate chapter. + def transform( + self, + size: tuple[int, int], + method: Transform | ImageTransformHandler | SupportsGetData, + data: Sequence[Any] | None = None, + resample: int = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: + """ + Transforms this image. This method creates a new image with the + given size, and the same mode as the original, and copies data + to the new image using the given transform. + + :param size: The output size in pixels, as a 2-tuple: + (width, height). + :param method: The transformation method. This is one of + :py:data:`Transform.EXTENT` (cut out a rectangular subregion), + :py:data:`Transform.AFFINE` (affine transform), + :py:data:`Transform.PERSPECTIVE` (perspective transform), + :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or + :py:data:`Transform.MESH` (map a number of source quadrilaterals + in one operation). + + It may also be an :py:class:`~PIL.Image.ImageTransformHandler` + object:: + + class Example(Image.ImageTransformHandler): + def transform(self, size, data, resample, fill=1): + # Return result + + Implementations of :py:class:`~PIL.Image.ImageTransformHandler` + for some of the :py:class:`Transform` methods are provided + in :py:mod:`~PIL.ImageTransform`. + + It may also be an object with a ``method.getdata`` method + that returns a tuple supplying new ``method`` and ``data`` values:: + + class Example: + def getdata(self): + method = Image.Transform.EXTENT + data = (0, 0, 100, 100) + return method, data + :param data: Extra data to the transformation method. + :param resample: Optional resampling filter. It can be one of + :py:data:`Resampling.NEAREST` (use nearest neighbour), + :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`Resampling.BICUBIC` (cubic spline + interpolation in a 4x4 environment). If omitted, or if the image + has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. + See: :ref:`concept-filters`. + :param fill: If ``method`` is an + :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of + the arguments passed to it. Otherwise, it is unused. + :param fillcolor: Optional fill color for the area outside the + transform in the output image. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST: + return ( + self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) + .transform(size, method, data, resample, fill, fillcolor) + .convert(self.mode) + ) + + if isinstance(method, ImageTransformHandler): + return method.transform(size, self, resample=resample, fill=fill) + + if hasattr(method, "getdata"): + # compatibility w. old-style transform objects + method, data = method.getdata() + + if data is None: + msg = "missing method data" + raise ValueError(msg) + + im = new(self.mode, size, fillcolor) + if self.mode == "P" and self.palette: + im.palette = self.palette.copy() + im.info = self.info.copy() + if method == Transform.MESH: + # list of quads + for box, quad in data: + im.__transformer( + box, self, Transform.QUAD, quad, resample, fillcolor is None + ) + else: + im.__transformer( + (0, 0) + size, self, method, data, resample, fillcolor is None + ) + + return im + + def __transformer( + self, + box: tuple[int, int, int, int], + image: Image, + method: Transform, + data: Sequence[float], + resample: int = Resampling.NEAREST, + fill: bool = True, + ) -> None: + w = box[2] - box[0] + h = box[3] - box[1] + + if method == Transform.AFFINE: + data = data[:6] + + elif method == Transform.EXTENT: + # convert extent to an affine transform + x0, y0, x1, y1 = data + xs = (x1 - x0) / w + ys = (y1 - y0) / h + method = Transform.AFFINE + data = (xs, 0, x0, 0, ys, y0) + + elif method == Transform.PERSPECTIVE: + data = data[:8] + + elif method == Transform.QUAD: + # quadrilateral warp. data specifies the four corners + # given as NW, SW, SE, and NE. + nw = data[:2] + sw = data[2:4] + se = data[4:6] + ne = data[6:8] + x0, y0 = nw + As = 1.0 / w + At = 1.0 / h + data = ( + x0, + (ne[0] - x0) * As, + (sw[0] - x0) * At, + (se[0] - sw[0] - ne[0] + x0) * As * At, + y0, + (ne[1] - y0) * As, + (sw[1] - y0) * At, + (se[1] - sw[1] - ne[1] + y0) * As * At, + ) + + else: + msg = "unknown transformation method" + raise ValueError(msg) + + if resample not in ( + Resampling.NEAREST, + Resampling.BILINEAR, + Resampling.BICUBIC, + ): + if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): + unusable: dict[int, str] = { + Resampling.BOX: "Image.Resampling.BOX", + Resampling.HAMMING: "Image.Resampling.HAMMING", + Resampling.LANCZOS: "Image.Resampling.LANCZOS", + } + msg = unusable[resample] + f" ({resample}) cannot be used." + else: + msg = f"Unknown resampling filter ({resample})." + + filters = [ + f"{filter[1]} ({filter[0]})" + for filter in ( + (Resampling.NEAREST, "Image.Resampling.NEAREST"), + (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), + (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), + ) + ] + msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" + raise ValueError(msg) + + image.load() + + self.load() + + if image.mode in ("1", "P"): + resample = Resampling.NEAREST + + self.im.transform(box, image.im, method, data, resample, fill) + + def transpose(self, method: Transpose) -> Image: + """ + Transpose image (flip or rotate in 90 degree steps) + + :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`, + :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`, + :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`, + :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`. + :returns: Returns a flipped or rotated copy of this image. + """ + + self.load() + return self._new(self.im.transpose(method)) + + def effect_spread(self, distance: int) -> Image: + """ + Randomly spread pixels in an image. + + :param distance: Distance to spread pixels. + """ + self.load() + return self._new(self.im.effect_spread(distance)) + + def toqimage(self) -> ImageQt.ImageQt: + """Returns a QImage copy of this image""" + from . import ImageQt + + if not ImageQt.qt_is_installed: + msg = "Qt bindings are not installed" + raise ImportError(msg) + return ImageQt.toqimage(self) + + def toqpixmap(self) -> ImageQt.QPixmap: + """Returns a QPixmap copy of this image""" + from . import ImageQt + + if not ImageQt.qt_is_installed: + msg = "Qt bindings are not installed" + raise ImportError(msg) + return ImageQt.toqpixmap(self) + + +# -------------------------------------------------------------------- +# Abstract handlers. + + +class ImagePointHandler(abc.ABC): + """ + Used as a mixin by point transforms + (for use with :py:meth:`~PIL.Image.Image.point`) + """ + + @abc.abstractmethod + def point(self, im: Image) -> Image: + pass + + +class ImageTransformHandler(abc.ABC): + """ + Used as a mixin by geometry transforms + (for use with :py:meth:`~PIL.Image.Image.transform`) + """ + + @abc.abstractmethod + def transform( + self, + size: tuple[int, int], + image: Image, + **options: Any, + ) -> Image: + pass + + +# -------------------------------------------------------------------- +# Factories + + +def _check_size(size: Any) -> None: + """ + Common check to enforce type and sanity check on size tuples + + :param size: Should be a 2 tuple of (width, height) + :returns: None, or raises a ValueError + """ + + if not isinstance(size, (list, tuple)): + msg = "Size must be a list or tuple" + raise ValueError(msg) + if len(size) != 2: + msg = "Size must be a sequence of length 2" + raise ValueError(msg) + if size[0] < 0 or size[1] < 0: + msg = "Width and height must be >= 0" + raise ValueError(msg) + + +def new( + mode: str, + size: tuple[int, int] | list[int], + color: float | tuple[float, ...] | str | None = 0, +) -> Image: + """ + Creates a new image with the given mode and size. + + :param mode: The mode to use for the new image. See: + :ref:`concept-modes`. + :param size: A 2-tuple, containing (width, height) in pixels. + :param color: What color to use for the image. Default is black. + If given, this should be a single integer or floating point value + for single-band modes, and a tuple for multi-band modes (one value + per band). When creating RGB or HSV images, you can also use color + strings as supported by the ImageColor module. If the color is + None, the image is not initialised. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + + _check_size(size) + + if color is None: + # don't initialize + return Image()._new(core.new(mode, size)) + + if isinstance(color, str): + # css3-style specifier + + from . import ImageColor + + color = ImageColor.getcolor(color, mode) + + im = Image() + if ( + mode == "P" + and isinstance(color, (list, tuple)) + and all(isinstance(i, int) for i in color) + ): + color_ints: tuple[int, ...] = cast(tuple[int, ...], tuple(color)) + if len(color_ints) == 3 or len(color_ints) == 4: + # RGB or RGBA value for a P image + from . import ImagePalette + + im.palette = ImagePalette.ImagePalette() + color = im.palette.getcolor(color_ints) + return im._new(core.fill(mode, size, color)) + + +def frombytes( + mode: str, + size: tuple[int, int], + data: bytes | bytearray | SupportsArrayInterface, + decoder_name: str = "raw", + *args: Any, +) -> Image: + """ + Creates a copy of an image memory from pixel data in a buffer. + + In its simplest form, this function takes three arguments + (mode, size, and unpacked pixel data). + + You can also use any pixel decoder supported by PIL. For more + information on available decoders, see the section + :ref:`Writing Your Own File Codec `. + + Note that this function decodes pixel data only, not entire images. + If you have an entire image in a string, wrap it in a + :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load + it. + + :param mode: The image mode. See: :ref:`concept-modes`. + :param size: The image size. + :param data: A byte buffer containing raw data for the given mode. + :param decoder_name: What decoder to use. + :param args: Additional parameters for the given decoder. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + _check_size(size) + + im = new(mode, size) + if im.width != 0 and im.height != 0: + decoder_args: Any = args + if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple): + # may pass tuple instead of argument list + decoder_args = decoder_args[0] + + if decoder_name == "raw" and decoder_args == (): + decoder_args = mode + + im.frombytes(data, decoder_name, decoder_args) + return im + + +def frombuffer( + mode: str, + size: tuple[int, int], + data: bytes | SupportsArrayInterface, + decoder_name: str = "raw", + *args: Any, +) -> Image: + """ + Creates an image memory referencing pixel data in a byte buffer. + + This function is similar to :py:func:`~PIL.Image.frombytes`, but uses data + in the byte buffer, where possible. This means that changes to the + original buffer object are reflected in this image). Not all modes can + share memory; supported modes include "L", "RGBX", "RGBA", and "CMYK". + + Note that this function decodes pixel data only, not entire images. + If you have an entire image file in a string, wrap it in a + :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it. + + The default parameters used for the "raw" decoder differs from that used for + :py:func:`~PIL.Image.frombytes`. This is a bug, and will probably be fixed in a + future release. The current release issues a warning if you do this; to disable + the warning, you should provide the full set of parameters. See below for details. + + :param mode: The image mode. See: :ref:`concept-modes`. + :param size: The image size. + :param data: A bytes or other buffer object containing raw + data for the given mode. + :param decoder_name: What decoder to use. + :param args: Additional parameters for the given decoder. For the + default encoder ("raw"), it's recommended that you provide the + full set of parameters:: + + frombuffer(mode, size, data, "raw", mode, 0, 1) + + :returns: An :py:class:`~PIL.Image.Image` object. + + .. versionadded:: 1.1.4 + """ + + _check_size(size) + + # may pass tuple instead of argument list + if len(args) == 1 and isinstance(args[0], tuple): + args = args[0] + + if decoder_name == "raw": + if args == (): + args = mode, 0, 1 + if args[0] in _MAPMODES: + im = new(mode, (0, 0)) + im = im._new(core.map_buffer(data, size, decoder_name, 0, args)) + if mode == "P": + from . import ImagePalette + + im.palette = ImagePalette.ImagePalette("RGB", im.im.getpalette("RGB")) + im.readonly = 1 + return im + + return frombytes(mode, size, data, decoder_name, args) + + +class SupportsArrayInterface(Protocol): + """ + An object that has an ``__array_interface__`` dictionary. + """ + + @property + def __array_interface__(self) -> dict[str, Any]: + raise NotImplementedError() + + +class SupportsArrowArrayInterface(Protocol): + """ + An object that has an ``__arrow_c_array__`` method corresponding to the arrow c + data interface. + """ + + def __arrow_c_array__( + self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037 + ) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037 + raise NotImplementedError() + + +def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: + """ + Creates an image memory from an object exporting the array interface + (using the buffer protocol):: + + from PIL import Image + import numpy as np + a = np.zeros((5, 5)) + im = Image.fromarray(a) + + If ``obj`` is not contiguous, then the ``tobytes`` method is called + and :py:func:`~PIL.Image.frombuffer` is used. + + In the case of NumPy, be aware that Pillow modes do not always correspond + to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, + 32-bit signed integer pixels, and 32-bit floating point pixels. + + Pillow images can also be converted to arrays:: + + from PIL import Image + import numpy as np + im = Image.open("hopper.jpg") + a = np.asarray(im) + + When converting Pillow images to arrays however, only pixel values are + transferred. This means that P and PA mode images will lose their palette. + + :param obj: Object with array interface + :param mode: Optional mode to use when reading ``obj``. Will be determined from + type if ``None``. Deprecated. + + This will not be used to convert the data after reading, but will be used to + change how the data is read:: + + from PIL import Image + import numpy as np + a = np.full((1, 1), 300) + im = Image.fromarray(a, mode="L") + im.getpixel((0, 0)) # 44 + im = Image.fromarray(a, mode="RGB") + im.getpixel((0, 0)) # (44, 1, 0) + + See: :ref:`concept-modes` for general information about modes. + :returns: An image object. + + .. versionadded:: 1.1.6 + """ + arr = obj.__array_interface__ + shape = arr["shape"] + ndim = len(shape) + strides = arr.get("strides", None) + if mode is None: + try: + typekey = (1, 1) + shape[2:], arr["typestr"] + except KeyError as e: + msg = "Cannot handle this data type" + raise TypeError(msg) from e + try: + mode, rawmode = _fromarray_typemap[typekey] + except KeyError as e: + typekey_shape, typestr = typekey + msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" + raise TypeError(msg) from e + else: + deprecate("'mode' parameter", 13) + rawmode = mode + if mode in ["1", "L", "I", "P", "F"]: + ndmax = 2 + elif mode == "RGB": + ndmax = 3 + else: + ndmax = 4 + if ndim > ndmax: + msg = f"Too many dimensions: {ndim} > {ndmax}." + raise ValueError(msg) + + size = 1 if ndim == 1 else shape[1], shape[0] + if strides is not None: + if hasattr(obj, "tobytes"): + obj = obj.tobytes() + elif hasattr(obj, "tostring"): + obj = obj.tostring() + else: + msg = "'strides' requires either tobytes() or tostring()" + raise ValueError(msg) + + return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) + + +def fromarrow( + obj: SupportsArrowArrayInterface, mode: str, size: tuple[int, int] +) -> Image: + """Creates an image with zero-copy shared memory from an object exporting + the arrow_c_array interface protocol:: + + from PIL import Image + import pyarrow as pa + arr = pa.array([0]*(5*5*4), type=pa.uint8()) + im = Image.fromarrow(arr, 'RGBA', (5, 5)) + + If the data representation of the ``obj`` is not compatible with + Pillow internal storage, a ValueError is raised. + + Pillow images can also be converted to Arrow objects:: + + from PIL import Image + import pyarrow as pa + im = Image.open('hopper.jpg') + arr = pa.array(im) + + As with array support, when converting Pillow images to arrays, + only pixel values are transferred. This means that P and PA mode + images will lose their palette. + + :param obj: Object with an arrow_c_array interface + :param mode: Image mode. + :param size: Image size. This must match the storage of the arrow object. + :returns: An Image object + + Note that according to the Arrow spec, both the producer and the + consumer should consider the exported array to be immutable, as + unsynchronized updates will potentially cause inconsistent data. + + See: :ref:`arrow-support` for more detailed information + + .. versionadded:: 11.2.1 + + """ + if not hasattr(obj, "__arrow_c_array__"): + msg = "arrow_c_array interface not found" + raise ValueError(msg) + + (schema_capsule, array_capsule) = obj.__arrow_c_array__() + _im = core.new_arrow(mode, size, schema_capsule, array_capsule) + if _im: + return Image()._new(_im) + + msg = "new_arrow returned None without an exception" + raise ValueError(msg) + + +def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: + """Creates an image instance from a QImage image""" + from . import ImageQt + + if not ImageQt.qt_is_installed: + msg = "Qt bindings are not installed" + raise ImportError(msg) + return ImageQt.fromqimage(im) + + +def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile: + """Creates an image instance from a QPixmap image""" + from . import ImageQt + + if not ImageQt.qt_is_installed: + msg = "Qt bindings are not installed" + raise ImportError(msg) + return ImageQt.fromqpixmap(im) + + +_fromarray_typemap = { + # (shape, typestr) => mode, rawmode + # first two members of shape are set to one + ((1, 1), "|b1"): ("1", "1;8"), + ((1, 1), "|u1"): ("L", "L"), + ((1, 1), "|i1"): ("I", "I;8"), + ((1, 1), "u2"): ("I", "I;16B"), + ((1, 1), "i2"): ("I", "I;16BS"), + ((1, 1), "u4"): ("I", "I;32B"), + ((1, 1), "i4"): ("I", "I;32BS"), + ((1, 1), "f4"): ("F", "F;32BF"), + ((1, 1), "f8"): ("F", "F;64BF"), + ((1, 1, 2), "|u1"): ("LA", "LA"), + ((1, 1, 3), "|u1"): ("RGB", "RGB"), + ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), + # shortcuts: + ((1, 1), f"{_ENDIAN}i4"): ("I", "I"), + ((1, 1), f"{_ENDIAN}f4"): ("F", "F"), +} + + +def _decompression_bomb_check(size: tuple[int, int]) -> None: + if MAX_IMAGE_PIXELS is None: + return + + pixels = max(1, size[0]) * max(1, size[1]) + + if pixels > 2 * MAX_IMAGE_PIXELS: + msg = ( + f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " + "pixels, could be decompression bomb DOS attack." + ) + raise DecompressionBombError(msg) + + if pixels > MAX_IMAGE_PIXELS: + warnings.warn( + f"Image size ({pixels} pixels) exceeds limit of {MAX_IMAGE_PIXELS} pixels, " + "could be decompression bomb DOS attack.", + DecompressionBombWarning, + ) + + +def open( + fp: StrOrBytesPath | IO[bytes], + mode: Literal["r"] = "r", + formats: list[str] | tuple[str, ...] | None = None, +) -> ImageFile.ImageFile: + """ + Opens and identifies the given image file. + + This is a lazy operation; this function identifies the file, but + the file remains open and the actual image data is not read from + the file until you try to process the data (or call the + :py:meth:`~PIL.Image.Image.load` method). See + :py:func:`~PIL.Image.new`. See :ref:`file-handling`. + + :param fp: A filename (string), os.PathLike object or a file object. + The file object must implement ``file.read``, + ``file.seek``, and ``file.tell`` methods, + and be opened in binary mode. The file object will also seek to zero + before reading. + :param mode: The mode. If given, this argument must be "r". + :param formats: A list or tuple of formats to attempt to load the file in. + This can be used to restrict the set of formats checked. + Pass ``None`` to try all supported formats. You can print the set of + available formats by running ``python3 -m PIL`` or using + the :py:func:`PIL.features.pilinfo` function. + :returns: An :py:class:`~PIL.Image.Image` object. + :exception FileNotFoundError: If the file cannot be found. + :exception PIL.UnidentifiedImageError: If the image cannot be opened and + identified. + :exception ValueError: If the ``mode`` is not "r", or if a ``StringIO`` + instance is used for ``fp``. + :exception TypeError: If ``formats`` is not ``None``, a list or a tuple. + """ + + if mode != "r": + msg = f"bad mode {repr(mode)}" # type: ignore[unreachable] + raise ValueError(msg) + elif isinstance(fp, io.StringIO): + msg = ( # type: ignore[unreachable] + "StringIO cannot be used to open an image. " + "Binary data must be used instead." + ) + raise ValueError(msg) + + if formats is None: + formats = ID + elif not isinstance(formats, (list, tuple)): + msg = "formats must be a list or tuple" # type: ignore[unreachable] + raise TypeError(msg) + + exclusive_fp = False + filename: str | bytes = "" + if is_path(fp): + filename = os.fspath(fp) + fp = builtins.open(filename, "rb") + exclusive_fp = True + else: + fp = cast(IO[bytes], fp) + + try: + fp.seek(0) + except (AttributeError, io.UnsupportedOperation): + fp = io.BytesIO(fp.read()) + exclusive_fp = True + + prefix = fp.read(16) + + preinit() + + warning_messages: list[str] = [] + + def _open_core( + fp: IO[bytes], + filename: str | bytes, + prefix: bytes, + formats: list[str] | tuple[str, ...], + ) -> ImageFile.ImageFile | None: + for i in formats: + i = i.upper() + if i not in OPEN: + init() + try: + factory, accept = OPEN[i] + result = not accept or accept(prefix) + if isinstance(result, str): + warning_messages.append(result) + elif result: + fp.seek(0) + im = factory(fp, filename) + _decompression_bomb_check(im.size) + return im + except (SyntaxError, IndexError, TypeError, struct.error) as e: + if WARN_POSSIBLE_FORMATS: + warning_messages.append(i + " opening failed. " + str(e)) + except BaseException: + if exclusive_fp: + fp.close() + raise + return None + + im = _open_core(fp, filename, prefix, formats) + + if im is None and formats is ID: + checked_formats = ID.copy() + if init(): + im = _open_core( + fp, + filename, + prefix, + tuple(format for format in formats if format not in checked_formats), + ) + + if im: + im._exclusive_fp = exclusive_fp + return im + + if exclusive_fp: + fp.close() + for message in warning_messages: + warnings.warn(message) + msg = "cannot identify image file %r" % (filename if filename else fp) + raise UnidentifiedImageError(msg) + + +# +# Image processing. + + +def alpha_composite(im1: Image, im2: Image) -> Image: + """ + Alpha composite im2 over im1. + + :param im1: The first image. Must have mode RGBA. + :param im2: The second image. Must have mode RGBA, and the same size as + the first image. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + im1.load() + im2.load() + return im1._new(core.alpha_composite(im1.im, im2.im)) + + +def blend(im1: Image, im2: Image, alpha: float) -> Image: + """ + Creates a new image by interpolating between two input images, using + a constant alpha:: + + out = image1 * (1.0 - alpha) + image2 * alpha + + :param im1: The first image. + :param im2: The second image. Must have the same mode and size as + the first image. + :param alpha: The interpolation alpha factor. If alpha is 0.0, a + copy of the first image is returned. If alpha is 1.0, a copy of + the second image is returned. There are no restrictions on the + alpha value. If necessary, the result is clipped to fit into + the allowed output range. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + im1.load() + im2.load() + return im1._new(core.blend(im1.im, im2.im, alpha)) + + +def composite(image1: Image, image2: Image, mask: Image) -> Image: + """ + Create composite image by blending images using a transparency mask. + + :param image1: The first image. + :param image2: The second image. Must have the same mode and + size as the first image. + :param mask: A mask image. This image can have mode + "1", "L", or "RGBA", and must have the same size as the + other two images. + """ + + image = image2.copy() + image.paste(image1, None, mask) + return image + + +def eval(image: Image, *args: Callable[[int], float]) -> Image: + """ + Applies the function (which should take one argument) to each pixel + in the given image. If the image has more than one band, the same + function is applied to each band. Note that the function is + evaluated once for each possible pixel value, so you cannot use + random components or other generators. + + :param image: The input image. + :param function: A function object, taking one integer argument. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + return image.point(args[0]) + + +def merge(mode: str, bands: Sequence[Image]) -> Image: + """ + Merge a set of single band images into a new multiband image. + + :param mode: The mode to use for the output image. See: + :ref:`concept-modes`. + :param bands: A sequence containing one single-band image for + each band in the output image. All bands must have the + same size. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + + if getmodebands(mode) != len(bands) or "*" in mode: + msg = "wrong number of bands" + raise ValueError(msg) + for band in bands[1:]: + if band.mode != getmodetype(mode): + msg = "mode mismatch" + raise ValueError(msg) + if band.size != bands[0].size: + msg = "size mismatch" + raise ValueError(msg) + for band in bands: + band.load() + return bands[0]._new(core.merge(mode, *[b.im for b in bands])) + + +# -------------------------------------------------------------------- +# Plugin registry + + +def register_open( + id: str, + factory: ( + Callable[[IO[bytes], str | bytes], ImageFile.ImageFile] + | type[ImageFile.ImageFile] + ), + accept: Callable[[bytes], bool | str] | None = None, +) -> None: + """ + Register an image file plugin. This function should not be used + in application code. + + :param id: An image format identifier. + :param factory: An image file factory method. + :param accept: An optional function that can be used to quickly + reject images having another format. + """ + id = id.upper() + if id not in ID: + ID.append(id) + OPEN[id] = factory, accept + + +def register_mime(id: str, mimetype: str) -> None: + """ + Registers an image MIME type by populating ``Image.MIME``. This function + should not be used in application code. + + ``Image.MIME`` provides a mapping from image format identifiers to mime + formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can + provide a different result for specific images. + + :param id: An image format identifier. + :param mimetype: The image MIME type for this format. + """ + MIME[id.upper()] = mimetype + + +def register_save( + id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] +) -> None: + """ + Registers an image save function. This function should not be + used in application code. + + :param id: An image format identifier. + :param driver: A function to save images in this format. + """ + SAVE[id.upper()] = driver + + +def register_save_all( + id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] +) -> None: + """ + Registers an image function to save all the frames + of a multiframe format. This function should not be + used in application code. + + :param id: An image format identifier. + :param driver: A function to save images in this format. + """ + SAVE_ALL[id.upper()] = driver + + +def register_extension(id: str, extension: str) -> None: + """ + Registers an image extension. This function should not be + used in application code. + + :param id: An image format identifier. + :param extension: An extension used for this format. + """ + EXTENSION[extension.lower()] = id.upper() + + +def register_extensions(id: str, extensions: list[str]) -> None: + """ + Registers image extensions. This function should not be + used in application code. + + :param id: An image format identifier. + :param extensions: A list of extensions used for this format. + """ + for extension in extensions: + register_extension(id, extension) + + +def registered_extensions() -> dict[str, str]: + """ + Returns a dictionary containing all file extensions belonging + to registered plugins + """ + init() + return EXTENSION + + +def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: + """ + Registers an image decoder. This function should not be + used in application code. + + :param name: The name of the decoder + :param decoder: An ImageFile.PyDecoder object + + .. versionadded:: 4.1.0 + """ + DECODERS[name] = decoder + + +def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: + """ + Registers an image encoder. This function should not be + used in application code. + + :param name: The name of the encoder + :param encoder: An ImageFile.PyEncoder object + + .. versionadded:: 4.1.0 + """ + ENCODERS[name] = encoder + + +# -------------------------------------------------------------------- +# Simple display support. + + +def _show(image: Image, **options: Any) -> None: + from . import ImageShow + + ImageShow.show(image, **options) + + +# -------------------------------------------------------------------- +# Effects + + +def effect_mandelbrot( + size: tuple[int, int], extent: tuple[float, float, float, float], quality: int +) -> Image: + """ + Generate a Mandelbrot set covering the given extent. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param extent: The extent to cover, as a 4-tuple: + (x0, y0, x1, y1). + :param quality: Quality. + """ + return Image()._new(core.effect_mandelbrot(size, extent, quality)) + + +def effect_noise(size: tuple[int, int], sigma: float) -> Image: + """ + Generate Gaussian noise centered around 128. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param sigma: Standard deviation of noise. + """ + return Image()._new(core.effect_noise(size, sigma)) + + +def linear_gradient(mode: str) -> Image: + """ + Generate 256x256 linear gradient from black to white, top to bottom. + + :param mode: Input mode. + """ + return Image()._new(core.linear_gradient(mode)) + + +def radial_gradient(mode: str) -> Image: + """ + Generate 256x256 radial gradient from black to white, centre to edge. + + :param mode: Input mode. + """ + return Image()._new(core.radial_gradient(mode)) + + +# -------------------------------------------------------------------- +# Resources + + +def _apply_env_variables(env: dict[str, str] | None = None) -> None: + env_dict = env if env is not None else os.environ + + for var_name, setter in [ + ("PILLOW_ALIGNMENT", core.set_alignment), + ("PILLOW_BLOCK_SIZE", core.set_block_size), + ("PILLOW_BLOCKS_MAX", core.set_blocks_max), + ]: + if var_name not in env_dict: + continue + + var = env_dict[var_name].lower() + + units = 1 + for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: + if var.endswith(postfix): + units = mul + var = var[: -len(postfix)] + + try: + var_int = int(var) * units + except ValueError: + warnings.warn(f"{var_name} is not int") + continue + + try: + setter(var_int) + except ValueError as e: + warnings.warn(f"{var_name}: {e}") + + +_apply_env_variables() +atexit.register(core.clear_cache) + + +if TYPE_CHECKING: + _ExifBase = MutableMapping[int, Any] +else: + _ExifBase = MutableMapping + + +class Exif(_ExifBase): + """ + This class provides read and write access to EXIF image data:: + + from PIL import Image + im = Image.open("exif.png") + exif = im.getexif() # Returns an instance of this class + + Information can be read and written, iterated over or deleted:: + + print(exif[274]) # 1 + exif[274] = 2 + for k, v in exif.items(): + print("Tag", k, "Value", v) # Tag 274 Value 2 + del exif[274] + + To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` + returns a dictionary:: + + from PIL import ExifTags + im = Image.open("exif_gps.jpg") + exif = im.getexif() + gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) + print(gps_ifd) + + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``, + ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. + + :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: + + print(exif[ExifTags.Base.Software]) # PIL + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 + """ + + endian: str | None = None + bigtiff = False + _loaded = False + + def __init__(self) -> None: + self._data: dict[int, Any] = {} + self._hidden_data: dict[int, Any] = {} + self._ifds: dict[int, dict[int, Any]] = {} + self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None + self._loaded_exif: bytes | None = None + + def _fixup(self, value: Any) -> Any: + try: + if len(value) == 1 and isinstance(value, tuple): + return value[0] + except Exception: + pass + return value + + def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]: + # Helper function + # returns a dict with any single item tuples/lists as individual values + return {k: self._fixup(v) for k, v in src_dict.items()} + + def _get_ifd_dict( + self, offset: int, group: int | None = None + ) -> dict[int, Any] | None: + try: + # an offset pointer to the location of the nested embedded IFD. + # It should be a long, but may be corrupted. + self.fp.seek(offset) + except (KeyError, TypeError): + return None + else: + from . import TiffImagePlugin + + info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) + info.load(self.fp) + return self._fixup_dict(dict(info)) + + def _get_head(self) -> bytes: + version = b"\x2b" if self.bigtiff else b"\x2a" + if self.endian == "<": + head = b"II" + version + b"\x00" + o32le(8) + else: + head = b"MM\x00" + version + o32be(8) + if self.bigtiff: + head += o32le(8) if self.endian == "<" else o32be(8) + head += b"\x00\x00\x00\x00" + return head + + def load(self, data: bytes) -> None: + # Extract EXIF information. This is highly experimental, + # and is likely to be replaced with something better in a future + # version. + + # The EXIF record consists of a TIFF file embedded in a JPEG + # application marker (!). + if data == self._loaded_exif: + return + self._loaded_exif = data + self._data.clear() + self._hidden_data.clear() + self._ifds.clear() + while data and data.startswith(b"Exif\x00\x00"): + data = data[6:] + if not data: + self._info = None + return + + self.fp: IO[bytes] = io.BytesIO(data) + self.head = self.fp.read(8) + # process dictionary + from . import TiffImagePlugin + + self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) + self.endian = self._info._endian + self.fp.seek(self._info.next) + self._info.load(self.fp) + + def load_from_fp(self, fp: IO[bytes], offset: int | None = None) -> None: + self._loaded_exif = None + self._data.clear() + self._hidden_data.clear() + self._ifds.clear() + + # process dictionary + from . import TiffImagePlugin + + self.fp = fp + if offset is not None: + self.head = self._get_head() + else: + self.head = self.fp.read(8) + self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) + if self.endian is None: + self.endian = self._info._endian + if offset is None: + offset = self._info.next + self.fp.tell() + self.fp.seek(offset) + self._info.load(self.fp) + + def _get_merged_dict(self) -> dict[int, Any]: + merged_dict = dict(self) + + # get EXIF extension + if ExifTags.IFD.Exif in self: + ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif], ExifTags.IFD.Exif) + if ifd: + merged_dict.update(ifd) + + # GPS + if ExifTags.IFD.GPSInfo in self: + merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( + self[ExifTags.IFD.GPSInfo], ExifTags.IFD.GPSInfo + ) + + return merged_dict + + def tobytes(self, offset: int = 8) -> bytes: + from . import TiffImagePlugin + + head = self._get_head() + ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) + for tag, ifd_dict in self._ifds.items(): + if tag not in self: + ifd[tag] = ifd_dict + for tag, value in self.items(): + if tag in [ + ExifTags.IFD.Exif, + ExifTags.IFD.GPSInfo, + ] and not isinstance(value, dict): + value = self.get_ifd(tag) + if ( + tag == ExifTags.IFD.Exif + and ExifTags.IFD.Interop in value + and not isinstance(value[ExifTags.IFD.Interop], dict) + ): + value = value.copy() + value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop) + ifd[tag] = value + return b"Exif\x00\x00" + head + ifd.tobytes(offset) + + def get_ifd(self, tag: int) -> dict[int, Any]: + if tag not in self._ifds: + if tag == ExifTags.IFD.IFD1: + if self._info is not None and self._info.next != 0: + ifd = self._get_ifd_dict(self._info.next) + if ifd is not None: + self._ifds[tag] = ifd + elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: + offset = self._hidden_data.get(tag, self.get(tag)) + if offset is not None: + ifd = self._get_ifd_dict(offset, tag) + if ifd is not None: + self._ifds[tag] = ifd + elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]: + if ExifTags.IFD.Exif not in self._ifds: + self.get_ifd(ExifTags.IFD.Exif) + tag_data = self._ifds[ExifTags.IFD.Exif][tag] + if tag == ExifTags.IFD.MakerNote: + from .TiffImagePlugin import ImageFileDirectory_v2 + + if tag_data.startswith(b"FUJIFILM"): + ifd_offset = i32le(tag_data, 8) + ifd_data = tag_data[ifd_offset:] + + makernote = {} + for i in range(struct.unpack(" 4: + (offset,) = struct.unpack("H", tag_data[:2])[0]): + ifd_tag, typ, count, data = struct.unpack( + ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] + ) + if ifd_tag == 0x1101: + # CameraInfo + (offset,) = struct.unpack(">L", data) + self.fp.seek(offset) + + camerainfo: dict[str, int | bytes] = { + "ModelID": self.fp.read(4) + } + + self.fp.read(4) + # Seconds since 2000 + camerainfo["TimeStamp"] = i32le(self.fp.read(12)) + + self.fp.read(4) + camerainfo["InternalSerialNumber"] = self.fp.read(4) + + self.fp.read(12) + parallax = self.fp.read(4) + handler = ImageFileDirectory_v2._load_dispatch[ + TiffTags.FLOAT + ][1] + camerainfo["Parallax"] = handler( + ImageFileDirectory_v2(), parallax, False + )[0] + + self.fp.read(4) + camerainfo["Category"] = self.fp.read(2) + + makernote = {0x1101: camerainfo} + self._ifds[tag] = makernote + else: + # Interop + ifd = self._get_ifd_dict(tag_data, tag) + if ifd is not None: + self._ifds[tag] = ifd + ifd = self._ifds.setdefault(tag, {}) + if tag == ExifTags.IFD.Exif and self._hidden_data: + ifd = { + k: v + for (k, v) in ifd.items() + if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote) + } + return ifd + + def hide_offsets(self) -> None: + for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo): + if tag in self: + self._hidden_data[tag] = self[tag] + del self[tag] + + def __str__(self) -> str: + if self._info is not None: + # Load all keys into self._data + for tag in self._info: + self[tag] + + return str(self._data) + + def __len__(self) -> int: + keys = set(self._data) + if self._info is not None: + keys.update(self._info) + return len(keys) + + def __getitem__(self, tag: int) -> Any: + if self._info is not None and tag not in self._data and tag in self._info: + self._data[tag] = self._fixup(self._info[tag]) + del self._info[tag] + return self._data[tag] + + def __contains__(self, tag: object) -> bool: + return tag in self._data or (self._info is not None and tag in self._info) + + def __setitem__(self, tag: int, value: Any) -> None: + if self._info is not None and tag in self._info: + del self._info[tag] + self._data[tag] = value + + def __delitem__(self, tag: int) -> None: + if self._info is not None and tag in self._info: + del self._info[tag] + else: + del self._data[tag] + + def __iter__(self) -> Iterator[int]: + keys = set(self._data) + if self._info is not None: + keys.update(self._info) + return iter(keys) diff --git a/python_modules/PIL/ImageChops.py b/python_modules/PIL/ImageChops.py new file mode 100644 index 000000000..29a5c995f --- /dev/null +++ b/python_modules/PIL/ImageChops.py @@ -0,0 +1,311 @@ +# +# The Python Imaging Library. +# $Id$ +# +# standard channel operations +# +# History: +# 1996-03-24 fl Created +# 1996-08-13 fl Added logical operations (for "1" images) +# 2000-10-12 fl Added offset method (from Image.py) +# +# Copyright (c) 1997-2000 by Secret Labs AB +# Copyright (c) 1996-2000 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +from __future__ import annotations + +from . import Image + + +def constant(image: Image.Image, value: int) -> Image.Image: + """Fill a channel with a given gray level. + + :rtype: :py:class:`~PIL.Image.Image` + """ + + return Image.new("L", image.size, value) + + +def duplicate(image: Image.Image) -> Image.Image: + """Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`. + + :rtype: :py:class:`~PIL.Image.Image` + """ + + return image.copy() + + +def invert(image: Image.Image) -> Image.Image: + """ + Invert an image (channel). :: + + out = MAX - image + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image.load() + return image._new(image.im.chop_invert()) + + +def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Compares the two images, pixel by pixel, and returns a new image containing + the lighter values. :: + + out = max(image1, image2) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_lighter(image2.im)) + + +def darker(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Compares the two images, pixel by pixel, and returns a new image containing + the darker values. :: + + out = min(image1, image2) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_darker(image2.im)) + + +def difference(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Returns the absolute value of the pixel-by-pixel difference between the two + images. :: + + out = abs(image1 - image2) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_difference(image2.im)) + + +def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Superimposes two images on top of each other. + + If you multiply an image with a solid black image, the result is black. If + you multiply with a solid white image, the image is unaffected. :: + + out = image1 * image2 / MAX + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_multiply(image2.im)) + + +def screen(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Superimposes two inverted images on top of each other. :: + + out = MAX - ((MAX - image1) * (MAX - image2) / MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_screen(image2.im)) + + +def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Superimposes two images on top of each other using the Soft Light algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_soft_light(image2.im)) + + +def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Superimposes two images on top of each other using the Hard Light algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_hard_light(image2.im)) + + +def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image: + """ + Superimposes two images on top of each other using the Overlay algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_overlay(image2.im)) + + +def add( + image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0 +) -> Image.Image: + """ + Adds two images, dividing the result by scale and adding the + offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: + + out = ((image1 + image2) / scale + offset) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_add(image2.im, scale, offset)) + + +def subtract( + image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0 +) -> Image.Image: + """ + Subtracts two images, dividing the result by scale and adding the offset. + If omitted, scale defaults to 1.0, and offset to 0.0. :: + + out = ((image1 - image2) / scale + offset) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_subtract(image2.im, scale, offset)) + + +def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: + """Add two images, without clipping the result. :: + + out = ((image1 + image2) % MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_add_modulo(image2.im)) + + +def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: + """Subtract two images, without clipping the result. :: + + out = ((image1 - image2) % MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_subtract_modulo(image2.im)) + + +def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image: + """Logical AND between two images. + + Both of the images must have mode "1". If you would like to perform a + logical AND on an image with a mode other than "1", try + :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask + as the second image. :: + + out = ((image1 and image2) % MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_and(image2.im)) + + +def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image: + """Logical OR between two images. + + Both of the images must have mode "1". :: + + out = ((image1 or image2) % MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_or(image2.im)) + + +def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image: + """Logical XOR between two images. + + Both of the images must have mode "1". :: + + out = ((bool(image1) != bool(image2)) % MAX) + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_xor(image2.im)) + + +def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image: + """Blend images using constant transparency weight. Alias for + :py:func:`PIL.Image.blend`. + + :rtype: :py:class:`~PIL.Image.Image` + """ + + return Image.blend(image1, image2, alpha) + + +def composite( + image1: Image.Image, image2: Image.Image, mask: Image.Image +) -> Image.Image: + """Create composite using transparency mask. Alias for + :py:func:`PIL.Image.composite`. + + :rtype: :py:class:`~PIL.Image.Image` + """ + + return Image.composite(image1, image2, mask) + + +def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image: + """Returns a copy of the image where data has been offset by the given + distances. Data wraps around the edges. If ``yoffset`` is omitted, it + is assumed to be equal to ``xoffset``. + + :param image: Input image. + :param xoffset: The horizontal distance. + :param yoffset: The vertical distance. If omitted, both + distances are set to the same value. + :rtype: :py:class:`~PIL.Image.Image` + """ + + if yoffset is None: + yoffset = xoffset + image.load() + return image._new(image.im.offset(xoffset, yoffset)) diff --git a/python_modules/PIL/ImageCms.py b/python_modules/PIL/ImageCms.py new file mode 100644 index 000000000..a1584f111 --- /dev/null +++ b/python_modules/PIL/ImageCms.py @@ -0,0 +1,1123 @@ +# The Python Imaging Library. +# $Id$ + +# Optional color management support, based on Kevin Cazabon's PyCMS +# library. + +# Originally released under LGPL. Graciously donated to PIL in +# March 2009, for distribution under the standard PIL license + +# History: + +# 2009-03-08 fl Added to PIL. + +# Copyright (C) 2002-2003 Kevin Cazabon +# Copyright (c) 2009 by Fredrik Lundh +# Copyright (c) 2013 by Eric Soroos + +# See the README file for information on usage and redistribution. See +# below for the original description. +from __future__ import annotations + +import operator +import sys +from enum import IntEnum, IntFlag +from functools import reduce +from typing import Any, Literal, SupportsFloat, SupportsInt, Union + +from . import Image, __version__ +from ._deprecate import deprecate +from ._typing import SupportsRead + +try: + from . import _imagingcms as core + + _CmsProfileCompatible = Union[ + str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile" + ] +except ImportError as ex: + # Allow error import for doc purposes, but error out when accessing + # anything in core. + from ._util import DeferredError + + core = DeferredError.new(ex) + +_DESCRIPTION = """ +pyCMS + + a Python / PIL interface to the littleCMS ICC Color Management System + Copyright (C) 2002-2003 Kevin Cazabon + kevin@cazabon.com + https://www.cazabon.com + + pyCMS home page: https://www.cazabon.com/pyCMS + littleCMS home page: https://www.littlecms.com + (littleCMS is Copyright (C) 1998-2001 Marti Maria) + + Originally released under LGPL. Graciously donated to PIL in + March 2009, for distribution under the standard PIL license + + The pyCMS.py module provides a "clean" interface between Python/PIL and + pyCMSdll, taking care of some of the more complex handling of the direct + pyCMSdll functions, as well as error-checking and making sure that all + relevant data is kept together. + + While it is possible to call pyCMSdll functions directly, it's not highly + recommended. + + Version History: + + 1.0.0 pil Oct 2013 Port to LCMS 2. + + 0.1.0 pil mod March 10, 2009 + + Renamed display profile to proof profile. The proof + profile is the profile of the device that is being + simulated, not the profile of the device which is + actually used to display/print the final simulation + (that'd be the output profile) - also see LCMSAPI.txt + input colorspace -> using 'renderingIntent' -> proof + colorspace -> using 'proofRenderingIntent' -> output + colorspace + + Added LCMS FLAGS support. + Added FLAGS["SOFTPROOFING"] as default flag for + buildProofTransform (otherwise the proof profile/intent + would be ignored). + + 0.1.0 pil March 2009 - added to PIL, as PIL.ImageCms + + 0.0.2 alpha Jan 6, 2002 + + Added try/except statements around type() checks of + potential CObjects... Python won't let you use type() + on them, and raises a TypeError (stupid, if you ask + me!) + + Added buildProofTransformFromOpenProfiles() function. + Additional fixes in DLL, see DLL code for details. + + 0.0.1 alpha first public release, Dec. 26, 2002 + + Known to-do list with current version (of Python interface, not pyCMSdll): + + none + +""" + +_VERSION = "1.0.0 pil" + + +def __getattr__(name: str) -> Any: + if name == "DESCRIPTION": + deprecate("PIL.ImageCms.DESCRIPTION", 12) + return _DESCRIPTION + elif name == "VERSION": + deprecate("PIL.ImageCms.VERSION", 12) + return _VERSION + elif name == "FLAGS": + deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") + return _FLAGS + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) + + +# --------------------------------------------------------------------. + + +# +# intent/direction values + + +class Intent(IntEnum): + PERCEPTUAL = 0 + RELATIVE_COLORIMETRIC = 1 + SATURATION = 2 + ABSOLUTE_COLORIMETRIC = 3 + + +class Direction(IntEnum): + INPUT = 0 + OUTPUT = 1 + PROOF = 2 + + +# +# flags + + +class Flags(IntFlag): + """Flags and documentation are taken from ``lcms2.h``.""" + + NONE = 0 + NOCACHE = 0x0040 + """Inhibit 1-pixel cache""" + NOOPTIMIZE = 0x0100 + """Inhibit optimizations""" + NULLTRANSFORM = 0x0200 + """Don't transform anyway""" + GAMUTCHECK = 0x1000 + """Out of Gamut alarm""" + SOFTPROOFING = 0x4000 + """Do softproofing""" + BLACKPOINTCOMPENSATION = 0x2000 + NOWHITEONWHITEFIXUP = 0x0004 + """Don't fix scum dot""" + HIGHRESPRECALC = 0x0400 + """Use more memory to give better accuracy""" + LOWRESPRECALC = 0x0800 + """Use less memory to minimize resources""" + # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: + USE_8BITS_DEVICELINK = 0x0008 + """Create 8 bits devicelinks""" + GUESSDEVICECLASS = 0x0020 + """Guess device class (for ``transform2devicelink``)""" + KEEP_SEQUENCE = 0x0080 + """Keep profile sequence for devicelink creation""" + FORCE_CLUT = 0x0002 + """Force CLUT optimization""" + CLUT_POST_LINEARIZATION = 0x0001 + """create postlinearization tables if possible""" + CLUT_PRE_LINEARIZATION = 0x0010 + """create prelinearization tables if possible""" + NONEGATIVES = 0x8000 + """Prevent negative numbers in floating point transforms""" + COPY_ALPHA = 0x04000000 + """Alpha channels are copied on ``cmsDoTransform()``""" + NODEFAULTRESOURCEDEF = 0x01000000 + + _GRIDPOINTS_1 = 1 << 16 + _GRIDPOINTS_2 = 2 << 16 + _GRIDPOINTS_4 = 4 << 16 + _GRIDPOINTS_8 = 8 << 16 + _GRIDPOINTS_16 = 16 << 16 + _GRIDPOINTS_32 = 32 << 16 + _GRIDPOINTS_64 = 64 << 16 + _GRIDPOINTS_128 = 128 << 16 + + @staticmethod + def GRIDPOINTS(n: int) -> Flags: + """ + Fine-tune control over number of gridpoints + + :param n: :py:class:`int` in range ``0 <= n <= 255`` + """ + return Flags.NONE | ((n & 0xFF) << 16) + + +_MAX_FLAG = reduce(operator.or_, Flags) + + +_FLAGS = { + "MATRIXINPUT": 1, + "MATRIXOUTPUT": 2, + "MATRIXONLY": (1 | 2), + "NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot + # Don't create prelinearization tables on precalculated transforms + # (internal use): + "NOPRELINEARIZATION": 16, + "GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink) + "NOTCACHE": 64, # Inhibit 1-pixel cache + "NOTPRECALC": 256, + "NULLTRANSFORM": 512, # Don't transform anyway + "HIGHRESPRECALC": 1024, # Use more memory to give better accuracy + "LOWRESPRECALC": 2048, # Use less memory to minimize resources + "WHITEBLACKCOMPENSATION": 8192, + "BLACKPOINTCOMPENSATION": 8192, + "GAMUTCHECK": 4096, # Out of Gamut alarm + "SOFTPROOFING": 16384, # Do softproofing + "PRESERVEBLACK": 32768, # Black preservation + "NODEFAULTRESOURCEDEF": 16777216, # CRD special + "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints +} + + +# --------------------------------------------------------------------. +# Experimental PIL-level API +# --------------------------------------------------------------------. + +## +# Profile. + + +class ImageCmsProfile: + def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None: + """ + :param profile: Either a string representing a filename, + a file like object containing a profile or a + low-level profile object + + """ + self.filename = None + self.product_name = None # profile.product_name + self.product_info = None # profile.product_info + + if isinstance(profile, str): + if sys.platform == "win32": + profile_bytes_path = profile.encode() + try: + profile_bytes_path.decode("ascii") + except UnicodeDecodeError: + with open(profile, "rb") as f: + self.profile = core.profile_frombytes(f.read()) + return + self.filename = profile + self.profile = core.profile_open(profile) + elif hasattr(profile, "read"): + self.profile = core.profile_frombytes(profile.read()) + elif isinstance(profile, core.CmsProfile): + self.profile = profile + else: + msg = "Invalid type for Profile" # type: ignore[unreachable] + raise TypeError(msg) + + def tobytes(self) -> bytes: + """ + Returns the profile in a format suitable for embedding in + saved images. + + :returns: a bytes object containing the ICC profile. + """ + + return core.profile_tobytes(self.profile) + + +class ImageCmsTransform(Image.ImagePointHandler): + """ + Transform. This can be used with the procedural API, or with the standard + :py:func:`~PIL.Image.Image.point` method. + + Will return the output profile in the ``output.info['icc_profile']``. + """ + + def __init__( + self, + input: ImageCmsProfile, + output: ImageCmsProfile, + input_mode: str, + output_mode: str, + intent: Intent = Intent.PERCEPTUAL, + proof: ImageCmsProfile | None = None, + proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, + flags: Flags = Flags.NONE, + ): + supported_modes = ( + "RGB", + "RGBA", + "RGBX", + "CMYK", + "I;16", + "I;16L", + "I;16B", + "YCbCr", + "LAB", + "L", + "1", + ) + for mode in (input_mode, output_mode): + if mode not in supported_modes: + deprecate( + mode, + 12, + { + "L;16": "I;16 or I;16L", + "L:16B": "I;16B", + "YCCA": "YCbCr", + "YCC": "YCbCr", + }.get(mode), + ) + if proof is None: + self.transform = core.buildTransform( + input.profile, output.profile, input_mode, output_mode, intent, flags + ) + else: + self.transform = core.buildProofTransform( + input.profile, + output.profile, + proof.profile, + input_mode, + output_mode, + intent, + proof_intent, + flags, + ) + # Note: inputMode and outputMode are for pyCMS compatibility only + self.input_mode = self.inputMode = input_mode + self.output_mode = self.outputMode = output_mode + + self.output_profile = output + + def point(self, im: Image.Image) -> Image.Image: + return self.apply(im) + + def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image: + if imOut is None: + imOut = Image.new(self.output_mode, im.size, None) + self.transform.apply(im.getim(), imOut.getim()) + imOut.info["icc_profile"] = self.output_profile.tobytes() + return imOut + + def apply_in_place(self, im: Image.Image) -> Image.Image: + if im.mode != self.output_mode: + msg = "mode mismatch" + raise ValueError(msg) # wrong output mode + self.transform.apply(im.getim(), im.getim()) + im.info["icc_profile"] = self.output_profile.tobytes() + return im + + +def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | None: + """ + (experimental) Fetches the profile for the current display device. + + :returns: ``None`` if the profile is not known. + """ + + if sys.platform != "win32": + return None + + from . import ImageWin # type: ignore[unused-ignore, unreachable] + + if isinstance(handle, ImageWin.HDC): + profile = core.get_display_profile_win32(int(handle), 1) + else: + profile = core.get_display_profile_win32(int(handle or 0)) + if profile is None: + return None + return ImageCmsProfile(profile) + + +# --------------------------------------------------------------------. +# pyCMS compatible layer +# --------------------------------------------------------------------. + + +class PyCMSError(Exception): + """(pyCMS) Exception class. + This is used for all errors in the pyCMS API.""" + + pass + + +def profileToProfile( + im: Image.Image, + inputProfile: _CmsProfileCompatible, + outputProfile: _CmsProfileCompatible, + renderingIntent: Intent = Intent.PERCEPTUAL, + outputMode: str | None = None, + inPlace: bool = False, + flags: Flags = Flags.NONE, +) -> Image.Image | None: + """ + (pyCMS) Applies an ICC transformation to a given image, mapping from + ``inputProfile`` to ``outputProfile``. + + If the input or output profiles specified are not valid filenames, a + :exc:`PyCMSError` will be raised. If ``inPlace`` is ``True`` and + ``outputMode != im.mode``, a :exc:`PyCMSError` will be raised. + If an error occurs during application of the profiles, + a :exc:`PyCMSError` will be raised. + If ``outputMode`` is not a mode supported by the ``outputProfile`` (or by pyCMS), + a :exc:`PyCMSError` will be raised. + + This function applies an ICC transformation to im from ``inputProfile``'s + color space to ``outputProfile``'s color space using the specified rendering + intent to decide how to handle out-of-gamut colors. + + ``outputMode`` can be used to specify that a color mode conversion is to + be done using these profiles, but the specified profiles must be able + to handle that mode. I.e., if converting im from RGB to CMYK using + profiles, the input profile must handle RGB data, and the output + profile must handle CMYK data. + + :param im: An open :py:class:`~PIL.Image.Image` object (i.e. Image.new(...) + or Image.open(...), etc.) + :param inputProfile: String, as a valid filename path to the ICC input + profile you wish to use for this image, or a profile object + :param outputProfile: String, as a valid filename path to the ICC output + profile you wish to use for this image, or a profile object + :param renderingIntent: Integer (0-3) specifying the rendering intent you + wish to use for the transform + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :param outputMode: A valid PIL mode for the output image (i.e. "RGB", + "CMYK", etc.). Note: if rendering the image "inPlace", outputMode + MUST be the same mode as the input, or omitted completely. If + omitted, the outputMode will be the same as the mode of the input + image (im.mode) + :param inPlace: Boolean. If ``True``, the original image is modified in-place, + and ``None`` is returned. If ``False`` (default), a new + :py:class:`~PIL.Image.Image` object is returned with the transform applied. + :param flags: Integer (0-...) specifying additional flags + :returns: Either None or a new :py:class:`~PIL.Image.Image` object, depending on + the value of ``inPlace`` + :exception PyCMSError: + """ + + if outputMode is None: + outputMode = im.mode + + if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) + + if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" + raise PyCMSError(msg) + + try: + if not isinstance(inputProfile, ImageCmsProfile): + inputProfile = ImageCmsProfile(inputProfile) + if not isinstance(outputProfile, ImageCmsProfile): + outputProfile = ImageCmsProfile(outputProfile) + transform = ImageCmsTransform( + inputProfile, + outputProfile, + im.mode, + outputMode, + renderingIntent, + flags=flags, + ) + if inPlace: + transform.apply_in_place(im) + imOut = None + else: + imOut = transform.apply(im) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + return imOut + + +def getOpenProfile( + profileFilename: str | SupportsRead[bytes] | core.CmsProfile, +) -> ImageCmsProfile: + """ + (pyCMS) Opens an ICC profile file. + + The PyCMSProfile object can be passed back into pyCMS for use in creating + transforms and such (as in ImageCms.buildTransformFromOpenProfiles()). + + If ``profileFilename`` is not a valid filename for an ICC profile, + a :exc:`PyCMSError` will be raised. + + :param profileFilename: String, as a valid filename path to the ICC profile + you wish to open, or a file-like object. + :returns: A CmsProfile class object. + :exception PyCMSError: + """ + + try: + return ImageCmsProfile(profileFilename) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def buildTransform( + inputProfile: _CmsProfileCompatible, + outputProfile: _CmsProfileCompatible, + inMode: str, + outMode: str, + renderingIntent: Intent = Intent.PERCEPTUAL, + flags: Flags = Flags.NONE, +) -> ImageCmsTransform: + """ + (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the + ``outputProfile``. Use applyTransform to apply the transform to a given + image. + + If the input or output profiles specified are not valid filenames, a + :exc:`PyCMSError` will be raised. If an error occurs during creation + of the transform, a :exc:`PyCMSError` will be raised. + + If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` + (or by pyCMS), a :exc:`PyCMSError` will be raised. + + This function builds and returns an ICC transform from the ``inputProfile`` + to the ``outputProfile`` using the ``renderingIntent`` to determine what to do + with out-of-gamut colors. It will ONLY work for converting images that + are in ``inMode`` to images that are in ``outMode`` color format (PIL mode, + i.e. "RGB", "RGBA", "CMYK", etc.). + + Building the transform is a fair part of the overhead in + ImageCms.profileToProfile(), so if you're planning on converting multiple + images using the same input/output settings, this can save you time. + Once you have a transform object, it can be used with + ImageCms.applyProfile() to convert images without the need to re-compute + the lookup table for the transform. + + The reason pyCMS returns a class object rather than a handle directly + to the transform is that it needs to keep track of the PIL input/output + modes that the transform is meant for. These attributes are stored in + the ``inMode`` and ``outMode`` attributes of the object (which can be + manually overridden if you really want to, but I don't know of any + time that would be of use, or would even work). + + :param inputProfile: String, as a valid filename path to the ICC input + profile you wish to use for this transform, or a profile object + :param outputProfile: String, as a valid filename path to the ICC output + profile you wish to use for this transform, or a profile object + :param inMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param outMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param renderingIntent: Integer (0-3) specifying the rendering intent you + wish to use for the transform + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :param flags: Integer (0-...) specifying additional flags + :returns: A CmsTransform class object. + :exception PyCMSError: + """ + + if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) + + if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" + raise PyCMSError(msg) + + try: + if not isinstance(inputProfile, ImageCmsProfile): + inputProfile = ImageCmsProfile(inputProfile) + if not isinstance(outputProfile, ImageCmsProfile): + outputProfile = ImageCmsProfile(outputProfile) + return ImageCmsTransform( + inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags + ) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def buildProofTransform( + inputProfile: _CmsProfileCompatible, + outputProfile: _CmsProfileCompatible, + proofProfile: _CmsProfileCompatible, + inMode: str, + outMode: str, + renderingIntent: Intent = Intent.PERCEPTUAL, + proofRenderingIntent: Intent = Intent.ABSOLUTE_COLORIMETRIC, + flags: Flags = Flags.SOFTPROOFING, +) -> ImageCmsTransform: + """ + (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the + ``outputProfile``, but tries to simulate the result that would be + obtained on the ``proofProfile`` device. + + If the input, output, or proof profiles specified are not valid + filenames, a :exc:`PyCMSError` will be raised. + + If an error occurs during creation of the transform, + a :exc:`PyCMSError` will be raised. + + If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` + (or by pyCMS), a :exc:`PyCMSError` will be raised. + + This function builds and returns an ICC transform from the ``inputProfile`` + to the ``outputProfile``, but tries to simulate the result that would be + obtained on the ``proofProfile`` device using ``renderingIntent`` and + ``proofRenderingIntent`` to determine what to do with out-of-gamut + colors. This is known as "soft-proofing". It will ONLY work for + converting images that are in ``inMode`` to images that are in outMode + color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.). + + Usage of the resulting transform object is exactly the same as with + ImageCms.buildTransform(). + + Proof profiling is generally used when using an output device to get a + good idea of what the final printed/displayed image would look like on + the ``proofProfile`` device when it's quicker and easier to use the + output device for judging color. Generally, this means that the + output device is a monitor, or a dye-sub printer (etc.), and the simulated + device is something more expensive, complicated, or time consuming + (making it difficult to make a real print for color judgement purposes). + + Soft-proofing basically functions by adjusting the colors on the + output device to match the colors of the device being simulated. However, + when the simulated device has a much wider gamut than the output + device, you may obtain marginal results. + + :param inputProfile: String, as a valid filename path to the ICC input + profile you wish to use for this transform, or a profile object + :param outputProfile: String, as a valid filename path to the ICC output + (monitor, usually) profile you wish to use for this transform, or a + profile object + :param proofProfile: String, as a valid filename path to the ICC proof + profile you wish to use for this transform, or a profile object + :param inMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param outMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param renderingIntent: Integer (0-3) specifying the rendering intent you + wish to use for the input->proof (simulated) transform + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :param proofRenderingIntent: Integer (0-3) specifying the rendering intent + you wish to use for proof->output transform + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :param flags: Integer (0-...) specifying additional flags + :returns: A CmsTransform class object. + :exception PyCMSError: + """ + + if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) + + if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" + raise PyCMSError(msg) + + try: + if not isinstance(inputProfile, ImageCmsProfile): + inputProfile = ImageCmsProfile(inputProfile) + if not isinstance(outputProfile, ImageCmsProfile): + outputProfile = ImageCmsProfile(outputProfile) + if not isinstance(proofProfile, ImageCmsProfile): + proofProfile = ImageCmsProfile(proofProfile) + return ImageCmsTransform( + inputProfile, + outputProfile, + inMode, + outMode, + renderingIntent, + proofProfile, + proofRenderingIntent, + flags, + ) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +buildTransformFromOpenProfiles = buildTransform +buildProofTransformFromOpenProfiles = buildProofTransform + + +def applyTransform( + im: Image.Image, transform: ImageCmsTransform, inPlace: bool = False +) -> Image.Image | None: + """ + (pyCMS) Applies a transform to a given image. + + If ``im.mode != transform.input_mode``, a :exc:`PyCMSError` is raised. + + If ``inPlace`` is ``True`` and ``transform.input_mode != transform.output_mode``, a + :exc:`PyCMSError` is raised. + + If ``im.mode``, ``transform.input_mode`` or ``transform.output_mode`` is not + supported by pyCMSdll or the profiles you used for the transform, a + :exc:`PyCMSError` is raised. + + If an error occurs while the transform is being applied, + a :exc:`PyCMSError` is raised. + + This function applies a pre-calculated transform (from + ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles()) + to an image. The transform can be used for multiple images, saving + considerable calculation time if doing the same conversion multiple times. + + If you want to modify im in-place instead of receiving a new image as + the return value, set ``inPlace`` to ``True``. This can only be done if + ``transform.input_mode`` and ``transform.output_mode`` are the same, because we + can't change the mode in-place (the buffer sizes for some modes are + different). The default behavior is to return a new :py:class:`~PIL.Image.Image` + object of the same dimensions in mode ``transform.output_mode``. + + :param im: An :py:class:`~PIL.Image.Image` object, and ``im.mode`` must be the same + as the ``input_mode`` supported by the transform. + :param transform: A valid CmsTransform class object + :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is + returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the + transform applied is returned (and ``im`` is not changed). The default is + ``False``. + :returns: Either ``None``, or a new :py:class:`~PIL.Image.Image` object, + depending on the value of ``inPlace``. The profile will be returned in + the image's ``info['icc_profile']``. + :exception PyCMSError: + """ + + try: + if inPlace: + transform.apply_in_place(im) + imOut = None + else: + imOut = transform.apply(im) + except (TypeError, ValueError) as v: + raise PyCMSError(v) from v + + return imOut + + +def createProfile( + colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0 +) -> core.CmsProfile: + """ + (pyCMS) Creates a profile. + + If colorSpace not in ``["LAB", "XYZ", "sRGB"]``, + a :exc:`PyCMSError` is raised. + + If using LAB and ``colorTemp`` is not a positive integer, + a :exc:`PyCMSError` is raised. + + If an error occurs while creating the profile, + a :exc:`PyCMSError` is raised. + + Use this function to create common profiles on-the-fly instead of + having to supply a profile on disk and knowing the path to it. It + returns a normal CmsProfile object that can be passed to + ImageCms.buildTransformFromOpenProfiles() to create a transform to apply + to images. + + :param colorSpace: String, the color space of the profile you wish to + create. + Currently only "LAB", "XYZ", and "sRGB" are supported. + :param colorTemp: Positive number for the white point for the profile, in + degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50 + illuminant if omitted (5000k). colorTemp is ONLY applied to LAB + profiles, and is ignored for XYZ and sRGB. + :returns: A CmsProfile class object + :exception PyCMSError: + """ + + if colorSpace not in ["LAB", "XYZ", "sRGB"]: + msg = ( + f"Color space not supported for on-the-fly profile creation ({colorSpace})" + ) + raise PyCMSError(msg) + + if colorSpace == "LAB": + try: + colorTemp = float(colorTemp) + except (TypeError, ValueError) as e: + msg = f'Color temperature must be numeric, "{colorTemp}" not valid' + raise PyCMSError(msg) from e + + try: + return core.createProfile(colorSpace, colorTemp) + except (TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileName(profile: _CmsProfileCompatible) -> str: + """ + + (pyCMS) Gets the internal product name for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, + a :exc:`PyCMSError` is raised If an error occurs while trying + to obtain the name tag, a :exc:`PyCMSError` is raised. + + Use this function to obtain the INTERNAL name of the profile (stored + in an ICC tag in the profile itself), usually the one used when the + profile was originally created. Sometimes this tag also contains + additional information supplied by the creator. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal name of the profile as stored + in an ICC tag. + :exception PyCMSError: + """ + + try: + # add an extra newline to preserve pyCMS compatibility + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + # do it in python, not c. + # // name was "%s - %s" (model, manufacturer) || Description , + # // but if the Model and Manufacturer were the same or the model + # // was long, Just the model, in 1.x + model = profile.profile.model + manufacturer = profile.profile.manufacturer + + if not (model or manufacturer): + return (profile.profile.profile_description or "") + "\n" + if not manufacturer or (model and len(model) > 30): + return f"{model}\n" + return f"{model} - {manufacturer}\n" + + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileInfo(profile: _CmsProfileCompatible) -> str: + """ + (pyCMS) Gets the internal product information for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, + a :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the info tag, + a :exc:`PyCMSError` is raised. + + Use this function to obtain the information stored in the profile's + info tag. This often contains details about the profile, and how it + was created, as supplied by the creator. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. + :exception PyCMSError: + """ + + try: + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + # add an extra newline to preserve pyCMS compatibility + # Python, not C. the white point bits weren't working well, + # so skipping. + # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint + description = profile.profile.profile_description + cpright = profile.profile.copyright + elements = [element for element in (description, cpright) if element] + return "\r\n\r\n".join(elements) + "\r\n\r\n" + + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileCopyright(profile: _CmsProfileCompatible) -> str: + """ + (pyCMS) Gets the copyright for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the copyright tag, + a :exc:`PyCMSError` is raised. + + Use this function to obtain the information stored in the profile's + copyright tag. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. + :exception PyCMSError: + """ + try: + # add an extra newline to preserve pyCMS compatibility + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + return (profile.profile.copyright or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileManufacturer(profile: _CmsProfileCompatible) -> str: + """ + (pyCMS) Gets the manufacturer for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the manufacturer tag, a + :exc:`PyCMSError` is raised. + + Use this function to obtain the information stored in the profile's + manufacturer tag. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. + :exception PyCMSError: + """ + try: + # add an extra newline to preserve pyCMS compatibility + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + return (profile.profile.manufacturer or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileModel(profile: _CmsProfileCompatible) -> str: + """ + (pyCMS) Gets the model for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the model tag, + a :exc:`PyCMSError` is raised. + + Use this function to obtain the information stored in the profile's + model tag. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. + :exception PyCMSError: + """ + + try: + # add an extra newline to preserve pyCMS compatibility + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + return (profile.profile.model or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getProfileDescription(profile: _CmsProfileCompatible) -> str: + """ + (pyCMS) Gets the description for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the description tag, + a :exc:`PyCMSError` is raised. + + Use this function to obtain the information stored in the profile's + description tag. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in an + ICC tag. + :exception PyCMSError: + """ + + try: + # add an extra newline to preserve pyCMS compatibility + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + return (profile.profile.profile_description or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def getDefaultIntent(profile: _CmsProfileCompatible) -> int: + """ + (pyCMS) Gets the default intent name for the given profile. + + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. + + If an error occurs while trying to obtain the default intent, a + :exc:`PyCMSError` is raised. + + Use this function to determine the default (and usually best optimized) + rendering intent for this profile. Most profiles support multiple + rendering intents, but are intended mostly for one type of conversion. + If you wish to use a different intent than returned, use + ImageCms.isIntentSupported() to verify it will work first. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: Integer 0-3 specifying the default rendering intent for this + profile. + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :exception PyCMSError: + """ + + try: + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + return profile.profile.rendering_intent + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def isIntentSupported( + profile: _CmsProfileCompatible, intent: Intent, direction: Direction +) -> Literal[-1, 1]: + """ + (pyCMS) Checks if a given intent is supported. + + Use this function to verify that you can use your desired + ``intent`` with ``profile``, and that ``profile`` can be used for the + input/output/proof profile as you desire. + + Some profiles are created specifically for one "direction", can cannot + be used for others. Some profiles can only be used for certain + rendering intents, so it's best to either verify this before trying + to create a transform with them (using this function), or catch the + potential :exc:`PyCMSError` that will occur if they don't + support the modes you select. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :param intent: Integer (0-3) specifying the rendering intent you wish to + use with this profile + + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 + + see the pyCMS documentation for details on rendering intents and what + they do. + :param direction: Integer specifying if the profile is to be used for + input, output, or proof + + INPUT = 0 (or use ImageCms.Direction.INPUT) + OUTPUT = 1 (or use ImageCms.Direction.OUTPUT) + PROOF = 2 (or use ImageCms.Direction.PROOF) + + :returns: 1 if the intent/direction are supported, -1 if they are not. + :exception PyCMSError: + """ + + try: + if not isinstance(profile, ImageCmsProfile): + profile = ImageCmsProfile(profile) + # FIXME: I get different results for the same data w. different + # compilers. Bug in LittleCMS or in the binding? + if profile.profile.is_intent_supported(intent, direction): + return 1 + else: + return -1 + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + + +def versions() -> tuple[str, str | None, str, str]: + """ + (pyCMS) Fetches versions. + """ + + deprecate( + "PIL.ImageCms.versions()", + 12, + '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', + ) + return _VERSION, core.littlecms_version, sys.version.split()[0], __version__ diff --git a/python_modules/PIL/ImageColor.py b/python_modules/PIL/ImageColor.py new file mode 100644 index 000000000..9a15a8eb7 --- /dev/null +++ b/python_modules/PIL/ImageColor.py @@ -0,0 +1,320 @@ +# +# The Python Imaging Library +# $Id$ +# +# map CSS3-style colour description strings to RGB +# +# History: +# 2002-10-24 fl Added support for CSS-style color strings +# 2002-12-15 fl Added RGBA support +# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2 +# 2004-07-19 fl Fixed gray/grey spelling issues +# 2009-03-05 fl Fixed rounding error in grayscale calculation +# +# Copyright (c) 2002-2004 by Secret Labs AB +# Copyright (c) 2002-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import re +from functools import lru_cache + +from . import Image + + +@lru_cache +def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]: + """ + Convert a color string to an RGB or RGBA tuple. If the string cannot be + parsed, this function raises a :py:exc:`ValueError` exception. + + .. versionadded:: 1.1.4 + + :param color: A color string + :return: ``(red, green, blue[, alpha])`` + """ + if len(color) > 100: + msg = "color specifier is too long" + raise ValueError(msg) + color = color.lower() + + rgb = colormap.get(color, None) + if rgb: + if isinstance(rgb, tuple): + return rgb + rgb_tuple = getrgb(rgb) + assert len(rgb_tuple) == 3 + colormap[color] = rgb_tuple + return rgb_tuple + + # check for known string formats + if re.match("#[a-f0-9]{3}$", color): + return int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16) + + if re.match("#[a-f0-9]{4}$", color): + return ( + int(color[1] * 2, 16), + int(color[2] * 2, 16), + int(color[3] * 2, 16), + int(color[4] * 2, 16), + ) + + if re.match("#[a-f0-9]{6}$", color): + return int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16) + + if re.match("#[a-f0-9]{8}$", color): + return ( + int(color[1:3], 16), + int(color[3:5], 16), + int(color[5:7], 16), + int(color[7:9], 16), + ) + + m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) + if m: + return int(m.group(1)), int(m.group(2)), int(m.group(3)) + + m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) + if m: + return ( + int((int(m.group(1)) * 255) / 100.0 + 0.5), + int((int(m.group(2)) * 255) / 100.0 + 0.5), + int((int(m.group(3)) * 255) / 100.0 + 0.5), + ) + + m = re.match( + r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color + ) + if m: + from colorsys import hls_to_rgb + + rgb_floats = hls_to_rgb( + float(m.group(1)) / 360.0, + float(m.group(3)) / 100.0, + float(m.group(2)) / 100.0, + ) + return ( + int(rgb_floats[0] * 255 + 0.5), + int(rgb_floats[1] * 255 + 0.5), + int(rgb_floats[2] * 255 + 0.5), + ) + + m = re.match( + r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color + ) + if m: + from colorsys import hsv_to_rgb + + rgb_floats = hsv_to_rgb( + float(m.group(1)) / 360.0, + float(m.group(2)) / 100.0, + float(m.group(3)) / 100.0, + ) + return ( + int(rgb_floats[0] * 255 + 0.5), + int(rgb_floats[1] * 255 + 0.5), + int(rgb_floats[2] * 255 + 0.5), + ) + + m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) + if m: + return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)) + msg = f"unknown color specifier: {repr(color)}" + raise ValueError(msg) + + +@lru_cache +def getcolor(color: str, mode: str) -> int | tuple[int, ...]: + """ + Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if + ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is + not color or a palette image, converts the RGB value to a grayscale value. + If the string cannot be parsed, this function raises a :py:exc:`ValueError` + exception. + + .. versionadded:: 1.1.4 + + :param color: A color string + :param mode: Convert result to this mode + :return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])`` + """ + # same as getrgb, but converts the result to the given mode + rgb, alpha = getrgb(color), 255 + if len(rgb) == 4: + alpha = rgb[3] + rgb = rgb[:3] + + if mode == "HSV": + from colorsys import rgb_to_hsv + + r, g, b = rgb + h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) + return int(h * 255), int(s * 255), int(v * 255) + elif Image.getmodebase(mode) == "L": + r, g, b = rgb + # ITU-R Recommendation 601-2 for nonlinear RGB + # scaled to 24 bits to match the convert's implementation. + graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 + if mode[-1] == "A": + return graylevel, alpha + return graylevel + elif mode[-1] == "A": + return rgb + (alpha,) + return rgb + + +colormap: dict[str, str | tuple[int, int, int]] = { + # X11 colour table from https://drafts.csswg.org/css-color-4/, with + # gray/grey spelling issues fixed. This is a superset of HTML 4.0 + # colour names used in CSS 1. + "aliceblue": "#f0f8ff", + "antiquewhite": "#faebd7", + "aqua": "#00ffff", + "aquamarine": "#7fffd4", + "azure": "#f0ffff", + "beige": "#f5f5dc", + "bisque": "#ffe4c4", + "black": "#000000", + "blanchedalmond": "#ffebcd", + "blue": "#0000ff", + "blueviolet": "#8a2be2", + "brown": "#a52a2a", + "burlywood": "#deb887", + "cadetblue": "#5f9ea0", + "chartreuse": "#7fff00", + "chocolate": "#d2691e", + "coral": "#ff7f50", + "cornflowerblue": "#6495ed", + "cornsilk": "#fff8dc", + "crimson": "#dc143c", + "cyan": "#00ffff", + "darkblue": "#00008b", + "darkcyan": "#008b8b", + "darkgoldenrod": "#b8860b", + "darkgray": "#a9a9a9", + "darkgrey": "#a9a9a9", + "darkgreen": "#006400", + "darkkhaki": "#bdb76b", + "darkmagenta": "#8b008b", + "darkolivegreen": "#556b2f", + "darkorange": "#ff8c00", + "darkorchid": "#9932cc", + "darkred": "#8b0000", + "darksalmon": "#e9967a", + "darkseagreen": "#8fbc8f", + "darkslateblue": "#483d8b", + "darkslategray": "#2f4f4f", + "darkslategrey": "#2f4f4f", + "darkturquoise": "#00ced1", + "darkviolet": "#9400d3", + "deeppink": "#ff1493", + "deepskyblue": "#00bfff", + "dimgray": "#696969", + "dimgrey": "#696969", + "dodgerblue": "#1e90ff", + "firebrick": "#b22222", + "floralwhite": "#fffaf0", + "forestgreen": "#228b22", + "fuchsia": "#ff00ff", + "gainsboro": "#dcdcdc", + "ghostwhite": "#f8f8ff", + "gold": "#ffd700", + "goldenrod": "#daa520", + "gray": "#808080", + "grey": "#808080", + "green": "#008000", + "greenyellow": "#adff2f", + "honeydew": "#f0fff0", + "hotpink": "#ff69b4", + "indianred": "#cd5c5c", + "indigo": "#4b0082", + "ivory": "#fffff0", + "khaki": "#f0e68c", + "lavender": "#e6e6fa", + "lavenderblush": "#fff0f5", + "lawngreen": "#7cfc00", + "lemonchiffon": "#fffacd", + "lightblue": "#add8e6", + "lightcoral": "#f08080", + "lightcyan": "#e0ffff", + "lightgoldenrodyellow": "#fafad2", + "lightgreen": "#90ee90", + "lightgray": "#d3d3d3", + "lightgrey": "#d3d3d3", + "lightpink": "#ffb6c1", + "lightsalmon": "#ffa07a", + "lightseagreen": "#20b2aa", + "lightskyblue": "#87cefa", + "lightslategray": "#778899", + "lightslategrey": "#778899", + "lightsteelblue": "#b0c4de", + "lightyellow": "#ffffe0", + "lime": "#00ff00", + "limegreen": "#32cd32", + "linen": "#faf0e6", + "magenta": "#ff00ff", + "maroon": "#800000", + "mediumaquamarine": "#66cdaa", + "mediumblue": "#0000cd", + "mediumorchid": "#ba55d3", + "mediumpurple": "#9370db", + "mediumseagreen": "#3cb371", + "mediumslateblue": "#7b68ee", + "mediumspringgreen": "#00fa9a", + "mediumturquoise": "#48d1cc", + "mediumvioletred": "#c71585", + "midnightblue": "#191970", + "mintcream": "#f5fffa", + "mistyrose": "#ffe4e1", + "moccasin": "#ffe4b5", + "navajowhite": "#ffdead", + "navy": "#000080", + "oldlace": "#fdf5e6", + "olive": "#808000", + "olivedrab": "#6b8e23", + "orange": "#ffa500", + "orangered": "#ff4500", + "orchid": "#da70d6", + "palegoldenrod": "#eee8aa", + "palegreen": "#98fb98", + "paleturquoise": "#afeeee", + "palevioletred": "#db7093", + "papayawhip": "#ffefd5", + "peachpuff": "#ffdab9", + "peru": "#cd853f", + "pink": "#ffc0cb", + "plum": "#dda0dd", + "powderblue": "#b0e0e6", + "purple": "#800080", + "rebeccapurple": "#663399", + "red": "#ff0000", + "rosybrown": "#bc8f8f", + "royalblue": "#4169e1", + "saddlebrown": "#8b4513", + "salmon": "#fa8072", + "sandybrown": "#f4a460", + "seagreen": "#2e8b57", + "seashell": "#fff5ee", + "sienna": "#a0522d", + "silver": "#c0c0c0", + "skyblue": "#87ceeb", + "slateblue": "#6a5acd", + "slategray": "#708090", + "slategrey": "#708090", + "snow": "#fffafa", + "springgreen": "#00ff7f", + "steelblue": "#4682b4", + "tan": "#d2b48c", + "teal": "#008080", + "thistle": "#d8bfd8", + "tomato": "#ff6347", + "turquoise": "#40e0d0", + "violet": "#ee82ee", + "wheat": "#f5deb3", + "white": "#ffffff", + "whitesmoke": "#f5f5f5", + "yellow": "#ffff00", + "yellowgreen": "#9acd32", +} diff --git a/python_modules/PIL/ImageDraw.py b/python_modules/PIL/ImageDraw.py new file mode 100644 index 000000000..6cf1ee626 --- /dev/null +++ b/python_modules/PIL/ImageDraw.py @@ -0,0 +1,1232 @@ +# +# The Python Imaging Library +# $Id$ +# +# drawing interface operations +# +# History: +# 1996-04-13 fl Created (experimental) +# 1996-08-07 fl Filled polygons, ellipses. +# 1996-08-13 fl Added text support +# 1998-06-28 fl Handle I and F images +# 1998-12-29 fl Added arc; use arc primitive to draw ellipses +# 1999-01-10 fl Added shape stuff (experimental) +# 1999-02-06 fl Added bitmap support +# 1999-02-11 fl Changed all primitives to take options +# 1999-02-20 fl Fixed backwards compatibility +# 2000-10-12 fl Copy on write, when necessary +# 2001-02-18 fl Use default ink for bitmap/text also in fill mode +# 2002-10-24 fl Added support for CSS-style color strings +# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing +# 2002-12-11 fl Refactored low-level drawing API (work in progress) +# 2004-08-26 fl Made Draw() a factory function, added getdraw() support +# 2004-09-04 fl Added width support to line primitive +# 2004-09-10 fl Added font mode handling +# 2006-06-19 fl Added font bearing support (getmask2) +# +# Copyright (c) 1997-2006 by Secret Labs AB +# Copyright (c) 1996-2006 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import math +import struct +from collections.abc import Sequence +from types import ModuleType +from typing import Any, AnyStr, Callable, Union, cast + +from . import Image, ImageColor +from ._deprecate import deprecate +from ._typing import Coords + +# experimental access to the outline API +Outline: Callable[[], Image.core._Outline] = Image.core.outline + +TYPE_CHECKING = False +if TYPE_CHECKING: + from . import ImageDraw2, ImageFont + +_Ink = Union[float, tuple[int, ...], str] + +""" +A simple 2D drawing interface for PIL images. +

+Application code should use the Draw factory, instead of +directly. +""" + + +class ImageDraw: + font: ( + ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None + ) = None + + def __init__(self, im: Image.Image, mode: str | None = None) -> None: + """ + Create a drawing instance. + + :param im: The image to draw in. + :param mode: Optional mode to use for color values. For RGB + images, this argument can be RGB or RGBA (to blend the + drawing into the image). For all other modes, this argument + must be the same as the image mode. If omitted, the mode + defaults to the mode of the image. + """ + im.load() + if im.readonly: + im._copy() # make it writeable + blend = 0 + if mode is None: + mode = im.mode + if mode != im.mode: + if mode == "RGBA" and im.mode == "RGB": + blend = 1 + else: + msg = "mode mismatch" + raise ValueError(msg) + if mode == "P": + self.palette = im.palette + else: + self.palette = None + self._image = im + self.im = im.im + self.draw = Image.core.draw(self.im, blend) + self.mode = mode + if mode in ("I", "F"): + self.ink = self.draw.draw_ink(1) + else: + self.ink = self.draw.draw_ink(-1) + if mode in ("1", "P", "I", "F"): + # FIXME: fix Fill2 to properly support matte for I+F images + self.fontmode = "1" + else: + self.fontmode = "L" # aliasing is okay for other modes + self.fill = False + + def getfont( + self, + ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: + """ + Get the current default font. + + To set the default font for this ImageDraw instance:: + + from PIL import ImageDraw, ImageFont + draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + To set the default font for all future ImageDraw instances:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + If the current default font is ``None``, + it is initialized with ``ImageFont.load_default()``. + + :returns: An image font.""" + if not self.font: + # FIXME: should add a font repository + from . import ImageFont + + self.font = ImageFont.load_default() + return self.font + + def _getfont( + self, font_size: float | None + ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: + if font_size is not None: + from . import ImageFont + + return ImageFont.load_default(font_size) + else: + return self.getfont() + + def _getink( + self, ink: _Ink | None, fill: _Ink | None = None + ) -> tuple[int | None, int | None]: + result_ink = None + result_fill = None + if ink is None and fill is None: + if self.fill: + result_fill = self.ink + else: + result_ink = self.ink + else: + if ink is not None: + if isinstance(ink, str): + ink = ImageColor.getcolor(ink, self.mode) + if self.palette and isinstance(ink, tuple): + ink = self.palette.getcolor(ink, self._image) + result_ink = self.draw.draw_ink(ink) + if fill is not None: + if isinstance(fill, str): + fill = ImageColor.getcolor(fill, self.mode) + if self.palette and isinstance(fill, tuple): + fill = self.palette.getcolor(fill, self._image) + result_fill = self.draw.draw_ink(fill) + return result_ink, result_fill + + def arc( + self, + xy: Coords, + start: float, + end: float, + fill: _Ink | None = None, + width: int = 1, + ) -> None: + """Draw an arc.""" + ink, fill = self._getink(fill) + if ink is not None: + self.draw.draw_arc(xy, start, end, ink, width) + + def bitmap( + self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None + ) -> None: + """Draw a bitmap.""" + bitmap.load() + ink, fill = self._getink(fill) + if ink is None: + ink = fill + if ink is not None: + self.draw.draw_bitmap(xy, bitmap.im, ink) + + def chord( + self, + xy: Coords, + start: float, + end: float, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: + """Draw a chord.""" + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_chord(xy, start, end, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: + self.draw.draw_chord(xy, start, end, ink, 0, width) + + def ellipse( + self, + xy: Coords, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: + """Draw an ellipse.""" + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_ellipse(xy, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: + self.draw.draw_ellipse(xy, ink, 0, width) + + def circle( + self, + xy: Sequence[float], + radius: float, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: + """Draw a circle given center coordinates and a radius.""" + ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius) + self.ellipse(ellipse_xy, fill, outline, width) + + def line( + self, + xy: Coords, + fill: _Ink | None = None, + width: int = 0, + joint: str | None = None, + ) -> None: + """Draw a line, or a connected sequence of line segments.""" + ink = self._getink(fill)[0] + if ink is not None: + self.draw.draw_lines(xy, ink, width) + if joint == "curve" and width > 4: + points: Sequence[Sequence[float]] + if isinstance(xy[0], (list, tuple)): + points = cast(Sequence[Sequence[float]], xy) + else: + points = [ + cast(Sequence[float], tuple(xy[i : i + 2])) + for i in range(0, len(xy), 2) + ] + for i in range(1, len(points) - 1): + point = points[i] + angles = [ + math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) + % 360 + for start, end in ( + (points[i - 1], point), + (point, points[i + 1]), + ) + ] + if angles[0] == angles[1]: + # This is a straight line, so no joint is required + continue + + def coord_at_angle( + coord: Sequence[float], angle: float + ) -> tuple[float, ...]: + x, y = coord + angle -= 90 + distance = width / 2 - 1 + return tuple( + p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) + for p, p_d in ( + (x, distance * math.cos(math.radians(angle))), + (y, distance * math.sin(math.radians(angle))), + ) + ) + + flipped = ( + angles[1] > angles[0] and angles[1] - 180 > angles[0] + ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0]) + coords = [ + (point[0] - width / 2 + 1, point[1] - width / 2 + 1), + (point[0] + width / 2 - 1, point[1] + width / 2 - 1), + ] + if flipped: + start, end = (angles[1] + 90, angles[0] + 90) + else: + start, end = (angles[0] - 90, angles[1] - 90) + self.pieslice(coords, start - 90, end - 90, fill) + + if width > 8: + # Cover potential gaps between the line and the joint + if flipped: + gap_coords = [ + coord_at_angle(point, angles[0] + 90), + point, + coord_at_angle(point, angles[1] + 90), + ] + else: + gap_coords = [ + coord_at_angle(point, angles[0] - 90), + point, + coord_at_angle(point, angles[1] - 90), + ] + self.line(gap_coords, fill, width=3) + + def shape( + self, + shape: Image.core._Outline, + fill: _Ink | None = None, + outline: _Ink | None = None, + ) -> None: + """(Experimental) Draw a shape.""" + shape.close() + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_outline(shape, fill_ink, 1) + if ink is not None and ink != fill_ink: + self.draw.draw_outline(shape, ink, 0) + + def pieslice( + self, + xy: Coords, + start: float, + end: float, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: + """Draw a pieslice.""" + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_pieslice(xy, start, end, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: + self.draw.draw_pieslice(xy, start, end, ink, 0, width) + + def point(self, xy: Coords, fill: _Ink | None = None) -> None: + """Draw one or more individual pixels.""" + ink, fill = self._getink(fill) + if ink is not None: + self.draw.draw_points(xy, ink) + + def polygon( + self, + xy: Coords, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: + """Draw a polygon.""" + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_polygon(xy, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: + if width == 1: + self.draw.draw_polygon(xy, ink, 0, width) + elif self.im is not None: + # To avoid expanding the polygon outwards, + # use the fill as a mask + mask = Image.new("1", self.im.size) + mask_ink = self._getink(1)[0] + draw = Draw(mask) + draw.draw.draw_polygon(xy, mask_ink, 1) + + self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im) + + def regular_polygon( + self, + bounding_circle: Sequence[Sequence[float] | float], + n_sides: int, + rotation: float = 0, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: + """Draw a regular polygon.""" + xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) + self.polygon(xy, fill, outline, width) + + def rectangle( + self, + xy: Coords, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: + """Draw a rectangle.""" + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_rectangle(xy, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: + self.draw.draw_rectangle(xy, ink, 0, width) + + def rounded_rectangle( + self, + xy: Coords, + radius: float = 0, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + *, + corners: tuple[bool, bool, bool, bool] | None = None, + ) -> None: + """Draw a rounded rectangle.""" + if isinstance(xy[0], (list, tuple)): + (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) + else: + x0, y0, x1, y1 = cast(Sequence[float], xy) + if x1 < x0: + msg = "x1 must be greater than or equal to x0" + raise ValueError(msg) + if y1 < y0: + msg = "y1 must be greater than or equal to y0" + raise ValueError(msg) + if corners is None: + corners = (True, True, True, True) + + d = radius * 2 + + x0 = round(x0) + y0 = round(y0) + x1 = round(x1) + y1 = round(y1) + full_x, full_y = False, False + if all(corners): + full_x = d >= x1 - x0 - 1 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 - 1 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) + + if d == 0 or not any(corners): + # If the corners have no curve, + # or there are no corners, + # that is a rectangle + return self.rectangle(xy, fill, outline, width) + + r = int(d // 2) + ink, fill_ink = self._getink(outline, fill) + + def draw_corners(pieslice: bool) -> None: + parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] + if full_x: + # Draw top and bottom halves + parts = ( + ((x0, y0, x0 + d, y0 + d), 180, 360), + ((x0, y1 - d, x0 + d, y1), 0, 180), + ) + elif full_y: + # Draw left and right halves + parts = ( + ((x0, y0, x0 + d, y0 + d), 90, 270), + ((x1 - d, y0, x1, y0 + d), 270, 90), + ) + else: + # Draw four separate corners + parts = tuple( + part + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) + ) + if corners[i] + ) + for part in parts: + if pieslice: + self.draw.draw_pieslice(*(part + (fill_ink, 1))) + else: + self.draw.draw_arc(*(part + (ink, width))) + + if fill_ink is not None: + draw_corners(True) + + if full_x: + self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) + elif x1 - r - 1 > x0 + r + 1: + self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) + if not full_x and not full_y: + left = [x0, y0, x0 + r, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, fill_ink, 1) + + right = [x1 - r, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: + draw_corners(False) + + if not full_x: + top = [x0, y0, x1, y0 + width - 1] + if corners[0]: + top[0] += r + 1 + if corners[1]: + top[2] -= r + 1 + self.draw.draw_rectangle(top, ink, 1) + + bottom = [x0, y1 - width + 1, x1, y1] + if corners[3]: + bottom[0] += r + 1 + if corners[2]: + bottom[2] -= r + 1 + self.draw.draw_rectangle(bottom, ink, 1) + if not full_y: + left = [x0, y0, x0 + width - 1, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, ink, 1) + + right = [x1 - width + 1, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, ink, 1) + + def _multiline_check(self, text: AnyStr) -> bool: + split_character = "\n" if isinstance(text, str) else b"\n" + + return split_character in text + + def text( + self, + xy: tuple[float, float], + text: AnyStr, + fill: _Ink | None = None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + stroke_fill: _Ink | None = None, + embedded_color: bool = False, + *args: Any, + **kwargs: Any, + ) -> None: + """Draw text.""" + if embedded_color and self.mode not in ("RGB", "RGBA"): + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) + + if font is None: + font = self._getfont(kwargs.get("font_size")) + + if self._multiline_check(text): + return self.multiline_text( + xy, + text, + fill, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + stroke_fill, + embedded_color, + ) + + def getink(fill: _Ink | None) -> int: + ink, fill_ink = self._getink(fill) + if ink is None: + assert fill_ink is not None + return fill_ink + return ink + + def draw_text(ink: int, stroke_width: float = 0) -> None: + mode = self.fontmode + if stroke_width == 0 and embedded_color: + mode = "RGBA" + coord = [] + for i in range(2): + coord.append(int(xy[i])) + start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) + try: + mask, offset = font.getmask2( # type: ignore[union-attr,misc] + text, + mode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + stroke_filled=True, + anchor=anchor, + ink=ink, + start=start, + *args, + **kwargs, + ) + coord = [coord[0] + offset[0], coord[1] + offset[1]] + except AttributeError: + try: + mask = font.getmask( # type: ignore[misc] + text, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + start=start, + *args, + **kwargs, + ) + except TypeError: + mask = font.getmask(text) + if mode == "RGBA": + # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A + # extract mask and set text alpha + color, mask = mask, mask.getband(3) + ink_alpha = struct.pack("i", ink)[3] + color.fillband(3, ink_alpha) + x, y = coord + if self.im is not None: + self.im.paste( + color, (x, y, x + mask.size[0], y + mask.size[1]), mask + ) + else: + self.draw.draw_bitmap(coord, mask, ink) + + ink = getink(fill) + if ink is not None: + stroke_ink = None + if stroke_width: + stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink + + if stroke_ink is not None: + # Draw stroked text + draw_text(stroke_ink, stroke_width) + + # Draw normal text + if ink != stroke_ink: + draw_text(ink) + else: + # Only draw normal text + draw_text(ink) + + def _prepare_multiline_text( + self, + xy: tuple[float, float], + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ), + anchor: str | None, + spacing: float, + align: str, + direction: str | None, + features: list[str] | None, + language: str | None, + stroke_width: float, + embedded_color: bool, + font_size: float | None, + ) -> tuple[ + ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, + list[tuple[tuple[float, float], str, AnyStr]], + ]: + if anchor is None: + anchor = "lt" if direction == "ttb" else "la" + elif len(anchor) != 2: + msg = "anchor must be a 2 character string" + raise ValueError(msg) + elif anchor[1] in "tb" and direction != "ttb": + msg = "anchor not supported for multiline text" + raise ValueError(msg) + + if font is None: + font = self._getfont(font_size) + + lines = text.split("\n" if isinstance(text, str) else b"\n") + line_spacing = ( + self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + + stroke_width + + spacing + ) + + top = xy[1] + parts = [] + if direction == "ttb": + left = xy[0] + for line in lines: + parts.append(((left, top), anchor, line)) + left += line_spacing + else: + widths = [] + max_width: float = 0 + for line in lines: + line_width = self.textlength( + line, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + widths.append(line_width) + max_width = max(max_width, line_width) + + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # align by align parameter + if align in ("left", "justify"): + pass + elif align == "center": + left += width_difference / 2.0 + elif align == "right": + left += width_difference + else: + msg = 'align must be "left", "center", "right" or "justify"' + raise ValueError(msg) + + if ( + align == "justify" + and width_difference != 0 + and idx != len(lines) - 1 + ): + words = line.split(" " if isinstance(text, str) else b" ") + if len(words) > 1: + # align left by anchor + if anchor[0] == "m": + left -= max_width / 2.0 + elif anchor[0] == "r": + left -= max_width + + word_widths = [ + self.textlength( + word, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + for word in words + ] + word_anchor = "l" + anchor[1] + width_difference = max_width - sum(word_widths) + for i, word in enumerate(words): + parts.append(((left, top), word_anchor, word)) + left += word_widths[i] + width_difference / (len(words) - 1) + top += line_spacing + continue + + # align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + parts.append(((left, top), anchor, line)) + top += line_spacing + + return font, parts + + def multiline_text( + self, + xy: tuple[float, float], + text: AnyStr, + fill: _Ink | None = None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + stroke_fill: _Ink | None = None, + embedded_color: bool = False, + *, + font_size: float | None = None, + ) -> None: + font, lines = self._prepare_multiline_text( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + font_size, + ) + + for xy, anchor, line in lines: + self.text( + xy, + line, + fill, + font, + anchor, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + stroke_fill=stroke_fill, + embedded_color=embedded_color, + ) + + def textlength( + self, + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + embedded_color: bool = False, + *, + font_size: float | None = None, + ) -> float: + """Get the length of a given string, in pixels with 1/64 precision.""" + if self._multiline_check(text): + msg = "can't measure length of multiline text" + raise ValueError(msg) + if embedded_color and self.mode not in ("RGB", "RGBA"): + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) + + if font is None: + font = self._getfont(font_size) + mode = "RGBA" if embedded_color else self.fontmode + return font.getlength(text, mode, direction, features, language) + + def textbbox( + self, + xy: tuple[float, float], + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + embedded_color: bool = False, + *, + font_size: float | None = None, + ) -> tuple[float, float, float, float]: + """Get the bounding box of a given string, in pixels.""" + if embedded_color and self.mode not in ("RGB", "RGBA"): + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) + + if font is None: + font = self._getfont(font_size) + + if self._multiline_check(text): + return self.multiline_textbbox( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + ) + + mode = "RGBA" if embedded_color else self.fontmode + bbox = font.getbbox( + text, mode, direction, features, language, stroke_width, anchor + ) + return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] + + def multiline_textbbox( + self, + xy: tuple[float, float], + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + embedded_color: bool = False, + *, + font_size: float | None = None, + ) -> tuple[float, float, float, float]: + font, lines = self._prepare_multiline_text( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + font_size, + ) + + bbox: tuple[float, float, float, float] | None = None + + for xy, anchor, line in lines: + bbox_line = self.textbbox( + xy, + line, + font, + anchor, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + embedded_color=embedded_color, + ) + if bbox is None: + bbox = bbox_line + else: + bbox = ( + min(bbox[0], bbox_line[0]), + min(bbox[1], bbox_line[1]), + max(bbox[2], bbox_line[2]), + max(bbox[3], bbox_line[3]), + ) + + if bbox is None: + return xy[0], xy[1], xy[0], xy[1] + return bbox + + +def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: + """ + A simple 2D drawing interface for PIL images. + + :param im: The image to draw in. + :param mode: Optional mode to use for color values. For RGB + images, this argument can be RGB or RGBA (to blend the + drawing into the image). For all other modes, this argument + must be the same as the image mode. If omitted, the mode + defaults to the mode of the image. + """ + try: + return getattr(im, "getdraw")(mode) + except AttributeError: + return ImageDraw(im, mode) + + +def getdraw( + im: Image.Image | None = None, hints: list[str] | None = None +) -> tuple[ImageDraw2.Draw | None, ModuleType]: + """ + :param im: The image to draw in. + :param hints: An optional list of hints. Deprecated. + :returns: A (drawing context, drawing resource factory) tuple. + """ + if hints is not None: + deprecate("'hints' parameter", 12) + from . import ImageDraw2 + + draw = ImageDraw2.Draw(im) if im is not None else None + return draw, ImageDraw2 + + +def floodfill( + image: Image.Image, + xy: tuple[int, int], + value: float | tuple[int, ...], + border: float | tuple[int, ...] | None = None, + thresh: float = 0, +) -> None: + """ + .. warning:: This method is experimental. + + Fills a bounded region with a given color. + + :param image: Target image. + :param xy: Seed position (a 2-item coordinate tuple). See + :ref:`coordinate-system`. + :param value: Fill color. + :param border: Optional border value. If given, the region consists of + pixels with a color different from the border color. If not given, + the region consists of pixels having the same color as the seed + pixel. + :param thresh: Optional threshold value which specifies a maximum + tolerable difference of a pixel value from the 'background' in + order for it to be replaced. Useful for filling regions of + non-homogeneous, but similar, colors. + """ + # based on an implementation by Eric S. Raymond + # amended by yo1995 @20180806 + pixel = image.load() + assert pixel is not None + x, y = xy + try: + background = pixel[x, y] + if _color_diff(value, background) <= thresh: + return # seed point already has fill color + pixel[x, y] = value + except (ValueError, IndexError): + return # seed point outside image + edge = {(x, y)} + # use a set to keep record of current and previous edge pixels + # to reduce memory consumption + full_edge = set() + while edge: + new_edge = set() + for x, y in edge: # 4 adjacent method + for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + # If already processed, or if a coordinate is negative, skip + if (s, t) in full_edge or s < 0 or t < 0: + continue + try: + p = pixel[s, t] + except (ValueError, IndexError): + pass + else: + full_edge.add((s, t)) + if border is None: + fill = _color_diff(p, background) <= thresh + else: + fill = p not in (value, border) + if fill: + pixel[s, t] = value + new_edge.add((s, t)) + full_edge = edge # discard pixels processed + edge = new_edge + + +def _compute_regular_polygon_vertices( + bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float +) -> list[tuple[float, float]]: + """ + Generate a list of vertices for a 2D regular polygon. + + :param bounding_circle: The bounding circle is a sequence defined + by a point and radius. The polygon is inscribed in this circle. + (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) + :param n_sides: Number of sides + (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) + :param rotation: Apply an arbitrary rotation to the polygon + (e.g. ``rotation=90``, applies a 90 degree rotation) + :return: List of regular polygon vertices + (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) + + How are the vertices computed? + 1. Compute the following variables + - theta: Angle between the apothem & the nearest polygon vertex + - side_length: Length of each polygon edge + - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) + - polygon_radius: Polygon radius (last element of bounding_circle) + - angles: Location of each polygon vertex in polar grid + (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) + + 2. For each angle in angles, get the polygon vertex at that angle + The vertex is computed using the equation below. + X= xcos(φ) + ysin(φ) + Y= −xsin(φ) + ycos(φ) + + Note: + φ = angle in degrees + x = 0 + y = polygon_radius + + The formula above assumes rotation around the origin. + In our case, we are rotating around the centroid. + To account for this, we use the formula below + X = xcos(φ) + ysin(φ) + centroid_x + Y = −xsin(φ) + ycos(φ) + centroid_y + """ + # 1. Error Handling + # 1.1 Check `n_sides` has an appropriate value + if not isinstance(n_sides, int): + msg = "n_sides should be an int" # type: ignore[unreachable] + raise TypeError(msg) + if n_sides < 3: + msg = "n_sides should be an int > 2" + raise ValueError(msg) + + # 1.2 Check `bounding_circle` has an appropriate value + if not isinstance(bounding_circle, (list, tuple)): + msg = "bounding_circle should be a sequence" + raise TypeError(msg) + + if len(bounding_circle) == 3: + if not all(isinstance(i, (int, float)) for i in bounding_circle): + msg = "bounding_circle should only contain numeric data" + raise ValueError(msg) + + *centroid, polygon_radius = cast(list[float], list(bounding_circle)) + elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)): + if not all( + isinstance(i, (int, float)) for i in bounding_circle[0] + ) or not isinstance(bounding_circle[1], (int, float)): + msg = "bounding_circle should only contain numeric data" + raise ValueError(msg) + + if len(bounding_circle[0]) != 2: + msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" + raise ValueError(msg) + + centroid = cast(list[float], list(bounding_circle[0])) + polygon_radius = cast(float, bounding_circle[1]) + else: + msg = ( + "bounding_circle should contain 2D coordinates " + "and a radius (e.g. (x, y, r) or ((x, y), r) )" + ) + raise ValueError(msg) + + if polygon_radius <= 0: + msg = "bounding_circle radius should be > 0" + raise ValueError(msg) + + # 1.3 Check `rotation` has an appropriate value + if not isinstance(rotation, (int, float)): + msg = "rotation should be an int or float" # type: ignore[unreachable] + raise ValueError(msg) + + # 2. Define Helper Functions + def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]: + return ( + round( + point[0] * math.cos(math.radians(360 - degrees)) + - point[1] * math.sin(math.radians(360 - degrees)) + + centroid[0], + 2, + ), + round( + point[1] * math.cos(math.radians(360 - degrees)) + + point[0] * math.sin(math.radians(360 - degrees)) + + centroid[1], + 2, + ), + ) + + def _compute_polygon_vertex(angle: float) -> tuple[float, float]: + start_point = [polygon_radius, 0] + return _apply_rotation(start_point, angle) + + def _get_angles(n_sides: int, rotation: float) -> list[float]: + angles = [] + degrees = 360 / n_sides + # Start with the bottom left polygon vertex + current_angle = (270 - 0.5 * degrees) + rotation + for _ in range(n_sides): + angles.append(current_angle) + current_angle += degrees + if current_angle > 360: + current_angle -= 360 + return angles + + # 3. Variable Declarations + angles = _get_angles(n_sides, rotation) + + # 4. Compute Vertices + return [_compute_polygon_vertex(angle) for angle in angles] + + +def _color_diff( + color1: float | tuple[int, ...], color2: float | tuple[int, ...] +) -> float: + """ + Uses 1-norm distance to calculate difference between two values. + """ + first = color1 if isinstance(color1, tuple) else (color1,) + second = color2 if isinstance(color2, tuple) else (color2,) + + return sum(abs(first[i] - second[i]) for i in range(len(second))) diff --git a/python_modules/PIL/ImageDraw2.py b/python_modules/PIL/ImageDraw2.py new file mode 100644 index 000000000..3d68658ed --- /dev/null +++ b/python_modules/PIL/ImageDraw2.py @@ -0,0 +1,243 @@ +# +# The Python Imaging Library +# $Id$ +# +# WCK-style drawing interface operations +# +# History: +# 2003-12-07 fl created +# 2005-05-15 fl updated; added to PIL as ImageDraw2 +# 2005-05-15 fl added text support +# 2005-05-20 fl added arc/chord/pieslice support +# +# Copyright (c) 2003-2005 by Secret Labs AB +# Copyright (c) 2003-2005 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + + +""" +(Experimental) WCK-style drawing interface operations + +.. seealso:: :py:mod:`PIL.ImageDraw` +""" +from __future__ import annotations + +from typing import Any, AnyStr, BinaryIO + +from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath +from ._typing import Coords, StrOrBytesPath + + +class Pen: + """Stores an outline color and width.""" + + def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None: + self.color = ImageColor.getrgb(color) + self.width = width + + +class Brush: + """Stores a fill color""" + + def __init__(self, color: str, opacity: int = 255) -> None: + self.color = ImageColor.getrgb(color) + + +class Font: + """Stores a TrueType font and color""" + + def __init__( + self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12 + ) -> None: + # FIXME: add support for bitmap fonts + self.color = ImageColor.getrgb(color) + self.font = ImageFont.truetype(file, size) + + +class Draw: + """ + (Experimental) WCK-style drawing interface + """ + + def __init__( + self, + image: Image.Image | str, + size: tuple[int, int] | list[int] | None = None, + color: float | tuple[float, ...] | str | None = None, + ) -> None: + if isinstance(image, str): + if size is None: + msg = "If image argument is mode string, size must be a list or tuple" + raise ValueError(msg) + image = Image.new(image, size, color) + self.draw = ImageDraw.Draw(image) + self.image = image + self.transform: tuple[float, float, float, float, float, float] | None = None + + def flush(self) -> Image.Image: + return self.image + + def render( + self, + op: str, + xy: Coords, + pen: Pen | Brush | None, + brush: Brush | Pen | None = None, + **kwargs: Any, + ) -> None: + # handle color arguments + outline = fill = None + width = 1 + if isinstance(pen, Pen): + outline = pen.color + width = pen.width + elif isinstance(brush, Pen): + outline = brush.color + width = brush.width + if isinstance(brush, Brush): + fill = brush.color + elif isinstance(pen, Brush): + fill = pen.color + # handle transformation + if self.transform: + path = ImagePath.Path(xy) + path.transform(self.transform) + xy = path + # render the item + if op in ("arc", "line"): + kwargs.setdefault("fill", outline) + else: + kwargs.setdefault("fill", fill) + kwargs.setdefault("outline", outline) + if op == "line": + kwargs.setdefault("width", width) + getattr(self.draw, op)(xy, **kwargs) + + def settransform(self, offset: tuple[float, float]) -> None: + """Sets a transformation offset.""" + (xoffset, yoffset) = offset + self.transform = (1, 0, xoffset, 0, 1, yoffset) + + def arc( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: + """ + Draws an arc (a portion of a circle outline) between the start and end + angles, inside the given bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` + """ + self.render("arc", xy, pen, *options, start=start, end=end) + + def chord( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: + """ + Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points + with a straight line. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` + """ + self.render("chord", xy, pen, *options, start=start, end=end) + + def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: + """ + Draws an ellipse inside the given bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` + """ + self.render("ellipse", xy, pen, *options) + + def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: + """ + Draws a line between the coordinates in the ``xy`` list. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` + """ + self.render("line", xy, pen, *options) + + def pieslice( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: + """ + Same as arc, but also draws straight lines between the end points and the + center of the bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` + """ + self.render("pieslice", xy, pen, *options, start=start, end=end) + + def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: + """ + Draws a polygon. + + The polygon outline consists of straight lines between the given + coordinates, plus a straight line between the last and the first + coordinate. + + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` + """ + self.render("polygon", xy, pen, *options) + + def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: + """ + Draws a rectangle. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` + """ + self.render("rectangle", xy, pen, *options) + + def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None: + """ + Draws the string at the given position. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` + """ + if self.transform: + path = ImagePath.Path(xy) + path.transform(self.transform) + xy = path + self.draw.text(xy, text, font=font.font, fill=font.color) + + def textbbox( + self, xy: tuple[float, float], text: AnyStr, font: Font + ) -> tuple[float, float, float, float]: + """ + Returns bounding box (in pixels) of given text. + + :return: ``(left, top, right, bottom)`` bounding box + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` + """ + if self.transform: + path = ImagePath.Path(xy) + path.transform(self.transform) + xy = path + return self.draw.textbbox(xy, text, font=font.font) + + def textlength(self, text: AnyStr, font: Font) -> float: + """ + Returns length (in pixels) of given text. + This is the amount by which following text should be offset. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength` + """ + return self.draw.textlength(text, font=font.font) diff --git a/python_modules/PIL/ImageEnhance.py b/python_modules/PIL/ImageEnhance.py new file mode 100644 index 000000000..0e7e6dd8a --- /dev/null +++ b/python_modules/PIL/ImageEnhance.py @@ -0,0 +1,113 @@ +# +# The Python Imaging Library. +# $Id$ +# +# image enhancement classes +# +# For a background, see "Image Processing By Interpolation and +# Extrapolation", Paul Haeberli and Douglas Voorhies. Available +# at http://www.graficaobscura.com/interp/index.html +# +# History: +# 1996-03-23 fl Created +# 2009-06-16 fl Fixed mean calculation +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image, ImageFilter, ImageStat + + +class _Enhance: + image: Image.Image + degenerate: Image.Image + + def enhance(self, factor: float) -> Image.Image: + """ + Returns an enhanced image. + + :param factor: A floating point value controlling the enhancement. + Factor 1.0 always returns a copy of the original image, + lower factors mean less color (brightness, contrast, + etc), and higher values more. There are no restrictions + on this value. + :rtype: :py:class:`~PIL.Image.Image` + """ + return Image.blend(self.degenerate, self.image, factor) + + +class Color(_Enhance): + """Adjust image color balance. + + This class can be used to adjust the colour balance of an image, in + a manner similar to the controls on a colour TV set. An enhancement + factor of 0.0 gives a black and white image. A factor of 1.0 gives + the original image. + """ + + def __init__(self, image: Image.Image) -> None: + self.image = image + self.intermediate_mode = "L" + if "A" in image.getbands(): + self.intermediate_mode = "LA" + + if self.intermediate_mode != image.mode: + image = image.convert(self.intermediate_mode).convert(image.mode) + self.degenerate = image + + +class Contrast(_Enhance): + """Adjust image contrast. + + This class can be used to control the contrast of an image, similar + to the contrast control on a TV set. An enhancement factor of 0.0 + gives a solid gray image. A factor of 1.0 gives the original image. + """ + + def __init__(self, image: Image.Image) -> None: + self.image = image + if image.mode != "L": + image = image.convert("L") + mean = int(ImageStat.Stat(image).mean[0] + 0.5) + self.degenerate = Image.new("L", image.size, mean) + if self.degenerate.mode != self.image.mode: + self.degenerate = self.degenerate.convert(self.image.mode) + + if "A" in self.image.getbands(): + self.degenerate.putalpha(self.image.getchannel("A")) + + +class Brightness(_Enhance): + """Adjust image brightness. + + This class can be used to control the brightness of an image. An + enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the + original image. + """ + + def __init__(self, image: Image.Image) -> None: + self.image = image + self.degenerate = Image.new(image.mode, image.size, 0) + + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) + + +class Sharpness(_Enhance): + """Adjust image sharpness. + + This class can be used to adjust the sharpness of an image. An + enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the + original image, and a factor of 2.0 gives a sharpened image. + """ + + def __init__(self, image: Image.Image) -> None: + self.image = image + self.degenerate = image.filter(ImageFilter.SMOOTH) + + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) diff --git a/python_modules/PIL/ImageFile.py b/python_modules/PIL/ImageFile.py new file mode 100644 index 000000000..bf556a2c6 --- /dev/null +++ b/python_modules/PIL/ImageFile.py @@ -0,0 +1,922 @@ +# +# The Python Imaging Library. +# $Id$ +# +# base class for image file handlers +# +# history: +# 1995-09-09 fl Created +# 1996-03-11 fl Fixed load mechanism. +# 1996-04-15 fl Added pcx/xbm decoders. +# 1996-04-30 fl Added encoders. +# 1996-12-14 fl Added load helpers +# 1997-01-11 fl Use encode_to_file where possible +# 1997-08-27 fl Flush output in _save +# 1998-03-05 fl Use memory mapping for some modes +# 1999-02-04 fl Use memory mapping also for "I;16" and "I;16B" +# 1999-05-31 fl Added image parser +# 2000-10-12 fl Set readonly flag on memory-mapped images +# 2002-03-20 fl Use better messages for common decoder errors +# 2003-04-21 fl Fall back on mmap/map_buffer if map is not available +# 2003-10-30 fl Added StubImageFile class +# 2004-02-25 fl Made incremental parser more robust +# +# Copyright (c) 1997-2004 by Secret Labs AB +# Copyright (c) 1995-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import abc +import io +import itertools +import logging +import os +import struct +from typing import IO, Any, NamedTuple, cast + +from . import ExifTags, Image +from ._deprecate import deprecate +from ._util import DeferredError, is_path + +TYPE_CHECKING = False +if TYPE_CHECKING: + from ._typing import StrOrBytesPath + +logger = logging.getLogger(__name__) + +MAXBLOCK = 65536 + +SAFEBLOCK = 1024 * 1024 + +LOAD_TRUNCATED_IMAGES = False +"""Whether or not to load truncated image files. User code may change this.""" + +ERRORS = { + -1: "image buffer overrun error", + -2: "decoding error", + -3: "unknown error", + -8: "bad configuration", + -9: "out of memory error", +} +""" +Dict of known error codes returned from :meth:`.PyDecoder.decode`, +:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and +:meth:`.PyEncoder.encode_to_file`. +""" + + +# +# -------------------------------------------------------------------- +# Helpers + + +def _get_oserror(error: int, *, encoder: bool) -> OSError: + try: + msg = Image.core.getcodecstatus(error) + except AttributeError: + msg = ERRORS.get(error) + if not msg: + msg = f"{'encoder' if encoder else 'decoder'} error {error}" + msg += f" when {'writing' if encoder else 'reading'} image file" + return OSError(msg) + + +def raise_oserror(error: int) -> OSError: + deprecate( + "raise_oserror", + 12, + action="It is only useful for translating error codes returned by a codec's " + "decode() method, which ImageFile already does automatically.", + ) + raise _get_oserror(error, encoder=False) + + +def _tilesort(t: _Tile) -> int: + # sort on offset + return t[2] + + +class _Tile(NamedTuple): + codec_name: str + extents: tuple[int, int, int, int] | None + offset: int = 0 + args: tuple[Any, ...] | str | None = None + + +# +# -------------------------------------------------------------------- +# ImageFile base class + + +class ImageFile(Image.Image): + """Base class for image file format handlers.""" + + def __init__( + self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None + ) -> None: + super().__init__() + + self._min_frame = 0 + + self.custom_mimetype: str | None = None + + self.tile: list[_Tile] = [] + """ A list of tile descriptors """ + + self.readonly = 1 # until we know better + + self.decoderconfig: tuple[Any, ...] = () + self.decodermaxblock = MAXBLOCK + + if is_path(fp): + # filename + self.fp = open(fp, "rb") + self.filename = os.fspath(fp) + self._exclusive_fp = True + else: + # stream + self.fp = cast(IO[bytes], fp) + self.filename = filename if filename is not None else "" + # can be overridden + self._exclusive_fp = False + + try: + try: + self._open() + except ( + IndexError, # end of data + TypeError, # end of data (ord) + KeyError, # unsupported mode + EOFError, # got header but not the first frame + struct.error, + ) as v: + raise SyntaxError(v) from v + + if not self.mode or self.size[0] <= 0 or self.size[1] <= 0: + msg = "not identified by this driver" + raise SyntaxError(msg) + except BaseException: + # close the file only if we have opened it this constructor + if self._exclusive_fp: + self.fp.close() + raise + + def _open(self) -> None: + pass + + def _close_fp(self): + if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + + def close(self) -> None: + """ + Closes the file pointer, if possible. + + This operation will destroy the image core and release its memory. + The image data will be unusable afterward. + + This function is required to close images that have multiple frames or + have not had their file read and closed by the + :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for + more information. + """ + try: + self._close_fp() + self.fp = None + except Exception as msg: + logger.debug("Error closing: %s", msg) + + super().close() + + def get_child_images(self) -> list[ImageFile]: + child_images = [] + exif = self.getexif() + ifds = [] + if ExifTags.Base.SubIFDs in exif: + subifd_offsets = exif[ExifTags.Base.SubIFDs] + if subifd_offsets: + if not isinstance(subifd_offsets, tuple): + subifd_offsets = (subifd_offsets,) + for subifd_offset in subifd_offsets: + ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) + if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): + assert exif._info is not None + ifds.append((ifd1, exif._info.next)) + + offset = None + for ifd, ifd_offset in ifds: + assert self.fp is not None + current_offset = self.fp.tell() + if offset is None: + offset = current_offset + + fp = self.fp + if ifd is not None: + thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset) + if thumbnail_offset is not None: + thumbnail_offset += getattr(self, "_exif_offset", 0) + self.fp.seek(thumbnail_offset) + + length = ifd.get(ExifTags.Base.JpegIFByteCount) + assert isinstance(length, int) + data = self.fp.read(length) + fp = io.BytesIO(data) + + with Image.open(fp) as im: + from . import TiffImagePlugin + + if thumbnail_offset is None and isinstance( + im, TiffImagePlugin.TiffImageFile + ): + im._frame_pos = [ifd_offset] + im._seek(0) + im.load() + child_images.append(im) + + if offset is not None: + assert self.fp is not None + self.fp.seek(offset) + return child_images + + def get_format_mimetype(self) -> str | None: + if self.custom_mimetype: + return self.custom_mimetype + if self.format is not None: + return Image.MIME.get(self.format.upper()) + return None + + def __getstate__(self) -> list[Any]: + return super().__getstate__() + [self.filename] + + def __setstate__(self, state: list[Any]) -> None: + self.tile = [] + if len(state) > 5: + self.filename = state[5] + super().__setstate__(state) + + def verify(self) -> None: + """Check file integrity""" + + # raise exception if something's wrong. must be called + # directly after open, and closes file when finished. + if self._exclusive_fp: + self.fp.close() + self.fp = None + + def load(self) -> Image.core.PixelAccess | None: + """Load image data based on tile list""" + + if not self.tile and self._im is None: + msg = "cannot load this image" + raise OSError(msg) + + pixel = Image.Image.load(self) + if not self.tile: + return pixel + + self.map: mmap.mmap | None = None + use_mmap = self.filename and len(self.tile) == 1 + + readonly = 0 + + # look for read/seek overrides + if hasattr(self, "load_read"): + read = self.load_read + # don't use mmap if there are custom read/seek functions + use_mmap = False + else: + read = self.fp.read + + if hasattr(self, "load_seek"): + seek = self.load_seek + use_mmap = False + else: + seek = self.fp.seek + + if use_mmap: + # try memory mapping + decoder_name, extents, offset, args = self.tile[0] + if isinstance(args, str): + args = (args, 0, 1) + if ( + decoder_name == "raw" + and isinstance(args, tuple) + and len(args) >= 3 + and args[0] == self.mode + and args[0] in Image._MAPMODES + ): + try: + # use mmap, if possible + import mmap + + with open(self.filename) as fp: + self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) + if offset + self.size[1] * args[1] > self.map.size(): + msg = "buffer is not large enough" + raise OSError(msg) + self.im = Image.core.map_buffer( + self.map, self.size, decoder_name, offset, args + ) + readonly = 1 + # After trashing self.im, + # we might need to reload the palette data. + if self.palette: + self.palette.dirty = 1 + except (AttributeError, OSError, ImportError): + self.map = None + + self.load_prepare() + err_code = -3 # initialize to unknown error + if not self.map: + # sort tiles in file order + self.tile.sort(key=_tilesort) + + # FIXME: This is a hack to handle TIFF's JpegTables tag. + prefix = getattr(self, "tile_prefix", b"") + + # Remove consecutive duplicates that only differ by their offset + self.tile = [ + list(tiles)[-1] + for _, tiles in itertools.groupby( + self.tile, lambda tile: (tile[0], tile[1], tile[3]) + ) + ] + for i, (decoder_name, extents, offset, args) in enumerate(self.tile): + seek(offset) + decoder = Image._getdecoder( + self.mode, decoder_name, args, self.decoderconfig + ) + try: + decoder.setimage(self.im, extents) + if decoder.pulls_fd: + decoder.setfd(self.fp) + err_code = decoder.decode(b"")[1] + else: + b = prefix + while True: + read_bytes = self.decodermaxblock + if i + 1 < len(self.tile): + next_offset = self.tile[i + 1].offset + if next_offset > offset: + read_bytes = next_offset - offset + try: + s = read(read_bytes) + except (IndexError, struct.error) as e: + # truncated png/gif + if LOAD_TRUNCATED_IMAGES: + break + else: + msg = "image file is truncated" + raise OSError(msg) from e + + if not s: # truncated jpeg + if LOAD_TRUNCATED_IMAGES: + break + else: + msg = ( + "image file is truncated " + f"({len(b)} bytes not processed)" + ) + raise OSError(msg) + + b = b + s + n, err_code = decoder.decode(b) + if n < 0: + break + b = b[n:] + finally: + # Need to cleanup here to prevent leaks + decoder.cleanup() + + self.tile = [] + self.readonly = readonly + + self.load_end() + + if self._exclusive_fp and self._close_exclusive_fp_after_loading: + self.fp.close() + self.fp = None + + if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0: + # still raised if decoder fails to return anything + raise _get_oserror(err_code, encoder=False) + + return Image.Image.load(self) + + def load_prepare(self) -> None: + # create image memory if necessary + if self._im is None: + self.im = Image.core.new(self.mode, self.size) + # create palette (optional) + if self.mode == "P": + Image.Image.load(self) + + def load_end(self) -> None: + # may be overridden + pass + + # may be defined for contained formats + # def load_seek(self, pos: int) -> None: + # pass + + # may be defined for blocked formats (e.g. PNG) + # def load_read(self, read_bytes: int) -> bytes: + # pass + + def _seek_check(self, frame: int) -> bool: + if ( + frame < self._min_frame + # Only check upper limit on frames if additional seek operations + # are not required to do so + or ( + not (hasattr(self, "_n_frames") and self._n_frames is None) + and frame >= getattr(self, "n_frames") + self._min_frame + ) + ): + msg = "attempt to seek outside sequence" + raise EOFError(msg) + + return self.tell() != frame + + +class StubHandler(abc.ABC): + def open(self, im: StubImageFile) -> None: + pass + + @abc.abstractmethod + def load(self, im: StubImageFile) -> Image.Image: + pass + + +class StubImageFile(ImageFile, metaclass=abc.ABCMeta): + """ + Base class for stub image loaders. + + A stub loader is an image loader that can identify files of a + certain format, but relies on external code to load the file. + """ + + @abc.abstractmethod + def _open(self) -> None: + pass + + def load(self) -> Image.core.PixelAccess | None: + loader = self._load() + if loader is None: + msg = f"cannot find loader for this {self.format} file" + raise OSError(msg) + image = loader.load(self) + assert image is not None + # become the other object (!) + self.__class__ = image.__class__ # type: ignore[assignment] + self.__dict__ = image.__dict__ + return image.load() + + @abc.abstractmethod + def _load(self) -> StubHandler | None: + """(Hook) Find actual image loader.""" + pass + + +class Parser: + """ + Incremental image parser. This class implements the standard + feed/close consumer interface. + """ + + incremental = None + image: Image.Image | None = None + data: bytes | None = None + decoder: Image.core.ImagingDecoder | PyDecoder | None = None + offset = 0 + finished = 0 + + def reset(self) -> None: + """ + (Consumer) Reset the parser. Note that you can only call this + method immediately after you've created a parser; parser + instances cannot be reused. + """ + assert self.data is None, "cannot reuse parsers" + + def feed(self, data: bytes) -> None: + """ + (Consumer) Feed data to the parser. + + :param data: A string buffer. + :exception OSError: If the parser failed to parse the image file. + """ + # collect data + + if self.finished: + return + + if self.data is None: + self.data = data + else: + self.data = self.data + data + + # parse what we have + if self.decoder: + if self.offset > 0: + # skip header + skip = min(len(self.data), self.offset) + self.data = self.data[skip:] + self.offset = self.offset - skip + if self.offset > 0 or not self.data: + return + + n, e = self.decoder.decode(self.data) + + if n < 0: + # end of stream + self.data = None + self.finished = 1 + if e < 0: + # decoding error + self.image = None + raise _get_oserror(e, encoder=False) + else: + # end of image + return + self.data = self.data[n:] + + elif self.image: + # if we end up here with no decoder, this file cannot + # be incrementally parsed. wait until we've gotten all + # available data + pass + + else: + # attempt to open this file + try: + with io.BytesIO(self.data) as fp: + im = Image.open(fp) + except OSError: + pass # not enough data + else: + flag = hasattr(im, "load_seek") or hasattr(im, "load_read") + if flag or len(im.tile) != 1: + # custom load code, or multiple tiles + self.decode = None + else: + # initialize decoder + im.load_prepare() + d, e, o, a = im.tile[0] + im.tile = [] + self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig) + self.decoder.setimage(im.im, e) + + # calculate decoder offset + self.offset = o + if self.offset <= len(self.data): + self.data = self.data[self.offset :] + self.offset = 0 + + self.image = im + + def __enter__(self) -> Parser: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def close(self) -> Image.Image: + """ + (Consumer) Close the stream. + + :returns: An image object. + :exception OSError: If the parser failed to parse the image file either + because it cannot be identified or cannot be + decoded. + """ + # finish decoding + if self.decoder: + # get rid of what's left in the buffers + self.feed(b"") + self.data = self.decoder = None + if not self.finished: + msg = "image was incomplete" + raise OSError(msg) + if not self.image: + msg = "cannot parse this image" + raise OSError(msg) + if self.data: + # incremental parsing not possible; reopen the file + # not that we have all data + with io.BytesIO(self.data) as fp: + try: + self.image = Image.open(fp) + finally: + self.image.load() + return self.image + + +# -------------------------------------------------------------------- + + +def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None: + """Helper to save image based on tile list + + :param im: Image object. + :param fp: File object. + :param tile: Tile list. + :param bufsize: Optional buffer size + """ + + im.load() + if not hasattr(im, "encoderconfig"): + im.encoderconfig = () + tile.sort(key=_tilesort) + # FIXME: make MAXBLOCK a configuration parameter + # It would be great if we could have the encoder specify what it needs + # But, it would need at least the image size in most cases. RawEncode is + # a tricky case. + bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c + try: + fh = fp.fileno() + fp.flush() + _encode_tile(im, fp, tile, bufsize, fh) + except (AttributeError, io.UnsupportedOperation) as exc: + _encode_tile(im, fp, tile, bufsize, None, exc) + if hasattr(fp, "flush"): + fp.flush() + + +def _encode_tile( + im: Image.Image, + fp: IO[bytes], + tile: list[_Tile], + bufsize: int, + fh: int | None, + exc: BaseException | None = None, +) -> None: + for encoder_name, extents, offset, args in tile: + if offset > 0: + fp.seek(offset) + encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig) + try: + encoder.setimage(im.im, extents) + if encoder.pushes_fd: + encoder.setfd(fp) + errcode = encoder.encode_to_pyfd()[1] + else: + if exc: + # compress to Python file-compatible object + while True: + errcode, data = encoder.encode(bufsize)[1:] + fp.write(data) + if errcode: + break + else: + # slight speedup: compress to real file object + assert fh is not None + errcode = encoder.encode_to_file(fh, bufsize) + if errcode < 0: + raise _get_oserror(errcode, encoder=True) from exc + finally: + encoder.cleanup() + + +def _safe_read(fp: IO[bytes], size: int) -> bytes: + """ + Reads large blocks in a safe way. Unlike fp.read(n), this function + doesn't trust the user. If the requested size is larger than + SAFEBLOCK, the file is read block by block. + + :param fp: File handle. Must implement a read method. + :param size: Number of bytes to read. + :returns: A string containing size bytes of data. + + Raises an OSError if the file is truncated and the read cannot be completed + + """ + if size <= 0: + return b"" + if size <= SAFEBLOCK: + data = fp.read(size) + if len(data) < size: + msg = "Truncated File Read" + raise OSError(msg) + return data + blocks: list[bytes] = [] + remaining_size = size + while remaining_size > 0: + block = fp.read(min(remaining_size, SAFEBLOCK)) + if not block: + break + blocks.append(block) + remaining_size -= len(block) + if sum(len(block) for block in blocks) < size: + msg = "Truncated File Read" + raise OSError(msg) + return b"".join(blocks) + + +class PyCodecState: + def __init__(self) -> None: + self.xsize = 0 + self.ysize = 0 + self.xoff = 0 + self.yoff = 0 + + def extents(self) -> tuple[int, int, int, int]: + return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize + + +class PyCodec: + fd: IO[bytes] | None + + def __init__(self, mode: str, *args: Any) -> None: + self.im: Image.core.ImagingCore | None = None + self.state = PyCodecState() + self.fd = None + self.mode = mode + self.init(args) + + def init(self, args: tuple[Any, ...]) -> None: + """ + Override to perform codec specific initialization + + :param args: Tuple of arg items from the tile entry + :returns: None + """ + self.args = args + + def cleanup(self) -> None: + """ + Override to perform codec specific cleanup + + :returns: None + """ + pass + + def setfd(self, fd: IO[bytes]) -> None: + """ + Called from ImageFile to set the Python file-like object + + :param fd: A Python file-like object + :returns: None + """ + self.fd = fd + + def setimage( + self, + im: Image.core.ImagingCore, + extents: tuple[int, int, int, int] | None = None, + ) -> None: + """ + Called from ImageFile to set the core output image for the codec + + :param im: A core image object + :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle + for this tile + :returns: None + """ + + # following c code + self.im = im + + if extents: + (x0, y0, x1, y1) = extents + else: + (x0, y0, x1, y1) = (0, 0, 0, 0) + + if x0 == 0 and x1 == 0: + self.state.xsize, self.state.ysize = self.im.size + else: + self.state.xoff = x0 + self.state.yoff = y0 + self.state.xsize = x1 - x0 + self.state.ysize = y1 - y0 + + if self.state.xsize <= 0 or self.state.ysize <= 0: + msg = "Size cannot be negative" + raise ValueError(msg) + + if ( + self.state.xsize + self.state.xoff > self.im.size[0] + or self.state.ysize + self.state.yoff > self.im.size[1] + ): + msg = "Tile cannot extend outside image" + raise ValueError(msg) + + +class PyDecoder(PyCodec): + """ + Python implementation of a format decoder. Override this class and + add the decoding logic in the :meth:`decode` method. + + See :ref:`Writing Your Own File Codec in Python` + """ + + _pulls_fd = False + + @property + def pulls_fd(self) -> bool: + return self._pulls_fd + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + """ + Override to perform the decoding process. + + :param buffer: A bytes object with the data to be decoded. + :returns: A tuple of ``(bytes consumed, errcode)``. + If finished with decoding return -1 for the bytes consumed. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + msg = "unavailable in base decoder" + raise NotImplementedError(msg) + + def set_as_raw( + self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = () + ) -> None: + """ + Convenience method to set the internal image from a stream of raw data + + :param data: Bytes to be set + :param rawmode: The rawmode to be used for the decoder. + If not specified, it will default to the mode of the image + :param extra: Extra arguments for the decoder. + :returns: None + """ + + if not rawmode: + rawmode = self.mode + d = Image._getdecoder(self.mode, "raw", rawmode, extra) + assert self.im is not None + d.setimage(self.im, self.state.extents()) + s = d.decode(data) + + if s[0] >= 0: + msg = "not enough image data" + raise ValueError(msg) + if s[1] != 0: + msg = "cannot decode image data" + raise ValueError(msg) + + +class PyEncoder(PyCodec): + """ + Python implementation of a format encoder. Override this class and + add the decoding logic in the :meth:`encode` method. + + See :ref:`Writing Your Own File Codec in Python` + """ + + _pushes_fd = False + + @property + def pushes_fd(self) -> bool: + return self._pushes_fd + + def encode(self, bufsize: int) -> tuple[int, int, bytes]: + """ + Override to perform the encoding process. + + :param bufsize: Buffer size. + :returns: A tuple of ``(bytes encoded, errcode, bytes)``. + If finished with encoding return 1 for the error code. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + msg = "unavailable in base encoder" + raise NotImplementedError(msg) + + def encode_to_pyfd(self) -> tuple[int, int]: + """ + If ``pushes_fd`` is ``True``, then this method will be used, + and ``encode()`` will only be called once. + + :returns: A tuple of ``(bytes consumed, errcode)``. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + if not self.pushes_fd: + return 0, -8 # bad configuration + bytes_consumed, errcode, data = self.encode(0) + if data: + assert self.fd is not None + self.fd.write(data) + return bytes_consumed, errcode + + def encode_to_file(self, fh: int, bufsize: int) -> int: + """ + :param fh: File handle. + :param bufsize: Buffer size. + + :returns: If finished successfully, return 0. + Otherwise, return an error code. Err codes are from + :data:`.ImageFile.ERRORS`. + """ + errcode = 0 + while errcode == 0: + status, errcode, buf = self.encode(bufsize) + if status > 0: + os.write(fh, buf[status:]) + return errcode diff --git a/python_modules/PIL/ImageFilter.py b/python_modules/PIL/ImageFilter.py new file mode 100644 index 000000000..b9ed54ab2 --- /dev/null +++ b/python_modules/PIL/ImageFilter.py @@ -0,0 +1,604 @@ +# +# The Python Imaging Library. +# $Id$ +# +# standard filters +# +# History: +# 1995-11-27 fl Created +# 2002-06-08 fl Added rank and mode filters +# 2003-09-15 fl Fixed rank calculation in rank filter; added expand call +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-2002 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import abc +import functools +from collections.abc import Sequence +from types import ModuleType +from typing import Any, Callable, cast + +TYPE_CHECKING = False +if TYPE_CHECKING: + from . import _imaging + from ._typing import NumpyArray + + +class Filter(abc.ABC): + @abc.abstractmethod + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: + pass + + +class MultibandFilter(Filter): + pass + + +class BuiltinFilter(MultibandFilter): + filterargs: tuple[Any, ...] + + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: + if image.mode == "P": + msg = "cannot filter palette images" + raise ValueError(msg) + return image.filter(*self.filterargs) + + +class Kernel(BuiltinFilter): + """ + Create a convolution kernel. This only supports 3x3 and 5x5 integer and floating + point kernels. + + Kernels can only be applied to "L" and "RGB" images. + + :param size: Kernel size, given as (width, height). This must be (3,3) or (5,5). + :param kernel: A sequence containing kernel weights. The kernel will be flipped + vertically before being applied to the image. + :param scale: Scale factor. If given, the result for each pixel is divided by this + value. The default is the sum of the kernel weights. + :param offset: Offset. If given, this value is added to the result, after it has + been divided by the scale factor. + """ + + name = "Kernel" + + def __init__( + self, + size: tuple[int, int], + kernel: Sequence[float], + scale: float | None = None, + offset: float = 0, + ) -> None: + if scale is None: + # default scale is sum of kernel + scale = functools.reduce(lambda a, b: a + b, kernel) + if size[0] * size[1] != len(kernel): + msg = "not enough coefficients in kernel" + raise ValueError(msg) + self.filterargs = size, scale, offset, kernel + + +class RankFilter(Filter): + """ + Create a rank filter. The rank filter sorts all pixels in + a window of the given size, and returns the ``rank``'th value. + + :param size: The kernel size, in pixels. + :param rank: What pixel value to pick. Use 0 for a min filter, + ``size * size / 2`` for a median filter, ``size * size - 1`` + for a max filter, etc. + """ + + name = "Rank" + + def __init__(self, size: int, rank: int) -> None: + self.size = size + self.rank = rank + + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: + if image.mode == "P": + msg = "cannot filter palette images" + raise ValueError(msg) + image = image.expand(self.size // 2, self.size // 2) + return image.rankfilter(self.size, self.rank) + + +class MedianFilter(RankFilter): + """ + Create a median filter. Picks the median pixel value in a window with the + given size. + + :param size: The kernel size, in pixels. + """ + + name = "Median" + + def __init__(self, size: int = 3) -> None: + self.size = size + self.rank = size * size // 2 + + +class MinFilter(RankFilter): + """ + Create a min filter. Picks the lowest pixel value in a window with the + given size. + + :param size: The kernel size, in pixels. + """ + + name = "Min" + + def __init__(self, size: int = 3) -> None: + self.size = size + self.rank = 0 + + +class MaxFilter(RankFilter): + """ + Create a max filter. Picks the largest pixel value in a window with the + given size. + + :param size: The kernel size, in pixels. + """ + + name = "Max" + + def __init__(self, size: int = 3) -> None: + self.size = size + self.rank = size * size - 1 + + +class ModeFilter(Filter): + """ + Create a mode filter. Picks the most frequent pixel value in a box with the + given size. Pixel values that occur only once or twice are ignored; if no + pixel value occurs more than twice, the original pixel value is preserved. + + :param size: The kernel size, in pixels. + """ + + name = "Mode" + + def __init__(self, size: int = 3) -> None: + self.size = size + + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: + return image.modefilter(self.size) + + +class GaussianBlur(MultibandFilter): + """Blurs the image with a sequence of extended box filters, which + approximates a Gaussian kernel. For details on accuracy see + + + :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two + numbers for x and y, or a single number for both. + """ + + name = "GaussianBlur" + + def __init__(self, radius: float | Sequence[float] = 2) -> None: + self.radius = radius + + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: + xy = self.radius + if isinstance(xy, (int, float)): + xy = (xy, xy) + if xy == (0, 0): + return image.copy() + return image.gaussian_blur(xy) + + +class BoxBlur(MultibandFilter): + """Blurs the image by setting each pixel to the average value of the pixels + in a square box extending radius pixels in each direction. + Supports float radius of arbitrary size. Uses an optimized implementation + which runs in linear time relative to the size of the image + for any radius value. + + :param radius: Size of the box in a direction. Either a sequence of two numbers for + x and y, or a single number for both. + + Radius 0 does not blur, returns an identical image. + Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total. + """ + + name = "BoxBlur" + + def __init__(self, radius: float | Sequence[float]) -> None: + xy = radius if isinstance(radius, (tuple, list)) else (radius, radius) + if xy[0] < 0 or xy[1] < 0: + msg = "radius must be >= 0" + raise ValueError(msg) + self.radius = radius + + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: + xy = self.radius + if isinstance(xy, (int, float)): + xy = (xy, xy) + if xy == (0, 0): + return image.copy() + return image.box_blur(xy) + + +class UnsharpMask(MultibandFilter): + """Unsharp mask filter. + + See Wikipedia's entry on `digital unsharp masking`_ for an explanation of + the parameters. + + :param radius: Blur Radius + :param percent: Unsharp strength, in percent + :param threshold: Threshold controls the minimum brightness change that + will be sharpened + + .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking + + """ + + name = "UnsharpMask" + + def __init__( + self, radius: float = 2, percent: int = 150, threshold: int = 3 + ) -> None: + self.radius = radius + self.percent = percent + self.threshold = threshold + + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: + return image.unsharp_mask(self.radius, self.percent, self.threshold) + + +class BLUR(BuiltinFilter): + name = "Blur" + # fmt: off + filterargs = (5, 5), 16, 0, ( + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + ) + # fmt: on + + +class CONTOUR(BuiltinFilter): + name = "Contour" + # fmt: off + filterargs = (3, 3), 1, 255, ( + -1, -1, -1, + -1, 8, -1, + -1, -1, -1, + ) + # fmt: on + + +class DETAIL(BuiltinFilter): + name = "Detail" + # fmt: off + filterargs = (3, 3), 6, 0, ( + 0, -1, 0, + -1, 10, -1, + 0, -1, 0, + ) + # fmt: on + + +class EDGE_ENHANCE(BuiltinFilter): + name = "Edge-enhance" + # fmt: off + filterargs = (3, 3), 2, 0, ( + -1, -1, -1, + -1, 10, -1, + -1, -1, -1, + ) + # fmt: on + + +class EDGE_ENHANCE_MORE(BuiltinFilter): + name = "Edge-enhance More" + # fmt: off + filterargs = (3, 3), 1, 0, ( + -1, -1, -1, + -1, 9, -1, + -1, -1, -1, + ) + # fmt: on + + +class EMBOSS(BuiltinFilter): + name = "Emboss" + # fmt: off + filterargs = (3, 3), 1, 128, ( + -1, 0, 0, + 0, 1, 0, + 0, 0, 0, + ) + # fmt: on + + +class FIND_EDGES(BuiltinFilter): + name = "Find Edges" + # fmt: off + filterargs = (3, 3), 1, 0, ( + -1, -1, -1, + -1, 8, -1, + -1, -1, -1, + ) + # fmt: on + + +class SHARPEN(BuiltinFilter): + name = "Sharpen" + # fmt: off + filterargs = (3, 3), 16, 0, ( + -2, -2, -2, + -2, 32, -2, + -2, -2, -2, + ) + # fmt: on + + +class SMOOTH(BuiltinFilter): + name = "Smooth" + # fmt: off + filterargs = (3, 3), 13, 0, ( + 1, 1, 1, + 1, 5, 1, + 1, 1, 1, + ) + # fmt: on + + +class SMOOTH_MORE(BuiltinFilter): + name = "Smooth More" + # fmt: off + filterargs = (5, 5), 100, 0, ( + 1, 1, 1, 1, 1, + 1, 5, 5, 5, 1, + 1, 5, 44, 5, 1, + 1, 5, 5, 5, 1, + 1, 1, 1, 1, 1, + ) + # fmt: on + + +class Color3DLUT(MultibandFilter): + """Three-dimensional color lookup table. + + Transforms 3-channel pixels using the values of the channels as coordinates + in the 3D lookup table and interpolating the nearest elements. + + This method allows you to apply almost any color transformation + in constant time by using pre-calculated decimated tables. + + .. versionadded:: 5.2.0 + + :param size: Size of the table. One int or tuple of (int, int, int). + Minimal size in any dimension is 2, maximum is 65. + :param table: Flat lookup table. A list of ``channels * size**3`` + float elements or a list of ``size**3`` channels-sized + tuples with floats. Channels are changed first, + then first dimension, then second, then third. + Value 0.0 corresponds lowest value of output, 1.0 highest. + :param channels: Number of channels in the table. Could be 3 or 4. + Default is 3. + :param target_mode: A mode for the result image. Should have not less + than ``channels`` channels. Default is ``None``, + which means that mode wouldn't be changed. + """ + + name = "Color 3D LUT" + + def __init__( + self, + size: int | tuple[int, int, int], + table: Sequence[float] | Sequence[Sequence[int]] | NumpyArray, + channels: int = 3, + target_mode: str | None = None, + **kwargs: bool, + ) -> None: + if channels not in (3, 4): + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) + self.size = size = self._check_size(size) + self.channels = channels + self.mode = target_mode + + # Hidden flag `_copy_table=False` could be used to avoid extra copying + # of the table if the table is specially made for the constructor. + copy_table = kwargs.get("_copy_table", True) + items = size[0] * size[1] * size[2] + wrong_size = False + + numpy: ModuleType | None = None + if hasattr(table, "shape"): + try: + import numpy + except ImportError: + pass + + if numpy and isinstance(table, numpy.ndarray): + numpy_table: NumpyArray = table + if copy_table: + numpy_table = numpy_table.copy() + + if numpy_table.shape in [ + (items * channels,), + (items, channels), + (size[2], size[1], size[0], channels), + ]: + table = numpy_table.reshape(items * channels) + else: + wrong_size = True + + else: + if copy_table: + table = list(table) + + # Convert to a flat list + if table and isinstance(table[0], (list, tuple)): + raw_table = cast(Sequence[Sequence[int]], table) + flat_table: list[int] = [] + for pixel in raw_table: + if len(pixel) != channels: + msg = ( + "The elements of the table should " + f"have a length of {channels}." + ) + raise ValueError(msg) + flat_table.extend(pixel) + table = flat_table + + if wrong_size or len(table) != items * channels: + msg = ( + "The table should have either channels * size**3 float items " + "or size**3 items of channels-sized tuples with floats. " + f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. " + f"Actual length: {len(table)}" + ) + raise ValueError(msg) + self.table = table + + @staticmethod + def _check_size(size: Any) -> tuple[int, int, int]: + try: + _, _, _ = size + except ValueError as e: + msg = "Size should be either an integer or a tuple of three integers." + raise ValueError(msg) from e + except TypeError: + size = (size, size, size) + size = tuple(int(x) for x in size) + for size_1d in size: + if not 2 <= size_1d <= 65: + msg = "Size should be in [2, 65] range." + raise ValueError(msg) + return size + + @classmethod + def generate( + cls, + size: int | tuple[int, int, int], + callback: Callable[[float, float, float], tuple[float, ...]], + channels: int = 3, + target_mode: str | None = None, + ) -> Color3DLUT: + """Generates new LUT using provided callback. + + :param size: Size of the table. Passed to the constructor. + :param callback: Function with three parameters which correspond + three color channels. Will be called ``size**3`` + times with values from 0.0 to 1.0 and should return + a tuple with ``channels`` elements. + :param channels: The number of channels which should return callback. + :param target_mode: Passed to the constructor of the resulting + lookup table. + """ + size_1d, size_2d, size_3d = cls._check_size(size) + if channels not in (3, 4): + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) + + table: list[float] = [0] * (size_1d * size_2d * size_3d * channels) + idx_out = 0 + for b in range(size_3d): + for g in range(size_2d): + for r in range(size_1d): + table[idx_out : idx_out + channels] = callback( + r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1) + ) + idx_out += channels + + return cls( + (size_1d, size_2d, size_3d), + table, + channels=channels, + target_mode=target_mode, + _copy_table=False, + ) + + def transform( + self, + callback: Callable[..., tuple[float, ...]], + with_normals: bool = False, + channels: int | None = None, + target_mode: str | None = None, + ) -> Color3DLUT: + """Transforms the table values using provided callback and returns + a new LUT with altered values. + + :param callback: A function which takes old lookup table values + and returns a new set of values. The number + of arguments which function should take is + ``self.channels`` or ``3 + self.channels`` + if ``with_normals`` flag is set. + Should return a tuple of ``self.channels`` or + ``channels`` elements if it is set. + :param with_normals: If true, ``callback`` will be called with + coordinates in the color cube as the first + three arguments. Otherwise, ``callback`` + will be called only with actual color values. + :param channels: The number of channels in the resulting lookup table. + :param target_mode: Passed to the constructor of the resulting + lookup table. + """ + if channels not in (None, 3, 4): + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) + ch_in = self.channels + ch_out = channels or ch_in + size_1d, size_2d, size_3d = self.size + + table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out) + idx_in = 0 + idx_out = 0 + for b in range(size_3d): + for g in range(size_2d): + for r in range(size_1d): + values = self.table[idx_in : idx_in + ch_in] + if with_normals: + values = callback( + r / (size_1d - 1), + g / (size_2d - 1), + b / (size_3d - 1), + *values, + ) + else: + values = callback(*values) + table[idx_out : idx_out + ch_out] = values + idx_in += ch_in + idx_out += ch_out + + return type(self)( + self.size, + table, + channels=ch_out, + target_mode=target_mode or self.mode, + _copy_table=False, + ) + + def __repr__(self) -> str: + r = [ + f"{self.__class__.__name__} from {self.table.__class__.__name__}", + "size={:d}x{:d}x{:d}".format(*self.size), + f"channels={self.channels:d}", + ] + if self.mode: + r.append(f"target_mode={self.mode}") + return "<{}>".format(" ".join(r)) + + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: + from . import Image + + return image.color_lut_3d( + self.mode or image.mode, + Image.Resampling.BILINEAR, + self.channels, + self.size, + self.table, + ) diff --git a/python_modules/PIL/ImageFont.py b/python_modules/PIL/ImageFont.py new file mode 100644 index 000000000..329c463ff --- /dev/null +++ b/python_modules/PIL/ImageFont.py @@ -0,0 +1,1339 @@ +# +# The Python Imaging Library. +# $Id$ +# +# PIL raster font management +# +# History: +# 1996-08-07 fl created (experimental) +# 1997-08-25 fl minor adjustments to handle fonts from pilfont 0.3 +# 1999-02-06 fl rewrote most font management stuff in C +# 1999-03-17 fl take pth files into account in load_path (from Richard Jones) +# 2001-02-17 fl added freetype support +# 2001-05-09 fl added TransposedFont wrapper class +# 2002-03-04 fl make sure we have a "L" or "1" font +# 2002-12-04 fl skip non-directory entries in the system path +# 2003-04-29 fl add embedded default font +# 2003-09-27 fl added support for truetype charmap encodings +# +# Todo: +# Adapt to PILFONT2 format (16-bit fonts, compressed, single file) +# +# Copyright (c) 1997-2003 by Secret Labs AB +# Copyright (c) 1996-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +from __future__ import annotations + +import base64 +import os +import sys +import warnings +from enum import IntEnum +from io import BytesIO +from types import ModuleType +from typing import IO, Any, BinaryIO, TypedDict, cast + +from . import Image, features +from ._typing import StrOrBytesPath +from ._util import DeferredError, is_path + +TYPE_CHECKING = False +if TYPE_CHECKING: + from . import ImageFile + from ._imaging import ImagingFont + from ._imagingft import Font + + +class Axis(TypedDict): + minimum: int | None + default: int | None + maximum: int | None + name: bytes | None + + +class Layout(IntEnum): + BASIC = 0 + RAQM = 1 + + +MAX_STRING_LENGTH = 1_000_000 + + +core: ModuleType | DeferredError +try: + from . import _imagingft as core +except ImportError as ex: + core = DeferredError.new(ex) + + +def _string_length_check(text: str | bytes | bytearray) -> None: + if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: + msg = "too many characters in string" + raise ValueError(msg) + + +# FIXME: add support for pilfont2 format (see FontFile.py) + +# -------------------------------------------------------------------- +# Font metrics format: +# "PILfont" LF +# fontdescriptor LF +# (optional) key=value... LF +# "DATA" LF +# binary data: 256*10*2 bytes (dx, dy, dstbox, srcbox) +# +# To place a character, cut out srcbox and paste at dstbox, +# relative to the character position. Then move the character +# position according to dx, dy. +# -------------------------------------------------------------------- + + +class ImageFont: + """PIL font wrapper""" + + font: ImagingFont + + def _load_pilfont(self, filename: str) -> None: + with open(filename, "rb") as fp: + image: ImageFile.ImageFile | None = None + root = os.path.splitext(filename)[0] + + for ext in (".png", ".gif", ".pbm"): + if image: + image.close() + try: + fullname = root + ext + image = Image.open(fullname) + except Exception: + pass + else: + if image and image.mode in ("1", "L"): + break + else: + if image: + image.close() + + msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}" + raise OSError(msg) + + self.file = fullname + + self._load_pilfont_data(fp, image) + image.close() + + def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: + # read PILfont header + if file.readline() != b"PILfont\n": + msg = "Not a PILfont file" + raise SyntaxError(msg) + file.readline().split(b";") + self.info = [] # FIXME: should be a dictionary + while True: + s = file.readline() + if not s or s == b"DATA\n": + break + self.info.append(s) + + # read PILfont metrics + data = file.read(256 * 20) + + # check image + if image.mode not in ("1", "L"): + msg = "invalid font image mode" + raise TypeError(msg) + + image.load() + + self.font = Image.core.font(image.im, data) + + def getmask( + self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any + ) -> Image.core.ImagingCore: + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :return: An internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module. + """ + _string_length_check(text) + Image._decompression_bomb_check(self.font.getsize(text)) + return self.font.getmask(text, mode) + + def getbbox( + self, text: str | bytes | bytearray, *args: Any, **kwargs: Any + ) -> tuple[int, int, int, int]: + """ + Returns bounding box (in pixels) of given text. + + .. versionadded:: 9.2.0 + + :param text: Text to render. + + :return: ``(left, top, right, bottom)`` bounding box + """ + _string_length_check(text) + width, height = self.font.getsize(text) + return 0, 0, width, height + + def getlength( + self, text: str | bytes | bytearray, *args: Any, **kwargs: Any + ) -> int: + """ + Returns length (in pixels) of given text. + This is the amount by which following text should be offset. + + .. versionadded:: 9.2.0 + """ + _string_length_check(text) + width, height = self.font.getsize(text) + return width + + +## +# Wrapper for FreeType fonts. Application code should use the +# truetype factory function to create font objects. + + +class FreeTypeFont: + """FreeType font wrapper (requires _imagingft service)""" + + font: Font + font_bytes: bytes + + def __init__( + self, + font: StrOrBytesPath | BinaryIO, + size: float = 10, + index: int = 0, + encoding: str = "", + layout_engine: Layout | None = None, + ) -> None: + # FIXME: use service provider instead + + if isinstance(core, DeferredError): + raise core.ex + + if size <= 0: + msg = f"font size must be greater than 0, not {size}" + raise ValueError(msg) + + self.path = font + self.size = size + self.index = index + self.encoding = encoding + + try: + from packaging.version import parse as parse_version + except ImportError: + pass + else: + if freetype_version := features.version_module("freetype2"): + if parse_version(freetype_version) < parse_version("2.9.1"): + warnings.warn( + "Support for FreeType 2.9.0 is deprecated and will be removed " + "in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 " + "or newer, preferably FreeType 2.10.4 which fixes " + "CVE-2020-15999.", + DeprecationWarning, + ) + + if layout_engine not in (Layout.BASIC, Layout.RAQM): + layout_engine = Layout.BASIC + if core.HAVE_RAQM: + layout_engine = Layout.RAQM + elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: + warnings.warn( + "Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout." + ) + layout_engine = Layout.BASIC + + self.layout_engine = layout_engine + + def load_from_bytes(f: IO[bytes]) -> None: + self.font_bytes = f.read() + self.font = core.getfont( + "", size, index, encoding, self.font_bytes, layout_engine + ) + + if is_path(font): + font = os.fspath(font) + if sys.platform == "win32": + font_bytes_path = font if isinstance(font, bytes) else font.encode() + try: + font_bytes_path.decode("ascii") + except UnicodeDecodeError: + # FreeType cannot load fonts with non-ASCII characters on Windows + # So load it into memory first + with open(font, "rb") as f: + load_from_bytes(f) + return + self.font = core.getfont( + font, size, index, encoding, layout_engine=layout_engine + ) + else: + load_from_bytes(cast(IO[bytes], font)) + + def __getstate__(self) -> list[Any]: + return [self.path, self.size, self.index, self.encoding, self.layout_engine] + + def __setstate__(self, state: list[Any]) -> None: + path, size, index, encoding, layout_engine = state + FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine) + + def getname(self) -> tuple[str | None, str | None]: + """ + :return: A tuple of the font family (e.g. Helvetica) and the font style + (e.g. Bold) + """ + return self.font.family, self.font.style + + def getmetrics(self) -> tuple[int, int]: + """ + :return: A tuple of the font ascent (the distance from the baseline to + the highest outline point) and descent (the distance from the + baseline to the lowest outline point, a negative value) + """ + return self.font.ascent, self.font.descent + + def getlength( + self, + text: str | bytes, + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + ) -> float: + """ + Returns length (in pixels with 1/64 precision) of given text when rendered + in font with provided direction, features, and language. + + This is the amount by which following text should be offset. + Text bounding box may extend past the length in some fonts, + e.g. when using italics or accents. + + The result is returned as a float; it is a whole number if using basic layout. + + Note that the sum of two lengths may not equal the length of a concatenated + string due to kerning. If you need to adjust for kerning, include the following + character and subtract its length. + + For example, instead of :: + + hello = font.getlength("Hello") + world = font.getlength("World") + hello_world = hello + world # not adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # may fail + + use :: + + hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning + world = font.getlength("World") + hello_world = hello + world # adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # True + + or disable kerning with (requires libraqm) :: + + hello = draw.textlength("Hello", font, features=["-kern"]) + world = draw.textlength("World", font, features=["-kern"]) + hello_world = hello + world # kerning is disabled, no need to adjust + assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) + + .. versionadded:: 8.0.0 + + :param text: Text to measure. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + :return: Either width for horizontal text, or height for vertical text. + """ + _string_length_check(text) + return self.font.getlength(text, mode, direction, features, language) / 64 + + def getbbox( + self, + text: str | bytes, + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ) -> tuple[float, float, float, float]: + """ + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + + Use :py:meth:`getlength()` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + :param stroke_width: The width of the text stroke. + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + + :return: ``(left, top, right, bottom)`` bounding box + """ + _string_length_check(text) + size, offset = self.font.getsize( + text, mode, direction, features, language, anchor + ) + left, top = offset[0] - stroke_width, offset[1] - stroke_width + width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width + return left, top, left + width, top + height + + def getmask( + self, + text: str | bytes, + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ink: int = 0, + start: tuple[float, float] | None = None, + ) -> Image.core.ImagingCore: + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. If the font has embedded color data, the bitmap + should have mode ``RGBA``. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + + :return: An internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module. + """ + return self.getmask2( + text, + mode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + anchor=anchor, + ink=ink, + start=start, + )[0] + + def getmask2( + self, + text: str | bytes, + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ink: int = 0, + start: tuple[float, float] | None = None, + *args: Any, + **kwargs: Any, + ) -> tuple[Image.core.ImagingCore, tuple[int, int]]: + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. If the font has embedded color data, the bitmap + should have mode ``RGBA``. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + + :return: A tuple of an internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module, and the text offset, the + gap between the starting coordinate and the first marking + """ + _string_length_check(text) + if start is None: + start = (0, 0) + + def fill(width: int, height: int) -> Image.core.ImagingCore: + size = (width, height) + Image._decompression_bomb_check(size) + return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) + + return self.font.render( + text, + fill, + mode, + direction, + features, + language, + stroke_width, + kwargs.get("stroke_filled", False), + anchor, + ink, + start, + ) + + def font_variant( + self, + font: StrOrBytesPath | BinaryIO | None = None, + size: float | None = None, + index: int | None = None, + encoding: str | None = None, + layout_engine: Layout | None = None, + ) -> FreeTypeFont: + """ + Create a copy of this FreeTypeFont object, + using any specified arguments to override the settings. + + Parameters are identical to the parameters used to initialize this + object. + + :return: A FreeTypeFont object. + """ + if font is None: + try: + font = BytesIO(self.font_bytes) + except AttributeError: + font = self.path + return FreeTypeFont( + font=font, + size=self.size if size is None else size, + index=self.index if index is None else index, + encoding=self.encoding if encoding is None else encoding, + layout_engine=layout_engine or self.layout_engine, + ) + + def get_variation_names(self) -> list[bytes]: + """ + :returns: A list of the named styles in a variation font. + :exception OSError: If the font is not a variation font. + """ + try: + names = self.font.getvarnames() + except AttributeError as e: + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e + return [name.replace(b"\x00", b"") for name in names] + + def set_variation_by_name(self, name: str | bytes) -> None: + """ + :param name: The name of the style. + :exception OSError: If the font is not a variation font. + """ + names = self.get_variation_names() + if not isinstance(name, bytes): + name = name.encode() + index = names.index(name) + 1 + + if index == getattr(self, "_last_variation_index", None): + # When the same name is set twice in a row, + # there is an 'unknown freetype error' + # https://savannah.nongnu.org/bugs/?56186 + return + self._last_variation_index = index + + self.font.setvarname(index) + + def get_variation_axes(self) -> list[Axis]: + """ + :returns: A list of the axes in a variation font. + :exception OSError: If the font is not a variation font. + """ + try: + axes = self.font.getvaraxes() + except AttributeError as e: + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e + for axis in axes: + if axis["name"]: + axis["name"] = axis["name"].replace(b"\x00", b"") + return axes + + def set_variation_by_axes(self, axes: list[float]) -> None: + """ + :param axes: A list of values for each axis. + :exception OSError: If the font is not a variation font. + """ + try: + self.font.setvaraxes(axes) + except AttributeError as e: + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e + + +class TransposedFont: + """Wrapper for writing rotated or mirrored text""" + + def __init__( + self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None + ): + """ + Wrapper that creates a transposed font from any existing font + object. + + :param font: A font object. + :param orientation: An optional orientation. If given, this should + be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM, + Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or + Image.Transpose.ROTATE_270. + """ + self.font = font + self.orientation = orientation # any 'transpose' argument, or None + + def getmask( + self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any + ) -> Image.core.ImagingCore: + im = self.font.getmask(text, mode, *args, **kwargs) + if self.orientation is not None: + return im.transpose(self.orientation) + return im + + def getbbox( + self, text: str | bytes, *args: Any, **kwargs: Any + ) -> tuple[int, int, float, float]: + # TransposedFont doesn't support getmask2, move top-left point to (0, 0) + # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont + left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) + width = right - left + height = bottom - top + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): + return 0, 0, height, width + return 0, 0, width, height + + def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float: + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): + msg = "text length is undefined for text rotated by 90 or 270 degrees" + raise ValueError(msg) + return self.font.getlength(text, *args, **kwargs) + + +def load(filename: str) -> ImageFont: + """ + Load a font file. This function loads a font object from the given + bitmap font file, and returns the corresponding font object. For loading TrueType + or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`. + + :param filename: Name of font file. + :return: A font object. + :exception OSError: If the file could not be read. + """ + f = ImageFont() + f._load_pilfont(filename) + return f + + +def truetype( + font: StrOrBytesPath | BinaryIO, + size: float = 10, + index: int = 0, + encoding: str = "", + layout_engine: Layout | None = None, +) -> FreeTypeFont: + """ + Load a TrueType or OpenType font from a file or file-like object, + and create a font object. This function loads a font object from the given + file or file-like object, and creates a font object for a font of the given + size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load` + and :py:func:`~PIL.ImageFont.load_path`. + + Pillow uses FreeType to open font files. On Windows, be aware that FreeType + will keep the file open as long as the FreeTypeFont object exists. Windows + limits the number of files that can be open in C at once to 512, so if many + fonts are opened simultaneously and that limit is approached, an + ``OSError`` may be thrown, reporting that FreeType "cannot open resource". + A workaround would be to copy the file(s) into memory, and open that instead. + + This function requires the _imagingft service. + + :param font: A filename or file-like object containing a TrueType font. + If the file is not found in this filename, the loader may also + search in other directories, such as: + + * The :file:`fonts/` directory on Windows, + * :file:`/Library/Fonts/`, :file:`/System/Library/Fonts/` + and :file:`~/Library/Fonts/` on macOS. + * :file:`~/.local/share/fonts`, :file:`/usr/local/share/fonts`, + and :file:`/usr/share/fonts` on Linux; or those specified by + the ``XDG_DATA_HOME`` and ``XDG_DATA_DIRS`` environment variables + for user-installed and system-wide fonts, respectively. + + :param size: The requested size, in pixels. + :param index: Which font face to load (default is first available face). + :param encoding: Which font encoding to use (default is Unicode). Possible + encodings include (see the FreeType documentation for more + information): + + * "unic" (Unicode) + * "symb" (Microsoft Symbol) + * "ADOB" (Adobe Standard) + * "ADBE" (Adobe Expert) + * "ADBC" (Adobe Custom) + * "armn" (Apple Roman) + * "sjis" (Shift JIS) + * "gb " (PRC) + * "big5" + * "wans" (Extended Wansung) + * "joha" (Johab) + * "lat1" (Latin-1) + + This specifies the character set to use. It does not alter the + encoding of any text provided in subsequent operations. + :param layout_engine: Which layout engine to use, if available: + :attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`. + If it is available, Raqm layout will be used by default. + Otherwise, basic layout will be used. + + Raqm layout is recommended for all non-English text. If Raqm layout + is not required, basic layout will have better performance. + + You can check support for Raqm layout using + :py:func:`PIL.features.check_feature` with ``feature="raqm"``. + + .. versionadded:: 4.2.0 + :return: A font object. + :exception OSError: If the file could not be read. + :exception ValueError: If the font size is not greater than zero. + """ + + def freetype(font: StrOrBytesPath | BinaryIO) -> FreeTypeFont: + return FreeTypeFont(font, size, index, encoding, layout_engine) + + try: + return freetype(font) + except OSError: + if not is_path(font): + raise + ttf_filename = os.path.basename(font) + + dirs = [] + if sys.platform == "win32": + # check the windows font repository + # NOTE: must use uppercase WINDIR, to work around bugs in + # 1.5.2's os.environ.get() + windir = os.environ.get("WINDIR") + if windir: + dirs.append(os.path.join(windir, "fonts")) + elif sys.platform in ("linux", "linux2"): + data_home = os.environ.get("XDG_DATA_HOME") + if not data_home: + # The freedesktop spec defines the following default directory for + # when XDG_DATA_HOME is unset or empty. This user-level directory + # takes precedence over system-level directories. + data_home = os.path.expanduser("~/.local/share") + xdg_dirs = [data_home] + + data_dirs = os.environ.get("XDG_DATA_DIRS") + if not data_dirs: + # Similarly, defaults are defined for the system-level directories + data_dirs = "/usr/local/share:/usr/share" + xdg_dirs += data_dirs.split(":") + + dirs += [os.path.join(xdg_dir, "fonts") for xdg_dir in xdg_dirs] + elif sys.platform == "darwin": + dirs += [ + "/Library/Fonts", + "/System/Library/Fonts", + os.path.expanduser("~/Library/Fonts"), + ] + + ext = os.path.splitext(ttf_filename)[1] + first_font_with_a_different_extension = None + for directory in dirs: + for walkroot, walkdir, walkfilenames in os.walk(directory): + for walkfilename in walkfilenames: + if ext and walkfilename == ttf_filename: + return freetype(os.path.join(walkroot, walkfilename)) + elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: + fontpath = os.path.join(walkroot, walkfilename) + if os.path.splitext(fontpath)[1] == ".ttf": + return freetype(fontpath) + if not ext and first_font_with_a_different_extension is None: + first_font_with_a_different_extension = fontpath + if first_font_with_a_different_extension: + return freetype(first_font_with_a_different_extension) + raise + + +def load_path(filename: str | bytes) -> ImageFont: + """ + Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a + bitmap font along the Python path. + + :param filename: Name of font file. + :return: A font object. + :exception OSError: If the file could not be read. + """ + if not isinstance(filename, str): + filename = filename.decode("utf-8") + for directory in sys.path: + try: + return load(os.path.join(directory, filename)) + except OSError: + pass + msg = f'cannot find font file "{filename}" in sys.path' + if os.path.exists(filename): + msg += f', did you mean ImageFont.load("{filename}") instead?' + + raise OSError(msg) + + +def load_default_imagefont() -> ImageFont: + f = ImageFont() + f._load_pilfont_data( + # courB08 + BytesIO( + base64.b64decode( + b""" +UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA +BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL +AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA +AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB +ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A +BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB +//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA +AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH +AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA +ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv +AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/ +/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5 +AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA +AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG +AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA +BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA +AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA +2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF +AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA//// ++gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA +////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA +BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv +AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA +AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA +AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA +BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP// +//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA +AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF +AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB +mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn +AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA +AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7 +AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA +Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB +//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA +AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ +AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC +DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ +AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/ ++wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5 +AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/ +///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG +AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA +BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA +Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC +eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG +AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA//// ++gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA +////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA +BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT +AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A +AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA +Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA +Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP// +//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA +AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ +AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA +LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5 +AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA +AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5 +AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA +AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG +AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA +EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK +AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA +pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG +AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// ++QAGAAIAzgAKANUAEw== +""" + ) + ), + Image.open( + BytesIO( + base64.b64decode( + b""" +iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u +Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 +M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g +LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F +IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA +Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791 +NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx +in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9 +SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY +AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt +y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG +ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY +lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H +/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3 +AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47 +c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/ +/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw +pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv +oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR +evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA +AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// +Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR +w7IkEbzhVQAAAABJRU5ErkJggg== +""" + ) + ) + ), + ) + return f + + +def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: + """If FreeType support is available, load a version of Aileron Regular, + https://dotcolon.net/fonts/aileron, with a more limited character set. + + Otherwise, load a "better than nothing" font. + + .. versionadded:: 1.1.4 + + :param size: The font size of Aileron Regular. + + .. versionadded:: 10.1.0 + + :return: A font object. + """ + if isinstance(core, ModuleType) or size is not None: + return truetype( + BytesIO( + base64.b64decode( + b""" +AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA +AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA +MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh +tdHjjERZ8AAAB2AAAAdBsb2NhuOexrgAABVQAAADqbWF4cAC7AEYAAAFYAAAAIG5hbWUr+h5lAAAk +OAAAA6Jwb3N0D3oPTQAAJ9wAAAEKAAEAAAABGhxJDqIhXw889QALA+gAAAAA0Bqf2QAAAADhCh2h/ +2r/LgOxAyAAAAAIAAIAAAAAAAAAAQAAA8r/GgAAA7j/av9qA7EAAQAAAAAAAAAAAAAAAAAAAHQAAQ +AAAHQAQwAFAAAAAAACAAAAAQABAAAAQAAAAAAAAAADAfoBkAAFAAgCigJYAAAASwKKAlgAAAFeADI +BPgAAAAAFAAAAAAAAAAAAAAcAAAAAAAAAAAAAAABVS1dOAEAAIPsCAwL/GgDIA8oA5iAAAJMAAAAA +AhICsgAAACAAAwH0AAAAAAAAAU0AAADYAAAA8gA5AVMAVgJEAEYCRAA1AuQAKQKOAEAAsAArATsAZ +AE7AB4CMABVAkQAUADc/+EBEgAgANwAJQEv//sCRAApAkQAggJEADwCRAAtAkQAIQJEADkCRAArAk +QAMgJEACwCRAAxANwAJQDc/+ECRABnAkQAUAJEAEQB8wAjA1QANgJ/AB0CcwBkArsALwLFAGQCSwB +kAjcAZALGAC8C2gBkAQgAZAIgADcCYQBkAj8AZANiAGQCzgBkAuEALwJWAGQC3QAvAmsAZAJJADQC +ZAAiAqoAXgJuACADuAAaAnEAGQJFABMCTwAuATMAYgEv//sBJwAiAkQAUAH0ADIBLAApAhMAJAJjA +EoCEQAeAmcAHgIlAB4BIgAVAmcAHgJRAEoA7gA+AOn/8wIKAEoA9wBGA1cASgJRAEoCSgAeAmMASg +JnAB4BSgBKAcsAGAE5ABQCUABCAgIAAQMRAAEB4v/6AgEAAQHOABQBLwBAAPoAYAEvACECRABNA0Y +AJAItAHgBKgAcAkQAUAEsAHQAygAgAi0AOQD3ADYA9wAWAaEANgGhABYCbAAlAYMAeAGDADkA6/9q +AhsAFAIKABUB/QAVAAAAAwAAAAMAAAAcAAEAAAAAAKQAAwABAAAAHAAEAIgAAAAeABAAAwAOAH4Aq +QCrALEAtAC3ALsgGSAdICYgOiBEISL7Av//AAAAIACpAKsAsAC0ALcAuyAYIBwgJiA5IEQhIvsB// +//4/+5/7j/tP+y/7D/reBR4E/gR+A14CzfTwVxAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAEGAAABAAAAAAAAAAECAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAMEBQYHCAkKCwwNDg8QERIT +FBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMT +U5PUFFSU1RVVldYWVpbXF1eX2BhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAA +AAAAAAYnFmAAAAAABlAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2htAAAAAAAAAABrbGlqAAAAAHAAbm9 +ycwBnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACYAJgAmAD4AUgCCAMoBCgFO +AVwBcgGIAaYBvAHKAdYB6AH2AgwCIAJKAogCpgLWAw4DIgNkA5wDugPUA+gD/AQQBEYEogS8BPoFJ +gVSBWoFgAWwBcoF1gX6BhQGJAZMBmgGiga0BuIHGgdUB2YHkAeiB8AH3AfyCAoIHAgqCDoITghcCG +oIogjSCPoJKglYCXwJwgnqCgIKKApACl4Klgq8CtwLDAs8C1YLjAuyC9oL7gwMDCYMSAxgDKAMrAz +qDQoNTA1mDYQNoA2uDcAN2g3oDfYODA4iDkoOXA5sDnoOnA7EDvwAAAAFAAAAAAH0ArwAAwAGAAkA +DAAPAAAxESERAxMhExcRASELARETAfT6qv6syKr+jgFUqsiqArz9RAGLAP/+1P8B/v3VAP8BLP4CA +P8AAgA5//IAuQKyAAMACwAANyMDMwIyFhQGIiY0oE4MZk84JCQ4JLQB/v3AJDgkJDgAAgBWAeUBPA +LfAAMABwAAEyMnMxcjJzOmRgpagkYKWgHl+vr6AAAAAAIARgAAAf4CsgAbAB8AAAEHMxUjByM3Iwc +jNyM1MzcjNTM3MwczNzMHMxUrAQczAZgdZXEvOi9bLzovWmYdZXEvOi9bLzovWp9bHlsBn4w429vb +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 +EQQ/BCQvEjQCFQZWFSEWQv40AcxCDiUblhP9uSEAAAAAAAAWAQ4AAQAAAAAAAAATACgAAQAAAAAAA +QAHAEwAAQAAAAAAAgAHAGQAAQAAAAAAAwAaAKIAAQAAAAAABAAHAM0AAQAAAAAABQA8AU8AAQAAAA +AABgAPAawAAQAAAAAACAALAdQAAQAAAAAACQALAfgAAQAAAAAACwAXAjQAAQAAAAAADAAXAnwAAwA +BBAkAAAAmAAAAAwABBAkAAQAOADwAAwABBAkAAgAOAFQAAwABBAkAAwA0AGwAAwABBAkABAAOAL0A +AwABBAkABQB4ANUAAwABBAkABgAeAYwAAwABBAkACAAWAbwAAwABBAkACQAWAeAAAwABBAkACwAuA +gQAAwABBAkADAAuAkwATgBvACAAUgBpAGcAaAB0AHMAIABSAGUAcwBlAHIAdgBlAGQALgAATm8gUm +lnaHRzIFJlc2VydmVkLgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAUgBlAGcAdQBsAGEAcgAAUmV +ndWxhcgAAMQAuADEAMAAyADsAVQBLAFcATgA7AEEAaQBsAGUAcgBvAG4ALQBSAGUAZwB1AGwAYQBy +AAAxLjEwMjtVS1dOO0FpbGVyb24tUmVndWxhcgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAVgBlA +HIAcwBpAG8AbgAgADEALgAxADAAMgA7AFAAUwAgADAAMAAxAC4AMQAwADIAOwBoAG8AdABjAG8Abg +B2ACAAMQAuADAALgA3ADAAOwBtAGEAawBlAG8AdABmAC4AbABpAGIAMgAuADUALgA1ADgAMwAyADk +AAFZlcnNpb24gMS4xMDI7UFMgMDAxLjEwMjtob3Rjb252IDEuMC43MDttYWtlb3RmLmxpYjIuNS41 +ODMyOQAAQQBpAGwAZQByAG8AbgAtAFIAZQBnAHUAbABhAHIAAEFpbGVyb24tUmVndWxhcgAAUwBvA +HIAYQAgAFMAYQBnAGEAbgBvAABTb3JhIFNhZ2FubwAAUwBvAHIAYQAgAFMAYQBnAGEAbgBvAABTb3 +JhIFNhZ2FubwAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBsAG8AbgAuAG4AZQB0AAB +odHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBs +AG8AbgAuAG4AZQB0AABodHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAAAACAAAAAAAA/4MAMgAAAAAAA +AAAAAAAAAAAAAAAAAAAAHQAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATAB +QAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAA +xADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0A +TgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAIsAqQCDAJMAjQDDAKoAtgC3A +LQAtQCrAL4AvwC8AIwAwADBAAAAAAAB//8AAgABAAAADAAAABwAAAACAAIAAwBxAAEAcgBzAAIABA +AAAAIAAAABAAAACgBMAGYAAkRGTFQADmxhdG4AGgAEAAAAAP//AAEAAAAWAANDQVQgAB5NT0wgABZ +ST00gABYAAP//AAEAAAAA//8AAgAAAAEAAmxpZ2EADmxvY2wAFAAAAAEAAQAAAAEAAAACAAYAEAAG +AAAAAgASADQABAAAAAEATAADAAAAAgAQABYAAQAcAAAAAQABAE8AAQABAGcAAQABAE8AAwAAAAIAE +AAWAAEAHAAAAAEAAQAvAAEAAQBnAAEAAQAvAAEAGgABAAgAAgAGAAwAcwACAE8AcgACAEwAAQABAE +kAAAABAAAACgBGAGAAAkRGTFQADmxhdG4AHAAEAAAAAP//AAIAAAABABYAA0NBVCAAFk1PTCAAFlJ +PTSAAFgAA//8AAgAAAAEAAmNwc3AADmtlcm4AFAAAAAEAAAAAAAEAAQACAAYADgABAAAAAQASAAIA +AAACAB4ANgABAAoABQAFAAoAAgABACQAPQAAAAEAEgAEAAAAAQAMAAEAOP/nAAEAAQAkAAIGigAEA +AAFJAXKABoAGQAA//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAD/sv+4/+z/7v/MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAD/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9T/6AAAAAD/8QAA +ABD/vQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7gAAAAAAAAAAAAAAAAAA//MAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAP/5AAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAD/4AAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//L/9AAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAA/+gAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/mAAAAAAAAAAAAAAAAAAD +/4gAA//AAAAAA//YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAP/OAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/zv/qAAAAAP/0AAAACAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZAAD/egAA/1kAAAAA/5D/rgAAAAAAAAAAAA +AAAAAAAAAAAAAAAAD/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAD/8AAA/7b/8P+wAAD/8P/E/98AAAAA/8P/+P/0//oAAAAAAAAAAAAA//gA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/w//C/9MAAP/SAAD/9wAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/yAAA/+kAAAAA//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9wAAAAD//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAP/2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAP/cAAAAAAAAAAAAAAAA/7YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/6AAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAkAFAAEAAAAAQACwAAABcA +BgAAAAAAAAAIAA4AAAAAAAsAEgAAAAAAAAATABkAAwANAAAAAQAJAAAAAAAAAAAAAAAAAAAAGAAAA +AAABwAAAAAAAAAAAAAAFQAFAAAAAAAYABgAAAAUAAAACgAAAAwAAgAPABEAFgAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAEAEQBdAAYAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAABwAAAAAACAAAAAAAAAAAAAcAAAAHAAAAEwAJ +ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA +gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC +YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA +AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ== +""" + ) + ), + 10 if size is None else size, + layout_engine=Layout.BASIC, + ) + return load_default_imagefont() diff --git a/python_modules/PIL/ImageGrab.py b/python_modules/PIL/ImageGrab.py new file mode 100644 index 000000000..1eb450734 --- /dev/null +++ b/python_modules/PIL/ImageGrab.py @@ -0,0 +1,196 @@ +# +# The Python Imaging Library +# $Id$ +# +# screen grabber +# +# History: +# 2001-04-26 fl created +# 2001-09-17 fl use builtin driver, if present +# 2002-11-19 fl added grabclipboard support +# +# Copyright (c) 2001-2002 by Secret Labs AB +# Copyright (c) 2001-2002 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +import os +import shutil +import subprocess +import sys +import tempfile + +from . import Image + +TYPE_CHECKING = False +if TYPE_CHECKING: + from . import ImageWin + + +def grab( + bbox: tuple[int, int, int, int] | None = None, + include_layered_windows: bool = False, + all_screens: bool = False, + xdisplay: str | None = None, + window: int | ImageWin.HWND | None = None, +) -> Image.Image: + im: Image.Image + if xdisplay is None: + if sys.platform == "darwin": + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + args = ["screencapture"] + if bbox: + left, top, right, bottom = bbox + args += ["-R", f"{left},{top},{right-left},{bottom-top}"] + subprocess.call(args + ["-x", filepath]) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + if bbox: + im_resized = im.resize((right - left, bottom - top)) + im.close() + return im_resized + return im + elif sys.platform == "win32": + if window is not None: + all_screens = -1 + offset, size, data = Image.core.grabscreen_win32( + include_layered_windows, + all_screens, + int(window) if window is not None else 0, + ) + im = Image.frombytes( + "RGB", + size, + data, + # RGB, 32-bit line padding, origin lower left corner + "raw", + "BGR", + (size[0] * 3 + 3) & -4, + -1, + ) + if bbox: + x0, y0 = offset + left, top, right, bottom = bbox + im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) + return im + # Cast to Optional[str] needed for Windows and macOS. + display_name: str | None = xdisplay + try: + if not Image.core.HAVE_XCB: + msg = "Pillow was built without XCB support" + raise OSError(msg) + size, data = Image.core.grabscreen_x11(display_name) + except OSError: + if display_name is None and sys.platform not in ("darwin", "win32"): + if shutil.which("gnome-screenshot"): + args = ["gnome-screenshot", "-f"] + elif shutil.which("grim"): + args = ["grim"] + elif shutil.which("spectacle"): + args = ["spectacle", "-n", "-b", "-f", "-o"] + else: + raise + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + subprocess.call(args + [filepath]) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + if bbox: + im_cropped = im.crop(bbox) + im.close() + return im_cropped + return im + else: + raise + else: + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) + return im + + +def grabclipboard() -> Image.Image | list[str] | None: + if sys.platform == "darwin": + p = subprocess.run( + ["osascript", "-e", "get the clipboard as «class PNGf»"], + capture_output=True, + ) + if p.returncode != 0: + return None + + import binascii + + data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3])) + return Image.open(data) + elif sys.platform == "win32": + fmt, data = Image.core.grabclipboard_win32() + if fmt == "file": # CF_HDROP + import struct + + o = struct.unpack_from("I", data)[0] + if data[16] == 0: + files = data[o:].decode("mbcs").split("\0") + else: + files = data[o:].decode("utf-16le").split("\0") + return files[: files.index("")] + if isinstance(data, bytes): + data = io.BytesIO(data) + if fmt == "png": + from . import PngImagePlugin + + return PngImagePlugin.PngImageFile(data) + elif fmt == "DIB": + from . import BmpImagePlugin + + return BmpImagePlugin.DibImageFile(data) + return None + else: + if os.getenv("WAYLAND_DISPLAY"): + session_type = "wayland" + elif os.getenv("DISPLAY"): + session_type = "x11" + else: # Session type check failed + session_type = None + + if shutil.which("wl-paste") and session_type in ("wayland", None): + args = ["wl-paste", "-t", "image"] + elif shutil.which("xclip") and session_type in ("x11", None): + args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] + else: + msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" + raise NotImplementedError(msg) + + p = subprocess.run(args, capture_output=True) + if p.returncode != 0: + err = p.stderr + for silent_error in [ + # wl-paste, when the clipboard is empty + b"Nothing is copied", + # Ubuntu/Debian wl-paste, when the clipboard is empty + b"No selection", + # Ubuntu/Debian wl-paste, when an image isn't available + b"No suitable type of content copied", + # wl-paste or Ubuntu/Debian xclip, when an image isn't available + b" not available", + # xclip, when an image isn't available + b"cannot convert ", + # xclip, when the clipboard isn't initialized + b"xclip: Error: There is no owner for the ", + ]: + if silent_error in err: + return None + msg = f"{args[0]} error" + if err: + msg += f": {err.strip().decode()}" + raise ChildProcessError(msg) + + data = io.BytesIO(p.stdout) + im = Image.open(data) + im.load() + return im diff --git a/python_modules/PIL/ImageMath.py b/python_modules/PIL/ImageMath.py new file mode 100644 index 000000000..c33809ced --- /dev/null +++ b/python_modules/PIL/ImageMath.py @@ -0,0 +1,368 @@ +# +# The Python Imaging Library +# $Id$ +# +# a simple math add-on for the Python Imaging Library +# +# History: +# 1999-02-15 fl Original PIL Plus release +# 2005-05-05 fl Simplified and cleaned up for PIL 1.1.6 +# 2005-09-12 fl Fixed int() and float() for Python 2.4.1 +# +# Copyright (c) 1999-2005 by Secret Labs AB +# Copyright (c) 2005 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import builtins +from types import CodeType +from typing import Any, Callable + +from . import Image, _imagingmath +from ._deprecate import deprecate + + +class _Operand: + """Wraps an image operand, providing standard operators""" + + def __init__(self, im: Image.Image): + self.im = im + + def __fixup(self, im1: _Operand | float) -> Image.Image: + # convert image to suitable mode + if isinstance(im1, _Operand): + # argument was an image. + if im1.im.mode in ("1", "L"): + return im1.im.convert("I") + elif im1.im.mode in ("I", "F"): + return im1.im + else: + msg = f"unsupported mode: {im1.im.mode}" + raise ValueError(msg) + else: + # argument was a constant + if isinstance(im1, (int, float)) and self.im.mode in ("1", "L", "I"): + return Image.new("I", self.im.size, im1) + else: + return Image.new("F", self.im.size, im1) + + def apply( + self, + op: str, + im1: _Operand | float, + im2: _Operand | float | None = None, + mode: str | None = None, + ) -> _Operand: + im_1 = self.__fixup(im1) + if im2 is None: + # unary operation + out = Image.new(mode or im_1.mode, im_1.size, None) + try: + op = getattr(_imagingmath, f"{op}_{im_1.mode}") + except AttributeError as e: + msg = f"bad operand type for '{op}'" + raise TypeError(msg) from e + _imagingmath.unop(op, out.getim(), im_1.getim()) + else: + # binary operation + im_2 = self.__fixup(im2) + if im_1.mode != im_2.mode: + # convert both arguments to floating point + if im_1.mode != "F": + im_1 = im_1.convert("F") + if im_2.mode != "F": + im_2 = im_2.convert("F") + if im_1.size != im_2.size: + # crop both arguments to a common size + size = ( + min(im_1.size[0], im_2.size[0]), + min(im_1.size[1], im_2.size[1]), + ) + if im_1.size != size: + im_1 = im_1.crop((0, 0) + size) + if im_2.size != size: + im_2 = im_2.crop((0, 0) + size) + out = Image.new(mode or im_1.mode, im_1.size, None) + try: + op = getattr(_imagingmath, f"{op}_{im_1.mode}") + except AttributeError as e: + msg = f"bad operand type for '{op}'" + raise TypeError(msg) from e + _imagingmath.binop(op, out.getim(), im_1.getim(), im_2.getim()) + return _Operand(out) + + # unary operators + def __bool__(self) -> bool: + # an image is "true" if it contains at least one non-zero pixel + return self.im.getbbox() is not None + + def __abs__(self) -> _Operand: + return self.apply("abs", self) + + def __pos__(self) -> _Operand: + return self + + def __neg__(self) -> _Operand: + return self.apply("neg", self) + + # binary operators + def __add__(self, other: _Operand | float) -> _Operand: + return self.apply("add", self, other) + + def __radd__(self, other: _Operand | float) -> _Operand: + return self.apply("add", other, self) + + def __sub__(self, other: _Operand | float) -> _Operand: + return self.apply("sub", self, other) + + def __rsub__(self, other: _Operand | float) -> _Operand: + return self.apply("sub", other, self) + + def __mul__(self, other: _Operand | float) -> _Operand: + return self.apply("mul", self, other) + + def __rmul__(self, other: _Operand | float) -> _Operand: + return self.apply("mul", other, self) + + def __truediv__(self, other: _Operand | float) -> _Operand: + return self.apply("div", self, other) + + def __rtruediv__(self, other: _Operand | float) -> _Operand: + return self.apply("div", other, self) + + def __mod__(self, other: _Operand | float) -> _Operand: + return self.apply("mod", self, other) + + def __rmod__(self, other: _Operand | float) -> _Operand: + return self.apply("mod", other, self) + + def __pow__(self, other: _Operand | float) -> _Operand: + return self.apply("pow", self, other) + + def __rpow__(self, other: _Operand | float) -> _Operand: + return self.apply("pow", other, self) + + # bitwise + def __invert__(self) -> _Operand: + return self.apply("invert", self) + + def __and__(self, other: _Operand | float) -> _Operand: + return self.apply("and", self, other) + + def __rand__(self, other: _Operand | float) -> _Operand: + return self.apply("and", other, self) + + def __or__(self, other: _Operand | float) -> _Operand: + return self.apply("or", self, other) + + def __ror__(self, other: _Operand | float) -> _Operand: + return self.apply("or", other, self) + + def __xor__(self, other: _Operand | float) -> _Operand: + return self.apply("xor", self, other) + + def __rxor__(self, other: _Operand | float) -> _Operand: + return self.apply("xor", other, self) + + def __lshift__(self, other: _Operand | float) -> _Operand: + return self.apply("lshift", self, other) + + def __rshift__(self, other: _Operand | float) -> _Operand: + return self.apply("rshift", self, other) + + # logical + def __eq__(self, other: _Operand | float) -> _Operand: # type: ignore[override] + return self.apply("eq", self, other) + + def __ne__(self, other: _Operand | float) -> _Operand: # type: ignore[override] + return self.apply("ne", self, other) + + def __lt__(self, other: _Operand | float) -> _Operand: + return self.apply("lt", self, other) + + def __le__(self, other: _Operand | float) -> _Operand: + return self.apply("le", self, other) + + def __gt__(self, other: _Operand | float) -> _Operand: + return self.apply("gt", self, other) + + def __ge__(self, other: _Operand | float) -> _Operand: + return self.apply("ge", self, other) + + +# conversions +def imagemath_int(self: _Operand) -> _Operand: + return _Operand(self.im.convert("I")) + + +def imagemath_float(self: _Operand) -> _Operand: + return _Operand(self.im.convert("F")) + + +# logical +def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand: + return self.apply("eq", self, other, mode="I") + + +def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand: + return self.apply("ne", self, other, mode="I") + + +def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand: + return self.apply("min", self, other) + + +def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand: + return self.apply("max", self, other) + + +def imagemath_convert(self: _Operand, mode: str) -> _Operand: + return _Operand(self.im.convert(mode)) + + +ops = { + "int": imagemath_int, + "float": imagemath_float, + "equal": imagemath_equal, + "notequal": imagemath_notequal, + "min": imagemath_min, + "max": imagemath_max, + "convert": imagemath_convert, +} + + +def lambda_eval( + expression: Callable[[dict[str, Any]], Any], + options: dict[str, Any] = {}, + **kw: Any, +) -> Any: + """ + Returns the result of an image function. + + :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band + images, use the :py:meth:`~PIL.Image.Image.split` method or + :py:func:`~PIL.Image.merge` function. + + :param expression: A function that receives a dictionary. + :param options: Values to add to the function's dictionary. Deprecated. + You can instead use one or more keyword arguments. + :param **kw: Values to add to the function's dictionary. + :return: The expression result. This is usually an image object, but can + also be an integer, a floating point value, or a pixel tuple, + depending on the expression. + """ + + if options: + deprecate( + "ImageMath.lambda_eval options", + 12, + "ImageMath.lambda_eval keyword arguments", + ) + + args: dict[str, Any] = ops.copy() + args.update(options) + args.update(kw) + for k, v in args.items(): + if isinstance(v, Image.Image): + args[k] = _Operand(v) + + out = expression(args) + try: + return out.im + except AttributeError: + return out + + +def unsafe_eval( + expression: str, + options: dict[str, Any] = {}, + **kw: Any, +) -> Any: + """ + Evaluates an image expression. This uses Python's ``eval()`` function to process + the expression string, and carries the security risks of doing so. It is not + recommended to process expressions without considering this. + :py:meth:`~lambda_eval` is a more secure alternative. + + :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band + images, use the :py:meth:`~PIL.Image.Image.split` method or + :py:func:`~PIL.Image.merge` function. + + :param expression: A string containing a Python-style expression. + :param options: Values to add to the evaluation context. Deprecated. + You can instead use one or more keyword arguments. + :param **kw: Values to add to the evaluation context. + :return: The evaluated expression. This is usually an image object, but can + also be an integer, a floating point value, or a pixel tuple, + depending on the expression. + """ + + if options: + deprecate( + "ImageMath.unsafe_eval options", + 12, + "ImageMath.unsafe_eval keyword arguments", + ) + + # build execution namespace + args: dict[str, Any] = ops.copy() + for k in [*options, *kw]: + if "__" in k or hasattr(builtins, k): + msg = f"'{k}' not allowed" + raise ValueError(msg) + + args.update(options) + args.update(kw) + for k, v in args.items(): + if isinstance(v, Image.Image): + args[k] = _Operand(v) + + compiled_code = compile(expression, "", "eval") + + def scan(code: CodeType) -> None: + for const in code.co_consts: + if type(const) is type(compiled_code): + scan(const) + + for name in code.co_names: + if name not in args and name != "abs": + msg = f"'{name}' not allowed" + raise ValueError(msg) + + scan(compiled_code) + out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) + try: + return out.im + except AttributeError: + return out + + +def eval( + expression: str, + _dict: dict[str, Any] = {}, + **kw: Any, +) -> Any: + """ + Evaluates an image expression. + + Deprecated. Use lambda_eval() or unsafe_eval() instead. + + :param expression: A string containing a Python-style expression. + :param _dict: Values to add to the evaluation context. You + can either use a dictionary, or one or more keyword + arguments. + :return: The evaluated expression. This is usually an image object, but can + also be an integer, a floating point value, or a pixel tuple, + depending on the expression. + + .. deprecated:: 10.3.0 + """ + + deprecate( + "ImageMath.eval", + 12, + "ImageMath.lambda_eval or ImageMath.unsafe_eval", + ) + return unsafe_eval(expression, _dict, **kw) diff --git a/python_modules/PIL/ImageMode.py b/python_modules/PIL/ImageMode.py new file mode 100644 index 000000000..92a08d2cb --- /dev/null +++ b/python_modules/PIL/ImageMode.py @@ -0,0 +1,92 @@ +# +# The Python Imaging Library. +# $Id$ +# +# standard mode descriptors +# +# History: +# 2006-03-20 fl Added +# +# Copyright (c) 2006 by Secret Labs AB. +# Copyright (c) 2006 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import sys +from functools import lru_cache +from typing import NamedTuple + +from ._deprecate import deprecate + + +class ModeDescriptor(NamedTuple): + """Wrapper for mode strings.""" + + mode: str + bands: tuple[str, ...] + basemode: str + basetype: str + typestr: str + + def __str__(self) -> str: + return self.mode + + +@lru_cache +def getmode(mode: str) -> ModeDescriptor: + """Gets a mode descriptor for the given mode.""" + endian = "<" if sys.byteorder == "little" else ">" + + modes = { + # core modes + # Bits need to be extended to bytes + "1": ("L", "L", ("1",), "|b1"), + "L": ("L", "L", ("L",), "|u1"), + "I": ("L", "I", ("I",), f"{endian}i4"), + "F": ("L", "F", ("F",), f"{endian}f4"), + "P": ("P", "L", ("P",), "|u1"), + "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), + "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), + "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), + "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), + "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), + # UNDONE - unsigned |u1i1i1 + "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), + "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), + # extra experimental modes + "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), + "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), + "LA": ("L", "L", ("L", "A"), "|u1"), + "La": ("L", "L", ("L", "a"), "|u1"), + "PA": ("RGB", "L", ("P", "A"), "|u1"), + } + if mode in modes: + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + base_mode, base_type, bands, type_str = modes[mode] + return ModeDescriptor(mode, bands, base_mode, base_type, type_str) + + mapping_modes = { + # I;16 == I;16L, and I;32 == I;32L + "I;16": "u2", + "I;16BS": ">i2", + "I;16N": f"{endian}u2", + "I;16NS": f"{endian}i2", + "I;32": "u4", + "I;32L": "i4", + "I;32LS": " +from __future__ import annotations + +import re + +from . import Image, _imagingmorph + +LUT_SIZE = 1 << 9 + +# fmt: off +ROTATION_MATRIX = [ + 6, 3, 0, + 7, 4, 1, + 8, 5, 2, +] +MIRROR_MATRIX = [ + 2, 1, 0, + 5, 4, 3, + 8, 7, 6, +] +# fmt: on + + +class LutBuilder: + """A class for building a MorphLut from a descriptive language + + The input patterns is a list of a strings sequences like these:: + + 4:(... + .1. + 111)->1 + + (whitespaces including linebreaks are ignored). The option 4 + describes a series of symmetry operations (in this case a + 4-rotation), the pattern is described by: + + - . or X - Ignore + - 1 - Pixel is on + - 0 - Pixel is off + + The result of the operation is described after "->" string. + + The default is to return the current pixel value, which is + returned if no other match is found. + + Operations: + + - 4 - 4 way rotation + - N - Negate + - 1 - Dummy op for no other operation (an op must always be given) + - M - Mirroring + + Example:: + + lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) + lut = lb.build_lut() + + """ + + def __init__( + self, patterns: list[str] | None = None, op_name: str | None = None + ) -> None: + if patterns is not None: + self.patterns = patterns + else: + self.patterns = [] + self.lut: bytearray | None = None + if op_name is not None: + known_patterns = { + "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], + "dilation4": ["4:(... .0. .1.)->1"], + "dilation8": ["4:(... .0. .1.)->1", "4:(... .0. ..1)->1"], + "erosion4": ["4:(... .1. .0.)->0"], + "erosion8": ["4:(... .1. .0.)->0", "4:(... .1. ..0)->0"], + "edge": [ + "1:(... ... ...)->0", + "4:(.0. .1. ...)->1", + "4:(01. .1. ...)->1", + ], + } + if op_name not in known_patterns: + msg = f"Unknown pattern {op_name}!" + raise Exception(msg) + + self.patterns = known_patterns[op_name] + + def add_patterns(self, patterns: list[str]) -> None: + self.patterns += patterns + + def build_default_lut(self) -> None: + symbols = [0, 1] + m = 1 << 4 # pos of current pixel + self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) + + def get_lut(self) -> bytearray | None: + return self.lut + + def _string_permute(self, pattern: str, permutation: list[int]) -> str: + """string_permute takes a pattern and a permutation and returns the + string permuted according to the permutation list. + """ + assert len(permutation) == 9 + return "".join(pattern[p] for p in permutation) + + def _pattern_permute( + self, basic_pattern: str, options: str, basic_result: int + ) -> list[tuple[str, int]]: + """pattern_permute takes a basic pattern and its result and clones + the pattern according to the modifications described in the $options + parameter. It returns a list of all cloned patterns.""" + patterns = [(basic_pattern, basic_result)] + + # rotations + if "4" in options: + res = patterns[-1][1] + for i in range(4): + patterns.append( + (self._string_permute(patterns[-1][0], ROTATION_MATRIX), res) + ) + # mirror + if "M" in options: + n = len(patterns) + for pattern, res in patterns[:n]: + patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res)) + + # negate + if "N" in options: + n = len(patterns) + for pattern, res in patterns[:n]: + # Swap 0 and 1 + pattern = pattern.replace("0", "Z").replace("1", "0").replace("Z", "1") + res = 1 - int(res) + patterns.append((pattern, res)) + + return patterns + + def build_lut(self) -> bytearray: + """Compile all patterns into a morphology lut. + + TBD :Build based on (file) morphlut:modify_lut + """ + self.build_default_lut() + assert self.lut is not None + patterns = [] + + # Parse and create symmetries of the patterns strings + for p in self.patterns: + m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) + if not m: + msg = 'Syntax error in pattern "' + p + '"' + raise Exception(msg) + options = m.group(1) + pattern = m.group(2) + result = int(m.group(3)) + + # Get rid of spaces + pattern = pattern.replace(" ", "").replace("\n", "") + + patterns += self._pattern_permute(pattern, options, result) + + # compile the patterns into regular expressions for speed + compiled_patterns = [] + for pattern in patterns: + p = pattern[0].replace(".", "X").replace("X", "[01]") + compiled_patterns.append((re.compile(p), pattern[1])) + + # Step through table and find patterns that match. + # Note that all the patterns are searched. The last one + # caught overrides + for i in range(LUT_SIZE): + # Build the bit pattern + bitpattern = bin(i)[2:] + bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] + + for pattern, r in compiled_patterns: + if pattern.match(bitpattern): + self.lut[i] = [0, 1][r] + + return self.lut + + +class MorphOp: + """A class for binary morphological operators""" + + def __init__( + self, + lut: bytearray | None = None, + op_name: str | None = None, + patterns: list[str] | None = None, + ) -> None: + """Create a binary morphological operator""" + self.lut = lut + if op_name is not None: + self.lut = LutBuilder(op_name=op_name).build_lut() + elif patterns is not None: + self.lut = LutBuilder(patterns=patterns).build_lut() + + def apply(self, image: Image.Image) -> tuple[int, Image.Image]: + """Run a single morphological operation on an image + + Returns a tuple of the number of changed pixels and the + morphed image""" + if self.lut is None: + msg = "No operator loaded" + raise Exception(msg) + + if image.mode != "L": + msg = "Image mode must be L" + raise ValueError(msg) + outimage = Image.new(image.mode, image.size, None) + count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim()) + return count, outimage + + def match(self, image: Image.Image) -> list[tuple[int, int]]: + """Get a list of coordinates matching the morphological operation on + an image. + + Returns a list of tuples of (x,y) coordinates + of all matching pixels. See :ref:`coordinate-system`.""" + if self.lut is None: + msg = "No operator loaded" + raise Exception(msg) + + if image.mode != "L": + msg = "Image mode must be L" + raise ValueError(msg) + return _imagingmorph.match(bytes(self.lut), image.getim()) + + def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: + """Get a list of all turned on pixels in a binary image + + Returns a list of tuples of (x,y) coordinates + of all matching pixels. See :ref:`coordinate-system`.""" + + if image.mode != "L": + msg = "Image mode must be L" + raise ValueError(msg) + return _imagingmorph.get_on_pixels(image.getim()) + + def load_lut(self, filename: str) -> None: + """Load an operator from an mrl file""" + with open(filename, "rb") as f: + self.lut = bytearray(f.read()) + + if len(self.lut) != LUT_SIZE: + self.lut = None + msg = "Wrong size operator file!" + raise Exception(msg) + + def save_lut(self, filename: str) -> None: + """Save an operator to an mrl file""" + if self.lut is None: + msg = "No operator loaded" + raise Exception(msg) + with open(filename, "wb") as f: + f.write(self.lut) + + def set_lut(self, lut: bytearray | None) -> None: + """Set the lut from an external source""" + self.lut = lut diff --git a/python_modules/PIL/ImageOps.py b/python_modules/PIL/ImageOps.py new file mode 100644 index 000000000..da28854b5 --- /dev/null +++ b/python_modules/PIL/ImageOps.py @@ -0,0 +1,745 @@ +# +# The Python Imaging Library. +# $Id$ +# +# standard image operations +# +# History: +# 2001-10-20 fl Created +# 2001-10-23 fl Added autocontrast operator +# 2001-12-18 fl Added Kevin's fit operator +# 2004-03-14 fl Fixed potential division by zero in equalize +# 2005-05-05 fl Fixed equalize for low number of values +# +# Copyright (c) 2001-2004 by Secret Labs AB +# Copyright (c) 2001-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import functools +import operator +import re +from collections.abc import Sequence +from typing import Literal, Protocol, cast, overload + +from . import ExifTags, Image, ImagePalette + +# +# helpers + + +def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: + if isinstance(border, tuple): + if len(border) == 2: + left, top = right, bottom = border + elif len(border) == 4: + left, top, right, bottom = border + else: + left = top = right = bottom = border + return left, top, right, bottom + + +def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: + if isinstance(color, str): + from . import ImageColor + + color = ImageColor.getcolor(color, mode) + return color + + +def _lut(image: Image.Image, lut: list[int]) -> Image.Image: + if image.mode == "P": + # FIXME: apply to lookup table, not image data + msg = "mode P support coming soon" + raise NotImplementedError(msg) + elif image.mode in ("L", "RGB"): + if image.mode == "RGB" and len(lut) == 256: + lut = lut + lut + lut + return image.point(lut) + else: + msg = f"not supported for mode {image.mode}" + raise OSError(msg) + + +# +# actions + + +def autocontrast( + image: Image.Image, + cutoff: float | tuple[float, float] = 0, + ignore: int | Sequence[int] | None = None, + mask: Image.Image | None = None, + preserve_tone: bool = False, +) -> Image.Image: + """ + Maximize (normalize) image contrast. This function calculates a + histogram of the input image (or mask region), removes ``cutoff`` percent of the + lightest and darkest pixels from the histogram, and remaps the image + so that the darkest pixel becomes black (0), and the lightest + becomes white (255). + + :param image: The image to process. + :param cutoff: The percent to cut off from the histogram on the low and + high ends. Either a tuple of (low, high), or a single + number for both. + :param ignore: The background pixel value (use None for no background). + :param mask: Histogram used in contrast operation is computed using pixels + within the mask. If no mask is given the entire image is used + for histogram computation. + :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast. + + .. versionadded:: 8.2.0 + + :return: An image. + """ + if preserve_tone: + histogram = image.convert("L").histogram(mask) + else: + histogram = image.histogram(mask) + + lut = [] + for layer in range(0, len(histogram), 256): + h = histogram[layer : layer + 256] + if ignore is not None: + # get rid of outliers + if isinstance(ignore, int): + h[ignore] = 0 + else: + for ix in ignore: + h[ix] = 0 + if cutoff: + # cut off pixels from both ends of the histogram + if not isinstance(cutoff, tuple): + cutoff = (cutoff, cutoff) + # get number of pixels + n = 0 + for ix in range(256): + n = n + h[ix] + # remove cutoff% pixels from the low end + cut = int(n * cutoff[0] // 100) + for lo in range(256): + if cut > h[lo]: + cut = cut - h[lo] + h[lo] = 0 + else: + h[lo] -= cut + cut = 0 + if cut <= 0: + break + # remove cutoff% samples from the high end + cut = int(n * cutoff[1] // 100) + for hi in range(255, -1, -1): + if cut > h[hi]: + cut = cut - h[hi] + h[hi] = 0 + else: + h[hi] -= cut + cut = 0 + if cut <= 0: + break + # find lowest/highest samples after preprocessing + for lo in range(256): + if h[lo]: + break + for hi in range(255, -1, -1): + if h[hi]: + break + if hi <= lo: + # don't bother + lut.extend(list(range(256))) + else: + scale = 255.0 / (hi - lo) + offset = -lo * scale + for ix in range(256): + ix = int(ix * scale + offset) + if ix < 0: + ix = 0 + elif ix > 255: + ix = 255 + lut.append(ix) + return _lut(image, lut) + + +def colorize( + image: Image.Image, + black: str | tuple[int, ...], + white: str | tuple[int, ...], + mid: str | int | tuple[int, ...] | None = None, + blackpoint: int = 0, + whitepoint: int = 255, + midpoint: int = 127, +) -> Image.Image: + """ + Colorize grayscale image. + This function calculates a color wedge which maps all black pixels in + the source image to the first color and all white pixels to the + second color. If ``mid`` is specified, it uses three-color mapping. + The ``black`` and ``white`` arguments should be RGB tuples or color names; + optionally you can use three-color mapping by also specifying ``mid``. + Mapping positions for any of the colors can be specified + (e.g. ``blackpoint``), where these parameters are the integer + value corresponding to where the corresponding color should be mapped. + These parameters must have logical order, such that + ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified). + + :param image: The image to colorize. + :param black: The color to use for black input pixels. + :param white: The color to use for white input pixels. + :param mid: The color to use for midtone input pixels. + :param blackpoint: an int value [0, 255] for the black mapping. + :param whitepoint: an int value [0, 255] for the white mapping. + :param midpoint: an int value [0, 255] for the midtone mapping. + :return: An image. + """ + + # Initial asserts + assert image.mode == "L" + if mid is None: + assert 0 <= blackpoint <= whitepoint <= 255 + else: + assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 + + # Define colors from arguments + rgb_black = cast(Sequence[int], _color(black, "RGB")) + rgb_white = cast(Sequence[int], _color(white, "RGB")) + rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None + + # Empty lists for the mapping + red = [] + green = [] + blue = [] + + # Create the low-end values + for i in range(blackpoint): + red.append(rgb_black[0]) + green.append(rgb_black[1]) + blue.append(rgb_black[2]) + + # Create the mapping (2-color) + if rgb_mid is None: + range_map = range(whitepoint - blackpoint) + + for i in range_map: + red.append( + rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) + ) + green.append( + rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) + ) + blue.append( + rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) + ) + + # Create the mapping (3-color) + else: + range_map1 = range(midpoint - blackpoint) + range_map2 = range(whitepoint - midpoint) + + for i in range_map1: + red.append( + rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) + ) + green.append( + rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) + ) + blue.append( + rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) + ) + for i in range_map2: + red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) + green.append( + rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) + ) + blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) + + # Create the high-end values + for i in range(256 - whitepoint): + red.append(rgb_white[0]) + green.append(rgb_white[1]) + blue.append(rgb_white[2]) + + # Return converted image + image = image.convert("RGB") + return _lut(image, red + green + blue) + + +def contain( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: + """ + Returns a resized version of the image, set to the maximum width and height + within the requested size, while maintaining the original aspect ratio. + + :param image: The image to resize. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. + :return: An image. + """ + + im_ratio = image.width / image.height + dest_ratio = size[0] / size[1] + + if im_ratio != dest_ratio: + if im_ratio > dest_ratio: + new_height = round(image.height / image.width * size[0]) + if new_height != size[1]: + size = (size[0], new_height) + else: + new_width = round(image.width / image.height * size[1]) + if new_width != size[0]: + size = (new_width, size[1]) + return image.resize(size, resample=method) + + +def cover( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: + """ + Returns a resized version of the image, so that the requested size is + covered, while maintaining the original aspect ratio. + + :param image: The image to resize. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. + :return: An image. + """ + + im_ratio = image.width / image.height + dest_ratio = size[0] / size[1] + + if im_ratio != dest_ratio: + if im_ratio < dest_ratio: + new_height = round(image.height / image.width * size[0]) + if new_height != size[1]: + size = (size[0], new_height) + else: + new_width = round(image.width / image.height * size[1]) + if new_width != size[0]: + size = (new_width, size[1]) + return image.resize(size, resample=method) + + +def pad( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + color: str | int | tuple[int, ...] | None = None, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: + """ + Returns a resized and padded version of the image, expanded to fill the + requested aspect ratio and size. + + :param image: The image to resize and crop. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. + :param color: The background color of the padded image. + :param centering: Control the position of the original image within the + padded version. + + (0.5, 0.5) will keep the image centered + (0, 0) will keep the image aligned to the top left + (1, 1) will keep the image aligned to the bottom + right + :return: An image. + """ + + resized = contain(image, size, method) + if resized.size == size: + out = resized + else: + out = Image.new(image.mode, size, color) + if resized.palette: + palette = resized.getpalette() + if palette is not None: + out.putpalette(palette) + if resized.width != size[0]: + x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) + out.paste(resized, (x, 0)) + else: + y = round((size[1] - resized.height) * max(0, min(centering[1], 1))) + out.paste(resized, (0, y)) + return out + + +def crop(image: Image.Image, border: int = 0) -> Image.Image: + """ + Remove border from image. The same amount of pixels are removed + from all four sides. This function works on all image modes. + + .. seealso:: :py:meth:`~PIL.Image.Image.crop` + + :param image: The image to crop. + :param border: The number of pixels to remove. + :return: An image. + """ + left, top, right, bottom = _border(border) + return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) + + +def scale( + image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC +) -> Image.Image: + """ + Returns a rescaled image by a specific factor given in parameter. + A factor greater than 1 expands the image, between 0 and 1 contracts the + image. + + :param image: The image to rescale. + :param factor: The expansion factor, as a float. + :param resample: Resampling method to use. Default is + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. + :returns: An :py:class:`~PIL.Image.Image` object. + """ + if factor == 1: + return image.copy() + elif factor <= 0: + msg = "the factor must be greater than 0" + raise ValueError(msg) + else: + size = (round(factor * image.width), round(factor * image.height)) + return image.resize(size, resample) + + +class SupportsGetMesh(Protocol): + """ + An object that supports the ``getmesh`` method, taking an image as an + argument, and returning a list of tuples. Each tuple contains two tuples, + the source box as a tuple of 4 integers, and a tuple of 8 integers for the + final quadrilateral, in order of top left, bottom left, bottom right, top + right. + """ + + def getmesh( + self, image: Image.Image + ) -> list[ + tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] + ]: ... + + +def deform( + image: Image.Image, + deformer: SupportsGetMesh, + resample: int = Image.Resampling.BILINEAR, +) -> Image.Image: + """ + Deform the image. + + :param image: The image to deform. + :param deformer: A deformer object. Any object that implements a + ``getmesh`` method can be used. + :param resample: An optional resampling filter. Same values possible as + in the PIL.Image.transform function. + :return: An image. + """ + return image.transform( + image.size, Image.Transform.MESH, deformer.getmesh(image), resample + ) + + +def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: + """ + Equalize the image histogram. This function applies a non-linear + mapping to the input image, in order to create a uniform + distribution of grayscale values in the output image. + + :param image: The image to equalize. + :param mask: An optional mask. If given, only the pixels selected by + the mask are included in the analysis. + :return: An image. + """ + if image.mode == "P": + image = image.convert("RGB") + h = image.histogram(mask) + lut = [] + for b in range(0, len(h), 256): + histo = [_f for _f in h[b : b + 256] if _f] + if len(histo) <= 1: + lut.extend(list(range(256))) + else: + step = (functools.reduce(operator.add, histo) - histo[-1]) // 255 + if not step: + lut.extend(list(range(256))) + else: + n = step // 2 + for i in range(256): + lut.append(n // step) + n = n + h[i + b] + return _lut(image, lut) + + +def expand( + image: Image.Image, + border: int | tuple[int, ...] = 0, + fill: str | int | tuple[int, ...] = 0, +) -> Image.Image: + """ + Add border to the image + + :param image: The image to expand. + :param border: Border width, in pixels. + :param fill: Pixel fill value (a color value). Default is 0 (black). + :return: An image. + """ + left, top, right, bottom = _border(border) + width = left + image.size[0] + right + height = top + image.size[1] + bottom + color = _color(fill, image.mode) + if image.palette: + palette = ImagePalette.ImagePalette(palette=image.getpalette()) + if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4): + color = palette.getcolor(color) + else: + palette = None + out = Image.new(image.mode, (width, height), color) + if palette: + out.putpalette(palette.palette) + out.paste(image, (left, top)) + return out + + +def fit( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + bleed: float = 0.0, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: + """ + Returns a resized and cropped version of the image, cropped to the + requested aspect ratio and size. + + This function was contributed by Kevin Cazabon. + + :param image: The image to resize and crop. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. + :param bleed: Remove a border around the outside of the image from all + four edges. The value is a decimal percentage (use 0.01 for + one percent). The default value is 0 (no border). + Cannot be greater than or equal to 0.5. + :param centering: Control the cropping position. Use (0.5, 0.5) for + center cropping (e.g. if cropping the width, take 50% off + of the left side, and therefore 50% off the right side). + (0.0, 0.0) will crop from the top left corner (i.e. if + cropping the width, take all of the crop off of the right + side, and if cropping the height, take all of it off the + bottom). (1.0, 0.0) will crop from the bottom left + corner, etc. (i.e. if cropping the width, take all of the + crop off the left side, and if cropping the height take + none from the top, and therefore all off the bottom). + :return: An image. + """ + + # by Kevin Cazabon, Feb 17/2000 + # kevin@cazabon.com + # https://www.cazabon.com + + centering_x, centering_y = centering + + if not 0.0 <= centering_x <= 1.0: + centering_x = 0.5 + if not 0.0 <= centering_y <= 1.0: + centering_y = 0.5 + + if not 0.0 <= bleed < 0.5: + bleed = 0.0 + + # calculate the area to use for resizing and cropping, subtracting + # the 'bleed' around the edges + + # number of pixels to trim off on Top and Bottom, Left and Right + bleed_pixels = (bleed * image.size[0], bleed * image.size[1]) + + live_size = ( + image.size[0] - bleed_pixels[0] * 2, + image.size[1] - bleed_pixels[1] * 2, + ) + + # calculate the aspect ratio of the live_size + live_size_ratio = live_size[0] / live_size[1] + + # calculate the aspect ratio of the output image + output_ratio = size[0] / size[1] + + # figure out if the sides or top/bottom will be cropped off + if live_size_ratio == output_ratio: + # live_size is already the needed ratio + crop_width = live_size[0] + crop_height = live_size[1] + elif live_size_ratio >= output_ratio: + # live_size is wider than what's needed, crop the sides + crop_width = output_ratio * live_size[1] + crop_height = live_size[1] + else: + # live_size is taller than what's needed, crop the top and bottom + crop_width = live_size[0] + crop_height = live_size[0] / output_ratio + + # make the crop + crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x + crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y + + crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) + + # resize the image and return it + return image.resize(size, method, box=crop) + + +def flip(image: Image.Image) -> Image.Image: + """ + Flip the image vertically (top to bottom). + + :param image: The image to flip. + :return: An image. + """ + return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + + +def grayscale(image: Image.Image) -> Image.Image: + """ + Convert the image to grayscale. + + :param image: The image to convert. + :return: An image. + """ + return image.convert("L") + + +def invert(image: Image.Image) -> Image.Image: + """ + Invert (negate) the image. + + :param image: The image to invert. + :return: An image. + """ + lut = list(range(255, -1, -1)) + return image.point(lut) if image.mode == "1" else _lut(image, lut) + + +def mirror(image: Image.Image) -> Image.Image: + """ + Flip image horizontally (left to right). + + :param image: The image to mirror. + :return: An image. + """ + return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + + +def posterize(image: Image.Image, bits: int) -> Image.Image: + """ + Reduce the number of bits for each color channel. + + :param image: The image to posterize. + :param bits: The number of bits to keep for each channel (1-8). + :return: An image. + """ + mask = ~(2 ** (8 - bits) - 1) + lut = [i & mask for i in range(256)] + return _lut(image, lut) + + +def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: + """ + Invert all pixel values above a threshold. + + :param image: The image to solarize. + :param threshold: All pixels above this grayscale level are inverted. + :return: An image. + """ + lut = [] + for i in range(256): + if i < threshold: + lut.append(i) + else: + lut.append(255 - i) + return _lut(image, lut) + + +@overload +def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ... + + +@overload +def exif_transpose( + image: Image.Image, *, in_place: Literal[False] = False +) -> Image.Image: ... + + +def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: + """ + If an image has an EXIF Orientation tag, other than 1, transpose the image + accordingly, and remove the orientation data. + + :param image: The image to transpose. + :param in_place: Boolean. Keyword-only argument. + If ``True``, the original image is modified in-place, and ``None`` is returned. + If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned + with the transposition applied. If there is no transposition, a copy of the + image will be returned. + """ + image.load() + image_exif = image.getexif() + orientation = image_exif.get(ExifTags.Base.Orientation, 1) + method = { + 2: Image.Transpose.FLIP_LEFT_RIGHT, + 3: Image.Transpose.ROTATE_180, + 4: Image.Transpose.FLIP_TOP_BOTTOM, + 5: Image.Transpose.TRANSPOSE, + 6: Image.Transpose.ROTATE_270, + 7: Image.Transpose.TRANSVERSE, + 8: Image.Transpose.ROTATE_90, + }.get(orientation) + if method is not None: + if in_place: + image.im = image.im.transpose(method) + image._size = image.im.size + else: + transposed_image = image.transpose(method) + exif_image = image if in_place else transposed_image + + exif = exif_image.getexif() + if ExifTags.Base.Orientation in exif: + del exif[ExifTags.Base.Orientation] + if "exif" in exif_image.info: + exif_image.info["exif"] = exif.tobytes() + elif "Raw profile type exif" in exif_image.info: + exif_image.info["Raw profile type exif"] = exif.tobytes().hex() + for key in ("XML:com.adobe.xmp", "xmp"): + if key in exif_image.info: + for pattern in ( + r'tiff:Orientation="([0-9])"', + r"([0-9])", + ): + value = exif_image.info[key] + if isinstance(value, str): + value = re.sub(pattern, "", value) + elif isinstance(value, tuple): + value = tuple( + re.sub(pattern.encode(), b"", v) for v in value + ) + else: + value = re.sub(pattern.encode(), b"", value) + exif_image.info[key] = value + if not in_place: + return transposed_image + elif not in_place: + return image.copy() + return None diff --git a/python_modules/PIL/ImagePalette.py b/python_modules/PIL/ImagePalette.py new file mode 100644 index 000000000..103697117 --- /dev/null +++ b/python_modules/PIL/ImagePalette.py @@ -0,0 +1,286 @@ +# +# The Python Imaging Library. +# $Id$ +# +# image palette object +# +# History: +# 1996-03-11 fl Rewritten. +# 1997-01-03 fl Up and running. +# 1997-08-23 fl Added load hack +# 2001-04-16 fl Fixed randint shadow bug in random() +# +# Copyright (c) 1997-2001 by Secret Labs AB +# Copyright (c) 1996-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import array +from collections.abc import Sequence +from typing import IO + +from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile + +TYPE_CHECKING = False +if TYPE_CHECKING: + from . import Image + + +class ImagePalette: + """ + Color palette for palette mapped images + + :param mode: The mode to use for the palette. See: + :ref:`concept-modes`. Defaults to "RGB" + :param palette: An optional palette. If given, it must be a bytearray, + an array or a list of ints between 0-255. The list must consist of + all channels for one color followed by the next color (e.g. RGBRGBRGB). + Defaults to an empty palette. + """ + + def __init__( + self, + mode: str = "RGB", + palette: Sequence[int] | bytes | bytearray | None = None, + ) -> None: + self.mode = mode + self.rawmode: str | None = None # if set, palette contains raw data + self.palette = palette or bytearray() + self.dirty: int | None = None + + @property + def palette(self) -> Sequence[int] | bytes | bytearray: + return self._palette + + @palette.setter + def palette(self, palette: Sequence[int] | bytes | bytearray) -> None: + self._colors: dict[tuple[int, ...], int] | None = None + self._palette = palette + + @property + def colors(self) -> dict[tuple[int, ...], int]: + if self._colors is None: + mode_len = len(self.mode) + self._colors = {} + for i in range(0, len(self.palette), mode_len): + color = tuple(self.palette[i : i + mode_len]) + if color in self._colors: + continue + self._colors[color] = i // mode_len + return self._colors + + @colors.setter + def colors(self, colors: dict[tuple[int, ...], int]) -> None: + self._colors = colors + + def copy(self) -> ImagePalette: + new = ImagePalette() + + new.mode = self.mode + new.rawmode = self.rawmode + if self.palette is not None: + new.palette = self.palette[:] + new.dirty = self.dirty + + return new + + def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]: + """ + Get palette contents in format suitable for the low-level + ``im.putpalette`` primitive. + + .. warning:: This method is experimental. + """ + if self.rawmode: + return self.rawmode, self.palette + return self.mode, self.tobytes() + + def tobytes(self) -> bytes: + """Convert palette to bytes. + + .. warning:: This method is experimental. + """ + if self.rawmode: + msg = "palette contains raw palette data" + raise ValueError(msg) + if isinstance(self.palette, bytes): + return self.palette + arr = array.array("B", self.palette) + return arr.tobytes() + + # Declare tostring as an alias for tobytes + tostring = tobytes + + def _new_color_index( + self, image: Image.Image | None = None, e: Exception | None = None + ) -> int: + if not isinstance(self.palette, bytearray): + self._palette = bytearray(self.palette) + index = len(self.palette) // 3 + special_colors: tuple[int | tuple[int, ...] | None, ...] = () + if image: + special_colors = ( + image.info.get("background"), + image.info.get("transparency"), + ) + while index in special_colors: + index += 1 + if index >= 256: + if image: + # Search for an unused index + for i, count in reversed(list(enumerate(image.histogram()))): + if count == 0 and i not in special_colors: + index = i + break + if index >= 256: + msg = "cannot allocate more than 256 colors" + raise ValueError(msg) from e + return index + + def getcolor( + self, + color: tuple[int, ...], + image: Image.Image | None = None, + ) -> int: + """Given an rgb tuple, allocate palette entry. + + .. warning:: This method is experimental. + """ + if self.rawmode: + msg = "palette contains raw palette data" + raise ValueError(msg) + if isinstance(color, tuple): + if self.mode == "RGB": + if len(color) == 4: + if color[3] != 255: + msg = "cannot add non-opaque RGBA color to RGB palette" + raise ValueError(msg) + color = color[:3] + elif self.mode == "RGBA": + if len(color) == 3: + color += (255,) + try: + return self.colors[color] + except KeyError as e: + # allocate new color slot + index = self._new_color_index(image, e) + assert isinstance(self._palette, bytearray) + self.colors[color] = index + if index * 3 < len(self.palette): + self._palette = ( + self._palette[: index * 3] + + bytes(color) + + self._palette[index * 3 + 3 :] + ) + else: + self._palette += bytes(color) + self.dirty = 1 + return index + else: + msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable] + raise ValueError(msg) + + def save(self, fp: str | IO[str]) -> None: + """Save palette to text file. + + .. warning:: This method is experimental. + """ + if self.rawmode: + msg = "palette contains raw palette data" + raise ValueError(msg) + if isinstance(fp, str): + fp = open(fp, "w") + fp.write("# Palette\n") + fp.write(f"# Mode: {self.mode}\n") + for i in range(256): + fp.write(f"{i}") + for j in range(i * len(self.mode), (i + 1) * len(self.mode)): + try: + fp.write(f" {self.palette[j]}") + except IndexError: + fp.write(" 0") + fp.write("\n") + fp.close() + + +# -------------------------------------------------------------------- +# Internal + + +def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette: + palette = ImagePalette() + palette.rawmode = rawmode + palette.palette = data + palette.dirty = 1 + return palette + + +# -------------------------------------------------------------------- +# Factories + + +def make_linear_lut(black: int, white: float) -> list[int]: + if black == 0: + return [int(white * i // 255) for i in range(256)] + + msg = "unavailable when black is non-zero" + raise NotImplementedError(msg) # FIXME + + +def make_gamma_lut(exp: float) -> list[int]: + return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)] + + +def negative(mode: str = "RGB") -> ImagePalette: + palette = list(range(256 * len(mode))) + palette.reverse() + return ImagePalette(mode, [i // len(mode) for i in palette]) + + +def random(mode: str = "RGB") -> ImagePalette: + from random import randint + + palette = [randint(0, 255) for _ in range(256 * len(mode))] + return ImagePalette(mode, palette) + + +def sepia(white: str = "#fff0c0") -> ImagePalette: + bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] + return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) + + +def wedge(mode: str = "RGB") -> ImagePalette: + palette = list(range(256 * len(mode))) + return ImagePalette(mode, [i // len(mode) for i in palette]) + + +def load(filename: str) -> tuple[bytes, str]: + # FIXME: supports GIMP gradients only + + with open(filename, "rb") as fp: + paletteHandlers: list[ + type[ + GimpPaletteFile.GimpPaletteFile + | GimpGradientFile.GimpGradientFile + | PaletteFile.PaletteFile + ] + ] = [ + GimpPaletteFile.GimpPaletteFile, + GimpGradientFile.GimpGradientFile, + PaletteFile.PaletteFile, + ] + for paletteHandler in paletteHandlers: + try: + fp.seek(0) + lut = paletteHandler(fp).getpalette() + if lut: + break + except (SyntaxError, ValueError): + pass + else: + msg = "cannot load palette" + raise OSError(msg) + + return lut # data, rawmode diff --git a/python_modules/PIL/ImagePath.py b/python_modules/PIL/ImagePath.py new file mode 100644 index 000000000..77e8a609a --- /dev/null +++ b/python_modules/PIL/ImagePath.py @@ -0,0 +1,20 @@ +# +# The Python Imaging Library +# $Id$ +# +# path interface +# +# History: +# 1996-11-04 fl Created +# 2002-04-14 fl Added documentation stub class +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image + +Path = Image.core.path diff --git a/python_modules/PIL/ImageQt.py b/python_modules/PIL/ImageQt.py new file mode 100644 index 000000000..df7a57b65 --- /dev/null +++ b/python_modules/PIL/ImageQt.py @@ -0,0 +1,220 @@ +# +# The Python Imaging Library. +# $Id$ +# +# a simple Qt image interface. +# +# history: +# 2006-06-03 fl: created +# 2006-06-04 fl: inherit from QImage instead of wrapping it +# 2006-06-05 fl: removed toimage helper; move string support to ImageQt +# 2013-11-13 fl: add support for Qt5 (aurelien.ballier@cyclonit.com) +# +# Copyright (c) 2006 by Secret Labs AB +# Copyright (c) 2006 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import sys +from io import BytesIO +from typing import Any, Callable, Union + +from . import Image +from ._util import is_path + +TYPE_CHECKING = False +if TYPE_CHECKING: + import PyQt6 + import PySide6 + + from . import ImageFile + + QBuffer: type + QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray] + QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice] + QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] + QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] + +qt_version: str | None +qt_versions = [ + ["6", "PyQt6"], + ["side6", "PySide6"], +] + +# If a version has already been imported, attempt it first +qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) +for version, qt_module in qt_versions: + try: + qRgba: Callable[[int, int, int, int], int] + if qt_module == "PyQt6": + from PyQt6.QtCore import QBuffer, QIODevice + from PyQt6.QtGui import QImage, QPixmap, qRgba + elif qt_module == "PySide6": + from PySide6.QtCore import QBuffer, QIODevice + from PySide6.QtGui import QImage, QPixmap, qRgba + except (ImportError, RuntimeError): + continue + qt_is_installed = True + qt_version = version + break +else: + qt_is_installed = False + qt_version = None + + +def rgb(r: int, g: int, b: int, a: int = 255) -> int: + """(Internal) Turns an RGB color into a Qt compatible color integer.""" + # use qRgb to pack the colors, and then turn the resulting long + # into a negative integer with the same bitpattern. + return qRgba(r, g, b, a) & 0xFFFFFFFF + + +def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile: + """ + :param im: QImage or PIL ImageQt object + """ + buffer = QBuffer() + qt_openmode: object + if qt_version == "6": + try: + qt_openmode = getattr(QIODevice, "OpenModeFlag") + except AttributeError: + qt_openmode = getattr(QIODevice, "OpenMode") + else: + qt_openmode = QIODevice + buffer.open(getattr(qt_openmode, "ReadWrite")) + # preserve alpha channel with png + # otherwise ppm is more friendly with Image.open + if im.hasAlphaChannel(): + im.save(buffer, "png") + else: + im.save(buffer, "ppm") + + b = BytesIO() + b.write(buffer.data()) + buffer.close() + b.seek(0) + + return Image.open(b) + + +def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile: + return fromqimage(im) + + +def align8to32(bytes: bytes, width: int, mode: str) -> bytes: + """ + converts each scanline of data from 8 bit to 32 bit aligned + """ + + bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode] + + # calculate bytes per line and the extra padding if needed + bits_per_line = bits_per_pixel * width + full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8) + bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0) + + extra_padding = -bytes_per_line % 4 + + # already 32 bit aligned by luck + if not extra_padding: + return bytes + + new_data = [ + bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding + for i in range(len(bytes) // bytes_per_line) + ] + + return b"".join(new_data) + + +def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]: + data = None + colortable = None + exclusive_fp = False + + # handle filename, if given instead of image name + if hasattr(im, "toUtf8"): + # FIXME - is this really the best way to do this? + im = str(im.toUtf8(), "utf-8") + if is_path(im): + im = Image.open(im) + exclusive_fp = True + assert isinstance(im, Image.Image) + + qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage + if im.mode == "1": + format = getattr(qt_format, "Format_Mono") + elif im.mode == "L": + format = getattr(qt_format, "Format_Indexed8") + colortable = [rgb(i, i, i) for i in range(256)] + elif im.mode == "P": + format = getattr(qt_format, "Format_Indexed8") + palette = im.getpalette() + assert palette is not None + colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)] + elif im.mode == "RGB": + # Populate the 4th channel with 255 + im = im.convert("RGBA") + + data = im.tobytes("raw", "BGRA") + format = getattr(qt_format, "Format_RGB32") + elif im.mode == "RGBA": + data = im.tobytes("raw", "BGRA") + format = getattr(qt_format, "Format_ARGB32") + elif im.mode == "I;16": + im = im.point(lambda i: i * 256) + + format = getattr(qt_format, "Format_Grayscale16") + else: + if exclusive_fp: + im.close() + msg = f"unsupported image mode {repr(im.mode)}" + raise ValueError(msg) + + size = im.size + __data = data or align8to32(im.tobytes(), size[0], im.mode) + if exclusive_fp: + im.close() + return {"data": __data, "size": size, "format": format, "colortable": colortable} + + +if qt_is_installed: + + class ImageQt(QImage): # type: ignore[misc] + def __init__(self, im: Image.Image | str | QByteArray) -> None: + """ + An PIL image wrapper for Qt. This is a subclass of PyQt's QImage + class. + + :param im: A PIL Image object, or a file name (given either as + Python string or a PyQt string object). + """ + im_data = _toqclass_helper(im) + # must keep a reference, or Qt will crash! + # All QImage constructors that take data operate on an existing + # buffer, so this buffer has to hang on for the life of the image. + # Fixes https://github.com/python-pillow/Pillow/issues/1370 + self.__data = im_data["data"] + super().__init__( + self.__data, + im_data["size"][0], + im_data["size"][1], + im_data["format"], + ) + if im_data["colortable"]: + self.setColorTable(im_data["colortable"]) + + +def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: + return ImageQt(im) + + +def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: + qimage = toqimage(im) + pixmap = getattr(QPixmap, "fromImage")(qimage) + if qt_version == "6": + pixmap.detach() + return pixmap diff --git a/python_modules/PIL/ImageSequence.py b/python_modules/PIL/ImageSequence.py new file mode 100644 index 000000000..a6fc340d5 --- /dev/null +++ b/python_modules/PIL/ImageSequence.py @@ -0,0 +1,86 @@ +# +# The Python Imaging Library. +# $Id$ +# +# sequence support classes +# +# history: +# 1997-02-20 fl Created +# +# Copyright (c) 1997 by Secret Labs AB. +# Copyright (c) 1997 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +## +from __future__ import annotations + +from typing import Callable + +from . import Image + + +class Iterator: + """ + This class implements an iterator object that can be used to loop + over an image sequence. + + You can use the ``[]`` operator to access elements by index. This operator + will raise an :py:exc:`IndexError` if you try to access a nonexistent + frame. + + :param im: An image object. + """ + + def __init__(self, im: Image.Image) -> None: + if not hasattr(im, "seek"): + msg = "im must have seek method" + raise AttributeError(msg) + self.im = im + self.position = getattr(self.im, "_min_frame", 0) + + def __getitem__(self, ix: int) -> Image.Image: + try: + self.im.seek(ix) + return self.im + except EOFError as e: + msg = "end of sequence" + raise IndexError(msg) from e + + def __iter__(self) -> Iterator: + return self + + def __next__(self) -> Image.Image: + try: + self.im.seek(self.position) + self.position += 1 + return self.im + except EOFError as e: + msg = "end of sequence" + raise StopIteration(msg) from e + + +def all_frames( + im: Image.Image | list[Image.Image], + func: Callable[[Image.Image], Image.Image] | None = None, +) -> list[Image.Image]: + """ + Applies a given function to all frames in an image or a list of images. + The frames are returned as a list of separate images. + + :param im: An image, or a list of images. + :param func: The function to apply to all of the image frames. + :returns: A list of images. + """ + if not isinstance(im, list): + im = [im] + + ims = [] + for imSequence in im: + current = imSequence.tell() + + ims += [im_frame.copy() for im_frame in Iterator(imSequence)] + + imSequence.seek(current) + return [func(im) for im in ims] if func else ims diff --git a/python_modules/PIL/ImageShow.py b/python_modules/PIL/ImageShow.py new file mode 100644 index 000000000..7705608e3 --- /dev/null +++ b/python_modules/PIL/ImageShow.py @@ -0,0 +1,362 @@ +# +# The Python Imaging Library. +# $Id$ +# +# im.show() drivers +# +# History: +# 2008-04-06 fl Created +# +# Copyright (c) Secret Labs AB 2008. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import abc +import os +import shutil +import subprocess +import sys +from shlex import quote +from typing import Any + +from . import Image + +_viewers = [] + + +def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None: + """ + The :py:func:`register` function is used to register additional viewers:: + + from PIL import ImageShow + ImageShow.register(MyViewer()) # MyViewer will be used as a last resort + ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised + ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised + + :param viewer: The viewer to be registered. + :param order: + Zero or a negative integer to prepend this viewer to the list, + a positive integer to append it. + """ + if isinstance(viewer, type) and issubclass(viewer, Viewer): + viewer = viewer() + if order > 0: + _viewers.append(viewer) + else: + _viewers.insert(0, viewer) + + +def show(image: Image.Image, title: str | None = None, **options: Any) -> bool: + r""" + Display a given image. + + :param image: An image object. + :param title: Optional title. Not all viewers can display the title. + :param \**options: Additional viewer options. + :returns: ``True`` if a suitable viewer was found, ``False`` otherwise. + """ + for viewer in _viewers: + if viewer.show(image, title=title, **options): + return True + return False + + +class Viewer: + """Base class for viewers.""" + + # main api + + def show(self, image: Image.Image, **options: Any) -> int: + """ + The main function for displaying an image. + Converts the given image to the target format and displays it. + """ + + if not ( + image.mode in ("1", "RGBA") + or (self.format == "PNG" and image.mode in ("I;16", "LA")) + ): + base = Image.getmodebase(image.mode) + if image.mode != base: + image = image.convert(base) + + return self.show_image(image, **options) + + # hook methods + + format: str | None = None + """The format to convert the image into.""" + options: dict[str, Any] = {} + """Additional options used to convert the image.""" + + def get_format(self, image: Image.Image) -> str | None: + """Return format name, or ``None`` to save as PGM/PPM.""" + return self.format + + def get_command(self, file: str, **options: Any) -> str: + """ + Returns the command used to display the file. + Not implemented in the base class. + """ + msg = "unavailable in base viewer" + raise NotImplementedError(msg) + + def save_image(self, image: Image.Image) -> str: + """Save to temporary file and return filename.""" + return image._dump(format=self.get_format(image), **self.options) + + def show_image(self, image: Image.Image, **options: Any) -> int: + """Display the given image.""" + return self.show_file(self.save_image(image), **options) + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + if not os.path.exists(path): + raise FileNotFoundError + os.system(self.get_command(path, **options)) # nosec + return 1 + + +# -------------------------------------------------------------------- + + +class WindowsViewer(Viewer): + """The default viewer on Windows is the default system application for PNG files.""" + + format = "PNG" + options = {"compress_level": 1, "save_all": True} + + def get_command(self, file: str, **options: Any) -> str: + return ( + f'start "Pillow" /WAIT "{file}" ' + "&& ping -n 4 127.0.0.1 >NUL " + f'&& del /f "{file}"' + ) + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + if not os.path.exists(path): + raise FileNotFoundError + subprocess.Popen( + self.get_command(path, **options), + shell=True, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW"), + ) # nosec + return 1 + + +if sys.platform == "win32": + register(WindowsViewer) + + +class MacViewer(Viewer): + """The default viewer on macOS using ``Preview.app``.""" + + format = "PNG" + options = {"compress_level": 1, "save_all": True} + + def get_command(self, file: str, **options: Any) -> str: + # on darwin open returns immediately resulting in the temp + # file removal while app is opening + command = "open -a Preview.app" + command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" + return command + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + if not os.path.exists(path): + raise FileNotFoundError + subprocess.call(["open", "-a", "Preview.app", path]) + + pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") + executable = (not pyinstaller and sys.executable) or shutil.which("python3") + if executable: + subprocess.Popen( + [ + executable, + "-c", + "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", + path, + ] + ) + return 1 + + +if sys.platform == "darwin": + register(MacViewer) + + +class UnixViewer(abc.ABC, Viewer): + format = "PNG" + options = {"compress_level": 1, "save_all": True} + + @abc.abstractmethod + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + pass + + def get_command(self, file: str, **options: Any) -> str: + command = self.get_command_ex(file, **options)[0] + return f"{command} {quote(file)}" + + +class XDGViewer(UnixViewer): + """ + The freedesktop.org ``xdg-open`` command. + """ + + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + command = executable = "xdg-open" + return command, executable + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + if not os.path.exists(path): + raise FileNotFoundError + subprocess.Popen(["xdg-open", path]) + return 1 + + +class DisplayViewer(UnixViewer): + """ + The ImageMagick ``display`` command. + This viewer supports the ``title`` parameter. + """ + + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: + command = executable = "display" + if title: + command += f" -title {quote(title)}" + return command, executable + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + if not os.path.exists(path): + raise FileNotFoundError + args = ["display"] + title = options.get("title") + if title: + args += ["-title", title] + args.append(path) + + subprocess.Popen(args) + return 1 + + +class GmDisplayViewer(UnixViewer): + """The GraphicsMagick ``gm display`` command.""" + + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + executable = "gm" + command = "gm display" + return command, executable + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + if not os.path.exists(path): + raise FileNotFoundError + subprocess.Popen(["gm", "display", path]) + return 1 + + +class EogViewer(UnixViewer): + """The GNOME Image Viewer ``eog`` command.""" + + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + executable = "eog" + command = "eog -n" + return command, executable + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + if not os.path.exists(path): + raise FileNotFoundError + subprocess.Popen(["eog", "-n", path]) + return 1 + + +class XVViewer(UnixViewer): + """ + The X Viewer ``xv`` command. + This viewer supports the ``title`` parameter. + """ + + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: + # note: xv is pretty outdated. most modern systems have + # imagemagick's display command instead. + command = executable = "xv" + if title: + command += f" -name {quote(title)}" + return command, executable + + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + if not os.path.exists(path): + raise FileNotFoundError + args = ["xv"] + title = options.get("title") + if title: + args += ["-name", title] + args.append(path) + + subprocess.Popen(args) + return 1 + + +if sys.platform not in ("win32", "darwin"): # unixoids + if shutil.which("xdg-open"): + register(XDGViewer) + if shutil.which("display"): + register(DisplayViewer) + if shutil.which("gm"): + register(GmDisplayViewer) + if shutil.which("eog"): + register(EogViewer) + if shutil.which("xv"): + register(XVViewer) + + +class IPythonViewer(Viewer): + """The viewer for IPython frontends.""" + + def show_image(self, image: Image.Image, **options: Any) -> int: + ipython_display(image) + return 1 + + +try: + from IPython.display import display as ipython_display +except ImportError: + pass +else: + register(IPythonViewer) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Syntax: python3 ImageShow.py imagefile [title]") + sys.exit() + + with Image.open(sys.argv[1]) as im: + print(show(im, *sys.argv[2:])) diff --git a/python_modules/PIL/ImageStat.py b/python_modules/PIL/ImageStat.py new file mode 100644 index 000000000..8bc504526 --- /dev/null +++ b/python_modules/PIL/ImageStat.py @@ -0,0 +1,160 @@ +# +# The Python Imaging Library. +# $Id$ +# +# global image statistics +# +# History: +# 1996-04-05 fl Created +# 1997-05-21 fl Added mask; added rms, var, stddev attributes +# 1997-08-05 fl Added median +# 1998-07-05 hk Fixed integer overflow error +# +# Notes: +# This class shows how to implement delayed evaluation of attributes. +# To get a certain value, simply access the corresponding attribute. +# The __getattr__ dispatcher takes care of the rest. +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996-97. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import math +from functools import cached_property + +from . import Image + + +class Stat: + def __init__( + self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None + ) -> None: + """ + Calculate statistics for the given image. If a mask is included, + only the regions covered by that mask are included in the + statistics. You can also pass in a previously calculated histogram. + + :param image: A PIL image, or a precalculated histogram. + + .. note:: + + For a PIL image, calculations rely on the + :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are + grouped into 256 bins, even if the image has more than 8 bits per + channel. So ``I`` and ``F`` mode images have a maximum ``mean``, + ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum + of more than 255. + + :param mask: An optional mask. + """ + if isinstance(image_or_list, Image.Image): + self.h = image_or_list.histogram(mask) + elif isinstance(image_or_list, list): + self.h = image_or_list + else: + msg = "first argument must be image or list" # type: ignore[unreachable] + raise TypeError(msg) + self.bands = list(range(len(self.h) // 256)) + + @cached_property + def extrema(self) -> list[tuple[int, int]]: + """ + Min/max values for each band in the image. + + .. note:: + This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and + simply returns the low and high bins used. This is correct for + images with 8 bits per channel, but fails for other modes such as + ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to + return per-band extrema for the image. This is more correct and + efficient because, for non-8-bit modes, the histogram method uses + :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used. + """ + + def minmax(histogram: list[int]) -> tuple[int, int]: + res_min, res_max = 255, 0 + for i in range(256): + if histogram[i]: + res_min = i + break + for i in range(255, -1, -1): + if histogram[i]: + res_max = i + break + return res_min, res_max + + return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)] + + @cached_property + def count(self) -> list[int]: + """Total number of pixels for each band in the image.""" + return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)] + + @cached_property + def sum(self) -> list[float]: + """Sum of all pixels for each band in the image.""" + + v = [] + for i in range(0, len(self.h), 256): + layer_sum = 0.0 + for j in range(256): + layer_sum += j * self.h[i + j] + v.append(layer_sum) + return v + + @cached_property + def sum2(self) -> list[float]: + """Squared sum of all pixels for each band in the image.""" + + v = [] + for i in range(0, len(self.h), 256): + sum2 = 0.0 + for j in range(256): + sum2 += (j**2) * float(self.h[i + j]) + v.append(sum2) + return v + + @cached_property + def mean(self) -> list[float]: + """Average (arithmetic mean) pixel level for each band in the image.""" + return [self.sum[i] / self.count[i] for i in self.bands] + + @cached_property + def median(self) -> list[int]: + """Median pixel level for each band in the image.""" + + v = [] + for i in self.bands: + s = 0 + half = self.count[i] // 2 + b = i * 256 + for j in range(256): + s = s + self.h[b + j] + if s > half: + break + v.append(j) + return v + + @cached_property + def rms(self) -> list[float]: + """RMS (root-mean-square) for each band in the image.""" + return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands] + + @cached_property + def var(self) -> list[float]: + """Variance for each band in the image.""" + return [ + (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] + for i in self.bands + ] + + @cached_property + def stddev(self) -> list[float]: + """Standard deviation for each band in the image.""" + return [math.sqrt(self.var[i]) for i in self.bands] + + +Global = Stat # compatibility diff --git a/python_modules/PIL/ImageTk.py b/python_modules/PIL/ImageTk.py new file mode 100644 index 000000000..3a4cb81e9 --- /dev/null +++ b/python_modules/PIL/ImageTk.py @@ -0,0 +1,266 @@ +# +# The Python Imaging Library. +# $Id$ +# +# a Tk display interface +# +# History: +# 96-04-08 fl Created +# 96-09-06 fl Added getimage method +# 96-11-01 fl Rewritten, removed image attribute and crop method +# 97-05-09 fl Use PyImagingPaste method instead of image type +# 97-05-12 fl Minor tweaks to match the IFUNC95 interface +# 97-05-17 fl Support the "pilbitmap" booster patch +# 97-06-05 fl Added file= and data= argument to image constructors +# 98-03-09 fl Added width and height methods to Image classes +# 98-07-02 fl Use default mode for "P" images without palette attribute +# 98-07-02 fl Explicitly destroy Tkinter image objects +# 99-07-24 fl Support multiple Tk interpreters (from Greg Couch) +# 99-07-26 fl Automatically hook into Tkinter (if possible) +# 99-08-15 fl Hook uses _imagingtk instead of _imaging +# +# Copyright (c) 1997-1999 by Secret Labs AB +# Copyright (c) 1996-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import tkinter +from io import BytesIO +from typing import Any + +from . import Image, ImageFile + +TYPE_CHECKING = False +if TYPE_CHECKING: + from ._typing import CapsuleType + +# -------------------------------------------------------------------- +# Check for Tkinter interface hooks + + +def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: + source = None + if "file" in kw: + source = kw.pop("file") + elif "data" in kw: + source = BytesIO(kw.pop("data")) + if not source: + return None + return Image.open(source) + + +def _pyimagingtkcall( + command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType +) -> None: + tk = photo.tk + try: + tk.call(command, photo, repr(ptr)) + except tkinter.TclError: + # activate Tkinter hook + # may raise an error if it cannot attach to Tkinter + from . import _imagingtk + + _imagingtk.tkinit(tk.interpaddr()) + tk.call(command, photo, repr(ptr)) + + +# -------------------------------------------------------------------- +# PhotoImage + + +class PhotoImage: + """ + A Tkinter-compatible photo image. This can be used + everywhere Tkinter expects an image object. If the image is an RGBA + image, pixels having alpha 0 are treated as transparent. + + The constructor takes either a PIL image, or a mode and a size. + Alternatively, you can use the ``file`` or ``data`` options to initialize + the photo image object. + + :param image: Either a PIL image, or a mode string. If a mode string is + used, a size must also be given. + :param size: If the first argument is a mode string, this defines the size + of the image. + :keyword file: A filename to load the image from (using + ``Image.open(file)``). + :keyword data: An 8-bit string containing image data (as loaded from an + image file). + """ + + def __init__( + self, + image: Image.Image | str | None = None, + size: tuple[int, int] | None = None, + **kw: Any, + ) -> None: + # Tk compatibility: file or data + if image is None: + image = _get_image_from_kw(kw) + + if image is None: + msg = "Image is required" + raise ValueError(msg) + elif isinstance(image, str): + mode = image + image = None + + if size is None: + msg = "If first argument is mode, size is required" + raise ValueError(msg) + else: + # got an image instead of a mode + mode = image.mode + if mode == "P": + # palette mapped data + image.apply_transparency() + image.load() + mode = image.palette.mode if image.palette else "RGB" + size = image.size + kw["width"], kw["height"] = size + + if mode not in ["1", "L", "RGB", "RGBA"]: + mode = Image.getmodebase(mode) + + self.__mode = mode + self.__size = size + self.__photo = tkinter.PhotoImage(**kw) + self.tk = self.__photo.tk + if image: + self.paste(image) + + def __del__(self) -> None: + try: + name = self.__photo.name + except AttributeError: + return + self.__photo.name = None + try: + self.__photo.tk.call("image", "delete", name) + except Exception: + pass # ignore internal errors + + def __str__(self) -> str: + """ + Get the Tkinter photo image identifier. This method is automatically + called by Tkinter whenever a PhotoImage object is passed to a Tkinter + method. + + :return: A Tkinter photo image identifier (a string). + """ + return str(self.__photo) + + def width(self) -> int: + """ + Get the width of the image. + + :return: The width, in pixels. + """ + return self.__size[0] + + def height(self) -> int: + """ + Get the height of the image. + + :return: The height, in pixels. + """ + return self.__size[1] + + def paste(self, im: Image.Image) -> None: + """ + Paste a PIL image into the photo image. Note that this can + be very slow if the photo image is displayed. + + :param im: A PIL image. The size must match the target region. If the + mode does not match, the image is converted to the mode of + the bitmap image. + """ + # convert to blittable + ptr = im.getim() + image = im.im + if not image.isblock() or im.mode != self.__mode: + block = Image.core.new_block(self.__mode, im.size) + image.convert2(block, image) # convert directly between buffers + ptr = block.ptr + + _pyimagingtkcall("PyImagingPhoto", self.__photo, ptr) + + +# -------------------------------------------------------------------- +# BitmapImage + + +class BitmapImage: + """ + A Tkinter-compatible bitmap image. This can be used everywhere Tkinter + expects an image object. + + The given image must have mode "1". Pixels having value 0 are treated as + transparent. Options, if any, are passed on to Tkinter. The most commonly + used option is ``foreground``, which is used to specify the color for the + non-transparent parts. See the Tkinter documentation for information on + how to specify colours. + + :param image: A PIL image. + """ + + def __init__(self, image: Image.Image | None = None, **kw: Any) -> None: + # Tk compatibility: file or data + if image is None: + image = _get_image_from_kw(kw) + + if image is None: + msg = "Image is required" + raise ValueError(msg) + self.__mode = image.mode + self.__size = image.size + + self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw) + + def __del__(self) -> None: + try: + name = self.__photo.name + except AttributeError: + return + self.__photo.name = None + try: + self.__photo.tk.call("image", "delete", name) + except Exception: + pass # ignore internal errors + + def width(self) -> int: + """ + Get the width of the image. + + :return: The width, in pixels. + """ + return self.__size[0] + + def height(self) -> int: + """ + Get the height of the image. + + :return: The height, in pixels. + """ + return self.__size[1] + + def __str__(self) -> str: + """ + Get the Tkinter bitmap image identifier. This method is automatically + called by Tkinter whenever a BitmapImage object is passed to a Tkinter + method. + + :return: A Tkinter bitmap image identifier (a string). + """ + return str(self.__photo) + + +def getimage(photo: PhotoImage) -> Image.Image: + """Copies the contents of a PhotoImage to a PIL image memory.""" + im = Image.new("RGBA", (photo.width(), photo.height())) + + _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim()) + + return im diff --git a/python_modules/PIL/ImageTransform.py b/python_modules/PIL/ImageTransform.py new file mode 100644 index 000000000..fb144ff38 --- /dev/null +++ b/python_modules/PIL/ImageTransform.py @@ -0,0 +1,136 @@ +# +# The Python Imaging Library. +# $Id$ +# +# transform wrappers +# +# History: +# 2002-04-08 fl Created +# +# Copyright (c) 2002 by Secret Labs AB +# Copyright (c) 2002 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from . import Image + + +class Transform(Image.ImageTransformHandler): + """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`.""" + + method: Image.Transform + + def __init__(self, data: Sequence[Any]) -> None: + self.data = data + + def getdata(self) -> tuple[Image.Transform, Sequence[int]]: + return self.method, self.data + + def transform( + self, + size: tuple[int, int], + image: Image.Image, + **options: Any, + ) -> Image.Image: + """Perform the transform. Called from :py:meth:`.Image.transform`.""" + # can be overridden + method, data = self.getdata() + return image.transform(size, method, data, **options) + + +class AffineTransform(Transform): + """ + Define an affine image transform. + + This function takes a 6-tuple (a, b, c, d, e, f) which contain the first + two rows from the inverse of an affine transform matrix. For each pixel + (x, y) in the output image, the new value is taken from a position (a x + + b y + c, d x + e y + f) in the input image, rounded to nearest pixel. + + This function can be used to scale, translate, rotate, and shear the + original image. + + See :py:meth:`.Image.transform` + + :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows + from the inverse of an affine transform matrix. + """ + + method = Image.Transform.AFFINE + + +class PerspectiveTransform(Transform): + """ + Define a perspective image transform. + + This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel + (x, y) in the output image, the new value is taken from a position + ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in + the input image, rounded to nearest pixel. + + This function can be used to scale, translate, rotate, and shear the + original image. + + See :py:meth:`.Image.transform` + + :param matrix: An 8-tuple (a, b, c, d, e, f, g, h). + """ + + method = Image.Transform.PERSPECTIVE + + +class ExtentTransform(Transform): + """ + Define a transform to extract a subregion from an image. + + Maps a rectangle (defined by two corners) from the image to a rectangle of + the given size. The resulting image will contain data sampled from between + the corners, such that (x0, y0) in the input image will end up at (0,0) in + the output image, and (x1, y1) at size. + + This method can be used to crop, stretch, shrink, or mirror an arbitrary + rectangle in the current image. It is slightly slower than crop, but about + as fast as a corresponding resize operation. + + See :py:meth:`.Image.transform` + + :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the + input image's coordinate system. See :ref:`coordinate-system`. + """ + + method = Image.Transform.EXTENT + + +class QuadTransform(Transform): + """ + Define a quad image transform. + + Maps a quadrilateral (a region defined by four corners) from the image to a + rectangle of the given size. + + See :py:meth:`.Image.transform` + + :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the + upper left, lower left, lower right, and upper right corner of the + source quadrilateral. + """ + + method = Image.Transform.QUAD + + +class MeshTransform(Transform): + """ + Define a mesh image transform. A mesh transform consists of one or more + individual quad transforms. + + See :py:meth:`.Image.transform` + + :param data: A list of (bbox, quad) tuples. + """ + + method = Image.Transform.MESH diff --git a/python_modules/PIL/ImageWin.py b/python_modules/PIL/ImageWin.py new file mode 100644 index 000000000..98c28f29f --- /dev/null +++ b/python_modules/PIL/ImageWin.py @@ -0,0 +1,247 @@ +# +# The Python Imaging Library. +# $Id$ +# +# a Windows DIB display interface +# +# History: +# 1996-05-20 fl Created +# 1996-09-20 fl Fixed subregion exposure +# 1997-09-21 fl Added draw primitive (for tzPrint) +# 2003-05-21 fl Added experimental Window/ImageWindow classes +# 2003-09-05 fl Added fromstring/tostring methods +# +# Copyright (c) Secret Labs AB 1997-2003. +# Copyright (c) Fredrik Lundh 1996-2003. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image + + +class HDC: + """ + Wraps an HDC integer. The resulting object can be passed to the + :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` + methods. + """ + + def __init__(self, dc: int) -> None: + self.dc = dc + + def __int__(self) -> int: + return self.dc + + +class HWND: + """ + Wraps an HWND integer. The resulting object can be passed to the + :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` + methods, instead of a DC. + """ + + def __init__(self, wnd: int) -> None: + self.wnd = wnd + + def __int__(self) -> int: + return self.wnd + + +class Dib: + """ + A Windows bitmap with the given mode and size. The mode can be one of "1", + "L", "P", or "RGB". + + If the display requires a palette, this constructor creates a suitable + palette and associates it with the image. For an "L" image, 128 graylevels + are allocated. For an "RGB" image, a 6x6x6 colour cube is used, together + with 20 graylevels. + + To make sure that palettes work properly under Windows, you must call the + ``palette`` method upon certain events from Windows. + + :param image: Either a PIL image, or a mode string. If a mode string is + used, a size must also be given. The mode can be one of "1", + "L", "P", or "RGB". + :param size: If the first argument is a mode string, this + defines the size of the image. + """ + + def __init__( + self, image: Image.Image | str, size: tuple[int, int] | None = None + ) -> None: + if isinstance(image, str): + mode = image + image = "" + if size is None: + msg = "If first argument is mode, size is required" + raise ValueError(msg) + else: + mode = image.mode + size = image.size + if mode not in ["1", "L", "P", "RGB"]: + mode = Image.getmodebase(mode) + self.image = Image.core.display(mode, size) + self.mode = mode + self.size = size + if image: + assert not isinstance(image, str) + self.paste(image) + + def expose(self, handle: int | HDC | HWND) -> None: + """ + Copy the bitmap contents to a device context. + + :param handle: Device context (HDC), cast to a Python integer, or an + HDC or HWND instance. In PythonWin, you can use + ``CDC.GetHandleAttrib()`` to get a suitable handle. + """ + handle_int = int(handle) + if isinstance(handle, HWND): + dc = self.image.getdc(handle_int) + try: + self.image.expose(dc) + finally: + self.image.releasedc(handle_int, dc) + else: + self.image.expose(handle_int) + + def draw( + self, + handle: int | HDC | HWND, + dst: tuple[int, int, int, int], + src: tuple[int, int, int, int] | None = None, + ) -> None: + """ + Same as expose, but allows you to specify where to draw the image, and + what part of it to draw. + + The destination and source areas are given as 4-tuple rectangles. If + the source is omitted, the entire image is copied. If the source and + the destination have different sizes, the image is resized as + necessary. + """ + if src is None: + src = (0, 0) + self.size + handle_int = int(handle) + if isinstance(handle, HWND): + dc = self.image.getdc(handle_int) + try: + self.image.draw(dc, dst, src) + finally: + self.image.releasedc(handle_int, dc) + else: + self.image.draw(handle_int, dst, src) + + def query_palette(self, handle: int | HDC | HWND) -> int: + """ + Installs the palette associated with the image in the given device + context. + + This method should be called upon **QUERYNEWPALETTE** and + **PALETTECHANGED** events from Windows. If this method returns a + non-zero value, one or more display palette entries were changed, and + the image should be redrawn. + + :param handle: Device context (HDC), cast to a Python integer, or an + HDC or HWND instance. + :return: The number of entries that were changed (if one or more entries, + this indicates that the image should be redrawn). + """ + handle_int = int(handle) + if isinstance(handle, HWND): + handle = self.image.getdc(handle_int) + try: + result = self.image.query_palette(handle) + finally: + self.image.releasedc(handle, handle) + else: + result = self.image.query_palette(handle_int) + return result + + def paste( + self, im: Image.Image, box: tuple[int, int, int, int] | None = None + ) -> None: + """ + Paste a PIL image into the bitmap image. + + :param im: A PIL image. The size must match the target region. + If the mode does not match, the image is converted to the + mode of the bitmap image. + :param box: A 4-tuple defining the left, upper, right, and + lower pixel coordinate. See :ref:`coordinate-system`. If + None is given instead of a tuple, all of the image is + assumed. + """ + im.load() + if self.mode != im.mode: + im = im.convert(self.mode) + if box: + self.image.paste(im.im, box) + else: + self.image.paste(im.im) + + def frombytes(self, buffer: bytes) -> None: + """ + Load display memory contents from byte data. + + :param buffer: A buffer containing display data (usually + data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`) + """ + self.image.frombytes(buffer) + + def tobytes(self) -> bytes: + """ + Copy display memory contents to bytes object. + + :return: A bytes object containing display data. + """ + return self.image.tobytes() + + +class Window: + """Create a Window with the given title size.""" + + def __init__( + self, title: str = "PIL", width: int | None = None, height: int | None = None + ) -> None: + self.hwnd = Image.core.createwindow( + title, self.__dispatcher, width or 0, height or 0 + ) + + def __dispatcher(self, action: str, *args: int) -> None: + getattr(self, f"ui_handle_{action}")(*args) + + def ui_handle_clear(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: + pass + + def ui_handle_damage(self, x0: int, y0: int, x1: int, y1: int) -> None: + pass + + def ui_handle_destroy(self) -> None: + pass + + def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: + pass + + def ui_handle_resize(self, width: int, height: int) -> None: + pass + + def mainloop(self) -> None: + Image.core.eventloop() + + +class ImageWindow(Window): + """Create an image window which displays the given image.""" + + def __init__(self, image: Image.Image | Dib, title: str = "PIL") -> None: + if not isinstance(image, Dib): + image = Dib(image) + self.image = image + width, height = image.size + super().__init__(title, width=width, height=height) + + def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: + self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/python_modules/PIL/ImtImagePlugin.py b/python_modules/PIL/ImtImagePlugin.py new file mode 100644 index 000000000..c4eccee34 --- /dev/null +++ b/python_modules/PIL/ImtImagePlugin.py @@ -0,0 +1,103 @@ +# +# The Python Imaging Library. +# $Id$ +# +# IM Tools support for PIL +# +# history: +# 1996-05-27 fl Created (read 8-bit images only) +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.2) +# +# Copyright (c) Secret Labs AB 1997-2001. +# Copyright (c) Fredrik Lundh 1996-2001. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import re + +from . import Image, ImageFile + +# +# -------------------------------------------------------------------- + +field = re.compile(rb"([a-z]*) ([^ \r\n]*)") + + +## +# Image plugin for IM Tools images. + + +class ImtImageFile(ImageFile.ImageFile): + format = "IMT" + format_description = "IM Tools" + + def _open(self) -> None: + # Quick rejection: if there's not a LF among the first + # 100 bytes, this is (probably) not a text header. + + assert self.fp is not None + + buffer = self.fp.read(100) + if b"\n" not in buffer: + msg = "not an IM file" + raise SyntaxError(msg) + + xsize = ysize = 0 + + while True: + if buffer: + s = buffer[:1] + buffer = buffer[1:] + else: + s = self.fp.read(1) + if not s: + break + + if s == b"\x0c": + # image data begins + self.tile = [ + ImageFile._Tile( + "raw", + (0, 0) + self.size, + self.fp.tell() - len(buffer), + self.mode, + ) + ] + + break + + else: + # read key/value pair + if b"\n" not in buffer: + buffer += self.fp.read(100) + lines = buffer.split(b"\n") + s += lines.pop(0) + buffer = b"\n".join(lines) + if len(s) == 1 or len(s) > 100: + break + if s[0] == ord(b"*"): + continue # comment + + m = field.match(s) + if not m: + break + k, v = m.group(1, 2) + if k == b"width": + xsize = int(v) + self._size = xsize, ysize + elif k == b"height": + ysize = int(v) + self._size = xsize, ysize + elif k == b"pixel" and v == b"n8": + self._mode = "L" + + +# +# -------------------------------------------------------------------- + +Image.register_open(ImtImageFile.format, ImtImageFile) + +# +# no extension registered (".im" is simply too common) diff --git a/python_modules/PIL/IptcImagePlugin.py b/python_modules/PIL/IptcImagePlugin.py new file mode 100644 index 000000000..fc024d668 --- /dev/null +++ b/python_modules/PIL/IptcImagePlugin.py @@ -0,0 +1,250 @@ +# +# The Python Imaging Library. +# $Id$ +# +# IPTC/NAA file handling +# +# history: +# 1995-10-01 fl Created +# 1998-03-09 fl Cleaned up and added to PIL +# 2002-06-18 fl Added getiptcinfo helper +# +# Copyright (c) Secret Labs AB 1997-2002. +# Copyright (c) Fredrik Lundh 1995. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from collections.abc import Sequence +from io import BytesIO +from typing import cast + +from . import Image, ImageFile +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._deprecate import deprecate + +COMPRESSION = {1: "raw", 5: "jpeg"} + + +def __getattr__(name: str) -> bytes: + if name == "PAD": + deprecate("IptcImagePlugin.PAD", 12) + return b"\0\0\0\0" + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) + + +# +# Helpers + + +def _i(c: bytes) -> int: + return i32((b"\0\0\0\0" + c)[-4:]) + + +def _i8(c: int | bytes) -> int: + return c if isinstance(c, int) else c[0] + + +def i(c: bytes) -> int: + """.. deprecated:: 10.2.0""" + deprecate("IptcImagePlugin.i", 12) + return _i(c) + + +def dump(c: Sequence[int | bytes]) -> None: + """.. deprecated:: 10.2.0""" + deprecate("IptcImagePlugin.dump", 12) + for i in c: + print(f"{_i8(i):02x}", end=" ") + print() + + +## +# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields +# from TIFF and JPEG files, use the getiptcinfo function. + + +class IptcImageFile(ImageFile.ImageFile): + format = "IPTC" + format_description = "IPTC/NAA" + + def getint(self, key: tuple[int, int]) -> int: + return _i(self.info[key]) + + def field(self) -> tuple[tuple[int, int] | None, int]: + # + # get a IPTC field header + s = self.fp.read(5) + if not s.strip(b"\x00"): + return None, 0 + + tag = s[1], s[2] + + # syntax + if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]: + msg = "invalid IPTC/NAA file" + raise SyntaxError(msg) + + # field size + size = s[3] + if size > 132: + msg = "illegal field length in IPTC/NAA file" + raise OSError(msg) + elif size == 128: + size = 0 + elif size > 128: + size = _i(self.fp.read(size - 128)) + else: + size = i16(s, 3) + + return tag, size + + def _open(self) -> None: + # load descriptive fields + while True: + offset = self.fp.tell() + tag, size = self.field() + if not tag or tag == (8, 10): + break + if size: + tagdata = self.fp.read(size) + else: + tagdata = None + if tag in self.info: + if isinstance(self.info[tag], list): + self.info[tag].append(tagdata) + else: + self.info[tag] = [self.info[tag], tagdata] + else: + self.info[tag] = tagdata + + # mode + layers = self.info[(3, 60)][0] + component = self.info[(3, 60)][1] + if (3, 65) in self.info: + id = self.info[(3, 65)][0] - 1 + else: + id = 0 + if layers == 1 and not component: + self._mode = "L" + elif layers == 3 and component: + self._mode = "RGB"[id] + elif layers == 4 and component: + self._mode = "CMYK"[id] + + # size + self._size = self.getint((3, 20)), self.getint((3, 30)) + + # compression + try: + compression = COMPRESSION[self.getint((3, 120))] + except KeyError as e: + msg = "Unknown IPTC image compression" + raise OSError(msg) from e + + # tile + if tag == (8, 10): + self.tile = [ + ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression) + ] + + def load(self) -> Image.core.PixelAccess | None: + if len(self.tile) != 1 or self.tile[0][0] != "iptc": + return ImageFile.ImageFile.load(self) + + offset, compression = self.tile[0][2:] + + self.fp.seek(offset) + + # Copy image data to temporary file + o = BytesIO() + if compression == "raw": + # To simplify access to the extracted file, + # prepend a PPM header + o.write(b"P5\n%d %d\n255\n" % self.size) + while True: + type, size = self.field() + if type != (8, 10): + break + while size > 0: + s = self.fp.read(min(size, 8192)) + if not s: + break + o.write(s) + size -= len(s) + + with Image.open(o) as _im: + _im.load() + self.im = _im.im + self.tile = [] + return Image.Image.load(self) + + +Image.register_open(IptcImageFile.format, IptcImageFile) + +Image.register_extension(IptcImageFile.format, ".iim") + + +def getiptcinfo( + im: ImageFile.ImageFile, +) -> dict[tuple[int, int], bytes | list[bytes]] | None: + """ + Get IPTC information from TIFF, JPEG, or IPTC file. + + :param im: An image containing IPTC data. + :returns: A dictionary containing IPTC information, or None if + no IPTC information block was found. + """ + from . import JpegImagePlugin, TiffImagePlugin + + data = None + + info: dict[tuple[int, int], bytes | list[bytes]] = {} + if isinstance(im, IptcImageFile): + # return info dictionary right away + for k, v in im.info.items(): + if isinstance(k, tuple): + info[k] = v + return info + + elif isinstance(im, JpegImagePlugin.JpegImageFile): + # extract the IPTC/NAA resource + photoshop = im.info.get("photoshop") + if photoshop: + data = photoshop.get(0x0404) + + elif isinstance(im, TiffImagePlugin.TiffImageFile): + # get raw data from the IPTC/NAA tag (PhotoShop tags the data + # as 4-byte integers, so we cannot use the get method...) + try: + data = im.tag_v2._tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] + except KeyError: + pass + + if data is None: + return None # no properties + + # create an IptcImagePlugin object without initializing it + class FakeImage: + pass + + fake_im = FakeImage() + fake_im.__class__ = IptcImageFile # type: ignore[assignment] + iptc_im = cast(IptcImageFile, fake_im) + + # parse the IPTC information chunk + iptc_im.info = {} + iptc_im.fp = BytesIO(data) + + try: + iptc_im._open() + except (IndexError, KeyError): + pass # expected failure + + for k, v in iptc_im.info.items(): + if isinstance(k, tuple): + info[k] = v + return info diff --git a/python_modules/PIL/Jpeg2KImagePlugin.py b/python_modules/PIL/Jpeg2KImagePlugin.py new file mode 100644 index 000000000..e0f4ecae5 --- /dev/null +++ b/python_modules/PIL/Jpeg2KImagePlugin.py @@ -0,0 +1,442 @@ +# +# The Python Imaging Library +# $Id$ +# +# JPEG2000 file handling +# +# History: +# 2014-03-12 ajh Created +# 2021-06-30 rogermb Extract dpi information from the 'resc' header box +# +# Copyright (c) 2014 Coriolis Systems Limited +# Copyright (c) 2014 Alastair Houghton +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +import os +import struct +from collections.abc import Callable +from typing import IO, cast + +from . import Image, ImageFile, ImagePalette, _binary + + +class BoxReader: + """ + A small helper class to read fields stored in JPEG2000 header boxes + and to easily step into and read sub-boxes. + """ + + def __init__(self, fp: IO[bytes], length: int = -1) -> None: + self.fp = fp + self.has_length = length >= 0 + self.length = length + self.remaining_in_box = -1 + + def _can_read(self, num_bytes: int) -> bool: + if self.has_length and self.fp.tell() + num_bytes > self.length: + # Outside box: ensure we don't read past the known file length + return False + if self.remaining_in_box >= 0: + # Inside box contents: ensure read does not go past box boundaries + return num_bytes <= self.remaining_in_box + else: + return True # No length known, just read + + def _read_bytes(self, num_bytes: int) -> bytes: + if not self._can_read(num_bytes): + msg = "Not enough data in header" + raise SyntaxError(msg) + + data = self.fp.read(num_bytes) + if len(data) < num_bytes: + msg = f"Expected to read {num_bytes} bytes but only got {len(data)}." + raise OSError(msg) + + if self.remaining_in_box > 0: + self.remaining_in_box -= num_bytes + return data + + def read_fields(self, field_format: str) -> tuple[int | bytes, ...]: + size = struct.calcsize(field_format) + data = self._read_bytes(size) + return struct.unpack(field_format, data) + + def read_boxes(self) -> BoxReader: + size = self.remaining_in_box + data = self._read_bytes(size) + return BoxReader(io.BytesIO(data), size) + + def has_next_box(self) -> bool: + if self.has_length: + return self.fp.tell() + self.remaining_in_box < self.length + else: + return True + + def next_box_type(self) -> bytes: + # Skip the rest of the box if it has not been read + if self.remaining_in_box > 0: + self.fp.seek(self.remaining_in_box, os.SEEK_CUR) + self.remaining_in_box = -1 + + # Read the length and type of the next box + lbox, tbox = cast(tuple[int, bytes], self.read_fields(">I4s")) + if lbox == 1: + lbox = cast(int, self.read_fields(">Q")[0]) + hlen = 16 + else: + hlen = 8 + + if lbox < hlen or not self._can_read(lbox - hlen): + msg = "Invalid header length" + raise SyntaxError(msg) + + self.remaining_in_box = lbox - hlen + return tbox + + +def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]: + """Parse the JPEG 2000 codestream to extract the size and component + count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" + + hdr = fp.read(2) + lsiz = _binary.i16be(hdr) + siz = hdr + fp.read(lsiz - 2) + lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( + ">HHIIIIIIIIH", siz + ) + + size = (xsiz - xosiz, ysiz - yosiz) + if csiz == 1: + ssiz = struct.unpack_from(">B", siz, 38) + if (ssiz[0] & 0x7F) + 1 > 8: + mode = "I;16" + else: + mode = "L" + elif csiz == 2: + mode = "LA" + elif csiz == 3: + mode = "RGB" + elif csiz == 4: + mode = "RGBA" + else: + msg = "unable to determine J2K image mode" + raise SyntaxError(msg) + + return size, mode + + +def _res_to_dpi(num: int, denom: int, exp: int) -> float | None: + """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, + calculated as (num / denom) * 10^exp and stored in dots per meter, + to floating-point dots per inch.""" + if denom == 0: + return None + return (254 * num * (10**exp)) / (10000 * denom) + + +def _parse_jp2_header( + fp: IO[bytes], +) -> tuple[ + tuple[int, int], + str, + str | None, + tuple[float, float] | None, + ImagePalette.ImagePalette | None, +]: + """Parse the JP2 header box to extract size, component count, + color space information, and optionally DPI information, + returning a (size, mode, mimetype, dpi) tuple.""" + + # Find the JP2 header box + reader = BoxReader(fp) + header = None + mimetype = None + while reader.has_next_box(): + tbox = reader.next_box_type() + + if tbox == b"jp2h": + header = reader.read_boxes() + break + elif tbox == b"ftyp": + if reader.read_fields(">4s")[0] == b"jpx ": + mimetype = "image/jpx" + assert header is not None + + size = None + mode = None + bpc = None + nc = None + dpi = None # 2-tuple of DPI info, or None + palette = None + + while header.has_next_box(): + tbox = header.next_box_type() + + if tbox == b"ihdr": + height, width, nc, bpc = header.read_fields(">IIHB") + assert isinstance(height, int) + assert isinstance(width, int) + assert isinstance(bpc, int) + size = (width, height) + if nc == 1 and (bpc & 0x7F) > 8: + mode = "I;16" + elif nc == 1: + mode = "L" + elif nc == 2: + mode = "LA" + elif nc == 3: + mode = "RGB" + elif nc == 4: + mode = "RGBA" + elif tbox == b"colr" and nc == 4: + meth, _, _, enumcs = header.read_fields(">BBBI") + if meth == 1 and enumcs == 12: + mode = "CMYK" + elif tbox == b"pclr" and mode in ("L", "LA"): + ne, npc = header.read_fields(">HB") + assert isinstance(ne, int) + assert isinstance(npc, int) + max_bitdepth = 0 + for bitdepth in header.read_fields(">" + ("B" * npc)): + assert isinstance(bitdepth, int) + if bitdepth > max_bitdepth: + max_bitdepth = bitdepth + if max_bitdepth <= 8: + palette = ImagePalette.ImagePalette("RGBA" if npc == 4 else "RGB") + for i in range(ne): + color: list[int] = [] + for value in header.read_fields(">" + ("B" * npc)): + assert isinstance(value, int) + color.append(value) + palette.getcolor(tuple(color)) + mode = "P" if mode == "L" else "PA" + elif tbox == b"res ": + res = header.read_boxes() + while res.has_next_box(): + tres = res.next_box_type() + if tres == b"resc": + vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") + assert isinstance(vrcn, int) + assert isinstance(vrcd, int) + assert isinstance(hrcn, int) + assert isinstance(hrcd, int) + assert isinstance(vrce, int) + assert isinstance(hrce, int) + hres = _res_to_dpi(hrcn, hrcd, hrce) + vres = _res_to_dpi(vrcn, vrcd, vrce) + if hres is not None and vres is not None: + dpi = (hres, vres) + break + + if size is None or mode is None: + msg = "Malformed JP2 header" + raise SyntaxError(msg) + + return size, mode, mimetype, dpi, palette + + +## +# Image plugin for JPEG2000 images. + + +class Jpeg2KImageFile(ImageFile.ImageFile): + format = "JPEG2000" + format_description = "JPEG 2000 (ISO 15444)" + + def _open(self) -> None: + sig = self.fp.read(4) + if sig == b"\xff\x4f\xff\x51": + self.codec = "j2k" + self._size, self._mode = _parse_codestream(self.fp) + self._parse_comment() + else: + sig = sig + self.fp.read(8) + + if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a": + self.codec = "jp2" + header = _parse_jp2_header(self.fp) + self._size, self._mode, self.custom_mimetype, dpi, self.palette = header + if dpi is not None: + self.info["dpi"] = dpi + if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + self.fp.seek(length - 2, os.SEEK_CUR) + self._parse_comment() + else: + msg = "not a JPEG 2000 file" + raise SyntaxError(msg) + + self._reduce = 0 + self.layers = 0 + + fd = -1 + length = -1 + + try: + fd = self.fp.fileno() + length = os.fstat(fd).st_size + except Exception: + fd = -1 + try: + pos = self.fp.tell() + self.fp.seek(0, io.SEEK_END) + length = self.fp.tell() + self.fp.seek(pos) + except Exception: + length = -1 + + self.tile = [ + ImageFile._Tile( + "jpeg2k", + (0, 0) + self.size, + 0, + (self.codec, self._reduce, self.layers, fd, length), + ) + ] + + def _parse_comment(self) -> None: + while True: + marker = self.fp.read(2) + if not marker: + break + typ = marker[1] + if typ in (0x90, 0xD9): + # Start of tile or end of codestream + break + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + if typ == 0x64: + # Comment + self.info["comment"] = self.fp.read(length - 2)[2:] + break + else: + self.fp.seek(length - 2, os.SEEK_CUR) + + @property # type: ignore[override] + def reduce( + self, + ) -> ( + Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image] + | int + ): + # https://github.com/python-pillow/Pillow/issues/4343 found that the + # new Image 'reduce' method was shadowed by this plugin's 'reduce' + # property. This attempts to allow for both scenarios + return self._reduce or super().reduce + + @reduce.setter + def reduce(self, value: int) -> None: + self._reduce = value + + def load(self) -> Image.core.PixelAccess | None: + if self.tile and self._reduce: + power = 1 << self._reduce + adjust = power >> 1 + self._size = ( + int((self.size[0] + adjust) / power), + int((self.size[1] + adjust) / power), + ) + + # Update the reduce and layers settings + t = self.tile[0] + assert isinstance(t[3], tuple) + t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4]) + self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)] + + return ImageFile.ImageFile.load(self) + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith( + (b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a") + ) + + +# ------------------------------------------------------------ +# Save support + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + # Get the keyword arguments + info = im.encoderinfo + + if isinstance(filename, str): + filename = filename.encode() + if filename.endswith(b".j2k") or info.get("no_jp2", False): + kind = "j2k" + else: + kind = "jp2" + + offset = info.get("offset", None) + tile_offset = info.get("tile_offset", None) + tile_size = info.get("tile_size", None) + quality_mode = info.get("quality_mode", "rates") + quality_layers = info.get("quality_layers", None) + if quality_layers is not None and not ( + isinstance(quality_layers, (list, tuple)) + and all( + isinstance(quality_layer, (int, float)) for quality_layer in quality_layers + ) + ): + msg = "quality_layers must be a sequence of numbers" + raise ValueError(msg) + + num_resolutions = info.get("num_resolutions", 0) + cblk_size = info.get("codeblock_size", None) + precinct_size = info.get("precinct_size", None) + irreversible = info.get("irreversible", False) + progression = info.get("progression", "LRCP") + cinema_mode = info.get("cinema_mode", "no") + mct = info.get("mct", 0) + signed = info.get("signed", False) + comment = info.get("comment") + if isinstance(comment, str): + comment = comment.encode() + plt = info.get("plt", False) + + fd = -1 + if hasattr(fp, "fileno"): + try: + fd = fp.fileno() + except Exception: + fd = -1 + + im.encoderconfig = ( + offset, + tile_offset, + tile_size, + quality_mode, + quality_layers, + num_resolutions, + cblk_size, + precinct_size, + irreversible, + progression, + cinema_mode, + mct, + signed, + fd, + comment, + plt, + ) + + ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)]) + + +# ------------------------------------------------------------ +# Registry stuff + + +Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept) +Image.register_save(Jpeg2KImageFile.format, _save) + +Image.register_extensions( + Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"] +) + +Image.register_mime(Jpeg2KImageFile.format, "image/jp2") diff --git a/python_modules/PIL/JpegImagePlugin.py b/python_modules/PIL/JpegImagePlugin.py new file mode 100644 index 000000000..defe9f773 --- /dev/null +++ b/python_modules/PIL/JpegImagePlugin.py @@ -0,0 +1,902 @@ +# +# The Python Imaging Library. +# $Id$ +# +# JPEG (JFIF) file handling +# +# See "Digital Compression and Coding of Continuous-Tone Still Images, +# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1) +# +# History: +# 1995-09-09 fl Created +# 1995-09-13 fl Added full parser +# 1996-03-25 fl Added hack to use the IJG command line utilities +# 1996-05-05 fl Workaround Photoshop 2.5 CMYK polarity bug +# 1996-05-28 fl Added draft support, JFIF version (0.1) +# 1996-12-30 fl Added encoder options, added progression property (0.2) +# 1997-08-27 fl Save mode 1 images as BW (0.3) +# 1998-07-12 fl Added YCbCr to draft and save methods (0.4) +# 1998-10-19 fl Don't hang on files using 16-bit DQT's (0.4.1) +# 2001-04-16 fl Extract DPI settings from JFIF files (0.4.2) +# 2002-07-01 fl Skip pad bytes before markers; identify Exif files (0.4.3) +# 2003-04-25 fl Added experimental EXIF decoder (0.5) +# 2003-06-06 fl Added experimental EXIF GPSinfo decoder +# 2003-09-13 fl Extract COM markers +# 2009-09-06 fl Added icc_profile support (from Florian Hoech) +# 2009-03-06 fl Changed CMYK handling; always use Adobe polarity (0.6) +# 2009-03-08 fl Added subsampling support (from Justin Huff). +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-1996 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import array +import io +import math +import os +import struct +import subprocess +import sys +import tempfile +import warnings +from typing import IO, Any + +from . import Image, ImageFile +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 +from ._binary import o16be as o16 +from ._deprecate import deprecate +from .JpegPresets import presets + +TYPE_CHECKING = False +if TYPE_CHECKING: + from .MpoImagePlugin import MpoImageFile + +# +# Parser + + +def Skip(self: JpegImageFile, marker: int) -> None: + n = i16(self.fp.read(2)) - 2 + ImageFile._safe_read(self.fp, n) + + +def APP(self: JpegImageFile, marker: int) -> None: + # + # Application marker. Store these in the APP dictionary. + # Also look for well-known application markers. + + n = i16(self.fp.read(2)) - 2 + s = ImageFile._safe_read(self.fp, n) + + app = f"APP{marker & 15}" + + self.app[app] = s # compatibility + self.applist.append((app, s)) + + if marker == 0xFFE0 and s.startswith(b"JFIF"): + # extract JFIF information + self.info["jfif"] = version = i16(s, 5) # version + self.info["jfif_version"] = divmod(version, 256) + # extract JFIF properties + try: + jfif_unit = s[7] + jfif_density = i16(s, 8), i16(s, 10) + except Exception: + pass + else: + if jfif_unit == 1: + self.info["dpi"] = jfif_density + elif jfif_unit == 2: # cm + # 1 dpcm = 2.54 dpi + self.info["dpi"] = tuple(d * 2.54 for d in jfif_density) + self.info["jfif_unit"] = jfif_unit + self.info["jfif_density"] = jfif_density + elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"): + # extract EXIF information + if "exif" in self.info: + self.info["exif"] += s[6:] + else: + self.info["exif"] = s + self._exif_offset = self.fp.tell() - n + 6 + elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"): + self.info["xmp"] = s.split(b"\x00", 1)[1] + elif marker == 0xFFE2 and s.startswith(b"FPXR\0"): + # extract FlashPix information (incomplete) + self.info["flashpix"] = s # FIXME: value will change + elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"): + # Since an ICC profile can be larger than the maximum size of + # a JPEG marker (64K), we need provisions to split it into + # multiple markers. The format defined by the ICC specifies + # one or more APP2 markers containing the following data: + # Identifying string ASCII "ICC_PROFILE\0" (12 bytes) + # Marker sequence number 1, 2, etc (1 byte) + # Number of markers Total of APP2's used (1 byte) + # Profile data (remainder of APP2 data) + # Decoders should use the marker sequence numbers to + # reassemble the profile, rather than assuming that the APP2 + # markers appear in the correct sequence. + self.icclist.append(s) + elif marker == 0xFFED and s.startswith(b"Photoshop 3.0\x00"): + # parse the image resource block + offset = 14 + photoshop = self.info.setdefault("photoshop", {}) + while s[offset : offset + 4] == b"8BIM": + try: + offset += 4 + # resource code + code = i16(s, offset) + offset += 2 + # resource name (usually empty) + name_len = s[offset] + # name = s[offset+1:offset+1+name_len] + offset += 1 + name_len + offset += offset & 1 # align + # resource data block + size = i32(s, offset) + offset += 4 + data = s[offset : offset + size] + if code == 0x03ED: # ResolutionInfo + photoshop[code] = { + "XResolution": i32(data, 0) / 65536, + "DisplayedUnitsX": i16(data, 4), + "YResolution": i32(data, 8) / 65536, + "DisplayedUnitsY": i16(data, 12), + } + else: + photoshop[code] = data + offset += size + offset += offset & 1 # align + except struct.error: + break # insufficient data + + elif marker == 0xFFEE and s.startswith(b"Adobe"): + self.info["adobe"] = i16(s, 5) + # extract Adobe custom properties + try: + adobe_transform = s[11] + except IndexError: + pass + else: + self.info["adobe_transform"] = adobe_transform + elif marker == 0xFFE2 and s.startswith(b"MPF\0"): + # extract MPO information + self.info["mp"] = s[4:] + # offset is current location minus buffer size + # plus constant header size + self.info["mpoffset"] = self.fp.tell() - n + 4 + + +def COM(self: JpegImageFile, marker: int) -> None: + # + # Comment marker. Store these in the APP dictionary. + n = i16(self.fp.read(2)) - 2 + s = ImageFile._safe_read(self.fp, n) + + self.info["comment"] = s + self.app["COM"] = s # compatibility + self.applist.append(("COM", s)) + + +def SOF(self: JpegImageFile, marker: int) -> None: + # + # Start of frame marker. Defines the size and mode of the + # image. JPEG is colour blind, so we use some simple + # heuristics to map the number of layers to an appropriate + # mode. Note that this could be made a bit brighter, by + # looking for JFIF and Adobe APP markers. + + n = i16(self.fp.read(2)) - 2 + s = ImageFile._safe_read(self.fp, n) + self._size = i16(s, 3), i16(s, 1) + + self.bits = s[0] + if self.bits != 8: + msg = f"cannot handle {self.bits}-bit layers" + raise SyntaxError(msg) + + self.layers = s[5] + if self.layers == 1: + self._mode = "L" + elif self.layers == 3: + self._mode = "RGB" + elif self.layers == 4: + self._mode = "CMYK" + else: + msg = f"cannot handle {self.layers}-layer images" + raise SyntaxError(msg) + + if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: + self.info["progressive"] = self.info["progression"] = 1 + + if self.icclist: + # fixup icc profile + self.icclist.sort() # sort by sequence number + if self.icclist[0][13] == len(self.icclist): + profile = [p[14:] for p in self.icclist] + icc_profile = b"".join(profile) + else: + icc_profile = None # wrong number of fragments + self.info["icc_profile"] = icc_profile + self.icclist = [] + + for i in range(6, len(s), 3): + t = s[i : i + 3] + # 4-tuples: id, vsamp, hsamp, qtable + self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2])) + + +def DQT(self: JpegImageFile, marker: int) -> None: + # + # Define quantization table. Note that there might be more + # than one table in each marker. + + # FIXME: The quantization tables can be used to estimate the + # compression quality. + + n = i16(self.fp.read(2)) - 2 + s = ImageFile._safe_read(self.fp, n) + while len(s): + v = s[0] + precision = 1 if (v // 16 == 0) else 2 # in bytes + qt_length = 1 + precision * 64 + if len(s) < qt_length: + msg = "bad quantization table marker" + raise SyntaxError(msg) + data = array.array("B" if precision == 1 else "H", s[1:qt_length]) + if sys.byteorder == "little" and precision > 1: + data.byteswap() # the values are always big-endian + self.quantization[v & 15] = [data[i] for i in zigzag_index] + s = s[qt_length:] + + +# +# JPEG marker table + +MARKER = { + 0xFFC0: ("SOF0", "Baseline DCT", SOF), + 0xFFC1: ("SOF1", "Extended Sequential DCT", SOF), + 0xFFC2: ("SOF2", "Progressive DCT", SOF), + 0xFFC3: ("SOF3", "Spatial lossless", SOF), + 0xFFC4: ("DHT", "Define Huffman table", Skip), + 0xFFC5: ("SOF5", "Differential sequential DCT", SOF), + 0xFFC6: ("SOF6", "Differential progressive DCT", SOF), + 0xFFC7: ("SOF7", "Differential spatial", SOF), + 0xFFC8: ("JPG", "Extension", None), + 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF), + 0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF), + 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF), + 0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip), + 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF), + 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF), + 0xFFCF: ("SOF15", "Differential spatial (AC)", SOF), + 0xFFD0: ("RST0", "Restart 0", None), + 0xFFD1: ("RST1", "Restart 1", None), + 0xFFD2: ("RST2", "Restart 2", None), + 0xFFD3: ("RST3", "Restart 3", None), + 0xFFD4: ("RST4", "Restart 4", None), + 0xFFD5: ("RST5", "Restart 5", None), + 0xFFD6: ("RST6", "Restart 6", None), + 0xFFD7: ("RST7", "Restart 7", None), + 0xFFD8: ("SOI", "Start of image", None), + 0xFFD9: ("EOI", "End of image", None), + 0xFFDA: ("SOS", "Start of scan", Skip), + 0xFFDB: ("DQT", "Define quantization table", DQT), + 0xFFDC: ("DNL", "Define number of lines", Skip), + 0xFFDD: ("DRI", "Define restart interval", Skip), + 0xFFDE: ("DHP", "Define hierarchical progression", SOF), + 0xFFDF: ("EXP", "Expand reference component", Skip), + 0xFFE0: ("APP0", "Application segment 0", APP), + 0xFFE1: ("APP1", "Application segment 1", APP), + 0xFFE2: ("APP2", "Application segment 2", APP), + 0xFFE3: ("APP3", "Application segment 3", APP), + 0xFFE4: ("APP4", "Application segment 4", APP), + 0xFFE5: ("APP5", "Application segment 5", APP), + 0xFFE6: ("APP6", "Application segment 6", APP), + 0xFFE7: ("APP7", "Application segment 7", APP), + 0xFFE8: ("APP8", "Application segment 8", APP), + 0xFFE9: ("APP9", "Application segment 9", APP), + 0xFFEA: ("APP10", "Application segment 10", APP), + 0xFFEB: ("APP11", "Application segment 11", APP), + 0xFFEC: ("APP12", "Application segment 12", APP), + 0xFFED: ("APP13", "Application segment 13", APP), + 0xFFEE: ("APP14", "Application segment 14", APP), + 0xFFEF: ("APP15", "Application segment 15", APP), + 0xFFF0: ("JPG0", "Extension 0", None), + 0xFFF1: ("JPG1", "Extension 1", None), + 0xFFF2: ("JPG2", "Extension 2", None), + 0xFFF3: ("JPG3", "Extension 3", None), + 0xFFF4: ("JPG4", "Extension 4", None), + 0xFFF5: ("JPG5", "Extension 5", None), + 0xFFF6: ("JPG6", "Extension 6", None), + 0xFFF7: ("JPG7", "Extension 7", None), + 0xFFF8: ("JPG8", "Extension 8", None), + 0xFFF9: ("JPG9", "Extension 9", None), + 0xFFFA: ("JPG10", "Extension 10", None), + 0xFFFB: ("JPG11", "Extension 11", None), + 0xFFFC: ("JPG12", "Extension 12", None), + 0xFFFD: ("JPG13", "Extension 13", None), + 0xFFFE: ("COM", "Comment", COM), +} + + +def _accept(prefix: bytes) -> bool: + # Magic number was taken from https://en.wikipedia.org/wiki/JPEG + return prefix.startswith(b"\xff\xd8\xff") + + +## +# Image plugin for JPEG and JFIF images. + + +class JpegImageFile(ImageFile.ImageFile): + format = "JPEG" + format_description = "JPEG (ISO 10918)" + + def _open(self) -> None: + s = self.fp.read(3) + + if not _accept(s): + msg = "not a JPEG file" + raise SyntaxError(msg) + s = b"\xff" + + # Create attributes + self.bits = self.layers = 0 + self._exif_offset = 0 + + # JPEG specifics (internal) + self.layer: list[tuple[int, int, int, int]] = [] + self._huffman_dc: dict[Any, Any] = {} + self._huffman_ac: dict[Any, Any] = {} + self.quantization: dict[int, list[int]] = {} + self.app: dict[str, bytes] = {} # compatibility + self.applist: list[tuple[str, bytes]] = [] + self.icclist: list[bytes] = [] + + while True: + i = s[0] + if i == 0xFF: + s = s + self.fp.read(1) + i = i16(s) + else: + # Skip non-0xFF junk + s = self.fp.read(1) + continue + + if i in MARKER: + name, description, handler = MARKER[i] + if handler is not None: + handler(self, i) + if i == 0xFFDA: # start of scan + rawmode = self.mode + if self.mode == "CMYK": + rawmode = "CMYK;I" # assume adobe conventions + self.tile = [ + ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, "")) + ] + # self.__offset = self.fp.tell() + break + s = self.fp.read(1) + elif i in {0, 0xFFFF}: + # padded marker or junk; move on + s = b"\xff" + elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) + s = self.fp.read(1) + else: + msg = "no marker found" + raise SyntaxError(msg) + + self._read_dpi_from_exif() + + def __getattr__(self, name: str) -> Any: + if name in ("huffman_ac", "huffman_dc"): + deprecate(name, 12) + return getattr(self, "_" + name) + raise AttributeError(name) + + def __getstate__(self) -> list[Any]: + return super().__getstate__() + [self.layers, self.layer] + + def __setstate__(self, state: list[Any]) -> None: + self.layers, self.layer = state[6:] + super().__setstate__(state) + + def load_read(self, read_bytes: int) -> bytes: + """ + internal: read more image data + For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker + so libjpeg can finish decoding + """ + s = self.fp.read(read_bytes) + + if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"): + # Premature EOF. + # Pretend file is finished adding EOI marker + self._ended = True + return b"\xff\xd9" + + return s + + def draft( + self, mode: str | None, size: tuple[int, int] | None + ) -> tuple[str, tuple[int, int, float, float]] | None: + if len(self.tile) != 1: + return None + + # Protect from second call + if self.decoderconfig: + return None + + d, e, o, a = self.tile[0] + scale = 1 + original_size = self.size + + assert isinstance(a, tuple) + if a[0] == "RGB" and mode in ["L", "YCbCr"]: + self._mode = mode + a = mode, "" + + if size: + scale = min(self.size[0] // size[0], self.size[1] // size[1]) + for s in [8, 4, 2, 1]: + if scale >= s: + break + assert e is not None + e = ( + e[0], + e[1], + (e[2] - e[0] + s - 1) // s + e[0], + (e[3] - e[1] + s - 1) // s + e[1], + ) + self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) + scale = s + + self.tile = [ImageFile._Tile(d, e, o, a)] + self.decoderconfig = (scale, 0) + + box = (0, 0, original_size[0] / scale, original_size[1] / scale) + return self.mode, box + + def load_djpeg(self) -> None: + # ALTERNATIVE: handle JPEGs via the IJG command line utilities + + f, path = tempfile.mkstemp() + os.close(f) + if os.path.exists(self.filename): + subprocess.check_call(["djpeg", "-outfile", path, self.filename]) + else: + try: + os.unlink(path) + except OSError: + pass + + msg = "Invalid Filename" + raise ValueError(msg) + + try: + with Image.open(path) as _im: + _im.load() + self.im = _im.im + finally: + try: + os.unlink(path) + except OSError: + pass + + self._mode = self.im.mode + self._size = self.im.size + + self.tile = [] + + def _getexif(self) -> dict[int, Any] | None: + return _getexif(self) + + def _read_dpi_from_exif(self) -> None: + # If DPI isn't in JPEG header, fetch from EXIF + if "dpi" in self.info or "exif" not in self.info: + return + try: + exif = self.getexif() + resolution_unit = exif[0x0128] + x_resolution = exif[0x011A] + try: + dpi = float(x_resolution[0]) / x_resolution[1] + except TypeError: + dpi = x_resolution + if math.isnan(dpi): + msg = "DPI is not a number" + raise ValueError(msg) + if resolution_unit == 3: # cm + # 1 dpcm = 2.54 dpi + dpi *= 2.54 + self.info["dpi"] = dpi, dpi + except ( + struct.error, # truncated EXIF + KeyError, # dpi not included + SyntaxError, # invalid/unreadable EXIF + TypeError, # dpi is an invalid float + ValueError, # dpi is an invalid float + ZeroDivisionError, # invalid dpi rational value + ): + self.info["dpi"] = 72, 72 + + def _getmp(self) -> dict[int, Any] | None: + return _getmp(self) + + +def _getexif(self: JpegImageFile) -> dict[int, Any] | None: + if "exif" not in self.info: + return None + return self.getexif()._get_merged_dict() + + +def _getmp(self: JpegImageFile) -> dict[int, Any] | None: + # Extract MP information. This method was inspired by the "highly + # experimental" _getexif version that's been in use for years now, + # itself based on the ImageFileDirectory class in the TIFF plugin. + + # The MP record essentially consists of a TIFF file embedded in a JPEG + # application marker. + try: + data = self.info["mp"] + except KeyError: + return None + file_contents = io.BytesIO(data) + head = file_contents.read(8) + endianness = ">" if head.startswith(b"\x4d\x4d\x00\x2a") else "<" + # process dictionary + from . import TiffImagePlugin + + try: + info = TiffImagePlugin.ImageFileDirectory_v2(head) + file_contents.seek(info.next) + info.load(file_contents) + mp = dict(info) + except Exception as e: + msg = "malformed MP Index (unreadable directory)" + raise SyntaxError(msg) from e + # it's an error not to have a number of images + try: + quant = mp[0xB001] + except KeyError as e: + msg = "malformed MP Index (no number of images)" + raise SyntaxError(msg) from e + # get MP entries + mpentries = [] + try: + rawmpentries = mp[0xB002] + for entrynum in range(quant): + unpackedentry = struct.unpack_from( + f"{endianness}LLLHH", rawmpentries, entrynum * 16 + ) + labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2") + mpentry = dict(zip(labels, unpackedentry)) + mpentryattr = { + "DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)), + "DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)), + "RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)), + "Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27, + "ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24, + "MPType": mpentry["Attribute"] & 0x00FFFFFF, + } + if mpentryattr["ImageDataFormat"] == 0: + mpentryattr["ImageDataFormat"] = "JPEG" + else: + msg = "unsupported picture format in MPO" + raise SyntaxError(msg) + mptypemap = { + 0x000000: "Undefined", + 0x010001: "Large Thumbnail (VGA Equivalent)", + 0x010002: "Large Thumbnail (Full HD Equivalent)", + 0x020001: "Multi-Frame Image (Panorama)", + 0x020002: "Multi-Frame Image: (Disparity)", + 0x020003: "Multi-Frame Image: (Multi-Angle)", + 0x030000: "Baseline MP Primary Image", + } + mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown") + mpentry["Attribute"] = mpentryattr + mpentries.append(mpentry) + mp[0xB002] = mpentries + except KeyError as e: + msg = "malformed MP Index (bad MP Entry)" + raise SyntaxError(msg) from e + # Next we should try and parse the individual image unique ID list; + # we don't because I've never seen this actually used in a real MPO + # file and so can't test it. + return mp + + +# -------------------------------------------------------------------- +# stuff to save JPEG files + +RAWMODE = { + "1": "L", + "L": "L", + "RGB": "RGB", + "RGBX": "RGB", + "CMYK": "CMYK;I", # assume adobe conventions + "YCbCr": "YCbCr", +} + +# fmt: off +zigzag_index = ( + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63, +) + +samplings = { + (1, 1, 1, 1, 1, 1): 0, + (2, 1, 1, 1, 1, 1): 1, + (2, 2, 1, 1, 1, 1): 2, +} +# fmt: on + + +def get_sampling(im: Image.Image) -> int: + # There's no subsampling when images have only 1 layer + # (grayscale images) or when they are CMYK (4 layers), + # so set subsampling to the default value. + # + # NOTE: currently Pillow can't encode JPEG to YCCK format. + # If YCCK support is added in the future, subsampling code will have + # to be updated (here and in JpegEncode.c) to deal with 4 layers. + if not isinstance(im, JpegImageFile) or im.layers in (1, 4): + return -1 + sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] + return samplings.get(sampling, -1) + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.width == 0 or im.height == 0: + msg = "cannot write empty image as JPEG" + raise ValueError(msg) + + try: + rawmode = RAWMODE[im.mode] + except KeyError as e: + msg = f"cannot write mode {im.mode} as JPEG" + raise OSError(msg) from e + + info = im.encoderinfo + + dpi = [round(x) for x in info.get("dpi", (0, 0))] + + quality = info.get("quality", -1) + subsampling = info.get("subsampling", -1) + qtables = info.get("qtables") + + if quality == "keep": + quality = -1 + subsampling = "keep" + qtables = "keep" + elif quality in presets: + preset = presets[quality] + quality = -1 + subsampling = preset.get("subsampling", -1) + qtables = preset.get("quantization") + elif not isinstance(quality, int): + msg = "Invalid quality setting" + raise ValueError(msg) + else: + if subsampling in presets: + subsampling = presets[subsampling].get("subsampling", -1) + if isinstance(qtables, str) and qtables in presets: + qtables = presets[qtables].get("quantization") + + if subsampling == "4:4:4": + subsampling = 0 + elif subsampling == "4:2:2": + subsampling = 1 + elif subsampling == "4:2:0": + subsampling = 2 + elif subsampling == "4:1:1": + # For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0. + # Set 4:2:0 if someone is still using that value. + subsampling = 2 + elif subsampling == "keep": + if im.format != "JPEG": + msg = "Cannot use 'keep' when original image is not a JPEG" + raise ValueError(msg) + subsampling = get_sampling(im) + + def validate_qtables( + qtables: ( + str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None + ), + ) -> list[list[int]] | None: + if qtables is None: + return qtables + if isinstance(qtables, str): + try: + lines = [ + int(num) + for line in qtables.splitlines() + for num in line.split("#", 1)[0].split() + ] + except ValueError as e: + msg = "Invalid quantization table" + raise ValueError(msg) from e + else: + qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] + if isinstance(qtables, (tuple, list, dict)): + if isinstance(qtables, dict): + qtables = [ + qtables[key] for key in range(len(qtables)) if key in qtables + ] + elif isinstance(qtables, tuple): + qtables = list(qtables) + if not (0 < len(qtables) < 5): + msg = "None or too many quantization tables" + raise ValueError(msg) + for idx, table in enumerate(qtables): + try: + if len(table) != 64: + msg = "Invalid quantization table" + raise TypeError(msg) + table_array = array.array("H", table) + except TypeError as e: + msg = "Invalid quantization table" + raise ValueError(msg) from e + else: + qtables[idx] = list(table_array) + return qtables + + if qtables == "keep": + if im.format != "JPEG": + msg = "Cannot use 'keep' when original image is not a JPEG" + raise ValueError(msg) + qtables = getattr(im, "quantization", None) + qtables = validate_qtables(qtables) + + extra = info.get("extra", b"") + + MAX_BYTES_IN_MARKER = 65533 + if xmp := info.get("xmp"): + overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" + max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len + if len(xmp) > max_data_bytes_in_marker: + msg = "XMP data is too long" + raise ValueError(msg) + size = o16(2 + overhead_len + len(xmp)) + extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp + + if icc_profile := info.get("icc_profile"): + overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) + max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len + markers = [] + while icc_profile: + markers.append(icc_profile[:max_data_bytes_in_marker]) + icc_profile = icc_profile[max_data_bytes_in_marker:] + i = 1 + for marker in markers: + size = o16(2 + overhead_len + len(marker)) + extra += ( + b"\xff\xe2" + + size + + b"ICC_PROFILE\0" + + o8(i) + + o8(len(markers)) + + marker + ) + i += 1 + + comment = info.get("comment", im.info.get("comment")) + + # "progressive" is the official name, but older documentation + # says "progression" + # FIXME: issue a warning if the wrong form is used (post-1.1.7) + progressive = info.get("progressive", False) or info.get("progression", False) + + optimize = info.get("optimize", False) + + exif = info.get("exif", b"") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + if len(exif) > MAX_BYTES_IN_MARKER: + msg = "EXIF data is too long" + raise ValueError(msg) + + # get keyword arguments + im.encoderconfig = ( + quality, + progressive, + info.get("smooth", 0), + optimize, + info.get("keep_rgb", False), + info.get("streamtype", 0), + dpi, + subsampling, + info.get("restart_marker_blocks", 0), + info.get("restart_marker_rows", 0), + qtables, + comment, + extra, + exif, + ) + + # if we optimize, libjpeg needs a buffer big enough to hold the whole image + # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is + # channels*size, this is a value that's been used in a django patch. + # https://github.com/matthewwithanm/django-imagekit/issues/50 + if optimize or progressive: + # CMYK can be bigger + if im.mode == "CMYK": + bufsize = 4 * im.size[0] * im.size[1] + # keep sets quality to -1, but the actual value may be high. + elif quality >= 95 or quality == -1: + bufsize = 2 * im.size[0] * im.size[1] + else: + bufsize = im.size[0] * im.size[1] + if exif: + bufsize += len(exif) + 5 + if extra: + bufsize += len(extra) + 1 + else: + # The EXIF info needs to be written as one block, + APP1, + one spare byte. + # Ensure that our buffer is big enough. Same with the icc_profile block. + bufsize = max(len(exif) + 5, len(extra) + 1) + + ImageFile._save( + im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize + ) + + +def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + # ALTERNATIVE: handle JPEGs via the IJG command line utilities. + tempfile = im._dump() + subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) + try: + os.unlink(tempfile) + except OSError: + pass + + +## +# Factory for making JPEG and MPO instances +def jpeg_factory( + fp: IO[bytes], filename: str | bytes | None = None +) -> JpegImageFile | MpoImageFile: + im = JpegImageFile(fp, filename) + try: + mpheader = im._getmp() + if mpheader is not None and mpheader[45057] > 1: + for segment, content in im.applist: + if segment == "APP1" and b' hdrgm:Version="' in content: + # Ultra HDR images are not yet supported + return im + # It's actually an MPO + from .MpoImagePlugin import MpoImageFile + + # Don't reload everything, just convert it. + im = MpoImageFile.adopt(im, mpheader) + except (TypeError, IndexError): + # It is really a JPEG + pass + except SyntaxError: + warnings.warn( + "Image appears to be a malformed MPO file, it will be " + "interpreted as a base JPEG file" + ) + return im + + +# --------------------------------------------------------------------- +# Registry stuff + +Image.register_open(JpegImageFile.format, jpeg_factory, _accept) +Image.register_save(JpegImageFile.format, _save) + +Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"]) + +Image.register_mime(JpegImageFile.format, "image/jpeg") diff --git a/python_modules/PIL/JpegPresets.py b/python_modules/PIL/JpegPresets.py new file mode 100644 index 000000000..d0e64a35e --- /dev/null +++ b/python_modules/PIL/JpegPresets.py @@ -0,0 +1,242 @@ +""" +JPEG quality settings equivalent to the Photoshop settings. +Can be used when saving JPEG files. + +The following presets are available by default: +``web_low``, ``web_medium``, ``web_high``, ``web_very_high``, ``web_maximum``, +``low``, ``medium``, ``high``, ``maximum``. +More presets can be added to the :py:data:`presets` dict if needed. + +To apply the preset, specify:: + + quality="preset_name" + +To apply only the quantization table:: + + qtables="preset_name" + +To apply only the subsampling setting:: + + subsampling="preset_name" + +Example:: + + im.save("image_name.jpg", quality="web_high") + +Subsampling +----------- + +Subsampling is the practice of encoding images by implementing less resolution +for chroma information than for luma information. +(ref.: https://en.wikipedia.org/wiki/Chroma_subsampling) + +Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and +4:2:0. + +You can get the subsampling of a JPEG with the +:func:`.JpegImagePlugin.get_sampling` function. + +In JPEG compressed data a JPEG marker is used instead of an EXIF tag. +(ref.: https://exiv2.org/tags.html) + + +Quantization tables +------------------- + +They are values use by the DCT (Discrete cosine transform) to remove +*unnecessary* information from the image (the lossy part of the compression). +(ref.: https://en.wikipedia.org/wiki/Quantization_matrix#Quantization_matrices, +https://en.wikipedia.org/wiki/JPEG#Quantization) + +You can get the quantization tables of a JPEG with:: + + im.quantization + +This will return a dict with a number of lists. You can pass this dict +directly as the qtables argument when saving a JPEG. + +The quantization table format in presets is a list with sublists. These formats +are interchangeable. + +Libjpeg ref.: +https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html + +""" + +from __future__ import annotations + +# fmt: off +presets = { + 'web_low': {'subsampling': 2, # "4:2:0" + 'quantization': [ + [20, 16, 25, 39, 50, 46, 62, 68, + 16, 18, 23, 38, 38, 53, 65, 68, + 25, 23, 31, 38, 53, 65, 68, 68, + 39, 38, 38, 53, 65, 68, 68, 68, + 50, 38, 53, 65, 68, 68, 68, 68, + 46, 53, 65, 68, 68, 68, 68, 68, + 62, 65, 68, 68, 68, 68, 68, 68, + 68, 68, 68, 68, 68, 68, 68, 68], + [21, 25, 32, 38, 54, 68, 68, 68, + 25, 28, 24, 38, 54, 68, 68, 68, + 32, 24, 32, 43, 66, 68, 68, 68, + 38, 38, 43, 53, 68, 68, 68, 68, + 54, 54, 66, 68, 68, 68, 68, 68, + 68, 68, 68, 68, 68, 68, 68, 68, + 68, 68, 68, 68, 68, 68, 68, 68, + 68, 68, 68, 68, 68, 68, 68, 68] + ]}, + 'web_medium': {'subsampling': 2, # "4:2:0" + 'quantization': [ + [16, 11, 11, 16, 23, 27, 31, 30, + 11, 12, 12, 15, 20, 23, 23, 30, + 11, 12, 13, 16, 23, 26, 35, 47, + 16, 15, 16, 23, 26, 37, 47, 64, + 23, 20, 23, 26, 39, 51, 64, 64, + 27, 23, 26, 37, 51, 64, 64, 64, + 31, 23, 35, 47, 64, 64, 64, 64, + 30, 30, 47, 64, 64, 64, 64, 64], + [17, 15, 17, 21, 20, 26, 38, 48, + 15, 19, 18, 17, 20, 26, 35, 43, + 17, 18, 20, 22, 26, 30, 46, 53, + 21, 17, 22, 28, 30, 39, 53, 64, + 20, 20, 26, 30, 39, 48, 64, 64, + 26, 26, 30, 39, 48, 63, 64, 64, + 38, 35, 46, 53, 64, 64, 64, 64, + 48, 43, 53, 64, 64, 64, 64, 64] + ]}, + 'web_high': {'subsampling': 0, # "4:4:4" + 'quantization': [ + [6, 4, 4, 6, 9, 11, 12, 16, + 4, 5, 5, 6, 8, 10, 12, 12, + 4, 5, 5, 6, 10, 12, 14, 19, + 6, 6, 6, 11, 12, 15, 19, 28, + 9, 8, 10, 12, 16, 20, 27, 31, + 11, 10, 12, 15, 20, 27, 31, 31, + 12, 12, 14, 19, 27, 31, 31, 31, + 16, 12, 19, 28, 31, 31, 31, 31], + [7, 7, 13, 24, 26, 31, 31, 31, + 7, 12, 16, 21, 31, 31, 31, 31, + 13, 16, 17, 31, 31, 31, 31, 31, + 24, 21, 31, 31, 31, 31, 31, 31, + 26, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31] + ]}, + 'web_very_high': {'subsampling': 0, # "4:4:4" + 'quantization': [ + [2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 4, 5, 7, 9, + 2, 2, 2, 4, 5, 7, 9, 12, + 3, 3, 4, 5, 8, 10, 12, 12, + 4, 4, 5, 7, 10, 12, 12, 12, + 5, 5, 7, 9, 12, 12, 12, 12, + 6, 6, 9, 12, 12, 12, 12, 12], + [3, 3, 5, 9, 13, 15, 15, 15, + 3, 4, 6, 11, 14, 12, 12, 12, + 5, 6, 9, 14, 12, 12, 12, 12, + 9, 11, 14, 12, 12, 12, 12, 12, + 13, 14, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12] + ]}, + 'web_maximum': {'subsampling': 0, # "4:4:4" + 'quantization': [ + [1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 2, + 1, 1, 1, 1, 1, 1, 2, 2, + 1, 1, 1, 1, 1, 2, 2, 3, + 1, 1, 1, 1, 2, 2, 3, 3, + 1, 1, 1, 2, 2, 3, 3, 3, + 1, 1, 2, 2, 3, 3, 3, 3], + [1, 1, 1, 2, 2, 3, 3, 3, + 1, 1, 1, 2, 3, 3, 3, 3, + 1, 1, 1, 3, 3, 3, 3, 3, + 2, 2, 3, 3, 3, 3, 3, 3, + 2, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3] + ]}, + 'low': {'subsampling': 2, # "4:2:0" + 'quantization': [ + [18, 14, 14, 21, 30, 35, 34, 17, + 14, 16, 16, 19, 26, 23, 12, 12, + 14, 16, 17, 21, 23, 12, 12, 12, + 21, 19, 21, 23, 12, 12, 12, 12, + 30, 26, 23, 12, 12, 12, 12, 12, + 35, 23, 12, 12, 12, 12, 12, 12, + 34, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12], + [20, 19, 22, 27, 20, 20, 17, 17, + 19, 25, 23, 14, 14, 12, 12, 12, + 22, 23, 14, 14, 12, 12, 12, 12, + 27, 14, 14, 12, 12, 12, 12, 12, + 20, 14, 12, 12, 12, 12, 12, 12, + 20, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12] + ]}, + 'medium': {'subsampling': 2, # "4:2:0" + 'quantization': [ + [12, 8, 8, 12, 17, 21, 24, 17, + 8, 9, 9, 11, 15, 19, 12, 12, + 8, 9, 10, 12, 19, 12, 12, 12, + 12, 11, 12, 21, 12, 12, 12, 12, + 17, 15, 19, 12, 12, 12, 12, 12, + 21, 19, 12, 12, 12, 12, 12, 12, + 24, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12], + [13, 11, 13, 16, 20, 20, 17, 17, + 11, 14, 14, 14, 14, 12, 12, 12, + 13, 14, 14, 14, 12, 12, 12, 12, + 16, 14, 14, 12, 12, 12, 12, 12, + 20, 14, 12, 12, 12, 12, 12, 12, + 20, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12] + ]}, + 'high': {'subsampling': 0, # "4:4:4" + 'quantization': [ + [6, 4, 4, 6, 9, 11, 12, 16, + 4, 5, 5, 6, 8, 10, 12, 12, + 4, 5, 5, 6, 10, 12, 12, 12, + 6, 6, 6, 11, 12, 12, 12, 12, + 9, 8, 10, 12, 12, 12, 12, 12, + 11, 10, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, + 16, 12, 12, 12, 12, 12, 12, 12], + [7, 7, 13, 24, 20, 20, 17, 17, + 7, 12, 16, 14, 14, 12, 12, 12, + 13, 16, 14, 14, 12, 12, 12, 12, + 24, 14, 14, 12, 12, 12, 12, 12, + 20, 14, 12, 12, 12, 12, 12, 12, + 20, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12, + 17, 12, 12, 12, 12, 12, 12, 12] + ]}, + 'maximum': {'subsampling': 0, # "4:4:4" + 'quantization': [ + [2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 4, 5, 7, 9, + 2, 2, 2, 4, 5, 7, 9, 12, + 3, 3, 4, 5, 8, 10, 12, 12, + 4, 4, 5, 7, 10, 12, 12, 12, + 5, 5, 7, 9, 12, 12, 12, 12, + 6, 6, 9, 12, 12, 12, 12, 12], + [3, 3, 5, 9, 13, 15, 15, 15, + 3, 4, 6, 10, 14, 12, 12, 12, + 5, 6, 9, 14, 12, 12, 12, 12, + 9, 10, 14, 12, 12, 12, 12, 12, + 13, 14, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12, + 15, 12, 12, 12, 12, 12, 12, 12] + ]}, +} +# fmt: on diff --git a/python_modules/PIL/McIdasImagePlugin.py b/python_modules/PIL/McIdasImagePlugin.py new file mode 100644 index 000000000..9a47933b6 --- /dev/null +++ b/python_modules/PIL/McIdasImagePlugin.py @@ -0,0 +1,78 @@ +# +# The Python Imaging Library. +# $Id$ +# +# Basic McIdas support for PIL +# +# History: +# 1997-05-05 fl Created (8-bit images only) +# 2009-03-08 fl Added 16/32-bit support. +# +# Thanks to Richard Jones and Craig Swank for specs and samples. +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import struct + +from . import Image, ImageFile + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04") + + +## +# Image plugin for McIdas area images. + + +class McIdasImageFile(ImageFile.ImageFile): + format = "MCIDAS" + format_description = "McIdas area file" + + def _open(self) -> None: + # parse area file directory + assert self.fp is not None + + s = self.fp.read(256) + if not _accept(s) or len(s) != 256: + msg = "not an McIdas area file" + raise SyntaxError(msg) + + self.area_descriptor_raw = s + self.area_descriptor = w = [0, *struct.unpack("!64i", s)] + + # get mode + if w[11] == 1: + mode = rawmode = "L" + elif w[11] == 2: + mode = rawmode = "I;16B" + elif w[11] == 4: + # FIXME: add memory map support + mode = "I" + rawmode = "I;32B" + else: + msg = "unsupported McIdas format" + raise SyntaxError(msg) + + self._mode = mode + self._size = w[10], w[9] + + offset = w[34] + w[15] + stride = w[15] + w[10] * w[11] * w[14] + + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1)) + ] + + +# -------------------------------------------------------------------- +# registry + +Image.register_open(McIdasImageFile.format, McIdasImageFile, _accept) + +# no default extension diff --git a/python_modules/PIL/MicImagePlugin.py b/python_modules/PIL/MicImagePlugin.py new file mode 100644 index 000000000..9ce38c427 --- /dev/null +++ b/python_modules/PIL/MicImagePlugin.py @@ -0,0 +1,102 @@ +# +# The Python Imaging Library. +# $Id$ +# +# Microsoft Image Composer support for PIL +# +# Notes: +# uses TiffImagePlugin.py to read the actual image streams +# +# History: +# 97-01-20 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import olefile + +from . import Image, TiffImagePlugin + +# +# -------------------------------------------------------------------- + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(olefile.MAGIC) + + +## +# Image plugin for Microsoft's Image Composer file format. + + +class MicImageFile(TiffImagePlugin.TiffImageFile): + format = "MIC" + format_description = "Microsoft Image Composer" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + # read the OLE directory and see if this is a likely + # to be a Microsoft Image Composer file + + try: + self.ole = olefile.OleFileIO(self.fp) + except OSError as e: + msg = "not an MIC file; invalid OLE file" + raise SyntaxError(msg) from e + + # find ACI subfiles with Image members (maybe not the + # best way to identify MIC files, but what the... ;-) + + self.images = [ + path + for path in self.ole.listdir() + if path[1:] and path[0].endswith(".ACI") and path[1] == "Image" + ] + + # if we didn't find any images, this is probably not + # an MIC file. + if not self.images: + msg = "not an MIC file; no image entries" + raise SyntaxError(msg) + + self.frame = -1 + self._n_frames = len(self.images) + self.is_animated = self._n_frames > 1 + + self.__fp = self.fp + self.seek(0) + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + filename = self.images[frame] + self.fp = self.ole.openstream(filename) + + TiffImagePlugin.TiffImageFile._open(self) + + self.frame = frame + + def tell(self) -> int: + return self.frame + + def close(self) -> None: + self.__fp.close() + self.ole.close() + super().close() + + def __exit__(self, *args: object) -> None: + self.__fp.close() + self.ole.close() + super().__exit__() + + +# +# -------------------------------------------------------------------- + +Image.register_open(MicImageFile.format, MicImageFile, _accept) + +Image.register_extension(MicImageFile.format, ".mic") diff --git a/python_modules/PIL/MpegImagePlugin.py b/python_modules/PIL/MpegImagePlugin.py new file mode 100644 index 000000000..47ebe9d62 --- /dev/null +++ b/python_modules/PIL/MpegImagePlugin.py @@ -0,0 +1,84 @@ +# +# The Python Imaging Library. +# $Id$ +# +# MPEG file handling +# +# History: +# 95-09-09 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1995. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image, ImageFile +from ._binary import i8 +from ._typing import SupportsRead + +# +# Bitstream parser + + +class BitStream: + def __init__(self, fp: SupportsRead[bytes]) -> None: + self.fp = fp + self.bits = 0 + self.bitbuffer = 0 + + def next(self) -> int: + return i8(self.fp.read(1)) + + def peek(self, bits: int) -> int: + while self.bits < bits: + self.bitbuffer = (self.bitbuffer << 8) + self.next() + self.bits += 8 + return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 + + def skip(self, bits: int) -> None: + while self.bits < bits: + self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) + self.bits += 8 + self.bits = self.bits - bits + + def read(self, bits: int) -> int: + v = self.peek(bits) + self.bits = self.bits - bits + return v + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"\x00\x00\x01\xb3") + + +## +# Image plugin for MPEG streams. This plugin can identify a stream, +# but it cannot read it. + + +class MpegImageFile(ImageFile.ImageFile): + format = "MPEG" + format_description = "MPEG" + + def _open(self) -> None: + assert self.fp is not None + + s = BitStream(self.fp) + if s.read(32) != 0x1B3: + msg = "not an MPEG file" + raise SyntaxError(msg) + + self._mode = "RGB" + self._size = s.read(12), s.read(12) + + +# -------------------------------------------------------------------- +# Registry stuff + +Image.register_open(MpegImageFile.format, MpegImageFile, _accept) + +Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"]) + +Image.register_mime(MpegImageFile.format, "video/mpeg") diff --git a/python_modules/PIL/MpoImagePlugin.py b/python_modules/PIL/MpoImagePlugin.py new file mode 100644 index 000000000..b1ae07873 --- /dev/null +++ b/python_modules/PIL/MpoImagePlugin.py @@ -0,0 +1,202 @@ +# +# The Python Imaging Library. +# $Id$ +# +# MPO file handling +# +# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the +# Camera & Imaging Products Association) +# +# The multi-picture object combines multiple JPEG images (with a modified EXIF +# data format) into a single file. While it can theoretically be used much like +# a GIF animation, it is commonly used to represent 3D photographs and is (as +# of this writing) the most commonly used format by 3D cameras. +# +# History: +# 2014-03-13 Feneric Created +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +import struct +from typing import IO, Any, cast + +from . import ( + Image, + ImageFile, + ImageSequence, + JpegImagePlugin, + TiffImagePlugin, +) +from ._binary import o32le +from ._util import DeferredError + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + JpegImagePlugin._save(im, fp, filename) + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + append_images = im.encoderinfo.get("append_images", []) + if not append_images and not getattr(im, "is_animated", False): + _save(im, fp, filename) + return + + mpf_offset = 28 + offsets: list[int] = [] + im_sequences = [im, *append_images] + total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences) + for im_sequence in im_sequences: + for im_frame in ImageSequence.Iterator(im_sequence): + if not offsets: + # APP2 marker + ifd_length = 66 + 16 * total + im_frame.encoderinfo["extra"] = ( + b"\xff\xe2" + + struct.pack(">H", 6 + ifd_length) + + b"MPF\0" + + b" " * ifd_length + ) + exif = im_frame.encoderinfo.get("exif") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + im_frame.encoderinfo["exif"] = exif + if exif: + mpf_offset += 4 + len(exif) + + JpegImagePlugin._save(im_frame, fp, filename) + offsets.append(fp.tell()) + else: + encoderinfo = im_frame._attach_default_encoderinfo(im) + im_frame.save(fp, "JPEG") + im_frame.encoderinfo = encoderinfo + offsets.append(fp.tell() - offsets[-1]) + + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[0xB000] = b"0100" + ifd[0xB001] = len(offsets) + + mpentries = b"" + data_offset = 0 + for i, size in enumerate(offsets): + if i == 0: + mptype = 0x030000 # Baseline MP Primary Image + else: + mptype = 0x000000 # Undefined + mpentries += struct.pack(" None: + self.fp.seek(0) # prep the fp in order to pass the JPEG test + JpegImagePlugin.JpegImageFile._open(self) + self._after_jpeg_open() + + def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None: + self.mpinfo = mpheader if mpheader is not None else self._getmp() + if self.mpinfo is None: + msg = "Image appears to be a malformed MPO file" + raise ValueError(msg) + self.n_frames = self.mpinfo[0xB001] + self.__mpoffsets = [ + mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] + ] + self.__mpoffsets[0] = 0 + # Note that the following assertion will only be invalid if something + # gets broken within JpegImagePlugin. + assert self.n_frames == len(self.__mpoffsets) + del self.info["mpoffset"] # no longer needed + self.is_animated = self.n_frames > 1 + self._fp = self.fp # FIXME: hack + self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame + self.__frame = 0 + self.offset = 0 + # for now we can only handle reading and individual frame extraction + self.readonly = 1 + + def load_seek(self, pos: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex + self._fp.seek(pos) + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + if isinstance(self._fp, DeferredError): + raise self._fp.ex + self.fp = self._fp + self.offset = self.__mpoffsets[frame] + + original_exif = self.info.get("exif") + if "exif" in self.info: + del self.info["exif"] + + self.fp.seek(self.offset + 2) # skip SOI marker + if not self.fp.read(2): + msg = "No data found for frame" + raise ValueError(msg) + self.fp.seek(self.offset) + JpegImagePlugin.JpegImageFile._open(self) + if self.info.get("exif") != original_exif: + self._reload_exif() + + self.tile = [ + ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1]) + ] + self.__frame = frame + + def tell(self) -> int: + return self.__frame + + @staticmethod + def adopt( + jpeg_instance: JpegImagePlugin.JpegImageFile, + mpheader: dict[int, Any] | None = None, + ) -> MpoImageFile: + """ + Transform the instance of JpegImageFile into + an instance of MpoImageFile. + After the call, the JpegImageFile is extended + to be an MpoImageFile. + + This is essentially useful when opening a JPEG + file that reveals itself as an MPO, to avoid + double call to _open. + """ + jpeg_instance.__class__ = MpoImageFile + mpo_instance = cast(MpoImageFile, jpeg_instance) + mpo_instance._after_jpeg_open(mpheader) + return mpo_instance + + +# --------------------------------------------------------------------- +# Registry stuff + +# Note that since MPO shares a factory with JPEG, we do not need to do a +# separate registration for it here. +# Image.register_open(MpoImageFile.format, +# JpegImagePlugin.jpeg_factory, _accept) +Image.register_save(MpoImageFile.format, _save) +Image.register_save_all(MpoImageFile.format, _save_all) + +Image.register_extension(MpoImageFile.format, ".mpo") + +Image.register_mime(MpoImageFile.format, "image/mpo") diff --git a/python_modules/PIL/MspImagePlugin.py b/python_modules/PIL/MspImagePlugin.py new file mode 100644 index 000000000..277087a86 --- /dev/null +++ b/python_modules/PIL/MspImagePlugin.py @@ -0,0 +1,200 @@ +# +# The Python Imaging Library. +# +# MSP file handling +# +# This is the format used by the Paint program in Windows 1 and 2. +# +# History: +# 95-09-05 fl Created +# 97-01-03 fl Read/write MSP images +# 17-02-21 es Fixed RLE interpretation +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1995-97. +# Copyright (c) Eric Soroos 2017. +# +# See the README file for information on usage and redistribution. +# +# More info on this format: https://archive.org/details/gg243631 +# Page 313: +# Figure 205. Windows Paint Version 1: "DanM" Format +# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03 +# +# See also: https://www.fileformat.info/format/mspaint/egff.htm +from __future__ import annotations + +import io +import struct +from typing import IO + +from . import Image, ImageFile +from ._binary import i16le as i16 +from ._binary import o16le as o16 + +# +# read MSP files + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith((b"DanM", b"LinS")) + + +## +# Image plugin for Windows MSP images. This plugin supports both +# uncompressed (Windows 1.0). + + +class MspImageFile(ImageFile.ImageFile): + format = "MSP" + format_description = "Windows Paint" + + def _open(self) -> None: + # Header + assert self.fp is not None + + s = self.fp.read(32) + if not _accept(s): + msg = "not an MSP file" + raise SyntaxError(msg) + + # Header checksum + checksum = 0 + for i in range(0, 32, 2): + checksum = checksum ^ i16(s, i) + if checksum != 0: + msg = "bad MSP checksum" + raise SyntaxError(msg) + + self._mode = "1" + self._size = i16(s, 4), i16(s, 6) + + if s.startswith(b"DanM"): + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")] + else: + self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] + + +class MspDecoder(ImageFile.PyDecoder): + # The algo for the MSP decoder is from + # https://www.fileformat.info/format/mspaint/egff.htm + # cc-by-attribution -- That page references is taken from the + # Encyclopedia of Graphics File Formats and is licensed by + # O'Reilly under the Creative Common/Attribution license + # + # For RLE encoded files, the 32byte header is followed by a scan + # line map, encoded as one 16bit word of encoded byte length per + # line. + # + # NOTE: the encoded length of the line can be 0. This was not + # handled in the previous version of this encoder, and there's no + # mention of how to handle it in the documentation. From the few + # examples I've seen, I've assumed that it is a fill of the + # background color, in this case, white. + # + # + # Pseudocode of the decoder: + # Read a BYTE value as the RunType + # If the RunType value is zero + # Read next byte as the RunCount + # Read the next byte as the RunValue + # Write the RunValue byte RunCount times + # If the RunType value is non-zero + # Use this value as the RunCount + # Read and write the next RunCount bytes literally + # + # e.g.: + # 0x00 03 ff 05 00 01 02 03 04 + # would yield the bytes: + # 0xff ff ff 00 01 02 03 04 + # + # which are then interpreted as a bit packed mode '1' image + + _pulls_fd = True + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + + img = io.BytesIO() + blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) + try: + self.fd.seek(32) + rowmap = struct.unpack_from( + f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2) + ) + except struct.error as e: + msg = "Truncated MSP file in row map" + raise OSError(msg) from e + + for x, rowlen in enumerate(rowmap): + try: + if rowlen == 0: + img.write(blank_line) + continue + row = self.fd.read(rowlen) + if len(row) != rowlen: + msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}" + raise OSError(msg) + idx = 0 + while idx < rowlen: + runtype = row[idx] + idx += 1 + if runtype == 0: + (runcount, runval) = struct.unpack_from("Bc", row, idx) + img.write(runval * runcount) + idx += 2 + else: + runcount = runtype + img.write(row[idx : idx + runcount]) + idx += runcount + + except struct.error as e: + msg = f"Corrupted MSP file in row {x}" + raise OSError(msg) from e + + self.set_as_raw(img.getvalue(), "1") + + return -1, 0 + + +Image.register_decoder("MSP", MspDecoder) + + +# +# write MSP files (uncompressed only) + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode != "1": + msg = f"cannot write mode {im.mode} as MSP" + raise OSError(msg) + + # create MSP header + header = [0] * 16 + + header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1 + header[2], header[3] = im.size + header[4], header[5] = 1, 1 + header[6], header[7] = 1, 1 + header[8], header[9] = im.size + + checksum = 0 + for h in header: + checksum = checksum ^ h + header[12] = checksum # FIXME: is this the right field? + + # header + for h in header: + fp.write(o16(h)) + + # image body + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")]) + + +# +# registry + +Image.register_open(MspImageFile.format, MspImageFile, _accept) +Image.register_save(MspImageFile.format, _save) + +Image.register_extension(MspImageFile.format, ".msp") diff --git a/python_modules/PIL/PSDraw.py b/python_modules/PIL/PSDraw.py new file mode 100644 index 000000000..7fd4c5c94 --- /dev/null +++ b/python_modules/PIL/PSDraw.py @@ -0,0 +1,237 @@ +# +# The Python Imaging Library +# $Id$ +# +# Simple PostScript graphics interface +# +# History: +# 1996-04-20 fl Created +# 1999-01-10 fl Added gsave/grestore to image method +# 2005-05-04 fl Fixed floating point issue in image (from Eric Etheridge) +# +# Copyright (c) 1997-2005 by Secret Labs AB. All rights reserved. +# Copyright (c) 1996 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import sys +from typing import IO + +from . import EpsImagePlugin + +TYPE_CHECKING = False + + +## +# Simple PostScript graphics interface. + + +class PSDraw: + """ + Sets up printing to the given file. If ``fp`` is omitted, + ``sys.stdout.buffer`` is assumed. + """ + + def __init__(self, fp: IO[bytes] | None = None) -> None: + if not fp: + fp = sys.stdout.buffer + self.fp = fp + + def begin_document(self, id: str | None = None) -> None: + """Set up printing of a document. (Write PostScript DSC header.)""" + # FIXME: incomplete + self.fp.write( + b"%!PS-Adobe-3.0\n" + b"save\n" + b"/showpage { } def\n" + b"%%EndComments\n" + b"%%BeginDocument\n" + ) + # self.fp.write(ERROR_PS) # debugging! + self.fp.write(EDROFF_PS) + self.fp.write(VDI_PS) + self.fp.write(b"%%EndProlog\n") + self.isofont: dict[bytes, int] = {} + + def end_document(self) -> None: + """Ends printing. (Write PostScript DSC footer.)""" + self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") + if hasattr(self.fp, "flush"): + self.fp.flush() + + def setfont(self, font: str, size: int) -> None: + """ + Selects which font to use. + + :param font: A PostScript font name + :param size: Size in points. + """ + font_bytes = bytes(font, "UTF-8") + if font_bytes not in self.isofont: + # reencode font + self.fp.write( + b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes) + ) + self.isofont[font_bytes] = 1 + # rough + self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font_bytes)) + + def line(self, xy0: tuple[int, int], xy1: tuple[int, int]) -> None: + """ + Draws a line between the two points. Coordinates are given in + PostScript point coordinates (72 points per inch, (0, 0) is the lower + left corner of the page). + """ + self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1)) + + def rectangle(self, box: tuple[int, int, int, int]) -> None: + """ + Draws a rectangle. + + :param box: A tuple of four integers, specifying left, bottom, width and + height. + """ + self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) + + def text(self, xy: tuple[int, int], text: str) -> None: + """ + Draws text at the given position. You must use + :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. + """ + text_bytes = bytes(text, "UTF-8") + text_bytes = b"\\(".join(text_bytes.split(b"(")) + text_bytes = b"\\)".join(text_bytes.split(b")")) + self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,))) + + if TYPE_CHECKING: + from . import Image + + def image( + self, box: tuple[int, int, int, int], im: Image.Image, dpi: int | None = None + ) -> None: + """Draw a PIL image, centered in the given box.""" + # default resolution depends on mode + if not dpi: + if im.mode == "1": + dpi = 200 # fax + else: + dpi = 100 # grayscale + # image size (on paper) + x = im.size[0] * 72 / dpi + y = im.size[1] * 72 / dpi + # max allowed size + xmax = float(box[2] - box[0]) + ymax = float(box[3] - box[1]) + if x > xmax: + y = y * xmax / x + x = xmax + if y > ymax: + x = x * ymax / y + y = ymax + dx = (xmax - x) / 2 + box[0] + dy = (ymax - y) / 2 + box[1] + self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy)) + if (x, y) != im.size: + # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) + sx = x / im.size[0] + sy = y / im.size[1] + self.fp.write(b"%f %f scale\n" % (sx, sy)) + EpsImagePlugin._save(im, self.fp, "", 0) + self.fp.write(b"\ngrestore\n") + + +# -------------------------------------------------------------------- +# PostScript driver + +# +# EDROFF.PS -- PostScript driver for Edroff 2 +# +# History: +# 94-01-25 fl: created (edroff 2.04) +# +# Copyright (c) Fredrik Lundh 1994. +# + + +EDROFF_PS = b"""\ +/S { show } bind def +/P { moveto show } bind def +/M { moveto } bind def +/X { 0 rmoveto } bind def +/Y { 0 exch rmoveto } bind def +/E { findfont + dup maxlength dict begin + { + 1 index /FID ne { def } { pop pop } ifelse + } forall + /Encoding exch def + dup /FontName exch def + currentdict end definefont pop +} bind def +/F { findfont exch scalefont dup setfont + [ exch /setfont cvx ] cvx bind def +} bind def +""" + +# +# VDI.PS -- PostScript driver for VDI meta commands +# +# History: +# 94-01-25 fl: created (edroff 2.04) +# +# Copyright (c) Fredrik Lundh 1994. +# + +VDI_PS = b"""\ +/Vm { moveto } bind def +/Va { newpath arcn stroke } bind def +/Vl { moveto lineto stroke } bind def +/Vc { newpath 0 360 arc closepath } bind def +/Vr { exch dup 0 rlineto + exch dup 0 exch rlineto + exch neg 0 rlineto + 0 exch neg rlineto + setgray fill } bind def +/Tm matrix def +/Ve { Tm currentmatrix pop + translate scale newpath 0 0 .5 0 360 arc closepath + Tm setmatrix +} bind def +/Vf { currentgray exch setgray fill setgray } bind def +""" + +# +# ERROR.PS -- Error handler +# +# History: +# 89-11-21 fl: created (pslist 1.10) +# + +ERROR_PS = b"""\ +/landscape false def +/errorBUF 200 string def +/errorNL { currentpoint 10 sub exch pop 72 exch moveto } def +errordict begin /handleerror { + initmatrix /Courier findfont 10 scalefont setfont + newpath 72 720 moveto $error begin /newerror false def + (PostScript Error) show errorNL errorNL + (Error: ) show + /errorname load errorBUF cvs show errorNL errorNL + (Command: ) show + /command load dup type /stringtype ne { errorBUF cvs } if show + errorNL errorNL + (VMstatus: ) show + vmstatus errorBUF cvs show ( bytes available, ) show + errorBUF cvs show ( bytes used at level ) show + errorBUF cvs show errorNL errorNL + (Operand stargck: ) show errorNL /ostargck load { + dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL + } forall errorNL + (Execution stargck: ) show errorNL /estargck load { + dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL + } forall + end showpage +} def end +""" diff --git a/python_modules/PIL/PaletteFile.py b/python_modules/PIL/PaletteFile.py new file mode 100644 index 000000000..2a26e5d4e --- /dev/null +++ b/python_modules/PIL/PaletteFile.py @@ -0,0 +1,54 @@ +# +# Python Imaging Library +# $Id$ +# +# stuff to read simple, teragon-style palette files +# +# History: +# 97-08-23 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from typing import IO + +from ._binary import o8 + + +class PaletteFile: + """File handler for Teragon-style palette files.""" + + rawmode = "RGB" + + def __init__(self, fp: IO[bytes]) -> None: + palette = [o8(i) * 3 for i in range(256)] + + while True: + s = fp.readline() + + if not s: + break + if s.startswith(b"#"): + continue + if len(s) > 100: + msg = "bad palette file" + raise SyntaxError(msg) + + v = [int(x) for x in s.split()] + try: + [i, r, g, b] = v + except ValueError: + [i, r] = v + g = b = r + + if 0 <= i <= 255: + palette[i] = o8(r) + o8(g) + o8(b) + + self.palette = b"".join(palette) + + def getpalette(self) -> tuple[bytes, str]: + return self.palette, self.rawmode diff --git a/python_modules/PIL/PalmImagePlugin.py b/python_modules/PIL/PalmImagePlugin.py new file mode 100644 index 000000000..15f712908 --- /dev/null +++ b/python_modules/PIL/PalmImagePlugin.py @@ -0,0 +1,217 @@ +# +# The Python Imaging Library. +# $Id$ +# + +## +# Image plugin for Palm pixmap images (output only). +## +from __future__ import annotations + +from typing import IO + +from . import Image, ImageFile +from ._binary import o8 +from ._binary import o16be as o16b + +# fmt: off +_Palm8BitColormapValues = ( + (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255), + (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204), + (255, 153, 204), (255, 102, 204), (255, 51, 204), (255, 0, 204), + (255, 255, 153), (255, 204, 153), (255, 153, 153), (255, 102, 153), + (255, 51, 153), (255, 0, 153), (204, 255, 255), (204, 204, 255), + (204, 153, 255), (204, 102, 255), (204, 51, 255), (204, 0, 255), + (204, 255, 204), (204, 204, 204), (204, 153, 204), (204, 102, 204), + (204, 51, 204), (204, 0, 204), (204, 255, 153), (204, 204, 153), + (204, 153, 153), (204, 102, 153), (204, 51, 153), (204, 0, 153), + (153, 255, 255), (153, 204, 255), (153, 153, 255), (153, 102, 255), + (153, 51, 255), (153, 0, 255), (153, 255, 204), (153, 204, 204), + (153, 153, 204), (153, 102, 204), (153, 51, 204), (153, 0, 204), + (153, 255, 153), (153, 204, 153), (153, 153, 153), (153, 102, 153), + (153, 51, 153), (153, 0, 153), (102, 255, 255), (102, 204, 255), + (102, 153, 255), (102, 102, 255), (102, 51, 255), (102, 0, 255), + (102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204), + (102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153), + (102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153), + (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255), + (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204), + (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204), + (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153), + (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255), + (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255), + (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204), + (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153), + (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153), + (255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102), + (255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51), + (255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51), + (255, 255, 0), (255, 204, 0), (255, 153, 0), (255, 102, 0), + (255, 51, 0), (255, 0, 0), (204, 255, 102), (204, 204, 102), + (204, 153, 102), (204, 102, 102), (204, 51, 102), (204, 0, 102), + (204, 255, 51), (204, 204, 51), (204, 153, 51), (204, 102, 51), + (204, 51, 51), (204, 0, 51), (204, 255, 0), (204, 204, 0), + (204, 153, 0), (204, 102, 0), (204, 51, 0), (204, 0, 0), + (153, 255, 102), (153, 204, 102), (153, 153, 102), (153, 102, 102), + (153, 51, 102), (153, 0, 102), (153, 255, 51), (153, 204, 51), + (153, 153, 51), (153, 102, 51), (153, 51, 51), (153, 0, 51), + (153, 255, 0), (153, 204, 0), (153, 153, 0), (153, 102, 0), + (153, 51, 0), (153, 0, 0), (102, 255, 102), (102, 204, 102), + (102, 153, 102), (102, 102, 102), (102, 51, 102), (102, 0, 102), + (102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51), + (102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0), + (102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0), + (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102), + (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51), + (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51), + (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0), + (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102), + (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102), + (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51), + (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0), + (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17), + (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119), + (136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221), + (238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128), + (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) +# fmt: on + + +# so build a prototype image to be used for palette resampling +def build_prototype_image() -> Image.Image: + image = Image.new("L", (1, len(_Palm8BitColormapValues))) + image.putdata(list(range(len(_Palm8BitColormapValues)))) + palettedata: tuple[int, ...] = () + for colormapValue in _Palm8BitColormapValues: + palettedata += colormapValue + palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) + image.putpalette(palettedata) + return image + + +Palm8BitColormapImage = build_prototype_image() + +# OK, we now have in Palm8BitColormapImage, +# a "P"-mode image with the right palette +# +# -------------------------------------------------------------------- + +_FLAGS = {"custom-colormap": 0x4000, "is-compressed": 0x8000, "has-transparent": 0x2000} + +_COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} + + +# +# -------------------------------------------------------------------- + +## +# (Internal) Image save plugin for the Palm format. + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode == "P": + rawmode = "P" + bpp = 8 + version = 1 + + elif im.mode == "L": + if im.encoderinfo.get("bpp") in (1, 2, 4): + # this is 8-bit grayscale, so we shift it to get the high-order bits, + # and invert it because + # Palm does grayscale from white (0) to black (1) + bpp = im.encoderinfo["bpp"] + maxval = (1 << bpp) - 1 + shift = 8 - bpp + im = im.point(lambda x: maxval - (x >> shift)) + elif im.info.get("bpp") in (1, 2, 4): + # here we assume that even though the inherent mode is 8-bit grayscale, + # only the lower bpp bits are significant. + # We invert them to match the Palm. + bpp = im.info["bpp"] + maxval = (1 << bpp) - 1 + im = im.point(lambda x: maxval - (x & maxval)) + else: + msg = f"cannot write mode {im.mode} as Palm" + raise OSError(msg) + + # we ignore the palette here + im._mode = "P" + rawmode = f"P;{bpp}" + version = 1 + + elif im.mode == "1": + # monochrome -- write it inverted, as is the Palm standard + rawmode = "1;I" + bpp = 1 + version = 0 + + else: + msg = f"cannot write mode {im.mode} as Palm" + raise OSError(msg) + + # + # make sure image data is available + im.load() + + # write header + + cols = im.size[0] + rows = im.size[1] + + rowbytes = int((cols + (16 // bpp - 1)) / (16 // bpp)) * 2 + transparent_index = 0 + compression_type = _COMPRESSION_TYPES["none"] + + flags = 0 + if im.mode == "P": + flags |= _FLAGS["custom-colormap"] + colormap = im.im.getpalette() + colors = len(colormap) // 3 + colormapsize = 4 * colors + 2 + else: + colormapsize = 0 + + if "offset" in im.info: + offset = (rowbytes * rows + 16 + 3 + colormapsize) // 4 + else: + offset = 0 + + fp.write(o16b(cols) + o16b(rows) + o16b(rowbytes) + o16b(flags)) + fp.write(o8(bpp)) + fp.write(o8(version)) + fp.write(o16b(offset)) + fp.write(o8(transparent_index)) + fp.write(o8(compression_type)) + fp.write(o16b(0)) # reserved by Palm + + # now write colormap if necessary + + if colormapsize: + fp.write(o16b(colors)) + for i in range(colors): + fp.write(o8(i)) + fp.write(colormap[3 * i : 3 * i + 3]) + + # now convert data to raw form + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))] + ) + + if hasattr(fp, "flush"): + fp.flush() + + +# +# -------------------------------------------------------------------- + +Image.register_save("Palm", _save) + +Image.register_extension("Palm", ".palm") + +Image.register_mime("Palm", "image/palm") diff --git a/python_modules/PIL/PcdImagePlugin.py b/python_modules/PIL/PcdImagePlugin.py new file mode 100644 index 000000000..3aa249988 --- /dev/null +++ b/python_modules/PIL/PcdImagePlugin.py @@ -0,0 +1,64 @@ +# +# The Python Imaging Library. +# $Id$ +# +# PCD file handling +# +# History: +# 96-05-10 fl Created +# 96-05-27 fl Added draft mode (128x192, 256x384) +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image, ImageFile + +## +# Image plugin for PhotoCD images. This plugin only reads the 768x512 +# image from the file; higher resolutions are encoded in a proprietary +# encoding. + + +class PcdImageFile(ImageFile.ImageFile): + format = "PCD" + format_description = "Kodak PhotoCD" + + def _open(self) -> None: + # rough + assert self.fp is not None + + self.fp.seek(2048) + s = self.fp.read(2048) + + if not s.startswith(b"PCD_"): + msg = "not a PCD file" + raise SyntaxError(msg) + + orientation = s[1538] & 3 + self.tile_post_rotate = None + if orientation == 1: + self.tile_post_rotate = 90 + elif orientation == 3: + self.tile_post_rotate = -90 + + self._mode = "RGB" + self._size = 768, 512 # FIXME: not correct for rotated images! + self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)] + + def load_end(self) -> None: + if self.tile_post_rotate: + # Handle rotated PCDs + self.im = self.im.rotate(self.tile_post_rotate) + self._size = self.im.size + + +# +# registry + +Image.register_open(PcdImageFile.format, PcdImageFile) + +Image.register_extension(PcdImageFile.format, ".pcd") diff --git a/python_modules/PIL/PcfFontFile.py b/python_modules/PIL/PcfFontFile.py new file mode 100644 index 000000000..0d1968b14 --- /dev/null +++ b/python_modules/PIL/PcfFontFile.py @@ -0,0 +1,254 @@ +# +# THIS IS WORK IN PROGRESS +# +# The Python Imaging Library +# $Id$ +# +# portable compiled font file parser +# +# history: +# 1997-08-19 fl created +# 2003-09-13 fl fixed loading of unicode fonts +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1997-2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +from typing import BinaryIO, Callable + +from . import FontFile, Image +from ._binary import i8 +from ._binary import i16be as b16 +from ._binary import i16le as l16 +from ._binary import i32be as b32 +from ._binary import i32le as l32 + +# -------------------------------------------------------------------- +# declarations + +PCF_MAGIC = 0x70636601 # "\x01fcp" + +PCF_PROPERTIES = 1 << 0 +PCF_ACCELERATORS = 1 << 1 +PCF_METRICS = 1 << 2 +PCF_BITMAPS = 1 << 3 +PCF_INK_METRICS = 1 << 4 +PCF_BDF_ENCODINGS = 1 << 5 +PCF_SWIDTHS = 1 << 6 +PCF_GLYPH_NAMES = 1 << 7 +PCF_BDF_ACCELERATORS = 1 << 8 + +BYTES_PER_ROW: list[Callable[[int], int]] = [ + lambda bits: ((bits + 7) >> 3), + lambda bits: ((bits + 15) >> 3) & ~1, + lambda bits: ((bits + 31) >> 3) & ~3, + lambda bits: ((bits + 63) >> 3) & ~7, +] + + +def sz(s: bytes, o: int) -> bytes: + return s[o : s.index(b"\0", o)] + + +class PcfFontFile(FontFile.FontFile): + """Font file plugin for the X11 PCF format.""" + + name = "name" + + def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"): + self.charset_encoding = charset_encoding + + magic = l32(fp.read(4)) + if magic != PCF_MAGIC: + msg = "not a PCF file" + raise SyntaxError(msg) + + super().__init__() + + count = l32(fp.read(4)) + self.toc = {} + for i in range(count): + type = l32(fp.read(4)) + self.toc[type] = l32(fp.read(4)), l32(fp.read(4)), l32(fp.read(4)) + + self.fp = fp + + self.info = self._load_properties() + + metrics = self._load_metrics() + bitmaps = self._load_bitmaps(metrics) + encoding = self._load_encoding() + + # + # create glyph structure + + for ch, ix in enumerate(encoding): + if ix is not None: + ( + xsize, + ysize, + left, + right, + width, + ascent, + descent, + attributes, + ) = metrics[ix] + self.glyph[ch] = ( + (width, 0), + (left, descent - ysize, xsize + left, descent), + (0, 0, xsize, ysize), + bitmaps[ix], + ) + + def _getformat( + self, tag: int + ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]: + format, size, offset = self.toc[tag] + + fp = self.fp + fp.seek(offset) + + format = l32(fp.read(4)) + + if format & 4: + i16, i32 = b16, b32 + else: + i16, i32 = l16, l32 + + return fp, format, i16, i32 + + def _load_properties(self) -> dict[bytes, bytes | int]: + # + # font properties + + properties = {} + + fp, format, i16, i32 = self._getformat(PCF_PROPERTIES) + + nprops = i32(fp.read(4)) + + # read property description + p = [(i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))) for _ in range(nprops)] + + if nprops & 3: + fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad + + data = fp.read(i32(fp.read(4))) + + for k, s, v in p: + property_value: bytes | int = sz(data, v) if s else v + properties[sz(data, k)] = property_value + + return properties + + def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]: + # + # font metrics + + metrics: list[tuple[int, int, int, int, int, int, int, int]] = [] + + fp, format, i16, i32 = self._getformat(PCF_METRICS) + + append = metrics.append + + if (format & 0xFF00) == 0x100: + # "compressed" metrics + for i in range(i16(fp.read(2))): + left = i8(fp.read(1)) - 128 + right = i8(fp.read(1)) - 128 + width = i8(fp.read(1)) - 128 + ascent = i8(fp.read(1)) - 128 + descent = i8(fp.read(1)) - 128 + xsize = right - left + ysize = ascent + descent + append((xsize, ysize, left, right, width, ascent, descent, 0)) + + else: + # "jumbo" metrics + for i in range(i32(fp.read(4))): + left = i16(fp.read(2)) + right = i16(fp.read(2)) + width = i16(fp.read(2)) + ascent = i16(fp.read(2)) + descent = i16(fp.read(2)) + attributes = i16(fp.read(2)) + xsize = right - left + ysize = ascent + descent + append((xsize, ysize, left, right, width, ascent, descent, attributes)) + + return metrics + + def _load_bitmaps( + self, metrics: list[tuple[int, int, int, int, int, int, int, int]] + ) -> list[Image.Image]: + # + # bitmap data + + fp, format, i16, i32 = self._getformat(PCF_BITMAPS) + + nbitmaps = i32(fp.read(4)) + + if nbitmaps != len(metrics): + msg = "Wrong number of bitmaps" + raise OSError(msg) + + offsets = [i32(fp.read(4)) for _ in range(nbitmaps)] + + bitmap_sizes = [i32(fp.read(4)) for _ in range(4)] + + # byteorder = format & 4 # non-zero => MSB + bitorder = format & 8 # non-zero => MSB + padindex = format & 3 + + bitmapsize = bitmap_sizes[padindex] + offsets.append(bitmapsize) + + data = fp.read(bitmapsize) + + pad = BYTES_PER_ROW[padindex] + mode = "1;R" + if bitorder: + mode = "1" + + bitmaps = [] + for i in range(nbitmaps): + xsize, ysize = metrics[i][:2] + b, e = offsets[i : i + 2] + bitmaps.append( + Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) + ) + + return bitmaps + + def _load_encoding(self) -> list[int | None]: + fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) + + first_col, last_col = i16(fp.read(2)), i16(fp.read(2)) + first_row, last_row = i16(fp.read(2)), i16(fp.read(2)) + + i16(fp.read(2)) # default + + nencoding = (last_col - first_col + 1) * (last_row - first_row + 1) + + # map character code to bitmap index + encoding: list[int | None] = [None] * min(256, nencoding) + + encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)] + + for i in range(first_col, len(encoding)): + try: + encoding_offset = encoding_offsets[ + ord(bytearray([i]).decode(self.charset_encoding)) + ] + if encoding_offset != 0xFFFF: + encoding[i] = encoding_offset + except UnicodeDecodeError: + # character is not supported in selected encoding + pass + + return encoding diff --git a/python_modules/PIL/PcxImagePlugin.py b/python_modules/PIL/PcxImagePlugin.py new file mode 100644 index 000000000..458d586c4 --- /dev/null +++ b/python_modules/PIL/PcxImagePlugin.py @@ -0,0 +1,228 @@ +# +# The Python Imaging Library. +# $Id$ +# +# PCX file handling +# +# This format was originally used by ZSoft's popular PaintBrush +# program for the IBM PC. It is also supported by many MS-DOS and +# Windows applications, including the Windows PaintBrush program in +# Windows 3. +# +# history: +# 1995-09-01 fl Created +# 1996-05-20 fl Fixed RGB support +# 1997-01-03 fl Fixed 2-bit and 4-bit support +# 1999-02-03 fl Fixed 8-bit support (broken in 1.0b1) +# 1999-02-07 fl Added write support +# 2002-06-09 fl Made 2-bit and 4-bit support a bit more robust +# 2002-07-30 fl Seek from to current position, not beginning of file +# 2003-06-03 fl Extract DPI settings (info["dpi"]) +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +import logging +from typing import IO + +from . import Image, ImageFile, ImagePalette +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 + +logger = logging.getLogger(__name__) + + +def _accept(prefix: bytes) -> bool: + return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] + + +## +# Image plugin for Paintbrush images. + + +class PcxImageFile(ImageFile.ImageFile): + format = "PCX" + format_description = "Paintbrush" + + def _open(self) -> None: + # header + assert self.fp is not None + + s = self.fp.read(68) + if not _accept(s): + msg = "not a PCX file" + raise SyntaxError(msg) + + # image + bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1 + if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: + msg = "bad PCX image size" + raise SyntaxError(msg) + logger.debug("BBox: %s %s %s %s", *bbox) + + offset = self.fp.tell() + 60 + + # format + version = s[1] + bits = s[3] + planes = s[65] + provided_stride = i16(s, 66) + logger.debug( + "PCX version %s, bits %s, planes %s, stride %s", + version, + bits, + planes, + provided_stride, + ) + + self.info["dpi"] = i16(s, 12), i16(s, 14) + + if bits == 1 and planes == 1: + mode = rawmode = "1" + + elif bits == 1 and planes in (2, 4): + mode = "P" + rawmode = f"P;{planes}L" + self.palette = ImagePalette.raw("RGB", s[16:64]) + + elif version == 5 and bits == 8 and planes == 1: + mode = rawmode = "L" + # FIXME: hey, this doesn't work with the incremental loader !!! + self.fp.seek(-769, io.SEEK_END) + s = self.fp.read(769) + if len(s) == 769 and s[0] == 12: + # check if the palette is linear grayscale + for i in range(256): + if s[i * 3 + 1 : i * 3 + 4] != o8(i) * 3: + mode = rawmode = "P" + break + if mode == "P": + self.palette = ImagePalette.raw("RGB", s[1:]) + + elif version == 5 and bits == 8 and planes == 3: + mode = "RGB" + rawmode = "RGB;L" + + else: + msg = "unknown PCX mode" + raise OSError(msg) + + self._mode = mode + self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] + + # Don't trust the passed in stride. + # Calculate the approximate position for ourselves. + # CVE-2020-35653 + stride = (self._size[0] * bits + 7) // 8 + + # While the specification states that this must be even, + # not all images follow this + if provided_stride != stride: + stride += stride % 2 + + bbox = (0, 0) + self.size + logger.debug("size: %sx%s", *self.size) + + self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))] + + +# -------------------------------------------------------------------- +# save PCX files + + +SAVE = { + # mode: (version, bits, planes, raw mode) + "1": (2, 1, 1, "1"), + "L": (5, 8, 1, "L"), + "P": (5, 8, 1, "P"), + "RGB": (5, 8, 3, "RGB;L"), +} + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + try: + version, bits, planes, rawmode = SAVE[im.mode] + except KeyError as e: + msg = f"Cannot save {im.mode} images as PCX" + raise ValueError(msg) from e + + # bytes per plane + stride = (im.size[0] * bits + 7) // 8 + # stride should be even + stride += stride % 2 + # Stride needs to be kept in sync with the PcxEncode.c version. + # Ideally it should be passed in in the state, but the bytes value + # gets overwritten. + + logger.debug( + "PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", + im.size[0], + bits, + stride, + ) + + # under windows, we could determine the current screen size with + # "Image.core.display_mode()[1]", but I think that's overkill... + + screen = im.size + + dpi = 100, 100 + + # PCX header + fp.write( + o8(10) + + o8(version) + + o8(1) + + o8(bits) + + o16(0) + + o16(0) + + o16(im.size[0] - 1) + + o16(im.size[1] - 1) + + o16(dpi[0]) + + o16(dpi[1]) + + b"\0" * 24 + + b"\xff" * 24 + + b"\0" + + o8(planes) + + o16(stride) + + o16(1) + + o16(screen[0]) + + o16(screen[1]) + + b"\0" * 54 + ) + + assert fp.tell() == 128 + + ImageFile._save( + im, fp, [ImageFile._Tile("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))] + ) + + if im.mode == "P": + # colour palette + fp.write(o8(12)) + palette = im.im.getpalette("RGB", "RGB") + palette += b"\x00" * (768 - len(palette)) + fp.write(palette) # 768 bytes + elif im.mode == "L": + # grayscale palette + fp.write(o8(12)) + for i in range(256): + fp.write(o8(i) * 3) + + +# -------------------------------------------------------------------- +# registry + + +Image.register_open(PcxImageFile.format, PcxImageFile, _accept) +Image.register_save(PcxImageFile.format, _save) + +Image.register_extension(PcxImageFile.format, ".pcx") + +Image.register_mime(PcxImageFile.format, "image/x-pcx") diff --git a/python_modules/PIL/PdfImagePlugin.py b/python_modules/PIL/PdfImagePlugin.py new file mode 100644 index 000000000..e9c20ddc1 --- /dev/null +++ b/python_modules/PIL/PdfImagePlugin.py @@ -0,0 +1,311 @@ +# +# The Python Imaging Library. +# $Id$ +# +# PDF (Acrobat) file handling +# +# History: +# 1996-07-16 fl Created +# 1997-01-18 fl Fixed header +# 2004-02-21 fl Fixes for 1/L/CMYK images, etc. +# 2004-02-24 fl Fixes for 1 and P images. +# +# Copyright (c) 1997-2004 by Secret Labs AB. All rights reserved. +# Copyright (c) 1996-1997 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +## +# Image plugin for PDF images (output only). +## +from __future__ import annotations + +import io +import math +import os +import time +from typing import IO, Any + +from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features + +# +# -------------------------------------------------------------------- + +# object ids: +# 1. catalogue +# 2. pages +# 3. image +# 4. page +# 5. page contents + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +## +# (Internal) Image save plugin for the PDF format. + + +def _write_image( + im: Image.Image, + filename: str | bytes, + existing_pdf: PdfParser.PdfParser, + image_refs: list[PdfParser.IndirectReference], +) -> tuple[PdfParser.IndirectReference, str]: + # FIXME: Should replace ASCIIHexDecode with RunLengthDecode + # (packbits) or LZWDecode (tiff/lzw compression). Note that + # PDF 1.2 also supports Flatedecode (zip compression). + + params = None + decode = None + + # + # Get image characteristics + + width, height = im.size + + dict_obj: dict[str, Any] = {"BitsPerComponent": 8} + if im.mode == "1": + if features.check("libtiff"): + decode_filter = "CCITTFaxDecode" + dict_obj["BitsPerComponent"] = 1 + params = PdfParser.PdfArray( + [ + PdfParser.PdfDict( + { + "K": -1, + "BlackIs1": True, + "Columns": width, + "Rows": height, + } + ) + ] + ) + else: + decode_filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale + elif im.mode == "L": + decode_filter = "DCTDecode" + # params = f"<< /Predictor 15 /Columns {width-2} >>" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale + elif im.mode == "LA": + decode_filter = "JPXDecode" + # params = f"<< /Predictor 15 /Columns {width-2} >>" + procset = "ImageB" # grayscale + dict_obj["SMaskInData"] = 1 + elif im.mode == "P": + decode_filter = "ASCIIHexDecode" + palette = im.getpalette() + assert palette is not None + dict_obj["ColorSpace"] = [ + PdfParser.PdfName("Indexed"), + PdfParser.PdfName("DeviceRGB"), + len(palette) // 3 - 1, + PdfParser.PdfBinary(palette), + ] + procset = "ImageI" # indexed color + + if "transparency" in im.info: + smask = im.convert("LA").getchannel("A") + smask.encoderinfo = {} + + image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] + dict_obj["SMask"] = image_ref + elif im.mode == "RGB": + decode_filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") + procset = "ImageC" # color images + elif im.mode == "RGBA": + decode_filter = "JPXDecode" + procset = "ImageC" # color images + dict_obj["SMaskInData"] = 1 + elif im.mode == "CMYK": + decode_filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") + procset = "ImageC" # color images + decode = [1, 0, 1, 0, 1, 0, 1, 0] + else: + msg = f"cannot save mode {im.mode}" + raise ValueError(msg) + + # + # image + + op = io.BytesIO() + + if decode_filter == "ASCIIHexDecode": + ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)]) + elif decode_filter == "CCITTFaxDecode": + im.save( + op, + "TIFF", + compression="group4", + # use a single strip + strip_size=math.ceil(width / 8) * height, + ) + elif decode_filter == "DCTDecode": + Image.SAVE["JPEG"](im, op, filename) + elif decode_filter == "JPXDecode": + del dict_obj["BitsPerComponent"] + Image.SAVE["JPEG2000"](im, op, filename) + else: + msg = f"unsupported PDF filter ({decode_filter})" + raise ValueError(msg) + + stream = op.getvalue() + filter: PdfParser.PdfArray | PdfParser.PdfName + if decode_filter == "CCITTFaxDecode": + stream = stream[8:] + filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)]) + else: + filter = PdfParser.PdfName(decode_filter) + + image_ref = image_refs.pop(0) + existing_pdf.write_obj( + image_ref, + stream=stream, + Type=PdfParser.PdfName("XObject"), + Subtype=PdfParser.PdfName("Image"), + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, + Filter=filter, + Decode=decode, + DecodeParms=params, + **dict_obj, + ) + + return image_ref, procset + + +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: + is_appending = im.encoderinfo.get("append", False) + filename_str = filename.decode() if isinstance(filename, bytes) else filename + if is_appending: + existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="r+b") + else: + existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="w+b") + + dpi = im.encoderinfo.get("dpi") + if dpi: + x_resolution = dpi[0] + y_resolution = dpi[1] + else: + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) + + info = { + "title": ( + None if is_appending else os.path.splitext(os.path.basename(filename))[0] + ), + "author": None, + "subject": None, + "keywords": None, + "creator": None, + "producer": None, + "creationDate": None if is_appending else time.gmtime(), + "modDate": None if is_appending else time.gmtime(), + } + for k, default in info.items(): + v = im.encoderinfo.get(k) if k in im.encoderinfo else default + if v: + existing_pdf.info[k[0].upper() + k[1:]] = v + + # + # make sure image data is available + im.load() + + existing_pdf.start_writing() + existing_pdf.write_header() + existing_pdf.write_comment(f"created by Pillow {__version__} PDF driver") + + # + # pages + ims = [im] + if save_all: + append_images = im.encoderinfo.get("append_images", []) + for append_im in append_images: + append_im.encoderinfo = im.encoderinfo.copy() + ims.append(append_im) + number_of_pages = 0 + image_refs = [] + page_refs = [] + contents_refs = [] + for im in ims: + im_number_of_pages = 1 + if save_all: + im_number_of_pages = getattr(im, "n_frames", 1) + number_of_pages += im_number_of_pages + for i in range(im_number_of_pages): + image_refs.append(existing_pdf.next_object_id(0)) + if im.mode == "P" and "transparency" in im.info: + image_refs.append(existing_pdf.next_object_id(0)) + + page_refs.append(existing_pdf.next_object_id(0)) + contents_refs.append(existing_pdf.next_object_id(0)) + existing_pdf.pages.append(page_refs[-1]) + + # + # catalog and list of pages + existing_pdf.write_catalog() + + page_number = 0 + for im_sequence in ims: + im_pages: ImageSequence.Iterator | list[Image.Image] = ( + ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] + ) + for im in im_pages: + image_ref, procset = _write_image(im, filename, existing_pdf, image_refs) + + # + # page + + existing_pdf.write_page( + page_refs[page_number], + Resources=PdfParser.PdfDict( + ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], + XObject=PdfParser.PdfDict(image=image_ref), + ), + MediaBox=[ + 0, + 0, + im.width * 72.0 / x_resolution, + im.height * 72.0 / y_resolution, + ], + Contents=contents_refs[page_number], + ) + + # + # page contents + + page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( + im.width * 72.0 / x_resolution, + im.height * 72.0 / y_resolution, + ) + + existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) + + page_number += 1 + + # + # trailer + existing_pdf.write_xref_and_trailer() + if hasattr(fp, "flush"): + fp.flush() + existing_pdf.close() + + +# +# -------------------------------------------------------------------- + + +Image.register_save("PDF", _save) +Image.register_save_all("PDF", _save_all) + +Image.register_extension("PDF", ".pdf") + +Image.register_mime("PDF", "application/pdf") diff --git a/python_modules/PIL/PdfParser.py b/python_modules/PIL/PdfParser.py new file mode 100644 index 000000000..73d8c21c0 --- /dev/null +++ b/python_modules/PIL/PdfParser.py @@ -0,0 +1,1074 @@ +from __future__ import annotations + +import calendar +import codecs +import collections +import mmap +import os +import re +import time +import zlib +from typing import IO, Any, NamedTuple, Union + + +# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set +# on page 656 +def encode_text(s: str) -> bytes: + return codecs.BOM_UTF16_BE + s.encode("utf_16_be") + + +PDFDocEncoding = { + 0x16: "\u0017", + 0x18: "\u02d8", + 0x19: "\u02c7", + 0x1A: "\u02c6", + 0x1B: "\u02d9", + 0x1C: "\u02dd", + 0x1D: "\u02db", + 0x1E: "\u02da", + 0x1F: "\u02dc", + 0x80: "\u2022", + 0x81: "\u2020", + 0x82: "\u2021", + 0x83: "\u2026", + 0x84: "\u2014", + 0x85: "\u2013", + 0x86: "\u0192", + 0x87: "\u2044", + 0x88: "\u2039", + 0x89: "\u203a", + 0x8A: "\u2212", + 0x8B: "\u2030", + 0x8C: "\u201e", + 0x8D: "\u201c", + 0x8E: "\u201d", + 0x8F: "\u2018", + 0x90: "\u2019", + 0x91: "\u201a", + 0x92: "\u2122", + 0x93: "\ufb01", + 0x94: "\ufb02", + 0x95: "\u0141", + 0x96: "\u0152", + 0x97: "\u0160", + 0x98: "\u0178", + 0x99: "\u017d", + 0x9A: "\u0131", + 0x9B: "\u0142", + 0x9C: "\u0153", + 0x9D: "\u0161", + 0x9E: "\u017e", + 0xA0: "\u20ac", +} + + +def decode_text(b: bytes) -> str: + if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: + return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be") + else: + return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b) + + +class PdfFormatError(RuntimeError): + """An error that probably indicates a syntactic or semantic error in the + PDF file structure""" + + pass + + +def check_format_condition(condition: bool, error_message: str) -> None: + if not condition: + raise PdfFormatError(error_message) + + +class IndirectReferenceTuple(NamedTuple): + object_id: int + generation: int + + +class IndirectReference(IndirectReferenceTuple): + def __str__(self) -> str: + return f"{self.object_id} {self.generation} R" + + def __bytes__(self) -> bytes: + return self.__str__().encode("us-ascii") + + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return False + assert isinstance(other, IndirectReference) + return other.object_id == self.object_id and other.generation == self.generation + + def __ne__(self, other: object) -> bool: + return not (self == other) + + def __hash__(self) -> int: + return hash((self.object_id, self.generation)) + + +class IndirectObjectDef(IndirectReference): + def __str__(self) -> str: + return f"{self.object_id} {self.generation} obj" + + +class XrefTable: + def __init__(self) -> None: + self.existing_entries: dict[int, tuple[int, int]] = ( + {} + ) # object ID => (offset, generation) + self.new_entries: dict[int, tuple[int, int]] = ( + {} + ) # object ID => (offset, generation) + self.deleted_entries = {0: 65536} # object ID => generation + self.reading_finished = False + + def __setitem__(self, key: int, value: tuple[int, int]) -> None: + if self.reading_finished: + self.new_entries[key] = value + else: + self.existing_entries[key] = value + if key in self.deleted_entries: + del self.deleted_entries[key] + + def __getitem__(self, key: int) -> tuple[int, int]: + try: + return self.new_entries[key] + except KeyError: + return self.existing_entries[key] + + def __delitem__(self, key: int) -> None: + if key in self.new_entries: + generation = self.new_entries[key][1] + 1 + del self.new_entries[key] + self.deleted_entries[key] = generation + elif key in self.existing_entries: + generation = self.existing_entries[key][1] + 1 + self.deleted_entries[key] = generation + elif key in self.deleted_entries: + generation = self.deleted_entries[key] + else: + msg = f"object ID {key} cannot be deleted because it doesn't exist" + raise IndexError(msg) + + def __contains__(self, key: int) -> bool: + return key in self.existing_entries or key in self.new_entries + + def __len__(self) -> int: + return len( + set(self.existing_entries.keys()) + | set(self.new_entries.keys()) + | set(self.deleted_entries.keys()) + ) + + def keys(self) -> set[int]: + return ( + set(self.existing_entries.keys()) - set(self.deleted_entries.keys()) + ) | set(self.new_entries.keys()) + + def write(self, f: IO[bytes]) -> int: + keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys())) + deleted_keys = sorted(set(self.deleted_entries.keys())) + startxref = f.tell() + f.write(b"xref\n") + while keys: + # find a contiguous sequence of object IDs + prev: int | None = None + for index, key in enumerate(keys): + if prev is None or prev + 1 == key: + prev = key + else: + contiguous_keys = keys[:index] + keys = keys[index:] + break + else: + contiguous_keys = keys + keys = [] + f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys))) + for object_id in contiguous_keys: + if object_id in self.new_entries: + f.write(b"%010d %05d n \n" % self.new_entries[object_id]) + else: + this_deleted_object_id = deleted_keys.pop(0) + check_format_condition( + object_id == this_deleted_object_id, + f"expected the next deleted object ID to be {object_id}, " + f"instead found {this_deleted_object_id}", + ) + try: + next_in_linked_list = deleted_keys[0] + except IndexError: + next_in_linked_list = 0 + f.write( + b"%010d %05d f \n" + % (next_in_linked_list, self.deleted_entries[object_id]) + ) + return startxref + + +class PdfName: + name: bytes + + def __init__(self, name: PdfName | bytes | str) -> None: + if isinstance(name, PdfName): + self.name = name.name + elif isinstance(name, bytes): + self.name = name + else: + self.name = name.encode("us-ascii") + + def name_as_str(self) -> str: + return self.name.decode("us-ascii") + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, PdfName) and other.name == self.name + ) or other == self.name + + def __hash__(self) -> int: + return hash(self.name) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({repr(self.name)})" + + @classmethod + def from_pdf_stream(cls, data: bytes) -> PdfName: + return cls(PdfParser.interpret_name(data)) + + allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} + + def __bytes__(self) -> bytes: + result = bytearray(b"/") + for b in self.name: + if b in self.allowed_chars: + result.append(b) + else: + result.extend(b"#%02X" % b) + return bytes(result) + + +class PdfArray(list[Any]): + def __bytes__(self) -> bytes: + return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" + + +TYPE_CHECKING = False +if TYPE_CHECKING: + _DictBase = collections.UserDict[Union[str, bytes], Any] +else: + _DictBase = collections.UserDict + + +class PdfDict(_DictBase): + def __setattr__(self, key: str, value: Any) -> None: + if key == "data": + collections.UserDict.__setattr__(self, key, value) + else: + self[key.encode("us-ascii")] = value + + def __getattr__(self, key: str) -> str | time.struct_time: + try: + value = self[key.encode("us-ascii")] + except KeyError as e: + raise AttributeError(key) from e + if isinstance(value, bytes): + value = decode_text(value) + if key.endswith("Date"): + if value.startswith("D:"): + value = value[2:] + + relationship = "Z" + if len(value) > 17: + relationship = value[14] + offset = int(value[15:17]) * 60 + if len(value) > 20: + offset += int(value[18:20]) + + format = "%Y%m%d%H%M%S"[: len(value) - 2] + value = time.strptime(value[: len(format) + 2], format) + if relationship in ["+", "-"]: + offset *= 60 + if relationship == "+": + offset *= -1 + value = time.gmtime(calendar.timegm(value) + offset) + return value + + def __bytes__(self) -> bytes: + out = bytearray(b"<<") + for key, value in self.items(): + if value is None: + continue + value = pdf_repr(value) + out.extend(b"\n") + out.extend(bytes(PdfName(key))) + out.extend(b" ") + out.extend(value) + out.extend(b"\n>>") + return bytes(out) + + +class PdfBinary: + def __init__(self, data: list[int] | bytes) -> None: + self.data = data + + def __bytes__(self) -> bytes: + return b"<%s>" % b"".join(b"%02X" % b for b in self.data) + + +class PdfStream: + def __init__(self, dictionary: PdfDict, buf: bytes) -> None: + self.dictionary = dictionary + self.buf = buf + + def decode(self) -> bytes: + try: + filter = self.dictionary[b"Filter"] + except KeyError: + return self.buf + if filter == b"FlateDecode": + try: + expected_length = self.dictionary[b"DL"] + except KeyError: + expected_length = self.dictionary[b"Length"] + return zlib.decompress(self.buf, bufsize=int(expected_length)) + else: + msg = f"stream filter {repr(filter)} unknown/unsupported" + raise NotImplementedError(msg) + + +def pdf_repr(x: Any) -> bytes: + if x is True: + return b"true" + elif x is False: + return b"false" + elif x is None: + return b"null" + elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): + return bytes(x) + elif isinstance(x, (int, float)): + return str(x).encode("us-ascii") + elif isinstance(x, time.struct_time): + return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" + elif isinstance(x, dict): + return bytes(PdfDict(x)) + elif isinstance(x, list): + return bytes(PdfArray(x)) + elif isinstance(x, str): + return pdf_repr(encode_text(x)) + elif isinstance(x, bytes): + # XXX escape more chars? handle binary garbage + x = x.replace(b"\\", b"\\\\") + x = x.replace(b"(", b"\\(") + x = x.replace(b")", b"\\)") + return b"(" + x + b")" + else: + return bytes(x) + + +class PdfParser: + """Based on + https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf + Supports PDF up to 1.4 + """ + + def __init__( + self, + filename: str | None = None, + f: IO[bytes] | None = None, + buf: bytes | bytearray | None = None, + start_offset: int = 0, + mode: str = "rb", + ) -> None: + if buf and f: + msg = "specify buf or f or filename, but not both buf and f" + raise RuntimeError(msg) + self.filename = filename + self.buf: bytes | bytearray | mmap.mmap | None = buf + self.f = f + self.start_offset = start_offset + self.should_close_buf = False + self.should_close_file = False + if filename is not None and f is None: + self.f = f = open(filename, mode) + self.should_close_file = True + if f is not None: + self.buf = self.get_buf_from_file(f) + self.should_close_buf = True + if not filename and hasattr(f, "name"): + self.filename = f.name + self.cached_objects: dict[IndirectReference, Any] = {} + self.root_ref: IndirectReference | None + self.info_ref: IndirectReference | None + self.pages_ref: IndirectReference | None + self.last_xref_section_offset: int | None + if self.buf: + self.read_pdf_info() + else: + self.file_size_total = self.file_size_this = 0 + self.root = PdfDict() + self.root_ref = None + self.info = PdfDict() + self.info_ref = None + self.page_tree_root = PdfDict() + self.pages: list[IndirectReference] = [] + self.orig_pages: list[IndirectReference] = [] + self.pages_ref = None + self.last_xref_section_offset = None + self.trailer_dict: dict[bytes, Any] = {} + self.xref_table = XrefTable() + self.xref_table.reading_finished = True + if f: + self.seek_end() + + def __enter__(self) -> PdfParser: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def start_writing(self) -> None: + self.close_buf() + self.seek_end() + + def close_buf(self) -> None: + if isinstance(self.buf, mmap.mmap): + self.buf.close() + self.buf = None + + def close(self) -> None: + if self.should_close_buf: + self.close_buf() + if self.f is not None and self.should_close_file: + self.f.close() + self.f = None + + def seek_end(self) -> None: + assert self.f is not None + self.f.seek(0, os.SEEK_END) + + def write_header(self) -> None: + assert self.f is not None + self.f.write(b"%PDF-1.4\n") + + def write_comment(self, s: str) -> None: + assert self.f is not None + self.f.write(f"% {s}\n".encode()) + + def write_catalog(self) -> IndirectReference: + assert self.f is not None + self.del_root() + self.root_ref = self.next_object_id(self.f.tell()) + self.pages_ref = self.next_object_id(0) + self.rewrite_pages() + self.write_obj(self.root_ref, Type=PdfName(b"Catalog"), Pages=self.pages_ref) + self.write_obj( + self.pages_ref, + Type=PdfName(b"Pages"), + Count=len(self.pages), + Kids=self.pages, + ) + return self.root_ref + + def rewrite_pages(self) -> None: + pages_tree_nodes_to_delete = [] + for i, page_ref in enumerate(self.orig_pages): + page_info = self.cached_objects[page_ref] + del self.xref_table[page_ref.object_id] + pages_tree_nodes_to_delete.append(page_info[PdfName(b"Parent")]) + if page_ref not in self.pages: + # the page has been deleted + continue + # make dict keys into strings for passing to write_page + stringified_page_info = {} + for key, value in page_info.items(): + # key should be a PdfName + stringified_page_info[key.name_as_str()] = value + stringified_page_info["Parent"] = self.pages_ref + new_page_ref = self.write_page(None, **stringified_page_info) + for j, cur_page_ref in enumerate(self.pages): + if cur_page_ref == page_ref: + # replace the page reference with the new one + self.pages[j] = new_page_ref + # delete redundant Pages tree nodes from xref table + for pages_tree_node_ref in pages_tree_nodes_to_delete: + while pages_tree_node_ref: + pages_tree_node = self.cached_objects[pages_tree_node_ref] + if pages_tree_node_ref.object_id in self.xref_table: + del self.xref_table[pages_tree_node_ref.object_id] + pages_tree_node_ref = pages_tree_node.get(b"Parent", None) + self.orig_pages = [] + + def write_xref_and_trailer( + self, new_root_ref: IndirectReference | None = None + ) -> None: + assert self.f is not None + if new_root_ref: + self.del_root() + self.root_ref = new_root_ref + if self.info: + self.info_ref = self.write_obj(None, self.info) + start_xref = self.xref_table.write(self.f) + num_entries = len(self.xref_table) + trailer_dict: dict[str | bytes, Any] = { + b"Root": self.root_ref, + b"Size": num_entries, + } + if self.last_xref_section_offset is not None: + trailer_dict[b"Prev"] = self.last_xref_section_offset + if self.info: + trailer_dict[b"Info"] = self.info_ref + self.last_xref_section_offset = start_xref + self.f.write( + b"trailer\n" + + bytes(PdfDict(trailer_dict)) + + b"\nstartxref\n%d\n%%%%EOF" % start_xref + ) + + def write_page( + self, ref: int | IndirectReference | None, *objs: Any, **dict_obj: Any + ) -> IndirectReference: + obj_ref = self.pages[ref] if isinstance(ref, int) else ref + if "Type" not in dict_obj: + dict_obj["Type"] = PdfName(b"Page") + if "Parent" not in dict_obj: + dict_obj["Parent"] = self.pages_ref + return self.write_obj(obj_ref, *objs, **dict_obj) + + def write_obj( + self, ref: IndirectReference | None, *objs: Any, **dict_obj: Any + ) -> IndirectReference: + assert self.f is not None + f = self.f + if ref is None: + ref = self.next_object_id(f.tell()) + else: + self.xref_table[ref.object_id] = (f.tell(), ref.generation) + f.write(bytes(IndirectObjectDef(*ref))) + stream = dict_obj.pop("stream", None) + if stream is not None: + dict_obj["Length"] = len(stream) + if dict_obj: + f.write(pdf_repr(dict_obj)) + for obj in objs: + f.write(pdf_repr(obj)) + if stream is not None: + f.write(b"stream\n") + f.write(stream) + f.write(b"\nendstream\n") + f.write(b"endobj\n") + return ref + + def del_root(self) -> None: + if self.root_ref is None: + return + del self.xref_table[self.root_ref.object_id] + del self.xref_table[self.root[b"Pages"].object_id] + + @staticmethod + def get_buf_from_file(f: IO[bytes]) -> bytes | mmap.mmap: + if hasattr(f, "getbuffer"): + return f.getbuffer() + elif hasattr(f, "getvalue"): + return f.getvalue() + else: + try: + return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + except ValueError: # cannot mmap an empty file + return b"" + + def read_pdf_info(self) -> None: + assert self.buf is not None + self.file_size_total = len(self.buf) + self.file_size_this = self.file_size_total - self.start_offset + self.read_trailer() + check_format_condition( + self.trailer_dict.get(b"Root") is not None, "Root is missing" + ) + self.root_ref = self.trailer_dict[b"Root"] + assert self.root_ref is not None + self.info_ref = self.trailer_dict.get(b"Info", None) + self.root = PdfDict(self.read_indirect(self.root_ref)) + if self.info_ref is None: + self.info = PdfDict() + else: + self.info = PdfDict(self.read_indirect(self.info_ref)) + check_format_condition(b"Type" in self.root, "/Type missing in Root") + check_format_condition( + self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog" + ) + check_format_condition( + self.root.get(b"Pages") is not None, "/Pages missing in Root" + ) + check_format_condition( + isinstance(self.root[b"Pages"], IndirectReference), + "/Pages in Root is not an indirect reference", + ) + self.pages_ref = self.root[b"Pages"] + assert self.pages_ref is not None + self.page_tree_root = self.read_indirect(self.pages_ref) + self.pages = self.linearize_page_tree(self.page_tree_root) + # save the original list of page references + # in case the user modifies, adds or deletes some pages + # and we need to rewrite the pages and their list + self.orig_pages = self.pages[:] + + def next_object_id(self, offset: int | None = None) -> IndirectReference: + try: + # TODO: support reuse of deleted objects + reference = IndirectReference(max(self.xref_table.keys()) + 1, 0) + except ValueError: + reference = IndirectReference(1, 0) + if offset is not None: + self.xref_table[reference.object_id] = (offset, 0) + return reference + + delimiter = rb"[][()<>{}/%]" + delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]" + whitespace = rb"[\000\011\012\014\015\040]" + whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]" + whitespace_optional = whitespace + b"*" + whitespace_mandatory = whitespace + b"+" + # No "\012" aka "\n" or "\015" aka "\r": + whitespace_optional_no_nl = rb"[\000\011\014\040]*" + newline_only = rb"[\r\n]+" + newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl + re_trailer_end = re.compile( + whitespace_mandatory + + rb"trailer" + + whitespace_optional + + rb"<<(.*>>)" + + newline + + rb"startxref" + + newline + + rb"([0-9]+)" + + newline + + rb"%%EOF" + + whitespace_optional + + rb"$", + re.DOTALL, + ) + re_trailer_prev = re.compile( + whitespace_optional + + rb"trailer" + + whitespace_optional + + rb"<<(.*?>>)" + + newline + + rb"startxref" + + newline + + rb"([0-9]+)" + + newline + + rb"%%EOF" + + whitespace_optional, + re.DOTALL, + ) + + def read_trailer(self) -> None: + assert self.buf is not None + search_start_offset = len(self.buf) - 16384 + if search_start_offset < self.start_offset: + search_start_offset = self.start_offset + m = self.re_trailer_end.search(self.buf, search_start_offset) + check_format_condition(m is not None, "trailer end not found") + # make sure we found the LAST trailer + last_match = m + while m: + last_match = m + m = self.re_trailer_end.search(self.buf, m.start() + 16) + if not m: + m = last_match + assert m is not None + trailer_data = m.group(1) + self.last_xref_section_offset = int(m.group(2)) + self.trailer_dict = self.interpret_trailer(trailer_data) + self.xref_table = XrefTable() + self.read_xref_table(xref_section_offset=self.last_xref_section_offset) + if b"Prev" in self.trailer_dict: + self.read_prev_trailer(self.trailer_dict[b"Prev"]) + + def read_prev_trailer(self, xref_section_offset: int) -> None: + assert self.buf is not None + trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset) + m = self.re_trailer_prev.search( + self.buf[trailer_offset : trailer_offset + 16384] + ) + check_format_condition(m is not None, "previous trailer not found") + assert m is not None + trailer_data = m.group(1) + check_format_condition( + int(m.group(2)) == xref_section_offset, + "xref section offset in previous trailer doesn't match what was expected", + ) + trailer_dict = self.interpret_trailer(trailer_data) + if b"Prev" in trailer_dict: + self.read_prev_trailer(trailer_dict[b"Prev"]) + + re_whitespace_optional = re.compile(whitespace_optional) + re_name = re.compile( + whitespace_optional + + rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + + delimiter_or_ws + + rb")" + ) + re_dict_start = re.compile(whitespace_optional + rb"<<") + re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional) + + @classmethod + def interpret_trailer(cls, trailer_data: bytes) -> dict[bytes, Any]: + trailer = {} + offset = 0 + while True: + m = cls.re_name.match(trailer_data, offset) + if not m: + m = cls.re_dict_end.match(trailer_data, offset) + check_format_condition( + m is not None and m.end() == len(trailer_data), + "name not found in trailer, remaining data: " + + repr(trailer_data[offset:]), + ) + break + key = cls.interpret_name(m.group(1)) + assert isinstance(key, bytes) + value, value_offset = cls.get_value(trailer_data, m.end()) + trailer[key] = value + if value_offset is None: + break + offset = value_offset + check_format_condition( + b"Size" in trailer and isinstance(trailer[b"Size"], int), + "/Size not in trailer or not an integer", + ) + check_format_condition( + b"Root" in trailer and isinstance(trailer[b"Root"], IndirectReference), + "/Root not in trailer or not an indirect reference", + ) + return trailer + + re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?") + + @classmethod + def interpret_name(cls, raw: bytes, as_text: bool = False) -> str | bytes: + name = b"" + for m in cls.re_hashes_in_name.finditer(raw): + if m.group(3): + name += m.group(1) + bytearray.fromhex(m.group(3).decode("us-ascii")) + else: + name += m.group(1) + if as_text: + return name.decode("utf-8") + else: + return bytes(name) + + re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")") + re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")") + re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")") + re_int = re.compile( + whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")" + ) + re_real = re.compile( + whitespace_optional + + rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + + delimiter_or_ws + + rb")" + ) + re_array_start = re.compile(whitespace_optional + rb"\[") + re_array_end = re.compile(whitespace_optional + rb"]") + re_string_hex = re.compile( + whitespace_optional + rb"<(" + whitespace_or_hex + rb"*)>" + ) + re_string_lit = re.compile(whitespace_optional + rb"\(") + re_indirect_reference = re.compile( + whitespace_optional + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"R(?=" + + delimiter_or_ws + + rb")" + ) + re_indirect_def_start = re.compile( + whitespace_optional + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"obj(?=" + + delimiter_or_ws + + rb")" + ) + re_indirect_def_end = re.compile( + whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")" + ) + re_comment = re.compile( + rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*" + ) + re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n") + re_stream_end = re.compile( + whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")" + ) + + @classmethod + def get_value( + cls, + data: bytes | bytearray | mmap.mmap, + offset: int, + expect_indirect: IndirectReference | None = None, + max_nesting: int = -1, + ) -> tuple[Any, int | None]: + if max_nesting == 0: + return None, None + m = cls.re_comment.match(data, offset) + if m: + offset = m.end() + m = cls.re_indirect_def_start.match(data, offset) + if m: + check_format_condition( + int(m.group(1)) > 0, + "indirect object definition: object ID must be greater than 0", + ) + check_format_condition( + int(m.group(2)) >= 0, + "indirect object definition: generation must be non-negative", + ) + check_format_condition( + expect_indirect is None + or expect_indirect + == IndirectReference(int(m.group(1)), int(m.group(2))), + "indirect object definition different than expected", + ) + object, object_offset = cls.get_value( + data, m.end(), max_nesting=max_nesting - 1 + ) + if object_offset is None: + return object, None + m = cls.re_indirect_def_end.match(data, object_offset) + check_format_condition( + m is not None, "indirect object definition end not found" + ) + assert m is not None + return object, m.end() + check_format_condition( + not expect_indirect, "indirect object definition not found" + ) + m = cls.re_indirect_reference.match(data, offset) + if m: + check_format_condition( + int(m.group(1)) > 0, + "indirect object reference: object ID must be greater than 0", + ) + check_format_condition( + int(m.group(2)) >= 0, + "indirect object reference: generation must be non-negative", + ) + return IndirectReference(int(m.group(1)), int(m.group(2))), m.end() + m = cls.re_dict_start.match(data, offset) + if m: + offset = m.end() + result: dict[Any, Any] = {} + m = cls.re_dict_end.match(data, offset) + current_offset: int | None = offset + while not m: + assert current_offset is not None + key, current_offset = cls.get_value( + data, current_offset, max_nesting=max_nesting - 1 + ) + if current_offset is None: + return result, None + value, current_offset = cls.get_value( + data, current_offset, max_nesting=max_nesting - 1 + ) + result[key] = value + if current_offset is None: + return result, None + m = cls.re_dict_end.match(data, current_offset) + current_offset = m.end() + m = cls.re_stream_start.match(data, current_offset) + if m: + stream_len = result.get(b"Length") + if stream_len is None or not isinstance(stream_len, int): + msg = f"bad or missing Length in stream dict ({stream_len})" + raise PdfFormatError(msg) + stream_data = data[m.end() : m.end() + stream_len] + m = cls.re_stream_end.match(data, m.end() + stream_len) + check_format_condition(m is not None, "stream end not found") + assert m is not None + current_offset = m.end() + return PdfStream(PdfDict(result), stream_data), current_offset + return PdfDict(result), current_offset + m = cls.re_array_start.match(data, offset) + if m: + offset = m.end() + results = [] + m = cls.re_array_end.match(data, offset) + current_offset = offset + while not m: + assert current_offset is not None + value, current_offset = cls.get_value( + data, current_offset, max_nesting=max_nesting - 1 + ) + results.append(value) + if current_offset is None: + return results, None + m = cls.re_array_end.match(data, current_offset) + return results, m.end() + m = cls.re_null.match(data, offset) + if m: + return None, m.end() + m = cls.re_true.match(data, offset) + if m: + return True, m.end() + m = cls.re_false.match(data, offset) + if m: + return False, m.end() + m = cls.re_name.match(data, offset) + if m: + return PdfName(cls.interpret_name(m.group(1))), m.end() + m = cls.re_int.match(data, offset) + if m: + return int(m.group(1)), m.end() + m = cls.re_real.match(data, offset) + if m: + # XXX Decimal instead of float??? + return float(m.group(1)), m.end() + m = cls.re_string_hex.match(data, offset) + if m: + # filter out whitespace + hex_string = bytearray( + b for b in m.group(1) if b in b"0123456789abcdefABCDEF" + ) + if len(hex_string) % 2 == 1: + # append a 0 if the length is not even - yes, at the end + hex_string.append(ord(b"0")) + return bytearray.fromhex(hex_string.decode("us-ascii")), m.end() + m = cls.re_string_lit.match(data, offset) + if m: + return cls.get_literal_string(data, m.end()) + # return None, offset # fallback (only for debugging) + msg = f"unrecognized object: {repr(data[offset : offset + 32])}" + raise PdfFormatError(msg) + + re_lit_str_token = re.compile( + rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" + ) + escaped_chars = { + b"n": b"\n", + b"r": b"\r", + b"t": b"\t", + b"b": b"\b", + b"f": b"\f", + b"(": b"(", + b")": b")", + b"\\": b"\\", + ord(b"n"): b"\n", + ord(b"r"): b"\r", + ord(b"t"): b"\t", + ord(b"b"): b"\b", + ord(b"f"): b"\f", + ord(b"("): b"(", + ord(b")"): b")", + ord(b"\\"): b"\\", + } + + @classmethod + def get_literal_string( + cls, data: bytes | bytearray | mmap.mmap, offset: int + ) -> tuple[bytes, int]: + nesting_depth = 0 + result = bytearray() + for m in cls.re_lit_str_token.finditer(data, offset): + result.extend(data[offset : m.start()]) + if m.group(1): + result.extend(cls.escaped_chars[m.group(1)[1]]) + elif m.group(2): + result.append(int(m.group(2)[1:], 8)) + elif m.group(3): + pass + elif m.group(5): + result.extend(b"\n") + elif m.group(6): + result.extend(b"(") + nesting_depth += 1 + elif m.group(7): + if nesting_depth == 0: + return bytes(result), m.end() + result.extend(b")") + nesting_depth -= 1 + offset = m.end() + msg = "unfinished literal string" + raise PdfFormatError(msg) + + re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) + re_xref_subsection_start = re.compile( + whitespace_optional + + rb"([0-9]+)" + + whitespace_mandatory + + rb"([0-9]+)" + + whitespace_optional + + newline_only + ) + re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") + + def read_xref_table(self, xref_section_offset: int) -> int: + assert self.buf is not None + subsection_found = False + m = self.re_xref_section_start.match( + self.buf, xref_section_offset + self.start_offset + ) + check_format_condition(m is not None, "xref section start not found") + assert m is not None + offset = m.end() + while True: + m = self.re_xref_subsection_start.match(self.buf, offset) + if not m: + check_format_condition( + subsection_found, "xref subsection start not found" + ) + break + subsection_found = True + offset = m.end() + first_object = int(m.group(1)) + num_objects = int(m.group(2)) + for i in range(first_object, first_object + num_objects): + m = self.re_xref_entry.match(self.buf, offset) + check_format_condition(m is not None, "xref entry not found") + assert m is not None + offset = m.end() + is_free = m.group(3) == b"f" + if not is_free: + generation = int(m.group(2)) + new_entry = (int(m.group(1)), generation) + if i not in self.xref_table: + self.xref_table[i] = new_entry + return offset + + def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any: + offset, generation = self.xref_table[ref[0]] + check_format_condition( + generation == ref[1], + f"expected to find generation {ref[1]} for object ID {ref[0]} in xref " + f"table, instead found generation {generation} at offset {offset}", + ) + assert self.buf is not None + value = self.get_value( + self.buf, + offset + self.start_offset, + expect_indirect=IndirectReference(*ref), + max_nesting=max_nesting, + )[0] + self.cached_objects[ref] = value + return value + + def linearize_page_tree( + self, node: PdfDict | None = None + ) -> list[IndirectReference]: + page_node = node if node is not None else self.page_tree_root + check_format_condition( + page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" + ) + pages = [] + for kid in page_node[b"Kids"]: + kid_object = self.read_indirect(kid) + if kid_object[b"Type"] == b"Page": + pages.append(kid) + else: + pages.extend(self.linearize_page_tree(node=kid_object)) + return pages diff --git a/python_modules/PIL/PixarImagePlugin.py b/python_modules/PIL/PixarImagePlugin.py new file mode 100644 index 000000000..d2b6d0a97 --- /dev/null +++ b/python_modules/PIL/PixarImagePlugin.py @@ -0,0 +1,72 @@ +# +# The Python Imaging Library. +# $Id$ +# +# PIXAR raster support for PIL +# +# history: +# 97-01-29 fl Created +# +# notes: +# This is incomplete; it is based on a few samples created with +# Photoshop 2.5 and 3.0, and a summary description provided by +# Greg Coats . Hopefully, "L" and +# "RGBA" support will be added in future versions. +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image, ImageFile +from ._binary import i16le as i16 + +# +# helpers + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"\200\350\000\000") + + +## +# Image plugin for PIXAR raster images. + + +class PixarImageFile(ImageFile.ImageFile): + format = "PIXAR" + format_description = "PIXAR raster image" + + def _open(self) -> None: + # assuming a 4-byte magic label + assert self.fp is not None + + s = self.fp.read(4) + if not _accept(s): + msg = "not a PIXAR file" + raise SyntaxError(msg) + + # read rest of header + s = s + self.fp.read(508) + + self._size = i16(s, 418), i16(s, 416) + + # get channel/depth descriptions + mode = i16(s, 424), i16(s, 426) + + if mode == (14, 2): + self._mode = "RGB" + # FIXME: to be continued... + + # create tile descriptor (assuming "dumped") + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)] + + +# +# -------------------------------------------------------------------- + +Image.register_open(PixarImageFile.format, PixarImageFile, _accept) + +Image.register_extension(PixarImageFile.format, ".pxr") diff --git a/python_modules/PIL/PngImagePlugin.py b/python_modules/PIL/PngImagePlugin.py new file mode 100644 index 000000000..1b9a89aef --- /dev/null +++ b/python_modules/PIL/PngImagePlugin.py @@ -0,0 +1,1551 @@ +# +# The Python Imaging Library. +# $Id$ +# +# PNG support code +# +# See "PNG (Portable Network Graphics) Specification, version 1.0; +# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.). +# +# history: +# 1996-05-06 fl Created (couldn't resist it) +# 1996-12-14 fl Upgraded, added read and verify support (0.2) +# 1996-12-15 fl Separate PNG stream parser +# 1996-12-29 fl Added write support, added getchunks +# 1996-12-30 fl Eliminated circular references in decoder (0.3) +# 1998-07-12 fl Read/write 16-bit images as mode I (0.4) +# 2001-02-08 fl Added transparency support (from Zircon) (0.5) +# 2001-04-16 fl Don't close data source in "open" method (0.6) +# 2004-02-24 fl Don't even pretend to support interlaced files (0.7) +# 2004-08-31 fl Do basic sanity check on chunk identifiers (0.8) +# 2004-09-20 fl Added PngInfo chunk container +# 2004-12-18 fl Added DPI read support (based on code by Niki Spahiev) +# 2008-08-13 fl Added tRNS support for RGB images +# 2009-03-06 fl Support for preserving ICC profiles (by Florian Hoech) +# 2009-03-08 fl Added zTXT support (from Lowell Alleman) +# 2009-03-29 fl Read interlaced PNG files (from Conrado Porto Lopes Gouvua) +# +# Copyright (c) 1997-2009 by Secret Labs AB +# Copyright (c) 1996 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import itertools +import logging +import re +import struct +import warnings +import zlib +from collections.abc import Callable +from enum import IntEnum +from typing import IO, Any, NamedTuple, NoReturn, cast + +from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 +from ._binary import o16be as o16 +from ._binary import o32be as o32 +from ._deprecate import deprecate +from ._util import DeferredError + +TYPE_CHECKING = False +if TYPE_CHECKING: + from . import _imaging + +logger = logging.getLogger(__name__) + +is_cid = re.compile(rb"\w\w\w\w").match + + +_MAGIC = b"\211PNG\r\n\032\n" + + +_MODES = { + # supported bits/color combinations, and corresponding modes/rawmodes + # Grayscale + (1, 0): ("1", "1"), + (2, 0): ("L", "L;2"), + (4, 0): ("L", "L;4"), + (8, 0): ("L", "L"), + (16, 0): ("I;16", "I;16B"), + # Truecolour + (8, 2): ("RGB", "RGB"), + (16, 2): ("RGB", "RGB;16B"), + # Indexed-colour + (1, 3): ("P", "P;1"), + (2, 3): ("P", "P;2"), + (4, 3): ("P", "P;4"), + (8, 3): ("P", "P"), + # Grayscale with alpha + (8, 4): ("LA", "LA"), + (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available + # Truecolour with alpha + (8, 6): ("RGBA", "RGBA"), + (16, 6): ("RGBA", "RGBA;16B"), +} + + +_simple_palette = re.compile(b"^\xff*\x00\xff*$") + +MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK +""" +Maximum decompressed size for a iTXt or zTXt chunk. +Eliminates decompression bombs where compressed chunks can expand 1000x. +See :ref:`Text in PNG File Format`. +""" +MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK +""" +Set the maximum total text chunk size. +See :ref:`Text in PNG File Format`. +""" + + +# APNG frame disposal modes +class Disposal(IntEnum): + OP_NONE = 0 + """ + No disposal is done on this frame before rendering the next frame. + See :ref:`Saving APNG sequences`. + """ + OP_BACKGROUND = 1 + """ + This frame’s modified region is cleared to fully transparent black before rendering + the next frame. + See :ref:`Saving APNG sequences`. + """ + OP_PREVIOUS = 2 + """ + This frame’s modified region is reverted to the previous frame’s contents before + rendering the next frame. + See :ref:`Saving APNG sequences`. + """ + + +# APNG frame blend modes +class Blend(IntEnum): + OP_SOURCE = 0 + """ + All color components of this frame, including alpha, overwrite the previous output + image contents. + See :ref:`Saving APNG sequences`. + """ + OP_OVER = 1 + """ + This frame should be alpha composited with the previous output image contents. + See :ref:`Saving APNG sequences`. + """ + + +def _safe_zlib_decompress(s: bytes) -> bytes: + dobj = zlib.decompressobj() + plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) + if dobj.unconsumed_tail: + msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK" + raise ValueError(msg) + return plaintext + + +def _crc32(data: bytes, seed: int = 0) -> int: + return zlib.crc32(data, seed) & 0xFFFFFFFF + + +# -------------------------------------------------------------------- +# Support classes. Suitable for PNG and related formats like MNG etc. + + +class ChunkStream: + def __init__(self, fp: IO[bytes]) -> None: + self.fp: IO[bytes] | None = fp + self.queue: list[tuple[bytes, int, int]] | None = [] + + def read(self) -> tuple[bytes, int, int]: + """Fetch a new chunk. Returns header information.""" + cid = None + + assert self.fp is not None + if self.queue: + cid, pos, length = self.queue.pop() + self.fp.seek(pos) + else: + s = self.fp.read(8) + cid = s[4:] + pos = self.fp.tell() + length = i32(s) + + if not is_cid(cid): + if not ImageFile.LOAD_TRUNCATED_IMAGES: + msg = f"broken PNG file (chunk {repr(cid)})" + raise SyntaxError(msg) + + return cid, pos, length + + def __enter__(self) -> ChunkStream: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def close(self) -> None: + self.queue = self.fp = None + + def push(self, cid: bytes, pos: int, length: int) -> None: + assert self.queue is not None + self.queue.append((cid, pos, length)) + + def call(self, cid: bytes, pos: int, length: int) -> bytes: + """Call the appropriate chunk handler""" + + logger.debug("STREAM %r %s %s", cid, pos, length) + return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length) + + def crc(self, cid: bytes, data: bytes) -> None: + """Read and verify checksum""" + + # Skip CRC checks for ancillary chunks if allowed to load truncated + # images + # 5th byte of first char is 1 [specs, section 5.4] + if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1): + self.crc_skip(cid, data) + return + + assert self.fp is not None + try: + crc1 = _crc32(data, _crc32(cid)) + crc2 = i32(self.fp.read(4)) + if crc1 != crc2: + msg = f"broken PNG file (bad header checksum in {repr(cid)})" + raise SyntaxError(msg) + except struct.error as e: + msg = f"broken PNG file (incomplete checksum in {repr(cid)})" + raise SyntaxError(msg) from e + + def crc_skip(self, cid: bytes, data: bytes) -> None: + """Read checksum""" + + assert self.fp is not None + self.fp.read(4) + + def verify(self, endchunk: bytes = b"IEND") -> list[bytes]: + # Simple approach; just calculate checksum for all remaining + # blocks. Must be called directly after open. + + cids = [] + + assert self.fp is not None + while True: + try: + cid, pos, length = self.read() + except struct.error as e: + msg = "truncated PNG file" + raise OSError(msg) from e + + if cid == endchunk: + break + self.crc(cid, ImageFile._safe_read(self.fp, length)) + cids.append(cid) + + return cids + + +class iTXt(str): + """ + Subclass of string to allow iTXt chunks to look like strings while + keeping their extra information + + """ + + lang: str | bytes | None + tkey: str | bytes | None + + @staticmethod + def __new__( + cls, text: str, lang: str | None = None, tkey: str | None = None + ) -> iTXt: + """ + :param cls: the class to use when creating the instance + :param text: value for this key + :param lang: language code + :param tkey: UTF-8 version of the key name + """ + + self = str.__new__(cls, text) + self.lang = lang + self.tkey = tkey + return self + + +class PngInfo: + """ + PNG chunk container (for use with save(pnginfo=)) + + """ + + def __init__(self) -> None: + self.chunks: list[tuple[bytes, bytes, bool]] = [] + + def add(self, cid: bytes, data: bytes, after_idat: bool = False) -> None: + """Appends an arbitrary chunk. Use with caution. + + :param cid: a byte string, 4 bytes long. + :param data: a byte string of the encoded data + :param after_idat: for use with private chunks. Whether the chunk + should be written after IDAT + + """ + + self.chunks.append((cid, data, after_idat)) + + def add_itxt( + self, + key: str | bytes, + value: str | bytes, + lang: str | bytes = "", + tkey: str | bytes = "", + zip: bool = False, + ) -> None: + """Appends an iTXt chunk. + + :param key: latin-1 encodable text key name + :param value: value for this key + :param lang: language code + :param tkey: UTF-8 version of the key name + :param zip: compression flag + + """ + + if not isinstance(key, bytes): + key = key.encode("latin-1", "strict") + if not isinstance(value, bytes): + value = value.encode("utf-8", "strict") + if not isinstance(lang, bytes): + lang = lang.encode("utf-8", "strict") + if not isinstance(tkey, bytes): + tkey = tkey.encode("utf-8", "strict") + + if zip: + self.add( + b"iTXt", + key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value), + ) + else: + self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) + + def add_text( + self, key: str | bytes, value: str | bytes | iTXt, zip: bool = False + ) -> None: + """Appends a text chunk. + + :param key: latin-1 encodable text key name + :param value: value for this key, text or an + :py:class:`PIL.PngImagePlugin.iTXt` instance + :param zip: compression flag + + """ + if isinstance(value, iTXt): + return self.add_itxt( + key, + value, + value.lang if value.lang is not None else b"", + value.tkey if value.tkey is not None else b"", + zip=zip, + ) + + # The tEXt chunk stores latin-1 text + if not isinstance(value, bytes): + try: + value = value.encode("latin-1", "strict") + except UnicodeError: + return self.add_itxt(key, value, zip=zip) + + if not isinstance(key, bytes): + key = key.encode("latin-1", "strict") + + if zip: + self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) + else: + self.add(b"tEXt", key + b"\0" + value) + + +# -------------------------------------------------------------------- +# PNG image stream (IHDR/IEND) + + +class _RewindState(NamedTuple): + info: dict[str | tuple[int, int], Any] + tile: list[ImageFile._Tile] + seq_num: int | None + + +class PngStream(ChunkStream): + def __init__(self, fp: IO[bytes]) -> None: + super().__init__(fp) + + # local copies of Image attributes + self.im_info: dict[str | tuple[int, int], Any] = {} + self.im_text: dict[str, str | iTXt] = {} + self.im_size = (0, 0) + self.im_mode = "" + self.im_tile: list[ImageFile._Tile] = [] + self.im_palette: tuple[str, bytes] | None = None + self.im_custom_mimetype: str | None = None + self.im_n_frames: int | None = None + self._seq_num: int | None = None + self.rewind_state = _RewindState({}, [], None) + + self.text_memory = 0 + + def check_text_memory(self, chunklen: int) -> None: + self.text_memory += chunklen + if self.text_memory > MAX_TEXT_MEMORY: + msg = ( + "Too much memory used in text chunks: " + f"{self.text_memory}>MAX_TEXT_MEMORY" + ) + raise ValueError(msg) + + def save_rewind(self) -> None: + self.rewind_state = _RewindState( + self.im_info.copy(), + self.im_tile, + self._seq_num, + ) + + def rewind(self) -> None: + self.im_info = self.rewind_state.info.copy() + self.im_tile = self.rewind_state.tile + self._seq_num = self.rewind_state.seq_num + + def chunk_iCCP(self, pos: int, length: int) -> bytes: + # ICC profile + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + # according to PNG spec, the iCCP chunk contains: + # Profile name 1-79 bytes (character string) + # Null separator 1 byte (null character) + # Compression method 1 byte (0) + # Compressed profile n bytes (zlib with deflate compression) + i = s.find(b"\0") + logger.debug("iCCP profile name %r", s[:i]) + comp_method = s[i + 1] + logger.debug("Compression method %s", comp_method) + if comp_method != 0: + msg = f"Unknown compression method {comp_method} in iCCP chunk" + raise SyntaxError(msg) + try: + icc_profile = _safe_zlib_decompress(s[i + 2 :]) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + icc_profile = None + else: + raise + except zlib.error: + icc_profile = None # FIXME + self.im_info["icc_profile"] = icc_profile + return s + + def chunk_IHDR(self, pos: int, length: int) -> bytes: + # image header + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + if length < 13: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + msg = "Truncated IHDR chunk" + raise ValueError(msg) + self.im_size = i32(s, 0), i32(s, 4) + try: + self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])] + except Exception: + pass + if s[12]: + self.im_info["interlace"] = 1 + if s[11]: + msg = "unknown filter category" + raise SyntaxError(msg) + return s + + def chunk_IDAT(self, pos: int, length: int) -> NoReturn: + # image data + if "bbox" in self.im_info: + tile = [ImageFile._Tile("zip", self.im_info["bbox"], pos, self.im_rawmode)] + else: + if self.im_n_frames is not None: + self.im_info["default_image"] = True + tile = [ImageFile._Tile("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] + self.im_tile = tile + self.im_idat = length + msg = "image data found" + raise EOFError(msg) + + def chunk_IEND(self, pos: int, length: int) -> NoReturn: + msg = "end of PNG image" + raise EOFError(msg) + + def chunk_PLTE(self, pos: int, length: int) -> bytes: + # palette + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + if self.im_mode == "P": + self.im_palette = "RGB", s + return s + + def chunk_tRNS(self, pos: int, length: int) -> bytes: + # transparency + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + if self.im_mode == "P": + if _simple_palette.match(s): + # tRNS contains only one full-transparent entry, + # other entries are full opaque + i = s.find(b"\0") + if i >= 0: + self.im_info["transparency"] = i + else: + # otherwise, we have a byte string with one alpha value + # for each palette entry + self.im_info["transparency"] = s + elif self.im_mode in ("1", "L", "I;16"): + self.im_info["transparency"] = i16(s) + elif self.im_mode == "RGB": + self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) + return s + + def chunk_gAMA(self, pos: int, length: int) -> bytes: + # gamma setting + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + self.im_info["gamma"] = i32(s) / 100000.0 + return s + + def chunk_cHRM(self, pos: int, length: int) -> bytes: + # chromaticity, 8 unsigned ints, actual value is scaled by 100,000 + # WP x,y, Red x,y, Green x,y Blue x,y + + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + raw_vals = struct.unpack(f">{len(s) // 4}I", s) + self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) + return s + + def chunk_sRGB(self, pos: int, length: int) -> bytes: + # srgb rendering intent, 1 byte + # 0 perceptual + # 1 relative colorimetric + # 2 saturation + # 3 absolute colorimetric + + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + if length < 1: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + msg = "Truncated sRGB chunk" + raise ValueError(msg) + self.im_info["srgb"] = s[0] + return s + + def chunk_pHYs(self, pos: int, length: int) -> bytes: + # pixels per unit + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + if length < 9: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + msg = "Truncated pHYs chunk" + raise ValueError(msg) + px, py = i32(s, 0), i32(s, 4) + unit = s[8] + if unit == 1: # meter + dpi = px * 0.0254, py * 0.0254 + self.im_info["dpi"] = dpi + elif unit == 0: + self.im_info["aspect"] = px, py + return s + + def chunk_tEXt(self, pos: int, length: int) -> bytes: + # text + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + try: + k, v = s.split(b"\0", 1) + except ValueError: + # fallback for broken tEXt tags + k = s + v = b"" + if k: + k_str = k.decode("latin-1", "strict") + v_str = v.decode("latin-1", "replace") + + self.im_info[k_str] = v if k == b"exif" else v_str + self.im_text[k_str] = v_str + self.check_text_memory(len(v_str)) + + return s + + def chunk_zTXt(self, pos: int, length: int) -> bytes: + # compressed text + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + try: + k, v = s.split(b"\0", 1) + except ValueError: + k = s + v = b"" + if v: + comp_method = v[0] + else: + comp_method = 0 + if comp_method != 0: + msg = f"Unknown compression method {comp_method} in zTXt chunk" + raise SyntaxError(msg) + try: + v = _safe_zlib_decompress(v[1:]) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + v = b"" + else: + raise + except zlib.error: + v = b"" + + if k: + k_str = k.decode("latin-1", "strict") + v_str = v.decode("latin-1", "replace") + + self.im_info[k_str] = self.im_text[k_str] = v_str + self.check_text_memory(len(v_str)) + + return s + + def chunk_iTXt(self, pos: int, length: int) -> bytes: + # international text + assert self.fp is not None + r = s = ImageFile._safe_read(self.fp, length) + try: + k, r = r.split(b"\0", 1) + except ValueError: + return s + if len(r) < 2: + return s + cf, cm, r = r[0], r[1], r[2:] + try: + lang, tk, v = r.split(b"\0", 2) + except ValueError: + return s + if cf != 0: + if cm == 0: + try: + v = _safe_zlib_decompress(v) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + else: + raise + except zlib.error: + return s + else: + return s + if k == b"XML:com.adobe.xmp": + self.im_info["xmp"] = v + try: + k_str = k.decode("latin-1", "strict") + lang_str = lang.decode("utf-8", "strict") + tk_str = tk.decode("utf-8", "strict") + v_str = v.decode("utf-8", "strict") + except UnicodeError: + return s + + self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str) + self.check_text_memory(len(v_str)) + + return s + + def chunk_eXIf(self, pos: int, length: int) -> bytes: + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + self.im_info["exif"] = b"Exif\x00\x00" + s + return s + + # APNG chunks + def chunk_acTL(self, pos: int, length: int) -> bytes: + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + if length < 8: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + msg = "APNG contains truncated acTL chunk" + raise ValueError(msg) + if self.im_n_frames is not None: + self.im_n_frames = None + warnings.warn("Invalid APNG, will use default PNG image if possible") + return s + n_frames = i32(s) + if n_frames == 0 or n_frames > 0x80000000: + warnings.warn("Invalid APNG, will use default PNG image if possible") + return s + self.im_n_frames = n_frames + self.im_info["loop"] = i32(s, 4) + self.im_custom_mimetype = "image/apng" + return s + + def chunk_fcTL(self, pos: int, length: int) -> bytes: + assert self.fp is not None + s = ImageFile._safe_read(self.fp, length) + if length < 26: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + msg = "APNG contains truncated fcTL chunk" + raise ValueError(msg) + seq = i32(s) + if (self._seq_num is None and seq != 0) or ( + self._seq_num is not None and self._seq_num != seq - 1 + ): + msg = "APNG contains frame sequence errors" + raise SyntaxError(msg) + self._seq_num = seq + width, height = i32(s, 4), i32(s, 8) + px, py = i32(s, 12), i32(s, 16) + im_w, im_h = self.im_size + if px + width > im_w or py + height > im_h: + msg = "APNG contains invalid frames" + raise SyntaxError(msg) + self.im_info["bbox"] = (px, py, px + width, py + height) + delay_num, delay_den = i16(s, 20), i16(s, 22) + if delay_den == 0: + delay_den = 100 + self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000 + self.im_info["disposal"] = s[24] + self.im_info["blend"] = s[25] + return s + + def chunk_fdAT(self, pos: int, length: int) -> bytes: + assert self.fp is not None + if length < 4: + if ImageFile.LOAD_TRUNCATED_IMAGES: + s = ImageFile._safe_read(self.fp, length) + return s + msg = "APNG contains truncated fDAT chunk" + raise ValueError(msg) + s = ImageFile._safe_read(self.fp, 4) + seq = i32(s) + if self._seq_num != seq - 1: + msg = "APNG contains frame sequence errors" + raise SyntaxError(msg) + self._seq_num = seq + return self.chunk_IDAT(pos + 4, length - 4) + + +# -------------------------------------------------------------------- +# PNG reader + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(_MAGIC) + + +## +# Image plugin for PNG images. + + +class PngImageFile(ImageFile.ImageFile): + format = "PNG" + format_description = "Portable network graphics" + + def _open(self) -> None: + if not _accept(self.fp.read(8)): + msg = "not a PNG file" + raise SyntaxError(msg) + self._fp = self.fp + self.__frame = 0 + + # + # Parse headers up to the first IDAT or fDAT chunk + + self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = [] + self.png: PngStream | None = PngStream(self.fp) + + while True: + # + # get next chunk + + cid, pos, length = self.png.read() + + try: + s = self.png.call(cid, pos, length) + except EOFError: + break + except AttributeError: + logger.debug("%r %s %s (unknown)", cid, pos, length) + s = ImageFile._safe_read(self.fp, length) + if cid[1:2].islower(): + self.private_chunks.append((cid, s)) + + self.png.crc(cid, s) + + # + # Copy relevant attributes from the PngStream. An alternative + # would be to let the PngStream class modify these attributes + # directly, but that introduces circular references which are + # difficult to break if things go wrong in the decoder... + # (believe me, I've tried ;-) + + self._mode = self.png.im_mode + self._size = self.png.im_size + self.info = self.png.im_info + self._text: dict[str, str | iTXt] | None = None + self.tile = self.png.im_tile + self.custom_mimetype = self.png.im_custom_mimetype + self.n_frames = self.png.im_n_frames or 1 + self.default_image = self.info.get("default_image", False) + + if self.png.im_palette: + rawmode, data = self.png.im_palette + self.palette = ImagePalette.raw(rawmode, data) + + if cid == b"fdAT": + self.__prepare_idat = length - 4 + else: + self.__prepare_idat = length # used by load_prepare() + + if self.png.im_n_frames is not None: + self._close_exclusive_fp_after_loading = False + self.png.save_rewind() + self.__rewind_idat = self.__prepare_idat + self.__rewind = self._fp.tell() + if self.default_image: + # IDAT chunk contains default image and not first animation frame + self.n_frames += 1 + self._seek(0) + self.is_animated = self.n_frames > 1 + + @property + def text(self) -> dict[str, str | iTXt]: + # experimental + if self._text is None: + # iTxt, tEXt and zTXt chunks may appear at the end of the file + # So load the file to ensure that they are read + if self.is_animated: + frame = self.__frame + # for APNG, seek to the final frame before loading + self.seek(self.n_frames - 1) + self.load() + if self.is_animated: + self.seek(frame) + assert self._text is not None + return self._text + + def verify(self) -> None: + """Verify PNG file""" + + if self.fp is None: + msg = "verify must be called directly after open" + raise RuntimeError(msg) + + # back up to beginning of IDAT block + self.fp.seek(self.tile[0][2] - 8) + + assert self.png is not None + self.png.verify() + self.png.close() + + if self._exclusive_fp: + self.fp.close() + self.fp = None + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + if frame < self.__frame: + self._seek(0, True) + + last_frame = self.__frame + for f in range(self.__frame + 1, frame + 1): + try: + self._seek(f) + except EOFError as e: + self.seek(last_frame) + msg = "no more images in APNG file" + raise EOFError(msg) from e + + def _seek(self, frame: int, rewind: bool = False) -> None: + assert self.png is not None + if isinstance(self._fp, DeferredError): + raise self._fp.ex + + self.dispose: _imaging.ImagingCore | None + dispose_extent = None + if frame == 0: + if rewind: + self._fp.seek(self.__rewind) + self.png.rewind() + self.__prepare_idat = self.__rewind_idat + self._im = None + self.info = self.png.im_info + self.tile = self.png.im_tile + self.fp = self._fp + self._prev_im = None + self.dispose = None + self.default_image = self.info.get("default_image", False) + self.dispose_op = self.info.get("disposal") + self.blend_op = self.info.get("blend") + dispose_extent = self.info.get("bbox") + self.__frame = 0 + else: + if frame != self.__frame + 1: + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) + + # ensure previous frame was loaded + self.load() + + if self.dispose: + self.im.paste(self.dispose, self.dispose_extent) + self._prev_im = self.im.copy() + + self.fp = self._fp + + # advance to the next frame + if self.__prepare_idat: + ImageFile._safe_read(self.fp, self.__prepare_idat) + self.__prepare_idat = 0 + frame_start = False + while True: + self.fp.read(4) # CRC + + try: + cid, pos, length = self.png.read() + except (struct.error, SyntaxError): + break + + if cid == b"IEND": + msg = "No more images in APNG file" + raise EOFError(msg) + if cid == b"fcTL": + if frame_start: + # there must be at least one fdAT chunk between fcTL chunks + msg = "APNG missing frame data" + raise SyntaxError(msg) + frame_start = True + + try: + self.png.call(cid, pos, length) + except UnicodeDecodeError: + break + except EOFError: + if cid == b"fdAT": + length -= 4 + if frame_start: + self.__prepare_idat = length + break + ImageFile._safe_read(self.fp, length) + except AttributeError: + logger.debug("%r %s %s (unknown)", cid, pos, length) + ImageFile._safe_read(self.fp, length) + + self.__frame = frame + self.tile = self.png.im_tile + self.dispose_op = self.info.get("disposal") + self.blend_op = self.info.get("blend") + dispose_extent = self.info.get("bbox") + + if not self.tile: + msg = "image not found in APNG frame" + raise EOFError(msg) + if dispose_extent: + self.dispose_extent: tuple[float, float, float, float] = dispose_extent + + # setup frame disposal (actual disposal done when needed in the next _seek()) + if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: + self.dispose_op = Disposal.OP_BACKGROUND + + self.dispose = None + if self.dispose_op == Disposal.OP_PREVIOUS: + if self._prev_im: + self.dispose = self._prev_im.copy() + self.dispose = self._crop(self.dispose, self.dispose_extent) + elif self.dispose_op == Disposal.OP_BACKGROUND: + self.dispose = Image.core.fill(self.mode, self.size) + self.dispose = self._crop(self.dispose, self.dispose_extent) + + def tell(self) -> int: + return self.__frame + + def load_prepare(self) -> None: + """internal: prepare to read PNG file""" + + if self.info.get("interlace"): + self.decoderconfig = self.decoderconfig + (1,) + + self.__idat = self.__prepare_idat # used by load_read() + ImageFile.ImageFile.load_prepare(self) + + def load_read(self, read_bytes: int) -> bytes: + """internal: read more image data""" + + assert self.png is not None + while self.__idat == 0: + # end of chunk, skip forward to next one + + self.fp.read(4) # CRC + + cid, pos, length = self.png.read() + + if cid not in [b"IDAT", b"DDAT", b"fdAT"]: + self.png.push(cid, pos, length) + return b"" + + if cid == b"fdAT": + try: + self.png.call(cid, pos, length) + except EOFError: + pass + self.__idat = length - 4 # sequence_num has already been read + else: + self.__idat = length # empty chunks are allowed + + # read more data from this chunk + if read_bytes <= 0: + read_bytes = self.__idat + else: + read_bytes = min(read_bytes, self.__idat) + + self.__idat = self.__idat - read_bytes + + return self.fp.read(read_bytes) + + def load_end(self) -> None: + """internal: finished reading image data""" + assert self.png is not None + if self.__idat != 0: + self.fp.read(self.__idat) + while True: + self.fp.read(4) # CRC + + try: + cid, pos, length = self.png.read() + except (struct.error, SyntaxError): + break + + if cid == b"IEND": + break + elif cid == b"fcTL" and self.is_animated: + # start of the next frame, stop reading + self.__prepare_idat = 0 + self.png.push(cid, pos, length) + break + + try: + self.png.call(cid, pos, length) + except UnicodeDecodeError: + break + except EOFError: + if cid == b"fdAT": + length -= 4 + try: + ImageFile._safe_read(self.fp, length) + except OSError as e: + if ImageFile.LOAD_TRUNCATED_IMAGES: + break + else: + raise e + except AttributeError: + logger.debug("%r %s %s (unknown)", cid, pos, length) + s = ImageFile._safe_read(self.fp, length) + if cid[1:2].islower(): + self.private_chunks.append((cid, s, True)) + self._text = self.png.im_text + if not self.is_animated: + self.png.close() + self.png = None + else: + if self._prev_im and self.blend_op == Blend.OP_OVER: + updated = self._crop(self.im, self.dispose_extent) + if self.im.mode == "RGB" and "transparency" in self.info: + mask = updated.convert_transparent( + "RGBA", self.info["transparency"] + ) + else: + if self.im.mode == "P" and "transparency" in self.info: + t = self.info["transparency"] + if isinstance(t, bytes): + updated.putpalettealphas(t) + elif isinstance(t, int): + updated.putpalettealpha(t) + mask = updated.convert("RGBA") + self._prev_im.paste(updated, self.dispose_extent, mask) + self.im = self._prev_im + + def _getexif(self) -> dict[int, Any] | None: + if "exif" not in self.info: + self.load() + if "exif" not in self.info and "Raw profile type exif" not in self.info: + return None + return self.getexif()._get_merged_dict() + + def getexif(self) -> Image.Exif: + if "exif" not in self.info: + self.load() + + return super().getexif() + + +# -------------------------------------------------------------------- +# PNG writer + +_OUTMODES = { + # supported PIL modes, and corresponding rawmode, bit depth and color type + "1": ("1", b"\x01", b"\x00"), + "L;1": ("L;1", b"\x01", b"\x00"), + "L;2": ("L;2", b"\x02", b"\x00"), + "L;4": ("L;4", b"\x04", b"\x00"), + "L": ("L", b"\x08", b"\x00"), + "LA": ("LA", b"\x08", b"\x04"), + "I": ("I;16B", b"\x10", b"\x00"), + "I;16": ("I;16B", b"\x10", b"\x00"), + "I;16B": ("I;16B", b"\x10", b"\x00"), + "P;1": ("P;1", b"\x01", b"\x03"), + "P;2": ("P;2", b"\x02", b"\x03"), + "P;4": ("P;4", b"\x04", b"\x03"), + "P": ("P", b"\x08", b"\x03"), + "RGB": ("RGB", b"\x08", b"\x02"), + "RGBA": ("RGBA", b"\x08", b"\x06"), +} + + +def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None: + """Write a PNG chunk (including CRC field)""" + + byte_data = b"".join(data) + + fp.write(o32(len(byte_data)) + cid) + fp.write(byte_data) + crc = _crc32(byte_data, _crc32(cid)) + fp.write(o32(crc)) + + +class _idat: + # wrap output from the encoder in IDAT chunks + + def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None: + self.fp = fp + self.chunk = chunk + + def write(self, data: bytes) -> None: + self.chunk(self.fp, b"IDAT", data) + + +class _fdat: + # wrap encoder output in fdAT chunks + + def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None: + self.fp = fp + self.chunk = chunk + self.seq_num = seq_num + + def write(self, data: bytes) -> None: + self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) + self.seq_num += 1 + + +class _Frame(NamedTuple): + im: Image.Image + bbox: tuple[int, int, int, int] | None + encoderinfo: dict[str, Any] + + +def _write_multiple_frames( + im: Image.Image, + fp: IO[bytes], + chunk: Callable[..., None], + mode: str, + rawmode: str, + default_image: Image.Image | None, + append_images: list[Image.Image], +) -> Image.Image | None: + duration = im.encoderinfo.get("duration") + loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) + disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) + blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) + + if default_image: + chain = itertools.chain(append_images) + else: + chain = itertools.chain([im], append_images) + + im_frames: list[_Frame] = [] + frame_count = 0 + for im_seq in chain: + for im_frame in ImageSequence.Iterator(im_seq): + if im_frame.mode == mode: + im_frame = im_frame.copy() + else: + im_frame = im_frame.convert(mode) + encoderinfo = im.encoderinfo.copy() + if isinstance(duration, (list, tuple)): + encoderinfo["duration"] = duration[frame_count] + elif duration is None and "duration" in im_frame.info: + encoderinfo["duration"] = im_frame.info["duration"] + if isinstance(disposal, (list, tuple)): + encoderinfo["disposal"] = disposal[frame_count] + if isinstance(blend, (list, tuple)): + encoderinfo["blend"] = blend[frame_count] + frame_count += 1 + + if im_frames: + previous = im_frames[-1] + prev_disposal = previous.encoderinfo.get("disposal") + prev_blend = previous.encoderinfo.get("blend") + if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2: + prev_disposal = Disposal.OP_BACKGROUND + + if prev_disposal == Disposal.OP_BACKGROUND: + base_im = previous.im.copy() + dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) + bbox = previous.bbox + if bbox: + dispose = dispose.crop(bbox) + else: + bbox = (0, 0) + im.size + base_im.paste(dispose, bbox) + elif prev_disposal == Disposal.OP_PREVIOUS: + base_im = im_frames[-2].im + else: + base_im = previous.im + delta = ImageChops.subtract_modulo( + im_frame.convert("RGBA"), base_im.convert("RGBA") + ) + bbox = delta.getbbox(alpha_only=False) + if ( + not bbox + and prev_disposal == encoderinfo.get("disposal") + and prev_blend == encoderinfo.get("blend") + and "duration" in encoderinfo + ): + previous.encoderinfo["duration"] += encoderinfo["duration"] + continue + else: + bbox = None + im_frames.append(_Frame(im_frame, bbox, encoderinfo)) + + if len(im_frames) == 1 and not default_image: + return im_frames[0].im + + # animation control + chunk( + fp, + b"acTL", + o32(len(im_frames)), # 0: num_frames + o32(loop), # 4: num_plays + ) + + # default image IDAT (if it exists) + if default_image: + if im.mode != mode: + im = im.convert(mode) + ImageFile._save( + im, + cast(IO[bytes], _idat(fp, chunk)), + [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)], + ) + + seq_num = 0 + for frame, frame_data in enumerate(im_frames): + im_frame = frame_data.im + if not frame_data.bbox: + bbox = (0, 0) + im_frame.size + else: + bbox = frame_data.bbox + im_frame = im_frame.crop(bbox) + size = im_frame.size + encoderinfo = frame_data.encoderinfo + frame_duration = int(round(encoderinfo.get("duration", 0))) + frame_disposal = encoderinfo.get("disposal", disposal) + frame_blend = encoderinfo.get("blend", blend) + # frame control + chunk( + fp, + b"fcTL", + o32(seq_num), # sequence_number + o32(size[0]), # width + o32(size[1]), # height + o32(bbox[0]), # x_offset + o32(bbox[1]), # y_offset + o16(frame_duration), # delay_numerator + o16(1000), # delay_denominator + o8(frame_disposal), # dispose_op + o8(frame_blend), # blend_op + ) + seq_num += 1 + # frame data + if frame == 0 and not default_image: + # first frame must be in IDAT chunks for backwards compatibility + ImageFile._save( + im_frame, + cast(IO[bytes], _idat(fp, chunk)), + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], + ) + else: + fdat_chunks = _fdat(fp, chunk, seq_num) + ImageFile._save( + im_frame, + cast(IO[bytes], fdat_chunks), + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], + ) + seq_num = fdat_chunks.seq_num + return None + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +def _save( + im: Image.Image, + fp: IO[bytes], + filename: str | bytes, + chunk: Callable[..., None] = putchunk, + save_all: bool = False, +) -> None: + # save an image to disk (called by the save method) + + if save_all: + default_image = im.encoderinfo.get( + "default_image", im.info.get("default_image") + ) + modes = set() + sizes = set() + append_images = im.encoderinfo.get("append_images", []) + for im_seq in itertools.chain([im], append_images): + for im_frame in ImageSequence.Iterator(im_seq): + modes.add(im_frame.mode) + sizes.add(im_frame.size) + for mode in ("RGBA", "RGB", "P"): + if mode in modes: + break + else: + mode = modes.pop() + size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2)) + else: + size = im.size + mode = im.mode + + outmode = mode + if mode == "P": + # + # attempt to minimize storage requirements for palette images + if "bits" in im.encoderinfo: + # number of bits specified by user + colors = min(1 << im.encoderinfo["bits"], 256) + else: + # check palette contents + if im.palette: + colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1) + else: + colors = 256 + + if colors <= 16: + if colors <= 2: + bits = 1 + elif colors <= 4: + bits = 2 + else: + bits = 4 + outmode += f";{bits}" + + # encoder options + im.encoderconfig = ( + im.encoderinfo.get("optimize", False), + im.encoderinfo.get("compress_level", -1), + im.encoderinfo.get("compress_type", -1), + im.encoderinfo.get("dictionary", b""), + ) + + # get the corresponding PNG mode + try: + rawmode, bit_depth, color_type = _OUTMODES[outmode] + except KeyError as e: + msg = f"cannot write mode {mode} as PNG" + raise OSError(msg) from e + if outmode == "I": + deprecate("Saving I mode images as PNG", 13, stacklevel=4) + + # + # write minimal PNG file + + fp.write(_MAGIC) + + chunk( + fp, + b"IHDR", + o32(size[0]), # 0: size + o32(size[1]), + bit_depth, + color_type, + b"\0", # 10: compression + b"\0", # 11: filter category + b"\0", # 12: interlace flag + ) + + chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"] + + icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) + if icc: + # ICC profile + # according to PNG spec, the iCCP chunk contains: + # Profile name 1-79 bytes (character string) + # Null separator 1 byte (null character) + # Compression method 1 byte (0) + # Compressed profile n bytes (zlib with deflate compression) + name = b"ICC Profile" + data = name + b"\0\0" + zlib.compress(icc) + chunk(fp, b"iCCP", data) + + # You must either have sRGB or iCCP. + # Disallow sRGB chunks when an iCCP-chunk has been emitted. + chunks.remove(b"sRGB") + + info = im.encoderinfo.get("pnginfo") + if info: + chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] + for info_chunk in info.chunks: + cid, data = info_chunk[:2] + if cid in chunks: + chunks.remove(cid) + chunk(fp, cid, data) + elif cid in chunks_multiple_allowed: + chunk(fp, cid, data) + elif cid[1:2].islower(): + # Private chunk + after_idat = len(info_chunk) == 3 and info_chunk[2] + if not after_idat: + chunk(fp, cid, data) + + if im.mode == "P": + palette_byte_number = colors * 3 + palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] + while len(palette_bytes) < palette_byte_number: + palette_bytes += b"\0" + chunk(fp, b"PLTE", palette_bytes) + + transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None)) + + if transparency or transparency == 0: + if im.mode == "P": + # limit to actual palette size + alpha_bytes = colors + if isinstance(transparency, bytes): + chunk(fp, b"tRNS", transparency[:alpha_bytes]) + else: + transparency = max(0, min(255, transparency)) + alpha = b"\xff" * transparency + b"\0" + chunk(fp, b"tRNS", alpha[:alpha_bytes]) + elif im.mode in ("1", "L", "I", "I;16"): + transparency = max(0, min(65535, transparency)) + chunk(fp, b"tRNS", o16(transparency)) + elif im.mode == "RGB": + red, green, blue = transparency + chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) + else: + if "transparency" in im.encoderinfo: + # don't bother with transparency if it's an RGBA + # and it's in the info dict. It's probably just stale. + msg = "cannot use transparency for this mode" + raise OSError(msg) + else: + if im.mode == "P" and im.im.getpalettemode() == "RGBA": + alpha = im.im.getpalette("RGBA", "A") + alpha_bytes = colors + chunk(fp, b"tRNS", alpha[:alpha_bytes]) + + dpi = im.encoderinfo.get("dpi") + if dpi: + chunk( + fp, + b"pHYs", + o32(int(dpi[0] / 0.0254 + 0.5)), + o32(int(dpi[1] / 0.0254 + 0.5)), + b"\x01", + ) + + if info: + chunks = [b"bKGD", b"hIST"] + for info_chunk in info.chunks: + cid, data = info_chunk[:2] + if cid in chunks: + chunks.remove(cid) + chunk(fp, cid, data) + + exif = im.encoderinfo.get("exif") + if exif: + if isinstance(exif, Image.Exif): + exif = exif.tobytes(8) + if exif.startswith(b"Exif\x00\x00"): + exif = exif[6:] + chunk(fp, b"eXIf", exif) + + single_im: Image.Image | None = im + if save_all: + single_im = _write_multiple_frames( + im, fp, chunk, mode, rawmode, default_image, append_images + ) + if single_im: + ImageFile._save( + single_im, + cast(IO[bytes], _idat(fp, chunk)), + [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)], + ) + + if info: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] + if cid[1:2].islower(): + # Private chunk + after_idat = len(info_chunk) == 3 and info_chunk[2] + if after_idat: + chunk(fp, cid, data) + + chunk(fp, b"IEND", b"") + + if hasattr(fp, "flush"): + fp.flush() + + +# -------------------------------------------------------------------- +# PNG chunk converter + + +def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]: + """Return a list of PNG chunks representing this image.""" + from io import BytesIO + + chunks = [] + + def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None: + byte_data = b"".join(data) + crc = o32(_crc32(byte_data, _crc32(cid))) + chunks.append((cid, byte_data, crc)) + + fp = BytesIO() + + try: + im.encoderinfo = params + _save(im, fp, "", append) + finally: + del im.encoderinfo + + return chunks + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(PngImageFile.format, PngImageFile, _accept) +Image.register_save(PngImageFile.format, _save) +Image.register_save_all(PngImageFile.format, _save_all) + +Image.register_extensions(PngImageFile.format, [".png", ".apng"]) + +Image.register_mime(PngImageFile.format, "image/png") diff --git a/python_modules/PIL/PpmImagePlugin.py b/python_modules/PIL/PpmImagePlugin.py new file mode 100644 index 000000000..db34d107a --- /dev/null +++ b/python_modules/PIL/PpmImagePlugin.py @@ -0,0 +1,375 @@ +# +# The Python Imaging Library. +# $Id$ +# +# PPM support for PIL +# +# History: +# 96-03-24 fl Created +# 98-03-06 fl Write RGBA images (as RGB, that is) +# +# Copyright (c) Secret Labs AB 1997-98. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import math +from typing import IO + +from . import Image, ImageFile +from ._binary import i16be as i16 +from ._binary import o8 +from ._binary import o32le as o32 + +# +# -------------------------------------------------------------------- + +b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" + +MODES = { + # standard + b"P1": "1", + b"P2": "L", + b"P3": "RGB", + b"P4": "1", + b"P5": "L", + b"P6": "RGB", + # extensions + b"P0CMYK": "CMYK", + b"Pf": "F", + # PIL extensions (for test purposes only) + b"PyP": "P", + b"PyRGBA": "RGBA", + b"PyCMYK": "CMYK", +} + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"P") and prefix[1] in b"0123456fy" + + +## +# Image plugin for PBM, PGM, and PPM images. + + +class PpmImageFile(ImageFile.ImageFile): + format = "PPM" + format_description = "Pbmplus image" + + def _read_magic(self) -> bytes: + assert self.fp is not None + + magic = b"" + # read until whitespace or longest available magic number + for _ in range(6): + c = self.fp.read(1) + if not c or c in b_whitespace: + break + magic += c + return magic + + def _read_token(self) -> bytes: + assert self.fp is not None + + token = b"" + while len(token) <= 10: # read until next whitespace or limit of 10 characters + c = self.fp.read(1) + if not c: + break + elif c in b_whitespace: # token ended + if not token: + # skip whitespace at start + continue + break + elif c == b"#": + # ignores rest of the line; stops at CR, LF or EOF + while self.fp.read(1) not in b"\r\n": + pass + continue + token += c + if not token: + # Token was not even 1 byte + msg = "Reached EOF while reading header" + raise ValueError(msg) + elif len(token) > 10: + msg_too_long = b"Token too long in file header: %s" % token + raise ValueError(msg_too_long) + return token + + def _open(self) -> None: + assert self.fp is not None + + magic_number = self._read_magic() + try: + mode = MODES[magic_number] + except KeyError: + msg = "not a PPM file" + raise SyntaxError(msg) + self._mode = mode + + if magic_number in (b"P1", b"P4"): + self.custom_mimetype = "image/x-portable-bitmap" + elif magic_number in (b"P2", b"P5"): + self.custom_mimetype = "image/x-portable-graymap" + elif magic_number in (b"P3", b"P6"): + self.custom_mimetype = "image/x-portable-pixmap" + + self._size = int(self._read_token()), int(self._read_token()) + + decoder_name = "raw" + if magic_number in (b"P1", b"P2", b"P3"): + decoder_name = "ppm_plain" + + args: str | tuple[str | int, ...] + if mode == "1": + args = "1;I" + elif mode == "F": + scale = float(self._read_token()) + if scale == 0.0 or not math.isfinite(scale): + msg = "scale must be finite and non-zero" + raise ValueError(msg) + self.info["scale"] = abs(scale) + + rawmode = "F;32F" if scale < 0 else "F;32BF" + args = (rawmode, 0, -1) + else: + maxval = int(self._read_token()) + if not 0 < maxval < 65536: + msg = "maxval must be greater than 0 and less than 65536" + raise ValueError(msg) + if maxval > 255 and mode == "L": + self._mode = "I" + + rawmode = mode + if decoder_name != "ppm_plain": + # If maxval matches a bit depth, use the raw decoder directly + if maxval == 65535 and mode == "L": + rawmode = "I;16B" + elif maxval != 255: + decoder_name = "ppm" + + args = rawmode if decoder_name == "raw" else (rawmode, maxval) + self.tile = [ + ImageFile._Tile(decoder_name, (0, 0) + self.size, self.fp.tell(), args) + ] + + +# +# -------------------------------------------------------------------- + + +class PpmPlainDecoder(ImageFile.PyDecoder): + _pulls_fd = True + _comment_spans: bool + + def _read_block(self) -> bytes: + assert self.fd is not None + + return self.fd.read(ImageFile.SAFEBLOCK) + + def _find_comment_end(self, block: bytes, start: int = 0) -> int: + a = block.find(b"\n", start) + b = block.find(b"\r", start) + return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) + + def _ignore_comments(self, block: bytes) -> bytes: + if self._comment_spans: + # Finish current comment + while block: + comment_end = self._find_comment_end(block) + if comment_end != -1: + # Comment ends in this block + # Delete tail of comment + block = block[comment_end + 1 :] + break + else: + # Comment spans whole block + # So read the next block, looking for the end + block = self._read_block() + + # Search for any further comments + self._comment_spans = False + while True: + comment_start = block.find(b"#") + if comment_start == -1: + # No comment found + break + comment_end = self._find_comment_end(block, comment_start) + if comment_end != -1: + # Comment ends in this block + # Delete comment + block = block[:comment_start] + block[comment_end + 1 :] + else: + # Comment continues to next block(s) + block = block[:comment_start] + self._comment_spans = True + break + return block + + def _decode_bitonal(self) -> bytearray: + """ + This is a separate method because in the plain PBM format, all data tokens are + exactly one byte, so the inter-token whitespace is optional. + """ + data = bytearray() + total_bytes = self.state.xsize * self.state.ysize + + while len(data) != total_bytes: + block = self._read_block() # read next block + if not block: + # eof + break + + block = self._ignore_comments(block) + + tokens = b"".join(block.split()) + for token in tokens: + if token not in (48, 49): + msg = b"Invalid token for this mode: %s" % bytes([token]) + raise ValueError(msg) + data = (data + tokens)[:total_bytes] + invert = bytes.maketrans(b"01", b"\xff\x00") + return data.translate(invert) + + def _decode_blocks(self, maxval: int) -> bytearray: + data = bytearray() + max_len = 10 + out_byte_count = 4 if self.mode == "I" else 1 + out_max = 65535 if self.mode == "I" else 255 + bands = Image.getmodebands(self.mode) + total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count + + half_token = b"" + while len(data) != total_bytes: + block = self._read_block() # read next block + if not block: + if half_token: + block = bytearray(b" ") # flush half_token + else: + # eof + break + + block = self._ignore_comments(block) + + if half_token: + block = half_token + block # stitch half_token to new block + half_token = b"" + + tokens = block.split() + + if block and not block[-1:].isspace(): # block might split token + half_token = tokens.pop() # save half token for later + if len(half_token) > max_len: # prevent buildup of half_token + msg = ( + b"Token too long found in data: %s" % half_token[: max_len + 1] + ) + raise ValueError(msg) + + for token in tokens: + if len(token) > max_len: + msg = b"Token too long found in data: %s" % token[: max_len + 1] + raise ValueError(msg) + value = int(token) + if value < 0: + msg_str = f"Channel value is negative: {value}" + raise ValueError(msg_str) + if value > maxval: + msg_str = f"Channel value too large for this mode: {value}" + raise ValueError(msg_str) + value = round(value / maxval * out_max) + data += o32(value) if self.mode == "I" else o8(value) + if len(data) == total_bytes: # finished! + break + return data + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + self._comment_spans = False + if self.mode == "1": + data = self._decode_bitonal() + rawmode = "1;8" + else: + maxval = self.args[-1] + data = self._decode_blocks(maxval) + rawmode = "I;32" if self.mode == "I" else self.mode + self.set_as_raw(bytes(data), rawmode) + return -1, 0 + + +class PpmDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + + data = bytearray() + maxval = self.args[-1] + in_byte_count = 1 if maxval < 256 else 2 + out_byte_count = 4 if self.mode == "I" else 1 + out_max = 65535 if self.mode == "I" else 255 + bands = Image.getmodebands(self.mode) + dest_length = self.state.xsize * self.state.ysize * bands * out_byte_count + while len(data) < dest_length: + pixels = self.fd.read(in_byte_count * bands) + if len(pixels) < in_byte_count * bands: + # eof + break + for b in range(bands): + value = ( + pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count) + ) + value = min(out_max, round(value / maxval * out_max)) + data += o32(value) if self.mode == "I" else o8(value) + rawmode = "I;32" if self.mode == "I" else self.mode + self.set_as_raw(bytes(data), rawmode) + return -1, 0 + + +# +# -------------------------------------------------------------------- + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode == "1": + rawmode, head = "1;I", b"P4" + elif im.mode == "L": + rawmode, head = "L", b"P5" + elif im.mode in ("I", "I;16"): + rawmode, head = "I;16B", b"P5" + elif im.mode in ("RGB", "RGBA"): + rawmode, head = "RGB", b"P6" + elif im.mode == "F": + rawmode, head = "F;32F", b"Pf" + else: + msg = f"cannot write mode {im.mode} as PPM" + raise OSError(msg) + fp.write(head + b"\n%d %d\n" % im.size) + if head == b"P6": + fp.write(b"255\n") + elif head == b"P5": + if rawmode == "L": + fp.write(b"255\n") + else: + fp.write(b"65535\n") + elif head == b"Pf": + fp.write(b"-1.0\n") + row_order = -1 if im.mode == "F" else 1 + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))] + ) + + +# +# -------------------------------------------------------------------- + + +Image.register_open(PpmImageFile.format, PpmImageFile, _accept) +Image.register_save(PpmImageFile.format, _save) + +Image.register_decoder("ppm", PpmDecoder) +Image.register_decoder("ppm_plain", PpmPlainDecoder) + +Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"]) + +Image.register_mime(PpmImageFile.format, "image/x-portable-anymap") diff --git a/python_modules/PIL/PsdImagePlugin.py b/python_modules/PIL/PsdImagePlugin.py new file mode 100644 index 000000000..f49aaeeb1 --- /dev/null +++ b/python_modules/PIL/PsdImagePlugin.py @@ -0,0 +1,333 @@ +# +# The Python Imaging Library +# $Id$ +# +# Adobe PSD 2.5/3.0 file handling +# +# History: +# 1995-09-01 fl Created +# 1997-01-03 fl Read most PSD images +# 1997-01-18 fl Fixed P and CMYK support +# 2001-10-21 fl Added seek/tell support (for layers) +# +# Copyright (c) 1997-2001 by Secret Labs AB. +# Copyright (c) 1995-2001 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +from functools import cached_property +from typing import IO + +from . import Image, ImageFile, ImagePalette +from ._binary import i8 +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import si16be as si16 +from ._binary import si32be as si32 +from ._util import DeferredError + +MODES = { + # (photoshop mode, bits) -> (pil mode, required channels) + (0, 1): ("1", 1), + (0, 8): ("L", 1), + (1, 8): ("L", 1), + (2, 8): ("P", 1), + (3, 8): ("RGB", 3), + (4, 8): ("CMYK", 4), + (7, 8): ("L", 1), # FIXME: multilayer + (8, 8): ("L", 1), # duotone + (9, 8): ("LAB", 3), +} + + +# --------------------------------------------------------------------. +# read PSD images + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"8BPS") + + +## +# Image plugin for Photoshop images. + + +class PsdImageFile(ImageFile.ImageFile): + format = "PSD" + format_description = "Adobe Photoshop" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + read = self.fp.read + + # + # header + + s = read(26) + if not _accept(s) or i16(s, 4) != 1: + msg = "not a PSD file" + raise SyntaxError(msg) + + psd_bits = i16(s, 22) + psd_channels = i16(s, 12) + psd_mode = i16(s, 24) + + mode, channels = MODES[(psd_mode, psd_bits)] + + if channels > psd_channels: + msg = "not enough channels" + raise OSError(msg) + if mode == "RGB" and psd_channels == 4: + mode = "RGBA" + channels = 4 + + self._mode = mode + self._size = i32(s, 18), i32(s, 14) + + # + # color mode data + + size = i32(read(4)) + if size: + data = read(size) + if mode == "P" and size == 768: + self.palette = ImagePalette.raw("RGB;L", data) + + # + # image resources + + self.resources = [] + + size = i32(read(4)) + if size: + # load resources + end = self.fp.tell() + size + while self.fp.tell() < end: + read(4) # signature + id = i16(read(2)) + name = read(i8(read(1))) + if not (len(name) & 1): + read(1) # padding + data = read(i32(read(4))) + if len(data) & 1: + read(1) # padding + self.resources.append((id, name, data)) + if id == 1039: # ICC profile + self.info["icc_profile"] = data + + # + # layer and mask information + + self._layers_position = None + + size = i32(read(4)) + if size: + end = self.fp.tell() + size + size = i32(read(4)) + if size: + self._layers_position = self.fp.tell() + self._layers_size = size + self.fp.seek(end) + self._n_frames: int | None = None + + # + # image descriptor + + self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels) + + # keep the file open + self._fp = self.fp + self.frame = 1 + self._min_frame = 1 + + @cached_property + def layers( + self, + ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: + layers = [] + if self._layers_position is not None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex + self._fp.seek(self._layers_position) + _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size)) + layers = _layerinfo(_layer_data, self._layers_size) + self._n_frames = len(layers) + return layers + + @property + def n_frames(self) -> int: + if self._n_frames is None: + self._n_frames = len(self.layers) + return self._n_frames + + @property + def is_animated(self) -> bool: + return len(self.layers) > 1 + + def seek(self, layer: int) -> None: + if not self._seek_check(layer): + return + if isinstance(self._fp, DeferredError): + raise self._fp.ex + + # seek to given layer (1..max) + _, mode, _, tile = self.layers[layer - 1] + self._mode = mode + self.tile = tile + self.frame = layer + self.fp = self._fp + + def tell(self) -> int: + # return layer number (0=image, 1..max=layers) + return self.frame + + +def _layerinfo( + fp: IO[bytes], ct_bytes: int +) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: + # read layerinfo block + layers = [] + + def read(size: int) -> bytes: + return ImageFile._safe_read(fp, size) + + ct = si16(read(2)) + + # sanity check + if ct_bytes < (abs(ct) * 20): + msg = "Layer block too short for number of layers requested" + raise SyntaxError(msg) + + for _ in range(abs(ct)): + # bounding box + y0 = si32(read(4)) + x0 = si32(read(4)) + y1 = si32(read(4)) + x1 = si32(read(4)) + + # image info + bands = [] + ct_types = i16(read(2)) + if ct_types > 4: + fp.seek(ct_types * 6 + 12, io.SEEK_CUR) + size = i32(read(4)) + fp.seek(size, io.SEEK_CUR) + continue + + for _ in range(ct_types): + type = i16(read(2)) + + if type == 65535: + b = "A" + else: + b = "RGBA"[type] + + bands.append(b) + read(4) # size + + # figure out the image mode + bands.sort() + if bands == ["R"]: + mode = "L" + elif bands == ["B", "G", "R"]: + mode = "RGB" + elif bands == ["A", "B", "G", "R"]: + mode = "RGBA" + else: + mode = "" # unknown + + # skip over blend flags and extra information + read(12) # filler + name = "" + size = i32(read(4)) # length of the extra data field + if size: + data_end = fp.tell() + size + + length = i32(read(4)) + if length: + fp.seek(length - 16, io.SEEK_CUR) + + length = i32(read(4)) + if length: + fp.seek(length, io.SEEK_CUR) + + length = i8(read(1)) + if length: + # Don't know the proper encoding, + # Latin-1 should be a good guess + name = read(length).decode("latin-1", "replace") + + fp.seek(data_end) + layers.append((name, mode, (x0, y0, x1, y1))) + + # get tiles + layerinfo = [] + for i, (name, mode, bbox) in enumerate(layers): + tile = [] + for m in mode: + t = _maketile(fp, m, bbox, 1) + if t: + tile.extend(t) + layerinfo.append((name, mode, bbox, tile)) + + return layerinfo + + +def _maketile( + file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int +) -> list[ImageFile._Tile]: + tiles = [] + read = file.read + + compression = i16(read(2)) + + xsize = bbox[2] - bbox[0] + ysize = bbox[3] - bbox[1] + + offset = file.tell() + + if compression == 0: + # + # raw compression + for channel in range(channels): + layer = mode[channel] + if mode == "CMYK": + layer += ";I" + tiles.append(ImageFile._Tile("raw", bbox, offset, layer)) + offset = offset + xsize * ysize + + elif compression == 1: + # + # packbits compression + i = 0 + bytecount = read(channels * ysize * 2) + offset = file.tell() + for channel in range(channels): + layer = mode[channel] + if mode == "CMYK": + layer += ";I" + tiles.append(ImageFile._Tile("packbits", bbox, offset, layer)) + for y in range(ysize): + offset = offset + i16(bytecount, i) + i += 2 + + file.seek(offset) + + if offset & 1: + read(1) # padding + + return tiles + + +# -------------------------------------------------------------------- +# registry + + +Image.register_open(PsdImageFile.format, PsdImageFile, _accept) + +Image.register_extension(PsdImageFile.format, ".psd") + +Image.register_mime(PsdImageFile.format, "image/vnd.adobe.photoshop") diff --git a/python_modules/PIL/QoiImagePlugin.py b/python_modules/PIL/QoiImagePlugin.py new file mode 100644 index 000000000..dba5d809f --- /dev/null +++ b/python_modules/PIL/QoiImagePlugin.py @@ -0,0 +1,234 @@ +# +# The Python Imaging Library. +# +# QOI support for PIL +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +from typing import IO + +from . import Image, ImageFile +from ._binary import i32be as i32 +from ._binary import o8 +from ._binary import o32be as o32 + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"qoif") + + +class QoiImageFile(ImageFile.ImageFile): + format = "QOI" + format_description = "Quite OK Image" + + def _open(self) -> None: + if not _accept(self.fp.read(4)): + msg = "not a QOI file" + raise SyntaxError(msg) + + self._size = i32(self.fp.read(4)), i32(self.fp.read(4)) + + channels = self.fp.read(1)[0] + self._mode = "RGB" if channels == 3 else "RGBA" + + self.fp.seek(1, os.SEEK_CUR) # colorspace + self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())] + + +class QoiDecoder(ImageFile.PyDecoder): + _pulls_fd = True + _previous_pixel: bytes | bytearray | None = None + _previously_seen_pixels: dict[int, bytes | bytearray] = {} + + def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: + self._previous_pixel = value + + r, g, b, a = value + hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + self._previously_seen_pixels[hash_value] = value + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + + self._previously_seen_pixels = {} + self._previous_pixel = bytearray((0, 0, 0, 255)) + + data = bytearray() + bands = Image.getmodebands(self.mode) + dest_length = self.state.xsize * self.state.ysize * bands + while len(data) < dest_length: + byte = self.fd.read(1)[0] + value: bytes | bytearray + if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB + value = bytearray(self.fd.read(3)) + self._previous_pixel[3:] + elif byte == 0b11111111: # QOI_OP_RGBA + value = self.fd.read(4) + else: + op = byte >> 6 + if op == 0: # QOI_OP_INDEX + op_index = byte & 0b00111111 + value = self._previously_seen_pixels.get( + op_index, bytearray((0, 0, 0, 0)) + ) + elif op == 1 and self._previous_pixel: # QOI_OP_DIFF + value = bytearray( + ( + (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) + % 256, + (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2) + % 256, + (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256, + self._previous_pixel[3], + ) + ) + elif op == 2 and self._previous_pixel: # QOI_OP_LUMA + second_byte = self.fd.read(1)[0] + diff_green = (byte & 0b00111111) - 32 + diff_red = ((second_byte & 0b11110000) >> 4) - 8 + diff_blue = (second_byte & 0b00001111) - 8 + + value = bytearray( + tuple( + (self._previous_pixel[i] + diff_green + diff) % 256 + for i, diff in enumerate((diff_red, 0, diff_blue)) + ) + ) + value += self._previous_pixel[3:] + elif op == 3 and self._previous_pixel: # QOI_OP_RUN + run_length = (byte & 0b00111111) + 1 + value = self._previous_pixel + if bands == 3: + value = value[:3] + data += value * run_length + continue + self._add_to_previous_pixels(value) + + if bands == 3: + value = value[:3] + data += value + self.set_as_raw(data) + return -1, 0 + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode == "RGB": + channels = 3 + elif im.mode == "RGBA": + channels = 4 + else: + msg = "Unsupported QOI image mode" + raise ValueError(msg) + + colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1 + + fp.write(b"qoif") + fp.write(o32(im.size[0])) + fp.write(o32(im.size[1])) + fp.write(o8(channels)) + fp.write(o8(colorspace)) + + ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)]) + + +class QoiEncoder(ImageFile.PyEncoder): + _pushes_fd = True + _previous_pixel: tuple[int, int, int, int] | None = None + _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {} + _run = 0 + + def _write_run(self) -> bytes: + data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN + self._run = 0 + return data + + def _delta(self, left: int, right: int) -> int: + result = (left - right) & 255 + if result >= 128: + result -= 256 + return result + + def encode(self, bufsize: int) -> tuple[int, int, bytes]: + assert self.im is not None + + self._previously_seen_pixels = {0: (0, 0, 0, 0)} + self._previous_pixel = (0, 0, 0, 255) + + data = bytearray() + w, h = self.im.size + bands = Image.getmodebands(self.mode) + + for y in range(h): + for x in range(w): + pixel = self.im.getpixel((x, y)) + if bands == 3: + pixel = (*pixel, 255) + + if pixel == self._previous_pixel: + self._run += 1 + if self._run == 62: + data += self._write_run() + else: + if self._run: + data += self._write_run() + + r, g, b, a = pixel + hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + if self._previously_seen_pixels.get(hash_value) == pixel: + data += o8(hash_value) # QOI_OP_INDEX + elif self._previous_pixel: + self._previously_seen_pixels[hash_value] = pixel + + prev_r, prev_g, prev_b, prev_a = self._previous_pixel + if prev_a == a: + delta_r = self._delta(r, prev_r) + delta_g = self._delta(g, prev_g) + delta_b = self._delta(b, prev_b) + + if ( + -2 <= delta_r < 2 + and -2 <= delta_g < 2 + and -2 <= delta_b < 2 + ): + data += o8( + 0b01000000 + | (delta_r + 2) << 4 + | (delta_g + 2) << 2 + | (delta_b + 2) + ) # QOI_OP_DIFF + else: + delta_gr = self._delta(delta_r, delta_g) + delta_gb = self._delta(delta_b, delta_g) + if ( + -8 <= delta_gr < 8 + and -32 <= delta_g < 32 + and -8 <= delta_gb < 8 + ): + data += o8( + 0b10000000 | (delta_g + 32) + ) # QOI_OP_LUMA + data += o8((delta_gr + 8) << 4 | (delta_gb + 8)) + else: + data += o8(0b11111110) # QOI_OP_RGB + data += bytes(pixel[:3]) + else: + data += o8(0b11111111) # QOI_OP_RGBA + data += bytes(pixel) + + self._previous_pixel = pixel + + if self._run: + data += self._write_run() + data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding + + return len(data), 0, data + + +Image.register_open(QoiImageFile.format, QoiImageFile, _accept) +Image.register_decoder("qoi", QoiDecoder) +Image.register_extension(QoiImageFile.format, ".qoi") + +Image.register_save(QoiImageFile.format, _save) +Image.register_encoder("qoi", QoiEncoder) diff --git a/python_modules/PIL/SgiImagePlugin.py b/python_modules/PIL/SgiImagePlugin.py new file mode 100644 index 000000000..853022150 --- /dev/null +++ b/python_modules/PIL/SgiImagePlugin.py @@ -0,0 +1,231 @@ +# +# The Python Imaging Library. +# $Id$ +# +# SGI image file handling +# +# See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli. +# +# +# +# History: +# 2017-22-07 mb Add RLE decompression +# 2016-16-10 mb Add save method without compression +# 1995-09-10 fl Created +# +# Copyright (c) 2016 by Mickael Bonfill. +# Copyright (c) 2008 by Karsten Hiddemann. +# Copyright (c) 1997 by Secret Labs AB. +# Copyright (c) 1995 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import os +import struct +from typing import IO + +from . import Image, ImageFile +from ._binary import i16be as i16 +from ._binary import o8 + + +def _accept(prefix: bytes) -> bool: + return len(prefix) >= 2 and i16(prefix) == 474 + + +MODES = { + (1, 1, 1): "L", + (1, 2, 1): "L", + (2, 1, 1): "L;16B", + (2, 2, 1): "L;16B", + (1, 3, 3): "RGB", + (2, 3, 3): "RGB;16B", + (1, 3, 4): "RGBA", + (2, 3, 4): "RGBA;16B", +} + + +## +# Image plugin for SGI images. +class SgiImageFile(ImageFile.ImageFile): + format = "SGI" + format_description = "SGI Image File Format" + + def _open(self) -> None: + # HEAD + assert self.fp is not None + + headlen = 512 + s = self.fp.read(headlen) + + if not _accept(s): + msg = "Not an SGI image file" + raise ValueError(msg) + + # compression : verbatim or RLE + compression = s[2] + + # bpc : 1 or 2 bytes (8bits or 16bits) + bpc = s[3] + + # dimension : 1, 2 or 3 (depending on xsize, ysize and zsize) + dimension = i16(s, 4) + + # xsize : width + xsize = i16(s, 6) + + # ysize : height + ysize = i16(s, 8) + + # zsize : channels count + zsize = i16(s, 10) + + # determine mode from bits/zsize + try: + rawmode = MODES[(bpc, dimension, zsize)] + except KeyError: + msg = "Unsupported SGI image mode" + raise ValueError(msg) + + self._size = xsize, ysize + self._mode = rawmode.split(";")[0] + if self.mode == "RGB": + self.custom_mimetype = "image/rgb" + + # orientation -1 : scanlines begins at the bottom-left corner + orientation = -1 + + # decoder info + if compression == 0: + pagesize = xsize * ysize * bpc + if bpc == 2: + self.tile = [ + ImageFile._Tile( + "SGI16", + (0, 0) + self.size, + headlen, + (self.mode, 0, orientation), + ) + ] + else: + self.tile = [] + offset = headlen + for layer in self.mode: + self.tile.append( + ImageFile._Tile( + "raw", (0, 0) + self.size, offset, (layer, 0, orientation) + ) + ) + offset += pagesize + elif compression == 1: + self.tile = [ + ImageFile._Tile( + "sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc) + ) + ] + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode not in {"RGB", "RGBA", "L"}: + msg = "Unsupported SGI image mode" + raise ValueError(msg) + + # Get the keyword arguments + info = im.encoderinfo + + # Byte-per-pixel precision, 1 = 8bits per pixel + bpc = info.get("bpc", 1) + + if bpc not in (1, 2): + msg = "Unsupported number of bytes per pixel" + raise ValueError(msg) + + # Flip the image, since the origin of SGI file is the bottom-left corner + orientation = -1 + # Define the file as SGI File Format + magic_number = 474 + # Run-Length Encoding Compression - Unsupported at this time + rle = 0 + + # X Dimension = width / Y Dimension = height + x, y = im.size + # Z Dimension: Number of channels + z = len(im.mode) + # Number of dimensions (x,y,z) + if im.mode == "L": + dimension = 1 if y == 1 else 2 + else: + dimension = 3 + + # Minimum Byte value + pinmin = 0 + # Maximum Byte value (255 = 8bits per pixel) + pinmax = 255 + # Image name (79 characters max, truncated below in write) + img_name = os.path.splitext(os.path.basename(filename))[0] + if isinstance(img_name, str): + img_name = img_name.encode("ascii", "ignore") + # Standard representation of pixel in the file + colormap = 0 + fp.write(struct.pack(">h", magic_number)) + fp.write(o8(rle)) + fp.write(o8(bpc)) + fp.write(struct.pack(">H", dimension)) + fp.write(struct.pack(">H", x)) + fp.write(struct.pack(">H", y)) + fp.write(struct.pack(">H", z)) + fp.write(struct.pack(">l", pinmin)) + fp.write(struct.pack(">l", pinmax)) + fp.write(struct.pack("4s", b"")) # dummy + fp.write(struct.pack("79s", img_name)) # truncates to 79 chars + fp.write(struct.pack("s", b"")) # force null byte after img_name + fp.write(struct.pack(">l", colormap)) + fp.write(struct.pack("404s", b"")) # dummy + + rawmode = "L" + if bpc == 2: + rawmode = "L;16B" + + for channel in im.split(): + fp.write(channel.tobytes("raw", rawmode, 0, orientation)) + + if hasattr(fp, "flush"): + fp.flush() + + +class SGI16Decoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + assert self.im is not None + + rawmode, stride, orientation = self.args + pagesize = self.state.xsize * self.state.ysize + zsize = len(self.mode) + self.fd.seek(512) + + for band in range(zsize): + channel = Image.new("L", (self.state.xsize, self.state.ysize)) + channel.frombytes( + self.fd.read(2 * pagesize), "raw", "L;16B", stride, orientation + ) + self.im.putband(channel.im, band) + + return -1, 0 + + +# +# registry + + +Image.register_decoder("SGI16", SGI16Decoder) +Image.register_open(SgiImageFile.format, SgiImageFile, _accept) +Image.register_save(SgiImageFile.format, _save) +Image.register_mime(SgiImageFile.format, "image/sgi") + +Image.register_extensions(SgiImageFile.format, [".bw", ".rgb", ".rgba", ".sgi"]) + +# End of file diff --git a/python_modules/PIL/SpiderImagePlugin.py b/python_modules/PIL/SpiderImagePlugin.py new file mode 100644 index 000000000..868019e80 --- /dev/null +++ b/python_modules/PIL/SpiderImagePlugin.py @@ -0,0 +1,331 @@ +# +# The Python Imaging Library. +# +# SPIDER image file handling +# +# History: +# 2004-08-02 Created BB +# 2006-03-02 added save method +# 2006-03-13 added support for stack images +# +# Copyright (c) 2004 by Health Research Inc. (HRI) RENSSELAER, NY 12144. +# Copyright (c) 2004 by William Baxter. +# Copyright (c) 2004 by Secret Labs AB. +# Copyright (c) 2004 by Fredrik Lundh. +# + +## +# Image plugin for the Spider image format. This format is used +# by the SPIDER software, in processing image data from electron +# microscopy and tomography. +## + +# +# SpiderImagePlugin.py +# +# The Spider image format is used by SPIDER software, in processing +# image data from electron microscopy and tomography. +# +# Spider home page: +# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html +# +# Details about the Spider image format: +# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html +# +from __future__ import annotations + +import os +import struct +import sys +from typing import IO, Any, cast + +from . import Image, ImageFile +from ._util import DeferredError + +TYPE_CHECKING = False + + +def isInt(f: Any) -> int: + try: + i = int(f) + if f - i == 0: + return 1 + else: + return 0 + except (ValueError, OverflowError): + return 0 + + +iforms = [1, 3, -11, -12, -21, -22] + + +# There is no magic number to identify Spider files, so just check a +# series of header locations to see if they have reasonable values. +# Returns no. of bytes in the header, if it is a valid Spider header, +# otherwise returns 0 + + +def isSpiderHeader(t: tuple[float, ...]) -> int: + h = (99,) + t # add 1 value so can use spider header index start=1 + # header values 1,2,5,12,13,22,23 should be integers + for i in [1, 2, 5, 12, 13, 22, 23]: + if not isInt(h[i]): + return 0 + # check iform + iform = int(h[5]) + if iform not in iforms: + return 0 + # check other header values + labrec = int(h[13]) # no. records in file header + labbyt = int(h[22]) # total no. of bytes in header + lenbyt = int(h[23]) # record length in bytes + if labbyt != (labrec * lenbyt): + return 0 + # looks like a valid header + return labbyt + + +def isSpiderImage(filename: str) -> int: + with open(filename, "rb") as fp: + f = fp.read(92) # read 23 * 4 bytes + t = struct.unpack(">23f", f) # try big-endian first + hdrlen = isSpiderHeader(t) + if hdrlen == 0: + t = struct.unpack("<23f", f) # little-endian + hdrlen = isSpiderHeader(t) + return hdrlen + + +class SpiderImageFile(ImageFile.ImageFile): + format = "SPIDER" + format_description = "Spider 2D image" + _close_exclusive_fp_after_loading = False + + def _open(self) -> None: + # check header + n = 27 * 4 # read 27 float values + f = self.fp.read(n) + + try: + self.bigendian = 1 + t = struct.unpack(">27f", f) # try big-endian first + hdrlen = isSpiderHeader(t) + if hdrlen == 0: + self.bigendian = 0 + t = struct.unpack("<27f", f) # little-endian + hdrlen = isSpiderHeader(t) + if hdrlen == 0: + msg = "not a valid Spider file" + raise SyntaxError(msg) + except struct.error as e: + msg = "not a valid Spider file" + raise SyntaxError(msg) from e + + h = (99,) + t # add 1 value : spider header index starts at 1 + iform = int(h[5]) + if iform != 1: + msg = "not a Spider 2D image" + raise SyntaxError(msg) + + self._size = int(h[12]), int(h[2]) # size in pixels (width, height) + self.istack = int(h[24]) + self.imgnumber = int(h[27]) + + if self.istack == 0 and self.imgnumber == 0: + # stk=0, img=0: a regular 2D image + offset = hdrlen + self._nimages = 1 + elif self.istack > 0 and self.imgnumber == 0: + # stk>0, img=0: Opening the stack for the first time + self.imgbytes = int(h[12]) * int(h[2]) * 4 + self.hdrlen = hdrlen + self._nimages = int(h[26]) + # Point to the first image in the stack + offset = hdrlen * 2 + self.imgnumber = 1 + elif self.istack == 0 and self.imgnumber > 0: + # stk=0, img>0: an image within the stack + offset = hdrlen + self.stkoffset + self.istack = 2 # So Image knows it's still a stack + else: + msg = "inconsistent stack header values" + raise SyntaxError(msg) + + if self.bigendian: + self.rawmode = "F;32BF" + else: + self.rawmode = "F;32F" + self._mode = "F" + + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)] + self._fp = self.fp # FIXME: hack + + @property + def n_frames(self) -> int: + return self._nimages + + @property + def is_animated(self) -> bool: + return self._nimages > 1 + + # 1st image index is zero (although SPIDER imgnumber starts at 1) + def tell(self) -> int: + if self.imgnumber < 1: + return 0 + else: + return self.imgnumber - 1 + + def seek(self, frame: int) -> None: + if self.istack == 0: + msg = "attempt to seek in a non-stack file" + raise EOFError(msg) + if not self._seek_check(frame): + return + if isinstance(self._fp, DeferredError): + raise self._fp.ex + self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) + self.fp = self._fp + self.fp.seek(self.stkoffset) + self._open() + + # returns a byte image after rescaling to 0..255 + def convert2byte(self, depth: int = 255) -> Image.Image: + extrema = self.getextrema() + assert isinstance(extrema[0], float) + minimum, maximum = cast(tuple[float, float], extrema) + m: float = 1 + if maximum != minimum: + m = depth / (maximum - minimum) + b = -m * minimum + return self.point(lambda i: i * m + b).convert("L") + + if TYPE_CHECKING: + from . import ImageTk + + # returns a ImageTk.PhotoImage object, after rescaling to 0..255 + def tkPhotoImage(self) -> ImageTk.PhotoImage: + from . import ImageTk + + return ImageTk.PhotoImage(self.convert2byte(), palette=256) + + +# -------------------------------------------------------------------- +# Image series + + +# given a list of filenames, return a list of images +def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None: + """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" + if filelist is None or len(filelist) < 1: + return None + + byte_imgs = [] + for img in filelist: + if not os.path.exists(img): + print(f"unable to find {img}") + continue + try: + with Image.open(img) as im: + assert isinstance(im, SpiderImageFile) + byte_im = im.convert2byte() + except Exception: + if not isSpiderImage(img): + print(f"{img} is not a Spider image file") + continue + byte_im.info["filename"] = img + byte_imgs.append(byte_im) + return byte_imgs + + +# -------------------------------------------------------------------- +# For saving images in Spider format + + +def makeSpiderHeader(im: Image.Image) -> list[bytes]: + nsam, nrow = im.size + lenbyt = nsam * 4 # There are labrec records in the header + labrec = int(1024 / lenbyt) + if 1024 % lenbyt != 0: + labrec += 1 + labbyt = labrec * lenbyt + nvalues = int(labbyt / 4) + if nvalues < 23: + return [] + + hdr = [0.0] * nvalues + + # NB these are Fortran indices + hdr[1] = 1.0 # nslice (=1 for an image) + hdr[2] = float(nrow) # number of rows per slice + hdr[3] = float(nrow) # number of records in the image + hdr[5] = 1.0 # iform for 2D image + hdr[12] = float(nsam) # number of pixels per line + hdr[13] = float(labrec) # number of records in file header + hdr[22] = float(labbyt) # total number of bytes in header + hdr[23] = float(lenbyt) # record length in bytes + + # adjust for Fortran indexing + hdr = hdr[1:] + hdr.append(0.0) + # pack binary data into a string + return [struct.pack("f", v) for v in hdr] + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode != "F": + im = im.convert("F") + + hdr = makeSpiderHeader(im) + if len(hdr) < 256: + msg = "Error creating Spider header" + raise OSError(msg) + + # write the SPIDER header + fp.writelines(hdr) + + rawmode = "F;32NF" # 32-bit native floating point + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) + + +def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + # get the filename extension and register it with Image + filename_ext = os.path.splitext(filename)[1] + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext + Image.register_extension(SpiderImageFile.format, ext) + _save(im, fp, filename) + + +# -------------------------------------------------------------------- + + +Image.register_open(SpiderImageFile.format, SpiderImageFile) +Image.register_save(SpiderImageFile.format, _save_spider) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") + sys.exit() + + filename = sys.argv[1] + if not isSpiderImage(filename): + print("input image must be in Spider format") + sys.exit() + + with Image.open(filename) as im: + print(f"image: {im}") + print(f"format: {im.format}") + print(f"size: {im.size}") + print(f"mode: {im.mode}") + print("max, min: ", end=" ") + print(im.getextrema()) + + if len(sys.argv) > 2: + outfile = sys.argv[2] + + # perform some image operation + im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + print( + f"saving a flipped version of {os.path.basename(filename)} " + f"as {outfile} " + ) + im.save(outfile, SpiderImageFile.format) diff --git a/python_modules/PIL/SunImagePlugin.py b/python_modules/PIL/SunImagePlugin.py new file mode 100644 index 000000000..8912379ea --- /dev/null +++ b/python_modules/PIL/SunImagePlugin.py @@ -0,0 +1,145 @@ +# +# The Python Imaging Library. +# $Id$ +# +# Sun image file handling +# +# History: +# 1995-09-10 fl Created +# 1996-05-28 fl Fixed 32-bit alignment +# 1998-12-29 fl Import ImagePalette module +# 2001-12-18 fl Fixed palette loading (from Jean-Claude Rimbault) +# +# Copyright (c) 1997-2001 by Secret Labs AB +# Copyright (c) 1995-1996 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +from . import Image, ImageFile, ImagePalette +from ._binary import i32be as i32 + + +def _accept(prefix: bytes) -> bool: + return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 + + +## +# Image plugin for Sun raster files. + + +class SunImageFile(ImageFile.ImageFile): + format = "SUN" + format_description = "Sun Raster File" + + def _open(self) -> None: + # The Sun Raster file header is 32 bytes in length + # and has the following format: + + # typedef struct _SunRaster + # { + # DWORD MagicNumber; /* Magic (identification) number */ + # DWORD Width; /* Width of image in pixels */ + # DWORD Height; /* Height of image in pixels */ + # DWORD Depth; /* Number of bits per pixel */ + # DWORD Length; /* Size of image data in bytes */ + # DWORD Type; /* Type of raster file */ + # DWORD ColorMapType; /* Type of color map */ + # DWORD ColorMapLength; /* Size of the color map in bytes */ + # } SUNRASTER; + + assert self.fp is not None + + # HEAD + s = self.fp.read(32) + if not _accept(s): + msg = "not an SUN raster file" + raise SyntaxError(msg) + + offset = 32 + + self._size = i32(s, 4), i32(s, 8) + + depth = i32(s, 12) + # data_length = i32(s, 16) # unreliable, ignore. + file_type = i32(s, 20) + palette_type = i32(s, 24) # 0: None, 1: RGB, 2: Raw/arbitrary + palette_length = i32(s, 28) + + if depth == 1: + self._mode, rawmode = "1", "1;I" + elif depth == 4: + self._mode, rawmode = "L", "L;4" + elif depth == 8: + self._mode = rawmode = "L" + elif depth == 24: + if file_type == 3: + self._mode, rawmode = "RGB", "RGB" + else: + self._mode, rawmode = "RGB", "BGR" + elif depth == 32: + if file_type == 3: + self._mode, rawmode = "RGB", "RGBX" + else: + self._mode, rawmode = "RGB", "BGRX" + else: + msg = "Unsupported Mode/Bit Depth" + raise SyntaxError(msg) + + if palette_length: + if palette_length > 1024: + msg = "Unsupported Color Palette Length" + raise SyntaxError(msg) + + if palette_type != 1: + msg = "Unsupported Palette Type" + raise SyntaxError(msg) + + offset = offset + palette_length + self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) + if self.mode == "L": + self._mode = "P" + rawmode = rawmode.replace("L", "P") + + # 16 bit boundaries on stride + stride = ((self.size[0] * depth + 15) // 16) * 2 + + # file type: Type is the version (or flavor) of the bitmap + # file. The following values are typically found in the Type + # field: + # 0000h Old + # 0001h Standard + # 0002h Byte-encoded + # 0003h RGB format + # 0004h TIFF format + # 0005h IFF format + # FFFFh Experimental + + # Old and standard are the same, except for the length tag. + # byte-encoded is run-length-encoded + # RGB looks similar to standard, but RGB byte order + # TIFF and IFF mean that they were converted from T/IFF + # Experimental means that it's something else. + # (https://www.fileformat.info/format/sunraster/egff.htm) + + if file_type in (0, 1, 3, 4, 5): + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride)) + ] + elif file_type == 2: + self.tile = [ + ImageFile._Tile("sun_rle", (0, 0) + self.size, offset, rawmode) + ] + else: + msg = "Unsupported Sun Raster file type" + raise SyntaxError(msg) + + +# +# registry + + +Image.register_open(SunImageFile.format, SunImageFile, _accept) + +Image.register_extension(SunImageFile.format, ".ras") diff --git a/python_modules/PIL/TarIO.py b/python_modules/PIL/TarIO.py new file mode 100644 index 000000000..86490a496 --- /dev/null +++ b/python_modules/PIL/TarIO.py @@ -0,0 +1,61 @@ +# +# The Python Imaging Library. +# $Id$ +# +# read files from within a tar file +# +# History: +# 95-06-18 fl Created +# 96-05-28 fl Open files in binary mode +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1995-96. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io + +from . import ContainerIO + + +class TarIO(ContainerIO.ContainerIO[bytes]): + """A file object that provides read access to a given member of a TAR file.""" + + def __init__(self, tarfile: str, file: str) -> None: + """ + Create file object. + + :param tarfile: Name of TAR file. + :param file: Name of member file. + """ + self.fh = open(tarfile, "rb") + + while True: + s = self.fh.read(512) + if len(s) != 512: + self.fh.close() + + msg = "unexpected end of tar file" + raise OSError(msg) + + name = s[:100].decode("utf-8") + i = name.find("\0") + if i == 0: + self.fh.close() + + msg = "cannot find subfile" + raise OSError(msg) + if i > 0: + name = name[:i] + + size = int(s[124:135], 8) + + if file == name: + break + + self.fh.seek((size + 511) & (~511), io.SEEK_CUR) + + # Open region + super().__init__(self.fh, self.fh.tell(), size) diff --git a/python_modules/PIL/TgaImagePlugin.py b/python_modules/PIL/TgaImagePlugin.py new file mode 100644 index 000000000..90d5b5cf4 --- /dev/null +++ b/python_modules/PIL/TgaImagePlugin.py @@ -0,0 +1,264 @@ +# +# The Python Imaging Library. +# $Id$ +# +# TGA file handling +# +# History: +# 95-09-01 fl created (reads 24-bit files only) +# 97-01-04 fl support more TGA versions, including compressed images +# 98-07-04 fl fixed orientation and alpha layer bugs +# 98-09-11 fl fixed orientation for runlength decoder +# +# Copyright (c) Secret Labs AB 1997-98. +# Copyright (c) Fredrik Lundh 1995-97. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import warnings +from typing import IO + +from . import Image, ImageFile, ImagePalette +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 + +# +# -------------------------------------------------------------------- +# Read RGA file + + +MODES = { + # map imagetype/depth to rawmode + (1, 8): "P", + (3, 1): "1", + (3, 8): "L", + (3, 16): "LA", + (2, 16): "BGRA;15Z", + (2, 24): "BGR", + (2, 32): "BGRA", +} + + +## +# Image plugin for Targa files. + + +class TgaImageFile(ImageFile.ImageFile): + format = "TGA" + format_description = "Targa" + + def _open(self) -> None: + # process header + assert self.fp is not None + + s = self.fp.read(18) + + id_len = s[0] + + colormaptype = s[1] + imagetype = s[2] + + depth = s[16] + + flags = s[17] + + self._size = i16(s, 12), i16(s, 14) + + # validate header fields + if ( + colormaptype not in (0, 1) + or self.size[0] <= 0 + or self.size[1] <= 0 + or depth not in (1, 8, 16, 24, 32) + ): + msg = "not a TGA file" + raise SyntaxError(msg) + + # image mode + if imagetype in (3, 11): + self._mode = "L" + if depth == 1: + self._mode = "1" # ??? + elif depth == 16: + self._mode = "LA" + elif imagetype in (1, 9): + self._mode = "P" if colormaptype else "L" + elif imagetype in (2, 10): + self._mode = "RGB" if depth == 24 else "RGBA" + else: + msg = "unknown TGA mode" + raise SyntaxError(msg) + + # orientation + orientation = flags & 0x30 + self._flip_horizontally = orientation in [0x10, 0x30] + if orientation in [0x20, 0x30]: + orientation = 1 + elif orientation in [0, 0x10]: + orientation = -1 + else: + msg = "unknown TGA orientation" + raise SyntaxError(msg) + + self.info["orientation"] = orientation + + if imagetype & 8: + self.info["compression"] = "tga_rle" + + if id_len: + self.info["id_section"] = self.fp.read(id_len) + + if colormaptype: + # read palette + start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] + if mapdepth == 16: + self.palette = ImagePalette.raw( + "BGRA;15Z", bytes(2 * start) + self.fp.read(2 * size) + ) + self.palette.mode = "RGBA" + elif mapdepth == 24: + self.palette = ImagePalette.raw( + "BGR", bytes(3 * start) + self.fp.read(3 * size) + ) + elif mapdepth == 32: + self.palette = ImagePalette.raw( + "BGRA", bytes(4 * start) + self.fp.read(4 * size) + ) + else: + msg = "unknown TGA map depth" + raise SyntaxError(msg) + + # setup tile descriptor + try: + rawmode = MODES[(imagetype & 7, depth)] + if imagetype & 8: + # compressed + self.tile = [ + ImageFile._Tile( + "tga_rle", + (0, 0) + self.size, + self.fp.tell(), + (rawmode, orientation, depth), + ) + ] + else: + self.tile = [ + ImageFile._Tile( + "raw", + (0, 0) + self.size, + self.fp.tell(), + (rawmode, 0, orientation), + ) + ] + except KeyError: + pass # cannot decode + + def load_end(self) -> None: + if self._flip_horizontally: + self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + + +# +# -------------------------------------------------------------------- +# Write TGA file + + +SAVE = { + "1": ("1", 1, 0, 3), + "L": ("L", 8, 0, 3), + "LA": ("LA", 16, 0, 3), + "P": ("P", 8, 1, 1), + "RGB": ("BGR", 24, 0, 2), + "RGBA": ("BGRA", 32, 0, 2), +} + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + try: + rawmode, bits, colormaptype, imagetype = SAVE[im.mode] + except KeyError as e: + msg = f"cannot write mode {im.mode} as TGA" + raise OSError(msg) from e + + if "rle" in im.encoderinfo: + rle = im.encoderinfo["rle"] + else: + compression = im.encoderinfo.get("compression", im.info.get("compression")) + rle = compression == "tga_rle" + if rle: + imagetype += 8 + + id_section = im.encoderinfo.get("id_section", im.info.get("id_section", "")) + id_len = len(id_section) + if id_len > 255: + id_len = 255 + id_section = id_section[:255] + warnings.warn("id_section has been trimmed to 255 characters") + + if colormaptype: + palette = im.im.getpalette("RGB", "BGR") + colormaplength, colormapentry = len(palette) // 3, 24 + else: + colormaplength, colormapentry = 0, 0 + + if im.mode in ("LA", "RGBA"): + flags = 8 + else: + flags = 0 + + orientation = im.encoderinfo.get("orientation", im.info.get("orientation", -1)) + if orientation > 0: + flags = flags | 0x20 + + fp.write( + o8(id_len) + + o8(colormaptype) + + o8(imagetype) + + o16(0) # colormapfirst + + o16(colormaplength) + + o8(colormapentry) + + o16(0) + + o16(0) + + o16(im.size[0]) + + o16(im.size[1]) + + o8(bits) + + o8(flags) + ) + + if id_section: + fp.write(id_section) + + if colormaptype: + fp.write(palette) + + if rle: + ImageFile._save( + im, + fp, + [ImageFile._Tile("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))], + ) + else: + ImageFile._save( + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))], + ) + + # write targa version 2 footer + fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000") + + +# +# -------------------------------------------------------------------- +# Registry + + +Image.register_open(TgaImageFile.format, TgaImageFile) +Image.register_save(TgaImageFile.format, _save) + +Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"]) + +Image.register_mime(TgaImageFile.format, "image/x-tga") diff --git a/python_modules/PIL/TiffImagePlugin.py b/python_modules/PIL/TiffImagePlugin.py new file mode 100644 index 000000000..daf20f2e8 --- /dev/null +++ b/python_modules/PIL/TiffImagePlugin.py @@ -0,0 +1,2339 @@ +# +# The Python Imaging Library. +# $Id$ +# +# TIFF file handling +# +# TIFF is a flexible, if somewhat aged, image file format originally +# defined by Aldus. Although TIFF supports a wide variety of pixel +# layouts and compression methods, the name doesn't really stand for +# "thousands of incompatible file formats," it just feels that way. +# +# To read TIFF data from a stream, the stream must be seekable. For +# progressive decoding, make sure to use TIFF files where the tag +# directory is placed first in the file. +# +# History: +# 1995-09-01 fl Created +# 1996-05-04 fl Handle JPEGTABLES tag +# 1996-05-18 fl Fixed COLORMAP support +# 1997-01-05 fl Fixed PREDICTOR support +# 1997-08-27 fl Added support for rational tags (from Perry Stoll) +# 1998-01-10 fl Fixed seek/tell (from Jan Blom) +# 1998-07-15 fl Use private names for internal variables +# 1999-06-13 fl Rewritten for PIL 1.0 (1.0) +# 2000-10-11 fl Additional fixes for Python 2.0 (1.1) +# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2) +# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3) +# 2001-12-18 fl Added workaround for broken Matrox library +# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart) +# 2003-05-19 fl Check FILLORDER tag +# 2003-09-26 fl Added RGBa support +# 2004-02-24 fl Added DPI support; fixed rational write support +# 2005-02-07 fl Added workaround for broken Corel Draw 10 files +# 2006-01-09 fl Added support for float/double tags (from Russell Nelson) +# +# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. +# Copyright (c) 1995-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import io +import itertools +import logging +import math +import os +import struct +import warnings +from collections.abc import Iterator, MutableMapping +from fractions import Fraction +from numbers import Number, Rational +from typing import IO, Any, Callable, NoReturn, cast + +from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 +from ._deprecate import deprecate +from ._typing import StrOrBytesPath +from ._util import DeferredError, is_path +from .TiffTags import TYPES + +TYPE_CHECKING = False +if TYPE_CHECKING: + from ._typing import Buffer, IntegralLike + +logger = logging.getLogger(__name__) + +# Set these to true to force use of libtiff for reading or writing. +READ_LIBTIFF = False +WRITE_LIBTIFF = False +STRIP_SIZE = 65536 + +II = b"II" # little-endian (Intel style) +MM = b"MM" # big-endian (Motorola style) + +# +# -------------------------------------------------------------------- +# Read TIFF files + +# a few tag names, just to make the code below a bit more readable +OSUBFILETYPE = 255 +IMAGEWIDTH = 256 +IMAGELENGTH = 257 +BITSPERSAMPLE = 258 +COMPRESSION = 259 +PHOTOMETRIC_INTERPRETATION = 262 +FILLORDER = 266 +IMAGEDESCRIPTION = 270 +STRIPOFFSETS = 273 +SAMPLESPERPIXEL = 277 +ROWSPERSTRIP = 278 +STRIPBYTECOUNTS = 279 +X_RESOLUTION = 282 +Y_RESOLUTION = 283 +PLANAR_CONFIGURATION = 284 +RESOLUTION_UNIT = 296 +TRANSFERFUNCTION = 301 +SOFTWARE = 305 +DATE_TIME = 306 +ARTIST = 315 +PREDICTOR = 317 +COLORMAP = 320 +TILEWIDTH = 322 +TILELENGTH = 323 +TILEOFFSETS = 324 +TILEBYTECOUNTS = 325 +SUBIFD = 330 +EXTRASAMPLES = 338 +SAMPLEFORMAT = 339 +JPEGTABLES = 347 +YCBCRSUBSAMPLING = 530 +REFERENCEBLACKWHITE = 532 +COPYRIGHT = 33432 +IPTC_NAA_CHUNK = 33723 # newsphoto properties +PHOTOSHOP_CHUNK = 34377 # photoshop properties +ICCPROFILE = 34675 +EXIFIFD = 34665 +XMP = 700 +JPEGQUALITY = 65537 # pseudo-tag by libtiff + +# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java +IMAGEJ_META_DATA_BYTE_COUNTS = 50838 +IMAGEJ_META_DATA = 50839 + +COMPRESSION_INFO = { + # Compression => pil compression name + 1: "raw", + 2: "tiff_ccitt", + 3: "group3", + 4: "group4", + 5: "tiff_lzw", + 6: "tiff_jpeg", # obsolete + 7: "jpeg", + 8: "tiff_adobe_deflate", + 32771: "tiff_raw_16", # 16-bit padding + 32773: "packbits", + 32809: "tiff_thunderscan", + 32946: "tiff_deflate", + 34676: "tiff_sgilog", + 34677: "tiff_sgilog24", + 34925: "lzma", + 50000: "zstd", + 50001: "webp", +} + +COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} + +OPEN_INFO = { + # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, + # ExtraSamples) => mode, rawmode + (II, 0, (1,), 1, (1,), ()): ("1", "1;I"), + (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), + (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), + (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), + (II, 1, (1,), 1, (1,), ()): ("1", "1"), + (MM, 1, (1,), 1, (1,), ()): ("1", "1"), + (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), + (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), + (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), + (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), + (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), + (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), + (II, 1, (1,), 1, (2,), ()): ("L", "L;2"), + (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), + (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), + (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), + (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), + (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), + (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), + (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), + (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), + (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), + (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), + (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), + (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), + (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), + (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), + (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), + (II, 1, (1,), 1, (8,), ()): ("L", "L"), + (MM, 1, (1,), 1, (8,), ()): ("L", "L"), + (II, 1, (2,), 1, (8,), ()): ("L", "L"), + (MM, 1, (2,), 1, (8,), ()): ("L", "L"), + (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), + (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), + (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), + (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), + (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), + (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), + (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), + (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), + (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), + (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), + (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), + (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), + (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), + (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), + (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), + (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), + (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), + (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), + (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), + (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), + (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), + (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 + (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 + (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), + (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), + (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), + (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), + (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), + (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), + (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), + (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), + (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), + (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), + (II, 3, (1,), 1, (4,), ()): ("P", "P;4"), + (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), + (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), + (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), + (II, 3, (1,), 1, (8,), ()): ("P", "P"), + (MM, 3, (1,), 1, (8,), ()): ("P", "P"), + (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), + (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), + (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), + (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), + (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), + (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), + (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), + (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), + (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), + (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), + (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"), + (II, 6, (1,), 1, (8,), ()): ("L", "L"), + (MM, 6, (1,), 1, (8,), ()): ("L", "L"), + # JPEG compressed images handled by LibTiff and auto-converted to RGBX + # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel + (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), + (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), + (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), + (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), +} + +MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) + +PREFIXES = [ + b"MM\x00\x2a", # Valid TIFF header with big-endian byte order + b"II\x2a\x00", # Valid TIFF header with little-endian byte order + b"MM\x2a\x00", # Invalid TIFF header, assume big-endian + b"II\x00\x2a", # Invalid TIFF header, assume little-endian + b"MM\x00\x2b", # BigTIFF with big-endian byte order + b"II\x2b\x00", # BigTIFF with little-endian byte order +] + +if not getattr(Image.core, "libtiff_support_custom_tags", True): + deprecate("Support for LibTIFF earlier than version 4", 12) + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(tuple(PREFIXES)) + + +def _limit_rational( + val: float | Fraction | IFDRational, max_val: int +) -> tuple[IntegralLike, IntegralLike]: + inv = abs(val) > 1 + n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) + return n_d[::-1] if inv else n_d + + +def _limit_signed_rational( + val: IFDRational, max_val: int, min_val: int +) -> tuple[IntegralLike, IntegralLike]: + frac = Fraction(val) + n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator + + if min(float(i) for i in n_d) < min_val: + n_d = _limit_rational(val, abs(min_val)) + + n_d_float = tuple(float(i) for i in n_d) + if max(n_d_float) > max_val: + n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val) + + return n_d + + +## +# Wrapper for TIFF IFDs. + +_load_dispatch = {} +_write_dispatch = {} + + +def _delegate(op: str) -> Any: + def delegate( + self: IFDRational, *args: tuple[float, ...] + ) -> bool | float | Fraction: + return getattr(self._val, op)(*args) + + return delegate + + +class IFDRational(Rational): + """Implements a rational class where 0/0 is a legal value to match + the in the wild use of exif rationals. + + e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used + """ + + """ If the denominator is 0, store this as a float('nan'), otherwise store + as a fractions.Fraction(). Delegate as appropriate + + """ + + __slots__ = ("_numerator", "_denominator", "_val") + + def __init__( + self, value: float | Fraction | IFDRational, denominator: int = 1 + ) -> None: + """ + :param value: either an integer numerator, a + float/rational/other number, or an IFDRational + :param denominator: Optional integer denominator + """ + self._val: Fraction | float + if isinstance(value, IFDRational): + self._numerator = value.numerator + self._denominator = value.denominator + self._val = value._val + return + + if isinstance(value, Fraction): + self._numerator = value.numerator + self._denominator = value.denominator + else: + if TYPE_CHECKING: + self._numerator = cast(IntegralLike, value) + else: + self._numerator = value + self._denominator = denominator + + if denominator == 0: + self._val = float("nan") + elif denominator == 1: + self._val = Fraction(value) + elif int(value) == value: + self._val = Fraction(int(value), denominator) + else: + self._val = Fraction(value / denominator) + + @property + def numerator(self) -> IntegralLike: + return self._numerator + + @property + def denominator(self) -> int: + return self._denominator + + def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]: + """ + + :param max_denominator: Integer, the maximum denominator value + :returns: Tuple of (numerator, denominator) + """ + + if self.denominator == 0: + return self.numerator, self.denominator + + assert isinstance(self._val, Fraction) + f = self._val.limit_denominator(max_denominator) + return f.numerator, f.denominator + + def __repr__(self) -> str: + return str(float(self._val)) + + def __hash__(self) -> int: # type: ignore[override] + return self._val.__hash__() + + def __eq__(self, other: object) -> bool: + val = self._val + if isinstance(other, IFDRational): + other = other._val + if isinstance(other, float): + val = float(val) + return val == other + + def __getstate__(self) -> list[float | Fraction | IntegralLike]: + return [self._val, self._numerator, self._denominator] + + def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None: + IFDRational.__init__(self, 0) + _val, _numerator, _denominator = state + assert isinstance(_val, (float, Fraction)) + self._val = _val + if TYPE_CHECKING: + self._numerator = cast(IntegralLike, _numerator) + else: + self._numerator = _numerator + assert isinstance(_denominator, int) + self._denominator = _denominator + + """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', + 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', + 'mod','rmod', 'pow','rpow', 'pos', 'neg', + 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', + 'ceil', 'floor', 'round'] + print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) + """ + + __add__ = _delegate("__add__") + __radd__ = _delegate("__radd__") + __sub__ = _delegate("__sub__") + __rsub__ = _delegate("__rsub__") + __mul__ = _delegate("__mul__") + __rmul__ = _delegate("__rmul__") + __truediv__ = _delegate("__truediv__") + __rtruediv__ = _delegate("__rtruediv__") + __floordiv__ = _delegate("__floordiv__") + __rfloordiv__ = _delegate("__rfloordiv__") + __mod__ = _delegate("__mod__") + __rmod__ = _delegate("__rmod__") + __pow__ = _delegate("__pow__") + __rpow__ = _delegate("__rpow__") + __pos__ = _delegate("__pos__") + __neg__ = _delegate("__neg__") + __abs__ = _delegate("__abs__") + __trunc__ = _delegate("__trunc__") + __lt__ = _delegate("__lt__") + __gt__ = _delegate("__gt__") + __le__ = _delegate("__le__") + __ge__ = _delegate("__ge__") + __bool__ = _delegate("__bool__") + __ceil__ = _delegate("__ceil__") + __floor__ = _delegate("__floor__") + __round__ = _delegate("__round__") + # Python >= 3.11 + if hasattr(Fraction, "__int__"): + __int__ = _delegate("__int__") + + +_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] + + +def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]: + def decorator(func: _LoaderFunc) -> _LoaderFunc: + from .TiffTags import TYPES + + if func.__name__.startswith("load_"): + TYPES[idx] = func.__name__[5:].replace("_", " ") + _load_dispatch[idx] = size, func # noqa: F821 + return func + + return decorator + + +def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + _write_dispatch[idx] = func # noqa: F821 + return func + + return decorator + + +def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None: + from .TiffTags import TYPES + + idx, fmt, name = idx_fmt_name + TYPES[idx] = name + size = struct.calcsize(f"={fmt}") + + def basic_handler( + self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True + ) -> tuple[Any, ...]: + return self._unpack(f"{len(data) // size}{fmt}", data) + + _load_dispatch[idx] = size, basic_handler # noqa: F821 + _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 + b"".join(self._pack(fmt, value) for value in values) + ) + + +if TYPE_CHECKING: + _IFDv2Base = MutableMapping[int, Any] +else: + _IFDv2Base = MutableMapping + + +class ImageFileDirectory_v2(_IFDv2Base): + """This class represents a TIFF tag directory. To speed things up, we + don't decode tags unless they're asked for. + + Exposes a dictionary interface of the tags in the directory:: + + ifd = ImageFileDirectory_v2() + ifd[key] = 'Some Data' + ifd.tagtype[key] = TiffTags.ASCII + print(ifd[key]) + 'Some Data' + + Individual values are returned as the strings or numbers, sequences are + returned as tuples of the values. + + The tiff metadata type of each item is stored in a dictionary of + tag types in + :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types + are read from a tiff file, guessed from the type added, or added + manually. + + Data Structures: + + * ``self.tagtype = {}`` + + * Key: numerical TIFF tag number + * Value: integer corresponding to the data type from + :py:data:`.TiffTags.TYPES` + + .. versionadded:: 3.0.0 + + 'Internal' data structures: + + * ``self._tags_v2 = {}`` + + * Key: numerical TIFF tag number + * Value: decoded data, as tuple for multiple values + + * ``self._tagdata = {}`` + + * Key: numerical TIFF tag number + * Value: undecoded byte string from file + + * ``self._tags_v1 = {}`` + + * Key: numerical TIFF tag number + * Value: decoded data in the v1 format + + Tags will be found in the private attributes ``self._tagdata``, and in + ``self._tags_v2`` once decoded. + + ``self.legacy_api`` is a value for internal use, and shouldn't be changed + from outside code. In cooperation with + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api`` + is true, then decoded tags will be populated into both ``_tags_v1`` and + ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF + save routine. Tags should be read from ``_tags_v1`` if + ``legacy_api == true``. + + """ + + _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {} + _write_dispatch: dict[int, Callable[..., Any]] = {} + + def __init__( + self, + ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00", + prefix: bytes | None = None, + group: int | None = None, + ) -> None: + """Initialize an ImageFileDirectory. + + To construct an ImageFileDirectory from a real file, pass the 8-byte + magic header to the constructor. To only set the endianness, pass it + as the 'prefix' keyword argument. + + :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets + endianness. + :param prefix: Override the endianness of the file. + """ + if not _accept(ifh): + msg = f"not a TIFF file (header {repr(ifh)} not valid)" + raise SyntaxError(msg) + self._prefix = prefix if prefix is not None else ifh[:2] + if self._prefix == MM: + self._endian = ">" + elif self._prefix == II: + self._endian = "<" + else: + msg = "not a TIFF IFD" + raise SyntaxError(msg) + self._bigtiff = ifh[2] == 43 + self.group = group + self.tagtype: dict[int, int] = {} + """ Dictionary of tag types """ + self.reset() + self.next = ( + self._unpack("Q", ifh[8:])[0] + if self._bigtiff + else self._unpack("L", ifh[4:])[0] + ) + self._legacy_api = False + + prefix = property(lambda self: self._prefix) + offset = property(lambda self: self._offset) + + @property + def legacy_api(self) -> bool: + return self._legacy_api + + @legacy_api.setter + def legacy_api(self, value: bool) -> NoReturn: + msg = "Not allowing setting of legacy api" + raise Exception(msg) + + def reset(self) -> None: + self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false + self._tags_v2: dict[int, Any] = {} # main tag storage + self._tagdata: dict[int, bytes] = {} + self.tagtype = {} # added 2008-06-05 by Florian Hoech + self._next = None + self._offset: int | None = None + + def __str__(self) -> str: + return str(dict(self)) + + def named(self) -> dict[str, Any]: + """ + :returns: dict of name|key: value + + Returns the complete tag dictionary, with named tags where possible. + """ + return { + TiffTags.lookup(code, self.group).name: value + for code, value in self.items() + } + + def __len__(self) -> int: + return len(set(self._tagdata) | set(self._tags_v2)) + + def __getitem__(self, tag: int) -> Any: + if tag not in self._tags_v2: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + self[tag] = handler(self, data, self.legacy_api) # check type + val = self._tags_v2[tag] + if self.legacy_api and not isinstance(val, (tuple, bytes)): + val = (val,) + return val + + def __contains__(self, tag: object) -> bool: + return tag in self._tags_v2 or tag in self._tagdata + + def __setitem__(self, tag: int, value: Any) -> None: + self._setitem(tag, value, self.legacy_api) + + def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: + basetypes = (Number, bytes, str) + + info = TiffTags.lookup(tag, self.group) + values = [value] if isinstance(value, basetypes) else value + + if tag not in self.tagtype: + if info.type: + self.tagtype[tag] = info.type + else: + self.tagtype[tag] = TiffTags.UNDEFINED + if all(isinstance(v, IFDRational) for v in values): + for v in values: + assert isinstance(v, IFDRational) + if v < 0: + self.tagtype[tag] = TiffTags.SIGNED_RATIONAL + break + else: + self.tagtype[tag] = TiffTags.RATIONAL + elif all(isinstance(v, int) for v in values): + short = True + signed_short = True + long = True + for v in values: + assert isinstance(v, int) + if short and not (0 <= v < 2**16): + short = False + if signed_short and not (-(2**15) < v < 2**15): + signed_short = False + if long and v < 0: + long = False + if short: + self.tagtype[tag] = TiffTags.SHORT + elif signed_short: + self.tagtype[tag] = TiffTags.SIGNED_SHORT + elif long: + self.tagtype[tag] = TiffTags.LONG + else: + self.tagtype[tag] = TiffTags.SIGNED_LONG + elif all(isinstance(v, float) for v in values): + self.tagtype[tag] = TiffTags.DOUBLE + elif all(isinstance(v, str) for v in values): + self.tagtype[tag] = TiffTags.ASCII + elif all(isinstance(v, bytes) for v in values): + self.tagtype[tag] = TiffTags.BYTE + + if self.tagtype[tag] == TiffTags.UNDEFINED: + values = [ + v.encode("ascii", "replace") if isinstance(v, str) else v + for v in values + ] + elif self.tagtype[tag] == TiffTags.RATIONAL: + values = [float(v) if isinstance(v, int) else v for v in values] + + is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) + if not is_ifd: + values = tuple( + info.cvt_enum(value) if isinstance(value, str) else value + for value in values + ) + + dest = self._tags_v1 if legacy_api else self._tags_v2 + + # Three branches: + # Spec'd length == 1, Actual length 1, store as element + # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. + # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. + # Don't mess with the legacy api, since it's frozen. + if not is_ifd and ( + (info.length == 1) + or self.tagtype[tag] == TiffTags.BYTE + or (info.length is None and len(values) == 1 and not legacy_api) + ): + # Don't mess with the legacy api, since it's frozen. + if legacy_api and self.tagtype[tag] in [ + TiffTags.RATIONAL, + TiffTags.SIGNED_RATIONAL, + ]: # rationals + values = (values,) + try: + (dest[tag],) = values + except ValueError: + # We've got a builtin tag with 1 expected entry + warnings.warn( + f"Metadata Warning, tag {tag} had too many entries: " + f"{len(values)}, expected 1" + ) + dest[tag] = values[0] + + else: + # Spec'd length > 1 or undefined + # Unspec'd, and length > 1 + dest[tag] = values + + def __delitem__(self, tag: int) -> None: + self._tags_v2.pop(tag, None) + self._tags_v1.pop(tag, None) + self._tagdata.pop(tag, None) + + def __iter__(self) -> Iterator[int]: + return iter(set(self._tagdata) | set(self._tags_v2)) + + def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]: + return struct.unpack(self._endian + fmt, data) + + def _pack(self, fmt: str, *values: Any) -> bytes: + return struct.pack(self._endian + fmt, *values) + + list( + map( + _register_basic, + [ + (TiffTags.SHORT, "H", "short"), + (TiffTags.LONG, "L", "long"), + (TiffTags.SIGNED_BYTE, "b", "signed byte"), + (TiffTags.SIGNED_SHORT, "h", "signed short"), + (TiffTags.SIGNED_LONG, "l", "signed long"), + (TiffTags.FLOAT, "f", "float"), + (TiffTags.DOUBLE, "d", "double"), + (TiffTags.IFD, "L", "long"), + (TiffTags.LONG8, "Q", "long8"), + ], + ) + ) + + @_register_loader(1, 1) # Basic type, except for the legacy API. + def load_byte(self, data: bytes, legacy_api: bool = True) -> bytes: + return data + + @_register_writer(1) # Basic type, except for the legacy API. + def write_byte(self, data: bytes | int | IFDRational) -> bytes: + if isinstance(data, IFDRational): + data = int(data) + if isinstance(data, int): + data = bytes((data,)) + return data + + @_register_loader(2, 1) + def load_string(self, data: bytes, legacy_api: bool = True) -> str: + if data.endswith(b"\0"): + data = data[:-1] + return data.decode("latin-1", "replace") + + @_register_writer(2) + def write_string(self, value: str | bytes | int) -> bytes: + # remerge of https://github.com/python-pillow/Pillow/pull/1416 + if isinstance(value, int): + value = str(value) + if not isinstance(value, bytes): + value = value.encode("ascii", "replace") + return value + b"\0" + + @_register_loader(5, 8) + def load_rational( + self, data: bytes, legacy_api: bool = True + ) -> tuple[tuple[int, int] | IFDRational, ...]: + vals = self._unpack(f"{len(data) // 4}L", data) + + def combine(a: int, b: int) -> tuple[int, int] | IFDRational: + return (a, b) if legacy_api else IFDRational(a, b) + + return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) + + @_register_writer(5) + def write_rational(self, *values: IFDRational) -> bytes: + return b"".join( + self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values + ) + + @_register_loader(7, 1) + def load_undefined(self, data: bytes, legacy_api: bool = True) -> bytes: + return data + + @_register_writer(7) + def write_undefined(self, value: bytes | int | IFDRational) -> bytes: + if isinstance(value, IFDRational): + value = int(value) + if isinstance(value, int): + value = str(value).encode("ascii", "replace") + return value + + @_register_loader(10, 8) + def load_signed_rational( + self, data: bytes, legacy_api: bool = True + ) -> tuple[tuple[int, int] | IFDRational, ...]: + vals = self._unpack(f"{len(data) // 4}l", data) + + def combine(a: int, b: int) -> tuple[int, int] | IFDRational: + return (a, b) if legacy_api else IFDRational(a, b) + + return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) + + @_register_writer(10) + def write_signed_rational(self, *values: IFDRational) -> bytes: + return b"".join( + self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) + for frac in values + ) + + def _ensure_read(self, fp: IO[bytes], size: int) -> bytes: + ret = fp.read(size) + if len(ret) != size: + msg = ( + "Corrupt EXIF data. " + f"Expecting to read {size} bytes but only got {len(ret)}. " + ) + raise OSError(msg) + return ret + + def load(self, fp: IO[bytes]) -> None: + self.reset() + self._offset = fp.tell() + + try: + tag_count = ( + self._unpack("Q", self._ensure_read(fp, 8)) + if self._bigtiff + else self._unpack("H", self._ensure_read(fp, 2)) + )[0] + for i in range(tag_count): + tag, typ, count, data = ( + self._unpack("HHQ8s", self._ensure_read(fp, 20)) + if self._bigtiff + else self._unpack("HHL4s", self._ensure_read(fp, 12)) + ) + + tagname = TiffTags.lookup(tag, self.group).name + typname = TYPES.get(typ, "unknown") + msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" + + try: + unit_size, handler = self._load_dispatch[typ] + except KeyError: + logger.debug("%s - unsupported type %s", msg, typ) + continue # ignore unsupported type + size = count * unit_size + if size > (8 if self._bigtiff else 4): + here = fp.tell() + (offset,) = self._unpack("Q" if self._bigtiff else "L", data) + msg += f" Tag Location: {here} - Data Location: {offset}" + fp.seek(offset) + data = ImageFile._safe_read(fp, size) + fp.seek(here) + else: + data = data[:size] + + if len(data) != size: + warnings.warn( + "Possibly corrupt EXIF data. " + f"Expecting to read {size} bytes but only got {len(data)}." + f" Skipping tag {tag}" + ) + logger.debug(msg) + continue + + if not data: + logger.debug(msg) + continue + + self._tagdata[tag] = data + self.tagtype[tag] = typ + + msg += " - value: " + msg += f"" if size > 32 else repr(data) + + logger.debug(msg) + + (self.next,) = ( + self._unpack("Q", self._ensure_read(fp, 8)) + if self._bigtiff + else self._unpack("L", self._ensure_read(fp, 4)) + ) + except OSError as msg: + warnings.warn(str(msg)) + return + + def _get_ifh(self) -> bytes: + ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42) + if self._bigtiff: + ifh += self._pack("HH", 8, 0) + ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8) + + return ifh + + def tobytes(self, offset: int = 0) -> bytes: + # FIXME What about tagdata? + result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2)) + + entries: list[tuple[int, int, int, bytes, bytes]] = [] + + fmt = "Q" if self._bigtiff else "L" + fmt_size = 8 if self._bigtiff else 4 + offset += ( + len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size + ) + stripoffsets = None + + # pass 1: convert tags to binary format + # always write tags in ascending order + for tag, value in sorted(self._tags_v2.items()): + if tag == STRIPOFFSETS: + stripoffsets = len(entries) + typ = self.tagtype[tag] + logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) + is_ifd = typ == TiffTags.LONG and isinstance(value, dict) + if is_ifd: + ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag) + values = self._tags_v2[tag] + for ifd_tag, ifd_value in values.items(): + ifd[ifd_tag] = ifd_value + data = ifd.tobytes(offset) + else: + values = value if isinstance(value, tuple) else (value,) + data = self._write_dispatch[typ](self, *values) + + tagname = TiffTags.lookup(tag, self.group).name + typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") + msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: " + msg += f"" if len(data) >= 16 else str(values) + logger.debug(msg) + + # count is sum of lengths for string and arbitrary data + if is_ifd: + count = 1 + elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: + count = len(data) + else: + count = len(values) + # figure out if data fits into the entry + if len(data) <= fmt_size: + entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b"")) + else: + entries.append((tag, typ, count, self._pack(fmt, offset), data)) + offset += (len(data) + 1) // 2 * 2 # pad to word + + # update strip offset data to point beyond auxiliary data + if stripoffsets is not None: + tag, typ, count, value, data = entries[stripoffsets] + if data: + size, handler = self._load_dispatch[typ] + values = [val + offset for val in handler(self, data, self.legacy_api)] + data = self._write_dispatch[typ](self, *values) + else: + value = self._pack(fmt, self._unpack(fmt, value)[0] + offset) + entries[stripoffsets] = tag, typ, count, value, data + + # pass 2: write entries to file + for tag, typ, count, value, data in entries: + logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) + result += self._pack( + "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value + ) + + # -- overwrite here for multi-page -- + result += self._pack(fmt, 0) # end of entries + + # pass 3: write auxiliary data to file + for tag, typ, count, value, data in entries: + result += data + if len(data) & 1: + result += b"\0" + + return result + + def save(self, fp: IO[bytes]) -> int: + if fp.tell() == 0: # skip TIFF header on subsequent pages + fp.write(self._get_ifh()) + + offset = fp.tell() + result = self.tobytes(offset) + fp.write(result) + return offset + len(result) + + +ImageFileDirectory_v2._load_dispatch = _load_dispatch +ImageFileDirectory_v2._write_dispatch = _write_dispatch +for idx, name in TYPES.items(): + name = name.replace(" ", "_") + setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1]) + setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx]) +del _load_dispatch, _write_dispatch, idx, name + + +# Legacy ImageFileDirectory support. +class ImageFileDirectory_v1(ImageFileDirectory_v2): + """This class represents the **legacy** interface to a TIFF tag directory. + + Exposes a dictionary interface of the tags in the directory:: + + ifd = ImageFileDirectory_v1() + ifd[key] = 'Some Data' + ifd.tagtype[key] = TiffTags.ASCII + print(ifd[key]) + ('Some Data',) + + Also contains a dictionary of tag types as read from the tiff image file, + :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. + + Values are returned as a tuple. + + .. deprecated:: 3.0.0 + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._legacy_api = True + + tags = property(lambda self: self._tags_v1) + tagdata = property(lambda self: self._tagdata) + + # defined in ImageFileDirectory_v2 + tagtype: dict[int, int] + """Dictionary of tag types""" + + @classmethod + def from_v2(cls, original: ImageFileDirectory_v2) -> ImageFileDirectory_v1: + """Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance. + + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + + """ + + ifd = cls(prefix=original.prefix) + ifd._tagdata = original._tagdata + ifd.tagtype = original.tagtype + ifd.next = original.next # an indicator for multipage tiffs + return ifd + + def to_v2(self) -> ImageFileDirectory_v2: + """Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance. + + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + + """ + + ifd = ImageFileDirectory_v2(prefix=self.prefix) + ifd._tagdata = dict(self._tagdata) + ifd.tagtype = dict(self.tagtype) + ifd._tags_v2 = dict(self._tags_v2) + return ifd + + def __contains__(self, tag: object) -> bool: + return tag in self._tags_v1 or tag in self._tagdata + + def __len__(self) -> int: + return len(set(self._tagdata) | set(self._tags_v1)) + + def __iter__(self) -> Iterator[int]: + return iter(set(self._tagdata) | set(self._tags_v1)) + + def __setitem__(self, tag: int, value: Any) -> None: + for legacy_api in (False, True): + self._setitem(tag, value, legacy_api) + + def __getitem__(self, tag: int) -> Any: + if tag not in self._tags_v1: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + for legacy in (False, True): + self._setitem(tag, handler(self, data, legacy), legacy) + val = self._tags_v1[tag] + if not isinstance(val, (tuple, bytes)): + val = (val,) + return val + + +# undone -- switch this pointer +ImageFileDirectory = ImageFileDirectory_v1 + + +## +# Image plugin for TIFF files. + + +class TiffImageFile(ImageFile.ImageFile): + format = "TIFF" + format_description = "Adobe TIFF" + _close_exclusive_fp_after_loading = False + + def __init__( + self, + fp: StrOrBytesPath | IO[bytes], + filename: str | bytes | None = None, + ) -> None: + self.tag_v2: ImageFileDirectory_v2 + """ Image file directory (tag dictionary) """ + + self.tag: ImageFileDirectory_v1 + """ Legacy tag entries """ + + super().__init__(fp, filename) + + def _open(self) -> None: + """Open the first image in a TIFF file""" + + # Header + ifh = self.fp.read(8) + if ifh[2] == 43: + ifh += self.fp.read(8) + + self.tag_v2 = ImageFileDirectory_v2(ifh) + + # setup frame pointers + self.__first = self.__next = self.tag_v2.next + self.__frame = -1 + self._fp = self.fp + self._frame_pos: list[int] = [] + self._n_frames: int | None = None + + logger.debug("*** TiffImageFile._open ***") + logger.debug("- __first: %s", self.__first) + logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes) + + # and load the first frame + self._seek(0) + + @property + def n_frames(self) -> int: + current_n_frames = self._n_frames + if current_n_frames is None: + current = self.tell() + self._seek(len(self._frame_pos)) + while self._n_frames is None: + self._seek(self.tell() + 1) + self.seek(current) + assert self._n_frames is not None + return self._n_frames + + def seek(self, frame: int) -> None: + """Select a given frame as current image""" + if not self._seek_check(frame): + return + self._seek(frame) + if self._im is not None and ( + self.im.size != self._tile_size + or self.im.mode != self.mode + or self.readonly + ): + self._im = None + + def _seek(self, frame: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex + self.fp = self._fp + + while len(self._frame_pos) <= frame: + if not self.__next: + msg = "no more images in TIFF file" + raise EOFError(msg) + logger.debug( + "Seeking to frame %s, on frame %s, __next %s, location: %s", + frame, + self.__frame, + self.__next, + self.fp.tell(), + ) + if self.__next >= 2**63: + msg = "Unable to seek to frame" + raise ValueError(msg) + self.fp.seek(self.__next) + self._frame_pos.append(self.__next) + logger.debug("Loading tags, location: %s", self.fp.tell()) + self.tag_v2.load(self.fp) + if self.tag_v2.next in self._frame_pos: + # This IFD has already been processed + # Declare this to be the end of the image + self.__next = 0 + else: + self.__next = self.tag_v2.next + if self.__next == 0: + self._n_frames = frame + 1 + if len(self._frame_pos) == 1: + self.is_animated = self.__next != 0 + self.__frame += 1 + self.fp.seek(self._frame_pos[frame]) + self.tag_v2.load(self.fp) + if XMP in self.tag_v2: + xmp = self.tag_v2[XMP] + if isinstance(xmp, tuple) and len(xmp) == 1: + xmp = xmp[0] + self.info["xmp"] = xmp + elif "xmp" in self.info: + del self.info["xmp"] + self._reload_exif() + # fill the legacy tag/ifd entries + self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) + self.__frame = frame + self._setup() + + def tell(self) -> int: + """Return the current frame number""" + return self.__frame + + def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]: + """ + Returns a dictionary of Photoshop "Image Resource Blocks". + The keys are the image resource ID. For more information, see + https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727 + + :returns: Photoshop "Image Resource Blocks" in a dictionary. + """ + blocks = {} + val = self.tag_v2.get(ExifTags.Base.ImageResources) + if val: + while val.startswith(b"8BIM"): + id = i16(val[4:6]) + n = math.ceil((val[6] + 1) / 2) * 2 + size = i32(val[6 + n : 10 + n]) + data = val[10 + n : 10 + n + size] + blocks[id] = {"data": data} + + val = val[math.ceil((10 + n + size) / 2) * 2 :] + return blocks + + def load(self) -> Image.core.PixelAccess | None: + if self.tile and self.use_load_libtiff: + return self._load_libtiff() + return super().load() + + def load_prepare(self) -> None: + if self._im is None: + Image._decompression_bomb_check(self._tile_size) + self.im = Image.core.new(self.mode, self._tile_size) + ImageFile.ImageFile.load_prepare(self) + + def load_end(self) -> None: + # allow closing if we're on the first frame, there's no next + # This is the ImageFile.load path only, libtiff specific below. + if not self.is_animated: + self._close_exclusive_fp_after_loading = True + + # load IFD data from fp before it is closed + exif = self.getexif() + for key in TiffTags.TAGS_V2_GROUPS: + if key not in exif: + continue + exif.get_ifd(key) + + ImageOps.exif_transpose(self, in_place=True) + if ExifTags.Base.Orientation in self.tag_v2: + del self.tag_v2[ExifTags.Base.Orientation] + + def _load_libtiff(self) -> Image.core.PixelAccess | None: + """Overload method triggered when we detect a compressed tiff + Calls out to libtiff""" + + Image.Image.load(self) + + self.load_prepare() + + if not len(self.tile) == 1: + msg = "Not exactly one tile" + raise OSError(msg) + + # (self._compression, (extents tuple), + # 0, (rawmode, self._compression, fp)) + extents = self.tile[0][1] + args = self.tile[0][3] + + # To be nice on memory footprint, if there's a + # file descriptor, use that instead of reading + # into a string in python. + try: + fp = hasattr(self.fp, "fileno") and self.fp.fileno() + # flush the file descriptor, prevents error on pypy 2.4+ + # should also eliminate the need for fp.tell + # in _seek + if hasattr(self.fp, "flush"): + self.fp.flush() + except OSError: + # io.BytesIO have a fileno, but returns an OSError if + # it doesn't use a file descriptor. + fp = False + + if fp: + assert isinstance(args, tuple) + args_list = list(args) + args_list[2] = fp + args = tuple(args_list) + + decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig) + try: + decoder.setimage(self.im, extents) + except ValueError as e: + msg = "Couldn't set the image" + raise OSError(msg) from e + + close_self_fp = self._exclusive_fp and not self.is_animated + if hasattr(self.fp, "getvalue"): + # We've got a stringio like thing passed in. Yay for all in memory. + # The decoder needs the entire file in one shot, so there's not + # a lot we can do here other than give it the entire file. + # unless we could do something like get the address of the + # underlying string for stringio. + # + # Rearranging for supporting byteio items, since they have a fileno + # that returns an OSError if there's no underlying fp. Easier to + # deal with here by reordering. + logger.debug("have getvalue. just sending in a string from getvalue") + n, err = decoder.decode(self.fp.getvalue()) + elif fp: + # we've got a actual file on disk, pass in the fp. + logger.debug("have fileno, calling fileno version of the decoder.") + if not close_self_fp: + self.fp.seek(0) + # Save and restore the file position, because libtiff will move it + # outside of the Python runtime, and that will confuse + # io.BufferedReader and possible others. + # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), + # because the buffer read head already may not equal the actual + # file position, and fp.seek() may just adjust it's internal + # pointer and not actually seek the OS file handle. + pos = os.lseek(fp, 0, os.SEEK_CUR) + # 4 bytes, otherwise the trace might error out + n, err = decoder.decode(b"fpfp") + os.lseek(fp, pos, os.SEEK_SET) + else: + # we have something else. + logger.debug("don't have fileno or getvalue. just reading") + self.fp.seek(0) + # UNDONE -- so much for that buffer size thing. + n, err = decoder.decode(self.fp.read()) + + self.tile = [] + self.readonly = 0 + + self.load_end() + + if close_self_fp: + self.fp.close() + self.fp = None # might be shared + + if err < 0: + msg = f"decoder error {err}" + raise OSError(msg) + + return Image.Image.load(self) + + def _setup(self) -> None: + """Setup this image object based on current tags""" + + if 0xBC01 in self.tag_v2: + msg = "Windows Media Photo files not yet supported" + raise OSError(msg) + + # extract relevant tags + self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] + self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) + + # photometric is a required tag, but not everyone is reading + # the specification + photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) + + # old style jpeg compression images most certainly are YCbCr + if self._compression == "tiff_jpeg": + photo = 6 + + fillorder = self.tag_v2.get(FILLORDER, 1) + + logger.debug("*** Summary ***") + logger.debug("- compression: %s", self._compression) + logger.debug("- photometric_interpretation: %s", photo) + logger.debug("- planar_configuration: %s", self._planar_configuration) + logger.debug("- fill_order: %s", fillorder) + logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) + + # size + try: + xsize = self.tag_v2[IMAGEWIDTH] + ysize = self.tag_v2[IMAGELENGTH] + except KeyError as e: + msg = "Missing dimensions" + raise TypeError(msg) from e + if not isinstance(xsize, int) or not isinstance(ysize, int): + msg = "Invalid dimensions" + raise ValueError(msg) + self._tile_size = xsize, ysize + orientation = self.tag_v2.get(ExifTags.Base.Orientation) + if orientation in (5, 6, 7, 8): + self._size = ysize, xsize + else: + self._size = xsize, ysize + + logger.debug("- size: %s", self.size) + + sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) + if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: + # SAMPLEFORMAT is properly per band, so an RGB image will + # be (1,1,1). But, we don't support per band pixel types, + # and anything more than one band is a uint8. So, just + # take the first element. Revisit this if adding support + # for more exotic images. + sample_format = (1,) + + bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) + extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) + if photo in (2, 6, 8): # RGB, YCbCr, LAB + bps_count = 3 + elif photo == 5: # CMYK + bps_count = 4 + else: + bps_count = 1 + bps_count += len(extra_tuple) + bps_actual_count = len(bps_tuple) + samples_per_pixel = self.tag_v2.get( + SAMPLESPERPIXEL, + 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, + ) + + if samples_per_pixel > MAX_SAMPLESPERPIXEL: + # DOS check, samples_per_pixel can be a Long, and we extend the tuple below + logger.error( + "More samples per pixel than can be decoded: %s", samples_per_pixel + ) + msg = "Invalid value for samples per pixel" + raise SyntaxError(msg) + + if samples_per_pixel < bps_actual_count: + # If a file has more values in bps_tuple than expected, + # remove the excess. + bps_tuple = bps_tuple[:samples_per_pixel] + elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: + # If a file has only one value in bps_tuple, when it should have more, + # presume it is the same number of bits for all of the samples. + bps_tuple = bps_tuple * samples_per_pixel + + if len(bps_tuple) != samples_per_pixel: + msg = "unknown data organization" + raise SyntaxError(msg) + + # mode: check photometric interpretation and bits per pixel + key = ( + self.tag_v2.prefix, + photo, + sample_format, + fillorder, + bps_tuple, + extra_tuple, + ) + logger.debug("format key: %s", key) + try: + self._mode, rawmode = OPEN_INFO[key] + except KeyError as e: + logger.debug("- unsupported format") + msg = "unknown pixel mode" + raise SyntaxError(msg) from e + + logger.debug("- raw mode: %s", rawmode) + logger.debug("- pil mode: %s", self.mode) + + self.info["compression"] = self._compression + + xres = self.tag_v2.get(X_RESOLUTION, 1) + yres = self.tag_v2.get(Y_RESOLUTION, 1) + + if xres and yres: + resunit = self.tag_v2.get(RESOLUTION_UNIT) + if resunit == 2: # dots per inch + self.info["dpi"] = (xres, yres) + elif resunit == 3: # dots per centimeter. convert to dpi + self.info["dpi"] = (xres * 2.54, yres * 2.54) + elif resunit is None: # used to default to 1, but now 2) + self.info["dpi"] = (xres, yres) + # For backward compatibility, + # we also preserve the old behavior + self.info["resolution"] = xres, yres + else: # No absolute unit of measurement + self.info["resolution"] = xres, yres + + # build tile descriptors + x = y = layer = 0 + self.tile = [] + self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw" + if self.use_load_libtiff: + # Decoder expects entire file as one tile. + # There's a buffer size limit in load (64k) + # so large g4 images will fail if we use that + # function. + # + # Setup the one tile for the whole image, then + # use the _load_libtiff function. + + # libtiff handles the fillmode for us, so 1;IR should + # actually be 1;I. Including the R double reverses the + # bits, so stripes of the image are reversed. See + # https://github.com/python-pillow/Pillow/issues/279 + if fillorder == 2: + # Replace fillorder with fillorder=1 + key = key[:3] + (1,) + key[4:] + logger.debug("format key: %s", key) + # this should always work, since all the + # fillorder==2 modes have a corresponding + # fillorder=1 mode + self._mode, rawmode = OPEN_INFO[key] + # YCbCr images with new jpeg compression with pixels in one plane + # unpacked straight into RGB values + if ( + photo == 6 + and self._compression == "jpeg" + and self._planar_configuration == 1 + ): + rawmode = "RGB" + # libtiff always returns the bytes in native order. + # we're expecting image byte order. So, if the rawmode + # contains I;16, we need to convert from native to image + # byte order. + elif rawmode == "I;16": + rawmode = "I;16N" + elif rawmode.endswith((";16B", ";16L")): + rawmode = rawmode[:-1] + "N" + + # Offset in the tile tuple is 0, we go from 0,0 to + # w,h, and we only do this once -- eds + a = (rawmode, self._compression, False, self.tag_v2.offset) + self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a)) + + elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: + # striped image + if STRIPOFFSETS in self.tag_v2: + offsets = self.tag_v2[STRIPOFFSETS] + h = self.tag_v2.get(ROWSPERSTRIP, ysize) + w = xsize + else: + # tiled image + offsets = self.tag_v2[TILEOFFSETS] + tilewidth = self.tag_v2.get(TILEWIDTH) + h = self.tag_v2.get(TILELENGTH) + if not isinstance(tilewidth, int) or not isinstance(h, int): + msg = "Invalid tile dimensions" + raise ValueError(msg) + w = tilewidth + + if w == xsize and h == ysize and self._planar_configuration != 2: + # Every tile covers the image. Only use the last offset + offsets = offsets[-1:] + + for offset in offsets: + if x + w > xsize: + stride = w * sum(bps_tuple) / 8 # bytes per line + else: + stride = 0 + + tile_rawmode = rawmode + if self._planar_configuration == 2: + # each band on it's own layer + tile_rawmode = rawmode[layer] + # adjust stride width accordingly + stride /= bps_count + + args = (tile_rawmode, int(stride), 1) + self.tile.append( + ImageFile._Tile( + self._compression, + (x, y, min(x + w, xsize), min(y + h, ysize)), + offset, + args, + ) + ) + x += w + if x >= xsize: + x, y = 0, y + h + if y >= ysize: + y = 0 + layer += 1 + else: + logger.debug("- unsupported data organization") + msg = "unknown data organization" + raise SyntaxError(msg) + + # Fix up info. + if ICCPROFILE in self.tag_v2: + self.info["icc_profile"] = self.tag_v2[ICCPROFILE] + + # fixup palette descriptor + + if self.mode in ["P", "PA"]: + palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] + self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) + + +# +# -------------------------------------------------------------------- +# Write TIFF files + +# little endian is default except for image modes with +# explicit big endian byte-order + +SAVE_INFO = { + # mode => rawmode, byteorder, photometrics, + # sampleformat, bitspersample, extra + "1": ("1", II, 1, 1, (1,), None), + "L": ("L", II, 1, 1, (8,), None), + "LA": ("LA", II, 1, 1, (8, 8), 2), + "P": ("P", II, 3, 1, (8,), None), + "PA": ("PA", II, 3, 1, (8, 8), 2), + "I": ("I;32S", II, 1, 2, (32,), None), + "I;16": ("I;16", II, 1, 1, (16,), None), + "I;16L": ("I;16L", II, 1, 1, (16,), None), + "F": ("F;32F", II, 1, 3, (32,), None), + "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), + "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), + "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), + "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), + "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), + "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), + "I;16B": ("I;16B", MM, 1, 1, (16,), None), +} + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + try: + rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] + except KeyError as e: + msg = f"cannot write mode {im.mode} as TIFF" + raise OSError(msg) from e + + encoderinfo = im.encoderinfo + encoderconfig = im.encoderconfig + + ifd = ImageFileDirectory_v2(prefix=prefix) + if encoderinfo.get("big_tiff"): + ifd._bigtiff = True + + try: + compression = encoderinfo["compression"] + except KeyError: + compression = im.info.get("compression") + if isinstance(compression, int): + # compression value may be from BMP. Ignore it + compression = None + if compression is None: + compression = "raw" + elif compression == "tiff_jpeg": + # OJPEG is obsolete, so use new-style JPEG compression instead + compression = "jpeg" + elif compression == "tiff_deflate": + compression = "tiff_adobe_deflate" + + libtiff = WRITE_LIBTIFF or compression != "raw" + + # required for color libtiff images + ifd[PLANAR_CONFIGURATION] = 1 + + ifd[IMAGEWIDTH] = im.size[0] + ifd[IMAGELENGTH] = im.size[1] + + # write any arbitrary tags passed in as an ImageFileDirectory + if "tiffinfo" in encoderinfo: + info = encoderinfo["tiffinfo"] + elif "exif" in encoderinfo: + info = encoderinfo["exif"] + if isinstance(info, bytes): + exif = Image.Exif() + exif.load(info) + info = exif + else: + info = {} + logger.debug("Tiffinfo Keys: %s", list(info)) + if isinstance(info, ImageFileDirectory_v1): + info = info.to_v2() + for key in info: + if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: + ifd[key] = info.get_ifd(key) + else: + ifd[key] = info.get(key) + try: + ifd.tagtype[key] = info.tagtype[key] + except Exception: + pass # might not be an IFD. Might not have populated type + + legacy_ifd = {} + if hasattr(im, "tag"): + legacy_ifd = im.tag.to_v2() + + supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})} + for tag in ( + # IFD offset that may not be correct in the saved image + EXIFIFD, + # Determined by the image format and should not be copied from legacy_ifd. + SAMPLEFORMAT, + ): + if tag in supplied_tags: + del supplied_tags[tag] + + # additions written by Greg Couch, gregc@cgl.ucsf.edu + # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com + if hasattr(im, "tag_v2"): + # preserve tags from original TIFF image file + for key in ( + RESOLUTION_UNIT, + X_RESOLUTION, + Y_RESOLUTION, + IPTC_NAA_CHUNK, + PHOTOSHOP_CHUNK, + XMP, + ): + if key in im.tag_v2: + if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in ( + TiffTags.BYTE, + TiffTags.UNDEFINED, + ): + del supplied_tags[key] + else: + ifd[key] = im.tag_v2[key] + ifd.tagtype[key] = im.tag_v2.tagtype[key] + + # preserve ICC profile (should also work when saving other formats + # which support profiles as TIFF) -- 2008-06-06 Florian Hoech + icc = encoderinfo.get("icc_profile", im.info.get("icc_profile")) + if icc: + ifd[ICCPROFILE] = icc + + for key, name in [ + (IMAGEDESCRIPTION, "description"), + (X_RESOLUTION, "resolution"), + (Y_RESOLUTION, "resolution"), + (X_RESOLUTION, "x_resolution"), + (Y_RESOLUTION, "y_resolution"), + (RESOLUTION_UNIT, "resolution_unit"), + (SOFTWARE, "software"), + (DATE_TIME, "date_time"), + (ARTIST, "artist"), + (COPYRIGHT, "copyright"), + ]: + if name in encoderinfo: + ifd[key] = encoderinfo[name] + + dpi = encoderinfo.get("dpi") + if dpi: + ifd[RESOLUTION_UNIT] = 2 + ifd[X_RESOLUTION] = dpi[0] + ifd[Y_RESOLUTION] = dpi[1] + + if bits != (1,): + ifd[BITSPERSAMPLE] = bits + if len(bits) != 1: + ifd[SAMPLESPERPIXEL] = len(bits) + if extra is not None: + ifd[EXTRASAMPLES] = extra + if format != 1: + ifd[SAMPLEFORMAT] = format + + if PHOTOMETRIC_INTERPRETATION not in ifd: + ifd[PHOTOMETRIC_INTERPRETATION] = photo + elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0: + if im.mode == "1": + inverted_im = im.copy() + px = inverted_im.load() + if px is not None: + for y in range(inverted_im.height): + for x in range(inverted_im.width): + px[x, y] = 0 if px[x, y] == 255 else 255 + im = inverted_im + else: + im = ImageOps.invert(im) + + if im.mode in ["P", "PA"]: + lut = im.im.getpalette("RGB", "RGB;L") + colormap = [] + colors = len(lut) // 3 + for i in range(3): + colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]] + colormap += [0] * (256 - colors) + ifd[COLORMAP] = colormap + # data orientation + w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] + stride = len(bits) * ((w * bits[0] + 7) // 8) + if ROWSPERSTRIP not in ifd: + # aim for given strip size (64 KB by default) when using libtiff writer + if libtiff: + im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) + rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) + # JPEG encoder expects multiple of 8 rows + if compression == "jpeg": + rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) + else: + rows_per_strip = h + if rows_per_strip == 0: + rows_per_strip = 1 + ifd[ROWSPERSTRIP] = rows_per_strip + strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP] + strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP] + if strip_byte_counts >= 2**16: + ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG + ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( + stride * h - strip_byte_counts * (strips_per_image - 1), + ) + ifd[STRIPOFFSETS] = tuple( + range(0, strip_byte_counts * strips_per_image, strip_byte_counts) + ) # this is adjusted by IFD writer + # no compression by default: + ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) + + if im.mode == "YCbCr": + for tag, default_value in { + YCBCRSUBSAMPLING: (1, 1), + REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), + }.items(): + ifd.setdefault(tag, default_value) + + blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] + if libtiff: + if "quality" in encoderinfo: + quality = encoderinfo["quality"] + if not isinstance(quality, int) or quality < 0 or quality > 100: + msg = "Invalid quality setting" + raise ValueError(msg) + if compression != "jpeg": + msg = "quality setting only supported for 'jpeg' compression" + raise ValueError(msg) + ifd[JPEGQUALITY] = quality + + logger.debug("Saving using libtiff encoder") + logger.debug("Items: %s", sorted(ifd.items())) + _fp = 0 + if hasattr(fp, "fileno"): + try: + fp.seek(0) + _fp = fp.fileno() + except io.UnsupportedOperation: + pass + + # optional types for non core tags + types = {} + # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library + # based on the data in the strip. + # OSUBFILETYPE is deprecated. + # The other tags expect arrays with a certain length (fixed or depending on + # BITSPERSAMPLE, etc), passing arrays with a different length will result in + # segfaults. Block these tags until we add extra validation. + # SUBIFD may also cause a segfault. + blocklist += [ + OSUBFILETYPE, + REFERENCEBLACKWHITE, + STRIPBYTECOUNTS, + STRIPOFFSETS, + TRANSFERFUNCTION, + SUBIFD, + ] + + # bits per sample is a single short in the tiff directory, not a list. + atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]} + # Merge the ones that we have with (optional) more bits from + # the original file, e.g x,y resolution so that we can + # save(load('')) == original file. + for tag, value in itertools.chain(ifd.items(), supplied_tags.items()): + # Libtiff can only process certain core items without adding + # them to the custom dictionary. + # Custom items are supported for int, float, unicode, string and byte + # values. Other types and tuples require a tagtype. + if tag not in TiffTags.LIBTIFF_CORE: + if not getattr(Image.core, "libtiff_support_custom_tags", False): + continue + + if tag in TiffTags.TAGS_V2_GROUPS: + types[tag] = TiffTags.LONG8 + elif tag in ifd.tagtype: + types[tag] = ifd.tagtype[tag] + elif not (isinstance(value, (int, float, str, bytes))): + continue + else: + type = TiffTags.lookup(tag).type + if type: + types[tag] = type + if tag not in atts and tag not in blocklist: + if isinstance(value, str): + atts[tag] = value.encode("ascii", "replace") + b"\0" + elif isinstance(value, IFDRational): + atts[tag] = float(value) + else: + atts[tag] = value + + if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1: + atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] + + logger.debug("Converted items: %s", sorted(atts.items())) + + # libtiff always expects the bytes in native order. + # we're storing image byte order. So, if the rawmode + # contains I;16, we need to convert from native to image + # byte order. + if im.mode in ("I;16", "I;16B", "I;16L"): + rawmode = "I;16N" + + # Pass tags as sorted list so that the tags are set in a fixed order. + # This is required by libtiff for some tags. For example, the JPEGQUALITY + # pseudo tag requires that the COMPRESS tag was already set. + tags = list(atts.items()) + tags.sort() + a = (rawmode, compression, _fp, filename, tags, types) + encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) + encoder.setimage(im.im, (0, 0) + im.size) + while True: + errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:] + if not _fp: + fp.write(data) + if errcode: + break + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" + raise OSError(msg) + + else: + for tag in blocklist: + del ifd[tag] + offset = ifd.save(fp) + + ImageFile._save( + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))], + ) + + # -- helper for multi-page save -- + if "_debug_multipage" in encoderinfo: + # just to access o32 and o16 (using correct byte order) + setattr(im, "_debug_multipage", ifd) + + +class AppendingTiffWriter(io.BytesIO): + fieldSizes = [ + 0, # None + 1, # byte + 1, # ascii + 2, # short + 4, # long + 8, # rational + 1, # sbyte + 1, # undefined + 2, # sshort + 4, # slong + 8, # srational + 4, # float + 8, # double + 4, # ifd + 2, # unicode + 4, # complex + 8, # long8 + ] + + Tags = { + 273, # StripOffsets + 288, # FreeOffsets + 324, # TileOffsets + 519, # JPEGQTables + 520, # JPEGDCTables + 521, # JPEGACTables + } + + def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None: + self.f: IO[bytes] + if is_path(fn): + self.name = fn + self.close_fp = True + try: + self.f = open(fn, "w+b" if new else "r+b") + except OSError: + self.f = open(fn, "w+b") + else: + self.f = cast(IO[bytes], fn) + self.close_fp = False + self.beginning = self.f.tell() + self.setup() + + def setup(self) -> None: + # Reset everything. + self.f.seek(self.beginning, os.SEEK_SET) + + self.whereToWriteNewIFDOffset: int | None = None + self.offsetOfNewPage = 0 + + self.IIMM = iimm = self.f.read(4) + self._bigtiff = b"\x2b" in iimm + if not iimm: + # empty file - first page + self.isFirst = True + return + + self.isFirst = False + if iimm not in PREFIXES: + msg = "Invalid TIFF file header" + raise RuntimeError(msg) + + self.setEndian("<" if iimm.startswith(II) else ">") + + if self._bigtiff: + self.f.seek(4, os.SEEK_CUR) + self.skipIFDs() + self.goToEnd() + + def finalize(self) -> None: + if self.isFirst: + return + + # fix offsets + self.f.seek(self.offsetOfNewPage) + + iimm = self.f.read(4) + if not iimm: + # Make it easy to finish a frame without committing to a new one. + return + + if iimm != self.IIMM: + msg = "IIMM of new page doesn't match IIMM of first page" + raise RuntimeError(msg) + + if self._bigtiff: + self.f.seek(4, os.SEEK_CUR) + ifd_offset = self._read(8 if self._bigtiff else 4) + ifd_offset += self.offsetOfNewPage + assert self.whereToWriteNewIFDOffset is not None + self.f.seek(self.whereToWriteNewIFDOffset) + self._write(ifd_offset, 8 if self._bigtiff else 4) + self.f.seek(ifd_offset) + self.fixIFD() + + def newFrame(self) -> None: + # Call this to finish a frame. + self.finalize() + self.setup() + + def __enter__(self) -> AppendingTiffWriter: + return self + + def __exit__(self, *args: object) -> None: + if self.close_fp: + self.close() + + def tell(self) -> int: + return self.f.tell() - self.offsetOfNewPage + + def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: + """ + :param offset: Distance to seek. + :param whence: Whether the distance is relative to the start, + end or current position. + :returns: The resulting position, relative to the start. + """ + if whence == os.SEEK_SET: + offset += self.offsetOfNewPage + + self.f.seek(offset, whence) + return self.tell() + + def goToEnd(self) -> None: + self.f.seek(0, os.SEEK_END) + pos = self.f.tell() + + # pad to 16 byte boundary + pad_bytes = 16 - pos % 16 + if 0 < pad_bytes < 16: + self.f.write(bytes(pad_bytes)) + self.offsetOfNewPage = self.f.tell() + + def setEndian(self, endian: str) -> None: + self.endian = endian + self.longFmt = f"{self.endian}L" + self.shortFmt = f"{self.endian}H" + self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L") + + def skipIFDs(self) -> None: + while True: + ifd_offset = self._read(8 if self._bigtiff else 4) + if ifd_offset == 0: + self.whereToWriteNewIFDOffset = self.f.tell() - ( + 8 if self._bigtiff else 4 + ) + break + + self.f.seek(ifd_offset) + num_tags = self._read(8 if self._bigtiff else 2) + self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR) + + def write(self, data: Buffer, /) -> int: + return self.f.write(data) + + def _fmt(self, field_size: int) -> str: + try: + return {2: "H", 4: "L", 8: "Q"}[field_size] + except KeyError: + msg = "offset is not supported" + raise RuntimeError(msg) + + def _read(self, field_size: int) -> int: + (value,) = struct.unpack( + self.endian + self._fmt(field_size), self.f.read(field_size) + ) + return value + + def readShort(self) -> int: + return self._read(2) + + def readLong(self) -> int: + return self._read(4) + + @staticmethod + def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: + if bytes_written is not None and bytes_written != expected: + msg = f"wrote only {bytes_written} bytes but wanted {expected}" + raise RuntimeError(msg) + + def _rewriteLast( + self, value: int, field_size: int, new_field_size: int = 0 + ) -> None: + self.f.seek(-field_size, os.SEEK_CUR) + if not new_field_size: + new_field_size = field_size + bytes_written = self.f.write( + struct.pack(self.endian + self._fmt(new_field_size), value) + ) + self._verify_bytes_written(bytes_written, new_field_size) + + def rewriteLastShortToLong(self, value: int) -> None: + self._rewriteLast(value, 2, 4) + + def rewriteLastShort(self, value: int) -> None: + return self._rewriteLast(value, 2) + + def rewriteLastLong(self, value: int) -> None: + return self._rewriteLast(value, 4) + + def _write(self, value: int, field_size: int) -> None: + bytes_written = self.f.write( + struct.pack(self.endian + self._fmt(field_size), value) + ) + self._verify_bytes_written(bytes_written, field_size) + + def writeShort(self, value: int) -> None: + self._write(value, 2) + + def writeLong(self, value: int) -> None: + self._write(value, 4) + + def close(self) -> None: + self.finalize() + if self.close_fp: + self.f.close() + + def fixIFD(self) -> None: + num_tags = self._read(8 if self._bigtiff else 2) + + for i in range(num_tags): + tag, field_type, count = struct.unpack( + self.tagFormat, self.f.read(12 if self._bigtiff else 8) + ) + + field_size = self.fieldSizes[field_type] + total_size = field_size * count + fmt_size = 8 if self._bigtiff else 4 + is_local = total_size <= fmt_size + if not is_local: + offset = self._read(fmt_size) + self.offsetOfNewPage + self._rewriteLast(offset, fmt_size) + + if tag in self.Tags: + cur_pos = self.f.tell() + + logger.debug( + "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d", + TiffTags.lookup(tag).name, + tag, + TYPES.get(field_type, "unknown"), + field_type, + field_size, + count, + ) + + if is_local: + self._fixOffsets(count, field_size) + self.f.seek(cur_pos + fmt_size) + else: + self.f.seek(offset) + self._fixOffsets(count, field_size) + self.f.seek(cur_pos) + + elif is_local: + # skip the locally stored value that is not an offset + self.f.seek(fmt_size, os.SEEK_CUR) + + def _fixOffsets(self, count: int, field_size: int) -> None: + for i in range(count): + offset = self._read(field_size) + offset += self.offsetOfNewPage + + new_field_size = 0 + if self._bigtiff and field_size in (2, 4) and offset >= 2**32: + # offset is now too large - we must convert long to long8 + new_field_size = 8 + elif field_size == 2 and offset >= 2**16: + # offset is now too large - we must convert short to long + new_field_size = 4 + if new_field_size: + if count != 1: + msg = "not implemented" + raise RuntimeError(msg) # XXX TODO + + # simple case - the offset is just one and therefore it is + # local (not referenced with another offset) + self._rewriteLast(offset, field_size, new_field_size) + # Move back past the new offset, past 'count', and before 'field_type' + rewind = -new_field_size - 4 - 2 + self.f.seek(rewind, os.SEEK_CUR) + self.writeShort(new_field_size) # rewrite the type + self.f.seek(2 - rewind, os.SEEK_CUR) + else: + self._rewriteLast(offset, field_size) + + def fixOffsets( + self, count: int, isShort: bool = False, isLong: bool = False + ) -> None: + if isShort: + field_size = 2 + elif isLong: + field_size = 4 + else: + field_size = 0 + return self._fixOffsets(count, field_size) + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + append_images = list(im.encoderinfo.get("append_images", [])) + if not hasattr(im, "n_frames") and not append_images: + return _save(im, fp, filename) + + cur_idx = im.tell() + try: + with AppendingTiffWriter(fp) as tf: + for ims in [im] + append_images: + encoderinfo = ims._attach_default_encoderinfo(im) + if not hasattr(ims, "encoderconfig"): + ims.encoderconfig = () + nfr = getattr(ims, "n_frames", 1) + + for idx in range(nfr): + ims.seek(idx) + ims.load() + _save(ims, tf, filename) + tf.newFrame() + ims.encoderinfo = encoderinfo + finally: + im.seek(cur_idx) + + +# +# -------------------------------------------------------------------- +# Register + +Image.register_open(TiffImageFile.format, TiffImageFile, _accept) +Image.register_save(TiffImageFile.format, _save) +Image.register_save_all(TiffImageFile.format, _save_all) + +Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"]) + +Image.register_mime(TiffImageFile.format, "image/tiff") diff --git a/python_modules/PIL/TiffTags.py b/python_modules/PIL/TiffTags.py new file mode 100644 index 000000000..86adaa458 --- /dev/null +++ b/python_modules/PIL/TiffTags.py @@ -0,0 +1,562 @@ +# +# The Python Imaging Library. +# $Id$ +# +# TIFF tags +# +# This module provides clear-text names for various well-known +# TIFF tags. the TIFF codec works just fine without it. +# +# Copyright (c) Secret Labs AB 1999. +# +# See the README file for information on usage and redistribution. +# + +## +# This module provides constants and clear-text names for various +# well-known TIFF tags. +## +from __future__ import annotations + +from typing import NamedTuple + + +class _TagInfo(NamedTuple): + value: int | None + name: str + type: int | None + length: int | None + enum: dict[str, int] + + +class TagInfo(_TagInfo): + __slots__: list[str] = [] + + def __new__( + cls, + value: int | None = None, + name: str = "unknown", + type: int | None = None, + length: int | None = None, + enum: dict[str, int] | None = None, + ) -> TagInfo: + return super().__new__(cls, value, name, type, length, enum or {}) + + def cvt_enum(self, value: str) -> int | str: + # Using get will call hash(value), which can be expensive + # for some types (e.g. Fraction). Since self.enum is rarely + # used, it's usually better to test it first. + return self.enum.get(value, value) if self.enum else value + + +def lookup(tag: int, group: int | None = None) -> TagInfo: + """ + :param tag: Integer tag number + :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in + + .. versionadded:: 8.3.0 + + :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible, + otherwise just populating the value and name from ``TAGS``. + If the tag is not recognized, "unknown" is returned for the name + + """ + + if group is not None: + info = TAGS_V2_GROUPS[group].get(tag) if group in TAGS_V2_GROUPS else None + else: + info = TAGS_V2.get(tag) + return info or TagInfo(tag, TAGS.get(tag, "unknown")) + + +## +# Map tag numbers to tag info. +# +# id: (Name, Type, Length[, enum_values]) +# +# The length here differs from the length in the tiff spec. For +# numbers, the tiff spec is for the number of fields returned. We +# agree here. For string-like types, the tiff spec uses the length of +# field in bytes. In Pillow, we are using the number of expected +# fields, in general 1 for string-like types. + + +BYTE = 1 +ASCII = 2 +SHORT = 3 +LONG = 4 +RATIONAL = 5 +SIGNED_BYTE = 6 +UNDEFINED = 7 +SIGNED_SHORT = 8 +SIGNED_LONG = 9 +SIGNED_RATIONAL = 10 +FLOAT = 11 +DOUBLE = 12 +IFD = 13 +LONG8 = 16 + +_tags_v2: dict[int, tuple[str, int, int] | tuple[str, int, int, dict[str, int]]] = { + 254: ("NewSubfileType", LONG, 1), + 255: ("SubfileType", SHORT, 1), + 256: ("ImageWidth", LONG, 1), + 257: ("ImageLength", LONG, 1), + 258: ("BitsPerSample", SHORT, 0), + 259: ( + "Compression", + SHORT, + 1, + { + "Uncompressed": 1, + "CCITT 1d": 2, + "Group 3 Fax": 3, + "Group 4 Fax": 4, + "LZW": 5, + "JPEG": 6, + "PackBits": 32773, + }, + ), + 262: ( + "PhotometricInterpretation", + SHORT, + 1, + { + "WhiteIsZero": 0, + "BlackIsZero": 1, + "RGB": 2, + "RGB Palette": 3, + "Transparency Mask": 4, + "CMYK": 5, + "YCbCr": 6, + "CieLAB": 8, + "CFA": 32803, # TIFF/EP, Adobe DNG + "LinearRaw": 32892, # Adobe DNG + }, + ), + 263: ("Threshholding", SHORT, 1), + 264: ("CellWidth", SHORT, 1), + 265: ("CellLength", SHORT, 1), + 266: ("FillOrder", SHORT, 1), + 269: ("DocumentName", ASCII, 1), + 270: ("ImageDescription", ASCII, 1), + 271: ("Make", ASCII, 1), + 272: ("Model", ASCII, 1), + 273: ("StripOffsets", LONG, 0), + 274: ("Orientation", SHORT, 1), + 277: ("SamplesPerPixel", SHORT, 1), + 278: ("RowsPerStrip", LONG, 1), + 279: ("StripByteCounts", LONG, 0), + 280: ("MinSampleValue", SHORT, 0), + 281: ("MaxSampleValue", SHORT, 0), + 282: ("XResolution", RATIONAL, 1), + 283: ("YResolution", RATIONAL, 1), + 284: ("PlanarConfiguration", SHORT, 1, {"Contiguous": 1, "Separate": 2}), + 285: ("PageName", ASCII, 1), + 286: ("XPosition", RATIONAL, 1), + 287: ("YPosition", RATIONAL, 1), + 288: ("FreeOffsets", LONG, 1), + 289: ("FreeByteCounts", LONG, 1), + 290: ("GrayResponseUnit", SHORT, 1), + 291: ("GrayResponseCurve", SHORT, 0), + 292: ("T4Options", LONG, 1), + 293: ("T6Options", LONG, 1), + 296: ("ResolutionUnit", SHORT, 1, {"none": 1, "inch": 2, "cm": 3}), + 297: ("PageNumber", SHORT, 2), + 301: ("TransferFunction", SHORT, 0), + 305: ("Software", ASCII, 1), + 306: ("DateTime", ASCII, 1), + 315: ("Artist", ASCII, 1), + 316: ("HostComputer", ASCII, 1), + 317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}), + 318: ("WhitePoint", RATIONAL, 2), + 319: ("PrimaryChromaticities", RATIONAL, 6), + 320: ("ColorMap", SHORT, 0), + 321: ("HalftoneHints", SHORT, 2), + 322: ("TileWidth", LONG, 1), + 323: ("TileLength", LONG, 1), + 324: ("TileOffsets", LONG, 0), + 325: ("TileByteCounts", LONG, 0), + 330: ("SubIFDs", LONG, 0), + 332: ("InkSet", SHORT, 1), + 333: ("InkNames", ASCII, 1), + 334: ("NumberOfInks", SHORT, 1), + 336: ("DotRange", SHORT, 0), + 337: ("TargetPrinter", ASCII, 1), + 338: ("ExtraSamples", SHORT, 0), + 339: ("SampleFormat", SHORT, 0), + 340: ("SMinSampleValue", DOUBLE, 0), + 341: ("SMaxSampleValue", DOUBLE, 0), + 342: ("TransferRange", SHORT, 6), + 347: ("JPEGTables", UNDEFINED, 1), + # obsolete JPEG tags + 512: ("JPEGProc", SHORT, 1), + 513: ("JPEGInterchangeFormat", LONG, 1), + 514: ("JPEGInterchangeFormatLength", LONG, 1), + 515: ("JPEGRestartInterval", SHORT, 1), + 517: ("JPEGLosslessPredictors", SHORT, 0), + 518: ("JPEGPointTransforms", SHORT, 0), + 519: ("JPEGQTables", LONG, 0), + 520: ("JPEGDCTables", LONG, 0), + 521: ("JPEGACTables", LONG, 0), + 529: ("YCbCrCoefficients", RATIONAL, 3), + 530: ("YCbCrSubSampling", SHORT, 2), + 531: ("YCbCrPositioning", SHORT, 1), + 532: ("ReferenceBlackWhite", RATIONAL, 6), + 700: ("XMP", BYTE, 0), + 33432: ("Copyright", ASCII, 1), + 33723: ("IptcNaaInfo", UNDEFINED, 1), + 34377: ("PhotoshopInfo", BYTE, 0), + # FIXME add more tags here + 34665: ("ExifIFD", LONG, 1), + 34675: ("ICCProfile", UNDEFINED, 1), + 34853: ("GPSInfoIFD", LONG, 1), + 36864: ("ExifVersion", UNDEFINED, 1), + 37724: ("ImageSourceData", UNDEFINED, 1), + 40965: ("InteroperabilityIFD", LONG, 1), + 41730: ("CFAPattern", UNDEFINED, 1), + # MPInfo + 45056: ("MPFVersion", UNDEFINED, 1), + 45057: ("NumberOfImages", LONG, 1), + 45058: ("MPEntry", UNDEFINED, 1), + 45059: ("ImageUIDList", UNDEFINED, 0), # UNDONE, check + 45060: ("TotalFrames", LONG, 1), + 45313: ("MPIndividualNum", LONG, 1), + 45569: ("PanOrientation", LONG, 1), + 45570: ("PanOverlap_H", RATIONAL, 1), + 45571: ("PanOverlap_V", RATIONAL, 1), + 45572: ("BaseViewpointNum", LONG, 1), + 45573: ("ConvergenceAngle", SIGNED_RATIONAL, 1), + 45574: ("BaselineLength", RATIONAL, 1), + 45575: ("VerticalDivergence", SIGNED_RATIONAL, 1), + 45576: ("AxisDistance_X", SIGNED_RATIONAL, 1), + 45577: ("AxisDistance_Y", SIGNED_RATIONAL, 1), + 45578: ("AxisDistance_Z", SIGNED_RATIONAL, 1), + 45579: ("YawAngle", SIGNED_RATIONAL, 1), + 45580: ("PitchAngle", SIGNED_RATIONAL, 1), + 45581: ("RollAngle", SIGNED_RATIONAL, 1), + 40960: ("FlashPixVersion", UNDEFINED, 1), + 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), + 50780: ("BestQualityScale", RATIONAL, 1), + 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one + 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 +} +_tags_v2_groups = { + # ExifIFD + 34665: { + 36864: ("ExifVersion", UNDEFINED, 1), + 40960: ("FlashPixVersion", UNDEFINED, 1), + 40965: ("InteroperabilityIFD", LONG, 1), + 41730: ("CFAPattern", UNDEFINED, 1), + }, + # GPSInfoIFD + 34853: { + 0: ("GPSVersionID", BYTE, 4), + 1: ("GPSLatitudeRef", ASCII, 2), + 2: ("GPSLatitude", RATIONAL, 3), + 3: ("GPSLongitudeRef", ASCII, 2), + 4: ("GPSLongitude", RATIONAL, 3), + 5: ("GPSAltitudeRef", BYTE, 1), + 6: ("GPSAltitude", RATIONAL, 1), + 7: ("GPSTimeStamp", RATIONAL, 3), + 8: ("GPSSatellites", ASCII, 0), + 9: ("GPSStatus", ASCII, 2), + 10: ("GPSMeasureMode", ASCII, 2), + 11: ("GPSDOP", RATIONAL, 1), + 12: ("GPSSpeedRef", ASCII, 2), + 13: ("GPSSpeed", RATIONAL, 1), + 14: ("GPSTrackRef", ASCII, 2), + 15: ("GPSTrack", RATIONAL, 1), + 16: ("GPSImgDirectionRef", ASCII, 2), + 17: ("GPSImgDirection", RATIONAL, 1), + 18: ("GPSMapDatum", ASCII, 0), + 19: ("GPSDestLatitudeRef", ASCII, 2), + 20: ("GPSDestLatitude", RATIONAL, 3), + 21: ("GPSDestLongitudeRef", ASCII, 2), + 22: ("GPSDestLongitude", RATIONAL, 3), + 23: ("GPSDestBearingRef", ASCII, 2), + 24: ("GPSDestBearing", RATIONAL, 1), + 25: ("GPSDestDistanceRef", ASCII, 2), + 26: ("GPSDestDistance", RATIONAL, 1), + 27: ("GPSProcessingMethod", UNDEFINED, 0), + 28: ("GPSAreaInformation", UNDEFINED, 0), + 29: ("GPSDateStamp", ASCII, 11), + 30: ("GPSDifferential", SHORT, 1), + }, + # InteroperabilityIFD + 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)}, +} + +# Legacy Tags structure +# these tags aren't included above, but were in the previous versions +TAGS: dict[int | tuple[int, int], str] = { + 347: "JPEGTables", + 700: "XMP", + # Additional Exif Info + 32932: "Wang Annotation", + 33434: "ExposureTime", + 33437: "FNumber", + 33445: "MD FileTag", + 33446: "MD ScalePixel", + 33447: "MD ColorTable", + 33448: "MD LabName", + 33449: "MD SampleInfo", + 33450: "MD PrepDate", + 33451: "MD PrepTime", + 33452: "MD FileUnits", + 33550: "ModelPixelScaleTag", + 33723: "IptcNaaInfo", + 33918: "INGR Packet Data Tag", + 33919: "INGR Flag Registers", + 33920: "IrasB Transformation Matrix", + 33922: "ModelTiepointTag", + 34264: "ModelTransformationTag", + 34377: "PhotoshopInfo", + 34735: "GeoKeyDirectoryTag", + 34736: "GeoDoubleParamsTag", + 34737: "GeoAsciiParamsTag", + 34850: "ExposureProgram", + 34852: "SpectralSensitivity", + 34855: "ISOSpeedRatings", + 34856: "OECF", + 34864: "SensitivityType", + 34865: "StandardOutputSensitivity", + 34866: "RecommendedExposureIndex", + 34867: "ISOSpeed", + 34868: "ISOSpeedLatitudeyyy", + 34869: "ISOSpeedLatitudezzz", + 34908: "HylaFAX FaxRecvParams", + 34909: "HylaFAX FaxSubAddress", + 34910: "HylaFAX FaxRecvTime", + 36864: "ExifVersion", + 36867: "DateTimeOriginal", + 36868: "DateTimeDigitized", + 37121: "ComponentsConfiguration", + 37122: "CompressedBitsPerPixel", + 37724: "ImageSourceData", + 37377: "ShutterSpeedValue", + 37378: "ApertureValue", + 37379: "BrightnessValue", + 37380: "ExposureBiasValue", + 37381: "MaxApertureValue", + 37382: "SubjectDistance", + 37383: "MeteringMode", + 37384: "LightSource", + 37385: "Flash", + 37386: "FocalLength", + 37396: "SubjectArea", + 37500: "MakerNote", + 37510: "UserComment", + 37520: "SubSec", + 37521: "SubSecTimeOriginal", + 37522: "SubsecTimeDigitized", + 40960: "FlashPixVersion", + 40961: "ColorSpace", + 40962: "PixelXDimension", + 40963: "PixelYDimension", + 40964: "RelatedSoundFile", + 40965: "InteroperabilityIFD", + 41483: "FlashEnergy", + 41484: "SpatialFrequencyResponse", + 41486: "FocalPlaneXResolution", + 41487: "FocalPlaneYResolution", + 41488: "FocalPlaneResolutionUnit", + 41492: "SubjectLocation", + 41493: "ExposureIndex", + 41495: "SensingMethod", + 41728: "FileSource", + 41729: "SceneType", + 41730: "CFAPattern", + 41985: "CustomRendered", + 41986: "ExposureMode", + 41987: "WhiteBalance", + 41988: "DigitalZoomRatio", + 41989: "FocalLengthIn35mmFilm", + 41990: "SceneCaptureType", + 41991: "GainControl", + 41992: "Contrast", + 41993: "Saturation", + 41994: "Sharpness", + 41995: "DeviceSettingDescription", + 41996: "SubjectDistanceRange", + 42016: "ImageUniqueID", + 42032: "CameraOwnerName", + 42033: "BodySerialNumber", + 42034: "LensSpecification", + 42035: "LensMake", + 42036: "LensModel", + 42037: "LensSerialNumber", + 42112: "GDAL_METADATA", + 42113: "GDAL_NODATA", + 42240: "Gamma", + 50215: "Oce Scanjob Description", + 50216: "Oce Application Selector", + 50217: "Oce Identification Number", + 50218: "Oce ImageLogic Characteristics", + # Adobe DNG + 50706: "DNGVersion", + 50707: "DNGBackwardVersion", + 50708: "UniqueCameraModel", + 50709: "LocalizedCameraModel", + 50710: "CFAPlaneColor", + 50711: "CFALayout", + 50712: "LinearizationTable", + 50713: "BlackLevelRepeatDim", + 50714: "BlackLevel", + 50715: "BlackLevelDeltaH", + 50716: "BlackLevelDeltaV", + 50717: "WhiteLevel", + 50718: "DefaultScale", + 50719: "DefaultCropOrigin", + 50720: "DefaultCropSize", + 50721: "ColorMatrix1", + 50722: "ColorMatrix2", + 50723: "CameraCalibration1", + 50724: "CameraCalibration2", + 50725: "ReductionMatrix1", + 50726: "ReductionMatrix2", + 50727: "AnalogBalance", + 50728: "AsShotNeutral", + 50729: "AsShotWhiteXY", + 50730: "BaselineExposure", + 50731: "BaselineNoise", + 50732: "BaselineSharpness", + 50733: "BayerGreenSplit", + 50734: "LinearResponseLimit", + 50735: "CameraSerialNumber", + 50736: "LensInfo", + 50737: "ChromaBlurRadius", + 50738: "AntiAliasStrength", + 50740: "DNGPrivateData", + 50778: "CalibrationIlluminant1", + 50779: "CalibrationIlluminant2", + 50784: "Alias Layer Metadata", +} + +TAGS_V2: dict[int, TagInfo] = {} +TAGS_V2_GROUPS: dict[int, dict[int, TagInfo]] = {} + + +def _populate() -> None: + for k, v in _tags_v2.items(): + # Populate legacy structure. + TAGS[k] = v[0] + if len(v) == 4: + for sk, sv in v[3].items(): + TAGS[(k, sv)] = sk + + TAGS_V2[k] = TagInfo(k, *v) + + for group, tags in _tags_v2_groups.items(): + TAGS_V2_GROUPS[group] = {k: TagInfo(k, *v) for k, v in tags.items()} + + +_populate() +## +# Map type numbers to type names -- defined in ImageFileDirectory. + +TYPES: dict[int, str] = {} + +# +# These tags are handled by default in libtiff, without +# adding to the custom dictionary. From tif_dir.c, searching for +# case TIFFTAG in the _TIFFVSetField function: +# Line: item. +# 148: case TIFFTAG_SUBFILETYPE: +# 151: case TIFFTAG_IMAGEWIDTH: +# 154: case TIFFTAG_IMAGELENGTH: +# 157: case TIFFTAG_BITSPERSAMPLE: +# 181: case TIFFTAG_COMPRESSION: +# 202: case TIFFTAG_PHOTOMETRIC: +# 205: case TIFFTAG_THRESHHOLDING: +# 208: case TIFFTAG_FILLORDER: +# 214: case TIFFTAG_ORIENTATION: +# 221: case TIFFTAG_SAMPLESPERPIXEL: +# 228: case TIFFTAG_ROWSPERSTRIP: +# 238: case TIFFTAG_MINSAMPLEVALUE: +# 241: case TIFFTAG_MAXSAMPLEVALUE: +# 244: case TIFFTAG_SMINSAMPLEVALUE: +# 247: case TIFFTAG_SMAXSAMPLEVALUE: +# 250: case TIFFTAG_XRESOLUTION: +# 256: case TIFFTAG_YRESOLUTION: +# 262: case TIFFTAG_PLANARCONFIG: +# 268: case TIFFTAG_XPOSITION: +# 271: case TIFFTAG_YPOSITION: +# 274: case TIFFTAG_RESOLUTIONUNIT: +# 280: case TIFFTAG_PAGENUMBER: +# 284: case TIFFTAG_HALFTONEHINTS: +# 288: case TIFFTAG_COLORMAP: +# 294: case TIFFTAG_EXTRASAMPLES: +# 298: case TIFFTAG_MATTEING: +# 305: case TIFFTAG_TILEWIDTH: +# 316: case TIFFTAG_TILELENGTH: +# 327: case TIFFTAG_TILEDEPTH: +# 333: case TIFFTAG_DATATYPE: +# 344: case TIFFTAG_SAMPLEFORMAT: +# 361: case TIFFTAG_IMAGEDEPTH: +# 364: case TIFFTAG_SUBIFD: +# 376: case TIFFTAG_YCBCRPOSITIONING: +# 379: case TIFFTAG_YCBCRSUBSAMPLING: +# 383: case TIFFTAG_TRANSFERFUNCTION: +# 389: case TIFFTAG_REFERENCEBLACKWHITE: +# 393: case TIFFTAG_INKNAMES: + +# Following pseudo-tags are also handled by default in libtiff: +# TIFFTAG_JPEGQUALITY 65537 + +# some of these are not in our TAGS_V2 dict and were included from tiff.h + +# This list also exists in encode.c +LIBTIFF_CORE = { + 255, + 256, + 257, + 258, + 259, + 262, + 263, + 266, + 274, + 277, + 278, + 280, + 281, + 340, + 341, + 282, + 283, + 284, + 286, + 287, + 296, + 297, + 321, + 320, + 338, + 32995, + 322, + 323, + 32998, + 32996, + 339, + 32997, + 330, + 531, + 530, + 301, + 532, + 333, + # as above + 269, # this has been in our tests forever, and works + 65537, +} + +LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes +LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff +LIBTIFF_CORE.remove(323) # Tiled images +LIBTIFF_CORE.remove(333) # Ink Names either + +# Note to advanced users: There may be combinations of these +# parameters and values that when added properly, will work and +# produce valid tiff images that may work in your application. +# It is safe to add and remove tags from this set from Pillow's point +# of view so long as you test against libtiff. diff --git a/python_modules/PIL/WalImageFile.py b/python_modules/PIL/WalImageFile.py new file mode 100644 index 000000000..87e32878b --- /dev/null +++ b/python_modules/PIL/WalImageFile.py @@ -0,0 +1,127 @@ +# +# The Python Imaging Library. +# $Id$ +# +# WAL file handling +# +# History: +# 2003-04-23 fl created +# +# Copyright (c) 2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +""" +This reader is based on the specification available from: +https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml +and has been tested with a few sample files found using google. + +.. note:: + This format cannot be automatically recognized, so the reader + is not registered for use with :py:func:`PIL.Image.open()`. + To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead. +""" +from __future__ import annotations + +from typing import IO + +from . import Image, ImageFile +from ._binary import i32le as i32 +from ._typing import StrOrBytesPath + + +class WalImageFile(ImageFile.ImageFile): + format = "WAL" + format_description = "Quake2 Texture" + + def _open(self) -> None: + self._mode = "P" + + # read header fields + header = self.fp.read(32 + 24 + 32 + 12) + self._size = i32(header, 32), i32(header, 36) + Image._decompression_bomb_check(self.size) + + # load pixel data + offset = i32(header, 40) + self.fp.seek(offset) + + # strings are null-terminated + self.info["name"] = header[:32].split(b"\0", 1)[0] + next_name = header[56 : 56 + 32].split(b"\0", 1)[0] + if next_name: + self.info["next_name"] = next_name + + def load(self) -> Image.core.PixelAccess | None: + if self._im is None: + self.im = Image.core.new(self.mode, self.size) + self.frombytes(self.fp.read(self.size[0] * self.size[1])) + self.putpalette(quake2palette) + return Image.Image.load(self) + + +def open(filename: StrOrBytesPath | IO[bytes]) -> WalImageFile: + """ + Load texture from a Quake2 WAL texture file. + + By default, a Quake2 standard palette is attached to the texture. + To override the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. + + :param filename: WAL file name, or an opened file handle. + :returns: An image instance. + """ + return WalImageFile(filename) + + +quake2palette = ( + # default palette taken from piffo 0.93 by Hans Häggström + b"\x01\x01\x01\x0b\x0b\x0b\x12\x12\x12\x17\x17\x17\x1b\x1b\x1b\x1e" + b"\x1e\x1e\x22\x22\x22\x26\x26\x26\x29\x29\x29\x2c\x2c\x2c\x2f\x2f" + b"\x2f\x32\x32\x32\x35\x35\x35\x37\x37\x37\x3a\x3a\x3a\x3c\x3c\x3c" + b"\x24\x1e\x13\x22\x1c\x12\x20\x1b\x12\x1f\x1a\x10\x1d\x19\x10\x1b" + b"\x17\x0f\x1a\x16\x0f\x18\x14\x0d\x17\x13\x0d\x16\x12\x0d\x14\x10" + b"\x0b\x13\x0f\x0b\x10\x0d\x0a\x0f\x0b\x0a\x0d\x0b\x07\x0b\x0a\x07" + b"\x23\x23\x26\x22\x22\x25\x22\x20\x23\x21\x1f\x22\x20\x1e\x20\x1f" + b"\x1d\x1e\x1d\x1b\x1c\x1b\x1a\x1a\x1a\x19\x19\x18\x17\x17\x17\x16" + b"\x16\x14\x14\x14\x13\x13\x13\x10\x10\x10\x0f\x0f\x0f\x0d\x0d\x0d" + b"\x2d\x28\x20\x29\x24\x1c\x27\x22\x1a\x25\x1f\x17\x38\x2e\x1e\x31" + b"\x29\x1a\x2c\x25\x17\x26\x20\x14\x3c\x30\x14\x37\x2c\x13\x33\x28" + b"\x12\x2d\x24\x10\x28\x1f\x0f\x22\x1a\x0b\x1b\x14\x0a\x13\x0f\x07" + b"\x31\x1a\x16\x30\x17\x13\x2e\x16\x10\x2c\x14\x0d\x2a\x12\x0b\x27" + b"\x0f\x0a\x25\x0f\x07\x21\x0d\x01\x1e\x0b\x01\x1c\x0b\x01\x1a\x0b" + b"\x01\x18\x0a\x01\x16\x0a\x01\x13\x0a\x01\x10\x07\x01\x0d\x07\x01" + b"\x29\x23\x1e\x27\x21\x1c\x26\x20\x1b\x25\x1f\x1a\x23\x1d\x19\x21" + b"\x1c\x18\x20\x1b\x17\x1e\x19\x16\x1c\x18\x14\x1b\x17\x13\x19\x14" + b"\x10\x17\x13\x0f\x14\x10\x0d\x12\x0f\x0b\x0f\x0b\x0a\x0b\x0a\x07" + b"\x26\x1a\x0f\x23\x19\x0f\x20\x17\x0f\x1c\x16\x0f\x19\x13\x0d\x14" + b"\x10\x0b\x10\x0d\x0a\x0b\x0a\x07\x33\x22\x1f\x35\x29\x26\x37\x2f" + b"\x2d\x39\x35\x34\x37\x39\x3a\x33\x37\x39\x30\x34\x36\x2b\x31\x34" + b"\x27\x2e\x31\x22\x2b\x2f\x1d\x28\x2c\x17\x25\x2a\x0f\x20\x26\x0d" + b"\x1e\x25\x0b\x1c\x22\x0a\x1b\x20\x07\x19\x1e\x07\x17\x1b\x07\x14" + b"\x18\x01\x12\x16\x01\x0f\x12\x01\x0b\x0d\x01\x07\x0a\x01\x01\x01" + b"\x2c\x21\x21\x2a\x1f\x1f\x29\x1d\x1d\x27\x1c\x1c\x26\x1a\x1a\x24" + b"\x18\x18\x22\x17\x17\x21\x16\x16\x1e\x13\x13\x1b\x12\x12\x18\x10" + b"\x10\x16\x0d\x0d\x12\x0b\x0b\x0d\x0a\x0a\x0a\x07\x07\x01\x01\x01" + b"\x2e\x30\x29\x2d\x2e\x27\x2b\x2c\x26\x2a\x2a\x24\x28\x29\x23\x27" + b"\x27\x21\x26\x26\x1f\x24\x24\x1d\x22\x22\x1c\x1f\x1f\x1a\x1c\x1c" + b"\x18\x19\x19\x16\x17\x17\x13\x13\x13\x10\x0f\x0f\x0d\x0b\x0b\x0a" + b"\x30\x1e\x1b\x2d\x1c\x19\x2c\x1a\x17\x2a\x19\x14\x28\x17\x13\x26" + b"\x16\x10\x24\x13\x0f\x21\x12\x0d\x1f\x10\x0b\x1c\x0f\x0a\x19\x0d" + b"\x0a\x16\x0b\x07\x12\x0a\x07\x0f\x07\x01\x0a\x01\x01\x01\x01\x01" + b"\x28\x29\x38\x26\x27\x36\x25\x26\x34\x24\x24\x31\x22\x22\x2f\x20" + b"\x21\x2d\x1e\x1f\x2a\x1d\x1d\x27\x1b\x1b\x25\x19\x19\x21\x17\x17" + b"\x1e\x14\x14\x1b\x13\x12\x17\x10\x0f\x13\x0d\x0b\x0f\x0a\x07\x07" + b"\x2f\x32\x29\x2d\x30\x26\x2b\x2e\x24\x29\x2c\x21\x27\x2a\x1e\x25" + b"\x28\x1c\x23\x26\x1a\x21\x25\x18\x1e\x22\x14\x1b\x1f\x10\x19\x1c" + b"\x0d\x17\x1a\x0a\x13\x17\x07\x10\x13\x01\x0d\x0f\x01\x0a\x0b\x01" + b"\x01\x3f\x01\x13\x3c\x0b\x1b\x39\x10\x20\x35\x14\x23\x31\x17\x23" + b"\x2d\x18\x23\x29\x18\x3f\x3f\x3f\x3f\x3f\x39\x3f\x3f\x31\x3f\x3f" + b"\x2a\x3f\x3f\x20\x3f\x3f\x14\x3f\x3c\x12\x3f\x39\x0f\x3f\x35\x0b" + b"\x3f\x32\x07\x3f\x2d\x01\x3d\x2a\x01\x3b\x26\x01\x39\x21\x01\x37" + b"\x1d\x01\x34\x1a\x01\x32\x16\x01\x2f\x12\x01\x2d\x0f\x01\x2a\x0b" + b"\x01\x27\x07\x01\x23\x01\x01\x1d\x01\x01\x17\x01\x01\x10\x01\x01" + b"\x3d\x01\x01\x19\x19\x3f\x3f\x01\x01\x01\x01\x3f\x16\x16\x13\x10" + b"\x10\x0f\x0d\x0d\x0b\x3c\x2e\x2a\x36\x27\x20\x30\x21\x18\x29\x1b" + b"\x10\x3c\x39\x37\x37\x32\x2f\x31\x2c\x28\x2b\x26\x21\x30\x22\x20" +) diff --git a/python_modules/PIL/WebPImagePlugin.py b/python_modules/PIL/WebPImagePlugin.py new file mode 100644 index 000000000..1716a18cc --- /dev/null +++ b/python_modules/PIL/WebPImagePlugin.py @@ -0,0 +1,320 @@ +from __future__ import annotations + +from io import BytesIO +from typing import IO, Any + +from . import Image, ImageFile + +try: + from . import _webp + + SUPPORTED = True +except ImportError: + SUPPORTED = False + + +_VP8_MODES_BY_IDENTIFIER = { + b"VP8 ": "RGB", + b"VP8X": "RGBA", + b"VP8L": "RGBA", # lossless +} + + +def _accept(prefix: bytes) -> bool | str: + is_riff_file_format = prefix.startswith(b"RIFF") + is_webp_file = prefix[8:12] == b"WEBP" + is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER + + if is_riff_file_format and is_webp_file and is_valid_vp8_mode: + if not SUPPORTED: + return ( + "image file could not be identified because WEBP support not installed" + ) + return True + return False + + +class WebPImageFile(ImageFile.ImageFile): + format = "WEBP" + format_description = "WebP image" + __loaded = 0 + __logical_frame = 0 + + def _open(self) -> None: + # Use the newer AnimDecoder API to parse the (possibly) animated file, + # and access muxed chunks like ICC/EXIF/XMP. + self._decoder = _webp.WebPAnimDecoder(self.fp.read()) + + # Get info from decoder + self._size, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() + self.info["loop"] = loop_count + bg_a, bg_r, bg_g, bg_b = ( + (bgcolor >> 24) & 0xFF, + (bgcolor >> 16) & 0xFF, + (bgcolor >> 8) & 0xFF, + bgcolor & 0xFF, + ) + self.info["background"] = (bg_r, bg_g, bg_b, bg_a) + self.n_frames = frame_count + self.is_animated = self.n_frames > 1 + self._mode = "RGB" if mode == "RGBX" else mode + self.rawmode = mode + + # Attempt to read ICC / EXIF / XMP chunks from file + icc_profile = self._decoder.get_chunk("ICCP") + exif = self._decoder.get_chunk("EXIF") + xmp = self._decoder.get_chunk("XMP ") + if icc_profile: + self.info["icc_profile"] = icc_profile + if exif: + self.info["exif"] = exif + if xmp: + self.info["xmp"] = xmp + + # Initialize seek state + self._reset(reset=False) + + def _getexif(self) -> dict[int, Any] | None: + if "exif" not in self.info: + return None + return self.getexif()._get_merged_dict() + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + + # Set logical frame to requested position + self.__logical_frame = frame + + def _reset(self, reset: bool = True) -> None: + if reset: + self._decoder.reset() + self.__physical_frame = 0 + self.__loaded = -1 + self.__timestamp = 0 + + def _get_next(self) -> tuple[bytes, int, int]: + # Get next frame + ret = self._decoder.get_next() + self.__physical_frame += 1 + + # Check if an error occurred + if ret is None: + self._reset() # Reset just to be safe + self.seek(0) + msg = "failed to decode next frame in WebP file" + raise EOFError(msg) + + # Compute duration + data, timestamp = ret + duration = timestamp - self.__timestamp + self.__timestamp = timestamp + + # libwebp gives frame end, adjust to start of frame + timestamp -= duration + return data, timestamp, duration + + def _seek(self, frame: int) -> None: + if self.__physical_frame == frame: + return # Nothing to do + if frame < self.__physical_frame: + self._reset() # Rewind to beginning + while self.__physical_frame < frame: + self._get_next() # Advance to the requested frame + + def load(self) -> Image.core.PixelAccess | None: + if self.__loaded != self.__logical_frame: + self._seek(self.__logical_frame) + + # We need to load the image data for this frame + data, timestamp, duration = self._get_next() + self.info["timestamp"] = timestamp + self.info["duration"] = duration + self.__loaded = self.__logical_frame + + # Set tile + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)] + + return super().load() + + def load_seek(self, pos: int) -> None: + pass + + def tell(self) -> int: + return self.__logical_frame + + +def _convert_frame(im: Image.Image) -> Image.Image: + # Make sure image mode is supported + if im.mode not in ("RGBX", "RGBA", "RGB"): + im = im.convert("RGBA" if im.has_transparency_data else "RGB") + return im + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + encoderinfo = im.encoderinfo.copy() + append_images = list(encoderinfo.get("append_images", [])) + + # If total frame count is 1, then save using the legacy API, which + # will preserve non-alpha modes + total = 0 + for ims in [im] + append_images: + total += getattr(ims, "n_frames", 1) + if total == 1: + _save(im, fp, filename) + return + + background: int | tuple[int, ...] = (0, 0, 0, 0) + if "background" in encoderinfo: + background = encoderinfo["background"] + elif "background" in im.info: + background = im.info["background"] + if isinstance(background, int): + # GifImagePlugin stores a global color table index in + # info["background"]. So it must be converted to an RGBA value + palette = im.getpalette() + if palette: + r, g, b = palette[background * 3 : (background + 1) * 3] + background = (r, g, b, 255) + else: + background = (background, background, background, 255) + + duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) + loop = im.encoderinfo.get("loop", 0) + minimize_size = im.encoderinfo.get("minimize_size", False) + kmin = im.encoderinfo.get("kmin", None) + kmax = im.encoderinfo.get("kmax", None) + allow_mixed = im.encoderinfo.get("allow_mixed", False) + verbose = False + lossless = im.encoderinfo.get("lossless", False) + quality = im.encoderinfo.get("quality", 80) + alpha_quality = im.encoderinfo.get("alpha_quality", 100) + method = im.encoderinfo.get("method", 0) + icc_profile = im.encoderinfo.get("icc_profile") or "" + exif = im.encoderinfo.get("exif", "") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + xmp = im.encoderinfo.get("xmp", "") + if allow_mixed: + lossless = False + + # Sensible keyframe defaults are from gif2webp.c script + if kmin is None: + kmin = 9 if lossless else 3 + if kmax is None: + kmax = 17 if lossless else 5 + + # Validate background color + if ( + not isinstance(background, (list, tuple)) + or len(background) != 4 + or not all(0 <= v < 256 for v in background) + ): + msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}" + raise OSError(msg) + + # Convert to packed uint + bg_r, bg_g, bg_b, bg_a = background + background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0) + + # Setup the WebP animation encoder + enc = _webp.WebPAnimEncoder( + im.size, + background, + loop, + minimize_size, + kmin, + kmax, + allow_mixed, + verbose, + ) + + # Add each frame + frame_idx = 0 + timestamp = 0 + cur_idx = im.tell() + try: + for ims in [im] + append_images: + # Get number of frames in this image + nfr = getattr(ims, "n_frames", 1) + + for idx in range(nfr): + ims.seek(idx) + + frame = _convert_frame(ims) + + # Append the frame to the animation encoder + enc.add( + frame.getim(), + round(timestamp), + lossless, + quality, + alpha_quality, + method, + ) + + # Update timestamp and frame index + if isinstance(duration, (list, tuple)): + timestamp += duration[frame_idx] + else: + timestamp += duration + frame_idx += 1 + + finally: + im.seek(cur_idx) + + # Force encoder to flush frames + enc.add(None, round(timestamp), lossless, quality, alpha_quality, 0) + + # Get the final output from the encoder + data = enc.assemble(icc_profile, exif, xmp) + if data is None: + msg = "cannot write file as WebP (encoder returned None)" + raise OSError(msg) + + fp.write(data) + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + lossless = im.encoderinfo.get("lossless", False) + quality = im.encoderinfo.get("quality", 80) + alpha_quality = im.encoderinfo.get("alpha_quality", 100) + icc_profile = im.encoderinfo.get("icc_profile") or "" + exif = im.encoderinfo.get("exif", b"") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + if exif.startswith(b"Exif\x00\x00"): + exif = exif[6:] + xmp = im.encoderinfo.get("xmp", "") + method = im.encoderinfo.get("method", 4) + exact = 1 if im.encoderinfo.get("exact") else 0 + + im = _convert_frame(im) + + data = _webp.WebPEncode( + im.getim(), + lossless, + float(quality), + float(alpha_quality), + icc_profile, + method, + exact, + exif, + xmp, + ) + if data is None: + msg = "cannot write file as WebP (encoder returned None)" + raise OSError(msg) + + fp.write(data) + + +Image.register_open(WebPImageFile.format, WebPImageFile, _accept) +if SUPPORTED: + Image.register_save(WebPImageFile.format, _save) + Image.register_save_all(WebPImageFile.format, _save_all) + Image.register_extension(WebPImageFile.format, ".webp") + Image.register_mime(WebPImageFile.format, "image/webp") diff --git a/python_modules/PIL/WmfImagePlugin.py b/python_modules/PIL/WmfImagePlugin.py new file mode 100644 index 000000000..d569cb4b8 --- /dev/null +++ b/python_modules/PIL/WmfImagePlugin.py @@ -0,0 +1,186 @@ +# +# The Python Imaging Library +# $Id$ +# +# WMF stub codec +# +# history: +# 1996-12-14 fl Created +# 2004-02-22 fl Turned into a stub driver +# 2004-02-23 fl Added EMF support +# +# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# +# WMF/EMF reference documentation: +# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-WMF/[MS-WMF].pdf +# http://wvware.sourceforge.net/caolan/index.html +# http://wvware.sourceforge.net/caolan/ora-wmf.html +from __future__ import annotations + +from typing import IO + +from . import Image, ImageFile +from ._binary import i16le as word +from ._binary import si16le as short +from ._binary import si32le as _long + +_handler = None + + +def register_handler(handler: ImageFile.StubHandler | None) -> None: + """ + Install application-specific WMF image handler. + + :param handler: Handler object. + """ + global _handler + _handler = handler + + +if hasattr(Image.core, "drawwmf"): + # install default handler (windows only) + + class WmfHandler(ImageFile.StubHandler): + def open(self, im: ImageFile.StubImageFile) -> None: + im._mode = "RGB" + self.bbox = im.info["wmf_bbox"] + + def load(self, im: ImageFile.StubImageFile) -> Image.Image: + im.fp.seek(0) # rewind + return Image.frombytes( + "RGB", + im.size, + Image.core.drawwmf(im.fp.read(), im.size, self.bbox), + "raw", + "BGR", + (im.size[0] * 3 + 3) & -4, + -1, + ) + + register_handler(WmfHandler()) + +# +# -------------------------------------------------------------------- +# Read WMF file + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith((b"\xd7\xcd\xc6\x9a\x00\x00", b"\x01\x00\x00\x00")) + + +## +# Image plugin for Windows metafiles. + + +class WmfStubImageFile(ImageFile.StubImageFile): + format = "WMF" + format_description = "Windows Metafile" + + def _open(self) -> None: + # check placable header + s = self.fp.read(44) + + if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): + # placeable windows metafile + + # get units per inch + inch = word(s, 14) + if inch == 0: + msg = "Invalid inch" + raise ValueError(msg) + self._inch: tuple[float, float] = inch, inch + + # get bounding box + x0 = short(s, 6) + y0 = short(s, 8) + x1 = short(s, 10) + y1 = short(s, 12) + + # normalize size to 72 dots per inch + self.info["dpi"] = 72 + size = ( + (x1 - x0) * self.info["dpi"] // inch, + (y1 - y0) * self.info["dpi"] // inch, + ) + + self.info["wmf_bbox"] = x0, y0, x1, y1 + + # sanity check (standard metafile header) + if s[22:26] != b"\x01\x00\t\x00": + msg = "Unsupported WMF file format" + raise SyntaxError(msg) + + elif s.startswith(b"\x01\x00\x00\x00") and s[40:44] == b" EMF": + # enhanced metafile + + # get bounding box + x0 = _long(s, 8) + y0 = _long(s, 12) + x1 = _long(s, 16) + y1 = _long(s, 20) + + # get frame (in 0.01 millimeter units) + frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36) + + size = x1 - x0, y1 - y0 + + # calculate dots per inch from bbox and frame + xdpi = 2540.0 * (x1 - x0) / (frame[2] - frame[0]) + ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) + + self.info["wmf_bbox"] = x0, y0, x1, y1 + + if xdpi == ydpi: + self.info["dpi"] = xdpi + else: + self.info["dpi"] = xdpi, ydpi + self._inch = xdpi, ydpi + + else: + msg = "Unsupported file format" + raise SyntaxError(msg) + + self._mode = "RGB" + self._size = size + + loader = self._load() + if loader: + loader.open(self) + + def _load(self) -> ImageFile.StubHandler | None: + return _handler + + def load( + self, dpi: float | tuple[float, float] | None = None + ) -> Image.core.PixelAccess | None: + if dpi is not None: + self.info["dpi"] = dpi + x0, y0, x1, y1 = self.info["wmf_bbox"] + if not isinstance(dpi, tuple): + dpi = dpi, dpi + self._size = ( + int((x1 - x0) * dpi[0] / self._inch[0]), + int((y1 - y0) * dpi[1] / self._inch[1]), + ) + return super().load() + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if _handler is None or not hasattr(_handler, "save"): + msg = "WMF save handler not installed" + raise OSError(msg) + _handler.save(im, fp, filename) + + +# +# -------------------------------------------------------------------- +# Registry stuff + + +Image.register_open(WmfStubImageFile.format, WmfStubImageFile, _accept) +Image.register_save(WmfStubImageFile.format, _save) + +Image.register_extensions(WmfStubImageFile.format, [".wmf", ".emf"]) diff --git a/python_modules/PIL/XVThumbImagePlugin.py b/python_modules/PIL/XVThumbImagePlugin.py new file mode 100644 index 000000000..cde28388f --- /dev/null +++ b/python_modules/PIL/XVThumbImagePlugin.py @@ -0,0 +1,83 @@ +# +# The Python Imaging Library. +# $Id$ +# +# XV Thumbnail file handler by Charles E. "Gene" Cash +# (gcash@magicnet.net) +# +# see xvcolor.c and xvbrowse.c in the sources to John Bradley's XV, +# available from ftp://ftp.cis.upenn.edu/pub/xv/ +# +# history: +# 98-08-15 cec created (b/w only) +# 98-12-09 cec added color palette +# 98-12-28 fl added to PIL (with only a few very minor modifications) +# +# To do: +# FIXME: make save work (this requires quantization support) +# +from __future__ import annotations + +from . import Image, ImageFile, ImagePalette +from ._binary import o8 + +_MAGIC = b"P7 332" + +# standard color palette for thumbnails (RGB332) +PALETTE = b"" +for r in range(8): + for g in range(8): + for b in range(4): + PALETTE = PALETTE + ( + o8((r * 255) // 7) + o8((g * 255) // 7) + o8((b * 255) // 3) + ) + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(_MAGIC) + + +## +# Image plugin for XV thumbnail images. + + +class XVThumbImageFile(ImageFile.ImageFile): + format = "XVThumb" + format_description = "XV thumbnail image" + + def _open(self) -> None: + # check magic + assert self.fp is not None + + if not _accept(self.fp.read(6)): + msg = "not an XV thumbnail file" + raise SyntaxError(msg) + + # Skip to beginning of next line + self.fp.readline() + + # skip info comments + while True: + s = self.fp.readline() + if not s: + msg = "Unexpected EOF reading XV thumbnail file" + raise SyntaxError(msg) + if s[0] != 35: # ie. when not a comment: '#' + break + + # parse header line (already read) + s = s.strip().split() + + self._mode = "P" + self._size = int(s[0]), int(s[1]) + + self.palette = ImagePalette.raw("RGB", PALETTE) + + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode) + ] + + +# -------------------------------------------------------------------- + +Image.register_open(XVThumbImageFile.format, XVThumbImageFile, _accept) diff --git a/python_modules/PIL/XbmImagePlugin.py b/python_modules/PIL/XbmImagePlugin.py new file mode 100644 index 000000000..1e57aa162 --- /dev/null +++ b/python_modules/PIL/XbmImagePlugin.py @@ -0,0 +1,98 @@ +# +# The Python Imaging Library. +# $Id$ +# +# XBM File handling +# +# History: +# 1995-09-08 fl Created +# 1996-11-01 fl Added save support +# 1997-07-07 fl Made header parser more tolerant +# 1997-07-22 fl Fixed yet another parser bug +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) +# 2001-05-13 fl Added hotspot handling (based on code from Bernhard Herzog) +# 2004-02-24 fl Allow some whitespace before first #define +# +# Copyright (c) 1997-2004 by Secret Labs AB +# Copyright (c) 1996-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import re +from typing import IO + +from . import Image, ImageFile + +# XBM header +xbm_head = re.compile( + rb"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" + b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+" + b"(?P" + b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" + b"#define[ \t]+[^_]*_y_hot[ \t]+(?P[0-9]+)[\r\n]+" + b")?" + rb"[\000-\377]*_bits\[]" +) + + +def _accept(prefix: bytes) -> bool: + return prefix.lstrip().startswith(b"#define") + + +## +# Image plugin for X11 bitmaps. + + +class XbmImageFile(ImageFile.ImageFile): + format = "XBM" + format_description = "X11 Bitmap" + + def _open(self) -> None: + assert self.fp is not None + + m = xbm_head.match(self.fp.read(512)) + + if not m: + msg = "not a XBM file" + raise SyntaxError(msg) + + xsize = int(m.group("width")) + ysize = int(m.group("height")) + + if m.group("hotspot"): + self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) + + self._mode = "1" + self._size = xsize, ysize + + self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end())] + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode != "1": + msg = f"cannot write mode {im.mode} as XBM" + raise OSError(msg) + + fp.write(f"#define im_width {im.size[0]}\n".encode("ascii")) + fp.write(f"#define im_height {im.size[1]}\n".encode("ascii")) + + hotspot = im.encoderinfo.get("hotspot") + if hotspot: + fp.write(f"#define im_x_hot {hotspot[0]}\n".encode("ascii")) + fp.write(f"#define im_y_hot {hotspot[1]}\n".encode("ascii")) + + fp.write(b"static char im_bits[] = {\n") + + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size)]) + + fp.write(b"};\n") + + +Image.register_open(XbmImageFile.format, XbmImageFile, _accept) +Image.register_save(XbmImageFile.format, _save) + +Image.register_extension(XbmImageFile.format, ".xbm") + +Image.register_mime(XbmImageFile.format, "image/xbm") diff --git a/python_modules/PIL/XpmImagePlugin.py b/python_modules/PIL/XpmImagePlugin.py new file mode 100644 index 000000000..3be240fbc --- /dev/null +++ b/python_modules/PIL/XpmImagePlugin.py @@ -0,0 +1,157 @@ +# +# The Python Imaging Library. +# $Id$ +# +# XPM File handling +# +# History: +# 1996-12-29 fl Created +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) +# +# Copyright (c) Secret Labs AB 1997-2001. +# Copyright (c) Fredrik Lundh 1996-2001. +# +# See the README file for information on usage and redistribution. +# +from __future__ import annotations + +import re + +from . import Image, ImageFile, ImagePalette +from ._binary import o8 + +# XPM header +xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') + + +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"/* XPM */") + + +## +# Image plugin for X11 pixel maps. + + +class XpmImageFile(ImageFile.ImageFile): + format = "XPM" + format_description = "X11 Pixel Map" + + def _open(self) -> None: + assert self.fp is not None + if not _accept(self.fp.read(9)): + msg = "not an XPM file" + raise SyntaxError(msg) + + # skip forward to next string + while True: + line = self.fp.readline() + if not line: + msg = "broken XPM file" + raise SyntaxError(msg) + m = xpm_head.match(line) + if m: + break + + self._size = int(m.group(1)), int(m.group(2)) + + palette_length = int(m.group(3)) + bpp = int(m.group(4)) + + # + # load palette description + + palette = {} + + for _ in range(palette_length): + line = self.fp.readline().rstrip() + + c = line[1 : bpp + 1] + s = line[bpp + 1 : -2].split() + + for i in range(0, len(s), 2): + if s[i] == b"c": + # process colour key + rgb = s[i + 1] + if rgb == b"None": + self.info["transparency"] = c + elif rgb.startswith(b"#"): + rgb_int = int(rgb[1:], 16) + palette[c] = ( + o8((rgb_int >> 16) & 255) + + o8((rgb_int >> 8) & 255) + + o8(rgb_int & 255) + ) + else: + # unknown colour + msg = "cannot read this XPM file" + raise ValueError(msg) + break + + else: + # missing colour key + msg = "cannot read this XPM file" + raise ValueError(msg) + + args: tuple[int, dict[bytes, bytes] | tuple[bytes, ...]] + if palette_length > 256: + self._mode = "RGB" + args = (bpp, palette) + else: + self._mode = "P" + self.palette = ImagePalette.raw("RGB", b"".join(palette.values())) + args = (bpp, tuple(palette.keys())) + + self.tile = [ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), args)] + + def load_read(self, read_bytes: int) -> bytes: + # + # load all image data in one chunk + + xsize, ysize = self.size + + assert self.fp is not None + s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] + + return b"".join(s) + + +class XpmDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + + data = bytearray() + bpp, palette = self.args + dest_length = self.state.xsize * self.state.ysize + if self.mode == "RGB": + dest_length *= 3 + pixel_header = False + while len(data) < dest_length: + line = self.fd.readline() + if not line: + break + if line.rstrip() == b"/* pixels */" and not pixel_header: + pixel_header = True + continue + line = b'"'.join(line.split(b'"')[1:-1]) + for i in range(0, len(line), bpp): + key = line[i : i + bpp] + if self.mode == "RGB": + data += palette[key] + else: + data += o8(palette.index(key)) + self.set_as_raw(bytes(data)) + return -1, 0 + + +# +# Registry + + +Image.register_open(XpmImageFile.format, XpmImageFile, _accept) +Image.register_decoder("xpm", XpmDecoder) + +Image.register_extension(XpmImageFile.format, ".xpm") + +Image.register_mime(XpmImageFile.format, "image/xpm") diff --git a/python_modules/PIL/__init__.py b/python_modules/PIL/__init__.py new file mode 100644 index 000000000..6e4c23f89 --- /dev/null +++ b/python_modules/PIL/__init__.py @@ -0,0 +1,87 @@ +"""Pillow (Fork of the Python Imaging Library) + +Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors. + https://github.com/python-pillow/Pillow/ + +Pillow is forked from PIL 1.1.7. + +PIL is the Python Imaging Library by Fredrik Lundh and contributors. +Copyright (c) 1999 by Secret Labs AB. + +Use PIL.__version__ for this Pillow version. + +;-) +""" + +from __future__ import annotations + +from . import _version + +# VERSION was removed in Pillow 6.0.0. +# PILLOW_VERSION was removed in Pillow 9.0.0. +# Use __version__ instead. +__version__ = _version.__version__ +del _version + + +_plugins = [ + "AvifImagePlugin", + "BlpImagePlugin", + "BmpImagePlugin", + "BufrStubImagePlugin", + "CurImagePlugin", + "DcxImagePlugin", + "DdsImagePlugin", + "EpsImagePlugin", + "FitsImagePlugin", + "FliImagePlugin", + "FpxImagePlugin", + "FtexImagePlugin", + "GbrImagePlugin", + "GifImagePlugin", + "GribStubImagePlugin", + "Hdf5StubImagePlugin", + "IcnsImagePlugin", + "IcoImagePlugin", + "ImImagePlugin", + "ImtImagePlugin", + "IptcImagePlugin", + "JpegImagePlugin", + "Jpeg2KImagePlugin", + "McIdasImagePlugin", + "MicImagePlugin", + "MpegImagePlugin", + "MpoImagePlugin", + "MspImagePlugin", + "PalmImagePlugin", + "PcdImagePlugin", + "PcxImagePlugin", + "PdfImagePlugin", + "PixarImagePlugin", + "PngImagePlugin", + "PpmImagePlugin", + "PsdImagePlugin", + "QoiImagePlugin", + "SgiImagePlugin", + "SpiderImagePlugin", + "SunImagePlugin", + "TgaImagePlugin", + "TiffImagePlugin", + "WebPImagePlugin", + "WmfImagePlugin", + "XbmImagePlugin", + "XpmImagePlugin", + "XVThumbImagePlugin", +] + + +class UnidentifiedImageError(OSError): + """ + Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. + + If a PNG image raises this error, setting :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` + to true may allow the image to be opened after all. The setting will ignore missing + data and checksum failures. + """ + + pass diff --git a/python_modules/PIL/__main__.py b/python_modules/PIL/__main__.py new file mode 100644 index 000000000..043156e89 --- /dev/null +++ b/python_modules/PIL/__main__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +import sys + +from .features import pilinfo + +pilinfo(supported_formats="--report" not in sys.argv) diff --git a/python_modules/PIL/_avif.pyi b/python_modules/PIL/_avif.pyi new file mode 100644 index 000000000..e27843e53 --- /dev/null +++ b/python_modules/PIL/_avif.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_binary.py b/python_modules/PIL/_binary.py new file mode 100644 index 000000000..4594ccce3 --- /dev/null +++ b/python_modules/PIL/_binary.py @@ -0,0 +1,112 @@ +# +# The Python Imaging Library. +# $Id$ +# +# Binary input/output support routines. +# +# Copyright (c) 1997-2003 by Secret Labs AB +# Copyright (c) 1995-2003 by Fredrik Lundh +# Copyright (c) 2012 by Brian Crowell +# +# See the README file for information on usage and redistribution. +# + + +"""Binary input/output support routines.""" +from __future__ import annotations + +from struct import pack, unpack_from + + +def i8(c: bytes) -> int: + return c[0] + + +def o8(i: int) -> bytes: + return bytes((i & 255,)) + + +# Input, le = little endian, be = big endian +def i16le(c: bytes, o: int = 0) -> int: + """ + Converts a 2-bytes (16 bits) string to an unsigned integer. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(" int: + """ + Converts a 2-bytes (16 bits) string to a signed integer. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(" int: + """ + Converts a 2-bytes (16 bits) string to a signed integer, big endian. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(">h", c, o)[0] + + +def i32le(c: bytes, o: int = 0) -> int: + """ + Converts a 4-bytes (32 bits) string to an unsigned integer. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(" int: + """ + Converts a 4-bytes (32 bits) string to a signed integer. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(" int: + """ + Converts a 4-bytes (32 bits) string to a signed integer, big endian. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(">i", c, o)[0] + + +def i16be(c: bytes, o: int = 0) -> int: + return unpack_from(">H", c, o)[0] + + +def i32be(c: bytes, o: int = 0) -> int: + return unpack_from(">I", c, o)[0] + + +# Output, le = little endian, be = big endian +def o16le(i: int) -> bytes: + return pack(" bytes: + return pack(" bytes: + return pack(">H", i) + + +def o32be(i: int) -> bytes: + return pack(">I", i) diff --git a/python_modules/PIL/_deprecate.py b/python_modules/PIL/_deprecate.py new file mode 100644 index 000000000..170d44490 --- /dev/null +++ b/python_modules/PIL/_deprecate.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import warnings + +from . import __version__ + + +def deprecate( + deprecated: str, + when: int | None, + replacement: str | None = None, + *, + action: str | None = None, + plural: bool = False, + stacklevel: int = 3, +) -> None: + """ + Deprecations helper. + + :param deprecated: Name of thing to be deprecated. + :param when: Pillow major version to be removed in. + :param replacement: Name of replacement. + :param action: Instead of "replacement", give a custom call to action + e.g. "Upgrade to new thing". + :param plural: if the deprecated thing is plural, needing "are" instead of "is". + + Usually of the form: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). + Use [replacement] instead." + + You can leave out the replacement sentence: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd)" + + Or with another call to action: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). + [action]." + """ + + is_ = "are" if plural else "is" + + if when is None: + removed = "a future version" + elif when <= int(__version__.split(".")[0]): + msg = f"{deprecated} {is_} deprecated and should be removed." + raise RuntimeError(msg) + elif when == 12: + removed = "Pillow 12 (2025-10-15)" + elif when == 13: + removed = "Pillow 13 (2026-10-15)" + else: + msg = f"Unknown removal version: {when}. Update {__name__}?" + raise ValueError(msg) + + if replacement and action: + msg = "Use only one of 'replacement' and 'action'" + raise ValueError(msg) + + if replacement: + action = f". Use {replacement} instead." + elif action: + action = f". {action.rstrip('.')}." + else: + action = "" + + warnings.warn( + f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", + DeprecationWarning, + stacklevel=stacklevel, + ) diff --git a/python_modules/PIL/_imaging.pyi b/python_modules/PIL/_imaging.pyi new file mode 100644 index 000000000..998bc52eb --- /dev/null +++ b/python_modules/PIL/_imaging.pyi @@ -0,0 +1,31 @@ +from typing import Any + +class ImagingCore: + def __getitem__(self, index: int) -> float: ... + def __getattr__(self, name: str) -> Any: ... + +class ImagingFont: + def __getattr__(self, name: str) -> Any: ... + +class ImagingDraw: + def __getattr__(self, name: str) -> Any: ... + +class PixelAccess: + def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: ... + def __setitem__( + self, xy: tuple[int, int], color: float | tuple[int, ...] + ) -> None: ... + +class ImagingDecoder: + def __getattr__(self, name: str) -> Any: ... + +class ImagingEncoder: + def __getattr__(self, name: str) -> Any: ... + +class _Outline: + def close(self) -> None: ... + def __getattr__(self, name: str) -> Any: ... + +def font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ... +def outline() -> _Outline: ... +def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_imagingcms.pyi b/python_modules/PIL/_imagingcms.pyi new file mode 100644 index 000000000..ddcf93ab1 --- /dev/null +++ b/python_modules/PIL/_imagingcms.pyi @@ -0,0 +1,143 @@ +import datetime +import sys +from typing import Literal, SupportsFloat, TypedDict + +from ._typing import CapsuleType + +littlecms_version: str | None + +_Tuple3f = tuple[float, float, float] +_Tuple2x3f = tuple[_Tuple3f, _Tuple3f] +_Tuple3x3f = tuple[_Tuple3f, _Tuple3f, _Tuple3f] + +class _IccMeasurementCondition(TypedDict): + observer: int + backing: _Tuple3f + geo: str + flare: float + illuminant_type: str + +class _IccViewingCondition(TypedDict): + illuminant: _Tuple3f + surround: _Tuple3f + illuminant_type: str + +class CmsProfile: + @property + def rendering_intent(self) -> int: ... + @property + def creation_date(self) -> datetime.datetime | None: ... + @property + def copyright(self) -> str | None: ... + @property + def target(self) -> str | None: ... + @property + def manufacturer(self) -> str | None: ... + @property + def model(self) -> str | None: ... + @property + def profile_description(self) -> str | None: ... + @property + def screening_description(self) -> str | None: ... + @property + def viewing_condition(self) -> str | None: ... + @property + def version(self) -> float: ... + @property + def icc_version(self) -> int: ... + @property + def attributes(self) -> int: ... + @property + def header_flags(self) -> int: ... + @property + def header_manufacturer(self) -> str: ... + @property + def header_model(self) -> str: ... + @property + def device_class(self) -> str: ... + @property + def connection_space(self) -> str: ... + @property + def xcolor_space(self) -> str: ... + @property + def profile_id(self) -> bytes: ... + @property + def is_matrix_shaper(self) -> bool: ... + @property + def technology(self) -> str | None: ... + @property + def colorimetric_intent(self) -> str | None: ... + @property + def perceptual_rendering_intent_gamut(self) -> str | None: ... + @property + def saturation_rendering_intent_gamut(self) -> str | None: ... + @property + def red_colorant(self) -> _Tuple2x3f | None: ... + @property + def green_colorant(self) -> _Tuple2x3f | None: ... + @property + def blue_colorant(self) -> _Tuple2x3f | None: ... + @property + def red_primary(self) -> _Tuple2x3f | None: ... + @property + def green_primary(self) -> _Tuple2x3f | None: ... + @property + def blue_primary(self) -> _Tuple2x3f | None: ... + @property + def media_white_point_temperature(self) -> float | None: ... + @property + def media_white_point(self) -> _Tuple2x3f | None: ... + @property + def media_black_point(self) -> _Tuple2x3f | None: ... + @property + def luminance(self) -> _Tuple2x3f | None: ... + @property + def chromatic_adaptation(self) -> tuple[_Tuple3x3f, _Tuple3x3f] | None: ... + @property + def chromaticity(self) -> _Tuple3x3f | None: ... + @property + def colorant_table(self) -> list[str] | None: ... + @property + def colorant_table_out(self) -> list[str] | None: ... + @property + def intent_supported(self) -> dict[int, tuple[bool, bool, bool]] | None: ... + @property + def clut(self) -> dict[int, tuple[bool, bool, bool]] | None: ... + @property + def icc_measurement_condition(self) -> _IccMeasurementCondition | None: ... + @property + def icc_viewing_condition(self) -> _IccViewingCondition | None: ... + def is_intent_supported(self, intent: int, direction: int, /) -> int: ... + +class CmsTransform: + def apply(self, id_in: CapsuleType, id_out: CapsuleType) -> int: ... + +def profile_open(profile: str, /) -> CmsProfile: ... +def profile_frombytes(profile: bytes, /) -> CmsProfile: ... +def profile_tobytes(profile: CmsProfile, /) -> bytes: ... +def buildTransform( + input_profile: CmsProfile, + output_profile: CmsProfile, + in_mode: str, + out_mode: str, + rendering_intent: int = 0, + cms_flags: int = 0, + /, +) -> CmsTransform: ... +def buildProofTransform( + input_profile: CmsProfile, + output_profile: CmsProfile, + proof_profile: CmsProfile, + in_mode: str, + out_mode: str, + rendering_intent: int = 0, + proof_intent: int = 0, + cms_flags: int = 0, + /, +) -> CmsTransform: ... +def createProfile( + color_space: Literal["LAB", "XYZ", "sRGB"], color_temp: SupportsFloat = 0.0, / +) -> CmsProfile: ... + +if sys.platform == "win32": + def get_display_profile_win32(handle: int = 0, is_dc: int = 0, /) -> str | None: ... diff --git a/python_modules/PIL/_imagingft.pyi b/python_modules/PIL/_imagingft.pyi new file mode 100644 index 000000000..1cb1429d6 --- /dev/null +++ b/python_modules/PIL/_imagingft.pyi @@ -0,0 +1,69 @@ +from typing import Any, Callable + +from . import ImageFont, _imaging + +class Font: + @property + def family(self) -> str | None: ... + @property + def style(self) -> str | None: ... + @property + def ascent(self) -> int: ... + @property + def descent(self) -> int: ... + @property + def height(self) -> int: ... + @property + def x_ppem(self) -> int: ... + @property + def y_ppem(self) -> int: ... + @property + def glyphs(self) -> int: ... + def render( + self, + string: str | bytes, + fill: Callable[[int, int], _imaging.ImagingCore], + mode: str, + dir: str | None, + features: list[str] | None, + lang: str | None, + stroke_width: float, + stroke_filled: bool, + anchor: str | None, + foreground_ink_long: int, + start: tuple[float, float], + /, + ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... + def getsize( + self, + string: str | bytes | bytearray, + mode: str, + dir: str | None, + features: list[str] | None, + lang: str | None, + anchor: str | None, + /, + ) -> tuple[tuple[int, int], tuple[int, int]]: ... + def getlength( + self, + string: str | bytes, + mode: str, + dir: str | None, + features: list[str] | None, + lang: str | None, + /, + ) -> float: ... + def getvarnames(self) -> list[bytes]: ... + def getvaraxes(self) -> list[ImageFont.Axis]: ... + def setvarname(self, instance_index: int, /) -> None: ... + def setvaraxes(self, axes: list[float], /) -> None: ... + +def getfont( + filename: str | bytes, + size: float, + index: int, + encoding: str, + font_bytes: bytes, + layout_engine: int, +) -> Font: ... +def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_imagingmath.pyi b/python_modules/PIL/_imagingmath.pyi new file mode 100644 index 000000000..e27843e53 --- /dev/null +++ b/python_modules/PIL/_imagingmath.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_imagingmorph.pyi b/python_modules/PIL/_imagingmorph.pyi new file mode 100644 index 000000000..e27843e53 --- /dev/null +++ b/python_modules/PIL/_imagingmorph.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_imagingtk.pyi b/python_modules/PIL/_imagingtk.pyi new file mode 100644 index 000000000..e27843e53 --- /dev/null +++ b/python_modules/PIL/_imagingtk.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_tkinter_finder.py b/python_modules/PIL/_tkinter_finder.py new file mode 100644 index 000000000..9c0143003 --- /dev/null +++ b/python_modules/PIL/_tkinter_finder.py @@ -0,0 +1,20 @@ +"""Find compiled module linking to Tcl / Tk libraries""" + +from __future__ import annotations + +import sys +import tkinter + +tk = getattr(tkinter, "_tkinter") + +try: + if hasattr(sys, "pypy_find_executable"): + TKINTER_LIB = tk.tklib_cffi.__file__ + else: + TKINTER_LIB = tk.__file__ +except AttributeError: + # _tkinter may be compiled directly into Python, in which case __file__ is + # not available. load_tkinter_funcs will check the binary first in any case. + TKINTER_LIB = None + +tk_version = str(tkinter.TkVersion) diff --git a/python_modules/PIL/_typing.py b/python_modules/PIL/_typing.py new file mode 100644 index 000000000..373938e71 --- /dev/null +++ b/python_modules/PIL/_typing.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import os +import sys +from collections.abc import Sequence +from typing import Any, Protocol, TypeVar, Union + +TYPE_CHECKING = False +if TYPE_CHECKING: + from numbers import _IntegralLike as IntegralLike + + try: + import numpy.typing as npt + + NumpyArray = npt.NDArray[Any] # requires numpy>=1.21 + except (ImportError, AttributeError): + pass + +if sys.version_info >= (3, 13): + from types import CapsuleType +else: + CapsuleType = object + +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + Buffer = Any + +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + try: + from typing_extensions import TypeGuard + except ImportError: + + class TypeGuard: # type: ignore[no-redef] + def __class_getitem__(cls, item: Any) -> type[bool]: + return bool + + +Coords = Union[Sequence[float], Sequence[Sequence[float]]] + + +_T_co = TypeVar("_T_co", covariant=True) + + +class SupportsRead(Protocol[_T_co]): + def read(self, length: int = ..., /) -> _T_co: ... + + +StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] + + +__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] diff --git a/python_modules/PIL/_util.py b/python_modules/PIL/_util.py new file mode 100644 index 000000000..8ef0d36f7 --- /dev/null +++ b/python_modules/PIL/_util.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import os +from typing import Any, NoReturn + +from ._typing import StrOrBytesPath, TypeGuard + + +def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: + return isinstance(f, (bytes, str, os.PathLike)) + + +class DeferredError: + def __init__(self, ex: BaseException): + self.ex = ex + + def __getattr__(self, elt: str) -> NoReturn: + raise self.ex + + @staticmethod + def new(ex: BaseException) -> Any: + """ + Creates an object that raises the wrapped exception ``ex`` when used, + and casts it to :py:obj:`~typing.Any` type. + """ + return DeferredError(ex) diff --git a/python_modules/PIL/_version.py b/python_modules/PIL/_version.py new file mode 100644 index 000000000..74e63356c --- /dev/null +++ b/python_modules/PIL/_version.py @@ -0,0 +1,4 @@ +# Master version for Pillow +from __future__ import annotations + +__version__ = "11.3.0" diff --git a/python_modules/PIL/_webp.pyi b/python_modules/PIL/_webp.pyi new file mode 100644 index 000000000..e27843e53 --- /dev/null +++ b/python_modules/PIL/_webp.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/features.py b/python_modules/PIL/features.py new file mode 100644 index 000000000..573f1d412 --- /dev/null +++ b/python_modules/PIL/features.py @@ -0,0 +1,361 @@ +from __future__ import annotations + +import collections +import os +import sys +import warnings +from typing import IO + +import PIL + +from . import Image +from ._deprecate import deprecate + +modules = { + "pil": ("PIL._imaging", "PILLOW_VERSION"), + "tkinter": ("PIL._tkinter_finder", "tk_version"), + "freetype2": ("PIL._imagingft", "freetype2_version"), + "littlecms2": ("PIL._imagingcms", "littlecms_version"), + "webp": ("PIL._webp", "webpdecoder_version"), + "avif": ("PIL._avif", "libavif_version"), +} + + +def check_module(feature: str) -> bool: + """ + Checks if a module is available. + + :param feature: The module to check for. + :returns: ``True`` if available, ``False`` otherwise. + :raises ValueError: If the module is not defined in this version of Pillow. + """ + if feature not in modules: + msg = f"Unknown module {feature}" + raise ValueError(msg) + + module, ver = modules[feature] + + try: + __import__(module) + return True + except ModuleNotFoundError: + return False + except ImportError as ex: + warnings.warn(str(ex)) + return False + + +def version_module(feature: str) -> str | None: + """ + :param feature: The module to check for. + :returns: + The loaded version number as a string, or ``None`` if unknown or not available. + :raises ValueError: If the module is not defined in this version of Pillow. + """ + if not check_module(feature): + return None + + module, ver = modules[feature] + + return getattr(__import__(module, fromlist=[ver]), ver) + + +def get_supported_modules() -> list[str]: + """ + :returns: A list of all supported modules. + """ + return [f for f in modules if check_module(f)] + + +codecs = { + "jpg": ("jpeg", "jpeglib"), + "jpg_2000": ("jpeg2k", "jp2klib"), + "zlib": ("zip", "zlib"), + "libtiff": ("libtiff", "libtiff"), +} + + +def check_codec(feature: str) -> bool: + """ + Checks if a codec is available. + + :param feature: The codec to check for. + :returns: ``True`` if available, ``False`` otherwise. + :raises ValueError: If the codec is not defined in this version of Pillow. + """ + if feature not in codecs: + msg = f"Unknown codec {feature}" + raise ValueError(msg) + + codec, lib = codecs[feature] + + return f"{codec}_encoder" in dir(Image.core) + + +def version_codec(feature: str) -> str | None: + """ + :param feature: The codec to check for. + :returns: + The version number as a string, or ``None`` if not available. + Checked at compile time for ``jpg``, run-time otherwise. + :raises ValueError: If the codec is not defined in this version of Pillow. + """ + if not check_codec(feature): + return None + + codec, lib = codecs[feature] + + version = getattr(Image.core, f"{lib}_version") + + if feature == "libtiff": + return version.split("\n")[0].split("Version ")[1] + + return version + + +def get_supported_codecs() -> list[str]: + """ + :returns: A list of all supported codecs. + """ + return [f for f in codecs if check_codec(f)] + + +features: dict[str, tuple[str, str | bool, str | None]] = { + "webp_anim": ("PIL._webp", True, None), + "webp_mux": ("PIL._webp", True, None), + "transp_webp": ("PIL._webp", True, None), + "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), + "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), + "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), + "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), + "mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"), + "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), + "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), + "xcb": ("PIL._imaging", "HAVE_XCB", None), +} + + +def check_feature(feature: str) -> bool | None: + """ + Checks if a feature is available. + + :param feature: The feature to check for. + :returns: ``True`` if available, ``False`` if unavailable, ``None`` if unknown. + :raises ValueError: If the feature is not defined in this version of Pillow. + """ + if feature not in features: + msg = f"Unknown feature {feature}" + raise ValueError(msg) + + module, flag, ver = features[feature] + + if isinstance(flag, bool): + deprecate(f'check_feature("{feature}")', 12) + try: + imported_module = __import__(module, fromlist=["PIL"]) + if isinstance(flag, bool): + return flag + return getattr(imported_module, flag) + except ModuleNotFoundError: + return None + except ImportError as ex: + warnings.warn(str(ex)) + return None + + +def version_feature(feature: str) -> str | None: + """ + :param feature: The feature to check for. + :returns: The version number as a string, or ``None`` if not available. + :raises ValueError: If the feature is not defined in this version of Pillow. + """ + if not check_feature(feature): + return None + + module, flag, ver = features[feature] + + if ver is None: + return None + + return getattr(__import__(module, fromlist=[ver]), ver) + + +def get_supported_features() -> list[str]: + """ + :returns: A list of all supported features. + """ + supported_features = [] + for f, (module, flag, _) in features.items(): + if flag is True: + for feature, (feature_module, _) in modules.items(): + if feature_module == module: + if check_module(feature): + supported_features.append(f) + break + elif check_feature(f): + supported_features.append(f) + return supported_features + + +def check(feature: str) -> bool | None: + """ + :param feature: A module, codec, or feature name. + :returns: + ``True`` if the module, codec, or feature is available, + ``False`` or ``None`` otherwise. + """ + + if feature in modules: + return check_module(feature) + if feature in codecs: + return check_codec(feature) + if feature in features: + return check_feature(feature) + warnings.warn(f"Unknown feature '{feature}'.", stacklevel=2) + return False + + +def version(feature: str) -> str | None: + """ + :param feature: + The module, codec, or feature to check for. + :returns: + The version number as a string, or ``None`` if unknown or not available. + """ + if feature in modules: + return version_module(feature) + if feature in codecs: + return version_codec(feature) + if feature in features: + return version_feature(feature) + return None + + +def get_supported() -> list[str]: + """ + :returns: A list of all supported modules, features, and codecs. + """ + + ret = get_supported_modules() + ret.extend(get_supported_features()) + ret.extend(get_supported_codecs()) + return ret + + +def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: + """ + Prints information about this installation of Pillow. + This function can be called with ``python3 -m PIL``. + It can also be called with ``python3 -m PIL.report`` or ``python3 -m PIL --report`` + to have "supported_formats" set to ``False``, omitting the list of all supported + image file formats. + + :param out: + The output stream to print to. Defaults to ``sys.stdout`` if ``None``. + :param supported_formats: + If ``True``, a list of all supported image file formats will be printed. + """ + + if out is None: + out = sys.stdout + + Image.init() + + print("-" * 68, file=out) + print(f"Pillow {PIL.__version__}", file=out) + py_version_lines = sys.version.splitlines() + print(f"Python {py_version_lines[0].strip()}", file=out) + for py_version in py_version_lines[1:]: + print(f" {py_version.strip()}", file=out) + print("-" * 68, file=out) + print(f"Python executable is {sys.executable or 'unknown'}", file=out) + if sys.prefix != sys.base_prefix: + print(f"Environment Python files loaded from {sys.prefix}", file=out) + print(f"System Python files loaded from {sys.base_prefix}", file=out) + print("-" * 68, file=out) + print( + f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}", + file=out, + ) + print( + f"Binary Pillow modules loaded from {os.path.dirname(Image.core.__file__)}", + file=out, + ) + print("-" * 68, file=out) + + for name, feature in [ + ("pil", "PIL CORE"), + ("tkinter", "TKINTER"), + ("freetype2", "FREETYPE2"), + ("littlecms2", "LITTLECMS2"), + ("webp", "WEBP"), + ("avif", "AVIF"), + ("jpg", "JPEG"), + ("jpg_2000", "OPENJPEG (JPEG2000)"), + ("zlib", "ZLIB (PNG/ZIP)"), + ("libtiff", "LIBTIFF"), + ("raqm", "RAQM (Bidirectional Text)"), + ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), + ("xcb", "XCB (X protocol)"), + ]: + if check(name): + v: str | None = None + if name == "jpg": + libjpeg_turbo_version = version_feature("libjpeg_turbo") + if libjpeg_turbo_version is not None: + v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo" + v += " " + libjpeg_turbo_version + if v is None: + v = version(name) + if v is not None: + version_static = name in ("pil", "jpg") + if name == "littlecms2": + # this check is also in src/_imagingcms.c:setup_module() + version_static = tuple(int(x) for x in v.split(".")) < (2, 7) + t = "compiled for" if version_static else "loaded" + if name == "zlib": + zlib_ng_version = version_feature("zlib_ng") + if zlib_ng_version is not None: + v += ", compiled for zlib-ng " + zlib_ng_version + elif name == "raqm": + for f in ("fribidi", "harfbuzz"): + v2 = version_feature(f) + if v2 is not None: + v += f", {f} {v2}" + print("---", feature, "support ok,", t, v, file=out) + else: + print("---", feature, "support ok", file=out) + else: + print("***", feature, "support not installed", file=out) + print("-" * 68, file=out) + + if supported_formats: + extensions = collections.defaultdict(list) + for ext, i in Image.EXTENSION.items(): + extensions[i].append(ext) + + for i in sorted(Image.ID): + line = f"{i}" + if i in Image.MIME: + line = f"{line} {Image.MIME[i]}" + print(line, file=out) + + if i in extensions: + print( + "Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out + ) + + features = [] + if i in Image.OPEN: + features.append("open") + if i in Image.SAVE: + features.append("save") + if i in Image.SAVE_ALL: + features.append("save_all") + if i in Image.DECODERS: + features.append("decode") + if i in Image.ENCODERS: + features.append("encode") + + print("Features: {}".format(", ".join(features)), file=out) + print("-" * 68, file=out) diff --git a/python_modules/PIL/py.typed b/python_modules/PIL/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/python_modules/PIL/report.py b/python_modules/PIL/report.py new file mode 100644 index 000000000..d2815e845 --- /dev/null +++ b/python_modules/PIL/report.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from .features import pilinfo + +pilinfo(supported_formats=False) diff --git a/python_modules/_cloudflare_compat_flags.pyi b/python_modules/_cloudflare_compat_flags.pyi new file mode 100644 index 000000000..147262959 --- /dev/null +++ b/python_modules/_cloudflare_compat_flags.pyi @@ -0,0 +1,4 @@ +python_workflows_implicit_dependencies: bool +python_request_headers_preserve_commas: bool + +def __getattr__(name: str) -> bool: ... diff --git a/python_modules/_pyodide_entrypoint_helper.pyi b/python_modules/_pyodide_entrypoint_helper.pyi new file mode 100644 index 000000000..3407c1d14 --- /dev/null +++ b/python_modules/_pyodide_entrypoint_helper.pyi @@ -0,0 +1,9 @@ +from collections.abc import Generator +from typing import Any + +cloudflareWorkersModule: Any +cloudflareSocketsModule: Any + +def doAnImport(module_name: str) -> Any: ... +def patch_env_helper(kwds: Any) -> Generator[None]: ... +def patchWaitUntil(ctx: Any) -> None: ... diff --git a/python_modules/_workers_sdk_entropy_import_context.pth b/python_modules/_workers_sdk_entropy_import_context.pth new file mode 100644 index 000000000..e0f6c4673 --- /dev/null +++ b/python_modules/_workers_sdk_entropy_import_context.pth @@ -0,0 +1 @@ +import _workers_sdk_entropy_import_context_loader diff --git a/python_modules/_workers_sdk_entropy_import_context.py b/python_modules/_workers_sdk_entropy_import_context.py new file mode 100644 index 000000000..630b26a82 --- /dev/null +++ b/python_modules/_workers_sdk_entropy_import_context.py @@ -0,0 +1,183 @@ +""" +Top level entropy patches for packages +""" + +import sys +from contextlib import contextmanager + +from _cloudflare.allow_entropy import ( + allow_bad_entropy_calls, +) +from _cloudflare.import_patch_manager import ( + block_calls, + register_after_snapshot, + register_before_first_request, + register_create_patch, + register_exec_patch, +) + + +class STATE: + imported_rust_package = False + numpy_random = None + + +@register_create_patch("tiktoken._tiktoken") +@register_exec_patch("cryptography.exceptions") +@register_exec_patch("jiter") +@contextmanager +def rust_package_context(module): + """Rust packages need one entropy call if they create a rust hash map at + init time. + + For reasons I don't entirely understand, in Pyodide 0.28 only the first Rust package to be + imported makes the get_entropy call. See gen_rust_import_tests() which tests that importing + four rust packages in different permutations works correctly. + """ + if STATE.imported_rust_package: + yield + return + STATE.imported_rust_package = True + with allow_bad_entropy_calls(1): + yield + + +@register_exec_patch("numpy.random") +@contextmanager +def numpy_random_context(numpy_random): + """numpy.random doesn't call getentropy() itself, but we want to block calls + that might use the bad seed. + + TODO: Maybe there are more calls we can whitelist? + TODO: Is it not enough to just block numpy.random.mtrand calls? + """ + yield + # Calling default_rng() with a given seed is fine, calling it without a seed + # will call getentropy() and fail. + block_calls(numpy_random, allowlist=("default_rng", "RandomState")) + + +@register_after_snapshot("numpy.random") +def numpy_random_after_snapshot(numpy_random): + r1 = numpy_random.random() + numpy_random.set_state(STATE.numpy_random) + r2 = numpy_random.random() + if r1 != r2: + raise RuntimeError("random seed in bad state") + + +@register_before_first_request("numpy.random") +def numpy_random_before_first_request(numpy_random): + numpy_random.seed() + + +@register_exec_patch("numpy.random.mtrand") +@contextmanager +def numpy_random_mtrand_context(module): + # numpy.random.mtrand calls secrets.randbits at top level to seed itself. + # This will fail if we don't let it through. + with allow_bad_entropy_calls(1): + yield + # Block calls until we get a chance to replace the bad random seed. + STATE.numpy_random = module.get_state() + block_calls(module, allowlist=("RandomState",)) + + +@register_exec_patch("pydantic_core") +@contextmanager +def pydantic_core_context(module): + try: + # Initial import needs one entropy call to initialize + # std::collections::HashMap hash seed + with allow_bad_entropy_calls(1): + yield + finally: + try: + with allow_bad_entropy_calls(1): + # validate_core_schema makes an ahash::AHashMap which makes + # another entropy call for its hash seed. It will throw an error + # but only after making the needed entropy call. + module.validate_core_schema(None) + except module.SchemaError: + pass + + +@register_exec_patch("aiohttp.http_websocket") +@contextmanager +def aiohttp_http_websocket_context(module): + import random + + Random = random.Random + + def patched_Random(): + return random + + random.Random = patched_Random + try: + yield + finally: + random.Random = Random + + +class NoSslFinder: + def find_spec(self, fullname, path, target): + if fullname == "ssl": + raise ModuleNotFoundError( + f"No module named {fullname!r}", name=fullname + ) from None + + +@contextmanager +def no_ssl(): + """ + Various packages will call ssl.create_default_context() at top level which uses entropy if they + can import ssl. By temporarily making importing ssl raise an import error, we exercise the + workaround code and so avoid the entropy calls. After, we put the ssl module back to the normal + value. + """ + try: + f = NoSslFinder() + ssl = sys.modules.pop("ssl", None) + sys.meta_path.insert(0, f) + yield + finally: + sys.meta_path.remove(f) + if ssl: + sys.modules["ssl"] = ssl + + +@register_exec_patch("aiohttp.connector") +@contextmanager +def aiohttp_connector_context(module): + with no_ssl(): + yield + + +@register_exec_patch("requests.adapters") +@contextmanager +def requests_adapters_context(module): + with no_ssl(): + yield + + +@register_exec_patch("urllib3.util.ssl_") +@contextmanager +def urllib3_util_ssl__context(module): + with no_ssl(): + yield + + +@register_exec_patch("langsmith._internal._constants") +@contextmanager +def langsmith__internal__constants_context(module): + # Langsmith uses a UUID to communicate with a background thread. This obviously won't work so we + # might as well allow it to make a UUID. + with allow_bad_entropy_calls(1): + yield + + +@register_exec_patch("langchain_openai.chat_models.base") +@contextmanager +def langchain_openai_chat_models_base_context(module): + with allow_bad_entropy_calls(1): + yield diff --git a/python_modules/_workers_sdk_entropy_import_context_loader.py b/python_modules/_workers_sdk_entropy_import_context_loader.py new file mode 100644 index 000000000..2e09aeb3d --- /dev/null +++ b/python_modules/_workers_sdk_entropy_import_context_loader.py @@ -0,0 +1,13 @@ +""" +Loader shim for _workers_sdk_entropy_import_context. + +This module is imported by the .pth file on every Python startup. The entropy +context patches depend on the `_cloudflare` package, which only exists inside +the workers runtime. When running outside of the workers runtime, `_cloudflare` +is not available, so we silently skip loading the patches. +""" + +import importlib.util + +if importlib.util.find_spec("_cloudflare") is not None: + import _workers_sdk_entropy_import_context # noqa: F401 diff --git a/python_modules/pillow-11.3.0.dist-info/INSTALLER b/python_modules/pillow-11.3.0.dist-info/INSTALLER new file mode 100644 index 000000000..5c69047b2 --- /dev/null +++ b/python_modules/pillow-11.3.0.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/python_modules/pillow-11.3.0.dist-info/METADATA b/python_modules/pillow-11.3.0.dist-info/METADATA new file mode 100644 index 000000000..ea8c3fd50 --- /dev/null +++ b/python_modules/pillow-11.3.0.dist-info/METADATA @@ -0,0 +1,177 @@ +Metadata-Version: 2.4 +Name: pillow +Version: 11.3.0 +Summary: Python Imaging Library (Fork) +Author-email: "Jeffrey A. Clark" +License-Expression: MIT-CMU +Project-URL: Changelog, https://github.com/python-pillow/Pillow/releases +Project-URL: Documentation, https://pillow.readthedocs.io +Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi +Project-URL: Homepage, https://python-pillow.github.io +Project-URL: Mastodon, https://fosstodon.org/@pillow +Project-URL: Release notes, https://pillow.readthedocs.io/en/stable/releasenotes/index.html +Project-URL: Source, https://github.com/python-pillow/Pillow +Keywords: Imaging +Classifier: Development Status :: 6 - Mature +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Multimedia :: Graphics +Classifier: Topic :: Multimedia :: Graphics :: Capture :: Digital Camera +Classifier: Topic :: Multimedia :: Graphics :: Capture :: Screen Capture +Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion +Classifier: Topic :: Multimedia :: Graphics :: Viewers +Classifier: Typing :: Typed +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: furo; extra == "docs" +Requires-Dist: olefile; extra == "docs" +Requires-Dist: sphinx>=8.2; extra == "docs" +Requires-Dist: sphinx-autobuild; extra == "docs" +Requires-Dist: sphinx-copybutton; extra == "docs" +Requires-Dist: sphinx-inline-tabs; extra == "docs" +Requires-Dist: sphinxext-opengraph; extra == "docs" +Provides-Extra: fpx +Requires-Dist: olefile; extra == "fpx" +Provides-Extra: mic +Requires-Dist: olefile; extra == "mic" +Provides-Extra: test-arrow +Requires-Dist: pyarrow; extra == "test-arrow" +Provides-Extra: tests +Requires-Dist: check-manifest; extra == "tests" +Requires-Dist: coverage>=7.4.2; extra == "tests" +Requires-Dist: defusedxml; extra == "tests" +Requires-Dist: markdown2; extra == "tests" +Requires-Dist: olefile; extra == "tests" +Requires-Dist: packaging; extra == "tests" +Requires-Dist: pyroma; extra == "tests" +Requires-Dist: pytest; extra == "tests" +Requires-Dist: pytest-cov; extra == "tests" +Requires-Dist: pytest-timeout; extra == "tests" +Requires-Dist: pytest-xdist; extra == "tests" +Requires-Dist: trove-classifiers>=2024.10.12; extra == "tests" +Provides-Extra: typing +Requires-Dist: typing-extensions; python_version < "3.10" and extra == "typing" +Provides-Extra: xmp +Requires-Dist: defusedxml; extra == "xmp" +Dynamic: license-file + +

+ Pillow logo +

+ +# Pillow + +## Python Imaging Library (Fork) + +Pillow is the friendly PIL fork by [Jeffrey A. Clark and +contributors](https://github.com/python-pillow/Pillow/graphs/contributors). +PIL is the Python Imaging Library by Fredrik Lundh and contributors. +As of 2019, Pillow development is +[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). + + + + + + + + + + + + + + + + + + +
docs + Documentation Status +
tests + GitHub Actions build status (Lint) + GitHub Actions build status (Test Linux and macOS) + GitHub Actions build status (Test Windows) + GitHub Actions build status (Test MinGW) + GitHub Actions build status (Test Cygwin) + GitHub Actions build status (Test Docker) + GitHub Actions build status (Wheels) + Code coverage + Fuzzing Status +
package + Zenodo + Tidelift + Newest PyPI version + Number of PyPI downloads + OpenSSF Best Practices +
social + Join the chat at https://gitter.im/python-pillow/Pillow + Follow on https://fosstodon.org/@pillow +
+ +## Overview + +The Python Imaging Library adds image processing capabilities to your Python interpreter. + +This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities. + +The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. + +## More information + +- [Documentation](https://pillow.readthedocs.io/) + - [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html) + - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html) +- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md) + - [Issues](https://github.com/python-pillow/Pillow/issues) + - [Pull requests](https://github.com/python-pillow/Pillow/pulls) +- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) +- [Changelog](https://github.com/python-pillow/Pillow/releases) + - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) + +## Report a vulnerability + +To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security). diff --git a/python_modules/pillow-11.3.0.dist-info/RECORD b/python_modules/pillow-11.3.0.dist-info/RECORD new file mode 100644 index 000000000..8a569d552 --- /dev/null +++ b/python_modules/pillow-11.3.0.dist-info/RECORD @@ -0,0 +1,119 @@ +PIL/AvifImagePlugin.py,sha256=5IiDMvMZQXLnS3t25XJjlwgNWmeVSNaGfReWAp-V5lo,8994 +PIL/BdfFontFile.py,sha256=PhlZfIRmEfmorbhZZeSM5eebGo1Ei7fL-lR9XlfTZZA,3285 +PIL/BlpImagePlugin.py,sha256=Ub4vVKBEniiNBEgNizxScEpO1VKbC1w6iecWUU7T-Vs,16533 +PIL/BmpImagePlugin.py,sha256=-SNdj2godmaKYAc08dEng6z3mRPbYYHezjveIR5e-tU,19855 +PIL/BufrStubImagePlugin.py,sha256=JSqDhkPNPnFw0Qcz-gQJl-D_iSCFdtcLvPynshKJ4WM,1730 +PIL/ContainerIO.py,sha256=wkBqL2GDAb5fh3wrtfTGUfqioJipCl-lg2GxbjQrTZw,4604 +PIL/CurImagePlugin.py,sha256=bICiwXZrzSONWBu4bKtshxZSNFj8su0lbDojYntEUYs,1797 +PIL/DcxImagePlugin.py,sha256=DhqsmW7MjmnUSTGZ-Skv9hz1XeX3XoQQoAl9GWLAEEY,2145 +PIL/DdsImagePlugin.py,sha256=fjdfZK_eQtUp_-bjoRmt-5wgOT5GTmvg6aI-itch4mo,18906 +PIL/EpsImagePlugin.py,sha256=ROWwCv08bC_B41eMf2AFe8UW6ZH4_XQ18x12KB_aQLM,16389 +PIL/ExifTags.py,sha256=zW6kVikCosiyoCo7J7R62evD3hoxjKPchnVh8po7CZc,9931 +PIL/FitsImagePlugin.py,sha256=-oDJnAH113CK5qPvwz9lL81fkV1gla_tNfqLcq8zKgo,4644 +PIL/FliImagePlugin.py,sha256=DaWuH8f-9GihS0VVZqF1bT3uDv1Vb0VBl0chnNd82Ow,4786 +PIL/FontFile.py,sha256=St7MxO5Q-oakCLWn3ZrgrtaT3wSsmAarxm8AU-G8Moc,3577 +PIL/FpxImagePlugin.py,sha256=aXfg0YdvNeJhxqh-f-f22D1NobQ8tSVCj-tpLE2PKfE,7293 +PIL/FtexImagePlugin.py,sha256=v2I5YkdfNA3iW35JzKnWry9v6Rgvr0oezGVOuArREac,3535 +PIL/GbrImagePlugin.py,sha256=5t0UfLubTPQcuDDbafwC78OLR7IsD5hjpvhUZ5g8z4A,3006 +PIL/GdImageFile.py,sha256=LP4Uxv3Y2ivGZIyOVuGJarDDVS7zK6F1Q6SNl4wyGuQ,2788 +PIL/GifImagePlugin.py,sha256=SkXboZwxTolq0uteXYX0ncrZiUxyASywqAurOcVAi3U,42201 +PIL/GimpGradientFile.py,sha256=Z_4TUYMdPyUsiP40KSIpMJ5yLGMnBaIKOAkHyiQGEWE,3906 +PIL/GimpPaletteFile.py,sha256=YHEhKThsEVlXVjFQUnGvhDgNsJcfFqUAN0O0ucG9G-Q,1815 +PIL/GribStubImagePlugin.py,sha256=degHg344X3JXL8u-x8NWn08BsmM9wRh-Jg08HHrvfOc,1738 +PIL/Hdf5StubImagePlugin.py,sha256=OuEQijGqVwTTSG4dB2vAyQzmN-NYT22tiuZHFH0Q0Sw,1741 +PIL/IcnsImagePlugin.py,sha256=qvi-OP0g8CRlNlJE--5_rPlfyxLFLlSOil66Fw4TMwU,12949 +PIL/IcoImagePlugin.py,sha256=QCo29Toh08UX8vEcdCAaIeuidSolbPiZlCnQ4rUu2SQ,12491 +PIL/ImImagePlugin.py,sha256=wo5OL2PAcQW2MwRkJnS-N16toZzXWL95jx9FBM7l9ok,11567 +PIL/Image.py,sha256=95Jefi2QFIfZYOyfHNBRTwBtwrnNZsn5oCsLQsBLdK8,148332 +PIL/ImageChops.py,sha256=GEjlymcoDtA5OOeIxQVIX96BD-s6AXhb7TmSLYn2tUg,7946 +PIL/ImageCms.py,sha256=A5ZVaTjjxR6AeDNNvK-hmu0QqKOMTscou6BUBTLob0g,41934 +PIL/ImageColor.py,sha256=IGA9C2umeED_EzS2Cvj6KsU0VutC9RstWIYPe8uDsVk,9441 +PIL/ImageDraw.py,sha256=Enr0ctBHKBnSHVBDlqcIbIAyHgVj5ZbLL-swVb8s8Vo,42845 +PIL/ImageDraw2.py,sha256=pdVMW7bVw3KwhXvRZh28Md4y-2xFfuo5fHcDnaYqVK4,7227 +PIL/ImageEnhance.py,sha256=4Elhz_lyyxLmx0GkSHrwOAmNJ2TkqVQPHejzGihZUMI,3627 +PIL/ImageFile.py,sha256=HLgKqn6K9J4HlnyiPFZUTAfcqxXYjE06fZeKO6V-haw,29334 +PIL/ImageFilter.py,sha256=MiTowY9micg1dSfwZkExXSBNPr2b_11kDCGreP6W8x4,18671 +PIL/ImageFont.py,sha256=rVQm3zwnTFZ1HSp4OeA5THKjTezhE8HMrnOhHzmqfEM,64292 +PIL/ImageGrab.py,sha256=I9PHpsQf2VyNX4T8QL-8awFNotyAzB1mGxTt_I5FbTE,6471 +PIL/ImageMath.py,sha256=XasMsgjaD9p2OZa7naOdpEACq3yJl-Q2RGTf4xo7CgM,11919 +PIL/ImageMode.py,sha256=5yOxODAZ7jG03DsUFrt7eQayTtIpWPgvfyhlXDWwcv8,2681 +PIL/ImageMorph.py,sha256=TowXnk1Q2wX9AXVBDWRRQhCfAbFOUWGMo00vq4yn-fU,8563 +PIL/ImageOps.py,sha256=A69qjt-mxDI99387z_4cHI-wtH85SLL_ENTI9EeOQGI,25525 +PIL/ImagePalette.py,sha256=M5tYUgadWR7mxUEByyVl7IV9QFFzAGiKKmAhCZtdG0w,9009 +PIL/ImagePath.py,sha256=5yUG5XCUil1KKTTA_8PgGhcmg-mnue-GK0FwTBlhjw4,371 +PIL/ImageQt.py,sha256=dQbadF2Lg59OJVjiNVcbz3wvymqEpL-uEZG32b8E-bg,6841 +PIL/ImageSequence.py,sha256=gx2EvywPBEjxNJujCqdpbfAm2BpyNV2_f1IaO3niubw,2200 +PIL/ImageShow.py,sha256=Ju0_Db2B4_n3yKJV9sDsF7_HAgciEdXlq6I1Eiw1YTo,10106 +PIL/ImageStat.py,sha256=S43FZ89r_u4hKCj59lVuWpyVJfhbUy3igXkp9DwaMgM,5325 +PIL/ImageTk.py,sha256=b5SntckGXs0ECsI2MmdJg3CSX6AtELsWh0Ohxu41u_k,8132 +PIL/ImageTransform.py,sha256=-qek7P3lzLddcXt9cWt5w_L11JGp2yY3AJtOfmJAkDc,3916 +PIL/ImageWin.py,sha256=LT05w8_vTfRrC3n9S9pM0TNbXrzZLEJHlCJil7Xv80k,8085 +PIL/ImtImagePlugin.py,sha256=SL5IrsHcblltxtX4v_HVFhYnR6haJ0AOd2NHhZKMImY,2665 +PIL/IptcImagePlugin.py,sha256=3BVI_oEbFEJC-yn6zmp5Joqf8edCJLKH9N5FQanyaV8,6719 +PIL/Jpeg2KImagePlugin.py,sha256=k9UoU7-Hq8vAWi9ZoosA4bfufNJsctBd4ttM1RFxwnk,13865 +PIL/JpegImagePlugin.py,sha256=WaCZTpdmzuCM5mi44bNyN4p1EXOsnKz63qv4XEbm8Ns,31786 +PIL/JpegPresets.py,sha256=lnqWHo4DLIHIulcdHp0NJ7CWexHt8T3w51kIKlLfkIA,12379 +PIL/McIdasImagePlugin.py,sha256=baOIkD-CIIeCgBFTf8kos928PKBuCUqYYa38u3WES_8,1877 +PIL/MicImagePlugin.py,sha256=aoIwkWVyr_X-dPvB6ldZOJF3a9kd_OeuEW3say5Y0QM,2564 +PIL/MpegImagePlugin.py,sha256=g7BZd93kWpFi41SG_wKFoi0yEPsioI4kj45b2F-3Vrw,2010 +PIL/MpoImagePlugin.py,sha256=S45qt7OcY7rBjYlwEk0nUmEj5IOu5z8KVLo066V1RBE,6722 +PIL/MspImagePlugin.py,sha256=oxk_MLUDvzJ4JDuOZCHkmqOPXniG42PHOyNGwe60slY,5892 +PIL/PSDraw.py,sha256=KMBGj3vXaFpblaIcA9KjFFTpdal41AQggY-UgzqoMkQ,6918 +PIL/PaletteFile.py,sha256=suDdAL6VMljXw4oEn1vhTt4DQ4vbpIHGd3A4oxOgE6s,1216 +PIL/PalmImagePlugin.py,sha256=WJ1b8I1xTSAXYDJhIpkVFCLu2LlpbiBD5d1Hr-m2l08,8748 +PIL/PcdImagePlugin.py,sha256=VweZ108HBHeNEfsoE26EOR4ktxqNGSOWOnd58DhS8Fo,1601 +PIL/PcfFontFile.py,sha256=NPZQ0XkbGB8uTlGqgmIPGkwuLMYBdykDeVuvFgIC7JU,7147 +PIL/PcxImagePlugin.py,sha256=2dqnjRjSLbjm8Opub4sZRhOIdYLdn3y7Q_ETV8EmiOQ,6224 +PIL/PdfImagePlugin.py,sha256=AbJA2f4qzH8G1olfmk18SzQlcx3WsipUYDc5bcR8Wvk,9349 +PIL/PdfParser.py,sha256=LnmX0Cm7ZQwGkB1uYP4rvXZUkERmURzmYo78zjeq6VI,37987 +PIL/PixarImagePlugin.py,sha256=l_4GwBd0mATnIXYJbwmmODU2vP7wewLu6BRviHCB2EI,1758 +PIL/PngImagePlugin.py,sha256=jPBNqZ50txFHWIsDikcdkeeBfLNY1PxT5wzcPMcmcmQ,51117 +PIL/PpmImagePlugin.py,sha256=QJM-V-odV7w-prA7B5bLRQcykdC4d7OJ5BBbCvPPIzY,12370 +PIL/PsdImagePlugin.py,sha256=ImnNRG4VANs2GATXVEB5Q-yy1Jskc6XRVRtZYi2fALg,8685 +PIL/QoiImagePlugin.py,sha256=RPO63QsgHAsyPpcxh7ymeMYlnjVu5gT5ELolkvJt0vc,8572 +PIL/SgiImagePlugin.py,sha256=3Ql89s8vycNWjcxJwMw28iksV9Yj2xWoKBQ6c5DHXBg,6389 +PIL/SpiderImagePlugin.py,sha256=Bsg6pfZMctas1xYx__oL-ZZseUReZdnLy5a-aKEJhpE,10249 +PIL/SunImagePlugin.py,sha256=Hdxkhk0pxpBGxYhPJfCDLwsYcO1KjxjtplNMFYibIvk,4589 +PIL/TarIO.py,sha256=BqYUChCBb9F7Sh-uZ86iz1Dtoy2D0obNwGm65z1rdc0,1442 +PIL/TgaImagePlugin.py,sha256=2vDsFTcBUBHw1V80wpVv4tgpLDbPr6yVHi6Fvaqf0HY,6980 +PIL/TiffImagePlugin.py,sha256=IK7Ur131NNyJET-wk50tzLkSyd7TI1lwSES4N_txy5w,85029 +PIL/TiffTags.py,sha256=-gbXLZ5rlHD6crwtY6TkafDm2tamlc5v8e7FjS8PcIg,17082 +PIL/WalImageFile.py,sha256=Lfuq_WZ_V_onwucfUc6GWfvY7z_K4s-5EdaQGu_2DD4,5704 +PIL/WebPImagePlugin.py,sha256=YFWo6_FYBSrzAf6XMbmrF4YRtR4x7tYecCWF7EA13WQ,10010 +PIL/WmfImagePlugin.py,sha256=Z1hzGuHGt08tBLsxgBV7ZVOLdQPykDMYd4RGkw1J8rw,5243 +PIL/XVThumbImagePlugin.py,sha256=cJSapkBasFt11O6XYXxqcyA-njxA5BD3wHhNj6VC7Fk,2115 +PIL/XbmImagePlugin.py,sha256=Fd6GVDEo73nyFICA3Z3w4LjkwoZWvhHB6rKCm5yVrho,2669 +PIL/XpmImagePlugin.py,sha256=jtUKavJCYwIAsJaJwSx8vJsx1oTbCywfDxePENmA93w,4400 +PIL/__init__.py,sha256=Q4KOEpR7S_Xsj30fvOsvR94xEpX4KUsVeUwaVP1fU80,2031 +PIL/__main__.py,sha256=Lpj4vef8mI7jA1sRCUAoVYaeePD_Uc898xF5c7XLx1A,133 +PIL/_avif.pyi,sha256=3fBxcSppJr6EOEcUojvflG3Eegg7lv2Qp0dNQQILrP4,63 +PIL/_binary.py,sha256=pcM6AL04GxgmGeLfcH1V1BZHENwIrQH0uxhJ7r0HIL0,2550 +PIL/_deprecate.py,sha256=JYJfJgemvedcdHMH6_RFTDBLNp4vSJqd-o32e3WzNlM,2034 +PIL/_imaging.cpython-313-wasm32-emscripten.so,sha256=VVq0eE0RznSOkOHVSggKOP0XUIXEs2kafWJGcejOGE4,1059007 +PIL/_imaging.pyi,sha256=StMbXUZS32AegATP1sUHfs5P05A3TD_BiQKsDHQBW40,868 +PIL/_imagingcms.pyi,sha256=brpjxRoiY_2ItyfTrjhKeGEsExe4GPG-25q9AQP8Jp8,4389 +PIL/_imagingft.cpython-313-wasm32-emscripten.so,sha256=4xLnJ-WNrEeMB4a370bV48E0_6q45NTZeInmBm3FVYc,605360 +PIL/_imagingft.pyi,sha256=IYdFGfApwsqYiJVoD5AVOvgMvnO1eP1J3cMA6L0YZJ0,1806 +PIL/_imagingmath.cpython-313-wasm32-emscripten.so,sha256=09jVv99pbgGakxOyZFTROVDiurcBdabOPJ1W2ZG4ibg,15459 +PIL/_imagingmath.pyi,sha256=3fBxcSppJr6EOEcUojvflG3Eegg7lv2Qp0dNQQILrP4,63 +PIL/_imagingmorph.cpython-313-wasm32-emscripten.so,sha256=9te_JRXP9hyQ2jVjg_QmOG57CTyTSKYrWhx1QDIEYEM,2584 +PIL/_imagingmorph.pyi,sha256=3fBxcSppJr6EOEcUojvflG3Eegg7lv2Qp0dNQQILrP4,63 +PIL/_imagingtk.cpython-313-wasm32-emscripten.so,sha256=zBJhe_d_CeEbGenqDoCiJz2S0k4cJk3kvMVkXmoOahw,3299 +PIL/_imagingtk.pyi,sha256=3fBxcSppJr6EOEcUojvflG3Eegg7lv2Qp0dNQQILrP4,63 +PIL/_tkinter_finder.py,sha256=GIZ4stmFhUosmHKSrdxcjStiocDNfyJn7RBie2SWxU0,538 +PIL/_typing.py,sha256=1NAWJ7Z59TP98cFv9qGpBMgSHbyR4CAByLjMRRbSZxY,1251 +PIL/_util.py,sha256=E76J1WLAe6Xg5yNWYztQwYzxUT_sR_VQxFJu7IZ3S3k,635 +PIL/_version.py,sha256=Zwv2LKWt6v32STL5K9uN7PdcJmZhDlokKTLkDA7Ky1w,87 +PIL/_webp.cpython-313-wasm32-emscripten.so,sha256=rDmrwirTAWmvM-aJ2tCxo_JHPd6TNC07ud7KecJTpHo,370564 +PIL/_webp.pyi,sha256=3fBxcSppJr6EOEcUojvflG3Eegg7lv2Qp0dNQQILrP4,63 +PIL/features.py,sha256=FfyYObVJbzYQUXf8KuRuqY6kvA8md2LorE81k3EuQrw,11479 +PIL/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +PIL/report.py,sha256=4JY6-IU7sH1RKuRbOvy1fUt0dAoi79FX4tYJN3p1DT0,100 +pillow-11.3.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +pillow-11.3.0.dist-info/METADATA,sha256=1T1NePio7-GCOWcR73aEA4bSukzNrUIfWMwAw7NAH3M,9023 +pillow-11.3.0.dist-info/RECORD,, +pillow-11.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pillow-11.3.0.dist-info/WHEEL,sha256=BpQa-T1rm3obVjGx9TDh4sk-6ZZL2DSSkDmWtR-U0xk,113 +pillow-11.3.0.dist-info/licenses/LICENSE,sha256=F_JArhARQ3B-XnMDpdgARQ2czHR1tGPO21Vc79s8bs4,1453 +pillow-11.3.0.dist-info/top_level.txt,sha256=riZqrk-hyZqh5f1Z0Zwii3dKfxEsByhu9cU9IODF-NY,4 +pillow-11.3.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 diff --git a/python_modules/pillow-11.3.0.dist-info/REQUESTED b/python_modules/pillow-11.3.0.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/python_modules/pillow-11.3.0.dist-info/WHEEL b/python_modules/pillow-11.3.0.dist-info/WHEEL new file mode 100644 index 000000000..4c684866c --- /dev/null +++ b/python_modules/pillow-11.3.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: false +Tag: cp313-cp313-pyodide_2025_0_wasm32 + diff --git a/python_modules/pillow-11.3.0.dist-info/licenses/LICENSE b/python_modules/pillow-11.3.0.dist-info/licenses/LICENSE new file mode 100644 index 000000000..10dd42d9e --- /dev/null +++ b/python_modules/pillow-11.3.0.dist-info/licenses/LICENSE @@ -0,0 +1,30 @@ +The Python Imaging Library (PIL) is + + Copyright © 1997-2011 by Secret Labs AB + Copyright © 1995-2011 by Fredrik Lundh and contributors + +Pillow is the friendly PIL fork. It is + + Copyright © 2010 by Jeffrey A. Clark and contributors + +Like PIL, Pillow is licensed under the open source MIT-CMU License: + +By obtaining, using, and/or copying this software and/or its associated +documentation, you agree that you have read, understood, and will comply +with the following terms and conditions: + +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appears in all copies, and that +both that copyright notice and this permission notice appear in supporting +documentation, and that the name of Secret Labs AB or the author not be +used in advertising or publicity pertaining to distribution of the software +without specific, written prior permission. + +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, +INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/python_modules/pillow-11.3.0.dist-info/top_level.txt b/python_modules/pillow-11.3.0.dist-info/top_level.txt new file mode 100644 index 000000000..b338169ce --- /dev/null +++ b/python_modules/pillow-11.3.0.dist-info/top_level.txt @@ -0,0 +1 @@ +PIL diff --git a/python_modules/pillow-11.3.0.dist-info/zip-safe b/python_modules/pillow-11.3.0.dist-info/zip-safe new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/python_modules/pillow-11.3.0.dist-info/zip-safe @@ -0,0 +1 @@ + diff --git a/python_modules/pyvenv.cfg b/python_modules/pyvenv.cfg new file mode 100644 index 000000000..e69de29bb diff --git a/python_modules/qrcode-8.2.dist-info/INSTALLER b/python_modules/qrcode-8.2.dist-info/INSTALLER new file mode 100644 index 000000000..5c69047b2 --- /dev/null +++ b/python_modules/qrcode-8.2.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/python_modules/qrcode-8.2.dist-info/LICENSE b/python_modules/qrcode-8.2.dist-info/LICENSE new file mode 100644 index 000000000..bb4b0c708 --- /dev/null +++ b/python_modules/qrcode-8.2.dist-info/LICENSE @@ -0,0 +1,48 @@ +Copyright (c) 2011, Lincoln Loop +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the package name nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------------------- + + +Original text and license from the pyqrnative package where this was forked +from (http://code.google.com/p/pyqrnative): + +#Ported from the Javascript library by Sam Curren +# +#QRCode for Javascript +#http://d-project.googlecode.com/svn/trunk/misc/qrcode/js/qrcode.js +# +#Copyright (c) 2009 Kazuhiko Arase +# +#URL: http://www.d-project.com/ +# +#Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php +# +# The word "QR Code" is registered trademark of +# DENSO WAVE INCORPORATED +# http://www.denso-wave.com/qrcode/faqpatent-e.html diff --git a/python_modules/qrcode-8.2.dist-info/METADATA b/python_modules/qrcode-8.2.dist-info/METADATA new file mode 100644 index 000000000..3e84181b8 --- /dev/null +++ b/python_modules/qrcode-8.2.dist-info/METADATA @@ -0,0 +1,668 @@ +Metadata-Version: 2.1 +Name: qrcode +Version: 8.2 +Summary: QR Code image generator +Home-page: https://github.com/lincolnloop/python-qrcode +License: BSD +Keywords: qr,denso-wave,IEC18004 +Author: Lincoln Loop +Author-email: info@lincolnloop.com +Requires-Python: >=3.9,<4.0 +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: License :: Other/Proprietary License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.13 +Classifier: Topic :: Multimedia :: Graphics +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Provides-Extra: all +Provides-Extra: pil +Provides-Extra: png +Requires-Dist: colorama ; sys_platform == "win32" +Requires-Dist: pillow (>=9.1.0) ; extra == "pil" or extra == "all" +Requires-Dist: pypng ; extra == "png" or extra == "all" +Description-Content-Type: text/x-rst + +============================= +Pure python QR Code generator +============================= + +Generate QR codes. + +A standard install uses pypng_ to generate PNG files and can also render QR +codes directly to the console. A standard install is just:: + + pip install qrcode + +For more image functionality, install qrcode with the ``pil`` dependency so +that pillow_ is installed and can be used for generating images:: + + pip install "qrcode[pil]" + +.. _pypng: https://pypi.python.org/pypi/pypng +.. _pillow: https://pypi.python.org/pypi/Pillow + + +What is a QR Code? +================== + +A Quick Response code is a two-dimensional pictographic code used for its fast +readability and comparatively large storage capacity. The code consists of +black modules arranged in a square pattern on a white background. The +information encoded can be made up of any kind of data (e.g., binary, +alphanumeric, or Kanji symbols) + +Usage +===== + +From the command line, use the installed ``qr`` script:: + + qr "Some text" > test.png + +Or in Python, use the ``make`` shortcut function: + +.. code:: python + + import qrcode + img = qrcode.make('Some data here') + type(img) # qrcode.image.pil.PilImage + img.save("some_file.png") + +Advanced Usage +-------------- + +For more control, use the ``QRCode`` class. For example: + +.. code:: python + + import qrcode + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data('Some data') + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + +The ``version`` parameter is an integer from 1 to 40 that controls the size of +the QR Code (the smallest, version 1, is a 21x21 matrix). +Set to ``None`` and use the ``fit`` parameter when making the code to determine +this automatically. + +``fill_color`` and ``back_color`` can change the background and the painting +color of the QR, when using the default image factory. Both parameters accept +RGB color tuples. + +.. code:: python + + + img = qr.make_image(back_color=(255, 195, 235), fill_color=(55, 95, 35)) + +The ``error_correction`` parameter controls the error correction used for the +QR Code. The following four constants are made available on the ``qrcode`` +package: + +``ERROR_CORRECT_L`` + About 7% or less errors can be corrected. +``ERROR_CORRECT_M`` (default) + About 15% or less errors can be corrected. +``ERROR_CORRECT_Q`` + About 25% or less errors can be corrected. +``ERROR_CORRECT_H``. + About 30% or less errors can be corrected. + +The ``box_size`` parameter controls how many pixels each "box" of the QR code +is. + +The ``border`` parameter controls how many boxes thick the border should be +(the default is 4, which is the minimum according to the specs). + +Other image factories +===================== + +You can encode as SVG, or use a new pure Python image processor to encode to +PNG images. + +The Python examples below use the ``make`` shortcut. The same ``image_factory`` +keyword argument is a valid option for the ``QRCode`` class for more advanced +usage. + +SVG +--- + +You can create the entire SVG or an SVG fragment. When building an entire SVG +image, you can use the factory that combines as a path (recommended, and +default for the script) or a factory that creates a simple set of rectangles. + +From your command line:: + + qr --factory=svg-path "Some text" > test.svg + qr --factory=svg "Some text" > test.svg + qr --factory=svg-fragment "Some text" > test.svg + +Or in Python: + +.. code:: python + + import qrcode + import qrcode.image.svg + + if method == 'basic': + # Simple factory, just a set of rects. + factory = qrcode.image.svg.SvgImage + elif method == 'fragment': + # Fragment factory (also just a set of rects) + factory = qrcode.image.svg.SvgFragmentImage + else: + # Combined path factory, fixes white space that may occur when zooming + factory = qrcode.image.svg.SvgPathImage + + img = qrcode.make('Some data here', image_factory=factory) + +Two other related factories are available that work the same, but also fill the +background of the SVG with white:: + + qrcode.image.svg.SvgFillImage + qrcode.image.svg.SvgPathFillImage + +The ``QRCode.make_image()`` method forwards additional keyword arguments to the +underlying ElementTree XML library. This helps to fine tune the root element of +the resulting SVG: + +.. code:: python + + import qrcode + qr = qrcode.QRCode(image_factory=qrcode.image.svg.SvgPathImage) + qr.add_data('Some data') + qr.make(fit=True) + + img = qr.make_image(attrib={'class': 'some-css-class'}) + +You can convert the SVG image into strings using the ``to_string()`` method. +Additional keyword arguments are forwarded to ElementTrees ``tostring()``: + +.. code:: python + + img.to_string(encoding='unicode') + + +Pure Python PNG +--------------- + +If Pillow is not installed, the default image factory will be a pure Python PNG +encoder that uses `pypng`. + +You can use the factory explicitly from your command line:: + + qr --factory=png "Some text" > test.png + +Or in Python: + +.. code:: python + + import qrcode + from qrcode.image.pure import PyPNGImage + img = qrcode.make('Some data here', image_factory=PyPNGImage) + + +Styled Image +------------ + +Works only with versions_ >=7.2 (SVG styled images require 7.4). + +.. _versions: https://github.com/lincolnloop/python-qrcode/blob/master/CHANGES.rst#72-19-july-2021 + +To apply styles to the QRCode, use the ``StyledPilImage`` or one of the +standard SVG_ image factories. These accept an optional ``module_drawer`` +parameter to control the shape of the QR Code. + +These QR Codes are not guaranteed to work with all readers, so do some +experimentation and set the error correction to high (especially if embedding +an image). + +Other PIL module drawers: + + .. image:: doc/module_drawers.png + +For SVGs, use ``SvgSquareDrawer``, ``SvgCircleDrawer``, +``SvgPathSquareDrawer``, or ``SvgPathCircleDrawer``. + +These all accept a ``size_ratio`` argument which allows for "gapped" squares or +circles by reducing this less than the default of ``Decimal(1)``. + + +The ``StyledPilImage`` additionally accepts an optional ``color_mask`` +parameter to change the colors of the QR Code, and an optional +``embedded_image_path`` to embed an image in the center of the code. + +Other color masks: + + .. image:: doc/color_masks.png + +Here is a code example to draw a QR code with rounded corners, radial gradient +and an embedded image: + +.. code:: python + + import qrcode + from qrcode.image.styledpil import StyledPilImage + from qrcode.image.styles.moduledrawers.pil import RoundedModuleDrawer + from qrcode.image.styles.colormasks import RadialGradiantColorMask + + qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H) + qr.add_data('Some data') + + img_1 = qr.make_image(image_factory=StyledPilImage, module_drawer=RoundedModuleDrawer()) + img_2 = qr.make_image(image_factory=StyledPilImage, color_mask=RadialGradiantColorMask()) + img_3 = qr.make_image(image_factory=StyledPilImage, embedded_image_path="/path/to/image.png") + +Examples +======== + +Get the text content from `print_ascii`: + +.. code:: python + + import io + import qrcode + qr = qrcode.QRCode() + qr.add_data("Some text") + f = io.StringIO() + qr.print_ascii(out=f) + f.seek(0) + print(f.read()) + +The `add_data` method will append data to the current QR object. To add new data by replacing previous content in the same object, first use clear method: + +.. code:: python + + import qrcode + qr = qrcode.QRCode() + qr.add_data('Some data') + img = qr.make_image() + qr.clear() + qr.add_data('New data') + other_img = qr.make_image() + +Pipe ascii output to text file in command line:: + + qr --ascii "Some data" > "test.txt" + cat test.txt + +Alternative to piping output to file to avoid PowerShell issues:: + + # qr "Some data" > test.png + qr --output=test.png "Some data" + +========== +Change log +========== + +8.2 (01 May 2025) +================= + +- Optimize QRColorMask apply_mask method for enhanced performance +- Fix typos on StyledPilImage embeded_* parameters. + The old parameters with the typos are still accepted + for backward compatibility. + + +8.1 (02 April 2025) +==================== + +- Added support for Python 3.13. + +8.0 (27 September 2024) +======================== + +- Added support for Python 3.11 and 3.12. + +- Drop support for Python <=3.8. + +- Change local development setup to use Poetry_. + +- Testsuite and code quality checks are done through Github Actions. + +- Code quality and formatting utilises ruff_. + +- Removed ``typing_extensions`` as a dependency, as it's no longer required + with having Python 3.9+ as a requirement. + having Python 3.9+ as a requirement. + +- Only allow high error correction rate (`qrcode.ERROR_CORRECT_H`) + when generating + QR codes with embedded images to ensure content is readable + +.. _Poetry: https://python-poetry.org +.. _ruff: https://astral.sh/ruff + + +7.4.2 (6 February 2023) +======================= + +- Allow ``pypng`` factory to allow for saving to a string (like + ``qr.save("some_file.png")``) in addition to file-like objects. + + +7.4.1 (3 February 2023) +======================= + +- Fix bad over-optimization in v7.4 that broke large QR codes. Thanks to + mattiasj-axis! + + +7.4 (1 February 2023) +===================== + +- Restructure the factory drawers, allowing different shapes in SVG image + factories as well. + +- Add a ``--factory-drawer`` option to the ``qr`` console script. + +- Optimize the output for the ``SVGPathImage`` factory (more than 30% reduction + in file sizes). + +- Add a ``pypng`` image factory as a pure Python PNG solution. If ``pillow`` is + *not* installed, then this becomes the default factory. + +- The ``pymaging`` image factory has been removed, but its factory shortcut and + the actual PymagingImage factory class now just link to the PyPNGImage + factory. + + +7.3.1 (1 October 2021) +====================== + +- Improvements for embedded image. + + +7.3 (19 August 2021) +==================== + +- Skip color mask if QR is black and white + + +7.2 (19 July 2021) +================== + +- Add Styled PIL image factory, allowing different color masks and shapes in QR codes + +- Small performance inprovement + +- Add check for border size parameter + + +7.1 (1 July 2021) +================= + +- Add --ascii parameter to command line interface allowing to output ascii when stdout is piped + +- Add --output parameter to command line interface to specify output file + +- Accept RGB tuples in fill_color and back_color + +- Add to_string method to SVG images + +- Replace inline styles with SVG attributes to avoid CSP issues + +- Add Python3.10 to supported versions + + +7.0 (29 June 2021) +================== + +- Drop Python < 3.6 support. + + +6.1 (14 January 2019) +===================== + +- Fix short chunks of data not being optimized to the correct mode. + +- Tests fixed for Python 3 + + +6.0 (23 March 2018) +=================== + +- Fix optimize length being ignored in ``QRCode.add_data``. + +- Better calculation of the best mask pattern and related optimizations. Big + thanks to cryptogun! + + +5.3 (18 May 2016) +================= + +* Fix incomplete block table for QR version 15. Thanks Rodrigo Queiro for the + report and Jacob Welsh for the investigation and fix. + +* Avoid unnecessary dependency for non MS platforms, thanks to Noah Vesely. + +* Make ``BaseImage.get_image()`` actually work. + + +5.2 (25 Jan 2016) +================= + +* Add ``--error-correction`` option to qr script. + +* Fix script piping to stdout in Python 3 and reading non-UTF-8 characters in + Python 3. + +* Fix script piping in Windows. + +* Add some useful behind-the-curtain methods for tinkerers. + +* Fix terminal output when using Python 2.6 + +* Fix terminal output to display correctly on MS command line. + +5.2.1 +----- + +* Small fix to terminal output in Python 3 (and fix tests) + +5.2.2 +----- + +* Revert some terminal changes from 5.2 that broke Python 3's real life tty + code generation and introduce a better way from Jacob Welsh. + + +5.1 (22 Oct 2014) +================= + +* Make ``qr`` script work in Windows. Thanks Ionel Cristian Mărieș + +* Fixed print_ascii function in Python 3. + +* Out-of-bounds code version numbers are handled more consistently with a + ValueError. + +* Much better test coverage (now only officially supporting Python 2.6+) + + +5.0 (17 Jun 2014) +================= + +* Speed optimizations. + +* Change the output when using the ``qr`` script to use ASCII rather than + just colors, better using the terminal real estate. + +* Fix a bug in passing bytecode data directly when in Python 3. + +* Substation speed optimizations to best-fit algorithm (thanks Jacob Welsh!). + +* Introduce a ``print_ascii`` method and use it as the default for the ``qr`` + script rather than ``print_tty``. + +5.0.1 +----- + +* Update version numbers correctly. + + +4.0 (4 Sep 2013) +================ + +* Made qrcode work on Python 2.4 - Thanks tcely. + Note: officially, qrcode only supports 2.5+. + +* Support pure-python PNG generation (via pymaging) for Python 2.6+ -- thanks + Adam Wisniewski! + +* SVG image generation now supports alternate sizing (the default box size of + 10 == 1mm per rectangle). + +* SVG path image generation allows cleaner SVG output by combining all QR rects + into a single path. Thank you, Viktor Stískala. + +* Added some extra simple SVG factories that fill the background white. + +4.0.1 +----- + +* Fix the pymaging backend not able to save the image to a buffer. Thanks ilj! + +4.0.2 +----- + +* Fix incorrect regex causing a comma to be considered part of the alphanumeric + set. + +* Switch to using setuptools for setup.py. + +4.0.3 +----- + +* Fix bad QR code generation due to the regex comma fix in version 4.0.2. + +4.0.4 +----- + +* Bad version number for previous hotfix release. + + +3.1 (12 Aug 2013) +================= + +* Important fixes for incorrect matches of the alphanumeric encoding mode. + Previously, the pattern would match if a single line was alphanumeric only + (even if others wern't). Also, the two characters ``{`` and ``}`` had snuck + in as valid characters. Thanks to Eran Tromer for the report and fix. + +* Optimized chunking -- if the parts of the data stream can be encoded more + efficiently, the data will be split into chunks of the most efficient modes. + +3.1.1 +----- + +* Update change log to contain version 3.1 changes. :P + +* Give the ``qr`` script an ``--optimize`` argument to control the chunk + optimization setting. + + +3.0 (25 Jun 2013) +================= + +* Python 3 support. + +* Add QRCode.get_matrix, an easy way to get the matrix array of a QR code + including the border. Thanks Hugh Rawlinson. + +* Add in a workaround so that Python 2.6 users can use SVG generation (they + must install ``lxml``). + +* Some initial tests! And tox support (``pip install tox``) for testing across + Python platforms. + + +2.7 (5 Mar 2013) +================ + +* Fix incorrect termination padding. + + +2.6 (2 Apr 2013) +================ + +* Fix the first four columns incorrectly shifted by one. Thanks to Josep + Gómez-Suay for the report and fix. + +* Fix strings within 4 bits of the QR version limit being incorrectly + terminated. Thanks to zhjie231 for the report. + + +2.5 (12 Mar 2013) +================= + +* The PilImage wrapper is more transparent - you can use any methods or + attributes available to the underlying PIL Image instance. + +* Fixed the first column of the QR Code coming up empty! Thanks to BecoKo. + +2.5.1 +----- + +* Fix installation error on Windows. + + +2.4 (23 Apr 2012) +================= + +* Use a pluggable backend system for generating images, thanks to Branko Čibej! + Comes with PIL and SVG backends built in. + +2.4.1 +----- + +* Fix a packaging issue + +2.4.2 +----- + +* Added a ``show`` method to the PIL image wrapper so the ``run_example`` + function actually works. + + +2.3 (29 Jan 2012) +================= + +* When adding data, auto-select the more efficient encoding methods for numbers + and alphanumeric data (KANJI still not supported). + +2.3.1 +----- + +* Encode unicode to utf-8 bytestrings when adding data to a QRCode. + + +2.2 (18 Jan 2012) +================= + +* Fixed tty output to work on both white and black backgrounds. + +* Added `border` parameter to allow customizing of the number of boxes used to + create the border of the QR code + + +2.1 (17 Jan 2012) +================= + +* Added a ``qr`` script which can be used to output a qr code to the tty using + background colors, or to a file via a pipe. + diff --git a/python_modules/qrcode-8.2.dist-info/RECORD b/python_modules/qrcode-8.2.dist-info/RECORD new file mode 100644 index 000000000..e235a1e4a --- /dev/null +++ b/python_modules/qrcode-8.2.dist-info/RECORD @@ -0,0 +1,42 @@ +../../Scripts/qr.exe,sha256=2LA_6m7Th5Ne3B9vQbJ2uaf0VYWHGqwFdCULvY0lJkQ,46080 +qrcode-8.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +qrcode-8.2.dist-info/LICENSE,sha256=QN-5A8lO4_eJUAExMRGGVI7Lpc79NVdiPXcA4lIquZQ,2143 +qrcode-8.2.dist-info/METADATA,sha256=Oo8b5tqUKLl4BiktBeMUgmS5BTwi55iUkYtnDpMK_DY,17686 +qrcode-8.2.dist-info/RECORD,, +qrcode-8.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +qrcode-8.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88 +qrcode-8.2.dist-info/entry_points.txt,sha256=jokYBrUZ_Sf1bO7FcE53iIhHYn1CJ9_a5SohTIayOP8,50 +qrcode/LUT.py,sha256=NjXKPfHSTFYoLlGkXhFjf2OUq_EGD6mrdyYHIG3dNck,3599 +qrcode/__init__.py,sha256=0C8jx3gDHSJ4yydlHN01ytyipNh2pMO3VYS9Dk-m4oU,645 +qrcode/base.py,sha256=9J_1LynF5dXJK14Azs8XyHJY66FfTluYJ66F8ZjeStY,7288 +qrcode/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +qrcode/compat/etree.py,sha256=rEyWRA9QMsVFva_9rOdth3RAkRpFOmkF59c2EQM44gE,152 +qrcode/compat/png.py,sha256=OCe5WsuiTI_UTqmyVqLbcJloSbZvgCYJdMCznKvMCCM,171 +qrcode/console_scripts.py,sha256=n-bQ5vpKtcjG30l1jkQ_q22HTV4X-JEMwkLRdJno8vc,5558 +qrcode/constants.py,sha256=0Csa8YYdeQ8NaFrRmt43maVg12O89d-oKgiKAVIO2s4,106 +qrcode/exceptions.py,sha256=L2fZuYOKscvdn72ra-wF8Gwsr2ZB9eRZWrp1f0IDx4E,45 +qrcode/image/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +qrcode/image/base.py,sha256=jCrbt4UD1ZfOC8jMFjK3elZfgUJ7M_FsHKRMVvje-BE,4965 +qrcode/image/pil.py,sha256=y5a3t6VB4gnvsTK4eSR04YEVP_Qk82iT1NXL6jFP1jQ,1589 +qrcode/image/pure.py,sha256=B8PJANvAHPyd1DaBtAC83Csb2xKbZ-fP4SSWuw1NNvU,1525 +qrcode/image/styledpil.py,sha256=RC7JoDS-Uzez2nN-I2xwePsGX3qYDeHg8YepT2FbO_M,4951 +qrcode/image/styles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +qrcode/image/styles/colormasks.py,sha256=h8asIvQKMTRuq6bFzvaiZgFyoa-2tqvIb_hB5H5XwM0,7936 +qrcode/image/styles/moduledrawers/__init__.py,sha256=Mklw5SjYiGbs2Aym38jwwrKt0plJGzwIVgZ--jiOVBc,430 +qrcode/image/styles/moduledrawers/base.py,sha256=gLFq20p07tBEmsFfirEM19XZshYsaNP2Wr1UNAkJq90,1019 +qrcode/image/styles/moduledrawers/pil.py,sha256=lkT8I8q8PUB_TdYBrP5DlzKN8UtQW-XQEYqoXBWkD7Y,9773 +qrcode/image/styles/moduledrawers/svg.py,sha256=-WngEvZF8LwtTpWmSdt0tDZ6dKtYzLctYDNY-Mi7crc,3936 +qrcode/image/svg.py,sha256=G2dmuybVP3fwkgyrFF5RfQT3dpWDCYl80OKe2Xal8gU,5188 +qrcode/main.py,sha256=OF7uHDAz2Tihpe6Fftef6fiVH2tpoVJ5ekLCIG4lyJA,16869 +qrcode/release.py,sha256=wJjVEklWnATUh8CU88HEKyhUgZU9hzpl__SZYyyNUZo,1080 +qrcode/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +qrcode/tests/consts.py,sha256=Tn2AbI9zTEi_KiAish6f74AbBWrZg8A-Js8jTrN_vF0,96 +qrcode/tests/test_example.py,sha256=z5p5Tnumnj0EWsKo6YJ4vuUQM7KjisvgLwbJt8wTAG0,244 +qrcode/tests/test_qrcode.py,sha256=FPjfdmLAXa0lrbspQXbtD1wPKMggCOPN_O4tT-jLJug,6461 +qrcode/tests/test_qrcode_pil.py,sha256=m12SfImnfgqzvKmdR4mMQCSVb9GyNnNJ4Edxwjm7tus,5148 +qrcode/tests/test_qrcode_pypng.py,sha256=fgTr78vX1T_cH03mS_ikHJgoITKLU11bJMXEd0Um5yA,944 +qrcode/tests/test_qrcode_svg.py,sha256=21enlZjXNUm0M_ZoK_AMp0UE4mELoCeIWE6UveQsJwk,1296 +qrcode/tests/test_release.py,sha256=SVCXUx4BeNz_d3osbBNdW2N3rstbErGvY5N2V_kHrhc,1341 +qrcode/tests/test_script.py,sha256=1gchpke_DKZLEhtN5cZJBZTT28fhbjvXfAPttvvM5tA,2908 +qrcode/tests/test_util.py,sha256=Pgnp1DRFe44YRH9deR9_9Nb9zH-ska61CIYkVWwuy9A,207 +qrcode/util.py,sha256=VOG4RrJ6QkPs0fLaZkfeMwxwxdIpKsRHm6dXvjo9Yl4,17103 diff --git a/python_modules/qrcode-8.2.dist-info/REQUESTED b/python_modules/qrcode-8.2.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/python_modules/qrcode-8.2.dist-info/WHEEL b/python_modules/qrcode-8.2.dist-info/WHEEL new file mode 100644 index 000000000..d73ccaae8 --- /dev/null +++ b/python_modules/qrcode-8.2.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: poetry-core 1.9.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/python_modules/qrcode-8.2.dist-info/entry_points.txt b/python_modules/qrcode-8.2.dist-info/entry_points.txt new file mode 100644 index 000000000..66dc3c52f --- /dev/null +++ b/python_modules/qrcode-8.2.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +qr=qrcode.console_scripts:main + diff --git a/python_modules/qrcode/LUT.py b/python_modules/qrcode/LUT.py new file mode 100644 index 000000000..115892f13 --- /dev/null +++ b/python_modules/qrcode/LUT.py @@ -0,0 +1,223 @@ +# Store all kinds of lookup table. + + +# # generate rsPoly lookup table. + +# from qrcode import base + +# def create_bytes(rs_blocks): +# for r in range(len(rs_blocks)): +# dcCount = rs_blocks[r].data_count +# ecCount = rs_blocks[r].total_count - dcCount +# rsPoly = base.Polynomial([1], 0) +# for i in range(ecCount): +# rsPoly = rsPoly * base.Polynomial([1, base.gexp(i)], 0) +# return ecCount, rsPoly + +# rsPoly_LUT = {} +# for version in range(1,41): +# for error_correction in range(4): +# rs_blocks_list = base.rs_blocks(version, error_correction) +# ecCount, rsPoly = create_bytes(rs_blocks_list) +# rsPoly_LUT[ecCount]=rsPoly.num +# print(rsPoly_LUT) + +# Result. Usage: input: ecCount, output: Polynomial.num +# e.g. rsPoly = base.Polynomial(LUT.rsPoly_LUT[ecCount], 0) +rsPoly_LUT = { + 7: [1, 127, 122, 154, 164, 11, 68, 117], + 10: [1, 216, 194, 159, 111, 199, 94, 95, 113, 157, 193], + 13: [1, 137, 73, 227, 17, 177, 17, 52, 13, 46, 43, 83, 132, 120], + 15: [1, 29, 196, 111, 163, 112, 74, 10, 105, 105, 139, 132, 151, 32, 134, 26], + 16: [1, 59, 13, 104, 189, 68, 209, 30, 8, 163, 65, 41, 229, 98, 50, 36, 59], + 17: [1, 119, 66, 83, 120, 119, 22, 197, 83, 249, 41, 143, 134, 85, 53, 125, 99, 79], + 18: [ + 1, + 239, + 251, + 183, + 113, + 149, + 175, + 199, + 215, + 240, + 220, + 73, + 82, + 173, + 75, + 32, + 67, + 217, + 146, + ], + 20: [ + 1, + 152, + 185, + 240, + 5, + 111, + 99, + 6, + 220, + 112, + 150, + 69, + 36, + 187, + 22, + 228, + 198, + 121, + 121, + 165, + 174, + ], + 22: [ + 1, + 89, + 179, + 131, + 176, + 182, + 244, + 19, + 189, + 69, + 40, + 28, + 137, + 29, + 123, + 67, + 253, + 86, + 218, + 230, + 26, + 145, + 245, + ], + 24: [ + 1, + 122, + 118, + 169, + 70, + 178, + 237, + 216, + 102, + 115, + 150, + 229, + 73, + 130, + 72, + 61, + 43, + 206, + 1, + 237, + 247, + 127, + 217, + 144, + 117, + ], + 26: [ + 1, + 246, + 51, + 183, + 4, + 136, + 98, + 199, + 152, + 77, + 56, + 206, + 24, + 145, + 40, + 209, + 117, + 233, + 42, + 135, + 68, + 70, + 144, + 146, + 77, + 43, + 94, + ], + 28: [ + 1, + 252, + 9, + 28, + 13, + 18, + 251, + 208, + 150, + 103, + 174, + 100, + 41, + 167, + 12, + 247, + 56, + 117, + 119, + 233, + 127, + 181, + 100, + 121, + 147, + 176, + 74, + 58, + 197, + ], + 30: [ + 1, + 212, + 246, + 77, + 73, + 195, + 192, + 75, + 98, + 5, + 70, + 103, + 177, + 22, + 217, + 138, + 51, + 181, + 246, + 72, + 25, + 18, + 46, + 228, + 74, + 216, + 195, + 11, + 106, + 130, + 150, + ], +} diff --git a/python_modules/qrcode/__init__.py b/python_modules/qrcode/__init__.py new file mode 100644 index 000000000..6b238d33e --- /dev/null +++ b/python_modules/qrcode/__init__.py @@ -0,0 +1,30 @@ +from qrcode.main import QRCode +from qrcode.main import make # noqa +from qrcode.constants import ( # noqa + ERROR_CORRECT_L, + ERROR_CORRECT_M, + ERROR_CORRECT_Q, + ERROR_CORRECT_H, +) + +from qrcode import image # noqa + + +def run_example(data="http://www.lincolnloop.com", *args, **kwargs): + """ + Build an example QR Code and display it. + + There's an even easier way than the code here though: just use the ``make`` + shortcut. + """ + qr = QRCode(*args, **kwargs) + qr.add_data(data) + + im = qr.make_image() + im.show() + + +if __name__ == "__main__": # pragma: no cover + import sys + + run_example(*sys.argv[1:]) diff --git a/python_modules/qrcode/base.py b/python_modules/qrcode/base.py new file mode 100644 index 000000000..20f81f6ff --- /dev/null +++ b/python_modules/qrcode/base.py @@ -0,0 +1,313 @@ +from typing import NamedTuple +from qrcode import constants + +EXP_TABLE = list(range(256)) + +LOG_TABLE = list(range(256)) + +for i in range(8): + EXP_TABLE[i] = 1 << i + +for i in range(8, 256): + EXP_TABLE[i] = ( + EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^ EXP_TABLE[i - 8] + ) + +for i in range(255): + LOG_TABLE[EXP_TABLE[i]] = i + +RS_BLOCK_OFFSET = { + constants.ERROR_CORRECT_L: 0, + constants.ERROR_CORRECT_M: 1, + constants.ERROR_CORRECT_Q: 2, + constants.ERROR_CORRECT_H: 3, +} + +RS_BLOCK_TABLE = ( + # L + # M + # Q + # H + # 1 + (1, 26, 19), + (1, 26, 16), + (1, 26, 13), + (1, 26, 9), + # 2 + (1, 44, 34), + (1, 44, 28), + (1, 44, 22), + (1, 44, 16), + # 3 + (1, 70, 55), + (1, 70, 44), + (2, 35, 17), + (2, 35, 13), + # 4 + (1, 100, 80), + (2, 50, 32), + (2, 50, 24), + (4, 25, 9), + # 5 + (1, 134, 108), + (2, 67, 43), + (2, 33, 15, 2, 34, 16), + (2, 33, 11, 2, 34, 12), + # 6 + (2, 86, 68), + (4, 43, 27), + (4, 43, 19), + (4, 43, 15), + # 7 + (2, 98, 78), + (4, 49, 31), + (2, 32, 14, 4, 33, 15), + (4, 39, 13, 1, 40, 14), + # 8 + (2, 121, 97), + (2, 60, 38, 2, 61, 39), + (4, 40, 18, 2, 41, 19), + (4, 40, 14, 2, 41, 15), + # 9 + (2, 146, 116), + (3, 58, 36, 2, 59, 37), + (4, 36, 16, 4, 37, 17), + (4, 36, 12, 4, 37, 13), + # 10 + (2, 86, 68, 2, 87, 69), + (4, 69, 43, 1, 70, 44), + (6, 43, 19, 2, 44, 20), + (6, 43, 15, 2, 44, 16), + # 11 + (4, 101, 81), + (1, 80, 50, 4, 81, 51), + (4, 50, 22, 4, 51, 23), + (3, 36, 12, 8, 37, 13), + # 12 + (2, 116, 92, 2, 117, 93), + (6, 58, 36, 2, 59, 37), + (4, 46, 20, 6, 47, 21), + (7, 42, 14, 4, 43, 15), + # 13 + (4, 133, 107), + (8, 59, 37, 1, 60, 38), + (8, 44, 20, 4, 45, 21), + (12, 33, 11, 4, 34, 12), + # 14 + (3, 145, 115, 1, 146, 116), + (4, 64, 40, 5, 65, 41), + (11, 36, 16, 5, 37, 17), + (11, 36, 12, 5, 37, 13), + # 15 + (5, 109, 87, 1, 110, 88), + (5, 65, 41, 5, 66, 42), + (5, 54, 24, 7, 55, 25), + (11, 36, 12, 7, 37, 13), + # 16 + (5, 122, 98, 1, 123, 99), + (7, 73, 45, 3, 74, 46), + (15, 43, 19, 2, 44, 20), + (3, 45, 15, 13, 46, 16), + # 17 + (1, 135, 107, 5, 136, 108), + (10, 74, 46, 1, 75, 47), + (1, 50, 22, 15, 51, 23), + (2, 42, 14, 17, 43, 15), + # 18 + (5, 150, 120, 1, 151, 121), + (9, 69, 43, 4, 70, 44), + (17, 50, 22, 1, 51, 23), + (2, 42, 14, 19, 43, 15), + # 19 + (3, 141, 113, 4, 142, 114), + (3, 70, 44, 11, 71, 45), + (17, 47, 21, 4, 48, 22), + (9, 39, 13, 16, 40, 14), + # 20 + (3, 135, 107, 5, 136, 108), + (3, 67, 41, 13, 68, 42), + (15, 54, 24, 5, 55, 25), + (15, 43, 15, 10, 44, 16), + # 21 + (4, 144, 116, 4, 145, 117), + (17, 68, 42), + (17, 50, 22, 6, 51, 23), + (19, 46, 16, 6, 47, 17), + # 22 + (2, 139, 111, 7, 140, 112), + (17, 74, 46), + (7, 54, 24, 16, 55, 25), + (34, 37, 13), + # 23 + (4, 151, 121, 5, 152, 122), + (4, 75, 47, 14, 76, 48), + (11, 54, 24, 14, 55, 25), + (16, 45, 15, 14, 46, 16), + # 24 + (6, 147, 117, 4, 148, 118), + (6, 73, 45, 14, 74, 46), + (11, 54, 24, 16, 55, 25), + (30, 46, 16, 2, 47, 17), + # 25 + (8, 132, 106, 4, 133, 107), + (8, 75, 47, 13, 76, 48), + (7, 54, 24, 22, 55, 25), + (22, 45, 15, 13, 46, 16), + # 26 + (10, 142, 114, 2, 143, 115), + (19, 74, 46, 4, 75, 47), + (28, 50, 22, 6, 51, 23), + (33, 46, 16, 4, 47, 17), + # 27 + (8, 152, 122, 4, 153, 123), + (22, 73, 45, 3, 74, 46), + (8, 53, 23, 26, 54, 24), + (12, 45, 15, 28, 46, 16), + # 28 + (3, 147, 117, 10, 148, 118), + (3, 73, 45, 23, 74, 46), + (4, 54, 24, 31, 55, 25), + (11, 45, 15, 31, 46, 16), + # 29 + (7, 146, 116, 7, 147, 117), + (21, 73, 45, 7, 74, 46), + (1, 53, 23, 37, 54, 24), + (19, 45, 15, 26, 46, 16), + # 30 + (5, 145, 115, 10, 146, 116), + (19, 75, 47, 10, 76, 48), + (15, 54, 24, 25, 55, 25), + (23, 45, 15, 25, 46, 16), + # 31 + (13, 145, 115, 3, 146, 116), + (2, 74, 46, 29, 75, 47), + (42, 54, 24, 1, 55, 25), + (23, 45, 15, 28, 46, 16), + # 32 + (17, 145, 115), + (10, 74, 46, 23, 75, 47), + (10, 54, 24, 35, 55, 25), + (19, 45, 15, 35, 46, 16), + # 33 + (17, 145, 115, 1, 146, 116), + (14, 74, 46, 21, 75, 47), + (29, 54, 24, 19, 55, 25), + (11, 45, 15, 46, 46, 16), + # 34 + (13, 145, 115, 6, 146, 116), + (14, 74, 46, 23, 75, 47), + (44, 54, 24, 7, 55, 25), + (59, 46, 16, 1, 47, 17), + # 35 + (12, 151, 121, 7, 152, 122), + (12, 75, 47, 26, 76, 48), + (39, 54, 24, 14, 55, 25), + (22, 45, 15, 41, 46, 16), + # 36 + (6, 151, 121, 14, 152, 122), + (6, 75, 47, 34, 76, 48), + (46, 54, 24, 10, 55, 25), + (2, 45, 15, 64, 46, 16), + # 37 + (17, 152, 122, 4, 153, 123), + (29, 74, 46, 14, 75, 47), + (49, 54, 24, 10, 55, 25), + (24, 45, 15, 46, 46, 16), + # 38 + (4, 152, 122, 18, 153, 123), + (13, 74, 46, 32, 75, 47), + (48, 54, 24, 14, 55, 25), + (42, 45, 15, 32, 46, 16), + # 39 + (20, 147, 117, 4, 148, 118), + (40, 75, 47, 7, 76, 48), + (43, 54, 24, 22, 55, 25), + (10, 45, 15, 67, 46, 16), + # 40 + (19, 148, 118, 6, 149, 119), + (18, 75, 47, 31, 76, 48), + (34, 54, 24, 34, 55, 25), + (20, 45, 15, 61, 46, 16), +) + + +def glog(n): + if n < 1: # pragma: no cover + raise ValueError(f"glog({n})") + return LOG_TABLE[n] + + +def gexp(n): + return EXP_TABLE[n % 255] + + +class Polynomial: + def __init__(self, num, shift): + if not num: # pragma: no cover + raise Exception(f"{len(num)}/{shift}") + + offset = 0 + for offset in range(len(num)): + if num[offset] != 0: + break + + self.num = num[offset:] + [0] * shift + + def __getitem__(self, index): + return self.num[index] + + def __iter__(self): + return iter(self.num) + + def __len__(self): + return len(self.num) + + def __mul__(self, other): + num = [0] * (len(self) + len(other) - 1) + + for i, item in enumerate(self): + for j, other_item in enumerate(other): + num[i + j] ^= gexp(glog(item) + glog(other_item)) + + return Polynomial(num, 0) + + def __mod__(self, other): + difference = len(self) - len(other) + if difference < 0: + return self + + ratio = glog(self[0]) - glog(other[0]) + + num = [ + item ^ gexp(glog(other_item) + ratio) + for item, other_item in zip(self, other) + ] + if difference: + num.extend(self[-difference:]) + + # recursive call + return Polynomial(num, 0) % other + + +class RSBlock(NamedTuple): + total_count: int + data_count: int + + +def rs_blocks(version, error_correction): + if error_correction not in RS_BLOCK_OFFSET: # pragma: no cover + raise Exception( + "bad rs block @ version: %s / error_correction: %s" + % (version, error_correction) + ) + offset = RS_BLOCK_OFFSET[error_correction] + rs_block = RS_BLOCK_TABLE[(version - 1) * 4 + offset] + + blocks = [] + + for i in range(0, len(rs_block), 3): + count, total_count, data_count = rs_block[i : i + 3] + for _ in range(count): + blocks.append(RSBlock(total_count, data_count)) + + return blocks diff --git a/python_modules/qrcode/compat/__init__.py b/python_modules/qrcode/compat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python_modules/qrcode/compat/etree.py b/python_modules/qrcode/compat/etree.py new file mode 100644 index 000000000..6739d2271 --- /dev/null +++ b/python_modules/qrcode/compat/etree.py @@ -0,0 +1,4 @@ +try: + import lxml.etree as ET # type: ignore # noqa: F401 +except ImportError: + import xml.etree.ElementTree as ET # type: ignore # noqa: F401 diff --git a/python_modules/qrcode/compat/png.py b/python_modules/qrcode/compat/png.py new file mode 100644 index 000000000..8d7b90566 --- /dev/null +++ b/python_modules/qrcode/compat/png.py @@ -0,0 +1,7 @@ +# Try to import png library. +PngWriter = None + +try: + from png import Writer as PngWriter # type: ignore # noqa: F401 +except ImportError: # pragma: no cover + pass diff --git a/python_modules/qrcode/console_scripts.py b/python_modules/qrcode/console_scripts.py new file mode 100644 index 000000000..ebe8810f8 --- /dev/null +++ b/python_modules/qrcode/console_scripts.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +""" +qr - Convert stdin (or the first argument) to a QR Code. + +When stdout is a tty the QR Code is printed to the terminal and when stdout is +a pipe to a file an image is written. The default image format is PNG. +""" + +import optparse +import os +import sys +from typing import NoReturn, Optional +from collections.abc import Iterable +from importlib import metadata + +import qrcode +from qrcode.image.base import BaseImage, DrawerAliases + +# The next block is added to get the terminal to display properly on MS platforms +if sys.platform.startswith(("win", "cygwin")): # pragma: no cover + import colorama # type: ignore + + colorama.init() + +default_factories = { + "pil": "qrcode.image.pil.PilImage", + "png": "qrcode.image.pure.PyPNGImage", + "svg": "qrcode.image.svg.SvgImage", + "svg-fragment": "qrcode.image.svg.SvgFragmentImage", + "svg-path": "qrcode.image.svg.SvgPathImage", + # Keeping for backwards compatibility: + "pymaging": "qrcode.image.pure.PymagingImage", +} + +error_correction = { + "L": qrcode.ERROR_CORRECT_L, + "M": qrcode.ERROR_CORRECT_M, + "Q": qrcode.ERROR_CORRECT_Q, + "H": qrcode.ERROR_CORRECT_H, +} + + +def main(args=None): + if args is None: + args = sys.argv[1:] + + version = metadata.version("qrcode") + parser = optparse.OptionParser(usage=(__doc__ or "").strip(), version=version) + + # Wrap parser.error in a typed NoReturn method for better typing. + def raise_error(msg: str) -> NoReturn: + parser.error(msg) + raise # pragma: no cover + + parser.add_option( + "--factory", + help="Full python path to the image factory class to " + "create the image with. You can use the following shortcuts to the " + f"built-in image factory classes: {commas(default_factories)}.", + ) + parser.add_option( + "--factory-drawer", + help=f"Use an alternate drawer. {get_drawer_help()}.", + ) + parser.add_option( + "--optimize", + type=int, + help="Optimize the data by looking for chunks " + "of at least this many characters that could use a more efficient " + "encoding method. Use 0 to turn off chunk optimization.", + ) + parser.add_option( + "--error-correction", + type="choice", + choices=sorted(error_correction.keys()), + default="M", + help="The error correction level to use. Choices are L (7%), " + "M (15%, default), Q (25%), and H (30%).", + ) + parser.add_option( + "--ascii", help="Print as ascii even if stdout is piped.", action="store_true" + ) + parser.add_option( + "--output", + help="The output file. If not specified, the image is sent to " + "the standard output.", + ) + + opts, args = parser.parse_args(args) + + if opts.factory: + module = default_factories.get(opts.factory, opts.factory) + try: + image_factory = get_factory(module) + except ValueError as e: + raise_error(str(e)) + else: + image_factory = None + + qr = qrcode.QRCode( + error_correction=error_correction[opts.error_correction], + image_factory=image_factory, + ) + + if args: + data = args[0] + data = data.encode(errors="surrogateescape") + else: + data = sys.stdin.buffer.read() + if opts.optimize is None: + qr.add_data(data) + else: + qr.add_data(data, optimize=opts.optimize) + + if opts.output: + img = qr.make_image() + with open(opts.output, "wb") as out: + img.save(out) + else: + if image_factory is None and (os.isatty(sys.stdout.fileno()) or opts.ascii): + qr.print_ascii(tty=not opts.ascii) + return + + kwargs = {} + aliases: Optional[DrawerAliases] = getattr( + qr.image_factory, "drawer_aliases", None + ) + if opts.factory_drawer: + if not aliases: + raise_error("The selected factory has no drawer aliases.") + if opts.factory_drawer not in aliases: + raise_error( + f"{opts.factory_drawer} factory drawer not found." + f" Expected {commas(aliases)}" + ) + drawer_cls, drawer_kwargs = aliases[opts.factory_drawer] + kwargs["module_drawer"] = drawer_cls(**drawer_kwargs) + img = qr.make_image(**kwargs) + + sys.stdout.flush() + img.save(sys.stdout.buffer) + + +def get_factory(module: str) -> type[BaseImage]: + if "." not in module: + raise ValueError("The image factory is not a full python path") + module, name = module.rsplit(".", 1) + imp = __import__(module, {}, {}, [name]) + return getattr(imp, name) + + +def get_drawer_help() -> str: + help: dict[str, set] = {} + for alias, module in default_factories.items(): + try: + image = get_factory(module) + except ImportError: # pragma: no cover + continue + aliases: Optional[DrawerAliases] = getattr(image, "drawer_aliases", None) + if not aliases: + continue + factories = help.setdefault(commas(aliases), set()) + factories.add(alias) + + return ". ".join( + f"For {commas(factories, 'and')}, use: {aliases}" + for aliases, factories in help.items() + ) + + +def commas(items: Iterable[str], joiner="or") -> str: + items = tuple(items) + if not items: + return "" + if len(items) == 1: + return items[0] + return f"{', '.join(items[:-1])} {joiner} {items[-1]}" + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/python_modules/qrcode/constants.py b/python_modules/qrcode/constants.py new file mode 100644 index 000000000..385dda088 --- /dev/null +++ b/python_modules/qrcode/constants.py @@ -0,0 +1,5 @@ +# QR error correct levels +ERROR_CORRECT_L = 1 +ERROR_CORRECT_M = 0 +ERROR_CORRECT_Q = 3 +ERROR_CORRECT_H = 2 diff --git a/python_modules/qrcode/exceptions.py b/python_modules/qrcode/exceptions.py new file mode 100644 index 000000000..b37bd30c3 --- /dev/null +++ b/python_modules/qrcode/exceptions.py @@ -0,0 +1,2 @@ +class DataOverflowError(Exception): + pass diff --git a/python_modules/qrcode/image/__init__.py b/python_modules/qrcode/image/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python_modules/qrcode/image/base.py b/python_modules/qrcode/image/base.py new file mode 100644 index 000000000..119c30a67 --- /dev/null +++ b/python_modules/qrcode/image/base.py @@ -0,0 +1,164 @@ +import abc +from typing import TYPE_CHECKING, Any, Optional, Union + +from qrcode.image.styles.moduledrawers.base import QRModuleDrawer + +if TYPE_CHECKING: + from qrcode.main import ActiveWithNeighbors, QRCode + + +DrawerAliases = dict[str, tuple[type[QRModuleDrawer], dict[str, Any]]] + + +class BaseImage: + """ + Base QRCode image output class. + """ + + kind: Optional[str] = None + allowed_kinds: Optional[tuple[str]] = None + needs_context = False + needs_processing = False + needs_drawrect = True + + def __init__(self, border, width, box_size, *args, **kwargs): + self.border = border + self.width = width + self.box_size = box_size + self.pixel_size = (self.width + self.border * 2) * self.box_size + self.modules = kwargs.pop("qrcode_modules") + self._img = self.new_image(**kwargs) + self.init_new_image() + + @abc.abstractmethod + def drawrect(self, row, col): + """ + Draw a single rectangle of the QR code. + """ + + def drawrect_context(self, row: int, col: int, qr: "QRCode"): + """ + Draw a single rectangle of the QR code given the surrounding context + """ + raise NotImplementedError("BaseImage.drawrect_context") # pragma: no cover + + def process(self): + """ + Processes QR code after completion + """ + raise NotImplementedError("BaseImage.drawimage") # pragma: no cover + + @abc.abstractmethod + def save(self, stream, kind=None): + """ + Save the image file. + """ + + def pixel_box(self, row, col): + """ + A helper method for pixel-based image generators that specifies the + four pixel coordinates for a single rect. + """ + x = (col + self.border) * self.box_size + y = (row + self.border) * self.box_size + return ( + (x, y), + (x + self.box_size - 1, y + self.box_size - 1), + ) + + @abc.abstractmethod + def new_image(self, **kwargs) -> Any: + """ + Build the image class. Subclasses should return the class created. + """ + + def init_new_image(self): + pass + + def get_image(self, **kwargs): + """ + Return the image class for further processing. + """ + return self._img + + def check_kind(self, kind, transform=None): + """ + Get the image type. + """ + if kind is None: + kind = self.kind + allowed = not self.allowed_kinds or kind in self.allowed_kinds + if transform: + kind = transform(kind) + if not allowed: + allowed = kind in self.allowed_kinds + if not allowed: + raise ValueError(f"Cannot set {type(self).__name__} type to {kind}") + return kind + + def is_eye(self, row: int, col: int): + """ + Find whether the referenced module is in an eye. + """ + return ( + (row < 7 and col < 7) + or (row < 7 and self.width - col < 8) + or (self.width - row < 8 and col < 7) + ) + + +class BaseImageWithDrawer(BaseImage): + default_drawer_class: type[QRModuleDrawer] + drawer_aliases: DrawerAliases = {} + + def get_default_module_drawer(self) -> QRModuleDrawer: + return self.default_drawer_class() + + def get_default_eye_drawer(self) -> QRModuleDrawer: + return self.default_drawer_class() + + needs_context = True + + module_drawer: "QRModuleDrawer" + eye_drawer: "QRModuleDrawer" + + def __init__( + self, + *args, + module_drawer: Union[QRModuleDrawer, str, None] = None, + eye_drawer: Union[QRModuleDrawer, str, None] = None, + **kwargs, + ): + self.module_drawer = ( + self.get_drawer(module_drawer) or self.get_default_module_drawer() + ) + # The eye drawer can be overridden by another module drawer as well, + # but you have to be more careful with these in order to make the QR + # code still parseable + self.eye_drawer = self.get_drawer(eye_drawer) or self.get_default_eye_drawer() + super().__init__(*args, **kwargs) + + def get_drawer( + self, drawer: Union[QRModuleDrawer, str, None] + ) -> Optional[QRModuleDrawer]: + if not isinstance(drawer, str): + return drawer + drawer_cls, kwargs = self.drawer_aliases[drawer] + return drawer_cls(**kwargs) + + def init_new_image(self): + self.module_drawer.initialize(img=self) + self.eye_drawer.initialize(img=self) + + return super().init_new_image() + + def drawrect_context(self, row: int, col: int, qr: "QRCode"): + box = self.pixel_box(row, col) + drawer = self.eye_drawer if self.is_eye(row, col) else self.module_drawer + is_active: Union[bool, ActiveWithNeighbors] = ( + qr.active_with_neighbors(row, col) + if drawer.needs_neighbors + else bool(qr.modules[row][col]) + ) + + drawer.drawrect(box, is_active) diff --git a/python_modules/qrcode/image/pil.py b/python_modules/qrcode/image/pil.py new file mode 100644 index 000000000..57ee13a8d --- /dev/null +++ b/python_modules/qrcode/image/pil.py @@ -0,0 +1,57 @@ +import qrcode.image.base +from PIL import Image, ImageDraw + + +class PilImage(qrcode.image.base.BaseImage): + """ + PIL image builder, default format is PNG. + """ + + kind = "PNG" + + def new_image(self, **kwargs): + if not Image: + raise ImportError("PIL library not found.") + + back_color = kwargs.get("back_color", "white") + fill_color = kwargs.get("fill_color", "black") + + try: + fill_color = fill_color.lower() + except AttributeError: + pass + + try: + back_color = back_color.lower() + except AttributeError: + pass + + # L mode (1 mode) color = (r*299 + g*587 + b*114)//1000 + if fill_color == "black" and back_color == "white": + mode = "1" + fill_color = 0 + if back_color == "white": + back_color = 255 + elif back_color == "transparent": + mode = "RGBA" + back_color = None + else: + mode = "RGB" + + img = Image.new(mode, (self.pixel_size, self.pixel_size), back_color) + self.fill_color = fill_color + self._idr = ImageDraw.Draw(img) + return img + + def drawrect(self, row, col): + box = self.pixel_box(row, col) + self._idr.rectangle(box, fill=self.fill_color) + + def save(self, stream, format=None, **kwargs): + kind = kwargs.pop("kind", self.kind) + if format is None: + format = kind + self._img.save(stream, format=format, **kwargs) + + def __getattr__(self, name): + return getattr(self._img, name) diff --git a/python_modules/qrcode/image/pure.py b/python_modules/qrcode/image/pure.py new file mode 100644 index 000000000..5a8b2c5e2 --- /dev/null +++ b/python_modules/qrcode/image/pure.py @@ -0,0 +1,56 @@ +from itertools import chain + +from qrcode.compat.png import PngWriter +from qrcode.image.base import BaseImage + + +class PyPNGImage(BaseImage): + """ + pyPNG image builder. + """ + + kind = "PNG" + allowed_kinds = ("PNG",) + needs_drawrect = False + + def new_image(self, **kwargs): + if not PngWriter: + raise ImportError("PyPNG library not installed.") + + return PngWriter(self.pixel_size, self.pixel_size, greyscale=True, bitdepth=1) + + def drawrect(self, row, col): + """ + Not used. + """ + + def save(self, stream, kind=None): + if isinstance(stream, str): + stream = open(stream, "wb") + self._img.write(stream, self.rows_iter()) + + def rows_iter(self): + yield from self.border_rows_iter() + border_col = [1] * (self.box_size * self.border) + for module_row in self.modules: + row = ( + border_col + + list( + chain.from_iterable( + ([not point] * self.box_size) for point in module_row + ) + ) + + border_col + ) + for _ in range(self.box_size): + yield row + yield from self.border_rows_iter() + + def border_rows_iter(self): + border_row = [1] * (self.box_size * (self.width + self.border * 2)) + for _ in range(self.border * self.box_size): + yield border_row + + +# Keeping this for backwards compatibility. +PymagingImage = PyPNGImage diff --git a/python_modules/qrcode/image/styledpil.py b/python_modules/qrcode/image/styledpil.py new file mode 100644 index 000000000..cd63a6e0c --- /dev/null +++ b/python_modules/qrcode/image/styledpil.py @@ -0,0 +1,120 @@ +import qrcode.image.base +from PIL import Image +from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask +from qrcode.image.styles.moduledrawers import SquareModuleDrawer + + +class StyledPilImage(qrcode.image.base.BaseImageWithDrawer): + """ + Styled PIL image builder, default format is PNG. + + This differs from the PilImage in that there is a module_drawer, a + color_mask, and an optional image + + The module_drawer should extend the QRModuleDrawer class and implement the + drawrect_context(self, box, active, context), and probably also the + initialize function. This will draw an individual "module" or square on + the QR code. + + The color_mask will extend the QRColorMask class and will at very least + implement the get_fg_pixel(image, x, y) function, calculating a color to + put on the image at the pixel location (x,y) (more advanced functionality + can be gotten by instead overriding other functions defined in the + QRColorMask class) + + The Image can be specified either by path or with a Pillow Image, and if it + is there will be placed in the middle of the QR code. No effort is done to + ensure that the QR code is still legible after the image has been placed + there; Q or H level error correction levels are recommended to maintain + data integrity A resampling filter can be specified (defaulting to + PIL.Image.Resampling.LANCZOS) for resizing; see PIL.Image.resize() for possible + options for this parameter. + The image size can be controlled by `embedded_image_ratio` which is a ratio + between 0 and 1 that's set in relation to the overall width of the QR code. + """ + + kind = "PNG" + + needs_processing = True + color_mask: QRColorMask + default_drawer_class = SquareModuleDrawer + + def __init__(self, *args, **kwargs): + self.color_mask = kwargs.get("color_mask", SolidFillColorMask()) + # allow embeded_ parameters with typos for backwards compatibility + embedded_image_path = kwargs.get( + "embedded_image_path", kwargs.get("embeded_image_path", None) + ) + self.embedded_image = kwargs.get( + "embedded_image", kwargs.get("embeded_image", None) + ) + self.embedded_image_ratio = kwargs.get( + "embedded_image_ratio", kwargs.get("embeded_image_ratio", 0.25) + ) + self.embedded_image_resample = kwargs.get( + "embedded_image_resample", + kwargs.get("embeded_image_resample", Image.Resampling.LANCZOS), + ) + if not self.embedded_image and embedded_image_path: + self.embedded_image = Image.open(embedded_image_path) + + # the paint_color is the color the module drawer will use to draw upon + # a canvas During the color mask process, pixels that are paint_color + # are replaced by a newly-calculated color + self.paint_color = tuple(0 for i in self.color_mask.back_color) + if self.color_mask.has_transparency: + self.paint_color = tuple([*self.color_mask.back_color[:3], 255]) + + super().__init__(*args, **kwargs) + + def new_image(self, **kwargs): + mode = ( + "RGBA" + if ( + self.color_mask.has_transparency + or (self.embedded_image and "A" in self.embedded_image.getbands()) + ) + else "RGB" + ) + # This is the background color. Should be white or whiteish + back_color = self.color_mask.back_color + + return Image.new(mode, (self.pixel_size, self.pixel_size), back_color) + + def init_new_image(self): + self.color_mask.initialize(self, self._img) + super().init_new_image() + + def process(self): + self.color_mask.apply_mask(self._img) + if self.embedded_image: + self.draw_embedded_image() + + def draw_embedded_image(self): + if not self.embedded_image: + return + total_width, _ = self._img.size + total_width = int(total_width) + logo_width_ish = int(total_width * self.embedded_image_ratio) + logo_offset = ( + int((int(total_width / 2) - int(logo_width_ish / 2)) / self.box_size) + * self.box_size + ) # round the offset to the nearest module + logo_position = (logo_offset, logo_offset) + logo_width = total_width - logo_offset * 2 + region = self.embedded_image + region = region.resize((logo_width, logo_width), self.embedded_image_resample) + if "A" in region.getbands(): + self._img.alpha_composite(region, logo_position) + else: + self._img.paste(region, logo_position) + + def save(self, stream, format=None, **kwargs): + if format is None: + format = kwargs.get("kind", self.kind) + if "kind" in kwargs: + del kwargs["kind"] + self._img.save(stream, format=format, **kwargs) + + def __getattr__(self, name): + return getattr(self._img, name) diff --git a/python_modules/qrcode/image/styles/__init__.py b/python_modules/qrcode/image/styles/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python_modules/qrcode/image/styles/colormasks.py b/python_modules/qrcode/image/styles/colormasks.py new file mode 100644 index 000000000..9599f7fb2 --- /dev/null +++ b/python_modules/qrcode/image/styles/colormasks.py @@ -0,0 +1,226 @@ +import math + +from PIL import Image + + +class QRColorMask: + """ + QRColorMask is used to color in the QRCode. + + By the time apply_mask is called, the QRModuleDrawer of the StyledPilImage + will have drawn all of the modules on the canvas (the color of these + modules will be mostly black, although antialiasing may result in + gradients) In the base class, apply_mask is implemented such that the + background color will remain, but the foreground pixels will be replaced by + a color determined by a call to get_fg_pixel. There is additional + calculation done to preserve the gradient artifacts of antialiasing. + + All QRColorMask objects should be careful about RGB vs RGBA color spaces. + + For examples of what these look like, see doc/color_masks.png + """ + + back_color = (255, 255, 255) + has_transparency = False + paint_color = back_color + + def initialize(self, styledPilImage, image): + self.paint_color = styledPilImage.paint_color + + def apply_mask(self, image, use_cache=False): + width, height = image.size + pixels = image.load() + fg_color_cache = {} if use_cache else None + for x in range(width): + for y in range(height): + current_color = pixels[x, y] + if current_color == self.back_color: + continue + if use_cache and current_color in fg_color_cache: + pixels[x, y] = fg_color_cache[current_color] + continue + norm = self.extrap_color( + self.back_color, self.paint_color, current_color + ) + if norm is not None: + new_color = self.interp_color( + self.get_bg_pixel(image, x, y), + self.get_fg_pixel(image, x, y), + norm, + ) + pixels[x, y] = new_color + + if use_cache: + fg_color_cache[current_color] = new_color + else: + pixels[x, y] = self.get_bg_pixel(image, x, y) + + def get_fg_pixel(self, image, x, y): + raise NotImplementedError("QRModuleDrawer.paint_fg_pixel") + + def get_bg_pixel(self, image, x, y): + return self.back_color + + # The following functions are helpful for color calculation: + + # interpolate a number between two numbers + def interp_num(self, n1, n2, norm): + return int(n2 * norm + n1 * (1 - norm)) + + # interpolate a color between two colorrs + def interp_color(self, col1, col2, norm): + return tuple(self.interp_num(col1[i], col2[i], norm) for i in range(len(col1))) + + # find the interpolation coefficient between two numbers + def extrap_num(self, n1, n2, interped_num): + if n2 == n1: + return None + else: + return (interped_num - n1) / (n2 - n1) + + # find the interpolation coefficient between two numbers + def extrap_color(self, col1, col2, interped_color): + normed = [] + for c1, c2, ci in zip(col1, col2, interped_color): + extrap = self.extrap_num(c1, c2, ci) + if extrap is not None: + normed.append(extrap) + if not normed: + return None + return sum(normed) / len(normed) + + +class SolidFillColorMask(QRColorMask): + """ + Just fills in the background with one color and the foreground with another + """ + + def __init__(self, back_color=(255, 255, 255), front_color=(0, 0, 0)): + self.back_color = back_color + self.front_color = front_color + self.has_transparency = len(self.back_color) == 4 + + def apply_mask(self, image): + if self.back_color == (255, 255, 255) and self.front_color == (0, 0, 0): + # Optimization: the image is already drawn by QRModuleDrawer in + # black and white, so if these are also our mask colors we don't + # need to do anything. This is much faster than actually applying a + # mask. + pass + else: + # TODO there's probably a way to use PIL.ImageMath instead of doing + # the individual pixel comparisons that the base class uses, which + # would be a lot faster. (In fact doing this would probably remove + # the need for the B&W optimization above.) + QRColorMask.apply_mask(self, image, use_cache=True) + + def get_fg_pixel(self, image, x, y): + return self.front_color + + +class RadialGradiantColorMask(QRColorMask): + """ + Fills in the foreground with a radial gradient from the center to the edge + """ + + def __init__( + self, back_color=(255, 255, 255), center_color=(0, 0, 0), edge_color=(0, 0, 255) + ): + self.back_color = back_color + self.center_color = center_color + self.edge_color = edge_color + self.has_transparency = len(self.back_color) == 4 + + def get_fg_pixel(self, image, x, y): + width, _ = image.size + normedDistanceToCenter = math.sqrt( + (x - width / 2) ** 2 + (y - width / 2) ** 2 + ) / (math.sqrt(2) * width / 2) + return self.interp_color( + self.center_color, self.edge_color, normedDistanceToCenter + ) + + +class SquareGradiantColorMask(QRColorMask): + """ + Fills in the foreground with a square gradient from the center to the edge + """ + + def __init__( + self, back_color=(255, 255, 255), center_color=(0, 0, 0), edge_color=(0, 0, 255) + ): + self.back_color = back_color + self.center_color = center_color + self.edge_color = edge_color + self.has_transparency = len(self.back_color) == 4 + + def get_fg_pixel(self, image, x, y): + width, _ = image.size + normedDistanceToCenter = max(abs(x - width / 2), abs(y - width / 2)) / ( + width / 2 + ) + return self.interp_color( + self.center_color, self.edge_color, normedDistanceToCenter + ) + + +class HorizontalGradiantColorMask(QRColorMask): + """ + Fills in the foreground with a gradient sweeping from the left to the right + """ + + def __init__( + self, back_color=(255, 255, 255), left_color=(0, 0, 0), right_color=(0, 0, 255) + ): + self.back_color = back_color + self.left_color = left_color + self.right_color = right_color + self.has_transparency = len(self.back_color) == 4 + + def get_fg_pixel(self, image, x, y): + width, _ = image.size + return self.interp_color(self.left_color, self.right_color, x / width) + + +class VerticalGradiantColorMask(QRColorMask): + """ + Fills in the forefround with a gradient sweeping from the top to the bottom + """ + + def __init__( + self, back_color=(255, 255, 255), top_color=(0, 0, 0), bottom_color=(0, 0, 255) + ): + self.back_color = back_color + self.top_color = top_color + self.bottom_color = bottom_color + self.has_transparency = len(self.back_color) == 4 + + def get_fg_pixel(self, image, x, y): + width, _ = image.size + return self.interp_color(self.top_color, self.bottom_color, y / width) + + +class ImageColorMask(QRColorMask): + """ + Fills in the foreground with pixels from another image, either passed by + path or passed by image object. + """ + + def __init__( + self, back_color=(255, 255, 255), color_mask_path=None, color_mask_image=None + ): + self.back_color = back_color + if color_mask_image: + self.color_img = color_mask_image + else: + self.color_img = Image.open(color_mask_path) + + self.has_transparency = len(self.back_color) == 4 + + def initialize(self, styledPilImage, image): + self.paint_color = styledPilImage.paint_color + self.color_img = self.color_img.resize(image.size) + + def get_fg_pixel(self, image, x, y): + width, _ = image.size + return self.color_img.getpixel((x, y)) diff --git a/python_modules/qrcode/image/styles/moduledrawers/__init__.py b/python_modules/qrcode/image/styles/moduledrawers/__init__.py new file mode 100644 index 000000000..99217d496 --- /dev/null +++ b/python_modules/qrcode/image/styles/moduledrawers/__init__.py @@ -0,0 +1,10 @@ +# For backwards compatibility, importing the PIL drawers here. +try: + from .pil import CircleModuleDrawer # noqa: F401 + from .pil import GappedSquareModuleDrawer # noqa: F401 + from .pil import HorizontalBarsDrawer # noqa: F401 + from .pil import RoundedModuleDrawer # noqa: F401 + from .pil import SquareModuleDrawer # noqa: F401 + from .pil import VerticalBarsDrawer # noqa: F401 +except ImportError: + pass diff --git a/python_modules/qrcode/image/styles/moduledrawers/base.py b/python_modules/qrcode/image/styles/moduledrawers/base.py new file mode 100644 index 000000000..154d2cfa8 --- /dev/null +++ b/python_modules/qrcode/image/styles/moduledrawers/base.py @@ -0,0 +1,33 @@ +import abc +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from qrcode.image.base import BaseImage + + +class QRModuleDrawer(abc.ABC): + """ + QRModuleDrawer exists to draw the modules of the QR Code onto images. + + For this, technically all that is necessary is a ``drawrect(self, box, + is_active)`` function which takes in the box in which it is to draw, + whether or not the box is "active" (a module exists there). If + ``needs_neighbors`` is set to True, then the method should also accept a + ``neighbors`` kwarg (the neighboring pixels). + + It is frequently necessary to also implement an "initialize" function to + set up values that only the containing Image class knows about. + + For examples of what these look like, see doc/module_drawers.png + """ + + needs_neighbors = False + + def __init__(self, **kwargs): + pass + + def initialize(self, img: "BaseImage") -> None: + self.img = img + + @abc.abstractmethod + def drawrect(self, box, is_active) -> None: ... diff --git a/python_modules/qrcode/image/styles/moduledrawers/pil.py b/python_modules/qrcode/image/styles/moduledrawers/pil.py new file mode 100644 index 000000000..4aa424962 --- /dev/null +++ b/python_modules/qrcode/image/styles/moduledrawers/pil.py @@ -0,0 +1,265 @@ +from typing import TYPE_CHECKING + +from PIL import Image, ImageDraw +from qrcode.image.styles.moduledrawers.base import QRModuleDrawer + +if TYPE_CHECKING: + from qrcode.image.styledpil import StyledPilImage + from qrcode.main import ActiveWithNeighbors + +# When drawing antialiased things, make them bigger and then shrink them down +# to size after the geometry has been drawn. +ANTIALIASING_FACTOR = 4 + + +class StyledPilQRModuleDrawer(QRModuleDrawer): + """ + A base class for StyledPilImage module drawers. + + NOTE: the color that this draws in should be whatever is equivalent to + black in the color space, and the specified QRColorMask will handle adding + colors as necessary to the image + """ + + img: "StyledPilImage" + + +class SquareModuleDrawer(StyledPilQRModuleDrawer): + """ + Draws the modules as simple squares + """ + + def initialize(self, *args, **kwargs): + super().initialize(*args, **kwargs) + self.imgDraw = ImageDraw.Draw(self.img._img) + + def drawrect(self, box, is_active: bool): + if is_active: + self.imgDraw.rectangle(box, fill=self.img.paint_color) + + +class GappedSquareModuleDrawer(StyledPilQRModuleDrawer): + """ + Draws the modules as simple squares that are not contiguous. + + The size_ratio determines how wide the squares are relative to the width of + the space they are printed in + """ + + def __init__(self, size_ratio=0.8): + self.size_ratio = size_ratio + + def initialize(self, *args, **kwargs): + super().initialize(*args, **kwargs) + self.imgDraw = ImageDraw.Draw(self.img._img) + self.delta = (1 - self.size_ratio) * self.img.box_size / 2 + + def drawrect(self, box, is_active: bool): + if is_active: + smaller_box = ( + box[0][0] + self.delta, + box[0][1] + self.delta, + box[1][0] - self.delta, + box[1][1] - self.delta, + ) + self.imgDraw.rectangle(smaller_box, fill=self.img.paint_color) + + +class CircleModuleDrawer(StyledPilQRModuleDrawer): + """ + Draws the modules as circles + """ + + circle = None + + def initialize(self, *args, **kwargs): + super().initialize(*args, **kwargs) + box_size = self.img.box_size + fake_size = box_size * ANTIALIASING_FACTOR + self.circle = Image.new( + self.img.mode, + (fake_size, fake_size), + self.img.color_mask.back_color, + ) + ImageDraw.Draw(self.circle).ellipse( + (0, 0, fake_size, fake_size), fill=self.img.paint_color + ) + self.circle = self.circle.resize((box_size, box_size), Image.Resampling.LANCZOS) + + def drawrect(self, box, is_active: bool): + if is_active: + self.img._img.paste(self.circle, (box[0][0], box[0][1])) + + +class RoundedModuleDrawer(StyledPilQRModuleDrawer): + """ + Draws the modules with all 90 degree corners replaced with rounded edges. + + radius_ratio determines the radius of the rounded edges - a value of 1 + means that an isolated module will be drawn as a circle, while a value of 0 + means that the radius of the rounded edge will be 0 (and thus back to 90 + degrees again). + """ + + needs_neighbors = True + + def __init__(self, radius_ratio=1): + self.radius_ratio = radius_ratio + + def initialize(self, *args, **kwargs): + super().initialize(*args, **kwargs) + self.corner_width = int(self.img.box_size / 2) + self.setup_corners() + + def setup_corners(self): + mode = self.img.mode + back_color = self.img.color_mask.back_color + front_color = self.img.paint_color + self.SQUARE = Image.new( + mode, (self.corner_width, self.corner_width), front_color + ) + + fake_width = self.corner_width * ANTIALIASING_FACTOR + radius = self.radius_ratio * fake_width + diameter = radius * 2 + base = Image.new( + mode, (fake_width, fake_width), back_color + ) # make something 4x bigger for antialiasing + base_draw = ImageDraw.Draw(base) + base_draw.ellipse((0, 0, diameter, diameter), fill=front_color) + base_draw.rectangle((radius, 0, fake_width, fake_width), fill=front_color) + base_draw.rectangle((0, radius, fake_width, fake_width), fill=front_color) + self.NW_ROUND = base.resize( + (self.corner_width, self.corner_width), Image.Resampling.LANCZOS + ) + self.SW_ROUND = self.NW_ROUND.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + self.SE_ROUND = self.NW_ROUND.transpose(Image.Transpose.ROTATE_180) + self.NE_ROUND = self.NW_ROUND.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + + def drawrect(self, box: list[list[int]], is_active: "ActiveWithNeighbors"): + if not is_active: + return + # find rounded edges + nw_rounded = not is_active.W and not is_active.N + ne_rounded = not is_active.N and not is_active.E + se_rounded = not is_active.E and not is_active.S + sw_rounded = not is_active.S and not is_active.W + + nw = self.NW_ROUND if nw_rounded else self.SQUARE + ne = self.NE_ROUND if ne_rounded else self.SQUARE + se = self.SE_ROUND if se_rounded else self.SQUARE + sw = self.SW_ROUND if sw_rounded else self.SQUARE + self.img._img.paste(nw, (box[0][0], box[0][1])) + self.img._img.paste(ne, (box[0][0] + self.corner_width, box[0][1])) + self.img._img.paste( + se, (box[0][0] + self.corner_width, box[0][1] + self.corner_width) + ) + self.img._img.paste(sw, (box[0][0], box[0][1] + self.corner_width)) + + +class VerticalBarsDrawer(StyledPilQRModuleDrawer): + """ + Draws vertically contiguous groups of modules as long rounded rectangles, + with gaps between neighboring bands (the size of these gaps is inversely + proportional to the horizontal_shrink). + """ + + needs_neighbors = True + + def __init__(self, horizontal_shrink=0.8): + self.horizontal_shrink = horizontal_shrink + + def initialize(self, *args, **kwargs): + super().initialize(*args, **kwargs) + self.half_height = int(self.img.box_size / 2) + self.delta = int((1 - self.horizontal_shrink) * self.half_height) + self.setup_edges() + + def setup_edges(self): + mode = self.img.mode + back_color = self.img.color_mask.back_color + front_color = self.img.paint_color + + height = self.half_height + width = height * 2 + shrunken_width = int(width * self.horizontal_shrink) + self.SQUARE = Image.new(mode, (shrunken_width, height), front_color) + + fake_width = width * ANTIALIASING_FACTOR + fake_height = height * ANTIALIASING_FACTOR + base = Image.new( + mode, (fake_width, fake_height), back_color + ) # make something 4x bigger for antialiasing + base_draw = ImageDraw.Draw(base) + base_draw.ellipse((0, 0, fake_width, fake_height * 2), fill=front_color) + + self.ROUND_TOP = base.resize((shrunken_width, height), Image.Resampling.LANCZOS) + self.ROUND_BOTTOM = self.ROUND_TOP.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + + def drawrect(self, box, is_active: "ActiveWithNeighbors"): + if is_active: + # find rounded edges + top_rounded = not is_active.N + bottom_rounded = not is_active.S + + top = self.ROUND_TOP if top_rounded else self.SQUARE + bottom = self.ROUND_BOTTOM if bottom_rounded else self.SQUARE + self.img._img.paste(top, (box[0][0] + self.delta, box[0][1])) + self.img._img.paste( + bottom, (box[0][0] + self.delta, box[0][1] + self.half_height) + ) + + +class HorizontalBarsDrawer(StyledPilQRModuleDrawer): + """ + Draws horizontally contiguous groups of modules as long rounded rectangles, + with gaps between neighboring bands (the size of these gaps is inversely + proportional to the vertical_shrink). + """ + + needs_neighbors = True + + def __init__(self, vertical_shrink=0.8): + self.vertical_shrink = vertical_shrink + + def initialize(self, *args, **kwargs): + super().initialize(*args, **kwargs) + self.half_width = int(self.img.box_size / 2) + self.delta = int((1 - self.vertical_shrink) * self.half_width) + self.setup_edges() + + def setup_edges(self): + mode = self.img.mode + back_color = self.img.color_mask.back_color + front_color = self.img.paint_color + + width = self.half_width + height = width * 2 + shrunken_height = int(height * self.vertical_shrink) + self.SQUARE = Image.new(mode, (width, shrunken_height), front_color) + + fake_width = width * ANTIALIASING_FACTOR + fake_height = height * ANTIALIASING_FACTOR + base = Image.new( + mode, (fake_width, fake_height), back_color + ) # make something 4x bigger for antialiasing + base_draw = ImageDraw.Draw(base) + base_draw.ellipse((0, 0, fake_width * 2, fake_height), fill=front_color) + + self.ROUND_LEFT = base.resize( + (width, shrunken_height), Image.Resampling.LANCZOS + ) + self.ROUND_RIGHT = self.ROUND_LEFT.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + + def drawrect(self, box, is_active: "ActiveWithNeighbors"): + if is_active: + # find rounded edges + left_rounded = not is_active.W + right_rounded = not is_active.E + + left = self.ROUND_LEFT if left_rounded else self.SQUARE + right = self.ROUND_RIGHT if right_rounded else self.SQUARE + self.img._img.paste(left, (box[0][0], box[0][1] + self.delta)) + self.img._img.paste( + right, (box[0][0] + self.half_width, box[0][1] + self.delta) + ) diff --git a/python_modules/qrcode/image/styles/moduledrawers/svg.py b/python_modules/qrcode/image/styles/moduledrawers/svg.py new file mode 100644 index 000000000..cf5b9e7d9 --- /dev/null +++ b/python_modules/qrcode/image/styles/moduledrawers/svg.py @@ -0,0 +1,139 @@ +import abc +from decimal import Decimal +from typing import TYPE_CHECKING, NamedTuple + +from qrcode.image.styles.moduledrawers.base import QRModuleDrawer +from qrcode.compat.etree import ET + +if TYPE_CHECKING: + from qrcode.image.svg import SvgFragmentImage, SvgPathImage + +ANTIALIASING_FACTOR = 4 + + +class Coords(NamedTuple): + x0: Decimal + y0: Decimal + x1: Decimal + y1: Decimal + xh: Decimal + yh: Decimal + + +class BaseSvgQRModuleDrawer(QRModuleDrawer): + img: "SvgFragmentImage" + + def __init__(self, *, size_ratio: Decimal = Decimal(1), **kwargs): + self.size_ratio = size_ratio + + def initialize(self, *args, **kwargs) -> None: + super().initialize(*args, **kwargs) + self.box_delta = (1 - self.size_ratio) * self.img.box_size / 2 + self.box_size = Decimal(self.img.box_size) * self.size_ratio + self.box_half = self.box_size / 2 + + def coords(self, box) -> Coords: + row, col = box[0] + x = row + self.box_delta + y = col + self.box_delta + + return Coords( + x, + y, + x + self.box_size, + y + self.box_size, + x + self.box_half, + y + self.box_half, + ) + + +class SvgQRModuleDrawer(BaseSvgQRModuleDrawer): + tag = "rect" + + def initialize(self, *args, **kwargs) -> None: + super().initialize(*args, **kwargs) + self.tag_qname = ET.QName(self.img._SVG_namespace, self.tag) + + def drawrect(self, box, is_active: bool): + if not is_active: + return + self.img._img.append(self.el(box)) + + @abc.abstractmethod + def el(self, box): ... + + +class SvgSquareDrawer(SvgQRModuleDrawer): + def initialize(self, *args, **kwargs) -> None: + super().initialize(*args, **kwargs) + self.unit_size = self.img.units(self.box_size) + + def el(self, box): + coords = self.coords(box) + return ET.Element( + self.tag_qname, # type: ignore + x=self.img.units(coords.x0), + y=self.img.units(coords.y0), + width=self.unit_size, + height=self.unit_size, + ) + + +class SvgCircleDrawer(SvgQRModuleDrawer): + tag = "circle" + + def initialize(self, *args, **kwargs) -> None: + super().initialize(*args, **kwargs) + self.radius = self.img.units(self.box_half) + + def el(self, box): + coords = self.coords(box) + return ET.Element( + self.tag_qname, # type: ignore + cx=self.img.units(coords.xh), + cy=self.img.units(coords.yh), + r=self.radius, + ) + + +class SvgPathQRModuleDrawer(BaseSvgQRModuleDrawer): + img: "SvgPathImage" + + def drawrect(self, box, is_active: bool): + if not is_active: + return + self.img._subpaths.append(self.subpath(box)) + + @abc.abstractmethod + def subpath(self, box) -> str: ... + + +class SvgPathSquareDrawer(SvgPathQRModuleDrawer): + def subpath(self, box) -> str: + coords = self.coords(box) + x0 = self.img.units(coords.x0, text=False) + y0 = self.img.units(coords.y0, text=False) + x1 = self.img.units(coords.x1, text=False) + y1 = self.img.units(coords.y1, text=False) + + return f"M{x0},{y0}H{x1}V{y1}H{x0}z" + + +class SvgPathCircleDrawer(SvgPathQRModuleDrawer): + def initialize(self, *args, **kwargs) -> None: + super().initialize(*args, **kwargs) + + def subpath(self, box) -> str: + coords = self.coords(box) + x0 = self.img.units(coords.x0, text=False) + yh = self.img.units(coords.yh, text=False) + h = self.img.units(self.box_half - self.box_delta, text=False) + x1 = self.img.units(coords.x1, text=False) + + # rx,ry is the centerpoint of the arc + # 1? is the x-axis-rotation + # 2? is the large-arc-flag + # 3? is the sweep flag + # x,y is the point the arc is drawn to + + return f"M{x0},{yh}A{h},{h} 0 0 0 {x1},{yh}A{h},{h} 0 0 0 {x0},{yh}z" diff --git a/python_modules/qrcode/image/svg.py b/python_modules/qrcode/image/svg.py new file mode 100644 index 000000000..4117559ab --- /dev/null +++ b/python_modules/qrcode/image/svg.py @@ -0,0 +1,175 @@ +import decimal +from decimal import Decimal +from typing import Optional, Union, overload, Literal + +import qrcode.image.base +from qrcode.compat.etree import ET +from qrcode.image.styles.moduledrawers import svg as svg_drawers +from qrcode.image.styles.moduledrawers.base import QRModuleDrawer + + +class SvgFragmentImage(qrcode.image.base.BaseImageWithDrawer): + """ + SVG image builder + + Creates a QR-code image as a SVG document fragment. + """ + + _SVG_namespace = "http://www.w3.org/2000/svg" + kind = "SVG" + allowed_kinds = ("SVG",) + default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgSquareDrawer + + def __init__(self, *args, **kwargs): + ET.register_namespace("svg", self._SVG_namespace) + super().__init__(*args, **kwargs) + # Save the unit size, for example the default box_size of 10 is '1mm'. + self.unit_size = self.units(self.box_size) + + @overload + def units(self, pixels: Union[int, Decimal], text: Literal[False]) -> Decimal: ... + + @overload + def units(self, pixels: Union[int, Decimal], text: Literal[True] = True) -> str: ... + + def units(self, pixels, text=True): + """ + A box_size of 10 (default) equals 1mm. + """ + units = Decimal(pixels) / 10 + if not text: + return units + units = units.quantize(Decimal("0.001")) + context = decimal.Context(traps=[decimal.Inexact]) + try: + for d in (Decimal("0.01"), Decimal("0.1"), Decimal("0")): + units = units.quantize(d, context=context) + except decimal.Inexact: + pass + return f"{units}mm" + + def save(self, stream, kind=None): + self.check_kind(kind=kind) + self._write(stream) + + def to_string(self, **kwargs): + return ET.tostring(self._img, **kwargs) + + def new_image(self, **kwargs): + return self._svg(**kwargs) + + def _svg(self, tag=None, version="1.1", **kwargs): + if tag is None: + tag = ET.QName(self._SVG_namespace, "svg") + dimension = self.units(self.pixel_size) + return ET.Element( + tag, # type: ignore + width=dimension, + height=dimension, + version=version, + **kwargs, + ) + + def _write(self, stream): + ET.ElementTree(self._img).write(stream, xml_declaration=False) + + +class SvgImage(SvgFragmentImage): + """ + Standalone SVG image builder + + Creates a QR-code image as a standalone SVG document. + """ + + background: Optional[str] = None + drawer_aliases: qrcode.image.base.DrawerAliases = { + "circle": (svg_drawers.SvgCircleDrawer, {}), + "gapped-circle": (svg_drawers.SvgCircleDrawer, {"size_ratio": Decimal(0.8)}), + "gapped-square": (svg_drawers.SvgSquareDrawer, {"size_ratio": Decimal(0.8)}), + } + + def _svg(self, tag="svg", **kwargs): + svg = super()._svg(tag=tag, **kwargs) + svg.set("xmlns", self._SVG_namespace) + if self.background: + svg.append( + ET.Element( + "rect", + fill=self.background, + x="0", + y="0", + width="100%", + height="100%", + ) + ) + return svg + + def _write(self, stream): + ET.ElementTree(self._img).write(stream, encoding="UTF-8", xml_declaration=True) + + +class SvgPathImage(SvgImage): + """ + SVG image builder with one single element (removes white spaces + between individual QR points). + """ + + QR_PATH_STYLE = { + "fill": "#000000", + "fill-opacity": "1", + "fill-rule": "nonzero", + "stroke": "none", + } + + needs_processing = True + path: Optional[ET.Element] = None + default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgPathSquareDrawer + drawer_aliases = { + "circle": (svg_drawers.SvgPathCircleDrawer, {}), + "gapped-circle": ( + svg_drawers.SvgPathCircleDrawer, + {"size_ratio": Decimal(0.8)}, + ), + "gapped-square": ( + svg_drawers.SvgPathSquareDrawer, + {"size_ratio": Decimal(0.8)}, + ), + } + + def __init__(self, *args, **kwargs): + self._subpaths: list[str] = [] + super().__init__(*args, **kwargs) + + def _svg(self, viewBox=None, **kwargs): + if viewBox is None: + dimension = self.units(self.pixel_size, text=False) + viewBox = "0 0 {d} {d}".format(d=dimension) + return super()._svg(viewBox=viewBox, **kwargs) + + def process(self): + # Store the path just in case someone wants to use it again or in some + # unique way. + self.path = ET.Element( + ET.QName("path"), # type: ignore + d="".join(self._subpaths), + id="qr-path", + **self.QR_PATH_STYLE, + ) + self._subpaths = [] + self._img.append(self.path) + + +class SvgFillImage(SvgImage): + """ + An SvgImage that fills the background to white. + """ + + background = "white" + + +class SvgPathFillImage(SvgPathImage): + """ + An SvgPathImage that fills the background to white. + """ + + background = "white" diff --git a/python_modules/qrcode/main.py b/python_modules/qrcode/main.py new file mode 100644 index 000000000..152c97b0d --- /dev/null +++ b/python_modules/qrcode/main.py @@ -0,0 +1,541 @@ +import sys +from bisect import bisect_left +from typing import ( + Generic, + NamedTuple, + Optional, + TypeVar, + cast, + overload, + Literal, +) + +from qrcode import constants, exceptions, util +from qrcode.image.base import BaseImage +from qrcode.image.pure import PyPNGImage + +ModulesType = list[list[Optional[bool]]] +# Cache modules generated just based on the QR Code version +precomputed_qr_blanks: dict[int, ModulesType] = {} + + +def make(data=None, **kwargs): + qr = QRCode(**kwargs) + qr.add_data(data) + return qr.make_image() + + +def _check_box_size(size): + if int(size) <= 0: + raise ValueError(f"Invalid box size (was {size}, expected larger than 0)") + + +def _check_border(size): + if int(size) < 0: + raise ValueError( + "Invalid border value (was %s, expected 0 or larger than that)" % size + ) + + +def _check_mask_pattern(mask_pattern): + if mask_pattern is None: + return + if not isinstance(mask_pattern, int): + raise TypeError( + f"Invalid mask pattern (was {type(mask_pattern)}, expected int)" + ) + if mask_pattern < 0 or mask_pattern > 7: + raise ValueError(f"Mask pattern should be in range(8) (got {mask_pattern})") + + +def copy_2d_array(x): + return [row[:] for row in x] + + +class ActiveWithNeighbors(NamedTuple): + NW: bool + N: bool + NE: bool + W: bool + me: bool + E: bool + SW: bool + S: bool + SE: bool + + def __bool__(self) -> bool: + return self.me + + +GenericImage = TypeVar("GenericImage", bound=BaseImage) +GenericImageLocal = TypeVar("GenericImageLocal", bound=BaseImage) + + +class QRCode(Generic[GenericImage]): + modules: ModulesType + _version: Optional[int] = None + + def __init__( + self, + version=None, + error_correction=constants.ERROR_CORRECT_M, + box_size=10, + border=4, + image_factory: Optional[type[GenericImage]] = None, + mask_pattern=None, + ): + _check_box_size(box_size) + _check_border(border) + self.version = version + self.error_correction = int(error_correction) + self.box_size = int(box_size) + # Spec says border should be at least four boxes wide, but allow for + # any (e.g. for producing printable QR codes). + self.border = int(border) + self.mask_pattern = mask_pattern + self.image_factory = image_factory + if image_factory is not None: + assert issubclass(image_factory, BaseImage) + self.clear() + + @property + def version(self) -> int: + if self._version is None: + self.best_fit() + return cast(int, self._version) + + @version.setter + def version(self, value) -> None: + if value is not None: + value = int(value) + util.check_version(value) + self._version = value + + @property + def mask_pattern(self): + return self._mask_pattern + + @mask_pattern.setter + def mask_pattern(self, pattern): + _check_mask_pattern(pattern) + self._mask_pattern = pattern + + def clear(self): + """ + Reset the internal data. + """ + self.modules = [[]] + self.modules_count = 0 + self.data_cache = None + self.data_list = [] + + def add_data(self, data, optimize=20): + """ + Add data to this QR Code. + + :param optimize: Data will be split into multiple chunks to optimize + the QR size by finding to more compressed modes of at least this + length. Set to ``0`` to avoid optimizing at all. + """ + if isinstance(data, util.QRData): + self.data_list.append(data) + elif optimize: + self.data_list.extend(util.optimal_data_chunks(data, minimum=optimize)) + else: + self.data_list.append(util.QRData(data)) + self.data_cache = None + + def make(self, fit=True): + """ + Compile the data into a QR Code array. + + :param fit: If ``True`` (or if a size has not been provided), find the + best fit for the data to avoid data overflow errors. + """ + if fit or (self.version is None): + self.best_fit(start=self.version) + if self.mask_pattern is None: + self.makeImpl(False, self.best_mask_pattern()) + else: + self.makeImpl(False, self.mask_pattern) + + def makeImpl(self, test, mask_pattern): + self.modules_count = self.version * 4 + 17 + + if self.version in precomputed_qr_blanks: + self.modules = copy_2d_array(precomputed_qr_blanks[self.version]) + else: + self.modules = [ + [None] * self.modules_count for i in range(self.modules_count) + ] + self.setup_position_probe_pattern(0, 0) + self.setup_position_probe_pattern(self.modules_count - 7, 0) + self.setup_position_probe_pattern(0, self.modules_count - 7) + self.setup_position_adjust_pattern() + self.setup_timing_pattern() + + precomputed_qr_blanks[self.version] = copy_2d_array(self.modules) + + self.setup_type_info(test, mask_pattern) + + if self.version >= 7: + self.setup_type_number(test) + + if self.data_cache is None: + self.data_cache = util.create_data( + self.version, self.error_correction, self.data_list + ) + self.map_data(self.data_cache, mask_pattern) + + def setup_position_probe_pattern(self, row, col): + for r in range(-1, 8): + if row + r <= -1 or self.modules_count <= row + r: + continue + + for c in range(-1, 8): + if col + c <= -1 or self.modules_count <= col + c: + continue + + if ( + (0 <= r <= 6 and c in {0, 6}) + or (0 <= c <= 6 and r in {0, 6}) + or (2 <= r <= 4 and 2 <= c <= 4) + ): + self.modules[row + r][col + c] = True + else: + self.modules[row + r][col + c] = False + + def best_fit(self, start=None): + """ + Find the minimum size required to fit in the data. + """ + if start is None: + start = 1 + util.check_version(start) + + # Corresponds to the code in util.create_data, except we don't yet know + # version, so optimistically assume start and check later + mode_sizes = util.mode_sizes_for_version(start) + buffer = util.BitBuffer() + for data in self.data_list: + buffer.put(data.mode, 4) + buffer.put(len(data), mode_sizes[data.mode]) + data.write(buffer) + + needed_bits = len(buffer) + self.version = bisect_left( + util.BIT_LIMIT_TABLE[self.error_correction], needed_bits, start + ) + if self.version == 41: + raise exceptions.DataOverflowError() + + # Now check whether we need more bits for the mode sizes, recursing if + # our guess was too low + if mode_sizes is not util.mode_sizes_for_version(self.version): + self.best_fit(start=self.version) + return self.version + + def best_mask_pattern(self): + """ + Find the most efficient mask pattern. + """ + min_lost_point = 0 + pattern = 0 + + for i in range(8): + self.makeImpl(True, i) + + lost_point = util.lost_point(self.modules) + + if i == 0 or min_lost_point > lost_point: + min_lost_point = lost_point + pattern = i + + return pattern + + def print_tty(self, out=None): + """ + Output the QR Code only using TTY colors. + + If the data has not been compiled yet, make it first. + """ + if out is None: + import sys + + out = sys.stdout + + if not out.isatty(): + raise OSError("Not a tty") + + if self.data_cache is None: + self.make() + + modcount = self.modules_count + out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n") + for r in range(modcount): + out.write("\x1b[1;47m \x1b[40m") + for c in range(modcount): + if self.modules[r][c]: + out.write(" ") + else: + out.write("\x1b[1;47m \x1b[40m") + out.write("\x1b[1;47m \x1b[0m\n") + out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n") + out.flush() + + def print_ascii(self, out=None, tty=False, invert=False): + """ + Output the QR Code using ASCII characters. + + :param tty: use fixed TTY color codes (forces invert=True) + :param invert: invert the ASCII characters (solid <-> transparent) + """ + if out is None: + out = sys.stdout + + if tty and not out.isatty(): + raise OSError("Not a tty") + + if self.data_cache is None: + self.make() + + modcount = self.modules_count + codes = [bytes((code,)).decode("cp437") for code in (255, 223, 220, 219)] + if tty: + invert = True + if invert: + codes.reverse() + + def get_module(x, y) -> int: + if invert and self.border and max(x, y) >= modcount + self.border: + return 1 + if min(x, y) < 0 or max(x, y) >= modcount: + return 0 + return cast(int, self.modules[x][y]) + + for r in range(-self.border, modcount + self.border, 2): + if tty: + if not invert or r < modcount + self.border - 1: + out.write("\x1b[48;5;232m") # Background black + out.write("\x1b[38;5;255m") # Foreground white + for c in range(-self.border, modcount + self.border): + pos = get_module(r, c) + (get_module(r + 1, c) << 1) + out.write(codes[pos]) + if tty: + out.write("\x1b[0m") + out.write("\n") + out.flush() + + @overload + def make_image( + self, image_factory: Literal[None] = None, **kwargs + ) -> GenericImage: ... + + @overload + def make_image( + self, image_factory: type[GenericImageLocal] = None, **kwargs + ) -> GenericImageLocal: ... + + def make_image(self, image_factory=None, **kwargs): + """ + Make an image from the QR Code data. + + If the data has not been compiled yet, make it first. + """ + # allow embeded_ parameters with typos for backwards compatibility + if ( + kwargs.get("embedded_image_path") + or kwargs.get("embedded_image") + or kwargs.get("embeded_image_path") + or kwargs.get("embeded_image") + ) and self.error_correction != constants.ERROR_CORRECT_H: + raise ValueError( + "Error correction level must be ERROR_CORRECT_H if an embedded image is provided" + ) + _check_box_size(self.box_size) + if self.data_cache is None: + self.make() + + if image_factory is not None: + assert issubclass(image_factory, BaseImage) + else: + image_factory = self.image_factory + if image_factory is None: + from qrcode.image.pil import Image, PilImage + + # Use PIL by default if available, otherwise use PyPNG. + image_factory = PilImage if Image else PyPNGImage + + im = image_factory( + self.border, + self.modules_count, + self.box_size, + qrcode_modules=self.modules, + **kwargs, + ) + + if im.needs_drawrect: + for r in range(self.modules_count): + for c in range(self.modules_count): + if im.needs_context: + im.drawrect_context(r, c, qr=self) + elif self.modules[r][c]: + im.drawrect(r, c) + if im.needs_processing: + im.process() + + return im + + # return true if and only if (row, col) is in the module + def is_constrained(self, row: int, col: int) -> bool: + return ( + row >= 0 + and row < len(self.modules) + and col >= 0 + and col < len(self.modules[row]) + ) + + def setup_timing_pattern(self): + for r in range(8, self.modules_count - 8): + if self.modules[r][6] is not None: + continue + self.modules[r][6] = r % 2 == 0 + + for c in range(8, self.modules_count - 8): + if self.modules[6][c] is not None: + continue + self.modules[6][c] = c % 2 == 0 + + def setup_position_adjust_pattern(self): + pos = util.pattern_position(self.version) + + for i in range(len(pos)): + row = pos[i] + + for j in range(len(pos)): + col = pos[j] + + if self.modules[row][col] is not None: + continue + + for r in range(-2, 3): + for c in range(-2, 3): + if ( + r == -2 + or r == 2 + or c == -2 + or c == 2 + or (r == 0 and c == 0) + ): + self.modules[row + r][col + c] = True + else: + self.modules[row + r][col + c] = False + + def setup_type_number(self, test): + bits = util.BCH_type_number(self.version) + + for i in range(18): + mod = not test and ((bits >> i) & 1) == 1 + self.modules[i // 3][i % 3 + self.modules_count - 8 - 3] = mod + + for i in range(18): + mod = not test and ((bits >> i) & 1) == 1 + self.modules[i % 3 + self.modules_count - 8 - 3][i // 3] = mod + + def setup_type_info(self, test, mask_pattern): + data = (self.error_correction << 3) | mask_pattern + bits = util.BCH_type_info(data) + + # vertical + for i in range(15): + mod = not test and ((bits >> i) & 1) == 1 + + if i < 6: + self.modules[i][8] = mod + elif i < 8: + self.modules[i + 1][8] = mod + else: + self.modules[self.modules_count - 15 + i][8] = mod + + # horizontal + for i in range(15): + mod = not test and ((bits >> i) & 1) == 1 + + if i < 8: + self.modules[8][self.modules_count - i - 1] = mod + elif i < 9: + self.modules[8][15 - i - 1 + 1] = mod + else: + self.modules[8][15 - i - 1] = mod + + # fixed module + self.modules[self.modules_count - 8][8] = not test + + def map_data(self, data, mask_pattern): + inc = -1 + row = self.modules_count - 1 + bitIndex = 7 + byteIndex = 0 + + mask_func = util.mask_func(mask_pattern) + + data_len = len(data) + + for col in range(self.modules_count - 1, 0, -2): + if col <= 6: + col -= 1 + + col_range = (col, col - 1) + + while True: + for c in col_range: + if self.modules[row][c] is None: + dark = False + + if byteIndex < data_len: + dark = ((data[byteIndex] >> bitIndex) & 1) == 1 + + if mask_func(row, c): + dark = not dark + + self.modules[row][c] = dark + bitIndex -= 1 + + if bitIndex == -1: + byteIndex += 1 + bitIndex = 7 + + row += inc + + if row < 0 or self.modules_count <= row: + row -= inc + inc = -inc + break + + def get_matrix(self): + """ + Return the QR Code as a multidimensional array, including the border. + + To return the array without a border, set ``self.border`` to 0 first. + """ + if self.data_cache is None: + self.make() + + if not self.border: + return self.modules + + width = len(self.modules) + self.border * 2 + code = [[False] * width] * self.border + x_border = [False] * self.border + for module in self.modules: + code.append(x_border + cast(list[bool], module) + x_border) + code += [[False] * width] * self.border + + return code + + def active_with_neighbors(self, row: int, col: int) -> ActiveWithNeighbors: + context: list[bool] = [] + for r in range(row - 1, row + 2): + for c in range(col - 1, col + 2): + context.append(self.is_constrained(r, c) and bool(self.modules[r][c])) + return ActiveWithNeighbors(*context) diff --git a/python_modules/qrcode/release.py b/python_modules/qrcode/release.py new file mode 100644 index 000000000..208ac1ee1 --- /dev/null +++ b/python_modules/qrcode/release.py @@ -0,0 +1,42 @@ +""" +This file provides zest.releaser entrypoints using when releasing new +qrcode versions. +""" + +import os +import re +import datetime + + +def update_manpage(data): + """ + Update the version in the manpage document. + """ + if data["name"] != "qrcode": + return + + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + filename = os.path.join(base_dir, "doc", "qr.1") + with open(filename) as f: + lines = f.readlines() + + changed = False + for i, line in enumerate(lines): + if not line.startswith(".TH "): + continue + parts = re.split(r'"([^"]*)"', line) + if len(parts) < 5: + continue + changed = parts[3] != data["new_version"] + if changed: + # Update version + parts[3] = data["new_version"] + # Update date + parts[1] = datetime.datetime.now().strftime("%-d %b %Y") + lines[i] = '"'.join(parts) + break + + if changed: + with open(filename, "w") as f: + for line in lines: + f.write(line) diff --git a/python_modules/qrcode/tests/__init__.py b/python_modules/qrcode/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python_modules/qrcode/tests/consts.py b/python_modules/qrcode/tests/consts.py new file mode 100644 index 000000000..b1240139b --- /dev/null +++ b/python_modules/qrcode/tests/consts.py @@ -0,0 +1,4 @@ +UNICODE_TEXT = "\u03b1\u03b2\u03b3" +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) +RED = (255, 0, 0) diff --git a/python_modules/qrcode/tests/test_example.py b/python_modules/qrcode/tests/test_example.py new file mode 100644 index 000000000..7190fe3e7 --- /dev/null +++ b/python_modules/qrcode/tests/test_example.py @@ -0,0 +1,13 @@ +from unittest import mock + +import pytest + +from qrcode import run_example + +pytest.importorskip("PIL", reason="Requires PIL") + + +@mock.patch("PIL.Image.Image.show") +def test_example(mock_show): + run_example() + mock_show.assert_called_with() diff --git a/python_modules/qrcode/tests/test_qrcode.py b/python_modules/qrcode/tests/test_qrcode.py new file mode 100644 index 000000000..652428480 --- /dev/null +++ b/python_modules/qrcode/tests/test_qrcode.py @@ -0,0 +1,271 @@ +import io +from unittest import mock + +import pytest + +import qrcode +import qrcode.util +from qrcode.exceptions import DataOverflowError +from qrcode.image.base import BaseImage +from qrcode.tests.consts import UNICODE_TEXT +from qrcode.util import MODE_8BIT_BYTE, MODE_ALPHA_NUM, MODE_NUMBER, QRData + + +def test_basic(): + qr = qrcode.QRCode(version=1) + qr.add_data("a") + qr.make(fit=False) + + +def test_large(): + qr = qrcode.QRCode(version=27) + qr.add_data("a") + qr.make(fit=False) + + +def test_invalid_version(): + with pytest.raises(ValueError): + qrcode.QRCode(version=42) + + +def test_invalid_border(): + with pytest.raises(ValueError): + qrcode.QRCode(border=-1) + + +def test_overflow(): + qr = qrcode.QRCode(version=1) + qr.add_data("abcdefghijklmno") + with pytest.raises(DataOverflowError): + qr.make(fit=False) + + +def test_add_qrdata(): + qr = qrcode.QRCode(version=1) + data = QRData("a") + qr.add_data(data) + qr.make(fit=False) + + +def test_fit(): + qr = qrcode.QRCode() + qr.add_data("a") + qr.make() + assert qr.version == 1 + qr.add_data("bcdefghijklmno") + qr.make() + assert qr.version == 2 + + +def test_mode_number(): + qr = qrcode.QRCode() + qr.add_data("1234567890123456789012345678901234", optimize=0) + qr.make() + assert qr.version == 1 + assert qr.data_list[0].mode == MODE_NUMBER + + +def test_mode_alpha(): + qr = qrcode.QRCode() + qr.add_data("ABCDEFGHIJ1234567890", optimize=0) + qr.make() + assert qr.version == 1 + assert qr.data_list[0].mode == MODE_ALPHA_NUM + + +def test_regression_mode_comma(): + qr = qrcode.QRCode() + qr.add_data(",", optimize=0) + qr.make() + assert qr.data_list[0].mode == MODE_8BIT_BYTE + + +def test_mode_8bit(): + qr = qrcode.QRCode() + qr.add_data("abcABC" + UNICODE_TEXT, optimize=0) + qr.make() + assert qr.version == 1 + assert qr.data_list[0].mode == MODE_8BIT_BYTE + + +def test_mode_8bit_newline(): + qr = qrcode.QRCode() + qr.add_data("ABCDEFGHIJ1234567890\n", optimize=0) + qr.make() + assert qr.data_list[0].mode == MODE_8BIT_BYTE + + +def test_make_image_with_wrong_pattern(): + with pytest.raises(TypeError): + qrcode.QRCode(mask_pattern="string pattern") + + with pytest.raises(ValueError): + qrcode.QRCode(mask_pattern=-1) + + with pytest.raises(ValueError): + qrcode.QRCode(mask_pattern=42) + + +def test_mask_pattern_setter(): + qr = qrcode.QRCode() + + with pytest.raises(TypeError): + qr.mask_pattern = "string pattern" + + with pytest.raises(ValueError): + qr.mask_pattern = -1 + + with pytest.raises(ValueError): + qr.mask_pattern = 8 + + +def test_qrcode_bad_factory(): + with pytest.raises(TypeError): + qrcode.QRCode(image_factory="not_BaseImage") # type: ignore + + with pytest.raises(AssertionError): + qrcode.QRCode(image_factory=dict) # type: ignore + + +def test_qrcode_factory(): + class MockFactory(BaseImage): + drawrect = mock.Mock() + new_image = mock.Mock() + + qr = qrcode.QRCode(image_factory=MockFactory) + qr.add_data(UNICODE_TEXT) + qr.make_image() + assert MockFactory.new_image.called + assert MockFactory.drawrect.called + + +def test_optimize(): + qr = qrcode.QRCode() + text = "A1abc12345def1HELLOa" + qr.add_data(text, optimize=4) + qr.make() + assert [d.mode for d in qr.data_list] == [ + MODE_8BIT_BYTE, + MODE_NUMBER, + MODE_8BIT_BYTE, + MODE_ALPHA_NUM, + MODE_8BIT_BYTE, + ] + assert qr.version == 2 + + +def test_optimize_short(): + qr = qrcode.QRCode() + text = "A1abc1234567def1HELLOa" + qr.add_data(text, optimize=7) + qr.make() + assert len(qr.data_list) == 3 + assert [d.mode for d in qr.data_list] == [ + MODE_8BIT_BYTE, + MODE_NUMBER, + MODE_8BIT_BYTE, + ] + assert qr.version == 2 + + +def test_optimize_longer_than_data(): + qr = qrcode.QRCode() + text = "ABCDEFGHIJK" + qr.add_data(text, optimize=12) + assert len(qr.data_list) == 1 + assert qr.data_list[0].mode == MODE_ALPHA_NUM + + +def test_optimize_size(): + text = "A1abc12345123451234512345def1HELLOHELLOHELLOHELLOa" * 5 + + qr = qrcode.QRCode() + qr.add_data(text) + qr.make() + assert qr.version == 10 + + qr = qrcode.QRCode() + qr.add_data(text, optimize=0) + qr.make() + assert qr.version == 11 + + +def test_qrdata_repr(): + data = b"hello" + data_obj = qrcode.util.QRData(data) + assert repr(data_obj) == repr(data) + + +def test_print_ascii_stdout(): + qr = qrcode.QRCode() + with mock.patch("sys.stdout") as fake_stdout: + fake_stdout.isatty.return_value = None + with pytest.raises(OSError): + qr.print_ascii(tty=True) + assert fake_stdout.isatty.called + + +def test_print_ascii(): + qr = qrcode.QRCode(border=0) + f = io.StringIO() + qr.print_ascii(out=f) + printed = f.getvalue() + f.close() + expected = "\u2588\u2580\u2580\u2580\u2580\u2580\u2588" + assert printed[: len(expected)] == expected + + f = io.StringIO() + f.isatty = lambda: True + qr.print_ascii(out=f, tty=True) + printed = f.getvalue() + f.close() + expected = "\x1b[48;5;232m\x1b[38;5;255m" + "\xa0\u2584\u2584\u2584\u2584\u2584\xa0" + assert printed[: len(expected)] == expected + + +def test_print_tty_stdout(): + qr = qrcode.QRCode() + with mock.patch("sys.stdout") as fake_stdout: + fake_stdout.isatty.return_value = None + pytest.raises(OSError, qr.print_tty) + assert fake_stdout.isatty.called + + +def test_print_tty(): + qr = qrcode.QRCode() + f = io.StringIO() + f.isatty = lambda: True + qr.print_tty(out=f) + printed = f.getvalue() + f.close() + BOLD_WHITE_BG = "\x1b[1;47m" + BLACK_BG = "\x1b[40m" + WHITE_BLOCK = BOLD_WHITE_BG + " " + BLACK_BG + EOL = "\x1b[0m\n" + expected = BOLD_WHITE_BG + " " * 23 + EOL + WHITE_BLOCK + " " * 7 + WHITE_BLOCK + assert printed[: len(expected)] == expected + + +def test_get_matrix(): + qr = qrcode.QRCode(border=0) + qr.add_data("1") + assert qr.get_matrix() == qr.modules + + +def test_get_matrix_border(): + qr = qrcode.QRCode(border=1) + qr.add_data("1") + matrix = [row[1:-1] for row in qr.get_matrix()[1:-1]] + assert matrix == qr.modules + + +def test_negative_size_at_construction(): + with pytest.raises(ValueError): + qrcode.QRCode(box_size=-1) + + +def test_negative_size_at_usage(): + qr = qrcode.QRCode() + qr.box_size = -1 + with pytest.raises(ValueError): + qr.make_image() diff --git a/python_modules/qrcode/tests/test_qrcode_pil.py b/python_modules/qrcode/tests/test_qrcode_pil.py new file mode 100644 index 000000000..95ef5af7b --- /dev/null +++ b/python_modules/qrcode/tests/test_qrcode_pil.py @@ -0,0 +1,157 @@ +import io + +import pytest + +import qrcode +import qrcode.util +from qrcode.tests.consts import BLACK, RED, UNICODE_TEXT, WHITE + +Image = pytest.importorskip("PIL.Image", reason="PIL is not installed") + +if Image: + from qrcode.image.styledpil import StyledPilImage + from qrcode.image.styles import colormasks, moduledrawers + + +def test_render_pil(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image() + img.save(io.BytesIO()) + assert isinstance(img.get_image(), Image.Image) + + +@pytest.mark.parametrize("back_color", ["TransParent", "red", (255, 195, 235)]) +def test_render_pil_background(back_color): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(back_color="TransParent") + img.save(io.BytesIO()) + + +def test_render_pil_with_rgb_color_tuples(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(back_color=(255, 195, 235), fill_color=(55, 95, 35)) + img.save(io.BytesIO()) + + +def test_render_with_pattern(): + qr = qrcode.QRCode(mask_pattern=3) + qr.add_data(UNICODE_TEXT) + img = qr.make_image() + img.save(io.BytesIO()) + + +def test_render_styled_Image(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=StyledPilImage) + img.save(io.BytesIO()) + + +def test_render_styled_with_embedded_image(): + embedded_img = Image.new("RGB", (10, 10), color="red") + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=StyledPilImage, embedded_image=embedded_img) + img.save(io.BytesIO()) + + +def test_render_styled_with_embedded_image_path(tmp_path): + tmpfile = str(tmp_path / "test.png") + embedded_img = Image.new("RGB", (10, 10), color="red") + embedded_img.save(tmpfile) + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=StyledPilImage, embedded_image_path=tmpfile) + img.save(io.BytesIO()) + + +@pytest.mark.parametrize( + "drawer", + [ + moduledrawers.CircleModuleDrawer, + moduledrawers.GappedSquareModuleDrawer, + moduledrawers.HorizontalBarsDrawer, + moduledrawers.RoundedModuleDrawer, + moduledrawers.SquareModuleDrawer, + moduledrawers.VerticalBarsDrawer, + ], +) +def test_render_styled_with_drawer(drawer): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=drawer(), + ) + img.save(io.BytesIO()) + + +@pytest.mark.parametrize( + "mask", + [ + colormasks.SolidFillColorMask(), + colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED), + colormasks.SolidFillColorMask(back_color=(255, 0, 255, 255), front_color=RED), + colormasks.RadialGradiantColorMask( + back_color=WHITE, center_color=BLACK, edge_color=RED + ), + colormasks.SquareGradiantColorMask( + back_color=WHITE, center_color=BLACK, edge_color=RED + ), + colormasks.HorizontalGradiantColorMask( + back_color=WHITE, left_color=RED, right_color=BLACK + ), + colormasks.VerticalGradiantColorMask( + back_color=WHITE, top_color=RED, bottom_color=BLACK + ), + colormasks.ImageColorMask( + back_color=WHITE, color_mask_image=Image.new("RGB", (10, 10), color="red") + ), + ], +) +def test_render_styled_with_mask(mask): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + +def test_embedded_image_and_error_correction(tmp_path): + "If an embedded image is specified, error correction must be the highest so the QR code is readable" + tmpfile = str(tmp_path / "test.png") + embedded_img = Image.new("RGB", (10, 10), color="red") + embedded_img.save(tmpfile) + + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + with pytest.raises(ValueError): + qr.make_image(embedded_image_path=tmpfile) + with pytest.raises(ValueError): + qr.make_image(embedded_image=embedded_img) + + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_M) + qr.add_data(UNICODE_TEXT) + with pytest.raises(ValueError): + qr.make_image(embedded_image_path=tmpfile) + with pytest.raises(ValueError): + qr.make_image(embedded_image=embedded_img) + + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_Q) + qr.add_data(UNICODE_TEXT) + with pytest.raises(ValueError): + qr.make_image(embedded_image_path=tmpfile) + with pytest.raises(ValueError): + qr.make_image(embedded_image=embedded_img) + + # The only accepted correction level when an embedded image is provided + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) + qr.add_data(UNICODE_TEXT) + qr.make_image(embedded_image_path=tmpfile) + qr.make_image(embedded_image=embedded_img) + + +def test_shortcut(): + qrcode.make("image") diff --git a/python_modules/qrcode/tests/test_qrcode_pypng.py b/python_modules/qrcode/tests/test_qrcode_pypng.py new file mode 100644 index 000000000..c502a3b3c --- /dev/null +++ b/python_modules/qrcode/tests/test_qrcode_pypng.py @@ -0,0 +1,35 @@ +import io +from unittest import mock + +import pytest + + +import qrcode +import qrcode.util +from qrcode.image.pure import PyPNGImage +from qrcode.tests.consts import UNICODE_TEXT + +png = pytest.importorskip("png", reason="png is not installed") + + +def test_render_pypng(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=PyPNGImage) + assert isinstance(img.get_image(), png.Writer) + + print(img.width, img.box_size, img.border) + img.save(io.BytesIO()) + + +def test_render_pypng_to_str(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=PyPNGImage) + assert isinstance(img.get_image(), png.Writer) + + mock_open = mock.mock_open() + with mock.patch("qrcode.image.pure.open", mock_open, create=True): + img.save("test_file.png") + mock_open.assert_called_once_with("test_file.png", "wb") + mock_open("test_file.png", "wb").write.assert_called() diff --git a/python_modules/qrcode/tests/test_qrcode_svg.py b/python_modules/qrcode/tests/test_qrcode_svg.py new file mode 100644 index 000000000..4774b2451 --- /dev/null +++ b/python_modules/qrcode/tests/test_qrcode_svg.py @@ -0,0 +1,54 @@ +import io + +import qrcode +from qrcode.image import svg +from qrcode.tests.consts import UNICODE_TEXT + + +class SvgImageWhite(svg.SvgImage): + background = "white" + + +def test_render_svg(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=svg.SvgImage) + img.save(io.BytesIO()) + + +def test_render_svg_path(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=svg.SvgPathImage) + img.save(io.BytesIO()) + + +def test_render_svg_fragment(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=svg.SvgFragmentImage) + img.save(io.BytesIO()) + + +def test_svg_string(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=svg.SvgFragmentImage) + file_like = io.BytesIO() + img.save(file_like) + file_like.seek(0) + assert file_like.read() in img.to_string() + + +def test_render_svg_with_background(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=SvgImageWhite) + img.save(io.BytesIO()) + + +def test_svg_circle_drawer(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer="circle") + img.save(io.BytesIO()) diff --git a/python_modules/qrcode/tests/test_release.py b/python_modules/qrcode/tests/test_release.py new file mode 100644 index 000000000..d61454b6e --- /dev/null +++ b/python_modules/qrcode/tests/test_release.py @@ -0,0 +1,43 @@ +import builtins +import datetime +import re +from unittest import mock + +from qrcode.release import update_manpage + +OPEN = f"{builtins.__name__}.open" +DATA = 'test\n.TH "date" "version" "description"\nthis' + + +@mock.patch(OPEN, new_callable=mock.mock_open, read_data=".TH invalid") +def test_invalid_data(mock_file): + update_manpage({"name": "qrcode", "new_version": "1.23"}) + mock_file.assert_called() + mock_file().write.assert_not_called() + + +@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) +def test_not_qrcode(mock_file): + update_manpage({"name": "not-qrcode"}) + mock_file.assert_not_called() + + +@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) +def test_no_change(mock_file): + update_manpage({"name": "qrcode", "new_version": "version"}) + mock_file.assert_called() + mock_file().write.assert_not_called() + + +@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) +def test_change(mock_file): + update_manpage({"name": "qrcode", "new_version": "3.11"}) + expected = re.split(r"([^\n]*(?:\n|$))", DATA)[1::2] + expected[1] = ( + expected[1] + .replace("version", "3.11") + .replace("date", datetime.datetime.now().strftime("%-d %b %Y")) + ) + mock_file().write.assert_has_calls( + [mock.call(line) for line in expected if line != ""], any_order=True + ) diff --git a/python_modules/qrcode/tests/test_script.py b/python_modules/qrcode/tests/test_script.py new file mode 100644 index 000000000..d6338ded4 --- /dev/null +++ b/python_modules/qrcode/tests/test_script.py @@ -0,0 +1,97 @@ +import sys +from unittest import mock + +import pytest + +from qrcode.console_scripts import commas, main + + +def bad_read(): + raise UnicodeDecodeError("utf-8", b"0x80", 0, 1, "invalid start byte") + + +@mock.patch("os.isatty", lambda *args: True) +@mock.patch("qrcode.main.QRCode.print_ascii") +def test_isatty(mock_print_ascii): + main(["testtext"]) + mock_print_ascii.assert_called_with(tty=True) + + +@mock.patch("os.isatty", lambda *args: False) +def test_piped(): + pytest.importorskip("PIL", reason="Requires PIL") + main(["testtext"]) + + +@mock.patch("os.isatty", lambda *args: True) +def test_stdin(): + with mock.patch("qrcode.main.QRCode.print_ascii") as mock_print_ascii: + with mock.patch("sys.stdin") as mock_stdin: + mock_stdin.buffer.read.return_value = "testtext" + main([]) + assert mock_stdin.buffer.read.called + mock_print_ascii.assert_called_with(tty=True) + + +@mock.patch("os.isatty", lambda *args: True) +def test_stdin_py3_unicodedecodeerror(): + with mock.patch("qrcode.main.QRCode.print_ascii") as mock_print_ascii: + with mock.patch("sys.stdin") as mock_stdin: + mock_stdin.buffer.read.return_value = "testtext" + mock_stdin.read.side_effect = bad_read + # sys.stdin.read() will raise an error... + with pytest.raises(UnicodeDecodeError): + sys.stdin.read() + # ... but it won't be used now. + main([]) + mock_print_ascii.assert_called_with(tty=True) + + +def test_optimize(): + pytest.importorskip("PIL", reason="Requires PIL") + main("testtext --optimize 0".split()) + + +def test_factory(): + main(["testtext", "--factory", "svg"]) + + +def test_bad_factory(): + with pytest.raises(SystemExit): + main(["testtext", "--factory", "nope"]) + + +@mock.patch.object(sys, "argv", "qr testtext output".split()) +def test_sys_argv(): + pytest.importorskip("PIL", reason="Requires PIL") + main() + + +def test_output(tmp_path): + pytest.importorskip("PIL", reason="Requires PIL") + main(["testtext", "--output", str(tmp_path / "test.png")]) + + +def test_factory_drawer_none(capsys): + pytest.importorskip("PIL", reason="Requires PIL") + with pytest.raises(SystemExit): + main("testtext --factory pil --factory-drawer nope".split()) + assert "The selected factory has no drawer aliases" in capsys.readouterr()[1] + + +def test_factory_drawer_bad(capsys): + with pytest.raises(SystemExit): + main("testtext --factory svg --factory-drawer sobad".split()) + assert "sobad factory drawer not found" in capsys.readouterr()[1] + + +def test_factory_drawer(capsys): + main("testtext --factory svg --factory-drawer circle".split()) + + +def test_commas(): + assert commas([]) == "" + assert commas(["A"]) == "A" + assert commas("AB") == "A or B" + assert commas("ABC") == "A, B or C" + assert commas("ABC", joiner="and") == "A, B and C" diff --git a/python_modules/qrcode/tests/test_util.py b/python_modules/qrcode/tests/test_util.py new file mode 100644 index 000000000..e57badbcc --- /dev/null +++ b/python_modules/qrcode/tests/test_util.py @@ -0,0 +1,11 @@ +import pytest + +from qrcode import util + + +def test_check_wrong_version(): + with pytest.raises(ValueError): + util.check_version(0) + + with pytest.raises(ValueError): + util.check_version(41) diff --git a/python_modules/qrcode/util.py b/python_modules/qrcode/util.py new file mode 100644 index 000000000..fe25548f9 --- /dev/null +++ b/python_modules/qrcode/util.py @@ -0,0 +1,584 @@ +import math +import re + +from qrcode import LUT, base, exceptions +from qrcode.base import RSBlock + +# QR encoding modes. +MODE_NUMBER = 1 << 0 +MODE_ALPHA_NUM = 1 << 1 +MODE_8BIT_BYTE = 1 << 2 +MODE_KANJI = 1 << 3 + +# Encoding mode sizes. +MODE_SIZE_SMALL = { + MODE_NUMBER: 10, + MODE_ALPHA_NUM: 9, + MODE_8BIT_BYTE: 8, + MODE_KANJI: 8, +} +MODE_SIZE_MEDIUM = { + MODE_NUMBER: 12, + MODE_ALPHA_NUM: 11, + MODE_8BIT_BYTE: 16, + MODE_KANJI: 10, +} +MODE_SIZE_LARGE = { + MODE_NUMBER: 14, + MODE_ALPHA_NUM: 13, + MODE_8BIT_BYTE: 16, + MODE_KANJI: 12, +} + +ALPHA_NUM = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" +RE_ALPHA_NUM = re.compile(b"^[" + re.escape(ALPHA_NUM) + rb"]*\Z") + +# The number of bits for numeric delimited data lengths. +NUMBER_LENGTH = {3: 10, 2: 7, 1: 4} + +PATTERN_POSITION_TABLE = [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170], +] + +G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0) +G18 = ( + (1 << 12) + | (1 << 11) + | (1 << 10) + | (1 << 9) + | (1 << 8) + | (1 << 5) + | (1 << 2) + | (1 << 0) +) +G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1) + +PAD0 = 0xEC +PAD1 = 0x11 + + +# Precompute bit count limits, indexed by error correction level and code size +def _data_count(block): + return block.data_count + + +BIT_LIMIT_TABLE = [ + [0] + + [ + 8 * sum(map(_data_count, base.rs_blocks(version, error_correction))) + for version in range(1, 41) + ] + for error_correction in range(4) +] + + +def BCH_type_info(data): + d = data << 10 + while BCH_digit(d) - BCH_digit(G15) >= 0: + d ^= G15 << (BCH_digit(d) - BCH_digit(G15)) + + return ((data << 10) | d) ^ G15_MASK + + +def BCH_type_number(data): + d = data << 12 + while BCH_digit(d) - BCH_digit(G18) >= 0: + d ^= G18 << (BCH_digit(d) - BCH_digit(G18)) + return (data << 12) | d + + +def BCH_digit(data): + digit = 0 + while data != 0: + digit += 1 + data >>= 1 + return digit + + +def pattern_position(version): + return PATTERN_POSITION_TABLE[version - 1] + + +def mask_func(pattern): + """ + Return the mask function for the given mask pattern. + """ + if pattern == 0: # 000 + return lambda i, j: (i + j) % 2 == 0 + if pattern == 1: # 001 + return lambda i, j: i % 2 == 0 + if pattern == 2: # 010 + return lambda i, j: j % 3 == 0 + if pattern == 3: # 011 + return lambda i, j: (i + j) % 3 == 0 + if pattern == 4: # 100 + return lambda i, j: (math.floor(i / 2) + math.floor(j / 3)) % 2 == 0 + if pattern == 5: # 101 + return lambda i, j: (i * j) % 2 + (i * j) % 3 == 0 + if pattern == 6: # 110 + return lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0 + if pattern == 7: # 111 + return lambda i, j: ((i * j) % 3 + (i + j) % 2) % 2 == 0 + raise TypeError("Bad mask pattern: " + pattern) # pragma: no cover + + +def mode_sizes_for_version(version): + if version < 10: + return MODE_SIZE_SMALL + elif version < 27: + return MODE_SIZE_MEDIUM + else: + return MODE_SIZE_LARGE + + +def length_in_bits(mode, version): + if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE, MODE_KANJI): + raise TypeError(f"Invalid mode ({mode})") # pragma: no cover + + check_version(version) + + return mode_sizes_for_version(version)[mode] + + +def check_version(version): + if version < 1 or version > 40: + raise ValueError(f"Invalid version (was {version}, expected 1 to 40)") + + +def lost_point(modules): + modules_count = len(modules) + + lost_point = 0 + + lost_point = _lost_point_level1(modules, modules_count) + lost_point += _lost_point_level2(modules, modules_count) + lost_point += _lost_point_level3(modules, modules_count) + lost_point += _lost_point_level4(modules, modules_count) + + return lost_point + + +def _lost_point_level1(modules, modules_count): + lost_point = 0 + + modules_range = range(modules_count) + container = [0] * (modules_count + 1) + + for row in modules_range: + this_row = modules[row] + previous_color = this_row[0] + length = 0 + for col in modules_range: + if this_row[col] == previous_color: + length += 1 + else: + if length >= 5: + container[length] += 1 + length = 1 + previous_color = this_row[col] + if length >= 5: + container[length] += 1 + + for col in modules_range: + previous_color = modules[0][col] + length = 0 + for row in modules_range: + if modules[row][col] == previous_color: + length += 1 + else: + if length >= 5: + container[length] += 1 + length = 1 + previous_color = modules[row][col] + if length >= 5: + container[length] += 1 + + lost_point += sum( + container[each_length] * (each_length - 2) + for each_length in range(5, modules_count + 1) + ) + + return lost_point + + +def _lost_point_level2(modules, modules_count): + lost_point = 0 + + modules_range = range(modules_count - 1) + for row in modules_range: + this_row = modules[row] + next_row = modules[row + 1] + # use iter() and next() to skip next four-block. e.g. + # d a f if top-right a != b bottom-right, + # c b e then both abcd and abef won't lost any point. + modules_range_iter = iter(modules_range) + for col in modules_range_iter: + top_right = this_row[col + 1] + if top_right != next_row[col + 1]: + # reduce 33.3% of runtime via next(). + # None: raise nothing if there is no next item. + next(modules_range_iter, None) + elif top_right != this_row[col]: + continue + elif top_right != next_row[col]: + continue + else: + lost_point += 3 + + return lost_point + + +def _lost_point_level3(modules, modules_count): + # 1 : 1 : 3 : 1 : 1 ratio (dark:light:dark:light:dark) pattern in + # row/column, preceded or followed by light area 4 modules wide. From ISOIEC. + # pattern1: 10111010000 + # pattern2: 00001011101 + modules_range = range(modules_count) + modules_range_short = range(modules_count - 10) + lost_point = 0 + + for row in modules_range: + this_row = modules[row] + modules_range_short_iter = iter(modules_range_short) + col = 0 + for col in modules_range_short_iter: + if ( + not this_row[col + 1] + and this_row[col + 4] + and not this_row[col + 5] + and this_row[col + 6] + and not this_row[col + 9] + and ( + this_row[col + 0] + and this_row[col + 2] + and this_row[col + 3] + and not this_row[col + 7] + and not this_row[col + 8] + and not this_row[col + 10] + or not this_row[col + 0] + and not this_row[col + 2] + and not this_row[col + 3] + and this_row[col + 7] + and this_row[col + 8] + and this_row[col + 10] + ) + ): + lost_point += 40 + # horspool algorithm. + # if this_row[col + 10]: + # pattern1 shift 4, pattern2 shift 2. So min=2. + # else: + # pattern1 shift 1, pattern2 shift 1. So min=1. + if this_row[col + 10]: + next(modules_range_short_iter, None) + + for col in modules_range: + modules_range_short_iter = iter(modules_range_short) + row = 0 + for row in modules_range_short_iter: + if ( + not modules[row + 1][col] + and modules[row + 4][col] + and not modules[row + 5][col] + and modules[row + 6][col] + and not modules[row + 9][col] + and ( + modules[row + 0][col] + and modules[row + 2][col] + and modules[row + 3][col] + and not modules[row + 7][col] + and not modules[row + 8][col] + and not modules[row + 10][col] + or not modules[row + 0][col] + and not modules[row + 2][col] + and not modules[row + 3][col] + and modules[row + 7][col] + and modules[row + 8][col] + and modules[row + 10][col] + ) + ): + lost_point += 40 + if modules[row + 10][col]: + next(modules_range_short_iter, None) + + return lost_point + + +def _lost_point_level4(modules, modules_count): + dark_count = sum(map(sum, modules)) + percent = float(dark_count) / (modules_count**2) + # Every 5% departure from 50%, rating++ + rating = int(abs(percent * 100 - 50) / 5) + return rating * 10 + + +def optimal_data_chunks(data, minimum=4): + """ + An iterator returning QRData chunks optimized to the data content. + + :param minimum: The minimum number of bytes in a row to split as a chunk. + """ + data = to_bytestring(data) + num_pattern = rb"\d" + alpha_pattern = b"[" + re.escape(ALPHA_NUM) + b"]" + if len(data) <= minimum: + num_pattern = re.compile(b"^" + num_pattern + b"+$") + alpha_pattern = re.compile(b"^" + alpha_pattern + b"+$") + else: + re_repeat = b"{" + str(minimum).encode("ascii") + b",}" + num_pattern = re.compile(num_pattern + re_repeat) + alpha_pattern = re.compile(alpha_pattern + re_repeat) + num_bits = _optimal_split(data, num_pattern) + for is_num, chunk in num_bits: + if is_num: + yield QRData(chunk, mode=MODE_NUMBER, check_data=False) + else: + for is_alpha, sub_chunk in _optimal_split(chunk, alpha_pattern): + mode = MODE_ALPHA_NUM if is_alpha else MODE_8BIT_BYTE + yield QRData(sub_chunk, mode=mode, check_data=False) + + +def _optimal_split(data, pattern): + while data: + match = re.search(pattern, data) + if not match: + break + start, end = match.start(), match.end() + if start: + yield False, data[:start] + yield True, data[start:end] + data = data[end:] + if data: + yield False, data + + +def to_bytestring(data): + """ + Convert data to a (utf-8 encoded) byte-string if it isn't a byte-string + already. + """ + if not isinstance(data, bytes): + data = str(data).encode("utf-8") + return data + + +def optimal_mode(data): + """ + Calculate the optimal mode for this chunk of data. + """ + if data.isdigit(): + return MODE_NUMBER + if RE_ALPHA_NUM.match(data): + return MODE_ALPHA_NUM + return MODE_8BIT_BYTE + + +class QRData: + """ + Data held in a QR compatible format. + + Doesn't currently handle KANJI. + """ + + def __init__(self, data, mode=None, check_data=True): + """ + If ``mode`` isn't provided, the most compact QR data type possible is + chosen. + """ + if check_data: + data = to_bytestring(data) + + if mode is None: + self.mode = optimal_mode(data) + else: + self.mode = mode + if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE): + raise TypeError(f"Invalid mode ({mode})") # pragma: no cover + if check_data and mode < optimal_mode(data): # pragma: no cover + raise ValueError(f"Provided data can not be represented in mode {mode}") + + self.data = data + + def __len__(self): + return len(self.data) + + def write(self, buffer): + if self.mode == MODE_NUMBER: + for i in range(0, len(self.data), 3): + chars = self.data[i : i + 3] + bit_length = NUMBER_LENGTH[len(chars)] + buffer.put(int(chars), bit_length) + elif self.mode == MODE_ALPHA_NUM: + for i in range(0, len(self.data), 2): + chars = self.data[i : i + 2] + if len(chars) > 1: + buffer.put( + ALPHA_NUM.find(chars[0]) * 45 + ALPHA_NUM.find(chars[1]), 11 + ) + else: + buffer.put(ALPHA_NUM.find(chars), 6) + else: + # Iterating a bytestring in Python 3 returns an integer, + # no need to ord(). + data = self.data + for c in data: + buffer.put(c, 8) + + def __repr__(self): + return repr(self.data) + + +class BitBuffer: + def __init__(self): + self.buffer: list[int] = [] + self.length = 0 + + def __repr__(self): + return ".".join([str(n) for n in self.buffer]) + + def get(self, index): + buf_index = math.floor(index / 8) + return ((self.buffer[buf_index] >> (7 - index % 8)) & 1) == 1 + + def put(self, num, length): + for i in range(length): + self.put_bit(((num >> (length - i - 1)) & 1) == 1) + + def __len__(self): + return self.length + + def put_bit(self, bit): + buf_index = self.length // 8 + if len(self.buffer) <= buf_index: + self.buffer.append(0) + if bit: + self.buffer[buf_index] |= 0x80 >> (self.length % 8) + self.length += 1 + + +def create_bytes(buffer: BitBuffer, rs_blocks: list[RSBlock]): + offset = 0 + + maxDcCount = 0 + maxEcCount = 0 + + dcdata: list[list[int]] = [] + ecdata: list[list[int]] = [] + + for rs_block in rs_blocks: + dcCount = rs_block.data_count + ecCount = rs_block.total_count - dcCount + + maxDcCount = max(maxDcCount, dcCount) + maxEcCount = max(maxEcCount, ecCount) + + current_dc = [0xFF & buffer.buffer[i + offset] for i in range(dcCount)] + offset += dcCount + + # Get error correction polynomial. + if ecCount in LUT.rsPoly_LUT: + rsPoly = base.Polynomial(LUT.rsPoly_LUT[ecCount], 0) + else: + rsPoly = base.Polynomial([1], 0) + for i in range(ecCount): + rsPoly = rsPoly * base.Polynomial([1, base.gexp(i)], 0) + + rawPoly = base.Polynomial(current_dc, len(rsPoly) - 1) + + modPoly = rawPoly % rsPoly + current_ec = [] + mod_offset = len(modPoly) - ecCount + for i in range(ecCount): + modIndex = i + mod_offset + current_ec.append(modPoly[modIndex] if (modIndex >= 0) else 0) + + dcdata.append(current_dc) + ecdata.append(current_ec) + + data = [] + for i in range(maxDcCount): + for dc in dcdata: + if i < len(dc): + data.append(dc[i]) + for i in range(maxEcCount): + for ec in ecdata: + if i < len(ec): + data.append(ec[i]) + + return data + + +def create_data(version, error_correction, data_list): + buffer = BitBuffer() + for data in data_list: + buffer.put(data.mode, 4) + buffer.put(len(data), length_in_bits(data.mode, version)) + data.write(buffer) + + # Calculate the maximum number of bits for the given version. + rs_blocks = base.rs_blocks(version, error_correction) + bit_limit = sum(block.data_count * 8 for block in rs_blocks) + if len(buffer) > bit_limit: + raise exceptions.DataOverflowError( + "Code length overflow. Data size (%s) > size available (%s)" + % (len(buffer), bit_limit) + ) + + # Terminate the bits (add up to four 0s). + for _ in range(min(bit_limit - len(buffer), 4)): + buffer.put_bit(False) + + # Delimit the string into 8-bit words, padding with 0s if necessary. + delimit = len(buffer) % 8 + if delimit: + for _ in range(8 - delimit): + buffer.put_bit(False) + + # Add special alternating padding bitstrings until buffer is full. + bytes_to_fill = (bit_limit - len(buffer)) // 8 + for i in range(bytes_to_fill): + if i % 2 == 0: + buffer.put(PAD0, 8) + else: + buffer.put(PAD1, 8) + + return create_bytes(buffer, rs_blocks) diff --git a/python_modules/workers/__init__.py b/python_modules/workers/__init__.py new file mode 100644 index 000000000..765cf2bba --- /dev/null +++ b/python_modules/workers/__init__.py @@ -0,0 +1,66 @@ +from ._workers import ( + Blob, + BlobEnding, + BlobValue, + Body, + Context, + DurableObject, + FetchKwargs, + FetchResponse, + File, + FormData, + FormDataValue, + Headers, + JSBody, + Request, + RequestInitCfProperties, + Response, + WorkerEntrypoint, + WorkflowEntrypoint, + fetch, + handler, + import_from_javascript, + patch_env, + python_from_rpc, + python_to_rpc, +) + +__all__ = [ + "Blob", + "BlobEnding", + "BlobValue", + "Body", + "Context", + "DurableObject", + "FetchKwargs", + "FetchResponse", + "File", + "FormData", + "FormDataValue", + "Headers", + "JSBody", + "Request", + "RequestInitCfProperties", + "Response", + "WorkerEntrypoint", + "WorkflowEntrypoint", + "env", + "fetch", + "handler", + "import_from_javascript", + "patch_env", + "python_from_rpc", + "python_to_rpc", + "waitUntil", + "wait_until", +] + + +def __getattr__(key): + if key == "env": + cloudflare_workers = import_from_javascript("cloudflare:workers") + return cloudflare_workers.env + if key in ("wait_until", "waitUntil"): + cloudflare_workers = import_from_javascript("cloudflare:workers") + return cloudflare_workers.waitUntil + raise AttributeError(f"module {__name__!r} has no attribute {key!r}") diff --git a/python_modules/workers/_workers.py b/python_modules/workers/_workers.py new file mode 100644 index 000000000..95f47781b --- /dev/null +++ b/python_modules/workers/_workers.py @@ -0,0 +1,1454 @@ +# This module defines a Workers API for Python. It is similar to the API provided by +# JS Workers, but with changes and additions to be more idiomatic to the Python +# programming language. +import datetime +import functools +import inspect +import json +from asyncio import create_task, gather +from collections.abc import ( + Awaitable, + Generator, + Iterable, + Iterator, + MutableMapping, + Sequence, +) +from contextlib import ExitStack, contextmanager +from enum import StrEnum +from http import HTTPMethod, HTTPStatus +from types import LambdaType +from typing import TYPE_CHECKING, Any, Never, Protocol, TypedDict, Unpack + +import _cloudflare_compat_flags + +# Get globals modules and import function from the entrypoint-helper +import _pyodide_entrypoint_helper +import js +import pyodide.http +from js import Object +from pyodide import __version__ as pyodide_version +from pyodide.ffi import ( + JsBuffer, + JsException, + JsProxy, + create_once_callable, + create_proxy, + destroy_proxies, + to_js, +) +from pyodide.http import pyfetch + +from workers.workflows import NonRetryableError + +if TYPE_CHECKING: + from js import DurableObjectState, Env, ExecutionContext + + +class Context(Protocol): + def waitUntil(self, other: Awaitable[Any]) -> None: ... + + +try: + from pyodide.ffi import jsnull +except ImportError: + jsnull = None + + +def _jsnull_to_none(x): + if x is jsnull: + return None + return x + + +def import_from_javascript(module_name: str) -> Any: + """ + Import a JavaScript ES module from Python. + + Args: + module_name: The name of the module to import. This can be a module name or a path. + + Returns: + The imported module object. + + Example: + cloudflare_workers = import_from_javascript("cloudflare:workers") + env = cloudflare_workers.env + + Note: + Behind the scenes import_from_javascript uses JSPI to do imports but that means we need an + async context. To enable importing cloudflare:workers and cloudflare:sockets in the global + scope we specifically imported them in the global scope and exposed them here. + """ + # Special case for global scope available modules + # JSPI won't work in the global scope in 0.26.0a2 so we need modules importable in the global + # scope to be imported beforehand. + if module_name == "cloudflare:workers": + return _pyodide_entrypoint_helper.cloudflareWorkersModule + elif module_name == "cloudflare:sockets": + return _pyodide_entrypoint_helper.cloudflareSocketsModule + + try: + from pyodide.ffi import run_sync + + # Call the JavaScript import function + return run_sync(_pyodide_entrypoint_helper.doAnImport(module_name)) + except JsException as e: + raise ImportError(f"Failed to import '{module_name}': {e}") from e + except RuntimeError as e: + if e.args[0] == "No suspender": + raise ImportError( + f"Failed to import '{module_name}': Only 'cloudflare:workers' and 'cloudflare:sockets' are available in the global scope." + ) from e + raise + except ImportError as e: + if e.args[0].startswith("cannot import name 'run_sync' from 'pyodide.ffi'"): + raise ImportError( + f"Failed to import '{module_name}': Only 'cloudflare:workers' and 'cloudflare:sockets' are available until the next python runtime version." + ) from e + raise + + +@contextmanager +def patch_env( + d: dict[str, Any] | Sequence[tuple[str, Any]] | None = None, **kwds: dict[str, Any] +) -> Iterator[None]: + if d: + kwds = dict(d) | kwds + yield from _pyodide_entrypoint_helper.patch_env_helper(to_js(kwds)) + + +type JSBody = ( + "js.Blob | JsBuffer | js.FormData | js.ReadableStream | js.URLSearchParams" +) +type Body = "str | FormData | JSBody" +type Headers = "dict[str, str] | list[tuple[str, str]] | js.Headers" + + +# https://developers.cloudflare.com/workers/runtime-apis/request/#the-cf-property-requestinitcfproperties +class RequestInitCfProperties(TypedDict, total=False): + apps: bool | None + cacheEverything: bool | None + cacheKey: str | None + cacheTags: list[str] | None + cacheTtl: int + cacheTtlByStatus: dict[str, int] + image: ( + Any | None + ) # TODO: https://developers.cloudflare.com/images/transform-images/transform-via-workers/ + mirage: bool | None + polish: str | None + resolveOverride: str | None + scrapeShield: bool | None + webp: bool | None + + +# This matches the Request options: +# https://developers.cloudflare.com/workers/runtime-apis/request/#options +class FetchKwargs(TypedDict, total=False): + headers: "Headers | None" + body: "Body | None" + method: HTTPMethod | None + redirect: str | None + cf: RequestInitCfProperties | None + fetcher: type[pyfetch] | None + + +# TODO: Pyodide's FetchResponse.headers returns a dict[str, str] which means +# duplicates are lost, we should fix that so it returns a http.client.HTTPMessage +class FetchResponse(pyodide.http.FetchResponse): + # TODO: Consider upstreaming the `body` attribute + # TODO: Behind a compat flag make this return a native stream (StreamReader?), or perhaps + # behind a different name, maybe `stream`? + @property + def body(self) -> "js.ReadableStream": + """ + Returns the body as a JavaScript ReadableStream from the JavaScript Response instance. + """ + return _jsnull_to_none(self.js_response.body) + + @property + def js_object(self) -> "js.Response": + return self.js_response + + """ + Instance methods defined below. + + Some methods are implemented by `FetchResponse`, these include `buffer` + (replacing JavaScript's `arrayBuffer`), `bytes`, `json`, and `text`. + + There are also some additional methods implemented by `FetchResponse`. + See https://pyodide.org/en/stable/usage/api/python-api/http.html#pyodide.http.FetchResponse + for details. + """ + + async def formData(self) -> "FormData": # TODO: Remove after certain compat date. + return await self.form_data() + + async def form_data(self) -> "FormData": + self._raise_if_failed() + try: + return FormData(await self.js_response.formData()) + except JsException as exc: + raise _to_python_exception(exc) from exc + + def replace_body(self, body: Body) -> "Response": + """ + Returns a new Response object with the same options (status, headers, etc) as + the original but with an updated body. + """ + b = body.js_object if isinstance(body, FormData) else body + js_resp = js.Response.new(b, self.js_response) + return Response(js_resp) + + async def blob(self) -> "Blob": + self._raise_if_failed() + return Blob(await self.js_object.blob()) + + """ + Static methods defined below. The `error` static method is not implemented as + it is not useful for the Workers use case. + """ + + @staticmethod + def redirect(url: str, status: HTTPStatus | int = HTTPStatus.FOUND): + code = status.value if isinstance(status, HTTPStatus) else status + try: + return js.Response.redirect(url, code) + except JsException as exc: + raise _to_python_exception(exc) from exc + + @staticmethod + def from_json( + data: str | dict[str, Any] | list[Any] | JsProxy, + status: HTTPStatus | int = HTTPStatus.OK, + status_text="", + headers: Headers = None, + ) -> "Response": + options = Response._create_options(status, status_text, headers) + js_resp = None + try: + if isinstance(data, JsProxy): + js_resp = js.Response.json(data, **options) + else: + if "headers" not in options: + options["headers"] = _to_js_headers( + {"content-type": "application/json"} + ) + elif not options["headers"].has("content-type"): + options["headers"].set("content-type", "application/json") + js_resp = js.Response.new(json.dumps(data), **options) + except JsException as exc: + raise _to_python_exception(exc) from exc + + return Response(js_resp) + + def json(self, *args: Never, **kwargs: Never): + if isinstance(self, Response): + return super().json() + # For compatibility, allow static use of Response.json() to mean Response.from_json(). + data = self + return Response.from_json(data, *args, **kwargs) + + +if pyodide_version == "0.26.0a2": + + async def _pyfetch_patched( + request: "str | js.Request", **kwargs: Any + ) -> "Response": + # This is copied from https://github.com/pyodide/pyodide/blob/d3f99e1d/src/py/pyodide/http.py + custom_fetch = kwargs["fetcher"] if "fetcher" in kwargs else js.fetch + kwargs["fetcher"] = None + try: + return Response( + await custom_fetch( + request, to_js(kwargs, dict_converter=Object.fromEntries) + ), + ) + except JsException as e: + raise OSError(e.message) from None +else: + _pyfetch_patched = pyfetch + + +async def fetch( + resource: "str | Request | js.Request", + **other_options: Unpack[FetchKwargs], +) -> "Response": + if isinstance(resource, Request): + resource = resource.js_object + if "method" in other_options and isinstance(other_options["method"], HTTPMethod): + other_options["method"] = other_options["method"].value + + resp = await _pyfetch_patched(resource, **other_options) + return Response(resp.js_response) + + +def _to_python_exception(exc: JsException) -> Exception: + if exc.name == "RangeError": + return ValueError(exc.message) + elif exc.name == "TypeError": + return TypeError(exc.message) + else: + return exc + + +def _from_js_error(exc: JsException) -> Exception: + # convert into Python exception after a full round trip + # Python - JS - Python + if not exc.message or not exc.message.startswith("PythonError"): + return _to_python_exception(exc) + + # extract the Python exception type from the traceback + error_message_last_line = exc.message.split("\n")[-2] + if error_message_last_line.startswith("TypeError"): + return TypeError(error_message_last_line) + elif error_message_last_line.startswith("ValueError"): + return ValueError(error_message_last_line) + elif error_message_last_line.startswith("workers.workflows.NonRetryableError"): + return NonRetryableError(error_message_last_line) + else: + return _to_python_exception(exc) + + +@contextmanager +def _manage_pyproxies(): + proxies = js.Array.new() + try: + yield proxies + finally: + destroy_proxies(proxies) + + +def _is_js_instance(val, js_cls_name): + return hasattr(val, "constructor") and val.constructor.name == js_cls_name + + +try: + import _cloudflare_compat_flags +except ImportError: + _cloudflare_compat_flags = object() + + +def get_compat_flag(flag: str) -> bool: + return getattr(_cloudflare_compat_flags, flag, False) + + +def _to_js_headers(headers: Headers): + if isinstance(headers, list): + # We should have a list[tuple[str, str]] + return js.Headers.new(headers) + elif isinstance(headers, dict): + return js.Headers.new(headers.items()) + elif _is_js_instance(headers, "Headers"): + return headers + else: + raise TypeError("Received unexpected type for headers argument") + + +@contextmanager +def _get_js_body(body): + if isinstance(body, bytes): + proxy_bytes = create_proxy(body) + proxy_buffer = proxy_bytes.getBuffer() + try: + yield proxy_buffer.data + return + finally: + proxy_buffer.release() + proxy_bytes.destroy() + if isinstance(body, FormData): + yield body.js_object + return + yield body + + +RESPONSE_ACCEPTED_TYPES = { + # BufferSource types + "Blob", + "ArrayBuffer", + "TypedArray", + "DataView", + "Uint8Array", + "Uint8ClampedArray", + "Int8Array", + "Uint16Array", + "Int16Array", + "Uint32Array", + "Int32Array", + "Float16Array", + "Float32Array", + "Float64Array", + "BigInt64Array", + "BigUint64Array", + # Other types + "FormData", + "ReadableStream", + "URLSearchParams", + "Response", +} + + +class Response(FetchResponse): + """ + This class represents the response to an HTTP request, with a similar API to that of the web + `Response` API: https://developer.mozilla.org/en-US/docs/Web/API/Response. + """ + + def __init__( + self, + body: Body = None, + status: HTTPStatus | int | None = None, + status_text="", + headers: Headers = None, + web_socket: "js.WebSocket | None" = None, + ): + """ + Represents the response to a request. + + Based on the JS API of the same name: + https://developer.mozilla.org/en-US/docs/Web/API/Response/Response. + """ + # Verify passed in types. + if hasattr(body, "constructor"): + if body.constructor.name not in RESPONSE_ACCEPTED_TYPES: + raise TypeError( + f"Unsupported type in Response: {body.constructor.name}" + ) + elif not isinstance(body, str | FormData | bytes) and body is not None: + raise TypeError(f"Unsupported type in Response: {type(body).__name__}") + + # Handle constructing a Response from a JS Response. + if _is_js_instance(body, "Response"): + if status is not None or len(status_text) > 0 or headers is not None: + raise ValueError( + "Expected no options when constructing Response from a js.Response" + ) + super().__init__(body.url, body) + return + + options = self._create_options(status, status_text, headers, web_socket) + + # To avoid unnecessary copies we use this context manager. + with _get_js_body(body) as js_body: + # Initialize via the FetchResponse super-class which gives us access to + # methods that we would ordinarily have to redeclare. + js_resp = js.Response.new(js_body, **options) + super().__init__(js_resp.url, js_resp) + + def __repr__(self): + body = [f"status={self.status}"] + if self.js_object.statusText: + body.append(f"status_text={self.status_text!r}") + if "content-type" in self.headers: + body.append(f"content_type={self.headers['content-type']!r}") + if self.js_object.url: + body.append(f"url={self.js_object.url!r}") + if self.js_object.type != "default": + body.append(f"type={self.js_object.type!r}") + return f"Response({', '.join(body)})" + + @staticmethod + def _create_options( + status: HTTPStatus | int | None = HTTPStatus.OK, + status_text="", + headers: Headers = None, + web_socket: "js.WebSocket | None" = None, + ): + options = {} + if status: + options["status"] = ( + status.value if isinstance(status, HTTPStatus) else status + ) + if status_text: + options["statusText"] = status_text + if headers: + options["headers"] = _to_js_headers(headers) + if web_socket: + options["webSocket"] = web_socket + return options + + +FormDataValue = "str | js.Blob | Blob" + + +def _py_value_to_js(item: FormDataValue) -> "str | js.Blob": + if isinstance(item, Blob): + return item.js_object + else: + return item + + +def _js_value_to_py(item: FormDataValue) -> "str | Blob | File": + if hasattr(item, "constructor") and (item.constructor.name in ("Blob", "File")): + if item.constructor.name == "File": + return File(item, item.name) + else: + return Blob(item) + else: + return item + + +class FormData(MutableMapping[str, FormDataValue]): + """ + This class represents a set of key/value pairs for forms. + + The API of this class follows that of https://pypi.org/project/multidict/ and + https://developer.mozilla.org/en-US/docs/Web/API/FormData. + """ + + def __init__( + self, form_data: "js.FormData | None | dict[str, FormDataValue]" = None + ): + if not form_data: + self._js_form_data = js.FormData.new() + return + + if isinstance(form_data, dict): + self._js_form_data = js.FormData.new() + for k, v in form_data.items(): + self._js_form_data.append(k, _py_value_to_js(v)) + return + + if _is_js_instance(form_data, "FormData"): + self._js_form_data = form_data + return + + raise TypeError("Expected form_data to be a dict or an instance of FormData") + + def __getitem__(self, key: str) -> FormDataValue: + return _js_value_to_py(self._js_form_data.get(key)) + + def __setitem__(self, key: str, value: FormDataValue): + if isinstance(value, list): + raise TypeError("Expected single item in arguments to FormData.__setitem__") + self._js_form_data.set(key, _py_value_to_js(value)) + + def append(self, key: str, value: FormDataValue, filename: str | None = None): + self._js_form_data.append(key, _py_value_to_js(value), filename) + + def delete(self, key: str): + self._js_form_data.delete(key) + + def __contains__(self, key: str) -> bool: + return self._js_form_data.has(key) + + def values(self) -> Generator[FormDataValue, None, None]: + for val in self._js_form_data.values(): + yield _js_value_to_py(val) + + def keys(self) -> Generator[str, None, None]: + yield from self._js_form_data.keys() + + def __iter__(self): + yield from self.keys() + + def items(self) -> Generator[tuple[str, FormDataValue], None, None]: + for k, v in self._js_form_data.entries(): + yield (k, _js_value_to_py(v)) + + def __delitem__(self, key: str): + self.delete(key) + + def __len__(self): + return len(self.keys()) + + def get_all(self, key: str) -> list[FormDataValue]: + return [_js_value_to_py(x) for x in self._js_form_data.getAll(key)] + + @property + def js_object(self) -> "js.FormData": + return self._js_form_data + + +def _supports_buffer_protocol(o): + try: + # memoryview used only for testing type; 'with' releases the view instantly + with memoryview(o): + return True + except TypeError: + return False + + +@contextmanager +def _make_blob_entry(e): + if isinstance(e, str): + yield e + return + if isinstance(e, Blob): + yield e._js_blob + return + if hasattr(e, "constructor") and (e.constructor.name in ("Blob", "File")): + yield e + return + if _supports_buffer_protocol(e): + px = create_proxy(e) + buf = px.getBuffer() + try: + yield buf.data + return + finally: + buf.release() + px.destroy() + raise TypeError(f"Don't know how to handle {type(e)} for Blob()") + + +def _is_iterable(obj): + if isinstance(obj, (str, bytes)): + return False + try: + iter(obj) + except TypeError: + return False + else: + return True + + +BlobValue = ( + "str | bytes | js.ArrayBuffer | js.TypedArray | js.DataView | js.Blob | Blob | File" +) + + +class BlobEnding(StrEnum): + TRANSPARENT = "transparent" + NATIVE = "native" + + +class Blob: + def __init__( + self, + blob_parts: "Iterable[BlobValue] | BlobValue", + content_type: str | None = None, + endings: BlobEnding | str | None = None, + ): + if endings: + endings = str(endings) + + is_single_item = not _is_iterable(blob_parts) + if is_single_item: + # Inherit the content_type if we have a single item. If a File is passed + # in then its metadata is lost. + if not content_type and isinstance(blob_parts, Blob): + content_type = blob_parts.content_type + if hasattr(blob_parts, "constructor") and ( + blob_parts.constructor.name in ("Blob", "File") + ): + if not content_type: + content_type = blob_parts.type + + # Otherwise create a new Blob below. + blob_parts = [blob_parts] + + with ExitStack() as stack: + args = [stack.enter_context(_make_blob_entry(e)) for e in blob_parts] + with _manage_pyproxies() as pyproxies: + self._js_blob = js.Blob.new( + to_js(args, pyproxies=pyproxies), + type=content_type, + endings=endings, + ) + + @property + def size(self) -> int: + return self._js_blob.size + + @property + def content_type(self) -> str: + return self._js_blob.type + + @property + def js_object(self) -> "js.Blob": + return self._js_blob + + async def text(self) -> str: + return await self.js_object.text() + + async def bytes(self) -> bytes: + return (await self.js_object.arrayBuffer()).to_bytes() + + def slice( + self, + start: int | None = None, + end: int | None = None, + content_type: str | None = None, + ): + js_sliced_blob = self.js_object.slice(start, end, content_type) + return Blob([js_sliced_blob]) + + +class File(Blob): + def __init__( + self, + blob_parts: "Iterable[BlobValue] | BlobValue", + filename: str, + content_type: str | None = None, + endings: BlobEnding | str | None = None, + last_modified: int | None = None, + ): + if endings: + endings = str(endings) + + is_single_item = not _is_iterable(blob_parts) + if is_single_item: + # Inherit the content_type and lastModified if we have a + # single item. + if not content_type and isinstance(blob_parts, Blob): + content_type = blob_parts.content_type + if not last_modified and isinstance(blob_parts, File): + last_modified = blob_parts.last_modified + if hasattr(blob_parts, "constructor") and ( + blob_parts.constructor.name in ("Blob", "File") + ): + if not content_type: + content_type = blob_parts.type + if blob_parts.constructor.name == "File": + if not last_modified: + last_modified = blob_parts.lastModified + + # Otherwise create a new File below. + blob_parts = [blob_parts] + + with ExitStack() as stack: + args = [stack.enter_context(_make_blob_entry(e)) for e in blob_parts] + with _manage_pyproxies() as pyproxies: + self._js_blob = js.File.new( + to_js(args, pyproxies=pyproxies), + filename, + type=content_type, + endings=endings, + lastModified=last_modified, + ) + + @property + def name(self) -> str: + return self._js_blob.name + + @property + def last_modified(self) -> int: + return self._js_blob.lastModified + + +class Request: + def __init__( + self, input: "Request | str | js.Request", **other_options: Unpack[FetchKwargs] + ): + if _is_js_instance(input, "Request"): + if len(other_options) > 0: + raise ValueError( + "Expected no options when constructing Request from a js.Request" + ) + self._js_request = input + return + + if "method" in other_options and isinstance( + other_options["method"], HTTPMethod + ): + other_options["method"] = other_options["method"].value + + if "headers" in other_options: + other_options["headers"] = _to_js_headers(other_options["headers"]) + self._js_request = js.Request.new( + input._js_request if isinstance(input, Request) else input, **other_options + ) + + def __repr__(self): + return ( + f"Request(method={self._js_request.method!r}, url={self._js_request.url!r})" + ) + + @property + def js_object(self) -> "js.Request": + return self._js_request + + # TODO: expose `body` as a native Python stream in the future, follow how we define `Response` + @property + def body(self) -> "js.ReadableStream": + return self.js_object.body + + @property + def body_used(self) -> bool: + return self.js_object.bodyUsed + + @property + def cache(self) -> str: + return self.js_object.cache + + @property + def credentials(self) -> str: + return self.js_object.credentials + + @property + def destination(self) -> str: + return self.js_object.destination + + @property + def headers(self): + # This is imported here because it costs a lot of CPU time when imported at the top-level. + # At least it does when we do so in our validator tests, doesn't seem to cause trouble in + # production. So as a workaround we do the import here. + # + # TODO(later): when dedicated snapshots are default we can move this import to the top-level. + import http.client + + result = http.client.HTTPMessage() + if not get_compat_flag("python_request_headers_preserve_commas"): + for key, val in self.js_object.headers: + result[key] = val.strip() + + return result + + # With the exception of Set-Cookie, duplicate headers can and are combined with a comma + # in the JS Headers API. We do the same when returning the headers to Python. + # + # See https://httpwg.org/specs/rfc9110.html#rfc.section.5.3. + js_headers = self.js_object.headers + set_cookie_headers = js_headers.getSetCookie() + if set_cookie_headers: + for value in set_cookie_headers: + result.add_header("Set-Cookie", value.strip()) + + for key, val in js_headers: + if key.lower() == "set-cookie": + continue + result.add_header(key, val.strip()) + + return result + + @property + def integrity(self) -> str: + return self.js_object.integrity + + @property + def is_history_navigation(self) -> bool: + return self.js_object.isHistoryNavigation + + @property + def keepalive(self) -> bool: + return self.js_object.keepalive + + @property + def method(self) -> HTTPMethod: + return HTTPMethod[self.js_object.method] + + @property + def mode(self) -> str: + return self.js_object.mode + + @property + def redirect(self) -> str: + return self.js_object.redirect + + @property + def referrer(self) -> str: + return self.js_object.referrer + + @property + def referrer_policy(self) -> str: + return self.js_object.referrerPolicy + + @property + def url(self) -> str: + return self.js_object.url + + def _raise_if_failed(self) -> None: + # TODO: https://github.com/pyodide/pyodide/blob/a53c17fd8/src/py/pyodide/http.py#L252 + if self.body_used: + # TODO: Use BodyUsedError in newer Pyodide versions. + raise OSError("Body already used") + + """ + Instance methods defined below. + + The naming of these methods should match Request's methods when possible. + + TODO: AbortController support. + """ + + async def buffer(self) -> "js.ArrayBuffer": + # The naming of this method matches that of Response. + self._raise_if_failed() + return await self.js_object.arrayBuffer() + + async def form_data(self) -> "FormData": + self._raise_if_failed() + try: + return FormData(await self.js_object.formData()) + except JsException as exc: + raise _to_python_exception(exc) from exc + + async def blob(self) -> Blob: + self._raise_if_failed() + return Blob(await self.js_object.blob()) + + async def bytes(self) -> bytes: + self._raise_if_failed() + return (await self.buffer()).to_bytes() + + def clone(self) -> "Request": + if self.body_used: + # TODO: Use BodyUsedError in newer Pyodide versions. + raise OSError("Body already used") + return Request( + self.js_object.clone(), + ) + + async def json(self, **kwargs: Any) -> Any: + self._raise_if_failed() + return json.loads(await self.text(), **kwargs) + + async def text(self) -> str: + self._raise_if_failed() + return await self.js_object.text() + + +def _python_from_rpc_default_converter(value, convert, cache): + if not hasattr(value, "constructor"): + # Assume that the object doesn't need conversion as it's not a JS object. + return value + + if value.constructor.name == "Response": + return Response(value) + elif value.constructor.name == "FormData": + return FormData(value) + elif value.constructor.name == "Blob": + return Blob(value) + elif value.constructor.name == "File": + return File(value) + elif value.constructor.name == "Request": + return Request(value) + elif value.constructor.name == "Date": + # TODO: Pyodide should gain support for this, we should upstream this. + return datetime.datetime.fromtimestamp(value.getTime() / 1000) + elif value.constructor.name == "Error": + return Exception(value.toString()) + elif value.constructor.name == "Number": + return value.valueOf() + + # We used to throw an error here, but since these conversions are now automatic when the default + # entrypoint is being used, it makes sense to be less loud about it and just pass through the + # JS value un-modified. + # + # This does mean that in the future we need to be careful when adding type wrappers for new + # types here, so if you're doing this make sure to do so behind a compat flag. + return value + + +def python_from_rpc(obj: "JsProxy"): + """ + Converts JS objects like Response, Request, Blob, etc. to equivalent Python objects defined in + this module and also other JS objects like Map, Set, etc. to equivalent Python stdlib objects. + + This method is used for Workers RPC in Python to convert JavaScript objects to Python. As such + it does not support serializing all JS object types. + """ + + if not hasattr(obj, "constructor"): + return obj + + if obj.constructor.name == "TestController": + # This object currently has no methods defined on it. If this changes we should + # implement a Python wrapper for it, but for now we'll just pass in None. + return None + + result = obj.to_py(default_converter=_python_from_rpc_default_converter) + + return result + + +def _raise_on_disabled_type(value): + if _is_js_instance(value, "RegExp"): + raise TypeError(f"{value.constructor.name} cannot be sent over RPC.") + + if isinstance(value, (tuple, bytearray, LambdaType)): + raise TypeError(f"{type(value)} cannot be sent over RPC.") + + if inspect.isawaitable(value): + # The caller is expected to await the value prior to conversion. + raise TypeError(f"Awaitable {type(value)} cannot be sent over RPC.") + + if _is_iterable(value): + if isinstance(value, dict): + for v in value.values(): + _raise_on_disabled_type(v) + else: + for v in value: + _raise_on_disabled_type(v) + + +def _python_to_rpc_default_converter(obj, convert, cache): + if obj is None: + return obj + + if hasattr(obj, "js_object"): + return obj.js_object + + if isinstance(obj, datetime.datetime): + # TODO: Pyodide should gain support for this, we should upstream this. + return js.Date.new(obj.timestamp() * 1000) + + if isinstance(obj, Exception): + return js.Error.new(str(obj)) + + _raise_on_disabled_type(obj) + + return obj + + +def python_to_rpc(value) -> JsProxy: + """ + Converts Python objects defined in this module (Response, Request, etc) and native Python types + like Map, Set, datetime to equivalent JavaScript types. + + This method is used for Workers RPC in Python to convert Python objects to JavaScript. As such + it does not support serializing all Python object types. + """ + + # `to_js` won't always call the default_converter, for example when a list of tuples is passed + _raise_on_disabled_type(value) + + result = to_js( + value, + default_converter=_python_to_rpc_default_converter, + dict_converter=js.Map.new, + ) + + return result + + +class _FetcherWrapper: + def __init__(self, binding): + self._binding = binding + + def _getattr_helper(self, name): + attr = getattr(self._binding, name) + + if not callable(attr): + return attr + + # Not using `@functools.wraps(attr)` here because `attr` is a JS proxy. + async def wrapper(*args, **kwargs): + js_args = [python_to_rpc(arg) for arg in args] + js_kwargs = {k: python_to_rpc(v) for k, v in kwargs.items()} + result = attr(*js_args, **js_kwargs) + if hasattr(result, "then") and callable(result.then): + return python_from_rpc(await result) + else: + return python_from_rpc(result) + + return wrapper + + def __getattr__(self, name): + result = self._getattr_helper(name) + setattr(self, name, result) + return result + + def fetch(self, *args, **kwargs): + return fetch(*args, fetcher=self._binding.fetch, **kwargs) + + +class _DurableObjectNamespaceWrapper: + def __init__(self, binding): + self._binding = binding + + def __getattr__(self, name): + return getattr(self._binding, name) + + def get(self, *args, **kwargs): + return _FetcherWrapper(self._binding.get(*args, **kwargs)) + + def getByName(self, *args, **kwargs): + return _FetcherWrapper(self._binding.getByName(*args, **kwargs)) + + def jurisdiction(self, *args, **kwargs): + return _DurableObjectNamespaceWrapper( + self._binding.jurisdiction(*args, **kwargs) + ) + + +class DurableObjectAbort(BaseException): + pass + + +class DurableObjectContext: + def __init__(self, ctx: "DurableObjectState"): + self._ctx = ctx + + def __getattr__(self, name: str): + result = getattr(self._ctx, name) + setattr(self, name, result) + return result + + def abort(self, reason: str | None = None): + # DurableObjectState.abort() terminates JS execution immediately. If Python + # calls it synchronously while asyncio is still running the task in the event loop, + # V8 unwinds the stack before asyncio can run its task-exit cleanup, leaving + # stale task state behind for the next request. + # + # Therefore, we queue the real abort into a microtask so Python can unwind first, + # then raise BaseException to stop user code without being swallowed by + # `except Exception` handlers. + ctx = self._ctx + + if reason is None: + callback = create_once_callable(lambda: ctx.abort()) + else: + callback = create_once_callable(lambda: ctx.abort(reason)) + + js.queueMicrotask(callback) + raise DurableObjectAbort(reason or "Durable Object abort requested") + + +class _WorkflowInstanceWrapper: + def __init__(self, binding): + self._binding = binding + + def __getattr__(self, name): + return getattr(self._binding, name) + + async def send_event(self, *args, **kwargs): + return self._binding.sendEvent(*args, **kwargs) + + async def pause(self, *args, **kwargs): + return self._binding.pause(*args, **kwargs) + + async def resume(self, *args, **kwargs): + return self._binding.resume(*args, **kwargs) + + async def terminate(self, *args, **kwargs): + return self._binding.terminate(*args, **kwargs) + + async def restart(self, *args, **kwargs): + return self._binding.restart(*args, **kwargs) + + async def status(self, *args, **kwargs): + return self._binding.status(*args, **kwargs) + + +class _WorkflowBindingWrapper: + def __init__(self, binding): + self._binding = binding + + def __getattr__(self, name): + return getattr(self._binding, name) + + async def get(self, *args, **kwargs): + return _WorkflowInstanceWrapper(await self._binding.get(*args, **kwargs)) + + async def create(self, *args, **kwargs): + return _WorkflowInstanceWrapper(await self._binding.create(*args, **kwargs)) + + async def create_batch(self, *args, **kwargs): + return [ + _WorkflowInstanceWrapper(w) + for w in await self._binding.createBatch(*args, **kwargs) + ] + + +class _EnvWrapper: + def __init__(self, env: Any): + self._env = env + + def _getattr_helper(self, name): + binding = getattr(self._env, name) + if _is_js_instance(binding, "Fetcher"): + return _FetcherWrapper(binding) + + if _is_js_instance(binding, "DurableObjectNamespace"): + return _DurableObjectNamespaceWrapper(binding) + + if _is_js_instance(binding, "WorkflowImpl"): + return _WorkflowBindingWrapper(binding) + + # TODO: Implement APIs for bindings. + return binding + + def __getattr__(self, name): + result = self._getattr_helper(name) + setattr(self, name, result) + return result + + +def handler(func): + """ + When applied to handlers such as `on_fetch` it will rewrite arguments passed in to native Python + types defined in this module. For example, the `request` argument to `on_fetch` gets converted + to an instance of the Request class defined in this module. + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + # TODO: support transforming kwargs + if len(args) > 0 and _is_js_instance(args[0], "Request"): + args = (Request(args[0]), *args[1:]) + + # Wrap `env` so that bindings can be used without to_js. + if len(args) > 1: + args = (args[0], _EnvWrapper(args[1]), *args[2:]) + + return func(*args, **kwargs) + + return wrapper + + +class _WorkflowStepWrapper: + def __init__(self, js_step): + self._js_step = js_step + self._memoized_dependencies = {} + self._in_flight = {} + self.step_closures = {} + + # Assign the appropriate method based on compat flag + if _cloudflare_compat_flags.python_workflows_implicit_dependencies: + self.do = self._do_implicit + else: + self.do = self._do_legacy + + def _do_legacy(self, name, depends=None, concurrent=False, config=None): + """Original signature - positional args allowed, explicit depends parameter.""" + return self._create_step_decorator( + name=name, + depends=depends, + concurrent=concurrent, + config=config, + implicit=False, + ) + + def _do_implicit(self, name=None, *, concurrent=False, config=None): + """New signature - keyword-only args, dependencies resolved from param names.""" + return self._create_step_decorator( + name=name, + depends=None, + concurrent=concurrent, + config=config, + implicit=True, + ) + + def _create_step_decorator(self, name, depends, concurrent, config, implicit): + """Shared decorator factory for both legacy and implicit modes.""" + + def decorator(func): + step_name = func.__name__ if name is None else name + + async def wrapper(): + results_future_list = self._build_dependency_list( + func, depends, implicit + ) + results = await self._gather_results(results_future_list, concurrent) + return await _do_call(self, step_name, config, func, *results) + + wrapper._step_name = step_name + self.step_closures[step_name] = wrapper + return wrapper + + return decorator + + def _build_dependency_list(self, func, depends, implicit): + """Build the dependency list based on mode (implicit vs legacy).""" + sig = inspect.signature(func) + results_future_list = [] + + if implicit: + # Implicit mode: resolve dependencies from parameter names + for p in sig.parameters.values(): + if p.name in self.step_closures: + results_future_list.append(self.step_closures[p.name]) + elif p.name == "ctx": + results_future_list.append(p) + else: + raise TypeError(f"Received unexpected parameter {p.name}") + else: + # Legacy mode: use explicit depends list, support ctx parameter + non_ctx_params = [p for p in sig.parameters.values() if p.name != "ctx"] + + if depends is None and len(non_ctx_params) > 0: + raise TypeError( + f"Step has {len(non_ctx_params)} non-ctx parameter(s) but no 'depends' list provided" + ) + + elif depends is not None and len(depends) != len(non_ctx_params): + raise TypeError( + f"Step declares {len(non_ctx_params)} non-ctx parameter(s) but 'depends' has {len(depends)} item(s)" + ) + + curr = 0 + for p in sig.parameters.values(): + if p.name == "ctx": + results_future_list.append(p) + else: + results_future_list.append(depends[curr]) + curr += 1 + + return results_future_list + + async def _gather_results(self, results_future_list, concurrent): + """Resolve dependencies concurrently or sequentially.""" + if concurrent: + return await gather( + *[self._resolve_dependency(dep) for dep in results_future_list or []] + ) + else: + return [ + await self._resolve_dependency(dep) for dep in results_future_list or [] + ] + + def sleep(self, *args, **kwargs): + return self._js_step.sleep(*args, **kwargs) + + def sleep_until(self, name, timestamp): + if not isinstance(timestamp, str): + timestamp = python_to_rpc(timestamp) + + return self._js_step.sleepUntil(name, timestamp) + + def wait_for_event(self, name, event_type, /, timeout="24 hours"): + return self._js_step.waitForEvent( + name, + to_js( + {"type": event_type, "timeout": timeout}, + dict_converter=Object.fromEntries, + ), + ) + + async def _resolve_dependency(self, dep): + if hasattr(dep, "name") and dep.name == "ctx": + return dep + elif dep._step_name in self._memoized_dependencies: + return self._memoized_dependencies[dep._step_name] + elif dep._step_name in self._in_flight: + return await self._in_flight[dep._step_name] + + return await dep() + + +async def _do_call(entrypoint, name, config, callback, *results): + async def _callback(ctx=None): + # deconstruct the actual ctx object + resolved_results = tuple( + python_from_rpc(ctx) + if isinstance(r, inspect.Parameter) and r.name == "ctx" + else r + for r in results + ) + result = callback(*resolved_results) + + if inspect.iscoroutine(result): + result = await result + return to_js(result, dict_converter=Object.fromEntries) + + async def _closure(): + try: + if config is None: + coroutine = await entrypoint._js_step.do(name, _callback) + else: + coroutine = await entrypoint._js_step.do( + name, to_js(config, dict_converter=Object.fromEntries), _callback + ) + + return python_from_rpc(coroutine) + except Exception as exc: + raise _from_js_error(exc) from exc + + task = create_task(_closure()) + entrypoint._in_flight[name] = task + + try: + result = await task + entrypoint._memoized_dependencies[name] = result + finally: + del entrypoint._in_flight[name] + + return result + + +def _wrap_subclass(cls): + # Override the class __init__ so that we can wrap the `env` in the constructor. + original_init = cls.__init__ + + def wrapped_init(self, *args, **kwargs): + args = list(args) + if len(args) > 0: + _pyodide_entrypoint_helper.patchWaitUntil(args[0]) + if issubclass(cls, DurableObject): + args[0] = DurableObjectContext(args[0]) + if len(args) > 1: + args[1] = _EnvWrapper(args[1]) + + original_init(self, *args, **kwargs) + + cls.__init__ = wrapped_init + + +def _wrap_workflow_step(cls): + run_fn = getattr(cls, "run", None) + if run_fn is None: + return + + # Only patch `on_run` for subclasses of WorkflowEntrypoint. + if not issubclass(cls, WorkflowEntrypoint): + # Not a workflow subclass, so don't wrap `on_run`. + return + + @functools.wraps(run_fn) + async def wrapped_run(self, event=None, step=None, /, *args, **kwargs): + if event is not None: + event = python_from_rpc(event) + if step is not None: + step = _WorkflowStepWrapper(step) + + result = run_fn(self, event, step, *args, **kwargs) + + if inspect.iscoroutine(result): + result = await result + + return result + + cls.run = wrapped_run + + +class DurableObject: + """ + Base class used to define a Durable Object. + """ + + ctx: "DurableObjectContext" + env: "Env" + + def __init__(self, ctx: "DurableObjectState", env: "Env"): + self.ctx = ctx + self.env = env + + def __init_subclass__(cls, **_kwargs): + _wrap_subclass(cls) + + +class WorkerEntrypoint: + """ + Base class used to define a Worker Entrypoint. + """ + + ctx: "ExecutionContext" + env: "Env" + + def __init__(self, ctx: "ExecutionContext", env: "Env"): + self.ctx = ctx + self.env = env + + def __init_subclass__(cls, **_kwargs: Any): + _wrap_subclass(cls) + + +class WorkflowEntrypoint: + """ + Base class used to define a Workflow Entrypoint. + """ + + ctx: "ExecutionContext" + env: "Env" + + def __init__(self, ctx: "ExecutionContext", env: "Env"): + self.ctx = ctx + self.env = env + + def __init_subclass__(cls, **_kwargs: Any): + _wrap_subclass(cls) + _wrap_workflow_step(cls) diff --git a/python_modules/workers/py.typed b/python_modules/workers/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/python_modules/workers/workflows.py b/python_modules/workers/workflows.py new file mode 100644 index 000000000..ee34fcf47 --- /dev/null +++ b/python_modules/workers/workflows.py @@ -0,0 +1,12 @@ +""" +Workflow-specific classes and exceptions for the workers module. +""" + + +class NonRetryableError(Exception): + """ + A marker exception used to signal that a workflow step should not be retried. + This is a special exception used by workflows. + """ + + pass diff --git a/python_modules/workers_runtime_sdk-1.1.5.dist-info/INSTALLER b/python_modules/workers_runtime_sdk-1.1.5.dist-info/INSTALLER new file mode 100644 index 000000000..5c69047b2 --- /dev/null +++ b/python_modules/workers_runtime_sdk-1.1.5.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/python_modules/workers_runtime_sdk-1.1.5.dist-info/METADATA b/python_modules/workers_runtime_sdk-1.1.5.dist-info/METADATA new file mode 100644 index 000000000..1405d489c --- /dev/null +++ b/python_modules/workers_runtime_sdk-1.1.5.dist-info/METADATA @@ -0,0 +1,15 @@ +Metadata-Version: 2.4 +Name: workers-runtime-sdk +Version: 1.1.5 +Summary: Python SDK for Cloudflare Workers +Project-URL: Homepage, https://github.com/cloudflare/workers-py +Project-URL: Bug Tracker, https://github.com/cloudflare/workers-py/issues +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Requires-Python: >=3.12 +Description-Content-Type: text/markdown + +# workers-runtime-sdk + +Runtime SDK for Python Cloudflare Workers. diff --git a/python_modules/workers_runtime_sdk-1.1.5.dist-info/RECORD b/python_modules/workers_runtime_sdk-1.1.5.dist-info/RECORD new file mode 100644 index 000000000..c64fa233b --- /dev/null +++ b/python_modules/workers_runtime_sdk-1.1.5.dist-info/RECORD @@ -0,0 +1,14 @@ +_cloudflare_compat_flags.pyi,sha256=xvA4GFtXkDbK6rIv4GMZ_4WYo--45cOvP7kuNcaehFk,131 +_pyodide_entrypoint_helper.pyi,sha256=WbDYGJQbNnFIFDlrVIeHeEukU-HuAn33e9wShldLkyU,264 +_workers_sdk_entropy_import_context.pth,sha256=Xj7M7o1SlvQfb21987pWiUqRgsf7whD93fW8j3CJ4F4,50 +_workers_sdk_entropy_import_context.py,sha256=dKZW2VpskaXBfBtc74qTLksIqiCBXnejvFA7atokRbg,5175 +_workers_sdk_entropy_import_context_loader.py,sha256=INu69ZEiIhOjA-l2ewnig1vKcqcQNPZ_-G65TvjyxuM,498 +workers/__init__.py,sha256=reEDBHb3EK8cKT9L7YMGIVw70C-kd3o_-9lZElu3O-4,1319 +workers/_workers.py,sha256=HshDzyWvthb3VETIzVICgaoBgrFFqIiUOQtXzNY2Lkw,46578 +workers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +workers/workflows.py,sha256=k3n99BbaVtJAA2kWWlG3Lag_ckrqiTrHhShWMdCYA78,270 +workers_runtime_sdk-1.1.5.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +workers_runtime_sdk-1.1.5.dist-info/METADATA,sha256=MEwOQlavR-QF3vd4tT9Wh2YtIhhaecRCDOPapfo_Buw,521 +workers_runtime_sdk-1.1.5.dist-info/RECORD,, +workers_runtime_sdk-1.1.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +workers_runtime_sdk-1.1.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 diff --git a/python_modules/workers_runtime_sdk-1.1.5.dist-info/REQUESTED b/python_modules/workers_runtime_sdk-1.1.5.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/python_modules/workers_runtime_sdk-1.1.5.dist-info/WHEEL b/python_modules/workers_runtime_sdk-1.1.5.dist-info/WHEEL new file mode 100644 index 000000000..b1b94fd58 --- /dev/null +++ b/python_modules/workers_runtime_sdk-1.1.5.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.29.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/src/handlers/__init__.py b/src/handlers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/handlers/_shared.py b/src/handlers/_shared.py new file mode 100644 index 000000000..95d60e418 --- /dev/null +++ b/src/handlers/_shared.py @@ -0,0 +1,38 @@ +import json + +from js import Object, Response +from pyodide.ffi import to_js + + +CORS_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", +} + + +def _opts(d): + return to_js(d, dict_converter=Object.fromEntries) + + +def json_response(payload, status=200): + body = json.dumps(payload) + return Response.new(body, _opts({ + "status": status, + "headers": {"Content-Type": "application/json; charset=utf-8", **CORS_HEADERS}, + })) + + +def error_response(message, status=400): + return json_response({"error": message}, status=status) + + +def cors_preflight(): + return Response.new("", _opts({"status": 204, "headers": CORS_HEADERS})) + + +async def read_json(request): + try: + return (await request.json()).to_py() + except Exception: + return None diff --git a/src/handlers/bmi.py b/src/handlers/bmi.py new file mode 100644 index 000000000..02f9a61a7 --- /dev/null +++ b/src/handlers/bmi.py @@ -0,0 +1,35 @@ +from ._shared import error_response, json_response, read_json + + +def calculate_bmi(height_m, weight_kg): + return round(weight_kg / (height_m ** 2), 2) + + +def category_for(bmi): + if bmi < 18.5: + return "Underweight" + if bmi < 24.9: + return "Normal weight" + if bmi < 29.9: + return "Overweight" + if bmi < 34.9: + return "Obese (Class I)" + if bmi < 39.9: + return "Obese (Class II)" + return "Obese (Class III)" + + +async def handle(request, env): + body = await read_json(request) + if body is None: + return error_response("invalid JSON") + try: + h = float(body["height_m"]) + w = float(body["weight_kg"]) + except (KeyError, TypeError, ValueError): + return error_response("height_m and weight_kg required (numbers)") + if h <= 0 or w <= 0: + return error_response("height_m and weight_kg must be > 0") + + bmi = calculate_bmi(h, w) + return json_response({"bmi": bmi, "category": category_for(bmi)}) diff --git a/src/handlers/hangman.py b/src/handlers/hangman.py new file mode 100644 index 000000000..a5fdd0b33 --- /dev/null +++ b/src/handlers/hangman.py @@ -0,0 +1,78 @@ +import random + +from ._shared import error_response, json_response, read_json + + +WORDS = { + "easy": [ + "egg", "toy", "water", "day", "shower", "tiger", "home", "coat", + "garden", "throw", "crown", "baby", "office", "beach", "phone", + "computer", "flower", "bank", "train", "brush", "game", + ], + "medium": [ + "soup", "tree", "purple", "orange", "rocket", "pillow", "guitar", + "kitchen", "window", "yellow", "planet", "doctor", "rabbit", + "engine", "ticket", "candle", "puzzle", "mountain", "ocean", + ], + "hard": [ + "syzygy", "labyrinth", "quixotic", "ephemeral", "petrichor", + "serendipity", "sonder", "limerence", "halcyon", + ], +} + +INITIAL_TRIES = {"easy": 8, "medium": 6, "hard": 4} + + +def pick_word(difficulty, seed): + pool = WORDS.get(difficulty, WORDS["medium"]) + rng = random.Random(seed) + return rng.choice(pool).upper() + + +def mask_word(word, guessed_letters): + return "".join((c if c in guessed_letters else "_") for c in word) + + +async def handle(request, env): + body = await read_json(request) + if body is None: + return error_response("invalid JSON") + + difficulty = (body.get("difficulty") or "medium").lower() + if difficulty not in WORDS: + return error_response("difficulty must be easy|medium|hard") + + seed = body.get("word_seed") + if not isinstance(seed, int): + return error_response("word_seed (int) required") + + guessed = body.get("guessed") or [] + if not isinstance(guessed, list) or not all(isinstance(g, str) and len(g) == 1 for g in guessed): + return error_response("guessed must be a list of single letters") + guessed_set = {g.upper() for g in guessed if g.isalpha()} + + word = pick_word(difficulty, seed) + wrong = sorted(g for g in guessed_set if g not in word) + max_tries = INITIAL_TRIES[difficulty] + tries_left = max_tries - len(wrong) + mask = mask_word(word, guessed_set) + + if "_" not in mask: + status = "win" + elif tries_left <= 0: + status = "lose" + else: + status = "ongoing" + + out = { + "mask": mask, + "wrong": wrong, + "tries_left": max(0, tries_left), + "max_tries": max_tries, + "status": status, + "word_length": len(word), + } + if status == "lose": + out["word"] = word + + return json_response(out) diff --git a/src/handlers/madlibs.py b/src/handlers/madlibs.py new file mode 100644 index 000000000..07c29c877 --- /dev/null +++ b/src/handlers/madlibs.py @@ -0,0 +1,60 @@ +from ._shared import error_response, json_response, read_json + + +def generate_madlib(choice, adjective, noun, verb, adverb): + stories = { + 1: ( + f"In a mystical and distant land, there was a brave and {adjective} explorer named {noun}. " + f"{noun.capitalize()} had always dreamed of {verb} {adverb} to discover hidden treasures. " + f"One day, while {verb} {adverb} deep in the dense {adjective} jungle, {noun} stumbled upon an ancient {noun}. " + f"The {noun} was covered in {adjective} vines and moss, but it {verb} {adverb} with the promise of untold riches. " + f"With {adjective} excitement, {noun} began to {verb} {adverb}, clearing away every obstacle. " + f"At the heart of the {noun}, a {adjective} light {verb} {adverb}, revealing a chest of {adjective} {noun}. " + f"{noun} decided to {verb} {adverb} home and share the {adjective} riches with the village — " + f"and lived {adverb} ever after." + ), + 2: ( + f"In the enchanting world of {noun}, there existed a {adjective} school known as the {adjective} Academy of {noun}. " + f"Our protagonist, {noun}, had always dreamed of {verb} {adverb} and becoming a {adjective} wizard. " + f"At the academy, {noun} learned to {verb} {adverb}, brew {adjective} potions, and cast {adjective} spells. " + f"With a {adjective} friend by their side, {noun} set out to {verb} {adverb} and uncover the secrets of the {adjective} forest, " + f"making the world a {adjective} place along the way." + ), + 3: ( + f"In the bustling city of {noun}, Dr. {noun} was renowned for {verb} {adverb} and pushing the boundaries of {adjective} science. " + f"One day, Dr. {noun} had a {adjective} idea: a {adjective} time machine. " + f"After many {adjective} experiments, Dr. {noun} stepped inside and {verb} {adverb} into the unknown. " + f"In a {adjective} era, Dr. {noun} encountered {adjective} figures and had the chance to {verb} {adverb} with legends — " + f"only to return with a {adjective} new appreciation for the {adjective} present." + ), + } + return stories.get(choice) + + +async def handle(request, env): + body = await read_json(request) + if body is None: + return error_response("invalid JSON") + + try: + choice = int(body.get("story", 1)) + except (TypeError, ValueError): + return error_response("story must be 1, 2, or 3") + if choice not in (1, 2, 3): + return error_response("story must be 1, 2, or 3") + + def field(key): + v = body.get(key) + if not isinstance(v, str) or not v.strip(): + return None + return v.strip()[:40] + + adjective = field("adjective") + noun = field("noun") + verb = field("verb") + adverb = field("adverb") + if not all([adjective, noun, verb, adverb]): + return error_response("adjective, noun, verb, adverb required") + + story = generate_madlib(choice, adjective, noun, verb, adverb) + return json_response({"story": story}) diff --git a/src/handlers/qr.py b/src/handlers/qr.py new file mode 100644 index 000000000..f2a5315e4 --- /dev/null +++ b/src/handlers/qr.py @@ -0,0 +1,28 @@ +import base64 +import io + +import qrcode + +from ._shared import error_response, json_response, read_json + + +async def handle(request, env): + body = await read_json(request) + if body is None: + return error_response("invalid JSON") + text = (body.get("text") or "").strip() + if not text: + return error_response("text required") + if len(text) > 1024: + return error_response("text too long (max 1024 chars)") + + q = qrcode.QRCode(version=1, box_size=10, border=4) + q.add_data(text) + q.make(fit=True) + img = q.make_image(fill_color="black", back_color="white") + + buf = io.BytesIO() + img.save(buf, format="PNG") + png_b64 = base64.b64encode(buf.getvalue()).decode("ascii") + + return json_response({"png_b64": png_b64}) diff --git a/src/handlers/rps.py b/src/handlers/rps.py new file mode 100644 index 000000000..f83c6a108 --- /dev/null +++ b/src/handlers/rps.py @@ -0,0 +1,32 @@ +from random import randint + +from ._shared import error_response, json_response, read_json + + +CHOICES = {1: "rock", 2: "paper", 3: "scissors"} +WIN_PAIRS = {"13", "21", "32"} # player vs cpu where player wins + + +async def handle(request, env): + body = await read_json(request) + if body is None: + return error_response("invalid JSON") + + choice = str(body.get("choice", "")).lower() + player_num = next((k for k, v in CHOICES.items() if v == choice), None) + if player_num is None: + return error_response("choice must be rock|paper|scissors") + + cpu_num = randint(1, 3) + if player_num == cpu_num: + result = "draw" + elif f"{player_num}{cpu_num}" in WIN_PAIRS: + result = "win" + else: + result = "lose" + + return json_response({ + "player": CHOICES[player_num], + "cpu": CHOICES[cpu_num], + "result": result, + }) diff --git a/src/handlers/tictactoe.py b/src/handlers/tictactoe.py new file mode 100644 index 000000000..27824976d --- /dev/null +++ b/src/handlers/tictactoe.py @@ -0,0 +1,95 @@ +import random + +from ._shared import error_response, json_response, read_json + + +WIN_LINES = [ + (0, 1, 2), (3, 4, 5), (6, 7, 8), # rows + (0, 3, 6), (1, 4, 7), (2, 5, 8), # columns + (0, 4, 8), (2, 4, 6), # diagonals +] + + +def winner_on(board): + for a, b, c in WIN_LINES: + if board[a] and board[a] == board[b] == board[c]: + return board[a] + return None + + +def is_full(board): + return all(cell for cell in board) + + +def free_squares(board, options): + return [i for i in options if not board[i]] + + +def computer_move(board, cpu_letter): + player_letter = "O" if cpu_letter == "X" else "X" + + # 1. win if possible + for i in range(9): + if not board[i]: + board[i] = cpu_letter + if winner_on(board) == cpu_letter: + board[i] = "" + return i + board[i] = "" + + # 2. block opponent + for i in range(9): + if not board[i]: + board[i] = player_letter + if winner_on(board) == player_letter: + board[i] = "" + return i + board[i] = "" + + # 3. corner, center, side + for group in ([0, 2, 6, 8], [4], [1, 3, 5, 7]): + free = free_squares(board, group) + if free: + return random.choice(free) + return None + + +async def handle(request, env): + body = await read_json(request) + if body is None: + return error_response("invalid JSON") + + board = body.get("board") + player = body.get("player") + move = body.get("move") + + if (not isinstance(board, list) or len(board) != 9 + or not all(c in ("", "X", "O") for c in board)): + return error_response("board must be 9-array of '', 'X', or 'O'") + if player not in ("X", "O"): + return error_response("player must be 'X' or 'O'") + if move is not None: + if not isinstance(move, int) or not (0 <= move < 9): + return error_response("move must be int 0..8") + if board[move]: + return error_response("square already taken") + board[move] = player + + win = winner_on(board) + if win: + return json_response({"board": board, "status": "win", "winner": win}) + if is_full(board): + return json_response({"board": board, "status": "draw"}) + + cpu_letter = "O" if player == "X" else "X" + cpu_idx = computer_move(board, cpu_letter) + if cpu_idx is not None: + board[cpu_idx] = cpu_letter + + win = winner_on(board) + if win: + return json_response({"board": board, "status": "win", "winner": win, "cpu_move": cpu_idx}) + if is_full(board): + return json_response({"board": board, "status": "draw", "cpu_move": cpu_idx}) + + return json_response({"board": board, "status": "ongoing", "cpu_move": cpu_idx}) diff --git a/src/worker.py b/src/worker.py new file mode 100644 index 000000000..813b36061 --- /dev/null +++ b/src/worker.py @@ -0,0 +1,27 @@ +from urllib.parse import urlparse + +from handlers import bmi, hangman, madlibs, qr, rps, tictactoe +from handlers._shared import cors_preflight, error_response + + +ROUTES = { + "/api/bmi": bmi.handle, + "/api/rps": rps.handle, + "/api/qr": qr.handle, + "/api/madlibs": madlibs.handle, + "/api/tictactoe": tictactoe.handle, + "/api/hangman": hangman.handle, +} + + +async def on_fetch(request, env): + path = urlparse(request.url).path + + if path in ROUTES: + if request.method == "OPTIONS": + return cors_preflight() + if request.method != "POST": + return error_response("POST only", 405) + return await ROUTES[path](request, env) + + return error_response("not found", 404) diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..748dea75e --- /dev/null +++ b/uv.lock @@ -0,0 +1,108 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[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/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" }, +] + +[[package]] +name = "pybegin-worker" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pillow" }, + { name = "qrcode" }, +] + +[package.metadata] +requires-dist = [ + { name = "pillow" }, + { name = "qrcode", specifier = ">=7.4" }, +] + +[[package]] +name = "qrcode" +version = "8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" }, +] diff --git a/website/api.js b/website/api.js new file mode 100644 index 000000000..01aa630ab --- /dev/null +++ b/website/api.js @@ -0,0 +1,81 @@ +// Tiny client wrapper around the Cloudflare Workers Python backend. +// Exposed as window.PBP_API so the JSX modules can reach it. +// +// Base URL is read from `window.PBP_API_BASE` (set in index.html). Leave empty +// for same-origin (e.g., local wrangler dev with assets), or set to the deployed +// Worker URL like "https://pybegin-api..workers.dev" for the +// split Pages-frontend + Worker-API production setup. + +(function () { + const BASE = (typeof window !== 'undefined' && window.PBP_API_BASE) || ''; + const RUNNABLE = { + rps: { + endpoint: '/api/rps', + inputs: [ + { key: 'choice', label: 'Your throw', type: 'select', options: ['rock', 'paper', 'scissors'], default: 'rock' }, + ], + }, + bmi: { + endpoint: '/api/bmi', + inputs: [ + { key: 'height_m', label: 'Height (m)', type: 'number', step: 0.01, min: 0.5, max: 2.5, default: 1.75 }, + { key: 'weight_kg', label: 'Weight (kg)', type: 'number', step: 0.1, min: 10, max: 400, default: 70 }, + ], + }, + qr: { + endpoint: '/api/qr', + inputs: [ + { key: 'text', label: 'Text or URL', type: 'text', default: 'https://github.com/Mrinank-Bhowmick/python-beginner-projects' }, + ], + }, + madlibs: { + endpoint: '/api/madlibs', + inputs: [ + { key: 'story', label: 'Story', type: 'select', options: [ + { value: 1, label: '1 · Mystical land' }, + { value: 2, label: '2 · Wizard academy' }, + { value: 3, label: '3 · Time machine' }, + ], default: 1 }, + { key: 'adjective', label: 'Adjective', type: 'text', default: 'sparkly' }, + { key: 'noun', label: 'Noun', type: 'text', default: 'penguin' }, + { key: 'verb', label: 'Verb', type: 'text', default: 'dancing' }, + { key: 'adverb', label: 'Adverb', type: 'text', default: 'wildly' }, + ], + }, + tictactoe: { + endpoint: '/api/tictactoe', + interactive: 'tictactoe', // custom UI component + }, + hangman: { + endpoint: '/api/hangman', + interactive: 'hangman', + }, + }; + + async function call(id, payload) { + const cfg = RUNNABLE[id]; + if (!cfg) throw new Error('Not runnable: ' + id); + const res = await fetch(BASE + cfg.endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload || {}), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status)); + return data; + } + + function defaults(id) { + const cfg = RUNNABLE[id]; + if (!cfg || !cfg.inputs) return {}; + const out = {}; + for (const f of cfg.inputs) out[f.key] = f.default; + return out; + } + + function isRunnable(id) { return !!RUNNABLE[id]; } + function isInteractive(id) { return !!(RUNNABLE[id] && RUNNABLE[id].interactive); } + function inputsFor(id) { return (RUNNABLE[id] && RUNNABLE[id].inputs) || []; } + + window.PBP_API = { call, defaults, isRunnable, isInteractive, inputsFor, RUNNABLE }; +})(); diff --git a/website/data.js b/website/data.js new file mode 100644 index 000000000..a7fd320cd --- /dev/null +++ b/website/data.js @@ -0,0 +1,70 @@ +// Shared data for all aesthetic variations. +// Pulled from github.com/Mrinank-Bhowmick/python-beginner-projects + +window.PBP_PROJECTS = [ + { id: 'snake', name: 'Snake Game', cat: 'games', blurb: 'Slither, eat, grow longer. The classic.', lines: 92, deps: 'pygame', emoji: '🐍' }, + { id: 'tictactoe', name: 'Tic-Tac-Toe', cat: 'games', blurb: 'Three in a row, terminal showdown.', lines: 64, deps: 'stdlib', emoji: '⭕', runnable: true }, + { id: 'hangman', name: 'Hangman', cat: 'games', blurb: 'Guess letters before the gallows complete.', lines: 78, deps: 'stdlib', emoji: '🪢', runnable: true }, + { id: 'flappy', name: 'Flappy Bird', cat: 'games', blurb: 'Tap to flap. Avoid the pipes. Die. Repeat.', lines: 140, deps: 'pygame', emoji: '🐦' }, + { id: 'rps', name: 'Rock · Paper · Scissors', cat: 'games', blurb: 'Best of three vs the random module.', lines: 48, deps: 'stdlib', emoji: '✊', runnable: true }, + { id: 'calc', name: 'Calculator', cat: 'tools', blurb: 'Tkinter buttons. Operator precedence. Beep.', lines: 110, deps: 'tkinter', emoji: '🧮' }, + { id: 'bmi', name: 'BMI Calculator', cat: 'tools', blurb: 'Height + weight → a single questionable number.', lines: 36, deps: 'stdlib', emoji: '⚖️', runnable: true }, + { id: 'weather', name: 'Weather App', cat: 'web', blurb: 'OpenWeather API, your city, today.', lines: 88, deps: 'requests', emoji: '☁️' }, + { id: 'qr', name: 'QR Code Generator', cat: 'tools', blurb: 'Text in, scannable square out.', lines: 22, deps: 'qrcode', emoji: '▦', runnable: true }, + { id: 'pwd', name: 'Password Generator', cat: 'tools', blurb: 'Random, strong, immediately forgotten.', lines: 30, deps: 'secrets', emoji: '🔐' }, + { id: 'yt', name: 'YouTube Downloader', cat: 'web', blurb: 'pytube wrapper. Save the lecture.', lines: 54, deps: 'pytube', emoji: '⬇' }, + { id: 'madlibs', name: 'Madlibs Generator', cat: 'fun', blurb: 'Fill in nouns. Receive nonsense. Laugh.', lines: 40, deps: 'stdlib', emoji: '✏️', runnable: true }, +]; + +window.PBP_CATEGORIES = [ + { id: 'all', name: 'All', count: 12 }, + { id: 'games', name: 'Games', count: 5 }, + { id: 'tools', name: 'Tools', count: 4 }, + { id: 'web', name: 'Web & API', count: 2 }, + { id: 'fun', name: 'Just for Fun', count: 1 }, +]; + +window.PBP_CONTRIBUTORS = [ + { handle: 'Mrinank-Bhowmick', name: 'Mrinank Bhowmick', commits: 412, role: 'maintainer' }, + { handle: 'ibra-kdbra', name: 'Ibra-kdbra', commits: 38 }, + { handle: 'PythonicBoat', name: 'Yashvardhan Singh', commits: 27 }, + { handle: 'Alby084', name: 'Alby084', commits: 24 }, + { handle: 'ca20110820', name: 'Cedric Anover', commits: 21 }, + { handle: 'kanchanraiii', name: 'Kanchan Rai', commits: 18 }, + { handle: 'TERNION-1121', name: 'Vikrant Bhadouriya', commits: 16 }, + { handle: 'Guyunjeong', name: 'Chloe', commits: 14 }, + { handle: 'rik-chatterjee', name: 'Rik Chatterjee', commits: 12 }, + { handle: 'omkarxpatel', name: 'Omkar Patel', commits: 11 }, + { handle: 'UffaModey', name: 'Fafa Modey', commits: 10 }, + { handle: 'anish2105', name: 'Anish Vantagodi', commits: 9 }, + { handle: 'JohnRTitor', name: 'Masum Reza', commits: 9 }, + { handle: 'sudipg4112001', name: 'Sudip Ghosh', commits: 8 }, + { handle: 'shashaaankkkkk', name: 'Shashank Shekhar', commits: 8 }, + { handle: 'jrbublitz', name: 'Jefferson Bublitz', commits: 7 }, + { handle: 'vagxrth', name: 'Vagarth Pandey', commits: 7 }, + { handle: 'shriyansnaik', name: 'Shriyans Naik', commits: 6 }, + { handle: 'xlo-u', name: 'Yash Upadhyay', commits: 6 }, + { handle: 'payallenka', name: 'Payallenka', commits: 5 }, +]; + +window.PBP_STATS = { projects: 100, contributors: 241, stars: 2300, forks: 903 }; + +window.PBP_PATHS = [ + { id: 'starter', name: 'First Steps', tag: 'Day 1', desc: 'Variables, input, print. Calculator → BMI → RPS.', items: ['bmi', 'rps', 'calc'], hue: 0 }, + { id: 'games', name: 'Make Games', tag: 'Week 1', desc: 'Loops, state, the random module. Tic-Tac-Toe → Hangman → Snake.', items: ['tictactoe', 'hangman', 'snake'], hue: 1 }, + { id: 'real', name: 'Real World', tag: 'Week 2', desc: 'HTTP, files, libraries. Weather → QR → YouTube.', items: ['weather', 'qr', 'yt'], hue: 2 }, +]; + +// Shared bookmark helper (localStorage) +window.PBP_BM = { + KEY: 'pbp_bookmarks_v1', + get() { try { return JSON.parse(localStorage.getItem(this.KEY)) || []; } catch { return []; } }, + set(arr) { localStorage.setItem(this.KEY, JSON.stringify(arr)); }, + toggle(id) { + const arr = this.get(); + const i = arr.indexOf(id); + if (i >= 0) arr.splice(i, 1); else arr.push(id); + this.set(arr); + return arr; + }, +}; diff --git a/website/index.html b/website/index.html new file mode 100644 index 000000000..d326d5f39 --- /dev/null +++ b/website/index.html @@ -0,0 +1,47 @@ + + + + + + pyBegin · python beginner projects + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/website/sticker-app-2.css b/website/sticker-app-2.css new file mode 100644 index 000000000..3d8db514b --- /dev/null +++ b/website/sticker-app-2.css @@ -0,0 +1,133 @@ +/* === Sticker Pack · sections === */ + +/* Hero */ +.s-hero { + display: grid; grid-template-columns: 1.05fr 1fr; gap: 60px; + align-items: center; padding: 60px 0 40px; +} +.s-tag { + display: inline-flex; align-items: center; gap: 9px; + padding: 8px 16px; border-radius: var(--s-radius-pill); + background: var(--s-surface); border: var(--s-border); + box-shadow: 3px 3px 0 var(--s-ink); + font-size: 12px; font-weight: 700; margin-bottom: 28px; + transform: rotate(-1deg); +} +.s-tag .dot { + width: 8px; height: 8px; border-radius: 50%; background: var(--s-accent-3); + box-shadow: 0 0 0 3px rgba(28, 201, 124, 0.18); + animation: s-pulse 2s ease-in-out infinite; +} +@keyframes s-pulse { 0%, 100% { box-shadow: 0 0 0 3px rgba(28, 201, 124, 0.18); } 50% { box-shadow: 0 0 0 6px rgba(28, 201, 124, 0); } } + +.s-headline { + font-family: var(--s-display); font-weight: 900; + font-size: clamp(56px, 9.5vw, 116px); + line-height: 0.9; letter-spacing: -0.035em; + margin: 0 0 24px; +} +.s-headline .scribble { position: relative; display: inline-block; color: var(--s-accent); } +.s-headline .scribble::after { + content: ''; position: absolute; bottom: -10px; left: -4px; right: -4px; height: 12px; + background-repeat: no-repeat; background-size: 100% 100%; +} +.s-sub { + font-size: 18px; line-height: 1.55; color: rgba(29, 24, 48, 0.72); + max-width: 480px; margin: 0 0 32px; +} +.s-hero-cta { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; } +.s-btn-pri, .s-btn-sec { + padding: 16px 26px; border-radius: var(--s-radius-pill); + font: inherit; font-size: 15px; font-weight: 800; cursor: pointer; + display: inline-flex; align-items: center; gap: 10px; + transition: transform .12s, box-shadow .12s; +} +.s-btn-pri { + background: var(--s-accent); color: var(--s-ink); + border: var(--s-border); box-shadow: var(--s-shadow); +} +.s-btn-sec { + background: transparent; color: var(--s-ink); border: var(--s-border); +} +.s-btn-pri:hover, .s-btn-sec:hover { transform: translate(-2px, -2px); } +.s-btn-pri:hover { box-shadow: 8px 8px 0 var(--s-ink); } +.s-btn-sec:hover { background: var(--s-surface); box-shadow: 3px 3px 0 var(--s-ink); } +.s-btn-pri:active, .s-btn-sec:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--s-ink) !important; } + +/* Sticker cluster — hero right */ +.s-stickers { position: relative; height: 520px; } +.s-sticker { + position: absolute; padding: 18px 22px; + border: var(--s-border); border-radius: var(--s-radius-lg); + box-shadow: var(--s-shadow); + display: flex; align-items: center; gap: 14px; + transition: transform .25s cubic-bezier(.4,1.6,.4,1); + cursor: pointer; user-select: none; +} +.s-sticker:hover { transform: rotate(0deg) translate(-3px, -3px) scale(1.04) !important; box-shadow: 9px 9px 0 var(--s-ink); z-index: 10; } +.s-sticker .emoji { font-size: 44px; line-height: 1; flex-shrink: 0; } +.s-sticker .lbl { font-weight: 800; font-size: 15px; } +.s-sticker .num { font-family: var(--s-mono); font-size: 11px; color: rgba(29, 24, 48, 0.6); margin-top: 2px; } +@keyframes s-bob1 { 0%, 100% { transform: rotate(-4deg) translateY(0); } 50% { transform: rotate(-4deg) translateY(-6px); } } +@keyframes s-bob2 { 0%, 100% { transform: rotate(3deg) translateY(0); } 50% { transform: rotate(3deg) translateY(-8px); } } +@keyframes s-bob3 { 0%, 100% { transform: rotate(2deg) translateY(0); } 50% { transform: rotate(2deg) translateY(-5px); } } +@keyframes s-bob4 { 0%, 100% { transform: rotate(-2deg) translateY(0); } 50% { transform: rotate(-2deg) translateY(-7px); } } +@keyframes s-bob5 { 0%, 100% { transform: rotate(4deg) translateY(0); } 50% { transform: rotate(4deg) translateY(-6px); } } + +/* Stats marquee */ +.s-stats { + margin-top: 30px; + background: var(--s-ink); border-radius: var(--s-radius-lg); padding: 28px; + display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; + color: var(--s-bg); border: var(--s-border); + box-shadow: var(--s-shadow-lg); +} +.s-stat { text-align: center; padding: 8px 0; border-right: 1.5px dashed rgba(254, 249, 239, 0.22); } +.s-stat:last-child { border-right: 0; } +.s-stat-n { font-family: var(--s-display); font-weight: 900; font-size: clamp(36px, 4vw, 52px); line-height: 1; color: var(--s-accent); } +.s-stat-l { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; margin-top: 6px; color: rgba(254, 249, 239, 0.75); } + +/* Sections */ +.s-section { margin-top: 80px; } +.s-section-h { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 30px; flex-wrap: wrap; } +.s-section-title { font-family: var(--s-display); font-weight: 900; font-size: clamp(40px, 5vw, 64px); letter-spacing: -0.03em; line-height: 0.95; margin: 0; } +.s-section-title em { font-style: italic; color: var(--s-accent); } +.s-section-side { font-size: 14px; color: rgba(29, 24, 48, 0.6); max-width: 360px; text-align: right; } + +/* Toolbar */ +.s-toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 22px; flex-wrap: wrap; } +.s-search { + flex: 1; min-width: 280px; display: flex; align-items: center; gap: 12px; + padding: 14px 22px; border-radius: var(--s-radius-pill); + background: var(--s-surface); border: var(--s-border); box-shadow: 4px 4px 0 var(--s-ink); + transition: box-shadow .12s, transform .12s; +} +.s-search:focus-within { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--s-ink); } +.s-search input { flex: 1; background: transparent; border: 0; outline: 0; color: var(--s-ink); font: inherit; font-size: 16px; font-weight: 500; } +.s-search input::placeholder { color: rgba(29, 24, 48, 0.4); } +.s-search-icon { font-size: 18px; } +.s-search-key { + font-family: var(--s-mono); font-size: 11px; padding: 3px 7px; + background: var(--s-bg); border: 1.5px solid var(--s-ink); border-radius: 5px; +} + +.s-sort { + padding: 12px 18px; border-radius: var(--s-radius-pill); + background: var(--s-surface); border: var(--s-border); box-shadow: 3px 3px 0 var(--s-ink); + font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; + display: flex; align-items: center; gap: 8px; +} + +.s-chips { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 24px; } +.s-chip { + padding: 9px 18px; border-radius: var(--s-radius-pill); + background: var(--s-surface); border: var(--s-border-thin); + font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; + display: flex; align-items: center; gap: 8px; + transition: transform .12s, box-shadow .12s; +} +.s-chip:hover { transform: translate(-1px, -1px); box-shadow: 2px 2px 0 var(--s-ink); } +.s-chip.active { background: var(--s-accent); box-shadow: 3px 3px 0 var(--s-ink); } +.s-chip.active.bm { background: var(--s-accent-4); } +.s-chip .ct { font-family: var(--s-mono); font-size: 11px; opacity: 0.7; font-weight: 700; } +.s-chip .ic { font-size: 14px; } diff --git a/website/sticker-app-3.css b/website/sticker-app-3.css new file mode 100644 index 000000000..5c6b38596 --- /dev/null +++ b/website/sticker-app-3.css @@ -0,0 +1,417 @@ +/* === Sticker Pack · gallery + paths + contributors === */ + +/* Gallery */ +.s-gallery.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; } +.s-gallery.list { display: flex; flex-direction: column; gap: 12px; } +.s-gallery.masonry { columns: 3; column-gap: 22px; } +.s-gallery.masonry .s-card { break-inside: avoid; margin-bottom: 22px; } + +.s-card { + position: relative; padding: 24px; + background: var(--s-surface); border: var(--s-border); border-radius: var(--s-radius-lg); + box-shadow: var(--s-shadow); + cursor: pointer; transition: transform .15s, box-shadow .15s; + text-align: left; +} +.s-card:hover { transform: translate(-3px, -3px); box-shadow: 9px 9px 0 var(--s-ink); z-index: 2; } +.s-card:active { transform: translate(2px, 2px); box-shadow: 2px 2px 0 var(--s-ink); } + +.s-gallery .s-card:nth-child(5n+1) { background: var(--s-surface-warm); } +.s-gallery .s-card:nth-child(5n+2) { background: var(--s-surface-cool); } +.s-gallery .s-card:nth-child(5n+3) { background: var(--s-surface-purple); } +.s-gallery .s-card:nth-child(5n+4) { background: var(--s-surface-yellow); } +.s-gallery .s-card:nth-child(5n+5) { background: var(--s-surface-green); } + +.s-card-emoji { font-size: 44px; margin-bottom: 14px; line-height: 1; display: inline-block; transition: transform .25s cubic-bezier(.4,1.6,.4,1); } +.s-card:hover .s-card-emoji { transform: rotate(-8deg) scale(1.1); } +.s-card-h { display: flex; align-items: start; justify-content: space-between; gap: 10px; margin-bottom: 8px; } +.s-card-name { font-family: var(--s-display); font-size: 22px; font-weight: 800; line-height: 1.1; letter-spacing: -0.015em; margin: 0; } +.s-card-blurb { font-size: 14px; color: rgba(29, 24, 48, 0.72); line-height: 1.5; margin: 0 0 16px; } +.s-card-meta { display: flex; gap: 8px; flex-wrap: wrap; } +.s-meta-pill { + padding: 4px 11px; border-radius: var(--s-radius-pill); + background: var(--s-ink); color: var(--s-bg); + font-family: var(--s-mono); font-size: 10px; font-weight: 700; + letter-spacing: 0.02em; +} +.s-meta-pill.dep { background: rgba(29,24,48,0.08); color: var(--s-ink); border: 1.5px solid var(--s-ink); } +.s-card-bm { + width: 34px; height: 34px; border-radius: 50%; + background: var(--s-surface); border: var(--s-border-thin); + display: grid; place-items: center; cursor: pointer; flex-shrink: 0; + font-size: 15px; line-height: 1; color: var(--s-ink); + transition: transform .12s, background .12s; +} +.s-card-bm:hover { transform: scale(1.08) rotate(-6deg); } +.s-card-bm.on { background: var(--s-accent); transform: rotate(-6deg); } + +/* Card density: minimal */ +.s-card.minimal { padding: 16px 20px; } +.s-card.minimal .s-card-emoji { display: none; } +.s-card.minimal .s-card-blurb { display: none; } +.s-card.minimal .s-card-h { margin-bottom: 10px; } + +/* Card with image banner */ +.s-card.image { padding-top: 12px; } +.s-card.image .s-img { + height: 140px; border-radius: var(--s-radius-md); + margin: 0 0 18px; + border: var(--s-border-thin); + display: grid; place-items: center; font-size: 60px; + background-image: repeating-linear-gradient(135deg, rgba(29,24,48,0.05) 0 6px, transparent 6px 12px); + overflow: hidden; position: relative; +} +.s-gallery .s-card:nth-child(5n+1) .s-img { background-color: rgba(255, 122, 89, 0.25); } +.s-gallery .s-card:nth-child(5n+2) .s-img { background-color: rgba(58, 166, 255, 0.22); } +.s-gallery .s-card:nth-child(5n+3) .s-img { background-color: rgba(165, 92, 255, 0.22); } +.s-gallery .s-card:nth-child(5n+4) .s-img { background-color: rgba(255, 209, 102, 0.3); } +.s-gallery .s-card:nth-child(5n+5) .s-img { background-color: rgba(28, 201, 124, 0.22); } + +/* List density */ +.s-gallery.list .s-card { display: grid; grid-template-columns: 56px 1fr auto auto auto; align-items: center; gap: 20px; padding: 14px 22px; } +.s-gallery.list .s-card-emoji { font-size: 30px; margin: 0; } +.s-gallery.list .s-card-h { margin: 0; display: contents; } +.s-gallery.list .s-card-name { font-size: 18px; } +.s-gallery.list .s-card-blurb { display: none; } +.s-gallery.list .s-card-meta { gap: 6px; } +.s-gallery.list .s-card-bm { margin-left: 4px; } + +/* Empty state */ +.s-empty { + padding: 60px 32px; text-align: center; + border: 2.5px dashed rgba(29,24,48,0.25); border-radius: var(--s-radius-lg); +} +.s-empty .em { font-size: 56px; } +.s-empty h3 { font-family: var(--s-display); font-size: 24px; margin: 16px 0 6px; font-weight: 800; } +.s-empty p { color: rgba(29,24,48,0.65); margin: 0; } + +/* Learning paths */ +.s-paths { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; } +.s-path { + padding: 30px; border: var(--s-border); border-radius: 28px; + box-shadow: var(--s-shadow); background: var(--s-surface); + position: relative; overflow: hidden; + transition: transform .15s, box-shadow .15s; +} +.s-path:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--s-ink); } +.s-path:nth-child(1) { background: var(--s-surface-warm); } +.s-path:nth-child(2) { background: var(--s-surface-cool); } +.s-path:nth-child(3) { background: var(--s-surface-purple); } +.s-path-head { display: flex; align-items: start; justify-content: space-between; gap: 14px; margin-bottom: 14px; } +.s-path-tag { + display: inline-block; padding: 5px 13px; border-radius: var(--s-radius-pill); + background: var(--s-ink); color: var(--s-bg); font-family: var(--s-mono); + font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; +} +.s-path-num { font-family: var(--s-display); font-size: 64px; font-weight: 900; line-height: 0.8; color: rgba(29,24,48,0.18); } +.s-path-name { font-family: var(--s-display); font-size: 34px; font-weight: 900; letter-spacing: -0.025em; line-height: 1; margin: 0 0 12px; } +.s-path-desc { font-size: 14px; color: rgba(29, 24, 48, 0.75); line-height: 1.55; margin: 0 0 22px; } +.s-path-list { display: flex; flex-direction: column; gap: 8px; } +.s-path-item { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: rgba(255,255,255,0.7); border-radius: 14px; border: 1.5px solid var(--s-ink); font-size: 14px; font-weight: 600; cursor: pointer; transition: background .12s; } +.s-path-item:hover { background: #fff; } +.s-path-item i { font-family: var(--s-mono); font-style: normal; font-size: 11px; color: rgba(29, 24, 48, 0.5); width: 16px; } +.s-path-item .em { font-size: 18px; margin-left: auto; } + +/* Contributors */ +.s-contributors { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; } +.s-contrib { + padding: 16px 12px; text-align: center; border-radius: 18px; + background: var(--s-surface); border: var(--s-border-thin); box-shadow: 4px 4px 0 var(--s-ink); + transition: transform .25s cubic-bezier(.4,1.6,.4,1); +} +.s-contrib:nth-child(7n+2) { transform: rotate(-1.5deg); } +.s-contrib:nth-child(7n+3) { transform: rotate(1.5deg); } +.s-contrib:nth-child(7n+4) { transform: rotate(-1deg); } +.s-contrib:nth-child(7n+5) { transform: rotate(1deg); } +.s-contrib:nth-child(7n+6) { transform: rotate(-2deg); } +.s-contrib:nth-child(7n) { transform: rotate(0.5deg); } +.s-contrib:hover { transform: rotate(0) translate(-2px, -2px) scale(1.04); box-shadow: 6px 6px 0 var(--s-ink); z-index: 5; } +.s-contrib.lead { + grid-column: span 3; grid-row: span 2; + padding: 36px; text-align: left; + background: var(--s-accent); + box-shadow: var(--s-shadow-lg); + border: var(--s-border); + transform: rotate(-1.5deg); +} +.s-contrib.lead:hover { transform: rotate(0) translate(-3px, -3px); box-shadow: 11px 11px 0 var(--s-ink); } +.s-avatar { + width: 56px; height: 56px; border-radius: 50%; + background: linear-gradient(135deg, #ffd4b0, var(--s-accent)); + border: var(--s-border-thin); + margin: 0 auto 10px; display: grid; place-items: center; + font-weight: 900; font-size: 20px; color: var(--s-ink); +} +.s-contrib:nth-child(7n+2) .s-avatar { background: linear-gradient(135deg, #d4f0ff, var(--s-accent-2)); } +.s-contrib:nth-child(7n+3) .s-avatar { background: linear-gradient(135deg, #e5d4ff, var(--s-accent-5)); } +.s-contrib:nth-child(7n+4) .s-avatar { background: linear-gradient(135deg, #d4ffd9, var(--s-accent-3)); } +.s-contrib:nth-child(7n+5) .s-avatar { background: linear-gradient(135deg, #fff5b0, var(--s-accent-4)); } +.s-contrib.lead .s-avatar { width: 84px; height: 84px; margin: 0 0 20px; font-size: 32px; background: var(--s-bg); } +.s-cname { font-weight: 800; font-size: 13px; line-height: 1.2; } +.s-chandle { font-family: var(--s-mono); font-size: 10px; color: rgba(29, 24, 48, 0.55); margin-top: 3px; } +.s-commits { font-family: var(--s-mono); font-size: 10px; color: var(--s-ink); margin-top: 6px; font-weight: 700; padding: 3px 8px; background: rgba(29,24,48,0.06); border-radius: var(--s-radius-pill); display: inline-block; } +.s-contrib.lead .s-cname { font-size: 32px; font-family: var(--s-display); font-weight: 900; letter-spacing: -0.025em; line-height: 1; margin-bottom: 4px; } +.s-contrib.lead .s-chandle { font-size: 14px; } +.s-contrib.lead .s-quote { margin-top: 20px; font-size: 16px; line-height: 1.5; font-weight: 500; } +.s-contrib.lead .s-badge { + display: inline-block; margin-top: 18px; padding: 7px 16px; + background: var(--s-ink); color: var(--s-bg); font-size: 11px; font-weight: 800; + letter-spacing: 0.08em; text-transform: uppercase; border-radius: var(--s-radius-pill); +} + +.s-more { + display: flex; justify-content: center; margin-top: 28px; +} +.s-more button { + padding: 14px 28px; border-radius: var(--s-radius-pill); + background: var(--s-surface); border: var(--s-border); box-shadow: var(--s-shadow); + font: inherit; font-size: 14px; font-weight: 700; cursor: pointer; + transition: transform .12s, box-shadow .12s; +} +.s-more button:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--s-ink); } + +/* CTA banner */ +.s-cta-banner { + margin-top: 80px; padding: 50px 56px; + background: var(--s-ink); color: var(--s-bg); + border: var(--s-border); border-radius: 28px; box-shadow: var(--s-shadow-lg); + display: grid; grid-template-columns: 1.4fr 1fr; gap: 40px; align-items: center; + position: relative; overflow: hidden; +} +.s-cta-banner::before { + content: ''; position: absolute; bottom: -40px; right: -40px; + width: 200px; height: 200px; border-radius: 50%; background: var(--s-accent); + opacity: 0.18; +} +.s-cta-banner h2 { font-family: var(--s-display); font-size: 44px; font-weight: 900; line-height: 1; letter-spacing: -0.025em; margin: 0 0 12px; } +.s-cta-banner h2 em { font-style: italic; color: var(--s-accent); } +.s-cta-banner p { font-size: 16px; color: rgba(254, 249, 239, 0.78); line-height: 1.55; margin: 0 0 22px; max-width: 480px; } +.s-cta-banner-codes { display: flex; flex-direction: column; gap: 10px; font-family: var(--s-mono); font-size: 13px; } +.s-cta-banner-code { + padding: 12px 16px; background: rgba(254,249,239,0.08); border-radius: 10px; + border: 1.5px dashed rgba(254,249,239,0.25); + color: var(--s-bg); display: flex; align-items: center; gap: 12px; +} +.s-cta-banner-code span:first-child { color: var(--s-accent); } +.s-cta-banner-code .copy { margin-left: auto; cursor: pointer; opacity: 0.6; font-size: 12px; } + +/* Footer */ +.s-footer { + margin: 60px 0 0; padding: 32px 0; + border-top: 2px dashed rgba(29, 24, 48, 0.3); + display: flex; justify-content: space-between; align-items: center; + font-size: 13px; color: rgba(29, 24, 48, 0.6); + flex-wrap: wrap; gap: 16px; +} +.s-heart { color: var(--s-accent); } +.s-footer-links { display: flex; gap: 18px; } +.s-footer a { color: inherit; text-decoration: none; } +.s-footer a:hover { color: var(--s-ink); text-decoration: underline; } + +/* Project modal */ +.s-modal-bg { + position: fixed; inset: 0; background: rgba(29, 24, 48, 0.5); + z-index: 1000; display: grid; place-items: center; padding: 24px; + animation: s-fade .15s; +} +@keyframes s-fade { from { opacity: 0; } } +.s-modal { + background: var(--s-surface); border: var(--s-border); border-radius: 28px; + box-shadow: 12px 12px 0 var(--s-accent); + max-width: 560px; width: 100%; padding: 36px; + animation: s-pop .25s cubic-bezier(.4,1.6,.4,1); + position: relative; +} +@keyframes s-pop { from { transform: translateY(20px) scale(.96); opacity: 0; } } +.s-modal-emoji { font-size: 72px; margin-bottom: 18px; } +.s-modal h2 { font-family: var(--s-display); font-size: 40px; font-weight: 900; letter-spacing: -0.025em; line-height: 1; margin: 0 0 8px; } +.s-modal-blurb { font-size: 16px; color: rgba(29,24,48,0.75); line-height: 1.55; margin: 0 0 22px; } +.s-modal-meta { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; } +.s-modal-code { + font-family: var(--s-mono); font-size: 13px; + background: var(--s-ink); color: var(--s-bg); padding: 16px; + border-radius: 12px; margin-bottom: 20px; +} +.s-modal-actions { display: flex; gap: 12px; } +.s-modal-x { + position: absolute; top: 16px; right: 16px; + width: 36px; height: 36px; border-radius: 50%; + background: var(--s-bg); border: var(--s-border-thin); cursor: pointer; + font-size: 18px; font-weight: 700; display: grid; place-items: center; + box-shadow: 2px 2px 0 var(--s-ink); +} + +/* Responsive */ +@media (max-width: 960px) { + .s-hero { grid-template-columns: 1fr; } + .s-stickers { height: 420px; } + .s-gallery.grid, .s-gallery.masonry { grid-template-columns: repeat(2, 1fr) !important; columns: 2; } + .s-paths { grid-template-columns: 1fr; } + .s-contributors { grid-template-columns: repeat(3, 1fr); } + .s-contrib.lead { grid-column: span 3; grid-row: span 1; } + .s-cta-banner { grid-template-columns: 1fr; padding: 36px; } +} +@media (max-width: 640px) { + .s-shell { padding: 0 18px; } + .s-navlinks { display: none; } + .s-stats { grid-template-columns: repeat(2, 1fr); gap: 14px; } + .s-stat { border-right: 0 !important; padding: 12px 0; } + .s-gallery.grid, .s-gallery.masonry { grid-template-columns: 1fr !important; columns: 1; } + .s-contributors { grid-template-columns: repeat(2, 1fr); } + .s-contrib.lead { grid-column: span 2; } +} + +/* ====== Try-it panel (modal demo) ====== */ +.s-meta-pill.run { + background: var(--s-accent); + color: var(--s-ink); + font-weight: 700; + border-color: var(--s-ink); +} +.s-try { + margin-top: 4px; + padding: 20px; + border: var(--s-border-thin); + border-radius: 16px; + background: var(--s-bg); + box-shadow: var(--s-shadow-sm); + margin-bottom: 22px; + font-family: var(--s-body); +} +.s-try-h { + font-family: var(--s-display); + font-size: 18px; + font-weight: 800; + margin-bottom: 14px; + color: var(--s-ink); +} +.s-try-h::before { content: ''; } +.s-try-form { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + gap: 10px 14px; + margin-bottom: 14px; +} +.s-try-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--s-ink); } +.s-try-field > span { font-weight: 700; } +.s-try-field input, .s-try-field select { + padding: 9px 12px; + border-radius: 10px; + border: var(--s-border-thin); + background: #fff; + color: var(--s-ink); + font: inherit; + font-size: 14px; +} +.s-try-field input:focus, .s-try-field select:focus { + outline: none; + box-shadow: 0 0 0 3px var(--s-accent); +} +.s-try-run { margin-bottom: 12px; } +.s-try-run:disabled { opacity: 0.55; cursor: wait; } +.s-try-error { + margin: 0 0 12px; + padding: 10px 14px; + border-radius: 10px; + background: #ffe1e1; + border: var(--s-border-thin); + border-color: #b34d4d; + color: #6e1414; + font-size: 13px; +} +.s-try-out { + margin-top: 6px; + padding: 14px 16px; + border-radius: 12px; + background: var(--s-surface-warm); + border: var(--s-border-thin); + color: var(--s-ink); +} +.s-try-out pre, .s-try-story pre { + margin: 0; + white-space: pre-wrap; + font: 14px/1.55 var(--s-body); + color: var(--s-ink); +} + +/* Tic-tac-toe board */ +.s-ttt-grid { + display: grid; + grid-template-columns: repeat(3, 78px); + grid-template-rows: repeat(3, 78px); + gap: 8px; + justify-content: center; + margin: 6px auto 4px; +} +.s-ttt-cell { + background: #fff; + border: var(--s-border-thin); + border-radius: 14px; + font: 800 38px/1 var(--s-display); + color: var(--s-ink); + cursor: pointer; + transition: transform 80ms ease; + box-shadow: var(--s-shadow-sm); +} +.s-ttt-cell:not(:disabled):hover { transform: translate(-2px, -2px); box-shadow: 5px 5px 0 var(--s-ink); } +.s-ttt-cell:disabled { cursor: default; } +.s-ttt-cell [data-mark="X"] { color: var(--s-accent); } +.s-ttt-cell [data-mark="O"] { color: #2f7a4f; } + +/* Hangman */ +.s-hg-row { + display: flex; + gap: 14px; + align-items: flex-end; + margin-bottom: 14px; +} +.s-hg-word { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; + margin: 6px 0 10px; +} +.s-hg-slot { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 28px; + padding: 6px 4px; + border-bottom: 3px solid var(--s-ink); + font: 800 26px/1 var(--s-display); + color: var(--s-ink); +} +.s-hg-meta { + display: flex; + justify-content: space-between; + font-size: 13px; + margin-bottom: 10px; + color: var(--s-ink); +} +.s-hg-wrong { color: #b34d4d; letter-spacing: 2px; font-weight: 700; } +.s-hg-keys { + display: grid; + grid-template-columns: repeat(13, 1fr); + gap: 5px; +} +.s-hg-key { + padding: 8px 0; + border-radius: 8px; + border: var(--s-border-thin); + background: #fff; + color: var(--s-ink); + font: 700 13px/1 var(--s-body); + cursor: pointer; + transition: transform 80ms ease; +} +.s-hg-key:not(:disabled):hover { transform: translateY(-1px); } +.s-hg-key.used { cursor: default; opacity: 0.55; } +.s-hg-key.hit { background: #c9f3d4; opacity: 1; } +.s-hg-key.miss { background: #ffd4d4; } + +@media (max-width: 640px) { + .s-hg-keys { grid-template-columns: repeat(9, 1fr); } + .s-ttt-grid { grid-template-columns: repeat(3, 64px); grid-template-rows: repeat(3, 64px); } + .s-ttt-cell { font-size: 30px; } +} diff --git a/website/sticker-app.css b/website/sticker-app.css new file mode 100644 index 000000000..4a5a5e3da --- /dev/null +++ b/website/sticker-app.css @@ -0,0 +1,100 @@ +/* === Sticker Pack · standalone prototype === */ +:root { + --s-bg: #fef9ef; + --s-ink: #1d1830; + --s-accent: #ff7a59; + --s-accent-2: #3aa6ff; + --s-accent-3: #1cc97c; + --s-accent-4: #ffd166; + --s-accent-5: #a55cff; + --s-surface: #ffffff; + --s-surface-warm: #ffe5d4; + --s-surface-cool: #d4f0ff; + --s-surface-purple: #e5e0ff; + --s-surface-green: #d4ffd9; + --s-surface-yellow: #fff5b0; + --s-radius-lg: 24px; + --s-radius-md: 16px; + --s-radius-pill: 99px; + --s-shadow: 6px 6px 0 var(--s-ink); + --s-shadow-lg: 8px 8px 0 var(--s-ink); + --s-shadow-sm: 3px 3px 0 var(--s-ink); + --s-border: 2.5px solid var(--s-ink); + --s-border-thin: 2px solid var(--s-ink); + --s-display: 'Fraunces', Georgia, serif; + --s-body: 'DM Sans', system-ui, sans-serif; + --s-mono: 'JetBrains Mono', ui-monospace, monospace; + --s-density: 1; +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + background: var(--s-bg); + background-image: + radial-gradient(circle at 15% 12%, #ffe5d4 0, transparent 28%), + radial-gradient(circle at 85% 6%, #d4f0ff 0, transparent 30%), + radial-gradient(circle at 75% 65%, #e5e0ff 0, transparent 26%), + radial-gradient(circle at 10% 80%, #fff0d4 0, transparent 22%); + color: var(--s-ink); + font-family: var(--s-body); + font-size: 16px; + line-height: 1.5; + min-height: 100vh; + overflow-x: hidden; +} +body::before { + content: ''; position: fixed; inset: 0; + background-image: radial-gradient(rgba(29, 24, 48, 0.07) 1.1px, transparent 1.1px); + background-size: 22px 22px; + pointer-events: none; z-index: 0; +} + +.s-display { font-family: var(--s-display); font-weight: 800; letter-spacing: -0.025em; } +.s-mono { font-family: var(--s-mono); } +.s-shell { max-width: 1280px; margin: 0 auto; padding: 0 32px; position: relative; z-index: 1; } + +/* Nav */ +.s-nav { display: flex; align-items: center; justify-content: space-between; padding: 24px 0 8px; } +.s-logo { display: flex; align-items: center; gap: 14px; } +.s-logomark { + width: 48px; height: 48px; border-radius: 14px; + background: var(--s-accent); + border: var(--s-border); box-shadow: 4px 4px 0 var(--s-ink); + display: grid; place-items: center; + transform: rotate(-6deg); transition: transform 0.3s cubic-bezier(.4,1.8,.4,1); +} +.s-logo:hover .s-logomark { transform: rotate(6deg) scale(1.05); } +.s-logomark svg { width: 28px; height: 28px; } +.s-brand-name { font-family: var(--s-display); font-size: 24px; font-weight: 900; letter-spacing: -0.025em; line-height: 1; } +.s-brand-sub { font-size: 12px; color: rgba(29, 24, 48, 0.55); font-family: var(--s-mono); margin-top: 2px; } +.s-navlinks { display: flex; gap: 4px; } +.s-navlinks button { + font: inherit; padding: 9px 16px; border-radius: var(--s-radius-pill); + font-size: 13px; font-weight: 600; cursor: pointer; + background: transparent; border: 0; color: var(--s-ink); +} +.s-navlinks button.active { background: var(--s-ink); color: var(--s-bg); } +.s-navlinks button:hover:not(.active) { background: rgba(29,24,48, 0.08); } +.s-nav-right { display: flex; gap: 10px; align-items: center; } +.s-bm-count { + width: 38px; height: 38px; border-radius: 50%; + background: var(--s-surface); border: var(--s-border-thin); + display: grid; place-items: center; font-weight: 700; font-size: 14px; + box-shadow: 3px 3px 0 var(--s-ink); position: relative; cursor: pointer; +} +.s-bm-count.has::after { + content: ''; position: absolute; top: 4px; right: 4px; + width: 8px; height: 8px; border-radius: 50%; background: var(--s-accent); + border: 1.5px solid var(--s-ink); +} +.s-cta { + padding: 12px 22px; border-radius: var(--s-radius-pill); + background: var(--s-ink); color: var(--s-bg); + border: var(--s-border); box-shadow: 4px 4px 0 var(--s-accent); + font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; + display: flex; align-items: center; gap: 8px; + transition: transform .12s, box-shadow .12s; +} +.s-cta:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--s-accent); } +.s-cta:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--s-accent); } diff --git a/website/sticker-app.jsx b/website/sticker-app.jsx new file mode 100644 index 000000000..c28193624 --- /dev/null +++ b/website/sticker-app.jsx @@ -0,0 +1,299 @@ +// === Sticker Pack · main App === + +function StickerApp() { + const [t, setTweak] = useTweaks(window.STICKER_TWEAK_DEFAULTS); + const [search, setSearch] = useState(''); + const [cat, setCat] = useState('all'); + const [bm, setBm] = useState(() => window.PBP_BM.get()); + const [sort, setSort] = useState('default'); + const [open, setOpen] = useState(null); + const [showBmOnly, setShowBmOnly] = useState(false); + + const toggleBm = (id) => setBm(window.PBP_BM.toggle(id)); + + // Filter + sort + const filtered = useMemo(() => { + let arr = window.PBP_PROJECTS.filter(p => { + if (showBmOnly && !bm.includes(p.id)) return false; + if (cat !== 'all' && p.cat !== cat) return false; + if (search) { + const q = search.toLowerCase(); + return p.name.toLowerCase().includes(q) || p.blurb.toLowerCase().includes(q) || p.deps.toLowerCase().includes(q); + } + return true; + }); + if (sort === 'alpha') arr = [...arr].sort((a,b) => a.name.localeCompare(b.name)); + if (sort === 'short') arr = [...arr].sort((a,b) => a.lines - b.lines); + if (sort === 'bookmarks') arr = [...arr].sort((a,b) => (bm.includes(b.id) ? 1 : 0) - (bm.includes(a.id) ? 1 : 0)); + return arr; + }, [search, cat, bm, sort, showBmOnly]); + + // Tweak CSS vars + const rootStyle = useMemo(() => ({ + '--s-accent': t.accent, + '--s-display': t.fontDisplay, + '--s-body': t.fontBody, + '--s-bg': t.surface === 'cream' ? '#fef9ef' : t.surface === 'mint' ? '#eefbf2' : t.surface === 'sky' ? '#eff6ff' : t.surface === 'lilac' ? '#f4eefe' : '#fef9ef', + }), [t]); + + return ( +
+
+ + {/* NAV */} + + + {/* HERO */} +
+
+
Live · 100 projects from 241 humans
+

+ Python +
+ that feels good +
+ to type. +

+

A scrappy collection of tiny Python projects. Each one is small enough to read in a coffee break and big enough to teach you something real. Pick a vibe. Type it out. Run it.

+
+ + +
+
+ +
+ + {/* STATS */} +
+
100
Projects
+
241
Contributors
+
2.3k
★ Stars
+
903
Forks
+
+ + {/* PATHS */} +
+
+

Three paths in.

+
Not sure where to start? Each path is three projects, building on each other. Finish one in a weekend.
+
+
+ {window.PBP_PATHS.map((p, idx) => ( +
+
+
{p.tag.toUpperCase()}
+
0{idx+1}
+
+

{p.name}

+

{p.desc}

+
+ {p.items.map((id, i) => { + const pr = window.PBP_PROJECTS.find(d => d.id === id); + return pr && ( +
setOpen(pr)}> + 0{i+1} + {pr.name} + {pr.emoji} +
+ ); + })} +
+
+ ))} +
+
+ + {/* GALLERY */} + + + {/* CONTRIBUTORS */} +
+
+

Made by humans.

+
241 contributors so far. PRs welcome. Add your own beginner project.
+
+
+
+
{window.PBP_CONTRIBUTORS[0].name[0]}
+
{window.PBP_CONTRIBUTORS[0].name}
+
@{window.PBP_CONTRIBUTORS[0].handle}
+

"I started this repo to learn Python myself. It turned into a place where 240+ people teach each other. Best happy accident I've ever shipped."

+
Maintainer · 412 commits
+
+ {window.PBP_CONTRIBUTORS.slice(1, 13).map(c => ( +
+
{c.name[0]}
+
{c.name.split(' ')[0]}
+
@{c.handle.length > 12 ? c.handle.slice(0,12) + '…' : c.handle}
+
{c.commits} ★
+
+ ))} +
+
+ +
+
+ + {/* CTA BANNER */} +
+
+

Want to contribute?

+

Drop your own beginner project as a PR. One folder, one Python file, one README. We merge if it runs.

+
+ +
+
+
+
+ $ + git clone python-beginner-projects.git + copy +
+
+ $ + cd projects && mkdir my-project + copy +
+
+
+ + +
+ + {open && setOpen(null)} isBm={bm.includes(open.id)} toggleBm={toggleBm} />} + + + + setTweak('accent', v)} /> + setTweak('surface', v)} /> + + + setTweak('layout', v)} /> + setTweak('cardStyle', v)} /> + + + setTweak('fontDisplay', v)} /> + setTweak('fontBody', v)} /> + +
+ ); +} + +ReactDOM.createRoot(document.getElementById('root')).render(); diff --git a/website/sticker-components.jsx b/website/sticker-components.jsx new file mode 100644 index 000000000..9fca3f761 --- /dev/null +++ b/website/sticker-components.jsx @@ -0,0 +1,361 @@ +// === Sticker Pack · standalone prototype components === + +const { useState, useEffect, useMemo, useRef } = React; + +function StickerLogo() { + return ( + + + + + + + ); +} + +function ScribbleSvg({ color }) { + // SVG inline so accent color works without rebuilding background-image + return ( + + + + ); +} + +function HeroStickers() { + // Bobbing pile of stickers in the hero — pulls a few highlight projects + const items = [ + { e: '🐍', l: 'Snake Game', n: '92 lines · pygame', bg: 'var(--s-surface-warm)', pos: { top: 6, left: 18 }, bob: 1, rot: -4 }, + { e: '☁️', l: 'Weather App', n: '88 lines · requests',bg: 'var(--s-surface-cool)', pos: { top: 80, right: 0 }, bob: 2, rot: 3 }, + { e: '🪢', l: 'Hangman', n: '78 lines · stdlib', bg: 'var(--s-surface-purple)',pos: { top: 215, left: -10 },bob: 3, rot: 2 }, + { e: '▦', l: 'QR Generator',n: '22 lines · qrcode', bg: 'var(--s-surface-green)', pos: { top: 290, right: 50 },bob: 4, rot: -2 }, + { e: '🧮', l: 'Calculator', n: '110 lines · tk', bg: 'var(--s-surface-yellow)',pos: { top: 400, left: 60 }, bob: 5, rot: 4 }, + { e: '🔐', l: 'Password Gen',n: '30 lines · secrets', bg: '#ffd4f0', pos: { top: 410, right: 10 },bob: 1, rot: -3 }, + ]; + return ( +
+ {items.map((s, i) => ( +
+ {s.e} +
+
{s.l}
+
{s.n}
+
+
+ ))} +
+ ); +} + +function ProjectCard({ p, cardStyle, isBm, toggleBm, onOpen }) { + return ( +
onOpen(p)}> + {cardStyle === 'image' &&
{p.emoji}
} + {cardStyle !== 'image' &&
{p.emoji}
} +
+

{p.name}

+ +
+

{p.blurb}

+
+ {p.lines} lines + {p.deps} + {p.runnable && ▶ Try it} +
+
+ ); +} + +function ProjectModal({ p, onClose, isBm, toggleBm }) { + useEffect(() => { + const k = (e) => e.key === 'Escape' && onClose(); + window.addEventListener('keydown', k); + return () => window.removeEventListener('keydown', k); + }, [onClose]); + if (!p) return null; + return ( +
+
e.stopPropagation()}> + +
{p.emoji}
+

{p.name}

+

{p.blurb}

+
+ {p.lines} lines + {p.deps} + {p.cat} +
+
+
$ git clone python-beginner-projects.git
+
$ cd projects/{p.id}
+
$ python {p.id}.py
+
+ {p.runnable && } +
+ + +
+
+
+ ); +} + +// ============================================================ +// Try It — live demo panel inside the project modal. +// Renders a simple input form for stateless projects (rps, bmi, qr, +// madlibs) and hands off to dedicated widgets for the turn-based ones +// (tictactoe, hangman). All POSTs go to the single Vercel backend. +// ============================================================ + +function TryItPanel({ project }) { + const api = window.PBP_API; + if (api.isInteractive(project.id)) { + if (project.id === 'tictactoe') return ; + if (project.id === 'hangman') return ; + } + return ; +} + +function SimpleRunner({ project }) { + const fields = window.PBP_API.inputsFor(project.id); + const [form, setForm] = useState(() => window.PBP_API.defaults(project.id)); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [out, setOut] = useState(null); + + const run = async () => { + setError(null); + setLoading(true); + try { + const res = await window.PBP_API.call(project.id, form); + setOut(res); + } catch (e) { + setError(e.message || 'Request failed'); + setOut(null); + } finally { + setLoading(false); + } + }; + + return ( +
+
▶ Try it live
+
+ {fields.map(f => ( + + ))} +
+ + {error &&
⚠ {error}
} + {out && } +
+ ); +} + +function castOption(field, raw) { + if (Array.isArray(field.options) && field.options.length && typeof field.options[0] === 'object') { + const match = field.options.find(o => String(o.value) === raw); + return match ? match.value : raw; + } + return raw; +} + +function SimpleOutput({ projectId, out }) { + if (projectId === 'qr' && out.png_b64) { + return ( +
+ QR code +
+ ); + } + if (projectId === 'bmi') { + return ( +
+
{out.bmi}
+
{out.category}
+
+ ); + } + if (projectId === 'rps') { + const verdict = { win: '🎉 You win', lose: '😅 You lose', draw: '🤝 Draw' }[out.result] || out.result; + return ( +
+
You: {out.player} · CPU: {out.cpu}
+
{verdict}
+
+ ); + } + if (projectId === 'madlibs') { + return
{out.story}
; + } + return
{JSON.stringify(out, null, 2)}
; +} + +// ---- Tic Tac Toe interactive ---- + +const EMPTY_BOARD = ['', '', '', '', '', '', '', '', '']; + +function TicTacToePlay() { + const [board, setBoard] = useState(EMPTY_BOARD); + const [status, setStatus] = useState('ongoing'); + const [winner, setWinner] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const reset = () => { setBoard(EMPTY_BOARD); setStatus('ongoing'); setWinner(null); setError(null); }; + + const play = async (idx) => { + if (board[idx] || status !== 'ongoing' || loading) return; + setError(null); + setLoading(true); + try { + const res = await window.PBP_API.call('tictactoe', { board, player: 'X', move: idx }); + setBoard(res.board); + setStatus(res.status); + setWinner(res.winner || null); + } catch (e) { + setError(e.message); + } finally { + setLoading(false); + } + }; + + const headline = status === 'win' + ? (winner === 'X' ? '🎉 You win' : '🤖 CPU wins') + : status === 'draw' ? '🤝 Draw' : 'Your move (X)'; + + return ( +
+
▶ Try it live · vs the bot
+
+
{headline}
+ +
+
+ {board.map((cell, i) => ( + + ))} +
+ {error &&
⚠ {error}
} +
+ ); +} + +// ---- Hangman interactive ---- + +const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); + +function HangmanPlay() { + const [seed, setSeed] = useState(() => Math.floor(Math.random() * 1e9)); + const [difficulty, setDifficulty] = useState('medium'); + const [guessed, setGuessed] = useState([]); + const [state, setState] = useState(null); // {mask, wrong, tries_left, max_tries, status, word_length, word?} + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const refresh = async (nextGuessed, nextDifficulty = difficulty, nextSeed = seed) => { + setError(null); + setLoading(true); + try { + const res = await window.PBP_API.call('hangman', { word_seed: nextSeed, guessed: nextGuessed, difficulty: nextDifficulty }); + setState(res); + } catch (e) { + setError(e.message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { refresh([], difficulty, seed); /* eslint-disable-next-line */ }, []); + + const guess = (letter) => { + if (loading || !state || state.status !== 'ongoing' || guessed.includes(letter)) return; + const next = [...guessed, letter]; + setGuessed(next); + refresh(next); + }; + + return ( +
+
▶ Try it live · pick letters
+
+ + +
+ {state && ( + <> +
+ {state.mask.split('').map((c, i) => {c === '_' ? '·' : c})} +
+
+ {state.tries_left} / {state.max_tries} tries left + {state.wrong.length ? 'wrong: ' + state.wrong.join(' ') : ''} +
+
+ {ALPHABET.map(l => { + const used = guessed.includes(l); + const hit = used && state.mask.includes(l); + return ( + + ); + })} +
+ {state.status === 'win' &&
🎉 You got it!
} + {state.status === 'lose' &&
😅 Out of tries — the word was {state.word}
} + + )} + {error &&
⚠ {error}
} +
+ ); +} + +window.StickerLogo = StickerLogo; +window.ScribbleSvg = ScribbleSvg; +window.HeroStickers = HeroStickers; +window.ProjectCard = ProjectCard; +window.ProjectModal = ProjectModal; diff --git a/website/tweaks-panel.jsx b/website/tweaks-panel.jsx new file mode 100644 index 000000000..79ccfe918 --- /dev/null +++ b/website/tweaks-panel.jsx @@ -0,0 +1,568 @@ + +// tweaks-panel.jsx +// Reusable Tweaks shell + form-control helpers. +// +// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode, +// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so +// individual prototypes don't re-roll it. Ships a consistent set of controls so you +// don't hand-draw , segmented radios, steppers, etc. +// +// Usage (in an HTML file that loads React + Babel): +// +// const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ +// "primaryColor": "#D97757", +// "palette": ["#D97757", "#29261b", "#f6f4ef"], +// "fontSize": 16, +// "density": "regular", +// "dark": false +// }/*EDITMODE-END*/; +// +// function App() { +// const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); +// return ( +//
+// Hello +// +// +// setTweak('fontSize', v)} /> +// setTweak('density', v)} /> +// +// setTweak('primaryColor', v)} /> +// setTweak('palette', v)} /> +// setTweak('dark', v)} /> +// +//
+// ); +// } +// +// ───────────────────────────────────────────────────────────────────────────── + +const __TWEAKS_STYLE = ` + .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px; + max-height:calc(100vh - 32px);display:flex;flex-direction:column; + transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right; + background:rgba(250,249,247,.78);color:#29261b; + -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%); + border:.5px solid rgba(255,255,255,.6);border-radius:14px; + box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18); + font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden} + .twk-hd{display:flex;align-items:center;justify-content:space-between; + padding:10px 8px 10px 14px;cursor:move;user-select:none} + .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em} + .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55); + width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1} + .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b} + .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px; + overflow-y:auto;overflow-x:hidden;min-height:0; + scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent} + .twk-body::-webkit-scrollbar{width:8px} + .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px} + .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px; + border:2px solid transparent;background-clip:content-box} + .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25); + border:2px solid transparent;background-clip:content-box} + .twk-row{display:flex;flex-direction:column;gap:5px} + .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px} + .twk-lbl{display:flex;justify-content:space-between;align-items:baseline; + color:rgba(41,38,27,.72)} + .twk-lbl>span:first-child{font-weight:500} + .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums} + + .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase; + color:rgba(41,38,27,.45);padding:10px 0 0} + .twk-sect:first-child{padding-top:0} + + .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px; + border:.5px solid rgba(0,0,0,.1);border-radius:7px; + background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none} + .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)} + select.twk-field{padding-right:22px; + background-image:url("data:image/svg+xml;utf8,"); + background-repeat:no-repeat;background-position:right 8px center} + + .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0; + border-radius:999px;background:rgba(0,0,0,.12);outline:none} + .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none; + width:14px;height:14px;border-radius:50%;background:#fff; + border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default} + .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%; + background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default} + + .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px; + background:rgba(0,0,0,.06);user-select:none} + .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px; + background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12); + transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s} + .twk-seg.dragging .twk-seg-thumb{transition:none} + .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0; + background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px; + border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2; + overflow-wrap:anywhere} + + .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px; + background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0} + .twk-toggle[data-on="1"]{background:#34c759} + .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%; + background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s} + .twk-toggle[data-on="1"] i{transform:translateX(14px)} + + .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px; + border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)} + .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize; + user-select:none;padding-right:8px} + .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent; + font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0; + outline:none;color:inherit;-moz-appearance:textfield} + .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{ + -webkit-appearance:none;margin:0} + .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)} + + .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px; + background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default} + .twk-btn:hover{background:rgba(0,0,0,.88)} + .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit} + .twk-btn.secondary:hover{background:rgba(0,0,0,.1)} + + .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px; + border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default; + background:transparent;flex-shrink:0} + .twk-swatch::-webkit-color-swatch-wrapper{padding:0} + .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px} + .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px} + + .twk-chips{display:flex;gap:6px} + .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px; + padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default; + box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06); + transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s} + .twk-chip:hover{transform:translateY(-1px); + box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)} + .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85), + 0 2px 6px rgba(0,0,0,.15)} + .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%; + display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)} + .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)} + .twk-chip>span>i:first-child{box-shadow:none} + .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px; + filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))} +`; + +// ── useTweaks ─────────────────────────────────────────────────────────────── +// Single source of truth for tweak values. setTweak persists via the host +// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk). +function useTweaks(defaults) { + const [values, setValues] = React.useState(defaults); + // Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a + // useState-style call doesn't write a "[object Object]" key into the persisted + // JSON block. + const setTweak = React.useCallback((keyOrEdits, val) => { + const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null + ? keyOrEdits : { [keyOrEdits]: val }; + setValues((prev) => ({ ...prev, ...edits })); + window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*'); + // Same-window signal so in-page listeners (deck-stage rail thumbnails) + // can react — the parent message only reaches the host, not peers. + window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits })); + }, []); + return [values, setTweak]; +} + +// ── TweaksPanel ───────────────────────────────────────────────────────────── +// Floating shell. Registers the protocol listener BEFORE announcing +// availability — if the announce ran first, the host's activate could land +// before our handler exists and the toolbar toggle would silently no-op. +// The close button posts __edit_mode_dismissed so the host's toolbar toggle +// flips off in lockstep; the host echoes __deactivate_edit_mode back which +// is what actually hides the panel. +function TweaksPanel({ title = 'Tweaks', noDeckControls = false, children }) { + const [open, setOpen] = React.useState(false); + const dragRef = React.useRef(null); + // Auto-inject a rail toggle when a is on the page. The + // toggle drives the deck's per-viewer _railVisible via window message; + // state is mirrored from the same localStorage key the deck reads so + // the control reflects reality across reloads. The mechanism is the + // message — authors who want custom placement can post it directly + // and pass noDeckControls to suppress this one. + const hasDeckStage = React.useMemo( + () => typeof document !== 'undefined' && !!document.querySelector('deck-stage'), + [], + ); + // deck-stage enables its rail in connectedCallback, but this panel can + // mount before that element has upgraded. The initial read catches the + // common case; the listener covers mounting first. (Older deck-stage.js + // copies still wait for the host's __omelette_rail_enabled postMessage — + // same listener handles those.) + const [railEnabled, setRailEnabled] = React.useState( + () => hasDeckStage && !!document.querySelector('deck-stage')?._railEnabled, + ); + React.useEffect(() => { + if (!hasDeckStage || railEnabled) return undefined; + const onMsg = (e) => { + if (e.data && e.data.type === '__omelette_rail_enabled') setRailEnabled(true); + }; + window.addEventListener('message', onMsg); + return () => window.removeEventListener('message', onMsg); + }, [hasDeckStage, railEnabled]); + const [railVisible, setRailVisible] = React.useState(() => { + try { return localStorage.getItem('deck-stage.railVisible') !== '0'; } catch (e) { return true; } + }); + const toggleRail = (on) => { + setRailVisible(on); + window.postMessage({ type: '__deck_rail_visible', on }, '*'); + }; + const offsetRef = React.useRef({ x: 16, y: 16 }); + const PAD = 16; + + const clampToViewport = React.useCallback(() => { + const panel = dragRef.current; + if (!panel) return; + const w = panel.offsetWidth, h = panel.offsetHeight; + const maxRight = Math.max(PAD, window.innerWidth - w - PAD); + const maxBottom = Math.max(PAD, window.innerHeight - h - PAD); + offsetRef.current = { + x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)), + y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)), + }; + panel.style.right = offsetRef.current.x + 'px'; + panel.style.bottom = offsetRef.current.y + 'px'; + }, []); + + React.useEffect(() => { + if (!open) return; + clampToViewport(); + if (typeof ResizeObserver === 'undefined') { + window.addEventListener('resize', clampToViewport); + return () => window.removeEventListener('resize', clampToViewport); + } + const ro = new ResizeObserver(clampToViewport); + ro.observe(document.documentElement); + return () => ro.disconnect(); + }, [open, clampToViewport]); + + React.useEffect(() => { + const onMsg = (e) => { + const t = e?.data?.type; + if (t === '__activate_edit_mode') setOpen(true); + else if (t === '__deactivate_edit_mode') setOpen(false); + }; + window.addEventListener('message', onMsg); + window.parent.postMessage({ type: '__edit_mode_available' }, '*'); + return () => window.removeEventListener('message', onMsg); + }, []); + + const dismiss = () => { + setOpen(false); + window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*'); + }; + + const onDragStart = (e) => { + const panel = dragRef.current; + if (!panel) return; + const r = panel.getBoundingClientRect(); + const sx = e.clientX, sy = e.clientY; + const startRight = window.innerWidth - r.right; + const startBottom = window.innerHeight - r.bottom; + const move = (ev) => { + offsetRef.current = { + x: startRight - (ev.clientX - sx), + y: startBottom - (ev.clientY - sy), + }; + clampToViewport(); + }; + const up = () => { + window.removeEventListener('mousemove', move); + window.removeEventListener('mouseup', up); + }; + window.addEventListener('mousemove', move); + window.addEventListener('mouseup', up); + }; + + if (!open) return null; + return ( + <> + +
+
+ {title} + +
+
+ {children} + {hasDeckStage && railEnabled && !noDeckControls && ( + + + + )} +
+
+ + ); +} + +// ── Layout helpers ────────────────────────────────────────────────────────── + +function TweakSection({ label, children }) { + return ( + <> +
{label}
+ {children} + + ); +} + +function TweakRow({ label, value, children, inline = false }) { + return ( +
+
+ {label} + {value != null && {value}} +
+ {children} +
+ ); +} + +// ── Controls ──────────────────────────────────────────────────────────────── + +function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) { + return ( + + onChange(Number(e.target.value))} /> + + ); +} + +function TweakToggle({ label, value, onChange }) { + return ( +
+
{label}
+ +
+ ); +} + +function TweakRadio({ label, value, options, onChange }) { + const trackRef = React.useRef(null); + const [dragging, setDragging] = React.useState(false); + // The active value is read by pointer-move handlers attached for the lifetime + // of a drag — ref it so a stale closure doesn't fire onChange for every move. + const valueRef = React.useRef(value); + valueRef.current = value; + + // Segments wrap mid-word once per-segment width runs out. The track is + // ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px + // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2 + // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall + // back to a dropdown rather than wrap. + const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length; + const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0); + const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0); + if (!fitsAsSegments) { + // onChange(e.target.value)}> + {options.map((o) => { + const v = typeof o === 'object' ? o.value : o; + const l = typeof o === 'object' ? o.label : o; + return ; + })} + + + ); +} + +function TweakText({ label, value, placeholder, onChange }) { + return ( + + onChange(e.target.value)} /> + + ); +} + +function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) { + const clamp = (n) => { + if (min != null && n < min) return min; + if (max != null && n > max) return max; + return n; + }; + const startRef = React.useRef({ x: 0, val: 0 }); + const onScrubStart = (e) => { + e.preventDefault(); + startRef.current = { x: e.clientX, val: value }; + const decimals = (String(step).split('.')[1] || '').length; + const move = (ev) => { + const dx = ev.clientX - startRef.current.x; + const raw = startRef.current.val + dx * step; + const snapped = Math.round(raw / step) * step; + onChange(clamp(Number(snapped.toFixed(decimals)))); + }; + const up = () => { + window.removeEventListener('pointermove', move); + window.removeEventListener('pointerup', up); + }; + window.addEventListener('pointermove', move); + window.addEventListener('pointerup', up); + }; + return ( +
+ {label} + onChange(clamp(Number(e.target.value)))} /> + {unit && {unit}} +
+ ); +} + +// Relative-luminance contrast pick — checkmarks drawn over a swatch need to +// read on both #111 and #fafafa without per-option configuration. Hex input +// only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light". +function __twkIsLight(hex) { + const h = String(hex).replace('#', ''); + const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0'); + const n = parseInt(x.slice(0, 6), 16); + if (Number.isNaN(n)) return true; + const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; + return r * 299 + g * 587 + b * 114 > 148000; +} + +const __TwkCheck = ({ light }) => ( + +); + +// TweakColor — curated color/palette picker. Each option is either a single +// hex string or an array of 1-5 hex strings; the card adapts — a lone color +// renders solid, a palette renders colors[0] as the hero (left ~2/3) with the +// rest stacked in a sharp column on the right. onChange emits the +// option in the shape it was passed (string stays string, array stays array). +// Without options it falls back to the native color input for back-compat. +function TweakColor({ label, value, options, onChange }) { + if (!options || !options.length) { + return ( +
+
{label}
+ onChange(e.target.value)} /> +
+ ); + } + // Native emits lowercase hex per the HTML spec, so + // compare case-insensitively. String() guards JSON.stringify(undefined), + // which returns the primitive undefined (no .toLowerCase). + const key = (o) => String(JSON.stringify(o)).toLowerCase(); + const cur = key(value); + return ( + +
+ {options.map((o, i) => { + const colors = Array.isArray(o) ? o : [o]; + const [hero, ...rest] = colors; + const sup = rest.slice(0, 4); + const on = key(o) === cur; + return ( + + ); + })} +
+
+ ); +} + +function TweakButton({ label, onClick, secondary = false }) { + return ( + + ); +} + +Object.assign(window, { + useTweaks, TweaksPanel, TweakSection, TweakRow, + TweakSlider, TweakToggle, TweakRadio, TweakSelect, + TweakText, TweakNumber, TweakColor, TweakButton, +}); diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 000000000..3b202d8be --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,11 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "pybegin-api", + "main": "src/worker.py", + "compatibility_date": "2024-09-23", + "compatibility_flags": ["python_workers"], + "routes": [ + { "pattern": "pybegin.its-mrinank.com", "custom_domain": true } + ], + "observability": { "enabled": true } +} From d8fc3cfea29d614b86582e4680195cfb67acfd73 Mon Sep 17 00:00:00 2001 From: Mrinank-Bhowmick <77621953+Mrinank-Bhowmick@users.noreply.github.com> Date: Thu, 21 May 2026 18:20:07 +0530 Subject: [PATCH 02/14] improved frontend and added Pyodide --- .gitignore | 7 + web/.gitignore | 45 + web/next.config.ts | 24 + web/package-lock.json | 1259 ++++++++++++++++++++++ web/package.json | 28 + web/public/_headers | 12 + web/public/playground/bmi.py | 20 + web/public/playground/hangman.py | 43 + web/public/playground/madlibs.py | 15 + web/public/playground/pwd.py | 16 + web/public/playground/qr.py | 15 + web/public/playground/rps.py | 21 + web/public/playground/tictactoe.py | 67 ++ web/public/pyodide-worker.js | 139 +++ web/src/app/globals.css | 952 ++++++++++++++++ web/src/app/layout.tsx | 72 ++ web/src/app/not-found.tsx | 22 + web/src/app/opengraph-image.tsx | 69 ++ web/src/app/page.tsx | 44 + web/src/app/playground/[id]/page.tsx | 82 ++ web/src/app/playground/page.tsx | 48 + web/src/app/projects/[id]/page.tsx | 128 +++ web/src/app/robots.ts | 10 + web/src/app/sitemap.ts | 29 + web/src/components/HeroStickers.tsx | 46 + web/src/components/Home.tsx | 384 +++++++ web/src/components/Playground.tsx | 452 ++++++++ web/src/components/PlaygroundNav.tsx | 34 + web/src/components/PlaygroundSidebar.tsx | 163 +++ web/src/components/ProjectCard.tsx | 44 + web/src/components/ProjectCredit.tsx | 23 + web/src/components/ProjectModal.tsx | 85 ++ web/src/components/ScribbleSvg.tsx | 26 + web/src/components/SiteFooter.tsx | 18 + web/src/components/SiteNav.tsx | 46 + web/src/components/StickerLogo.tsx | 15 + web/src/components/TryItPanel.tsx | 406 +++++++ web/src/lib/api.ts | 108 ++ web/src/lib/bookmarks.ts | 27 + web/src/lib/cmTheme.ts | 41 + web/src/lib/data.ts | 76 ++ web/src/types.ts | 48 + web/tsconfig.json | 34 + website/api.js | 81 -- website/data.js | 70 -- website/index.html | 47 - website/sticker-app-2.css | 133 --- website/sticker-app-3.css | 417 ------- website/sticker-app.css | 100 -- website/sticker-app.jsx | 299 ----- website/sticker-components.jsx | 361 ------- website/tweaks-panel.jsx | 568 ---------- 52 files changed, 5243 insertions(+), 2076 deletions(-) create mode 100644 web/.gitignore create mode 100644 web/next.config.ts create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/public/_headers create mode 100644 web/public/playground/bmi.py create mode 100644 web/public/playground/hangman.py create mode 100644 web/public/playground/madlibs.py create mode 100644 web/public/playground/pwd.py create mode 100644 web/public/playground/qr.py create mode 100644 web/public/playground/rps.py create mode 100644 web/public/playground/tictactoe.py create mode 100644 web/public/pyodide-worker.js create mode 100644 web/src/app/globals.css create mode 100644 web/src/app/layout.tsx create mode 100644 web/src/app/not-found.tsx create mode 100644 web/src/app/opengraph-image.tsx create mode 100644 web/src/app/page.tsx create mode 100644 web/src/app/playground/[id]/page.tsx create mode 100644 web/src/app/playground/page.tsx create mode 100644 web/src/app/projects/[id]/page.tsx create mode 100644 web/src/app/robots.ts create mode 100644 web/src/app/sitemap.ts create mode 100644 web/src/components/HeroStickers.tsx create mode 100644 web/src/components/Home.tsx create mode 100644 web/src/components/Playground.tsx create mode 100644 web/src/components/PlaygroundNav.tsx create mode 100644 web/src/components/PlaygroundSidebar.tsx create mode 100644 web/src/components/ProjectCard.tsx create mode 100644 web/src/components/ProjectCredit.tsx create mode 100644 web/src/components/ProjectModal.tsx create mode 100644 web/src/components/ScribbleSvg.tsx create mode 100644 web/src/components/SiteFooter.tsx create mode 100644 web/src/components/SiteNav.tsx create mode 100644 web/src/components/StickerLogo.tsx create mode 100644 web/src/components/TryItPanel.tsx create mode 100644 web/src/lib/api.ts create mode 100644 web/src/lib/bookmarks.ts create mode 100644 web/src/lib/cmTheme.ts create mode 100644 web/src/lib/data.ts create mode 100644 web/src/types.ts create mode 100644 web/tsconfig.json delete mode 100644 website/api.js delete mode 100644 website/data.js delete mode 100644 website/index.html delete mode 100644 website/sticker-app-2.css delete mode 100644 website/sticker-app-3.css delete mode 100644 website/sticker-app.css delete mode 100644 website/sticker-app.jsx delete mode 100644 website/sticker-components.jsx delete mode 100644 website/tweaks-panel.jsx diff --git a/.gitignore b/.gitignore index 90a4c0015..becd20775 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,10 @@ cython_debug/ #.DS_Store .DS_Store + +# Next.js frontend (web/) — the Python `lib/` rule above would otherwise +# swallow web/src/lib, so explicitly un-ignore it. +!web/src/lib/ +web/node_modules/ +web/.next/ +web/out/ diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 000000000..d36cbddfb --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +# NEXT_PUBLIC_API_BASE is a public URL, not a secret — keep it in version control. +!.env.production + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.env.production \ No newline at end of file diff --git a/web/next.config.ts b/web/next.config.ts new file mode 100644 index 000000000..f160ef1c3 --- /dev/null +++ b/web/next.config.ts @@ -0,0 +1,24 @@ +import type { NextConfig } from "next"; + +// Cross-origin isolation headers — required for SharedArrayBuffer (the +// playground's blocking stdin). In production these come from public/_headers +// on Cloudflare Pages; this `headers()` block applies them during `next dev`. +const coopCoep = [ + { key: "Cross-Origin-Opener-Policy", value: "same-origin" }, + { key: "Cross-Origin-Embedder-Policy", value: "require-corp" }, +]; + +const nextConfig: NextConfig = { + // Static export — builds to ./out, deployed to Cloudflare Pages. + output: "export", + // No Next.js image optimization server in a static export. + images: { unoptimized: true }, + // Pretty, trailing-slash URLs map cleanly to Pages' static routing. + trailingSlash: true, + // Applies in `next dev` only; ignored by `output: export` (Pages uses _headers). + async headers() { + return [{ source: "/:path*", headers: coopCoep }]; + }, +}; + +export default nextConfig; diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 000000000..c5aac0592 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1259 @@ +{ + "name": "web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.1.0", + "dependencies": { + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lint": "^6.9.6", + "@lezer/highlight": "^1.2.3", + "@uiw/codemirror-themes": "^4.25.9", + "@uiw/react-codemirror": "^4.25.9", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "next": "16.2.6", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz", + "integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz", + "integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", + "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@next/env": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.9.tgz", + "integrity": "sha512-QFAqr+pu6lDmNpAlecODcF49TlsrZ0bj15zPzfhiqSDl+Um3EsDLFLppixC7kFLn+rdDM2LTvVjn5CPvefpRgw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.9.tgz", + "integrity": "sha512-DAHKb/L9ELwjY4nCf/MP/mIllHOn4GQe7RR4x8AMJuNeh9nGRRoo1uPxrxMmUL/bKqe6kDmDbIZ2AlhlqyIJuw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.9.tgz", + "integrity": "sha512-HftqCBUYShAOH0pGi1CHP8vfm5L8fQ3+0j0VI6lQD6QpK+UBu3J7nxfEN5O/BXMilMNf9ZyFJRvRcuMMOLHMng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.9", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 000000000..68ccaec99 --- /dev/null +++ b/web/package.json @@ -0,0 +1,28 @@ +{ + "name": "web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lint": "^6.9.6", + "@lezer/highlight": "^1.2.3", + "@uiw/codemirror-themes": "^4.25.9", + "@uiw/react-codemirror": "^4.25.9", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "next": "16.2.6", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } +} diff --git a/web/public/_headers b/web/public/_headers new file mode 100644 index 000000000..92c4b3652 --- /dev/null +++ b/web/public/_headers @@ -0,0 +1,12 @@ +# Cross-origin isolation — enables SharedArrayBuffer, which the Pyodide +# playground uses to bridge a blocking input() to the terminal. +# The site loads no cross-origin subresources except the Pyodide CDN +# (which sends CORP headers), so applying this site-wide is safe and keeps +# client-side navigation isolated. +/* + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp + +# The generated social image has no file extension; label its content type. +/opengraph-image + Content-Type: image/png diff --git a/web/public/playground/bmi.py b/web/public/playground/bmi.py new file mode 100644 index 000000000..89c8f2e23 --- /dev/null +++ b/web/public/playground/bmi.py @@ -0,0 +1,20 @@ +# BMI Calculator +# Asks for your height and weight, then tells you your Body Mass Index. + +height = float(input("Your height in metres (e.g. 1.75): ")) +weight = float(input("Your weight in kilograms (e.g. 70): ")) + +bmi = weight / (height ** 2) + +if bmi < 18.5: + category = "Underweight" +elif bmi < 25: + category = "Normal weight" +elif bmi < 30: + category = "Overweight" +else: + category = "Obese" + +print() +print(f"Your BMI is {bmi:.1f}") +print(f"Category: {category}") diff --git a/web/public/playground/hangman.py b/web/public/playground/hangman.py new file mode 100644 index 000000000..3d27e76d6 --- /dev/null +++ b/web/public/playground/hangman.py @@ -0,0 +1,43 @@ +# Hangman +# Guess the hidden word one letter at a time before you run out of tries. + +import random + +words = ["python", "browser", "rocket", "puzzle", "guitar", "planet", "coffee"] +word = random.choice(words) + +guessed = set() +tries = 6 + +print("Welcome to Hangman! Guess the word, one letter at a time.") +print(f"The word has {len(word)} letters.") +print() + +while tries > 0: + shown = " ".join(letter if letter in guessed else "_" for letter in word) + print("Word: ", shown) + print("Tries left:", tries) + + if all(letter in guessed for letter in word): + print("\nYou win! 🎉 The word was:", word) + break + + guess = input("Guess a letter: ").strip().lower() + + if len(guess) != 1 or not guess.isalpha(): + print("Please type a single letter.\n") + continue + + if guess in guessed: + print("You already tried that one.\n") + continue + + guessed.add(guess) + + if guess in word: + print("Good guess!\n") + else: + tries -= 1 + print("Nope, that letter isn't in the word.\n") +else: + print("\nOut of tries! 😅 The word was:", word) diff --git a/web/public/playground/madlibs.py b/web/public/playground/madlibs.py new file mode 100644 index 000000000..03676c3e6 --- /dev/null +++ b/web/public/playground/madlibs.py @@ -0,0 +1,15 @@ +# Madlibs Generator +# Fill in a few words and get back a silly story. + +adjective = input("Give me an adjective: ") +animal = input("Give me an animal: ") +verb = input("Give me a verb (ending in -ing): ") +place = input("Give me a place: ") +adverb = input("Give me an adverb: ") + +print() +print("=== Your story ===") +print() +print(f"One {adjective} morning, a {animal} was {verb} near {place}.") +print(f"It looked around {adverb} and decided today would be the day.") +print(f"And so the {animal} kept {verb} — happily ever after.") diff --git a/web/public/playground/pwd.py b/web/public/playground/pwd.py new file mode 100644 index 000000000..508656389 --- /dev/null +++ b/web/public/playground/pwd.py @@ -0,0 +1,16 @@ +# Password Generator +# Builds a strong random password using the `secrets` module. + +import secrets +import string + +length = int(input("How many characters should the password have? ")) +length = max(4, min(length, 64)) + +alphabet = string.ascii_letters + string.digits + "!@#$%^&*-_" +password = "".join(secrets.choice(alphabet) for _ in range(length)) + +print() +print("Your new password:") +print(password) +print(f"({length} characters — keep it somewhere safe!)") diff --git a/web/public/playground/qr.py b/web/public/playground/qr.py new file mode 100644 index 000000000..d40c2727c --- /dev/null +++ b/web/public/playground/qr.py @@ -0,0 +1,15 @@ +# QR Code Generator +# Turns any text or URL into a QR code drawn right in the terminal. + +import qrcode + +text = input("Enter text or a URL to encode: ") + +qr = qrcode.QRCode(border=2) +qr.add_data(text) +qr.make(fit=True) + +print() +print(f"QR code for: {text}") +print() +qr.print_ascii() diff --git a/web/public/playground/rps.py b/web/public/playground/rps.py new file mode 100644 index 000000000..6525aea57 --- /dev/null +++ b/web/public/playground/rps.py @@ -0,0 +1,21 @@ +# Rock, Paper, Scissors +# Play a round against the computer's random choice. + +import random + +choices = ["rock", "paper", "scissors"] +beats = {"rock": "scissors", "paper": "rock", "scissors": "paper"} + +you = input("Pick rock, paper or scissors: ").strip().lower() +if you not in choices: + print("That's not a valid choice!") +else: + cpu = random.choice(choices) + print(f"You chose {you}, the computer chose {cpu}.") + + if you == cpu: + print("It's a draw!") + elif beats[you] == cpu: + print("You win! 🎉") + else: + print("You lose! 😅") diff --git a/web/public/playground/tictactoe.py b/web/public/playground/tictactoe.py new file mode 100644 index 000000000..52fa03d00 --- /dev/null +++ b/web/public/playground/tictactoe.py @@ -0,0 +1,67 @@ +# Tic-Tac-Toe +# You are X, the computer is O. Enter a cell number 1-9 to play. + +import random + +board = [" "] * 9 +wins = [ + (0, 1, 2), (3, 4, 5), (6, 7, 8), + (0, 3, 6), (1, 4, 7), (2, 5, 8), + (0, 4, 8), (2, 4, 6), +] + + +def show(): + print() + for row in range(3): + cells = [] + for col in range(3): + i = row * 3 + col + cells.append(board[i] if board[i] != " " else str(i + 1)) + print(" " + " | ".join(cells)) + if row < 2: + print(" ---------") + print() + + +def winner(): + for a, b, c in wins: + if board[a] != " " and board[a] == board[b] == board[c]: + return board[a] + return None + + +print("Tic-Tac-Toe — you are X.") +show() + +while True: + move = input("Your move (1-9): ").strip() + if not move.isdigit() or not (1 <= int(move) <= 9): + print("Please enter a number from 1 to 9.") + continue + spot = int(move) - 1 + if board[spot] != " ": + print("That cell is taken — try another.") + continue + + board[spot] = "X" + show() + + if winner() == "X": + print("You win! 🎉") + break + if " " not in board: + print("It's a draw! 🤝") + break + + cpu = random.choice([i for i, v in enumerate(board) if v == " "]) + board[cpu] = "O" + print(f"Computer played {cpu + 1}.") + show() + + if winner() == "O": + print("Computer wins! 🤖") + break + if " " not in board: + print("It's a draw! 🤝") + break diff --git a/web/public/pyodide-worker.js b/web/public/pyodide-worker.js new file mode 100644 index 000000000..5e7893123 --- /dev/null +++ b/web/public/pyodide-worker.js @@ -0,0 +1,139 @@ +/* pyBegin playground — Pyodide Web Worker. + * + * Runs real CPython (via WebAssembly) in the browser. stdout/stderr stream to + * the main thread; input() blocks the worker thread on a SharedArrayBuffer + * until the main thread supplies a line typed in the terminal. Also answers + * `lint` requests (compile() syntax check) for the editor's error squiggles. + * Cross-origin isolation (COOP/COEP) is required — see public/_headers. + */ + +/* global loadPyodide */ + +const PYODIDE_VERSION = "0.29.4"; +importScripts( + `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/pyodide.js`, +); + +let pyodide = null; +let ctrl = null; // Int32Array view over the shared buffer +let data = null; // Uint8Array view for the input line bytes +let busy = false; // true while a program is running (no lint then) +const decoder = new TextDecoder(); + +// Called synchronously by Python on input(). Blocks the worker thread until +// the main thread writes a line into the shared buffer. The control flag is +// cleared *before* requesting input so a fast (type-ahead) reply can't be lost. +function blockingStdin() { + if (!ctrl) return null; + Atomics.store(ctrl, 0, 0); + self.postMessage({ type: "stdin" }); + Atomics.wait(ctrl, 0, 0); + const len = Atomics.load(ctrl, 1); + if (len < 0) return null; // EOF + return decoder.decode(data.slice(0, len)); +} + +async function boot() { + self.postMessage({ type: "loading", text: "Downloading Python runtime…" }); + pyodide = await loadPyodide(); + pyodide.setStdout({ + write: (buf) => { + self.postMessage({ type: "stdout", text: decoder.decode(buf) }); + return buf.length; + }, + }); + pyodide.setStderr({ + write: (buf) => { + self.postMessage({ type: "stderr", text: decoder.decode(buf) }); + return buf.length; + }, + }); + // autoEOF:true — each input() invokes blockingStdin exactly once and treats + // its returned newline-terminated string as one complete line. + // (autoEOF:false makes Pyodide call stdin repeatedly until null → hangs.) + pyodide.setStdin({ stdin: blockingStdin, autoEOF: true }); + + // Syntax checker for the editor's lint squiggles. + pyodide.runPython(` +import json as _json +def _pybegin_lint(src): + try: + compile(src, "", "exec") + return "[]" + except SyntaxError as e: + return _json.dumps([{ + "line": e.lineno or 1, + "col": e.offset or 1, + "endLine": getattr(e, "end_lineno", None) or (e.lineno or 1), + "endCol": getattr(e, "end_offset", None) or ((e.offset or 1) + 1), + "msg": e.msg or "Syntax error", + }]) + except Exception as e: + return _json.dumps([{"line": 1, "col": 1, "endLine": 1, "endCol": 2, + "msg": type(e).__name__ + ": " + str(e)}]) +`); + self.postMessage({ type: "ready" }); +} + +const bootPromise = boot(); + +self.onmessage = async (e) => { + const msg = e.data; + + // --- lint: compile-check the code, return diagnostics --- + if (msg.type === "lint") { + try { + await bootPromise; + } catch { + return; + } + if (busy) { + self.postMessage({ type: "lint-result", id: msg.id, errors: [] }); + return; + } + let errors = []; + try { + const fn = pyodide.globals.get("_pybegin_lint"); + errors = JSON.parse(fn(msg.code)); + fn.destroy(); + } catch { + errors = []; + } + self.postMessage({ type: "lint-result", id: msg.id, errors }); + return; + } + + if (msg.type !== "run") return; + + try { + await bootPromise; + } catch (err) { + self.postMessage({ type: "stderr", text: "Failed to load Python: " + err + "\n" }); + self.postMessage({ type: "done" }); + return; + } + + ctrl = new Int32Array(msg.sab, 0, 2); + data = new Uint8Array(msg.sab, 8); + busy = true; + + try { + if (msg.packages && msg.packages.length) { + self.postMessage({ type: "loading", text: "Installing " + msg.packages.join(", ") + "…" }); + await pyodide.loadPackage("micropip"); + const micropip = pyodide.pyimport("micropip"); + for (const p of msg.packages) await micropip.install(p); + } + self.postMessage({ type: "running" }); + // Fresh namespace per run so leftover variables don't leak between runs. + const globals = pyodide.toPy({ __name__: "__main__" }); + await pyodide.runPythonAsync(msg.code, { globals }); + self.postMessage({ type: "done" }); + } catch (err) { + const text = (err && err.message ? err.message : String(err)) + "\n"; + self.postMessage({ type: "stderr", text }); + self.postMessage({ type: "done" }); + } finally { + busy = false; + } +}; diff --git a/web/src/app/globals.css b/web/src/app/globals.css new file mode 100644 index 000000000..ef7eb64c3 --- /dev/null +++ b/web/src/app/globals.css @@ -0,0 +1,952 @@ +/* === pyBegin · Sticker Pack styles === + Merged from sticker-app.css, sticker-app-2.css, sticker-app-3.css. + Former design-tweak variables are now fixed defaults in :root. */ + +:root { + --s-bg: #fef9ef; + --s-ink: #1d1830; + --s-accent: #ff7a59; + --s-accent-2: #3aa6ff; + --s-accent-3: #1cc97c; + --s-accent-4: #ffd166; + --s-accent-5: #a55cff; + --s-surface: #ffffff; + --s-surface-warm: #ffe5d4; + --s-surface-cool: #d4f0ff; + --s-surface-purple: #e5e0ff; + --s-surface-green: #d4ffd9; + --s-surface-yellow: #fff5b0; + --s-radius-lg: 24px; + --s-radius-md: 16px; + --s-radius-pill: 99px; + --s-shadow: 6px 6px 0 var(--s-ink); + --s-shadow-lg: 8px 8px 0 var(--s-ink); + --s-shadow-sm: 3px 3px 0 var(--s-ink); + --s-border: 2.5px solid var(--s-ink); + --s-border-thin: 2px solid var(--s-ink); + --s-display: var(--font-display), "Fraunces", Georgia, serif; + --s-body: var(--font-body), "DM Sans", system-ui, sans-serif; + --s-mono: var(--font-mono), "JetBrains Mono", ui-monospace, monospace; + --s-density: 1; +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + background: var(--s-bg); + background-image: + radial-gradient(circle at 15% 12%, #ffe5d4 0, transparent 28%), + radial-gradient(circle at 85% 6%, #d4f0ff 0, transparent 30%), + radial-gradient(circle at 75% 65%, #e5e0ff 0, transparent 26%), + radial-gradient(circle at 10% 80%, #fff0d4 0, transparent 22%); + color: var(--s-ink); + font-family: var(--s-body); + font-size: 16px; + line-height: 1.5; + min-height: 100vh; + overflow-x: hidden; +} +body::before { + content: ''; position: fixed; inset: 0; + background-image: radial-gradient(rgba(29, 24, 48, 0.07) 1.1px, transparent 1.1px); + background-size: 22px 22px; + pointer-events: none; z-index: 0; +} + +a { color: inherit; } +.s-display { font-family: var(--s-display); font-weight: 800; letter-spacing: -0.025em; } +.s-mono { font-family: var(--s-mono); } +.s-shell { max-width: 1280px; margin: 0 auto; padding: 0 32px; position: relative; z-index: 1; } + +/* Nav */ +.s-nav { display: flex; align-items: center; justify-content: space-between; padding: 24px 0 8px; } +.s-logo { display: flex; align-items: center; gap: 14px; text-decoration: none; } +.s-logomark { + width: 48px; height: 48px; border-radius: 14px; + background: var(--s-accent); + border: var(--s-border); box-shadow: 4px 4px 0 var(--s-ink); + display: grid; place-items: center; + transform: rotate(-6deg); transition: transform 0.3s cubic-bezier(.4,1.8,.4,1); +} +.s-logo:hover .s-logomark { transform: rotate(6deg) scale(1.05); } +.s-logomark svg { width: 28px; height: 28px; } +.s-brand-name { font-family: var(--s-display); font-size: 24px; font-weight: 900; letter-spacing: -0.025em; line-height: 1; } +.s-brand-sub { font-size: 12px; color: rgba(29, 24, 48, 0.55); font-family: var(--s-mono); margin-top: 2px; } +.s-navlinks { display: flex; gap: 4px; } +.s-navlinks a, .s-navlinks button { + font: inherit; padding: 9px 16px; border-radius: var(--s-radius-pill); + font-size: 13px; font-weight: 600; cursor: pointer; + background: transparent; border: 0; color: var(--s-ink); text-decoration: none; + display: inline-block; +} +.s-navlinks a.active, .s-navlinks button.active { background: var(--s-ink); color: var(--s-bg); } +.s-navlinks a:hover:not(.active), .s-navlinks button:hover:not(.active) { background: rgba(29,24,48, 0.08); } +.s-nav-right { display: flex; gap: 10px; align-items: center; } +.s-bm-count { + width: 38px; height: 38px; border-radius: 50%; + background: var(--s-surface); border: var(--s-border-thin); + display: grid; place-items: center; font-weight: 700; font-size: 14px; + box-shadow: 3px 3px 0 var(--s-ink); position: relative; cursor: pointer; +} +.s-bm-count.has::after { + content: ''; position: absolute; top: 4px; right: 4px; + width: 8px; height: 8px; border-radius: 50%; background: var(--s-accent); + border: 1.5px solid var(--s-ink); +} +.s-cta { + padding: 12px 22px; border-radius: var(--s-radius-pill); + background: var(--s-ink); color: var(--s-bg); + border: var(--s-border); box-shadow: 4px 4px 0 var(--s-accent); + font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; + display: flex; align-items: center; gap: 8px; text-decoration: none; + transition: transform .12s, box-shadow .12s; +} +.s-cta:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--s-accent); } +.s-cta:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--s-accent); } + +/* Hero */ +.s-hero { + display: grid; grid-template-columns: 1.05fr 1fr; gap: 60px; + align-items: center; padding: 60px 0 40px; +} +.s-tag { + display: inline-flex; align-items: center; gap: 9px; + padding: 8px 16px; border-radius: var(--s-radius-pill); + background: var(--s-surface); border: var(--s-border); + box-shadow: 3px 3px 0 var(--s-ink); + font-size: 12px; font-weight: 700; margin-bottom: 28px; + transform: rotate(-1deg); +} +.s-tag .dot { + width: 8px; height: 8px; border-radius: 50%; background: var(--s-accent-3); + box-shadow: 0 0 0 3px rgba(28, 201, 124, 0.18); + animation: s-pulse 2s ease-in-out infinite; +} +@keyframes s-pulse { 0%, 100% { box-shadow: 0 0 0 3px rgba(28, 201, 124, 0.18); } 50% { box-shadow: 0 0 0 6px rgba(28, 201, 124, 0); } } + +.s-headline { + font-family: var(--s-display); font-weight: 900; + font-size: clamp(56px, 9.5vw, 116px); + line-height: 0.9; letter-spacing: -0.035em; + margin: 0 0 24px; +} +.s-headline .scribble { position: relative; display: inline-block; color: var(--s-accent); } +.s-sub { + font-size: 18px; line-height: 1.55; color: rgba(29, 24, 48, 0.72); + max-width: 480px; margin: 0 0 32px; +} +.s-hero-cta { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; } +.s-btn-pri, .s-btn-sec { + padding: 16px 26px; border-radius: var(--s-radius-pill); + font: inherit; font-size: 15px; font-weight: 800; cursor: pointer; + display: inline-flex; align-items: center; gap: 10px; text-decoration: none; + transition: transform .12s, box-shadow .12s; +} +.s-btn-pri { + background: var(--s-accent); color: var(--s-ink); + border: var(--s-border); box-shadow: var(--s-shadow); +} +.s-btn-sec { + background: transparent; color: var(--s-ink); border: var(--s-border); +} +.s-btn-pri:hover, .s-btn-sec:hover { transform: translate(-2px, -2px); } +.s-btn-pri:hover { box-shadow: 8px 8px 0 var(--s-ink); } +.s-btn-sec:hover { background: var(--s-surface); box-shadow: 3px 3px 0 var(--s-ink); } +.s-btn-pri:active, .s-btn-sec:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--s-ink) !important; } + +/* Sticker cluster — hero right */ +.s-stickers { position: relative; height: 520px; } +.s-sticker { + position: absolute; padding: 18px 22px; + border: var(--s-border); border-radius: var(--s-radius-lg); + box-shadow: var(--s-shadow); + display: flex; align-items: center; gap: 14px; + transition: transform .25s cubic-bezier(.4,1.6,.4,1); + cursor: pointer; user-select: none; +} +.s-sticker:hover { transform: rotate(0deg) translate(-3px, -3px) scale(1.04) !important; box-shadow: 9px 9px 0 var(--s-ink); z-index: 10; } +.s-sticker .emoji { font-size: 44px; line-height: 1; flex-shrink: 0; } +.s-sticker .lbl { font-weight: 800; font-size: 15px; } +.s-sticker .num { font-family: var(--s-mono); font-size: 11px; color: rgba(29, 24, 48, 0.6); margin-top: 2px; } +@keyframes s-bob1 { 0%, 100% { transform: rotate(-4deg) translateY(0); } 50% { transform: rotate(-4deg) translateY(-6px); } } +@keyframes s-bob2 { 0%, 100% { transform: rotate(3deg) translateY(0); } 50% { transform: rotate(3deg) translateY(-8px); } } +@keyframes s-bob3 { 0%, 100% { transform: rotate(2deg) translateY(0); } 50% { transform: rotate(2deg) translateY(-5px); } } +@keyframes s-bob4 { 0%, 100% { transform: rotate(-2deg) translateY(0); } 50% { transform: rotate(-2deg) translateY(-7px); } } +@keyframes s-bob5 { 0%, 100% { transform: rotate(4deg) translateY(0); } 50% { transform: rotate(4deg) translateY(-6px); } } + +/* Stats marquee */ +.s-stats { + margin-top: 30px; + background: var(--s-ink); border-radius: var(--s-radius-lg); padding: 28px; + display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; + color: var(--s-bg); border: var(--s-border); + box-shadow: var(--s-shadow-lg); +} +.s-stat { text-align: center; padding: 8px 0; border-right: 1.5px dashed rgba(254, 249, 239, 0.22); } +.s-stat:last-child { border-right: 0; } +.s-stat-n { font-family: var(--s-display); font-weight: 900; font-size: clamp(36px, 4vw, 52px); line-height: 1; color: var(--s-accent); } +.s-stat-l { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; margin-top: 6px; color: rgba(254, 249, 239, 0.75); } + +/* Sections */ +.s-section { margin-top: 80px; } +.s-section-h { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 30px; flex-wrap: wrap; } +.s-section-title { font-family: var(--s-display); font-weight: 900; font-size: clamp(40px, 5vw, 64px); letter-spacing: -0.03em; line-height: 0.95; margin: 0; } +.s-section-title em { font-style: italic; color: var(--s-accent); } +.s-section-side { font-size: 14px; color: rgba(29, 24, 48, 0.6); max-width: 360px; text-align: right; } + +/* Toolbar */ +.s-toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 22px; flex-wrap: wrap; } +.s-search { + flex: 1; min-width: 280px; display: flex; align-items: center; gap: 12px; + padding: 14px 22px; border-radius: var(--s-radius-pill); + background: var(--s-surface); border: var(--s-border); box-shadow: 4px 4px 0 var(--s-ink); + transition: box-shadow .12s, transform .12s; +} +.s-search:focus-within { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--s-ink); } +.s-search input { flex: 1; background: transparent; border: 0; outline: 0; color: var(--s-ink); font: inherit; font-size: 16px; font-weight: 500; } +.s-search input::placeholder { color: rgba(29, 24, 48, 0.4); } +.s-search-icon { font-size: 18px; } +.s-search-key { + font-family: var(--s-mono); font-size: 11px; padding: 3px 7px; + background: var(--s-bg); border: 1.5px solid var(--s-ink); border-radius: 5px; +} + +.s-sort { + padding: 12px 18px; border-radius: var(--s-radius-pill); + background: var(--s-surface); border: var(--s-border); box-shadow: 3px 3px 0 var(--s-ink); + font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; + display: flex; align-items: center; gap: 8px; +} + +.s-chips { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 24px; } +.s-chip { + padding: 9px 18px; border-radius: var(--s-radius-pill); + background: var(--s-surface); border: var(--s-border-thin); + font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; + display: flex; align-items: center; gap: 8px; + transition: transform .12s, box-shadow .12s; +} +.s-chip:hover { transform: translate(-1px, -1px); box-shadow: 2px 2px 0 var(--s-ink); } +.s-chip.active { background: var(--s-accent); box-shadow: 3px 3px 0 var(--s-ink); } +.s-chip.active.bm { background: var(--s-accent-4); } +.s-chip .ct { font-family: var(--s-mono); font-size: 11px; opacity: 0.7; font-weight: 700; } +.s-chip .ic { font-size: 14px; } + +/* Gallery */ +.s-gallery.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; } +.s-gallery.list { display: flex; flex-direction: column; gap: 12px; } +.s-gallery.masonry { columns: 3; column-gap: 22px; } +.s-gallery.masonry .s-card { break-inside: avoid; margin-bottom: 22px; } + +.s-card { + position: relative; padding: 24px; + background: var(--s-surface); border: var(--s-border); border-radius: var(--s-radius-lg); + box-shadow: var(--s-shadow); + cursor: pointer; transition: transform .15s, box-shadow .15s; + text-align: left; width: 100%; font: inherit; color: inherit; display: block; +} +.s-card:hover { transform: translate(-3px, -3px); box-shadow: 9px 9px 0 var(--s-ink); z-index: 2; } +.s-card:active { transform: translate(2px, 2px); box-shadow: 2px 2px 0 var(--s-ink); } + +.s-gallery .s-card:nth-child(5n+1) { background: var(--s-surface-warm); } +.s-gallery .s-card:nth-child(5n+2) { background: var(--s-surface-cool); } +.s-gallery .s-card:nth-child(5n+3) { background: var(--s-surface-purple); } +.s-gallery .s-card:nth-child(5n+4) { background: var(--s-surface-yellow); } +.s-gallery .s-card:nth-child(5n+5) { background: var(--s-surface-green); } + +.s-card-emoji { font-size: 44px; margin-bottom: 14px; line-height: 1; display: inline-block; transition: transform .25s cubic-bezier(.4,1.6,.4,1); } +.s-card:hover .s-card-emoji { transform: rotate(-8deg) scale(1.1); } +.s-card-h { display: flex; align-items: start; justify-content: space-between; gap: 10px; margin-bottom: 8px; } +.s-card-name { font-family: var(--s-display); font-size: 22px; font-weight: 800; line-height: 1.1; letter-spacing: -0.015em; margin: 0; } +.s-card-blurb { font-size: 14px; color: rgba(29, 24, 48, 0.72); line-height: 1.5; margin: 0 0 16px; } +.s-card-meta { display: flex; gap: 8px; flex-wrap: wrap; } +.s-meta-pill { + padding: 4px 11px; border-radius: var(--s-radius-pill); + background: var(--s-ink); color: var(--s-bg); + font-family: var(--s-mono); font-size: 10px; font-weight: 700; + letter-spacing: 0.02em; +} +.s-meta-pill.dep { background: rgba(29,24,48,0.08); color: var(--s-ink); border: 1.5px solid var(--s-ink); } +.s-card-bm { + width: 34px; height: 34px; border-radius: 50%; + background: var(--s-surface); border: var(--s-border-thin); + display: grid; place-items: center; cursor: pointer; flex-shrink: 0; + font-size: 15px; line-height: 1; color: var(--s-ink); + transition: transform .12s, background .12s; +} +.s-card-bm:hover { transform: scale(1.08) rotate(-6deg); } +.s-card-bm.on { background: var(--s-accent); transform: rotate(-6deg); } + +/* Card density: minimal */ +.s-card.minimal { padding: 16px 20px; } +.s-card.minimal .s-card-emoji { display: none; } +.s-card.minimal .s-card-blurb { display: none; } +.s-card.minimal .s-card-h { margin-bottom: 10px; } + +/* Card with image banner */ +.s-card.image { padding-top: 12px; } +.s-card.image .s-img { + height: 140px; border-radius: var(--s-radius-md); + margin: 0 0 18px; + border: var(--s-border-thin); + display: grid; place-items: center; font-size: 60px; + background-image: repeating-linear-gradient(135deg, rgba(29,24,48,0.05) 0 6px, transparent 6px 12px); + overflow: hidden; position: relative; +} +.s-gallery .s-card:nth-child(5n+1) .s-img { background-color: rgba(255, 122, 89, 0.25); } +.s-gallery .s-card:nth-child(5n+2) .s-img { background-color: rgba(58, 166, 255, 0.22); } +.s-gallery .s-card:nth-child(5n+3) .s-img { background-color: rgba(165, 92, 255, 0.22); } +.s-gallery .s-card:nth-child(5n+4) .s-img { background-color: rgba(255, 209, 102, 0.3); } +.s-gallery .s-card:nth-child(5n+5) .s-img { background-color: rgba(28, 201, 124, 0.22); } + +/* List density */ +.s-gallery.list .s-card { display: grid; grid-template-columns: 56px 1fr auto auto auto; align-items: center; gap: 20px; padding: 14px 22px; } +.s-gallery.list .s-card-emoji { font-size: 30px; margin: 0; } +.s-gallery.list .s-card-h { margin: 0; display: contents; } +.s-gallery.list .s-card-name { font-size: 18px; } +.s-gallery.list .s-card-blurb { display: none; } +.s-gallery.list .s-card-meta { gap: 6px; } +.s-gallery.list .s-card-bm { margin-left: 4px; } + +/* Empty state */ +.s-empty { + padding: 60px 32px; text-align: center; + border: 2.5px dashed rgba(29,24,48,0.25); border-radius: var(--s-radius-lg); +} +.s-empty .em { font-size: 56px; } +.s-empty h3 { font-family: var(--s-display); font-size: 24px; margin: 16px 0 6px; font-weight: 800; } +.s-empty p { color: rgba(29,24,48,0.65); margin: 0; } + +/* Learning paths */ +.s-paths { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; } +.s-path { + padding: 30px; border: var(--s-border); border-radius: 28px; + box-shadow: var(--s-shadow); background: var(--s-surface); + position: relative; overflow: hidden; + transition: transform .15s, box-shadow .15s; +} +.s-path:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--s-ink); } +.s-path:nth-child(1) { background: var(--s-surface-warm); } +.s-path:nth-child(2) { background: var(--s-surface-cool); } +.s-path:nth-child(3) { background: var(--s-surface-purple); } +.s-path-head { display: flex; align-items: start; justify-content: space-between; gap: 14px; margin-bottom: 14px; } +.s-path-tag { + display: inline-block; padding: 5px 13px; border-radius: var(--s-radius-pill); + background: var(--s-ink); color: var(--s-bg); font-family: var(--s-mono); + font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; +} +.s-path-num { font-family: var(--s-display); font-size: 64px; font-weight: 900; line-height: 0.8; color: rgba(29,24,48,0.18); } +.s-path-name { font-family: var(--s-display); font-size: 34px; font-weight: 900; letter-spacing: -0.025em; line-height: 1; margin: 0 0 12px; } +.s-path-desc { font-size: 14px; color: rgba(29, 24, 48, 0.75); line-height: 1.55; margin: 0 0 22px; } +.s-path-list { display: flex; flex-direction: column; gap: 8px; } +.s-path-item { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: rgba(255,255,255,0.7); border-radius: 14px; border: 1.5px solid var(--s-ink); font-size: 14px; font-weight: 600; cursor: pointer; transition: background .12s; width: 100%; font-family: inherit; color: inherit; text-align: left; } +.s-path-item:hover { background: #fff; } +.s-path-item i { font-family: var(--s-mono); font-style: normal; font-size: 11px; color: rgba(29, 24, 48, 0.5); width: 16px; } +.s-path-item .em { font-size: 18px; margin-left: auto; } + +/* Contributors */ +.s-contributors { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; } +.s-contrib { + padding: 16px 12px; text-align: center; border-radius: 18px; + background: var(--s-surface); border: var(--s-border-thin); box-shadow: 4px 4px 0 var(--s-ink); + transition: transform .25s cubic-bezier(.4,1.6,.4,1); +} +.s-contrib:nth-child(7n+2) { transform: rotate(-1.5deg); } +.s-contrib:nth-child(7n+3) { transform: rotate(1.5deg); } +.s-contrib:nth-child(7n+4) { transform: rotate(-1deg); } +.s-contrib:nth-child(7n+5) { transform: rotate(1deg); } +.s-contrib:nth-child(7n+6) { transform: rotate(-2deg); } +.s-contrib:nth-child(7n) { transform: rotate(0.5deg); } +.s-contrib:hover { transform: rotate(0) translate(-2px, -2px) scale(1.04); box-shadow: 6px 6px 0 var(--s-ink); z-index: 5; } +.s-contrib.lead { + grid-column: span 3; grid-row: span 2; + padding: 36px; text-align: left; + background: var(--s-accent); + box-shadow: var(--s-shadow-lg); + border: var(--s-border); + transform: rotate(-1.5deg); +} +.s-contrib.lead:hover { transform: rotate(0) translate(-3px, -3px); box-shadow: 11px 11px 0 var(--s-ink); } +.s-avatar { + width: 56px; height: 56px; border-radius: 50%; + background: linear-gradient(135deg, #ffd4b0, var(--s-accent)); + border: var(--s-border-thin); + margin: 0 auto 10px; display: grid; place-items: center; + font-weight: 900; font-size: 20px; color: var(--s-ink); +} +.s-contrib:nth-child(7n+2) .s-avatar { background: linear-gradient(135deg, #d4f0ff, var(--s-accent-2)); } +.s-contrib:nth-child(7n+3) .s-avatar { background: linear-gradient(135deg, #e5d4ff, var(--s-accent-5)); } +.s-contrib:nth-child(7n+4) .s-avatar { background: linear-gradient(135deg, #d4ffd9, var(--s-accent-3)); } +.s-contrib:nth-child(7n+5) .s-avatar { background: linear-gradient(135deg, #fff5b0, var(--s-accent-4)); } +.s-contrib.lead .s-avatar { width: 84px; height: 84px; margin: 0 0 20px; font-size: 32px; background: var(--s-bg); } +.s-cname { font-weight: 800; font-size: 13px; line-height: 1.2; } +.s-chandle { font-family: var(--s-mono); font-size: 10px; color: rgba(29, 24, 48, 0.55); margin-top: 3px; } +.s-commits { font-family: var(--s-mono); font-size: 10px; color: var(--s-ink); margin-top: 6px; font-weight: 700; padding: 3px 8px; background: rgba(29,24,48,0.06); border-radius: var(--s-radius-pill); display: inline-block; } +.s-contrib.lead .s-cname { font-size: 32px; font-family: var(--s-display); font-weight: 900; letter-spacing: -0.025em; line-height: 1; margin-bottom: 4px; } +.s-contrib.lead .s-chandle { font-size: 14px; } +.s-contrib.lead .s-quote { margin-top: 20px; font-size: 16px; line-height: 1.5; font-weight: 500; } +.s-contrib.lead .s-badge { + display: inline-block; margin-top: 18px; padding: 7px 16px; + background: var(--s-ink); color: var(--s-bg); font-size: 11px; font-weight: 800; + letter-spacing: 0.08em; text-transform: uppercase; border-radius: var(--s-radius-pill); +} + +.s-more { display: flex; justify-content: center; margin-top: 28px; } +.s-more button, .s-more a { + padding: 14px 28px; border-radius: var(--s-radius-pill); + background: var(--s-surface); border: var(--s-border); box-shadow: var(--s-shadow); + font: inherit; font-size: 14px; font-weight: 700; cursor: pointer; text-decoration: none; + transition: transform .12s, box-shadow .12s; color: var(--s-ink); +} +.s-more button:hover, .s-more a:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--s-ink); } + +/* CTA banner */ +.s-cta-banner { + margin-top: 80px; padding: 50px 56px; + background: var(--s-ink); color: var(--s-bg); + border: var(--s-border); border-radius: 28px; box-shadow: var(--s-shadow-lg); + display: grid; grid-template-columns: 1.4fr 1fr; gap: 40px; align-items: center; + position: relative; overflow: hidden; +} +.s-cta-banner::before { + content: ''; position: absolute; bottom: -40px; right: -40px; + width: 200px; height: 200px; border-radius: 50%; background: var(--s-accent); + opacity: 0.18; +} +.s-cta-banner h2 { font-family: var(--s-display); font-size: 44px; font-weight: 900; line-height: 1; letter-spacing: -0.025em; margin: 0 0 12px; } +.s-cta-banner h2 em { font-style: italic; color: var(--s-accent); } +.s-cta-banner p { font-size: 16px; color: rgba(254, 249, 239, 0.78); line-height: 1.55; margin: 0 0 22px; max-width: 480px; } +.s-cta-banner-codes { display: flex; flex-direction: column; gap: 10px; font-family: var(--s-mono); font-size: 13px; } +.s-cta-banner-code { + padding: 12px 16px; background: rgba(254,249,239,0.08); border-radius: 10px; + border: 1.5px dashed rgba(254,249,239,0.25); + color: var(--s-bg); display: flex; align-items: center; gap: 12px; +} +.s-cta-banner-code span:first-child { color: var(--s-accent); } +.s-cta-banner-code .copy { margin-left: auto; cursor: pointer; opacity: 0.6; font-size: 12px; } + +/* Footer */ +.s-footer { + margin: 60px 0 0; padding: 32px 0; + border-top: 2px dashed rgba(29, 24, 48, 0.3); + display: flex; justify-content: space-between; align-items: center; + font-size: 13px; color: rgba(29, 24, 48, 0.6); + flex-wrap: wrap; gap: 16px; +} +.s-heart { color: var(--s-accent); } +.s-footer-links { display: flex; gap: 18px; } +.s-footer a { color: inherit; text-decoration: none; } +.s-footer a:hover { color: var(--s-ink); text-decoration: underline; } + +/* Project modal */ +.s-modal-bg { + position: fixed; inset: 0; background: rgba(29, 24, 48, 0.5); + z-index: 1000; display: grid; place-items: center; padding: 24px; + animation: s-fade .15s; overflow-y: auto; +} +@keyframes s-fade { from { opacity: 0; } } +.s-modal { + background: var(--s-surface); border: var(--s-border); border-radius: 28px; + box-shadow: 12px 12px 0 var(--s-accent); + max-width: 560px; width: 100%; padding: 36px; + animation: s-pop .25s cubic-bezier(.4,1.6,.4,1); + position: relative; margin: auto; +} +@keyframes s-pop { from { transform: translateY(20px) scale(.96); opacity: 0; } } +.s-modal-emoji { font-size: 72px; margin-bottom: 18px; } +.s-modal h2 { font-family: var(--s-display); font-size: 40px; font-weight: 900; letter-spacing: -0.025em; line-height: 1; margin: 0 0 8px; } +.s-modal-blurb { font-size: 16px; color: rgba(29,24,48,0.75); line-height: 1.55; margin: 0 0 22px; } +.s-modal-meta { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; } +.s-modal-code { + font-family: var(--s-mono); font-size: 13px; + background: var(--s-ink); color: var(--s-bg); padding: 16px; + border-radius: 12px; margin-bottom: 20px; +} +.s-modal-actions { display: flex; gap: 12px; flex-wrap: wrap; } +.s-modal-x { + position: absolute; top: 16px; right: 16px; + width: 36px; height: 36px; border-radius: 50%; + background: var(--s-bg); border: var(--s-border-thin); cursor: pointer; + font-size: 18px; font-weight: 700; display: grid; place-items: center; + box-shadow: 2px 2px 0 var(--s-ink); +} + +/* Responsive */ +@media (max-width: 960px) { + .s-hero { grid-template-columns: 1fr; } + .s-stickers { height: 420px; } + .s-gallery.grid, .s-gallery.masonry { grid-template-columns: repeat(2, 1fr) !important; columns: 2; } + .s-paths { grid-template-columns: 1fr; } + .s-contributors { grid-template-columns: repeat(3, 1fr); } + .s-contrib.lead { grid-column: span 3; grid-row: span 1; } + .s-cta-banner { grid-template-columns: 1fr; padding: 36px; } +} +@media (max-width: 640px) { + .s-shell { padding: 0 18px; } + .s-navlinks { display: none; } + .s-stats { grid-template-columns: repeat(2, 1fr); gap: 14px; } + .s-stat { border-right: 0 !important; padding: 12px 0; } + .s-gallery.grid, .s-gallery.masonry { grid-template-columns: 1fr !important; columns: 1; } + .s-contributors { grid-template-columns: repeat(2, 1fr); } + .s-contrib.lead { grid-column: span 2; } +} + +/* ====== Try-it panel (modal demo) ====== */ +.s-meta-pill.run { + background: var(--s-accent); + color: var(--s-ink); + font-weight: 700; + border-color: var(--s-ink); +} +.s-meta-pill.play { + background: var(--s-accent-3); + color: var(--s-ink); + font-weight: 700; +} +.s-try { + margin-top: 4px; + padding: 20px; + border: var(--s-border-thin); + border-radius: 16px; + background: var(--s-bg); + box-shadow: var(--s-shadow-sm); + margin-bottom: 22px; + font-family: var(--s-body); +} +.s-try-h { + font-family: var(--s-display); + font-size: 18px; + font-weight: 800; + margin-bottom: 14px; + color: var(--s-ink); +} +.s-try-form { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + gap: 10px 14px; + margin-bottom: 14px; +} +.s-try-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--s-ink); } +.s-try-field > span { font-weight: 700; } +.s-try-field input, .s-try-field select { + padding: 9px 12px; + border-radius: 10px; + border: var(--s-border-thin); + background: #fff; + color: var(--s-ink); + font: inherit; + font-size: 14px; +} +.s-try-field input:focus, .s-try-field select:focus { + outline: none; + box-shadow: 0 0 0 3px var(--s-accent); +} +.s-try-run { margin-bottom: 12px; } +.s-try-run:disabled { opacity: 0.55; cursor: wait; } +.s-try-error { + margin: 0 0 12px; + padding: 10px 14px; + border-radius: 10px; + background: #ffe1e1; + border: var(--s-border-thin); + border-color: #b34d4d; + color: #6e1414; + font-size: 13px; +} +.s-try-out { + margin-top: 6px; + padding: 14px 16px; + border-radius: 12px; + background: var(--s-surface-warm); + border: var(--s-border-thin); + color: var(--s-ink); +} +.s-try-out pre, .s-try-story pre { + margin: 0; + white-space: pre-wrap; + font: 14px/1.55 var(--s-body); + color: var(--s-ink); +} + +/* Tic-tac-toe board */ +.s-ttt-grid { + display: grid; + grid-template-columns: repeat(3, 78px); + grid-template-rows: repeat(3, 78px); + gap: 8px; + justify-content: center; + margin: 6px auto 4px; +} +.s-ttt-cell { + background: #fff; + border: var(--s-border-thin); + border-radius: 14px; + font: 800 38px/1 var(--s-display); + color: var(--s-ink); + cursor: pointer; + transition: transform 80ms ease; + box-shadow: var(--s-shadow-sm); +} +.s-ttt-cell:not(:disabled):hover { transform: translate(-2px, -2px); box-shadow: 5px 5px 0 var(--s-ink); } +.s-ttt-cell:disabled { cursor: default; } +.s-ttt-cell [data-mark="X"] { color: var(--s-accent); } +.s-ttt-cell [data-mark="O"] { color: #2f7a4f; } + +/* Hangman */ +.s-hg-row { + display: flex; + gap: 14px; + align-items: flex-end; + margin-bottom: 14px; +} +.s-hg-word { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; + margin: 6px 0 10px; +} +.s-hg-slot { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 28px; + padding: 6px 4px; + border-bottom: 3px solid var(--s-ink); + font: 800 26px/1 var(--s-display); + color: var(--s-ink); +} +.s-hg-meta { + display: flex; + justify-content: space-between; + font-size: 13px; + margin-bottom: 10px; + color: var(--s-ink); +} +.s-hg-wrong { color: #b34d4d; letter-spacing: 2px; font-weight: 700; } +.s-hg-keys { + display: grid; + grid-template-columns: repeat(13, 1fr); + gap: 5px; +} +.s-hg-key { + padding: 8px 0; + border-radius: 8px; + border: var(--s-border-thin); + background: #fff; + color: var(--s-ink); + font: 700 13px/1 var(--s-body); + cursor: pointer; + transition: transform 80ms ease; +} +.s-hg-key:not(:disabled):hover { transform: translateY(-1px); } +.s-hg-key.used { cursor: default; opacity: 0.55; } +.s-hg-key.hit { background: #c9f3d4; opacity: 1; } +.s-hg-key.miss { background: #ffd4d4; } + +@media (max-width: 640px) { + .s-hg-keys { grid-template-columns: repeat(9, 1fr); } + .s-ttt-grid { grid-template-columns: repeat(3, 64px); grid-template-rows: repeat(3, 64px); } + .s-ttt-cell { font-size: 30px; } +} + +/* ====== Project detail page ====== */ +.s-proj { + padding: 40px 0 20px; + display: grid; + grid-template-columns: 1.3fr 1fr; + gap: 48px; + align-items: start; +} +.s-proj-emoji { font-size: 84px; line-height: 1; margin-bottom: 12px; } +.s-proj h1 { + font-family: var(--s-display); font-weight: 900; + font-size: clamp(40px, 6vw, 72px); letter-spacing: -0.03em; + line-height: 0.95; margin: 0 0 14px; +} +.s-proj-blurb { font-size: 19px; line-height: 1.55; color: rgba(29,24,48,0.75); margin: 0 0 22px; max-width: 520px; } +.s-proj-meta { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 26px; } +.s-proj-actions { display: flex; gap: 12px; flex-wrap: wrap; } +.s-proj-side { + background: var(--s-surface); border: var(--s-border); border-radius: var(--s-radius-lg); + box-shadow: var(--s-shadow); padding: 26px; +} +.s-proj-side h3 { font-family: var(--s-display); font-size: 20px; margin: 0 0 14px; font-weight: 800; } +.s-proj-code { + font-family: var(--s-mono); font-size: 13px; + background: var(--s-ink); color: var(--s-bg); padding: 16px; + border-radius: 12px; line-height: 1.7; +} +.s-proj-code .c-accent { color: var(--s-accent); } +.s-proj-code .c-dim { color: rgba(254,249,239,0.6); } +.s-crumb { font-family: var(--s-mono); font-size: 12px; color: rgba(29,24,48,0.55); padding-top: 20px; } +.s-crumb a { color: inherit; } +@media (max-width: 760px) { + .s-proj { grid-template-columns: 1fr; gap: 28px; } +} + + +/* ====== Contributor credit ====== */ +.s-credit { + display: inline-flex; align-items: center; gap: 6px; + font-size: 13px; font-family: var(--s-body); + color: rgba(29, 24, 48, 0.72); + background: var(--s-surface); border: var(--s-border-thin); + border-radius: var(--s-radius-pill); padding: 7px 14px; + box-shadow: var(--s-shadow-sm); +} +.s-credit a { color: var(--s-ink); font-weight: 800; text-decoration: none; } +.s-credit a:hover { color: var(--s-accent); text-decoration: underline; } +.s-modal .s-credit { margin-bottom: 18px; } + +/* ============================================================ + Playground — full-bleed IDE workspace (Playground.html design) + ============================================================ */ +.pg-app { + --pg-bg: #fef9ef; + --pg-ink: #1d1830; + --pg-accent: #ff7a59; + --pg-paper: #ffffff; + --pg-paper-warm: #ffe5d4; + --pg-paper-cool: #d4f0ff; + --pg-paper-yellow: #fff5b0; + --pg-dark: #fbf3e0; + --pg-muted: rgba(29, 24, 48, 0.55); + --pg-shadow: 5px 5px 0 var(--pg-ink); + --pg-border: 2.5px solid var(--pg-ink); + --pg-border-thin: 2px solid var(--pg-ink); + --pg-radius: 18px; + --pg-radius-pill: 99px; + --pg-display: var(--font-display), "Fraunces", Georgia, serif; + --pg-body: var(--font-body), "DM Sans", system-ui, sans-serif; + --pg-mono: var(--font-mono), "JetBrains Mono", ui-monospace, monospace; + --tok-line: #bcaa92; + + position: relative; z-index: 1; + height: 100vh; display: flex; flex-direction: column; + font-family: var(--pg-body); font-size: 14px; +} +.pg-app button { font: inherit; cursor: pointer; } + +/* top nav */ +.pg-nav { + flex-shrink: 0; height: 64px; + display: flex; align-items: center; justify-content: space-between; + padding: 0 24px; +} +.pg-nav-left { display: flex; align-items: center; gap: 14px; text-decoration: none; color: var(--pg-ink); } +.pg-logomark { + width: 38px; height: 38px; border-radius: 11px; + background: var(--pg-accent); + border: var(--pg-border-thin); box-shadow: 3px 3px 0 var(--pg-ink); + display: grid; place-items: center; + transform: rotate(-5deg); transition: transform .25s cubic-bezier(.4,1.8,.4,1); +} +.pg-nav-left:hover .pg-logomark { transform: rotate(5deg) scale(1.05); } +.pg-logomark svg { width: 22px; height: 22px; } +.pg-brand-name { font-family: var(--pg-display); font-size: 19px; font-weight: 900; line-height: 1; letter-spacing: -0.025em; } +.pg-brand-sub { font-family: var(--pg-mono); font-size: 10px; color: var(--pg-muted); margin-top: 2px; } +.pg-nav-mid { display: flex; gap: 2px; } +.pg-nav-mid a { + padding: 8px 16px; border-radius: var(--pg-radius-pill); + color: var(--pg-ink); text-decoration: none; + font-size: 13px; font-weight: 600; +} +.pg-nav-mid a.active { background: var(--pg-ink); color: var(--pg-bg); } +.pg-nav-mid a:hover:not(.active) { background: rgba(29,24,48,0.08); } +.pg-star-btn { + padding: 9px 18px; border-radius: var(--pg-radius-pill); + background: var(--pg-ink); color: var(--pg-bg); + border: var(--pg-border-thin); box-shadow: 3px 3px 0 var(--pg-accent); + font-size: 12px; font-weight: 700; text-decoration: none; +} + +/* main = sidebar + workspace */ +.pg-main { flex: 1; display: flex; min-height: 0; } + +/* sidebar */ +.pg-side { + width: 280px; flex-shrink: 0; + display: flex; flex-direction: column; + background: rgba(255, 255, 255, 0.45); +} +.pg-side-head { padding: 18px 18px 12px; } +.pg-side-title { font-family: var(--pg-display); font-size: 22px; font-weight: 900; letter-spacing: -0.02em; line-height: 1; } +.pg-side-sub { font-size: 12px; color: var(--pg-muted); margin-top: 4px; } +.pg-side-search { + margin: 0 14px 12px; + padding: 10px 14px; border-radius: var(--pg-radius-pill); + background: var(--pg-paper); border: var(--pg-border-thin); box-shadow: 3px 3px 0 var(--pg-ink); + display: flex; align-items: center; gap: 8px; +} +.pg-side-search input { flex: 1; background: transparent; border: 0; outline: 0; font-size: 13px; color: var(--pg-ink); font-family: inherit; } +.pg-side-search input::placeholder { color: rgba(29,24,48,0.4); } +.pg-side-list { flex: 1; overflow-y: auto; padding: 4px 14px 24px; } +.pg-group { margin-top: 14px; } +.pg-group-h { + font-family: var(--pg-mono); font-size: 10px; font-weight: 700; + letter-spacing: 0.12em; text-transform: uppercase; + color: var(--pg-muted); padding: 6px 8px; + display: flex; align-items: center; gap: 6px; + width: 100%; background: transparent; border: 0; border-radius: 7px; + text-align: left; +} +.pg-group-h:hover { background: rgba(29,24,48,0.06); color: var(--pg-ink); } +.pg-group-h .chev { width: 12px; height: 12px; transition: transform .18s cubic-bezier(.4,1.4,.4,1); flex-shrink: 0; } +.pg-group.collapsed .pg-group-h .chev { transform: rotate(-90deg); } +.pg-group-h .label { flex: 1; } +.pg-group-h .ct { background: rgba(29,24,48,0.08); border-radius: 99px; padding: 2px 7px; font-size: 9px; } +.pg-group-items { overflow: hidden; transition: max-height .25s ease, opacity .2s ease; padding-top: 4px; } +.pg-group.collapsed .pg-group-items { opacity: 0; padding-top: 0; pointer-events: none; } +.pg-item { + display: flex; align-items: center; gap: 10px; + width: 100%; padding: 9px 12px; margin-bottom: 4px; + background: transparent; border: 2px solid transparent; border-radius: 11px; + color: var(--pg-ink); text-align: left; text-decoration: none; + font-size: 13px; font-weight: 600; + transition: background .12s, transform .12s, box-shadow .12s, border-color .12s; +} +.pg-item:hover:not(.active) { background: rgba(255,255,255,0.7); } +.pg-item.active { + background: var(--pg-accent); border-color: var(--pg-ink); + box-shadow: 3px 3px 0 var(--pg-ink); transform: translate(-1px, -1px); +} +.pg-item .em { font-size: 17px; line-height: 1; width: 22px; text-align: center; flex-shrink: 0; } +.pg-item .nm { flex: 1; } +.pg-item .ln { font-family: var(--pg-mono); font-size: 10px; color: rgba(29,24,48,0.5); } +.pg-item.active .ln { color: rgba(29,24,48,0.7); } +.pg-side-foot { + padding: 14px 18px 18px; + display: flex; gap: 8px; align-items: center; + font-family: var(--pg-mono); font-size: 11px; color: var(--pg-muted); +} +.pg-side-foot .dot { width: 8px; height: 8px; border-radius: 50%; background: #1cc97c; box-shadow: 0 0 0 3px rgba(28,201,124,0.2); } + +/* workspace */ +.pg-work { flex: 1; display: flex; flex-direction: column; min-width: 0; padding: 20px; gap: 14px; } + +/* toolbar */ +.pg-toolbar { + flex-shrink: 0; display: flex; align-items: center; gap: 18px; + padding: 14px 20px; border-radius: var(--pg-radius); + background: var(--pg-paper); border: var(--pg-border); box-shadow: var(--pg-shadow); +} +.pg-toolbar-left { display: flex; align-items: center; gap: 14px; flex: 1; min-width: 0; } +.pg-proj-emoji { + width: 48px; height: 48px; border-radius: 13px; + background: var(--pg-paper-warm); border: var(--pg-border-thin); + display: grid; place-items: center; font-size: 26px; + transform: rotate(-3deg); flex-shrink: 0; +} +.pg-proj-name { font-family: var(--pg-display); font-size: 22px; font-weight: 900; letter-spacing: -0.02em; line-height: 1; margin: 0; } +.pg-proj-meta { display: flex; gap: 8px; margin-top: 6px; align-items: center; } +.pg-proj-meta .pill { + padding: 3px 9px; border-radius: 99px; + background: rgba(29,24,48,0.08); font-family: var(--pg-mono); + font-size: 10px; font-weight: 700; +} +.pg-proj-meta .pill.lang { background: var(--pg-ink); color: var(--pg-bg); } +.pg-proj-blurb { + font-size: 12px; color: var(--pg-muted); margin-left: 4px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.pg-toolbar-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; } +.pg-status { + display: flex; align-items: center; gap: 8px; + padding: 8px 14px; border-radius: var(--pg-radius-pill); + background: rgba(29,24,48,0.06); + font-family: var(--pg-mono); font-size: 11px; font-weight: 600; +} +.pg-status .d { width: 7px; height: 7px; border-radius: 50%; background: #1cc97c; } +.pg-status.running .d { background: #ffd166; animation: pg-pulse 1s ease-in-out infinite; } +.pg-status.waiting .d { background: #3aa6ff; animation: pg-pulse 1s ease-in-out infinite; } +.pg-status.done .d { background: #1cc97c; } +.pg-status.error { background: #ffe5e0; } +.pg-status.error .d { background: #ff4d3d; } +@keyframes pg-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } +.pg-btn { + display: inline-flex; align-items: center; gap: 8px; + padding: 10px 18px; border-radius: var(--pg-radius-pill); + border: var(--pg-border-thin); font-size: 13px; font-weight: 700; + background: var(--pg-paper); color: var(--pg-ink); + box-shadow: 3px 3px 0 var(--pg-ink); + transition: transform .1s, box-shadow .1s; +} +.pg-btn:hover { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 var(--pg-ink); } +.pg-btn:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--pg-ink); } +.pg-btn.run { background: #4ade80; padding: 11px 22px; font-size: 14px; } +.pg-btn.stop { background: #ffb4a8; } +.pg-btn .ic { font-size: 13px; } + +/* editor + console split */ +.pg-split { flex: 1; display: flex; gap: 14px; min-height: 0; } +.pg-split.row { flex-direction: row; } +.pg-window { + display: flex; flex-direction: column; min-height: 0; + background: var(--pg-dark); + border: var(--pg-border); border-radius: var(--pg-radius); + box-shadow: var(--pg-shadow); overflow: hidden; +} +.pg-winbar { + flex-shrink: 0; display: flex; align-items: center; gap: 14px; + padding: 10px 16px; border-bottom: var(--pg-border-thin); +} +.pg-editor .pg-winbar { background: var(--pg-paper-warm); } +.pg-console .pg-winbar { background: var(--pg-paper-cool); } +.pg-winbar .dots { display: flex; gap: 6px; } +.pg-winbar .dots i { width: 11px; height: 11px; border-radius: 50%; border: 1.5px solid var(--pg-ink); display: block; } +.pg-winbar .dots i:nth-child(1) { background: #ff7a7a; } +.pg-winbar .dots i:nth-child(2) { background: #ffd166; } +.pg-winbar .dots i:nth-child(3) { background: #4ade80; } +.pg-winbar .filename { font-family: var(--pg-mono); font-size: 12px; font-weight: 700; } +.pg-winbar .filename .dim { color: var(--pg-muted); font-weight: 500; margin-left: 6px; } +.pg-winbar .spacer { flex: 1; } +.pg-winbar .pill { + padding: 3px 9px; border-radius: 99px; font-family: var(--pg-mono); + font-size: 10px; font-weight: 700; background: rgba(29,24,48,0.1); +} +.pg-winbar .iconbtn { + width: 28px; height: 28px; border-radius: 8px; + background: transparent; border: 1.5px solid transparent; + display: grid; place-items: center; color: var(--pg-ink); font-size: 13px; +} +.pg-winbar .iconbtn:hover { background: rgba(29,24,48,0.1); border-color: var(--pg-ink); } + +/* editor pane */ +.pg-editor { flex: 1.55 1 0; min-width: 0; } +.pg-editor-body { flex: 1; min-height: 0; min-width: 0; display: flex; } +.pg-editor-body > * { flex: 1; min-width: 0; height: 100%; } +.pg-editor-body .cm-editor { height: 100%; } +.pg-editor-body .cm-scroller { font-size: 13.5px; padding: 6px 0; } + +/* console pane */ +.pg-console { flex: 1 1 0; min-width: 280px; } +.pg-console-body { + flex: 1; min-height: 0; display: flex; + background: var(--pg-dark); +} +.pg-term { + flex: 1; min-height: 0; min-width: 0; + padding: 10px 6px 10px 12px; +} +.pg-term .xterm { height: 100%; } + +/* responsive */ +@media (max-width: 1200px) { + .pg-split.row { flex-direction: column; } + .pg-split.row .pg-editor { flex: 1.4 1 0; } + .pg-split.row .pg-console { flex: 1 1 0; min-height: 200px; min-width: 0; } +} +@media (max-width: 1100px) { .pg-side { width: 240px; } } +@media (max-width: 880px) { + .pg-app { height: auto; min-height: 100vh; } + .pg-main { flex-direction: column; } + .pg-side { width: 100%; } + .pg-nav-mid { display: none; } + .pg-split.row { min-height: 70vh; } +} +@media (max-width: 640px) { + .pg-toolbar { flex-direction: column; align-items: stretch; gap: 12px; } + .pg-toolbar-right { justify-content: flex-end; } +} + diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx new file mode 100644 index 000000000..af44a6f19 --- /dev/null +++ b/web/src/app/layout.tsx @@ -0,0 +1,72 @@ +import type { Metadata } from "next"; +import { Fraunces, DM_Sans, JetBrains_Mono } from "next/font/google"; +import "./globals.css"; + +const display = Fraunces({ + subsets: ["latin"], + variable: "--font-display", + weight: ["400", "600", "900"], + style: ["normal", "italic"], + display: "swap", +}); + +const body = DM_Sans({ + subsets: ["latin"], + variable: "--font-body", + weight: ["400", "500", "600", "700", "800"], + display: "swap", +}); + +const mono = JetBrains_Mono({ + subsets: ["latin"], + variable: "--font-mono", + weight: ["400", "500", "700"], + display: "swap", +}); + +export const metadata: Metadata = { + metadataBase: new URL("https://pybegin.pages.dev"), + title: { + default: "pyBegin · 250+ Python beginner projects you can run in the browser", + template: "%s · pyBegin", + }, + description: + "A scrappy collection of tiny Python projects — each small enough to read in a coffee break. Browse 250+ beginner projects, then edit and run them live in an in-browser Python playground.", + keywords: [ + "python beginner projects", + "learn python", + "python playground", + "run python in browser", + "python code editor", + "python examples", + ], + authors: [{ name: "Mrinank Bhowmick" }], + openGraph: { + type: "website", + siteName: "pyBegin", + title: "pyBegin · 250+ Python beginner projects", + description: + "Browse 250+ tiny Python projects and run them live in an in-browser playground. No install, no setup.", + url: "/", + }, + twitter: { + card: "summary_large_image", + title: "pyBegin · 250+ Python beginner projects", + description: + "Browse 250+ tiny Python projects and run them live in an in-browser playground.", + }, + alternates: { canonical: "/" }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/web/src/app/not-found.tsx b/web/src/app/not-found.tsx new file mode 100644 index 000000000..007ccdb4e --- /dev/null +++ b/web/src/app/not-found.tsx @@ -0,0 +1,22 @@ +import Link from "next/link"; +import SiteNav from "@/components/SiteNav"; +import SiteFooter from "@/components/SiteFooter"; + +export default function NotFound() { + return ( +
+ +
+
🫥
+

Page not found.

+

That page slithered off somewhere.

+
+ + ← Back home + +
+
+ +
+ ); +} diff --git a/web/src/app/opengraph-image.tsx b/web/src/app/opengraph-image.tsx new file mode 100644 index 000000000..329511681 --- /dev/null +++ b/web/src/app/opengraph-image.tsx @@ -0,0 +1,69 @@ +import { ImageResponse } from "next/og"; + +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; +export const alt = "pyBegin — 250+ Python beginner projects"; +export const dynamic = "force-static"; + +// Static social-share image, generated at build time. +export default function OgImage() { + return new ImageResponse( + ( +
+
+
+ 🐍 +
+
+ pyBegin +
+
+
+ Python that feels good to type. +
+
+ 250+ beginner projects · live in-browser playground +
+
+ ), + { ...size }, + ); +} diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx new file mode 100644 index 000000000..96f1faffb --- /dev/null +++ b/web/src/app/page.tsx @@ -0,0 +1,44 @@ +import type { Metadata } from "next"; +import Home from "@/components/Home"; +import { PROJECTS } from "@/lib/data"; + +export const metadata: Metadata = { + alternates: { canonical: "/" }, +}; + +// JSON-LD: the site + the project collection, for rich search results. +const jsonLd = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "WebSite", + name: "pyBegin", + url: "https://pybegin.pages.dev/", + description: + "250+ tiny Python beginner projects with an in-browser Python playground.", + }, + { + "@type": "ItemList", + name: "Python beginner projects", + numberOfItems: PROJECTS.length, + itemListElement: PROJECTS.map((p, i) => ({ + "@type": "ListItem", + position: i + 1, + name: p.name, + url: `https://pybegin.pages.dev/projects/${p.id}/`, + })), + }, + ], +}; + +export default function Page() { + return ( + <> + - - - - - - - - - - - - - - -
- - diff --git a/website/sticker-app-2.css b/website/sticker-app-2.css deleted file mode 100644 index 3d8db514b..000000000 --- a/website/sticker-app-2.css +++ /dev/null @@ -1,133 +0,0 @@ -/* === Sticker Pack · sections === */ - -/* Hero */ -.s-hero { - display: grid; grid-template-columns: 1.05fr 1fr; gap: 60px; - align-items: center; padding: 60px 0 40px; -} -.s-tag { - display: inline-flex; align-items: center; gap: 9px; - padding: 8px 16px; border-radius: var(--s-radius-pill); - background: var(--s-surface); border: var(--s-border); - box-shadow: 3px 3px 0 var(--s-ink); - font-size: 12px; font-weight: 700; margin-bottom: 28px; - transform: rotate(-1deg); -} -.s-tag .dot { - width: 8px; height: 8px; border-radius: 50%; background: var(--s-accent-3); - box-shadow: 0 0 0 3px rgba(28, 201, 124, 0.18); - animation: s-pulse 2s ease-in-out infinite; -} -@keyframes s-pulse { 0%, 100% { box-shadow: 0 0 0 3px rgba(28, 201, 124, 0.18); } 50% { box-shadow: 0 0 0 6px rgba(28, 201, 124, 0); } } - -.s-headline { - font-family: var(--s-display); font-weight: 900; - font-size: clamp(56px, 9.5vw, 116px); - line-height: 0.9; letter-spacing: -0.035em; - margin: 0 0 24px; -} -.s-headline .scribble { position: relative; display: inline-block; color: var(--s-accent); } -.s-headline .scribble::after { - content: ''; position: absolute; bottom: -10px; left: -4px; right: -4px; height: 12px; - background-repeat: no-repeat; background-size: 100% 100%; -} -.s-sub { - font-size: 18px; line-height: 1.55; color: rgba(29, 24, 48, 0.72); - max-width: 480px; margin: 0 0 32px; -} -.s-hero-cta { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; } -.s-btn-pri, .s-btn-sec { - padding: 16px 26px; border-radius: var(--s-radius-pill); - font: inherit; font-size: 15px; font-weight: 800; cursor: pointer; - display: inline-flex; align-items: center; gap: 10px; - transition: transform .12s, box-shadow .12s; -} -.s-btn-pri { - background: var(--s-accent); color: var(--s-ink); - border: var(--s-border); box-shadow: var(--s-shadow); -} -.s-btn-sec { - background: transparent; color: var(--s-ink); border: var(--s-border); -} -.s-btn-pri:hover, .s-btn-sec:hover { transform: translate(-2px, -2px); } -.s-btn-pri:hover { box-shadow: 8px 8px 0 var(--s-ink); } -.s-btn-sec:hover { background: var(--s-surface); box-shadow: 3px 3px 0 var(--s-ink); } -.s-btn-pri:active, .s-btn-sec:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--s-ink) !important; } - -/* Sticker cluster — hero right */ -.s-stickers { position: relative; height: 520px; } -.s-sticker { - position: absolute; padding: 18px 22px; - border: var(--s-border); border-radius: var(--s-radius-lg); - box-shadow: var(--s-shadow); - display: flex; align-items: center; gap: 14px; - transition: transform .25s cubic-bezier(.4,1.6,.4,1); - cursor: pointer; user-select: none; -} -.s-sticker:hover { transform: rotate(0deg) translate(-3px, -3px) scale(1.04) !important; box-shadow: 9px 9px 0 var(--s-ink); z-index: 10; } -.s-sticker .emoji { font-size: 44px; line-height: 1; flex-shrink: 0; } -.s-sticker .lbl { font-weight: 800; font-size: 15px; } -.s-sticker .num { font-family: var(--s-mono); font-size: 11px; color: rgba(29, 24, 48, 0.6); margin-top: 2px; } -@keyframes s-bob1 { 0%, 100% { transform: rotate(-4deg) translateY(0); } 50% { transform: rotate(-4deg) translateY(-6px); } } -@keyframes s-bob2 { 0%, 100% { transform: rotate(3deg) translateY(0); } 50% { transform: rotate(3deg) translateY(-8px); } } -@keyframes s-bob3 { 0%, 100% { transform: rotate(2deg) translateY(0); } 50% { transform: rotate(2deg) translateY(-5px); } } -@keyframes s-bob4 { 0%, 100% { transform: rotate(-2deg) translateY(0); } 50% { transform: rotate(-2deg) translateY(-7px); } } -@keyframes s-bob5 { 0%, 100% { transform: rotate(4deg) translateY(0); } 50% { transform: rotate(4deg) translateY(-6px); } } - -/* Stats marquee */ -.s-stats { - margin-top: 30px; - background: var(--s-ink); border-radius: var(--s-radius-lg); padding: 28px; - display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; - color: var(--s-bg); border: var(--s-border); - box-shadow: var(--s-shadow-lg); -} -.s-stat { text-align: center; padding: 8px 0; border-right: 1.5px dashed rgba(254, 249, 239, 0.22); } -.s-stat:last-child { border-right: 0; } -.s-stat-n { font-family: var(--s-display); font-weight: 900; font-size: clamp(36px, 4vw, 52px); line-height: 1; color: var(--s-accent); } -.s-stat-l { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; margin-top: 6px; color: rgba(254, 249, 239, 0.75); } - -/* Sections */ -.s-section { margin-top: 80px; } -.s-section-h { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 30px; flex-wrap: wrap; } -.s-section-title { font-family: var(--s-display); font-weight: 900; font-size: clamp(40px, 5vw, 64px); letter-spacing: -0.03em; line-height: 0.95; margin: 0; } -.s-section-title em { font-style: italic; color: var(--s-accent); } -.s-section-side { font-size: 14px; color: rgba(29, 24, 48, 0.6); max-width: 360px; text-align: right; } - -/* Toolbar */ -.s-toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 22px; flex-wrap: wrap; } -.s-search { - flex: 1; min-width: 280px; display: flex; align-items: center; gap: 12px; - padding: 14px 22px; border-radius: var(--s-radius-pill); - background: var(--s-surface); border: var(--s-border); box-shadow: 4px 4px 0 var(--s-ink); - transition: box-shadow .12s, transform .12s; -} -.s-search:focus-within { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--s-ink); } -.s-search input { flex: 1; background: transparent; border: 0; outline: 0; color: var(--s-ink); font: inherit; font-size: 16px; font-weight: 500; } -.s-search input::placeholder { color: rgba(29, 24, 48, 0.4); } -.s-search-icon { font-size: 18px; } -.s-search-key { - font-family: var(--s-mono); font-size: 11px; padding: 3px 7px; - background: var(--s-bg); border: 1.5px solid var(--s-ink); border-radius: 5px; -} - -.s-sort { - padding: 12px 18px; border-radius: var(--s-radius-pill); - background: var(--s-surface); border: var(--s-border); box-shadow: 3px 3px 0 var(--s-ink); - font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; - display: flex; align-items: center; gap: 8px; -} - -.s-chips { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 24px; } -.s-chip { - padding: 9px 18px; border-radius: var(--s-radius-pill); - background: var(--s-surface); border: var(--s-border-thin); - font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; - display: flex; align-items: center; gap: 8px; - transition: transform .12s, box-shadow .12s; -} -.s-chip:hover { transform: translate(-1px, -1px); box-shadow: 2px 2px 0 var(--s-ink); } -.s-chip.active { background: var(--s-accent); box-shadow: 3px 3px 0 var(--s-ink); } -.s-chip.active.bm { background: var(--s-accent-4); } -.s-chip .ct { font-family: var(--s-mono); font-size: 11px; opacity: 0.7; font-weight: 700; } -.s-chip .ic { font-size: 14px; } diff --git a/website/sticker-app-3.css b/website/sticker-app-3.css deleted file mode 100644 index 5c6b38596..000000000 --- a/website/sticker-app-3.css +++ /dev/null @@ -1,417 +0,0 @@ -/* === Sticker Pack · gallery + paths + contributors === */ - -/* Gallery */ -.s-gallery.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; } -.s-gallery.list { display: flex; flex-direction: column; gap: 12px; } -.s-gallery.masonry { columns: 3; column-gap: 22px; } -.s-gallery.masonry .s-card { break-inside: avoid; margin-bottom: 22px; } - -.s-card { - position: relative; padding: 24px; - background: var(--s-surface); border: var(--s-border); border-radius: var(--s-radius-lg); - box-shadow: var(--s-shadow); - cursor: pointer; transition: transform .15s, box-shadow .15s; - text-align: left; -} -.s-card:hover { transform: translate(-3px, -3px); box-shadow: 9px 9px 0 var(--s-ink); z-index: 2; } -.s-card:active { transform: translate(2px, 2px); box-shadow: 2px 2px 0 var(--s-ink); } - -.s-gallery .s-card:nth-child(5n+1) { background: var(--s-surface-warm); } -.s-gallery .s-card:nth-child(5n+2) { background: var(--s-surface-cool); } -.s-gallery .s-card:nth-child(5n+3) { background: var(--s-surface-purple); } -.s-gallery .s-card:nth-child(5n+4) { background: var(--s-surface-yellow); } -.s-gallery .s-card:nth-child(5n+5) { background: var(--s-surface-green); } - -.s-card-emoji { font-size: 44px; margin-bottom: 14px; line-height: 1; display: inline-block; transition: transform .25s cubic-bezier(.4,1.6,.4,1); } -.s-card:hover .s-card-emoji { transform: rotate(-8deg) scale(1.1); } -.s-card-h { display: flex; align-items: start; justify-content: space-between; gap: 10px; margin-bottom: 8px; } -.s-card-name { font-family: var(--s-display); font-size: 22px; font-weight: 800; line-height: 1.1; letter-spacing: -0.015em; margin: 0; } -.s-card-blurb { font-size: 14px; color: rgba(29, 24, 48, 0.72); line-height: 1.5; margin: 0 0 16px; } -.s-card-meta { display: flex; gap: 8px; flex-wrap: wrap; } -.s-meta-pill { - padding: 4px 11px; border-radius: var(--s-radius-pill); - background: var(--s-ink); color: var(--s-bg); - font-family: var(--s-mono); font-size: 10px; font-weight: 700; - letter-spacing: 0.02em; -} -.s-meta-pill.dep { background: rgba(29,24,48,0.08); color: var(--s-ink); border: 1.5px solid var(--s-ink); } -.s-card-bm { - width: 34px; height: 34px; border-radius: 50%; - background: var(--s-surface); border: var(--s-border-thin); - display: grid; place-items: center; cursor: pointer; flex-shrink: 0; - font-size: 15px; line-height: 1; color: var(--s-ink); - transition: transform .12s, background .12s; -} -.s-card-bm:hover { transform: scale(1.08) rotate(-6deg); } -.s-card-bm.on { background: var(--s-accent); transform: rotate(-6deg); } - -/* Card density: minimal */ -.s-card.minimal { padding: 16px 20px; } -.s-card.minimal .s-card-emoji { display: none; } -.s-card.minimal .s-card-blurb { display: none; } -.s-card.minimal .s-card-h { margin-bottom: 10px; } - -/* Card with image banner */ -.s-card.image { padding-top: 12px; } -.s-card.image .s-img { - height: 140px; border-radius: var(--s-radius-md); - margin: 0 0 18px; - border: var(--s-border-thin); - display: grid; place-items: center; font-size: 60px; - background-image: repeating-linear-gradient(135deg, rgba(29,24,48,0.05) 0 6px, transparent 6px 12px); - overflow: hidden; position: relative; -} -.s-gallery .s-card:nth-child(5n+1) .s-img { background-color: rgba(255, 122, 89, 0.25); } -.s-gallery .s-card:nth-child(5n+2) .s-img { background-color: rgba(58, 166, 255, 0.22); } -.s-gallery .s-card:nth-child(5n+3) .s-img { background-color: rgba(165, 92, 255, 0.22); } -.s-gallery .s-card:nth-child(5n+4) .s-img { background-color: rgba(255, 209, 102, 0.3); } -.s-gallery .s-card:nth-child(5n+5) .s-img { background-color: rgba(28, 201, 124, 0.22); } - -/* List density */ -.s-gallery.list .s-card { display: grid; grid-template-columns: 56px 1fr auto auto auto; align-items: center; gap: 20px; padding: 14px 22px; } -.s-gallery.list .s-card-emoji { font-size: 30px; margin: 0; } -.s-gallery.list .s-card-h { margin: 0; display: contents; } -.s-gallery.list .s-card-name { font-size: 18px; } -.s-gallery.list .s-card-blurb { display: none; } -.s-gallery.list .s-card-meta { gap: 6px; } -.s-gallery.list .s-card-bm { margin-left: 4px; } - -/* Empty state */ -.s-empty { - padding: 60px 32px; text-align: center; - border: 2.5px dashed rgba(29,24,48,0.25); border-radius: var(--s-radius-lg); -} -.s-empty .em { font-size: 56px; } -.s-empty h3 { font-family: var(--s-display); font-size: 24px; margin: 16px 0 6px; font-weight: 800; } -.s-empty p { color: rgba(29,24,48,0.65); margin: 0; } - -/* Learning paths */ -.s-paths { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; } -.s-path { - padding: 30px; border: var(--s-border); border-radius: 28px; - box-shadow: var(--s-shadow); background: var(--s-surface); - position: relative; overflow: hidden; - transition: transform .15s, box-shadow .15s; -} -.s-path:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--s-ink); } -.s-path:nth-child(1) { background: var(--s-surface-warm); } -.s-path:nth-child(2) { background: var(--s-surface-cool); } -.s-path:nth-child(3) { background: var(--s-surface-purple); } -.s-path-head { display: flex; align-items: start; justify-content: space-between; gap: 14px; margin-bottom: 14px; } -.s-path-tag { - display: inline-block; padding: 5px 13px; border-radius: var(--s-radius-pill); - background: var(--s-ink); color: var(--s-bg); font-family: var(--s-mono); - font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; -} -.s-path-num { font-family: var(--s-display); font-size: 64px; font-weight: 900; line-height: 0.8; color: rgba(29,24,48,0.18); } -.s-path-name { font-family: var(--s-display); font-size: 34px; font-weight: 900; letter-spacing: -0.025em; line-height: 1; margin: 0 0 12px; } -.s-path-desc { font-size: 14px; color: rgba(29, 24, 48, 0.75); line-height: 1.55; margin: 0 0 22px; } -.s-path-list { display: flex; flex-direction: column; gap: 8px; } -.s-path-item { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: rgba(255,255,255,0.7); border-radius: 14px; border: 1.5px solid var(--s-ink); font-size: 14px; font-weight: 600; cursor: pointer; transition: background .12s; } -.s-path-item:hover { background: #fff; } -.s-path-item i { font-family: var(--s-mono); font-style: normal; font-size: 11px; color: rgba(29, 24, 48, 0.5); width: 16px; } -.s-path-item .em { font-size: 18px; margin-left: auto; } - -/* Contributors */ -.s-contributors { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; } -.s-contrib { - padding: 16px 12px; text-align: center; border-radius: 18px; - background: var(--s-surface); border: var(--s-border-thin); box-shadow: 4px 4px 0 var(--s-ink); - transition: transform .25s cubic-bezier(.4,1.6,.4,1); -} -.s-contrib:nth-child(7n+2) { transform: rotate(-1.5deg); } -.s-contrib:nth-child(7n+3) { transform: rotate(1.5deg); } -.s-contrib:nth-child(7n+4) { transform: rotate(-1deg); } -.s-contrib:nth-child(7n+5) { transform: rotate(1deg); } -.s-contrib:nth-child(7n+6) { transform: rotate(-2deg); } -.s-contrib:nth-child(7n) { transform: rotate(0.5deg); } -.s-contrib:hover { transform: rotate(0) translate(-2px, -2px) scale(1.04); box-shadow: 6px 6px 0 var(--s-ink); z-index: 5; } -.s-contrib.lead { - grid-column: span 3; grid-row: span 2; - padding: 36px; text-align: left; - background: var(--s-accent); - box-shadow: var(--s-shadow-lg); - border: var(--s-border); - transform: rotate(-1.5deg); -} -.s-contrib.lead:hover { transform: rotate(0) translate(-3px, -3px); box-shadow: 11px 11px 0 var(--s-ink); } -.s-avatar { - width: 56px; height: 56px; border-radius: 50%; - background: linear-gradient(135deg, #ffd4b0, var(--s-accent)); - border: var(--s-border-thin); - margin: 0 auto 10px; display: grid; place-items: center; - font-weight: 900; font-size: 20px; color: var(--s-ink); -} -.s-contrib:nth-child(7n+2) .s-avatar { background: linear-gradient(135deg, #d4f0ff, var(--s-accent-2)); } -.s-contrib:nth-child(7n+3) .s-avatar { background: linear-gradient(135deg, #e5d4ff, var(--s-accent-5)); } -.s-contrib:nth-child(7n+4) .s-avatar { background: linear-gradient(135deg, #d4ffd9, var(--s-accent-3)); } -.s-contrib:nth-child(7n+5) .s-avatar { background: linear-gradient(135deg, #fff5b0, var(--s-accent-4)); } -.s-contrib.lead .s-avatar { width: 84px; height: 84px; margin: 0 0 20px; font-size: 32px; background: var(--s-bg); } -.s-cname { font-weight: 800; font-size: 13px; line-height: 1.2; } -.s-chandle { font-family: var(--s-mono); font-size: 10px; color: rgba(29, 24, 48, 0.55); margin-top: 3px; } -.s-commits { font-family: var(--s-mono); font-size: 10px; color: var(--s-ink); margin-top: 6px; font-weight: 700; padding: 3px 8px; background: rgba(29,24,48,0.06); border-radius: var(--s-radius-pill); display: inline-block; } -.s-contrib.lead .s-cname { font-size: 32px; font-family: var(--s-display); font-weight: 900; letter-spacing: -0.025em; line-height: 1; margin-bottom: 4px; } -.s-contrib.lead .s-chandle { font-size: 14px; } -.s-contrib.lead .s-quote { margin-top: 20px; font-size: 16px; line-height: 1.5; font-weight: 500; } -.s-contrib.lead .s-badge { - display: inline-block; margin-top: 18px; padding: 7px 16px; - background: var(--s-ink); color: var(--s-bg); font-size: 11px; font-weight: 800; - letter-spacing: 0.08em; text-transform: uppercase; border-radius: var(--s-radius-pill); -} - -.s-more { - display: flex; justify-content: center; margin-top: 28px; -} -.s-more button { - padding: 14px 28px; border-radius: var(--s-radius-pill); - background: var(--s-surface); border: var(--s-border); box-shadow: var(--s-shadow); - font: inherit; font-size: 14px; font-weight: 700; cursor: pointer; - transition: transform .12s, box-shadow .12s; -} -.s-more button:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--s-ink); } - -/* CTA banner */ -.s-cta-banner { - margin-top: 80px; padding: 50px 56px; - background: var(--s-ink); color: var(--s-bg); - border: var(--s-border); border-radius: 28px; box-shadow: var(--s-shadow-lg); - display: grid; grid-template-columns: 1.4fr 1fr; gap: 40px; align-items: center; - position: relative; overflow: hidden; -} -.s-cta-banner::before { - content: ''; position: absolute; bottom: -40px; right: -40px; - width: 200px; height: 200px; border-radius: 50%; background: var(--s-accent); - opacity: 0.18; -} -.s-cta-banner h2 { font-family: var(--s-display); font-size: 44px; font-weight: 900; line-height: 1; letter-spacing: -0.025em; margin: 0 0 12px; } -.s-cta-banner h2 em { font-style: italic; color: var(--s-accent); } -.s-cta-banner p { font-size: 16px; color: rgba(254, 249, 239, 0.78); line-height: 1.55; margin: 0 0 22px; max-width: 480px; } -.s-cta-banner-codes { display: flex; flex-direction: column; gap: 10px; font-family: var(--s-mono); font-size: 13px; } -.s-cta-banner-code { - padding: 12px 16px; background: rgba(254,249,239,0.08); border-radius: 10px; - border: 1.5px dashed rgba(254,249,239,0.25); - color: var(--s-bg); display: flex; align-items: center; gap: 12px; -} -.s-cta-banner-code span:first-child { color: var(--s-accent); } -.s-cta-banner-code .copy { margin-left: auto; cursor: pointer; opacity: 0.6; font-size: 12px; } - -/* Footer */ -.s-footer { - margin: 60px 0 0; padding: 32px 0; - border-top: 2px dashed rgba(29, 24, 48, 0.3); - display: flex; justify-content: space-between; align-items: center; - font-size: 13px; color: rgba(29, 24, 48, 0.6); - flex-wrap: wrap; gap: 16px; -} -.s-heart { color: var(--s-accent); } -.s-footer-links { display: flex; gap: 18px; } -.s-footer a { color: inherit; text-decoration: none; } -.s-footer a:hover { color: var(--s-ink); text-decoration: underline; } - -/* Project modal */ -.s-modal-bg { - position: fixed; inset: 0; background: rgba(29, 24, 48, 0.5); - z-index: 1000; display: grid; place-items: center; padding: 24px; - animation: s-fade .15s; -} -@keyframes s-fade { from { opacity: 0; } } -.s-modal { - background: var(--s-surface); border: var(--s-border); border-radius: 28px; - box-shadow: 12px 12px 0 var(--s-accent); - max-width: 560px; width: 100%; padding: 36px; - animation: s-pop .25s cubic-bezier(.4,1.6,.4,1); - position: relative; -} -@keyframes s-pop { from { transform: translateY(20px) scale(.96); opacity: 0; } } -.s-modal-emoji { font-size: 72px; margin-bottom: 18px; } -.s-modal h2 { font-family: var(--s-display); font-size: 40px; font-weight: 900; letter-spacing: -0.025em; line-height: 1; margin: 0 0 8px; } -.s-modal-blurb { font-size: 16px; color: rgba(29,24,48,0.75); line-height: 1.55; margin: 0 0 22px; } -.s-modal-meta { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; } -.s-modal-code { - font-family: var(--s-mono); font-size: 13px; - background: var(--s-ink); color: var(--s-bg); padding: 16px; - border-radius: 12px; margin-bottom: 20px; -} -.s-modal-actions { display: flex; gap: 12px; } -.s-modal-x { - position: absolute; top: 16px; right: 16px; - width: 36px; height: 36px; border-radius: 50%; - background: var(--s-bg); border: var(--s-border-thin); cursor: pointer; - font-size: 18px; font-weight: 700; display: grid; place-items: center; - box-shadow: 2px 2px 0 var(--s-ink); -} - -/* Responsive */ -@media (max-width: 960px) { - .s-hero { grid-template-columns: 1fr; } - .s-stickers { height: 420px; } - .s-gallery.grid, .s-gallery.masonry { grid-template-columns: repeat(2, 1fr) !important; columns: 2; } - .s-paths { grid-template-columns: 1fr; } - .s-contributors { grid-template-columns: repeat(3, 1fr); } - .s-contrib.lead { grid-column: span 3; grid-row: span 1; } - .s-cta-banner { grid-template-columns: 1fr; padding: 36px; } -} -@media (max-width: 640px) { - .s-shell { padding: 0 18px; } - .s-navlinks { display: none; } - .s-stats { grid-template-columns: repeat(2, 1fr); gap: 14px; } - .s-stat { border-right: 0 !important; padding: 12px 0; } - .s-gallery.grid, .s-gallery.masonry { grid-template-columns: 1fr !important; columns: 1; } - .s-contributors { grid-template-columns: repeat(2, 1fr); } - .s-contrib.lead { grid-column: span 2; } -} - -/* ====== Try-it panel (modal demo) ====== */ -.s-meta-pill.run { - background: var(--s-accent); - color: var(--s-ink); - font-weight: 700; - border-color: var(--s-ink); -} -.s-try { - margin-top: 4px; - padding: 20px; - border: var(--s-border-thin); - border-radius: 16px; - background: var(--s-bg); - box-shadow: var(--s-shadow-sm); - margin-bottom: 22px; - font-family: var(--s-body); -} -.s-try-h { - font-family: var(--s-display); - font-size: 18px; - font-weight: 800; - margin-bottom: 14px; - color: var(--s-ink); -} -.s-try-h::before { content: ''; } -.s-try-form { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); - gap: 10px 14px; - margin-bottom: 14px; -} -.s-try-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--s-ink); } -.s-try-field > span { font-weight: 700; } -.s-try-field input, .s-try-field select { - padding: 9px 12px; - border-radius: 10px; - border: var(--s-border-thin); - background: #fff; - color: var(--s-ink); - font: inherit; - font-size: 14px; -} -.s-try-field input:focus, .s-try-field select:focus { - outline: none; - box-shadow: 0 0 0 3px var(--s-accent); -} -.s-try-run { margin-bottom: 12px; } -.s-try-run:disabled { opacity: 0.55; cursor: wait; } -.s-try-error { - margin: 0 0 12px; - padding: 10px 14px; - border-radius: 10px; - background: #ffe1e1; - border: var(--s-border-thin); - border-color: #b34d4d; - color: #6e1414; - font-size: 13px; -} -.s-try-out { - margin-top: 6px; - padding: 14px 16px; - border-radius: 12px; - background: var(--s-surface-warm); - border: var(--s-border-thin); - color: var(--s-ink); -} -.s-try-out pre, .s-try-story pre { - margin: 0; - white-space: pre-wrap; - font: 14px/1.55 var(--s-body); - color: var(--s-ink); -} - -/* Tic-tac-toe board */ -.s-ttt-grid { - display: grid; - grid-template-columns: repeat(3, 78px); - grid-template-rows: repeat(3, 78px); - gap: 8px; - justify-content: center; - margin: 6px auto 4px; -} -.s-ttt-cell { - background: #fff; - border: var(--s-border-thin); - border-radius: 14px; - font: 800 38px/1 var(--s-display); - color: var(--s-ink); - cursor: pointer; - transition: transform 80ms ease; - box-shadow: var(--s-shadow-sm); -} -.s-ttt-cell:not(:disabled):hover { transform: translate(-2px, -2px); box-shadow: 5px 5px 0 var(--s-ink); } -.s-ttt-cell:disabled { cursor: default; } -.s-ttt-cell [data-mark="X"] { color: var(--s-accent); } -.s-ttt-cell [data-mark="O"] { color: #2f7a4f; } - -/* Hangman */ -.s-hg-row { - display: flex; - gap: 14px; - align-items: flex-end; - margin-bottom: 14px; -} -.s-hg-word { - display: flex; - gap: 8px; - flex-wrap: wrap; - justify-content: center; - margin: 6px 0 10px; -} -.s-hg-slot { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 28px; - padding: 6px 4px; - border-bottom: 3px solid var(--s-ink); - font: 800 26px/1 var(--s-display); - color: var(--s-ink); -} -.s-hg-meta { - display: flex; - justify-content: space-between; - font-size: 13px; - margin-bottom: 10px; - color: var(--s-ink); -} -.s-hg-wrong { color: #b34d4d; letter-spacing: 2px; font-weight: 700; } -.s-hg-keys { - display: grid; - grid-template-columns: repeat(13, 1fr); - gap: 5px; -} -.s-hg-key { - padding: 8px 0; - border-radius: 8px; - border: var(--s-border-thin); - background: #fff; - color: var(--s-ink); - font: 700 13px/1 var(--s-body); - cursor: pointer; - transition: transform 80ms ease; -} -.s-hg-key:not(:disabled):hover { transform: translateY(-1px); } -.s-hg-key.used { cursor: default; opacity: 0.55; } -.s-hg-key.hit { background: #c9f3d4; opacity: 1; } -.s-hg-key.miss { background: #ffd4d4; } - -@media (max-width: 640px) { - .s-hg-keys { grid-template-columns: repeat(9, 1fr); } - .s-ttt-grid { grid-template-columns: repeat(3, 64px); grid-template-rows: repeat(3, 64px); } - .s-ttt-cell { font-size: 30px; } -} diff --git a/website/sticker-app.css b/website/sticker-app.css deleted file mode 100644 index 4a5a5e3da..000000000 --- a/website/sticker-app.css +++ /dev/null @@ -1,100 +0,0 @@ -/* === Sticker Pack · standalone prototype === */ -:root { - --s-bg: #fef9ef; - --s-ink: #1d1830; - --s-accent: #ff7a59; - --s-accent-2: #3aa6ff; - --s-accent-3: #1cc97c; - --s-accent-4: #ffd166; - --s-accent-5: #a55cff; - --s-surface: #ffffff; - --s-surface-warm: #ffe5d4; - --s-surface-cool: #d4f0ff; - --s-surface-purple: #e5e0ff; - --s-surface-green: #d4ffd9; - --s-surface-yellow: #fff5b0; - --s-radius-lg: 24px; - --s-radius-md: 16px; - --s-radius-pill: 99px; - --s-shadow: 6px 6px 0 var(--s-ink); - --s-shadow-lg: 8px 8px 0 var(--s-ink); - --s-shadow-sm: 3px 3px 0 var(--s-ink); - --s-border: 2.5px solid var(--s-ink); - --s-border-thin: 2px solid var(--s-ink); - --s-display: 'Fraunces', Georgia, serif; - --s-body: 'DM Sans', system-ui, sans-serif; - --s-mono: 'JetBrains Mono', ui-monospace, monospace; - --s-density: 1; -} - -* { box-sizing: border-box; } -html, body { margin: 0; padding: 0; } -body { - background: var(--s-bg); - background-image: - radial-gradient(circle at 15% 12%, #ffe5d4 0, transparent 28%), - radial-gradient(circle at 85% 6%, #d4f0ff 0, transparent 30%), - radial-gradient(circle at 75% 65%, #e5e0ff 0, transparent 26%), - radial-gradient(circle at 10% 80%, #fff0d4 0, transparent 22%); - color: var(--s-ink); - font-family: var(--s-body); - font-size: 16px; - line-height: 1.5; - min-height: 100vh; - overflow-x: hidden; -} -body::before { - content: ''; position: fixed; inset: 0; - background-image: radial-gradient(rgba(29, 24, 48, 0.07) 1.1px, transparent 1.1px); - background-size: 22px 22px; - pointer-events: none; z-index: 0; -} - -.s-display { font-family: var(--s-display); font-weight: 800; letter-spacing: -0.025em; } -.s-mono { font-family: var(--s-mono); } -.s-shell { max-width: 1280px; margin: 0 auto; padding: 0 32px; position: relative; z-index: 1; } - -/* Nav */ -.s-nav { display: flex; align-items: center; justify-content: space-between; padding: 24px 0 8px; } -.s-logo { display: flex; align-items: center; gap: 14px; } -.s-logomark { - width: 48px; height: 48px; border-radius: 14px; - background: var(--s-accent); - border: var(--s-border); box-shadow: 4px 4px 0 var(--s-ink); - display: grid; place-items: center; - transform: rotate(-6deg); transition: transform 0.3s cubic-bezier(.4,1.8,.4,1); -} -.s-logo:hover .s-logomark { transform: rotate(6deg) scale(1.05); } -.s-logomark svg { width: 28px; height: 28px; } -.s-brand-name { font-family: var(--s-display); font-size: 24px; font-weight: 900; letter-spacing: -0.025em; line-height: 1; } -.s-brand-sub { font-size: 12px; color: rgba(29, 24, 48, 0.55); font-family: var(--s-mono); margin-top: 2px; } -.s-navlinks { display: flex; gap: 4px; } -.s-navlinks button { - font: inherit; padding: 9px 16px; border-radius: var(--s-radius-pill); - font-size: 13px; font-weight: 600; cursor: pointer; - background: transparent; border: 0; color: var(--s-ink); -} -.s-navlinks button.active { background: var(--s-ink); color: var(--s-bg); } -.s-navlinks button:hover:not(.active) { background: rgba(29,24,48, 0.08); } -.s-nav-right { display: flex; gap: 10px; align-items: center; } -.s-bm-count { - width: 38px; height: 38px; border-radius: 50%; - background: var(--s-surface); border: var(--s-border-thin); - display: grid; place-items: center; font-weight: 700; font-size: 14px; - box-shadow: 3px 3px 0 var(--s-ink); position: relative; cursor: pointer; -} -.s-bm-count.has::after { - content: ''; position: absolute; top: 4px; right: 4px; - width: 8px; height: 8px; border-radius: 50%; background: var(--s-accent); - border: 1.5px solid var(--s-ink); -} -.s-cta { - padding: 12px 22px; border-radius: var(--s-radius-pill); - background: var(--s-ink); color: var(--s-bg); - border: var(--s-border); box-shadow: 4px 4px 0 var(--s-accent); - font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; - display: flex; align-items: center; gap: 8px; - transition: transform .12s, box-shadow .12s; -} -.s-cta:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--s-accent); } -.s-cta:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--s-accent); } diff --git a/website/sticker-app.jsx b/website/sticker-app.jsx deleted file mode 100644 index c28193624..000000000 --- a/website/sticker-app.jsx +++ /dev/null @@ -1,299 +0,0 @@ -// === Sticker Pack · main App === - -function StickerApp() { - const [t, setTweak] = useTweaks(window.STICKER_TWEAK_DEFAULTS); - const [search, setSearch] = useState(''); - const [cat, setCat] = useState('all'); - const [bm, setBm] = useState(() => window.PBP_BM.get()); - const [sort, setSort] = useState('default'); - const [open, setOpen] = useState(null); - const [showBmOnly, setShowBmOnly] = useState(false); - - const toggleBm = (id) => setBm(window.PBP_BM.toggle(id)); - - // Filter + sort - const filtered = useMemo(() => { - let arr = window.PBP_PROJECTS.filter(p => { - if (showBmOnly && !bm.includes(p.id)) return false; - if (cat !== 'all' && p.cat !== cat) return false; - if (search) { - const q = search.toLowerCase(); - return p.name.toLowerCase().includes(q) || p.blurb.toLowerCase().includes(q) || p.deps.toLowerCase().includes(q); - } - return true; - }); - if (sort === 'alpha') arr = [...arr].sort((a,b) => a.name.localeCompare(b.name)); - if (sort === 'short') arr = [...arr].sort((a,b) => a.lines - b.lines); - if (sort === 'bookmarks') arr = [...arr].sort((a,b) => (bm.includes(b.id) ? 1 : 0) - (bm.includes(a.id) ? 1 : 0)); - return arr; - }, [search, cat, bm, sort, showBmOnly]); - - // Tweak CSS vars - const rootStyle = useMemo(() => ({ - '--s-accent': t.accent, - '--s-display': t.fontDisplay, - '--s-body': t.fontBody, - '--s-bg': t.surface === 'cream' ? '#fef9ef' : t.surface === 'mint' ? '#eefbf2' : t.surface === 'sky' ? '#eff6ff' : t.surface === 'lilac' ? '#f4eefe' : '#fef9ef', - }), [t]); - - return ( -
-
- - {/* NAV */} - - - {/* HERO */} -
-
-
Live · 100 projects from 241 humans
-

- Python -
- that feels good -
- to type. -

-

A scrappy collection of tiny Python projects. Each one is small enough to read in a coffee break and big enough to teach you something real. Pick a vibe. Type it out. Run it.

-
- - -
-
- -
- - {/* STATS */} -
-
100
Projects
-
241
Contributors
-
2.3k
★ Stars
-
903
Forks
-
- - {/* PATHS */} -
-
-

Three paths in.

-
Not sure where to start? Each path is three projects, building on each other. Finish one in a weekend.
-
-
- {window.PBP_PATHS.map((p, idx) => ( -
-
-
{p.tag.toUpperCase()}
-
0{idx+1}
-
-

{p.name}

-

{p.desc}

-
- {p.items.map((id, i) => { - const pr = window.PBP_PROJECTS.find(d => d.id === id); - return pr && ( -
setOpen(pr)}> - 0{i+1} - {pr.name} - {pr.emoji} -
- ); - })} -
-
- ))} -
-
- - {/* GALLERY */} - - - {/* CONTRIBUTORS */} -
-
-

Made by humans.

-
241 contributors so far. PRs welcome. Add your own beginner project.
-
-
-
-
{window.PBP_CONTRIBUTORS[0].name[0]}
-
{window.PBP_CONTRIBUTORS[0].name}
-
@{window.PBP_CONTRIBUTORS[0].handle}
-

"I started this repo to learn Python myself. It turned into a place where 240+ people teach each other. Best happy accident I've ever shipped."

-
Maintainer · 412 commits
-
- {window.PBP_CONTRIBUTORS.slice(1, 13).map(c => ( -
-
{c.name[0]}
-
{c.name.split(' ')[0]}
-
@{c.handle.length > 12 ? c.handle.slice(0,12) + '…' : c.handle}
-
{c.commits} ★
-
- ))} -
-
- -
-
- - {/* CTA BANNER */} -
-
-

Want to contribute?

-

Drop your own beginner project as a PR. One folder, one Python file, one README. We merge if it runs.

-
- -
-
-
-
- $ - git clone python-beginner-projects.git - copy -
-
- $ - cd projects && mkdir my-project - copy -
-
-
- - -
- - {open && setOpen(null)} isBm={bm.includes(open.id)} toggleBm={toggleBm} />} - - - - setTweak('accent', v)} /> - setTweak('surface', v)} /> - - - setTweak('layout', v)} /> - setTweak('cardStyle', v)} /> - - - setTweak('fontDisplay', v)} /> - setTweak('fontBody', v)} /> - -
- ); -} - -ReactDOM.createRoot(document.getElementById('root')).render(); diff --git a/website/sticker-components.jsx b/website/sticker-components.jsx deleted file mode 100644 index 9fca3f761..000000000 --- a/website/sticker-components.jsx +++ /dev/null @@ -1,361 +0,0 @@ -// === Sticker Pack · standalone prototype components === - -const { useState, useEffect, useMemo, useRef } = React; - -function StickerLogo() { - return ( - - - - - - - ); -} - -function ScribbleSvg({ color }) { - // SVG inline so accent color works without rebuilding background-image - return ( - - - - ); -} - -function HeroStickers() { - // Bobbing pile of stickers in the hero — pulls a few highlight projects - const items = [ - { e: '🐍', l: 'Snake Game', n: '92 lines · pygame', bg: 'var(--s-surface-warm)', pos: { top: 6, left: 18 }, bob: 1, rot: -4 }, - { e: '☁️', l: 'Weather App', n: '88 lines · requests',bg: 'var(--s-surface-cool)', pos: { top: 80, right: 0 }, bob: 2, rot: 3 }, - { e: '🪢', l: 'Hangman', n: '78 lines · stdlib', bg: 'var(--s-surface-purple)',pos: { top: 215, left: -10 },bob: 3, rot: 2 }, - { e: '▦', l: 'QR Generator',n: '22 lines · qrcode', bg: 'var(--s-surface-green)', pos: { top: 290, right: 50 },bob: 4, rot: -2 }, - { e: '🧮', l: 'Calculator', n: '110 lines · tk', bg: 'var(--s-surface-yellow)',pos: { top: 400, left: 60 }, bob: 5, rot: 4 }, - { e: '🔐', l: 'Password Gen',n: '30 lines · secrets', bg: '#ffd4f0', pos: { top: 410, right: 10 },bob: 1, rot: -3 }, - ]; - return ( -
- {items.map((s, i) => ( -
- {s.e} -
-
{s.l}
-
{s.n}
-
-
- ))} -
- ); -} - -function ProjectCard({ p, cardStyle, isBm, toggleBm, onOpen }) { - return ( -
onOpen(p)}> - {cardStyle === 'image' &&
{p.emoji}
} - {cardStyle !== 'image' &&
{p.emoji}
} -
-

{p.name}

- -
-

{p.blurb}

-
- {p.lines} lines - {p.deps} - {p.runnable && ▶ Try it} -
-
- ); -} - -function ProjectModal({ p, onClose, isBm, toggleBm }) { - useEffect(() => { - const k = (e) => e.key === 'Escape' && onClose(); - window.addEventListener('keydown', k); - return () => window.removeEventListener('keydown', k); - }, [onClose]); - if (!p) return null; - return ( -
-
e.stopPropagation()}> - -
{p.emoji}
-

{p.name}

-

{p.blurb}

-
- {p.lines} lines - {p.deps} - {p.cat} -
-
-
$ git clone python-beginner-projects.git
-
$ cd projects/{p.id}
-
$ python {p.id}.py
-
- {p.runnable && } -
- - -
-
-
- ); -} - -// ============================================================ -// Try It — live demo panel inside the project modal. -// Renders a simple input form for stateless projects (rps, bmi, qr, -// madlibs) and hands off to dedicated widgets for the turn-based ones -// (tictactoe, hangman). All POSTs go to the single Vercel backend. -// ============================================================ - -function TryItPanel({ project }) { - const api = window.PBP_API; - if (api.isInteractive(project.id)) { - if (project.id === 'tictactoe') return ; - if (project.id === 'hangman') return ; - } - return ; -} - -function SimpleRunner({ project }) { - const fields = window.PBP_API.inputsFor(project.id); - const [form, setForm] = useState(() => window.PBP_API.defaults(project.id)); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [out, setOut] = useState(null); - - const run = async () => { - setError(null); - setLoading(true); - try { - const res = await window.PBP_API.call(project.id, form); - setOut(res); - } catch (e) { - setError(e.message || 'Request failed'); - setOut(null); - } finally { - setLoading(false); - } - }; - - return ( -
-
▶ Try it live
-
- {fields.map(f => ( - - ))} -
- - {error &&
⚠ {error}
} - {out && } -
- ); -} - -function castOption(field, raw) { - if (Array.isArray(field.options) && field.options.length && typeof field.options[0] === 'object') { - const match = field.options.find(o => String(o.value) === raw); - return match ? match.value : raw; - } - return raw; -} - -function SimpleOutput({ projectId, out }) { - if (projectId === 'qr' && out.png_b64) { - return ( -
- QR code -
- ); - } - if (projectId === 'bmi') { - return ( -
-
{out.bmi}
-
{out.category}
-
- ); - } - if (projectId === 'rps') { - const verdict = { win: '🎉 You win', lose: '😅 You lose', draw: '🤝 Draw' }[out.result] || out.result; - return ( -
-
You: {out.player} · CPU: {out.cpu}
-
{verdict}
-
- ); - } - if (projectId === 'madlibs') { - return
{out.story}
; - } - return
{JSON.stringify(out, null, 2)}
; -} - -// ---- Tic Tac Toe interactive ---- - -const EMPTY_BOARD = ['', '', '', '', '', '', '', '', '']; - -function TicTacToePlay() { - const [board, setBoard] = useState(EMPTY_BOARD); - const [status, setStatus] = useState('ongoing'); - const [winner, setWinner] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const reset = () => { setBoard(EMPTY_BOARD); setStatus('ongoing'); setWinner(null); setError(null); }; - - const play = async (idx) => { - if (board[idx] || status !== 'ongoing' || loading) return; - setError(null); - setLoading(true); - try { - const res = await window.PBP_API.call('tictactoe', { board, player: 'X', move: idx }); - setBoard(res.board); - setStatus(res.status); - setWinner(res.winner || null); - } catch (e) { - setError(e.message); - } finally { - setLoading(false); - } - }; - - const headline = status === 'win' - ? (winner === 'X' ? '🎉 You win' : '🤖 CPU wins') - : status === 'draw' ? '🤝 Draw' : 'Your move (X)'; - - return ( -
-
▶ Try it live · vs the bot
-
-
{headline}
- -
-
- {board.map((cell, i) => ( - - ))} -
- {error &&
⚠ {error}
} -
- ); -} - -// ---- Hangman interactive ---- - -const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); - -function HangmanPlay() { - const [seed, setSeed] = useState(() => Math.floor(Math.random() * 1e9)); - const [difficulty, setDifficulty] = useState('medium'); - const [guessed, setGuessed] = useState([]); - const [state, setState] = useState(null); // {mask, wrong, tries_left, max_tries, status, word_length, word?} - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const refresh = async (nextGuessed, nextDifficulty = difficulty, nextSeed = seed) => { - setError(null); - setLoading(true); - try { - const res = await window.PBP_API.call('hangman', { word_seed: nextSeed, guessed: nextGuessed, difficulty: nextDifficulty }); - setState(res); - } catch (e) { - setError(e.message); - } finally { - setLoading(false); - } - }; - - useEffect(() => { refresh([], difficulty, seed); /* eslint-disable-next-line */ }, []); - - const guess = (letter) => { - if (loading || !state || state.status !== 'ongoing' || guessed.includes(letter)) return; - const next = [...guessed, letter]; - setGuessed(next); - refresh(next); - }; - - return ( -
-
▶ Try it live · pick letters
-
- - -
- {state && ( - <> -
- {state.mask.split('').map((c, i) => {c === '_' ? '·' : c})} -
-
- {state.tries_left} / {state.max_tries} tries left - {state.wrong.length ? 'wrong: ' + state.wrong.join(' ') : ''} -
-
- {ALPHABET.map(l => { - const used = guessed.includes(l); - const hit = used && state.mask.includes(l); - return ( - - ); - })} -
- {state.status === 'win' &&
🎉 You got it!
} - {state.status === 'lose' &&
😅 Out of tries — the word was {state.word}
} - - )} - {error &&
⚠ {error}
} -
- ); -} - -window.StickerLogo = StickerLogo; -window.ScribbleSvg = ScribbleSvg; -window.HeroStickers = HeroStickers; -window.ProjectCard = ProjectCard; -window.ProjectModal = ProjectModal; diff --git a/website/tweaks-panel.jsx b/website/tweaks-panel.jsx deleted file mode 100644 index 79ccfe918..000000000 --- a/website/tweaks-panel.jsx +++ /dev/null @@ -1,568 +0,0 @@ - -// tweaks-panel.jsx -// Reusable Tweaks shell + form-control helpers. -// -// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode, -// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so -// individual prototypes don't re-roll it. Ships a consistent set of controls so you -// don't hand-draw , segmented radios, steppers, etc. -// -// Usage (in an HTML file that loads React + Babel): -// -// const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ -// "primaryColor": "#D97757", -// "palette": ["#D97757", "#29261b", "#f6f4ef"], -// "fontSize": 16, -// "density": "regular", -// "dark": false -// }/*EDITMODE-END*/; -// -// function App() { -// const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); -// return ( -//
-// Hello -// -// -// setTweak('fontSize', v)} /> -// setTweak('density', v)} /> -// -// setTweak('primaryColor', v)} /> -// setTweak('palette', v)} /> -// setTweak('dark', v)} /> -// -//
-// ); -// } -// -// ───────────────────────────────────────────────────────────────────────────── - -const __TWEAKS_STYLE = ` - .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px; - max-height:calc(100vh - 32px);display:flex;flex-direction:column; - transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right; - background:rgba(250,249,247,.78);color:#29261b; - -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%); - border:.5px solid rgba(255,255,255,.6);border-radius:14px; - box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18); - font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden} - .twk-hd{display:flex;align-items:center;justify-content:space-between; - padding:10px 8px 10px 14px;cursor:move;user-select:none} - .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em} - .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55); - width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1} - .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b} - .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px; - overflow-y:auto;overflow-x:hidden;min-height:0; - scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent} - .twk-body::-webkit-scrollbar{width:8px} - .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px} - .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px; - border:2px solid transparent;background-clip:content-box} - .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25); - border:2px solid transparent;background-clip:content-box} - .twk-row{display:flex;flex-direction:column;gap:5px} - .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px} - .twk-lbl{display:flex;justify-content:space-between;align-items:baseline; - color:rgba(41,38,27,.72)} - .twk-lbl>span:first-child{font-weight:500} - .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums} - - .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase; - color:rgba(41,38,27,.45);padding:10px 0 0} - .twk-sect:first-child{padding-top:0} - - .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px; - border:.5px solid rgba(0,0,0,.1);border-radius:7px; - background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none} - .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)} - select.twk-field{padding-right:22px; - background-image:url("data:image/svg+xml;utf8,"); - background-repeat:no-repeat;background-position:right 8px center} - - .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0; - border-radius:999px;background:rgba(0,0,0,.12);outline:none} - .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none; - width:14px;height:14px;border-radius:50%;background:#fff; - border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default} - .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%; - background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default} - - .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px; - background:rgba(0,0,0,.06);user-select:none} - .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px; - background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12); - transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s} - .twk-seg.dragging .twk-seg-thumb{transition:none} - .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0; - background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px; - border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2; - overflow-wrap:anywhere} - - .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px; - background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0} - .twk-toggle[data-on="1"]{background:#34c759} - .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%; - background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s} - .twk-toggle[data-on="1"] i{transform:translateX(14px)} - - .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px; - border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)} - .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize; - user-select:none;padding-right:8px} - .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent; - font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0; - outline:none;color:inherit;-moz-appearance:textfield} - .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{ - -webkit-appearance:none;margin:0} - .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)} - - .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px; - background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default} - .twk-btn:hover{background:rgba(0,0,0,.88)} - .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit} - .twk-btn.secondary:hover{background:rgba(0,0,0,.1)} - - .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px; - border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default; - background:transparent;flex-shrink:0} - .twk-swatch::-webkit-color-swatch-wrapper{padding:0} - .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px} - .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px} - - .twk-chips{display:flex;gap:6px} - .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px; - padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default; - box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06); - transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s} - .twk-chip:hover{transform:translateY(-1px); - box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)} - .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85), - 0 2px 6px rgba(0,0,0,.15)} - .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%; - display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)} - .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)} - .twk-chip>span>i:first-child{box-shadow:none} - .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px; - filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))} -`; - -// ── useTweaks ─────────────────────────────────────────────────────────────── -// Single source of truth for tweak values. setTweak persists via the host -// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk). -function useTweaks(defaults) { - const [values, setValues] = React.useState(defaults); - // Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a - // useState-style call doesn't write a "[object Object]" key into the persisted - // JSON block. - const setTweak = React.useCallback((keyOrEdits, val) => { - const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null - ? keyOrEdits : { [keyOrEdits]: val }; - setValues((prev) => ({ ...prev, ...edits })); - window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*'); - // Same-window signal so in-page listeners (deck-stage rail thumbnails) - // can react — the parent message only reaches the host, not peers. - window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits })); - }, []); - return [values, setTweak]; -} - -// ── TweaksPanel ───────────────────────────────────────────────────────────── -// Floating shell. Registers the protocol listener BEFORE announcing -// availability — if the announce ran first, the host's activate could land -// before our handler exists and the toolbar toggle would silently no-op. -// The close button posts __edit_mode_dismissed so the host's toolbar toggle -// flips off in lockstep; the host echoes __deactivate_edit_mode back which -// is what actually hides the panel. -function TweaksPanel({ title = 'Tweaks', noDeckControls = false, children }) { - const [open, setOpen] = React.useState(false); - const dragRef = React.useRef(null); - // Auto-inject a rail toggle when a is on the page. The - // toggle drives the deck's per-viewer _railVisible via window message; - // state is mirrored from the same localStorage key the deck reads so - // the control reflects reality across reloads. The mechanism is the - // message — authors who want custom placement can post it directly - // and pass noDeckControls to suppress this one. - const hasDeckStage = React.useMemo( - () => typeof document !== 'undefined' && !!document.querySelector('deck-stage'), - [], - ); - // deck-stage enables its rail in connectedCallback, but this panel can - // mount before that element has upgraded. The initial read catches the - // common case; the listener covers mounting first. (Older deck-stage.js - // copies still wait for the host's __omelette_rail_enabled postMessage — - // same listener handles those.) - const [railEnabled, setRailEnabled] = React.useState( - () => hasDeckStage && !!document.querySelector('deck-stage')?._railEnabled, - ); - React.useEffect(() => { - if (!hasDeckStage || railEnabled) return undefined; - const onMsg = (e) => { - if (e.data && e.data.type === '__omelette_rail_enabled') setRailEnabled(true); - }; - window.addEventListener('message', onMsg); - return () => window.removeEventListener('message', onMsg); - }, [hasDeckStage, railEnabled]); - const [railVisible, setRailVisible] = React.useState(() => { - try { return localStorage.getItem('deck-stage.railVisible') !== '0'; } catch (e) { return true; } - }); - const toggleRail = (on) => { - setRailVisible(on); - window.postMessage({ type: '__deck_rail_visible', on }, '*'); - }; - const offsetRef = React.useRef({ x: 16, y: 16 }); - const PAD = 16; - - const clampToViewport = React.useCallback(() => { - const panel = dragRef.current; - if (!panel) return; - const w = panel.offsetWidth, h = panel.offsetHeight; - const maxRight = Math.max(PAD, window.innerWidth - w - PAD); - const maxBottom = Math.max(PAD, window.innerHeight - h - PAD); - offsetRef.current = { - x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)), - y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)), - }; - panel.style.right = offsetRef.current.x + 'px'; - panel.style.bottom = offsetRef.current.y + 'px'; - }, []); - - React.useEffect(() => { - if (!open) return; - clampToViewport(); - if (typeof ResizeObserver === 'undefined') { - window.addEventListener('resize', clampToViewport); - return () => window.removeEventListener('resize', clampToViewport); - } - const ro = new ResizeObserver(clampToViewport); - ro.observe(document.documentElement); - return () => ro.disconnect(); - }, [open, clampToViewport]); - - React.useEffect(() => { - const onMsg = (e) => { - const t = e?.data?.type; - if (t === '__activate_edit_mode') setOpen(true); - else if (t === '__deactivate_edit_mode') setOpen(false); - }; - window.addEventListener('message', onMsg); - window.parent.postMessage({ type: '__edit_mode_available' }, '*'); - return () => window.removeEventListener('message', onMsg); - }, []); - - const dismiss = () => { - setOpen(false); - window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*'); - }; - - const onDragStart = (e) => { - const panel = dragRef.current; - if (!panel) return; - const r = panel.getBoundingClientRect(); - const sx = e.clientX, sy = e.clientY; - const startRight = window.innerWidth - r.right; - const startBottom = window.innerHeight - r.bottom; - const move = (ev) => { - offsetRef.current = { - x: startRight - (ev.clientX - sx), - y: startBottom - (ev.clientY - sy), - }; - clampToViewport(); - }; - const up = () => { - window.removeEventListener('mousemove', move); - window.removeEventListener('mouseup', up); - }; - window.addEventListener('mousemove', move); - window.addEventListener('mouseup', up); - }; - - if (!open) return null; - return ( - <> - -
-
- {title} - -
-
- {children} - {hasDeckStage && railEnabled && !noDeckControls && ( - - - - )} -
-
- - ); -} - -// ── Layout helpers ────────────────────────────────────────────────────────── - -function TweakSection({ label, children }) { - return ( - <> -
{label}
- {children} - - ); -} - -function TweakRow({ label, value, children, inline = false }) { - return ( -
-
- {label} - {value != null && {value}} -
- {children} -
- ); -} - -// ── Controls ──────────────────────────────────────────────────────────────── - -function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) { - return ( - - onChange(Number(e.target.value))} /> - - ); -} - -function TweakToggle({ label, value, onChange }) { - return ( -
-
{label}
- -
- ); -} - -function TweakRadio({ label, value, options, onChange }) { - const trackRef = React.useRef(null); - const [dragging, setDragging] = React.useState(false); - // The active value is read by pointer-move handlers attached for the lifetime - // of a drag — ref it so a stale closure doesn't fire onChange for every move. - const valueRef = React.useRef(value); - valueRef.current = value; - - // Segments wrap mid-word once per-segment width runs out. The track is - // ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px - // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2 - // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall - // back to a dropdown rather than wrap. - const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length; - const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0); - const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0); - if (!fitsAsSegments) { - // onChange(e.target.value)}> - {options.map((o) => { - const v = typeof o === 'object' ? o.value : o; - const l = typeof o === 'object' ? o.label : o; - return ; - })} - - - ); -} - -function TweakText({ label, value, placeholder, onChange }) { - return ( - - onChange(e.target.value)} /> - - ); -} - -function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) { - const clamp = (n) => { - if (min != null && n < min) return min; - if (max != null && n > max) return max; - return n; - }; - const startRef = React.useRef({ x: 0, val: 0 }); - const onScrubStart = (e) => { - e.preventDefault(); - startRef.current = { x: e.clientX, val: value }; - const decimals = (String(step).split('.')[1] || '').length; - const move = (ev) => { - const dx = ev.clientX - startRef.current.x; - const raw = startRef.current.val + dx * step; - const snapped = Math.round(raw / step) * step; - onChange(clamp(Number(snapped.toFixed(decimals)))); - }; - const up = () => { - window.removeEventListener('pointermove', move); - window.removeEventListener('pointerup', up); - }; - window.addEventListener('pointermove', move); - window.addEventListener('pointerup', up); - }; - return ( -
- {label} - onChange(clamp(Number(e.target.value)))} /> - {unit && {unit}} -
- ); -} - -// Relative-luminance contrast pick — checkmarks drawn over a swatch need to -// read on both #111 and #fafafa without per-option configuration. Hex input -// only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light". -function __twkIsLight(hex) { - const h = String(hex).replace('#', ''); - const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0'); - const n = parseInt(x.slice(0, 6), 16); - if (Number.isNaN(n)) return true; - const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; - return r * 299 + g * 587 + b * 114 > 148000; -} - -const __TwkCheck = ({ light }) => ( - -); - -// TweakColor — curated color/palette picker. Each option is either a single -// hex string or an array of 1-5 hex strings; the card adapts — a lone color -// renders solid, a palette renders colors[0] as the hero (left ~2/3) with the -// rest stacked in a sharp column on the right. onChange emits the -// option in the shape it was passed (string stays string, array stays array). -// Without options it falls back to the native color input for back-compat. -function TweakColor({ label, value, options, onChange }) { - if (!options || !options.length) { - return ( -
-
{label}
- onChange(e.target.value)} /> -
- ); - } - // Native emits lowercase hex per the HTML spec, so - // compare case-insensitively. String() guards JSON.stringify(undefined), - // which returns the primitive undefined (no .toLowerCase). - const key = (o) => String(JSON.stringify(o)).toLowerCase(); - const cur = key(value); - return ( - -
- {options.map((o, i) => { - const colors = Array.isArray(o) ? o : [o]; - const [hero, ...rest] = colors; - const sup = rest.slice(0, 4); - const on = key(o) === cur; - return ( - - ); - })} -
-
- ); -} - -function TweakButton({ label, onClick, secondary = false }) { - return ( - - ); -} - -Object.assign(window, { - useTweaks, TweaksPanel, TweakSection, TweakRow, - TweakSlider, TweakToggle, TweakRadio, TweakSelect, - TweakText, TweakNumber, TweakColor, TweakButton, -}); From f65d55bcb6b93df07c53da52408bbfa7f37cda21 Mon Sep 17 00:00:00 2001 From: Mrinank-Bhowmick <77621953+Mrinank-Bhowmick@users.noreply.github.com> Date: Thu, 21 May 2026 18:35:19 +0530 Subject: [PATCH 03/14] fixed links --- web/src/app/globals.css | 6 +++++ web/src/app/playground/[id]/page.tsx | 4 +++- web/src/components/Playground.tsx | 13 +++++++++++ web/src/lib/data.ts | 34 ++++++++++++++++++---------- web/src/types.ts | 2 ++ 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/web/src/app/globals.css b/web/src/app/globals.css index ef7eb64c3..84eb4fbfa 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -850,6 +850,12 @@ a { color: inherit; } font-size: 12px; color: var(--pg-muted); margin-left: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.pg-credit { + display: inline-flex; align-items: center; gap: 5px; + margin-top: 6px; font-size: 11px; font-weight: 600; + color: var(--pg-muted); text-decoration: none; +} +.pg-credit:hover { color: var(--pg-accent); text-decoration: underline; } .pg-toolbar-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; } .pg-status { display: flex; align-items: center; gap: 8px; diff --git a/web/src/app/playground/[id]/page.tsx b/web/src/app/playground/[id]/page.tsx index c70fb5685..4e932243d 100644 --- a/web/src/app/playground/[id]/page.tsx +++ b/web/src/app/playground/[id]/page.tsx @@ -5,7 +5,7 @@ import { notFound } from "next/navigation"; import PlaygroundNav from "@/components/PlaygroundNav"; import PlaygroundSidebar from "@/components/PlaygroundSidebar"; import Playground, { type PlaygroundProject } from "@/components/Playground"; -import { PROJECTS, getProject } from "@/lib/data"; +import { PROJECTS, getProject, projectFolderUrl } from "@/lib/data"; interface Params { params: Promise<{ id: string }>; @@ -64,6 +64,8 @@ export default async function ProjectPlaygroundPage({ params }: Params) { deps: p.deps, lines: p.lines, blurb: p.blurb, + author: p.author, + folderUrl: projectFolderUrl(p), }; return ( diff --git a/web/src/components/Playground.tsx b/web/src/components/Playground.tsx index 78f70c466..8f738f9bc 100644 --- a/web/src/components/Playground.tsx +++ b/web/src/components/Playground.tsx @@ -47,6 +47,9 @@ export interface PlaygroundProject { deps: string; lines: number; blurb: string; + /** Contributor handle + link to the project's source folder on GitHub. */ + author?: string; + folderUrl?: string; } const SAB_SIZE = 8 + 8192; @@ -377,6 +380,16 @@ export default function Playground({ · {project.blurb} )} + {project.author && project.folderUrl && ( + + 🧑‍💻 Original project by @{project.author} — view source on GitHub ↗ + + )}
diff --git a/web/src/lib/data.ts b/web/src/lib/data.ts index e0a59e642..d55b07b5d 100644 --- a/web/src/lib/data.ts +++ b/web/src/lib/data.ts @@ -14,19 +14,20 @@ import type { // `author` is the GitHub handle of the contributor who first added the // project folder (resolved from git history via the GitHub API). +// `repoPath` is that project's folder path under projects/ in the repo. export const PROJECTS: Project[] = [ - { id: "snake", name: "Snake Game", cat: "games", blurb: "Slither, eat, grow longer. The classic.", lines: 92, deps: "pygame", emoji: "🐍", author: "EbrG786" }, - { id: "tictactoe", name: "Tic-Tac-Toe", cat: "games", blurb: "Three in a row, terminal showdown.", lines: 64, deps: "stdlib", emoji: "⭕", author: "Mrinank-Bhowmick", runnable: true, playground: true }, - { id: "hangman", name: "Hangman", cat: "games", blurb: "Guess letters before the gallows complete.", lines: 78, deps: "stdlib", emoji: "🪢", author: "Mrinank-Bhowmick", runnable: true, playground: true }, - { id: "flappy", name: "Flappy Bird", cat: "games", blurb: "Tap to flap. Avoid the pipes. Die. Repeat.", lines: 140, deps: "pygame", emoji: "🐦", author: "Nikita0509" }, - { id: "rps", name: "Rock · Paper · Scissors", cat: "games", blurb: "Best of three vs the random module.", lines: 48, deps: "stdlib", emoji: "✊", author: "ZackeryRSmith", runnable: true, playground: true }, - { id: "calc", name: "Calculator", cat: "tools", blurb: "Tkinter buttons. Operator precedence. Beep.", lines: 110, deps: "tkinter", emoji: "🧮", author: "shubham7668" }, - { id: "bmi", name: "BMI Calculator", cat: "tools", blurb: "Height + weight → a single questionable number.", lines: 36, deps: "stdlib", emoji: "⚖️", author: "Mrinank-Bhowmick", runnable: true, playground: true }, - { id: "weather", name: "Weather App", cat: "web", blurb: "OpenWeather API, your city, today.", lines: 88, deps: "requests", emoji: "☁️", author: "srujan-landeri" }, - { id: "qr", name: "QR Code Generator", cat: "tools", blurb: "Text in, scannable square out.", lines: 22, deps: "qrcode", emoji: "▦", author: "SubramanyaKS", runnable: true, playground: true }, - { id: "pwd", name: "Password Generator", cat: "tools", blurb: "Random, strong, immediately forgotten.", lines: 30, deps: "secrets", emoji: "🔐", author: "jrbublitz", playground: true }, - { id: "yt", name: "YouTube Downloader", cat: "web", blurb: "pytube wrapper. Save the lecture.", lines: 54, deps: "pytube", emoji: "⬇", author: "vk0812" }, - { id: "madlibs", name: "Madlibs Generator", cat: "fun", blurb: "Fill in nouns. Receive nonsense. Laugh.", lines: 40, deps: "stdlib", emoji: "✏️", author: "ZackeryRSmith", runnable: true, playground: true }, + { id: "snake", name: "Snake Game", cat: "games", blurb: "Slither, eat, grow longer. The classic.", lines: 92, deps: "pygame", emoji: "🐍", author: "EbrG786", repoPath: "Snake Game" }, + { id: "tictactoe", name: "Tic-Tac-Toe", cat: "games", blurb: "Three in a row, terminal showdown.", lines: 64, deps: "stdlib", emoji: "⭕", author: "Mrinank-Bhowmick", repoPath: "Tic-Tac-Toe", runnable: true, playground: true }, + { id: "hangman", name: "Hangman", cat: "games", blurb: "Guess letters before the gallows complete.", lines: 78, deps: "stdlib", emoji: "🪢", author: "Mrinank-Bhowmick", repoPath: "Hangman", runnable: true, playground: true }, + { id: "flappy", name: "Flappy Bird", cat: "games", blurb: "Tap to flap. Avoid the pipes. Die. Repeat.", lines: 140, deps: "pygame", emoji: "🐦", author: "Nikita0509", repoPath: "Flappybird_game" }, + { id: "rps", name: "Rock · Paper · Scissors", cat: "games", blurb: "Best of three vs the random module.", lines: 48, deps: "stdlib", emoji: "✊", author: "ZackeryRSmith", repoPath: "Rock_Paper_Scissors", runnable: true, playground: true }, + { id: "calc", name: "Calculator", cat: "tools", blurb: "Tkinter buttons. Operator precedence. Beep.", lines: 110, deps: "tkinter", emoji: "🧮", author: "shubham7668", repoPath: "Calculator" }, + { id: "bmi", name: "BMI Calculator", cat: "tools", blurb: "Height + weight → a single questionable number.", lines: 36, deps: "stdlib", emoji: "⚖️", author: "Mrinank-Bhowmick", repoPath: "BMI_calculator", runnable: true, playground: true }, + { id: "weather", name: "Weather App", cat: "web", blurb: "OpenWeather API, your city, today.", lines: 88, deps: "requests", emoji: "☁️", author: "srujan-landeri", repoPath: "API Based Weather Report" }, + { id: "qr", name: "QR Code Generator", cat: "tools", blurb: "Text in, scannable square out.", lines: 22, deps: "qrcode", emoji: "▦", author: "SubramanyaKS", repoPath: "QRCode-Generator", runnable: true, playground: true }, + { id: "pwd", name: "Password Generator", cat: "tools", blurb: "Random, strong, immediately forgotten.", lines: 30, deps: "secrets", emoji: "🔐", author: "jrbublitz", repoPath: "Password Projects/Password Generator", playground: true }, + { id: "yt", name: "YouTube Downloader", cat: "web", blurb: "pytube wrapper. Save the lecture.", lines: 54, deps: "pytube", emoji: "⬇", author: "vk0812", repoPath: "YouTube Video Downloader" }, + { id: "madlibs", name: "Madlibs Generator", cat: "fun", blurb: "Fill in nouns. Receive nonsense. Laugh.", lines: 40, deps: "stdlib", emoji: "✏️", author: "ZackeryRSmith", repoPath: "Madlibs Generator", runnable: true, playground: true }, ]; export const CATEGORIES: Category[] = [ @@ -74,3 +75,12 @@ export const REPO_URL = export function getProject(id: string): Project | undefined { return PROJECTS.find((p) => p.id === id); } + +/** GitHub URL of a project's source folder in the repo. */ +export function projectFolderUrl(p: Project): string { + const path = p.repoPath + .split("/") + .map((seg) => encodeURIComponent(seg)) + .join("/"); + return `${REPO_URL}/tree/main/projects/${path}`; +} diff --git a/web/src/types.ts b/web/src/types.ts index d0d31adb2..672b17699 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -12,6 +12,8 @@ export interface Project { emoji: string; /** GitHub handle of the contributor who first added this project. */ author: string; + /** Folder path under projects/ in the repo (for the source link). */ + repoPath: string; /** Has a server-backed "Try it" demo via the Cloudflare Worker. */ runnable?: boolean; /** Can run fully in-browser via Pyodide (pure-stdlib console programs). */ From 100cdfcff3222f851f860177ef927af34aa99592 Mon Sep 17 00:00:00 2001 From: Mrinank-Bhowmick <77621953+Mrinank-Bhowmick@users.noreply.github.com> Date: Fri, 22 May 2026 00:25:01 +0530 Subject: [PATCH 04/14] web overhaul: in-browser playground, tutorials, pnpm migration web/ (pyBegin site): - Playground: 60 annotated, self-contained tutorial versions of the runnable projects (originals under projects/ left untouched); rendered README panel; draggable sidebar / editor / console / README splitters; lucide-react icons replacing emoji glyphs. - Per-project contextual emojis and contributor credit (GitHub handle + avatar), generated into catalog.json / authors.json by scripts/. - Fixes: AES256 needs pycryptodome; jokenpo / blind-auction / pig-latin made self-contained; first-run Pyodide worker hang. - Migrated npm -> pnpm with supply-chain hardening: pinned packageManager, minimumReleaseAge, blocked install scripts, postcss security override. projects/: README documentation pass across the catalog; ToDoList Pyodide-runnable verdict corrected. Removed stale root config (wrangler.jsonc, pyproject.toml, uv.lock) and .wrangler cache. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 2 + .wrangler/cache/pages.json | 4 - .wrangler/cache/wrangler-account.json | 6 - AGENTS.md | 48 + projects/AES256/AES256.py | 12 +- projects/AES256/README.md | 34 +- projects/API Based Weather Report/README.md | 5 + projects/AWS_s3_upload/readme.md | 6 +- .../README.md | 23 + projects/Advisor/README.md | 22 + projects/Alarm Clock/README.md | 26 + .../README.md | 25 + projects/AnalogClock/readme.md | 27 +- projects/Audio Converter/README.md | 5 + projects/AudioAPI/ReadMe.md | 5 + projects/AudioRecorder/README.md | 19 + projects/Audiobook/Readme.md | 5 + projects/AutoGui/ReadMe.md | 5 + projects/BFS visualizer/Readme.md | 6 +- projects/BMI WebApp/README.md | 18 + projects/BMI_calculator/Readme.md | 5 + projects/Battleship/README.md | 5 + projects/Battleship/main.py | 9 - projects/Bigram_Autocomplete/README.md | 17 + projects/Bitcoin Mining/Readme.md | 5 + projects/BlackJack/ReadMe.md | 5 + projects/Blender_tools/README.md | 5 + projects/Blind Auction/README.md | 7 + projects/Blind Auction/main.py | 6 +- projects/Blind_Auction/README.md | 17 + projects/Blind_Auction/main.py | 3 - projects/Book Data Extractor/README.md | 6 +- projects/Browser/README.md | 19 + projects/Budget-manager/README.md | 17 + projects/CRUD-with-postgresql/README.md | 4 + projects/Calculate Age/README.md | 5 + projects/Calculator-GUI-With-Python/README.md | 18 + projects/Calculator/Readme.md | 5 + projects/Calculator/main.py | 8 +- projects/Calendar/README.md | 17 + projects/Captcha_Genrator/README.md | 5 + projects/Card Game/Readme.md | 6 +- projects/Chat Application/readme.md | 5 + projects/Chat-GPT-Discord-Bot/README.md | 5 + projects/Chess/README.md | 18 + projects/Code_Reviewer/README.md | 21 + projects/Coin Flip/README.md | 4 + projects/Coin Flip/coinflip.py | 3 - projects/Collatz_Conjecture/README.md | 5 +- projects/Comics_Scraper/README.md | 20 + projects/Connect Four/README.md | 19 + .../README.md" | 21 + projects/Countdown/README.md | 17 + projects/CustomEncryptionDecryption/README.md | 4 + projects/Data Entry Automation/Readme.md | 5 +- projects/Data_Abstractor/README.md | 4 + projects/Desktop Weather Notifier/README.md | 21 + projects/Desktop-Notification/README.md | 18 + .../Diabetes Monitoring Dashboard/README.md | 4 + projects/Dice Simulator/Readme.md | 4 + projects/Dice-Roll-Simulator/README.md | 4 + projects/Discord Bot/README.md | 20 + projects/DnD Dice/README.md | 19 + projects/Drowsiness Detector/Readme.md | 4 + projects/Encryptor and Decryptor/README.md | 17 + projects/English Thesaurus/README.md | 19 + projects/Expense-Tracker/README.md | 4 + projects/Eye Blink Detection/Readme.md | 4 + projects/Fidget Spinner Game/README.md | 17 + projects/File_Organizer/README.md | 19 + projects/Find_imbd_rating/README.md | 20 + projects/Flappybird_game/README.md | 18 + projects/Full_Calendar/README.md | 17 + projects/GPA-Calculator/README.md | 17 + .../README.md" | 18 + projects/Game of Cricket/README.md | 17 + projects/Gpt-And-Langchain/README.md | 4 + projects/Gradient-Image/README.md | 19 + projects/Guess Number/README.md | 26 + projects/Guess The Word/README.md | 17 + projects/HandCricket/Readme.md | 6 +- projects/HandTrack/README.md | 21 + projects/Hangman/README.md | 17 + projects/Higher-Lower/README.md | 17 + projects/Historical Data Breaches/README.md | 6 +- projects/IP Blacklist Checker/README.md | 19 + projects/IPv4_Calculator-main/README.md | 13 + projects/Image Manipulation/README.md | 18 + projects/Image Sketcher/README.md | 19 + projects/Image Toonification/Readme.md | 4 + projects/Image compressor/README.md | 18 + projects/Image to PDF Converter/README.md | 18 + .../README.md | 21 + projects/Image-Upscale/README.md | 19 + projects/Image-to-art/Readme.md | 3 + projects/ImagegenChatbot/readme.md | 4 + projects/Instagram Post Creation/README.md | 19 + projects/Internet Speed Tester/README.md | 3 + projects/Internet-speed-test/README.md | 3 + projects/Inverse Matrix Calculator/README.md | 16 + projects/JARVIS.PY/jarvisreadme.md | 5 +- projects/Jokenpo/README.md | 16 + projects/Jokey/README.md | 17 + projects/KbdXylo/README.md | 17 + projects/Language-Translate/README.md | 3 + .../Language_learning_assistant/README.md | 3 + projects/Live AQI/README.md | 19 + projects/Loan Calculator/README.md | 5 +- projects/Location Search App (GUI)/README.md | 17 + projects/Lyrics-Extractor/README.md | 19 + projects/ML-Notebooks_Beginners/README.md | 22 + projects/MQTT Client/README.md | 5 +- projects/Madlibs Generator/README.md | 16 + projects/Make-API/README.md | 19 + .../readme.md | 14 +- projects/Mastermind/README.md | 16 + projects/Medium Article Reader/README.md | 19 + projects/Merge PDFs/README.md | 17 + projects/Message-Spam/README.md | 5 +- projects/Minecraft-in-Python-main/README.md | 20 + .../README.md | 4 + projects/Mobile Document Scanner/README.md | 21 + projects/Mongo CRUD/README.md | 21 + projects/Morse-Code-Translator/README.md | 17 + projects/MorseCode Translator/README.md | 17 + projects/MotivateBot/README.md | 23 + projects/Movie recommendation/README.md | 22 + projects/MovieApi_and_ML/README.md | 20 + projects/Music Player/README.md | 18 + projects/NASA-APOD/README.md | 4 + projects/Neurons/README.md | 17 + projects/Neurons/main.py | 11 - projects/Number Guessing App/README.md | 17 + projects/OTP-Verfication-System/README.md | 19 + projects/Online Trivia/README.md | 18 + .../README.md | 19 + projects/Organize_Directory/README.md | 17 + projects/Othello/README.md | 4 + projects/Otp_Generator/README.md | 17 + projects/PDF_Reader/README.md | 23 + projects/PONG/README.md | 20 + .../Password Breach Frequency/README.md | 5 +- .../Password Generator/README.md | 17 + .../Password Hashing/readme.md | 5 +- .../Password Meter/README.md | 17 + .../Password_manager/README.md | 19 + projects/Password Projects/README.md | 4 + .../WiFi Password Generator/README.md | 19 + .../strong-password-detector/README.md | 17 + projects/Personal-Finance-tracker/README.md | 17 + projects/Pig_latin/README.md | 17 + projects/Pokemon Battle/README.md | 18 + projects/Port_Scaner/README.md | 18 +- projects/Print_Colored_Text/README.md | 18 + projects/ProjectEuler/README.md | 16 + projects/Python Banking System/readme.md | 14 + projects/Python story generator/README.md | 19 + projects/QRCode Scanner/Readme.md | 10 + projects/QRCode-Generator/README.md | 19 + projects/Qt5_YouTube/README.md | 19 + projects/QuickWordCloud/README.md | 4 + projects/Quiz Game/README.md | 18 + projects/Random-Quote-Generator/README.md | 4 + projects/Rename_Images/README.md | 17 + projects/Resize_Image/README.md | 18 + projects/RestrauntAPI/README.md | 19 + projects/Rock_Paper_Scissors/README.md | 17 + projects/Roll_A_dice/README.md | 21 + projects/Rubik-tracking/README.md | 20 + projects/SMS_ChatBot/README.md | 5 +- projects/Sales Optimizer/Readme.md | 13 +- projects/Scientific-Calculator/ReadMe.md | 14 + projects/ScreenRecorder/README.md | 11 + projects/Seek_with_hand_track/README.md | 21 + projects/Selfie_with_Python/README.md | 19 + projects/Send-Email/README.md | 19 + .../README.md | 4 + projects/SketchifyMe/README.md | 16 +- projects/Skycast/README.md | 5 +- projects/Slice-Audio/README.md | 4 + projects/Snake Game/README.md | 18 + .../Social_media_content_creation/README.md | 4 + projects/Socket/README.md | 4 + projects/SongsMashup/README.md | 18 + projects/Space Shooter/README.md | 18 + projects/Speed-Type-test/README.md | 18 + projects/Split_Tip/README.md | 17 + projects/Spotify Player/README.md | 20 + projects/Stock-Market-Dashboard/README.md | 18 + projects/Subnetting Flsm/README.md | 14 + projects/Subtitle_synchronizer.py/README.md | 19 + projects/Sudoku-Solver/README.md | 18 + projects/Sudoku_solver/README.md | 17 + projects/TennisTournamentSim/README.md | 17 + projects/Tesla/README.md | 18 + projects/Tetris Game/README.md | 18 + projects/Text Editor/README.md | 24 + projects/Text Summarizer/READme.md | 4 + projects/Text to Speech/readme.md | 4 + .../README.md | 21 + projects/TextDetection/README.md | 20 + projects/Text_to_SpreadSheet/README.md | 18 + projects/Tic-Tac-Toe/README.md | 24 + .../Tic-Tac-Toe-Terminal/README.md | 17 + projects/Tic-Tac-Toe/TicTacToe-GUI/README.md | 18 + projects/TicTacToe-TylerPear/README.md | 17 + projects/Tile Matching/README.md | 17 + projects/Timer/README.md | 17 + projects/Tkinter/README.md | 18 + projects/ToDoList/README.md | 9 +- projects/Turtle Pattern/README.md | 17 + projects/Turtle_Graphics/README.md | 17 + projects/Twitter-Bot/README.md | 4 + projects/Type Racer Game/README.md | 17 + projects/Video Reversal/README.md | 4 + projects/Video-subtitle-generator/readme.md | 5 +- projects/Voice-to-Text/README.md | 20 + projects/Watermarker/README.md | 4 + projects/Weather/README.md | 20 + .../README.md | 5 +- projects/WebButtonSimpelGUI/README.md | 4 + projects/Website Blocker/readme.md | 5 +- .../README.md | 4 + projects/Windows Logo/README.md | 17 + projects/Wine_quality_predictor/Readme.md | 5 +- projects/Word_Predictor/README.md | 20 + projects/Worksheet_to_text/README.md | 18 + .../World-Cup-Player-Comparison/README.md | 22 + projects/YouTube Video Downloader/README.md | 18 + projects/bittorrent-downloader/README.md | 20 + projects/caesar_cipher/README.md | 17 + projects/car game/README.md | 18 + projects/career-guide-bot/readme.md | 5 + projects/character-picture-grid/README.md | 17 + projects/chore-assignment-emailer/README.md | 19 + projects/comma-code/README.md | 17 + projects/computer-algebra/README.md | 4 + projects/currency converter/README.md | 19 + projects/custom-invitations/README.md | 18 + projects/custom-seating-cards/README.md | 18 + projects/dictionary.com-scraper/README.md | 19 + projects/duplicate_search/README.md | 19 + projects/excel-to-csv-converter/README.md | 19 + projects/facebook_video_downloader/README.md | 4 + projects/facerecoginition/README.md | 20 + projects/fantasy-game-inventory/README.md | 17 + projects/fill-gaps/README.md | 17 + projects/find-unneeded-files/README.md | 17 + projects/game-snake_water_gun/README.md | 4 + projects/goodreads-quotes-scraper/README.md | 19 + projects/healthmanagementsystem/README.md | 17 + projects/hill_cipher/Readme.md | 4 + projects/image-site-downloader/README.md | 19 + projects/indeed-scraper/README.md | 17 + projects/instant-messenger-bot/README.md | 17 + projects/inventory management/README.md | 19 + projects/link-verification/README.md | 17 + projects/linkedin-scrape/README.md | 19 + projects/looking-busy/README.md | 17 + projects/love-calculator/README.md | 16 + projects/maths/README.md | 16 + projects/minesweeper/README.md | 17 + projects/movie-rater/README.md | 5 +- projects/multiplayer_socket/README.md | 20 + projects/pdf-paranoia/README.md | 18 + projects/pdf_to_text/README.md | 5 +- projects/ping_pong/README.md | 17 + projects/prettified-stopwatch/README.md | 19 + projects/proxy-scrapper/README.md | 11 +- .../README.md | 20 + projects/reciept generator/README.md | 8 + projects/reddit-scraper/README.md | 19 + projects/regex-search/README.md | 17 + projects/regex-strip/README.md | 17 + projects/reuters-scraper/README.md | 20 + projects/scheduledShutdown/README.md | 17 + projects/scrap-ycombinator/README.md | 21 + projects/servo motor classifier/Readme.md | 17 +- projects/snake_water_gun_game/README.md | 18 + projects/space_battle/README.md | 18 + projects/sponge-bob/README.md | 17 + projects/student-management-system/README.md | 4 + projects/takeImage/README.md | 4 + projects/text-translate/README.md | 5 +- projects/todo.md | 293 ++ projects/url_shortener/README.md | 5 +- projects/video_transcoder_project/README.md | 4 + projects/web-crawler(movie extract)/README.md | 23 + projects/what-for-dinner/README.md | 18 + projects/xls_to_xlsx/README.md | 4 + pyproject.toml | 9 - python_modules/.synced | 1 - python_modules/PIL/AvifImagePlugin.py | 291 -- python_modules/PIL/BdfFontFile.py | 122 - python_modules/PIL/BlpImagePlugin.py | 497 -- python_modules/PIL/BmpImagePlugin.py | 515 -- python_modules/PIL/BufrStubImagePlugin.py | 75 - python_modules/PIL/ContainerIO.py | 173 - python_modules/PIL/CurImagePlugin.py | 75 - python_modules/PIL/DcxImagePlugin.py | 83 - python_modules/PIL/DdsImagePlugin.py | 624 --- python_modules/PIL/EpsImagePlugin.py | 476 -- python_modules/PIL/ExifTags.py | 382 -- python_modules/PIL/FitsImagePlugin.py | 152 - python_modules/PIL/FliImagePlugin.py | 178 - python_modules/PIL/FontFile.py | 134 - python_modules/PIL/FpxImagePlugin.py | 257 - python_modules/PIL/FtexImagePlugin.py | 114 - python_modules/PIL/GbrImagePlugin.py | 103 - python_modules/PIL/GdImageFile.py | 102 - python_modules/PIL/GifImagePlugin.py | 1213 ----- python_modules/PIL/GimpGradientFile.py | 149 - python_modules/PIL/GimpPaletteFile.py | 72 - python_modules/PIL/GribStubImagePlugin.py | 75 - python_modules/PIL/Hdf5StubImagePlugin.py | 75 - python_modules/PIL/IcnsImagePlugin.py | 411 -- python_modules/PIL/IcoImagePlugin.py | 381 -- python_modules/PIL/ImImagePlugin.py | 389 -- python_modules/PIL/Image.py | 4245 ----------------- python_modules/PIL/ImageChops.py | 311 -- python_modules/PIL/ImageCms.py | 1123 ----- python_modules/PIL/ImageColor.py | 320 -- python_modules/PIL/ImageDraw.py | 1232 ----- python_modules/PIL/ImageDraw2.py | 243 - python_modules/PIL/ImageEnhance.py | 113 - python_modules/PIL/ImageFile.py | 922 ---- python_modules/PIL/ImageFilter.py | 604 --- python_modules/PIL/ImageFont.py | 1339 ------ python_modules/PIL/ImageGrab.py | 196 - python_modules/PIL/ImageMath.py | 368 -- python_modules/PIL/ImageMode.py | 92 - python_modules/PIL/ImageMorph.py | 265 - python_modules/PIL/ImageOps.py | 745 --- python_modules/PIL/ImagePalette.py | 286 -- python_modules/PIL/ImagePath.py | 20 - python_modules/PIL/ImageQt.py | 220 - python_modules/PIL/ImageSequence.py | 86 - python_modules/PIL/ImageShow.py | 362 -- python_modules/PIL/ImageStat.py | 160 - python_modules/PIL/ImageTk.py | 266 -- python_modules/PIL/ImageTransform.py | 136 - python_modules/PIL/ImageWin.py | 247 - python_modules/PIL/ImtImagePlugin.py | 103 - python_modules/PIL/IptcImagePlugin.py | 250 - python_modules/PIL/Jpeg2KImagePlugin.py | 442 -- python_modules/PIL/JpegImagePlugin.py | 902 ---- python_modules/PIL/JpegPresets.py | 242 - python_modules/PIL/McIdasImagePlugin.py | 78 - python_modules/PIL/MicImagePlugin.py | 102 - python_modules/PIL/MpegImagePlugin.py | 84 - python_modules/PIL/MpoImagePlugin.py | 202 - python_modules/PIL/MspImagePlugin.py | 200 - python_modules/PIL/PSDraw.py | 237 - python_modules/PIL/PaletteFile.py | 54 - python_modules/PIL/PalmImagePlugin.py | 217 - python_modules/PIL/PcdImagePlugin.py | 64 - python_modules/PIL/PcfFontFile.py | 254 - python_modules/PIL/PcxImagePlugin.py | 228 - python_modules/PIL/PdfImagePlugin.py | 311 -- python_modules/PIL/PdfParser.py | 1074 ----- python_modules/PIL/PixarImagePlugin.py | 72 - python_modules/PIL/PngImagePlugin.py | 1551 ------ python_modules/PIL/PpmImagePlugin.py | 375 -- python_modules/PIL/PsdImagePlugin.py | 333 -- python_modules/PIL/QoiImagePlugin.py | 234 - python_modules/PIL/SgiImagePlugin.py | 231 - python_modules/PIL/SpiderImagePlugin.py | 331 -- python_modules/PIL/SunImagePlugin.py | 145 - python_modules/PIL/TarIO.py | 61 - python_modules/PIL/TgaImagePlugin.py | 264 - python_modules/PIL/TiffImagePlugin.py | 2339 --------- python_modules/PIL/TiffTags.py | 562 --- python_modules/PIL/WalImageFile.py | 127 - python_modules/PIL/WebPImagePlugin.py | 320 -- python_modules/PIL/WmfImagePlugin.py | 186 - python_modules/PIL/XVThumbImagePlugin.py | 83 - python_modules/PIL/XbmImagePlugin.py | 98 - python_modules/PIL/XpmImagePlugin.py | 157 - python_modules/PIL/__init__.py | 87 - python_modules/PIL/__main__.py | 7 - python_modules/PIL/_avif.pyi | 3 - python_modules/PIL/_binary.py | 112 - python_modules/PIL/_deprecate.py | 72 - python_modules/PIL/_imaging.pyi | 31 - python_modules/PIL/_imagingcms.pyi | 143 - python_modules/PIL/_imagingft.pyi | 69 - python_modules/PIL/_imagingmath.pyi | 3 - python_modules/PIL/_imagingmorph.pyi | 3 - python_modules/PIL/_imagingtk.pyi | 3 - python_modules/PIL/_tkinter_finder.py | 20 - python_modules/PIL/_typing.py | 54 - python_modules/PIL/_util.py | 26 - python_modules/PIL/_version.py | 4 - python_modules/PIL/_webp.pyi | 3 - python_modules/PIL/features.py | 361 -- python_modules/PIL/py.typed | 0 python_modules/PIL/report.py | 5 - python_modules/_cloudflare_compat_flags.pyi | 4 - python_modules/_pyodide_entrypoint_helper.pyi | 9 - .../_workers_sdk_entropy_import_context.pth | 1 - .../_workers_sdk_entropy_import_context.py | 183 - ...rkers_sdk_entropy_import_context_loader.py | 13 - .../pillow-11.3.0.dist-info/INSTALLER | 1 - .../pillow-11.3.0.dist-info/METADATA | 177 - python_modules/pillow-11.3.0.dist-info/RECORD | 119 - .../pillow-11.3.0.dist-info/REQUESTED | 0 python_modules/pillow-11.3.0.dist-info/WHEEL | 5 - .../pillow-11.3.0.dist-info/licenses/LICENSE | 30 - .../pillow-11.3.0.dist-info/top_level.txt | 1 - .../pillow-11.3.0.dist-info/zip-safe | 1 - python_modules/pyvenv.cfg | 0 python_modules/qrcode-8.2.dist-info/INSTALLER | 1 - python_modules/qrcode-8.2.dist-info/LICENSE | 48 - python_modules/qrcode-8.2.dist-info/METADATA | 668 --- python_modules/qrcode-8.2.dist-info/RECORD | 42 - python_modules/qrcode-8.2.dist-info/REQUESTED | 0 python_modules/qrcode-8.2.dist-info/WHEEL | 4 - .../qrcode-8.2.dist-info/entry_points.txt | 3 - python_modules/qrcode/LUT.py | 223 - python_modules/qrcode/__init__.py | 30 - python_modules/qrcode/base.py | 313 -- python_modules/qrcode/compat/__init__.py | 0 python_modules/qrcode/compat/etree.py | 4 - python_modules/qrcode/compat/png.py | 7 - python_modules/qrcode/console_scripts.py | 181 - python_modules/qrcode/constants.py | 5 - python_modules/qrcode/exceptions.py | 2 - python_modules/qrcode/image/__init__.py | 0 python_modules/qrcode/image/base.py | 164 - python_modules/qrcode/image/pil.py | 57 - python_modules/qrcode/image/pure.py | 56 - python_modules/qrcode/image/styledpil.py | 120 - .../qrcode/image/styles/__init__.py | 0 .../qrcode/image/styles/colormasks.py | 226 - .../image/styles/moduledrawers/__init__.py | 10 - .../qrcode/image/styles/moduledrawers/base.py | 33 - .../qrcode/image/styles/moduledrawers/pil.py | 265 - .../qrcode/image/styles/moduledrawers/svg.py | 139 - python_modules/qrcode/image/svg.py | 175 - python_modules/qrcode/main.py | 541 --- python_modules/qrcode/release.py | 42 - python_modules/qrcode/tests/__init__.py | 0 python_modules/qrcode/tests/consts.py | 4 - python_modules/qrcode/tests/test_example.py | 13 - python_modules/qrcode/tests/test_qrcode.py | 271 -- .../qrcode/tests/test_qrcode_pil.py | 157 - .../qrcode/tests/test_qrcode_pypng.py | 35 - .../qrcode/tests/test_qrcode_svg.py | 54 - python_modules/qrcode/tests/test_release.py | 43 - python_modules/qrcode/tests/test_script.py | 97 - python_modules/qrcode/tests/test_util.py | 11 - python_modules/qrcode/util.py | 584 --- python_modules/workers/__init__.py | 66 - python_modules/workers/_workers.py | 1454 ------ python_modules/workers/py.typed | 0 python_modules/workers/workflows.py | 12 - .../INSTALLER | 1 - .../METADATA | 15 - .../RECORD | 14 - .../REQUESTED | 0 .../workers_runtime_sdk-1.1.5.dist-info/WHEEL | 4 - src/handlers/__init__.py | 0 src/handlers/_shared.py | 38 - src/handlers/bmi.py | 35 - src/handlers/hangman.py | 78 - src/handlers/madlibs.py | 60 - src/handlers/qr.py | 28 - src/handlers/rps.py | 32 - src/handlers/tictactoe.py | 95 - src/worker.py | 27 - uv.lock | 108 - web/.gitignore | 6 +- web/package-lock.json | 1259 ----- web/package.json | 10 +- web/pnpm-lock.yaml | 880 ++++ web/pnpm-workspace.yaml | 6 + web/public/playground/aes256.py | 93 + web/public/playground/battleship.py | 140 + web/public/playground/bigram-autocomplete.py | 56 + web/public/playground/bitcoin-mining.py | 52 + web/public/playground/blackjack.py | 328 ++ web/public/playground/blind-auction-2.py | 58 + web/public/playground/blind-auction.py | 55 + web/public/playground/bmi.py | 8 +- web/public/playground/caesar-cipher.py | 91 + web/public/playground/calculate-age.py | 58 + web/public/playground/calendar.py | 43 + web/public/playground/card-game.py | 151 + .../playground/character-picture-grid.py | 35 + web/public/playground/coin-flip.py | 52 + web/public/playground/collatz-conjecture.py | 37 + web/public/playground/comma-code.py | 21 + web/public/playground/countdown.py | 25 + web/public/playground/dice-roll-simulator.py | 30 + web/public/playground/dice-simulator.py | 75 + web/public/playground/dnd-dice.py | 34 + .../playground/fantasy-game-inventory.py | 38 + web/public/playground/game-of-cricket.py | 126 + web/public/playground/game-snake-water-gun.py | 123 + web/public/playground/gpa-calculator.py | 68 + web/public/playground/guess-the-word.py | 85 + web/public/playground/handcricket.py | 170 + web/public/playground/hangman.py | 12 +- web/public/playground/higher-lower.py | 30 + .../playground/inverse-matrix-calculator.py | 131 + web/public/playground/ipv4-calculator-main.py | 207 + web/public/playground/jokenpo.py | 193 + web/public/playground/loan-calculator.py | 62 + web/public/playground/love-calculator.py | 103 + web/public/playground/madlibs.py | 6 +- web/public/playground/mastermind.py | 59 + web/public/playground/maths.py | 135 + web/public/playground/minesweeper.py | 165 + .../playground/morse-code-translator.py | 118 + web/public/playground/morsecode-translator.py | 106 + web/public/playground/neurons.py | 106 + web/public/playground/number-guessing-app.py | 83 + web/public/playground/otp-generator.py | 75 + .../playground/personal-finance-tracker.py | 63 + web/public/playground/pig-latin.py | 51 + web/public/playground/pokemon-battle.py | 195 + web/public/playground/projecteuler.py | 12 + web/public/playground/pwd.py | 9 +- .../playground/python-banking-system.py | 132 + web/public/playground/qr.py | 7 +- web/public/playground/quiz-game.py | 108 + web/public/playground/regex-strip.py | 22 + web/public/playground/rps.py | 8 +- web/public/playground/split-tip.py | 21 + web/public/playground/subnetting-flsm.py | 198 + web/public/playground/sudoku-solver.py | 138 + web/public/playground/tennistournamentsim.py | 440 ++ web/public/playground/tictactoe-tylerpear.py | 192 + web/public/playground/tictactoe.py | 14 +- web/public/playground/timer.py | 20 + web/scripts/gen-authors.mjs | 74 + web/scripts/gen-catalog.mjs | 314 ++ web/src/app/globals.css | 399 +- web/src/app/playground/[id]/page.tsx | 36 +- web/src/app/projects/[id]/page.tsx | 115 +- web/src/app/sitemap.ts | 6 +- web/src/components/Playground.tsx | 166 +- web/src/components/PlaygroundNav.tsx | 4 +- web/src/components/PlaygroundSidebar.tsx | 127 +- web/src/components/ProjectCard.tsx | 7 +- web/src/components/ProjectModal.tsx | 4 +- web/src/components/ResizeHandle.tsx | 72 + web/src/components/RunnableBadge.tsx | 18 + web/src/components/TryItPanel.tsx | 406 -- web/src/lib/api.ts | 108 - web/src/lib/authors.json | 276 ++ web/src/lib/catalog.json | 2958 ++++++++++++ web/src/lib/catalog.ts | 23 + web/src/lib/data.ts | 12 +- web/src/types.ts | 26 +- wrangler.jsonc | 11 - 556 files changed, 14745 insertions(+), 43973 deletions(-) delete mode 100644 .wrangler/cache/pages.json delete mode 100644 .wrangler/cache/wrangler-account.json create mode 100644 AGENTS.md create mode 100644 projects/Adjactive_compartive_superlative/README.md create mode 100644 projects/Advisor/README.md create mode 100644 projects/Alarm Clock/README.md create mode 100644 projects/Amazon Product Availbility Checker/README.md create mode 100644 projects/AudioRecorder/README.md create mode 100644 projects/BMI WebApp/README.md create mode 100644 projects/Bigram_Autocomplete/README.md create mode 100644 projects/Blind_Auction/README.md create mode 100644 projects/Browser/README.md create mode 100644 projects/Budget-manager/README.md create mode 100644 projects/Calculator-GUI-With-Python/README.md create mode 100644 projects/Calendar/README.md create mode 100644 projects/Chess/README.md create mode 100644 projects/Code_Reviewer/README.md create mode 100644 projects/Comics_Scraper/README.md create mode 100644 projects/Connect Four/README.md create mode 100644 "projects/Conway\342\200\231s-Game-Of-Life/README.md" create mode 100644 projects/Countdown/README.md create mode 100644 projects/Desktop Weather Notifier/README.md create mode 100644 projects/Desktop-Notification/README.md create mode 100644 projects/Discord Bot/README.md create mode 100644 projects/DnD Dice/README.md create mode 100644 projects/Encryptor and Decryptor/README.md create mode 100644 projects/English Thesaurus/README.md create mode 100644 projects/Fidget Spinner Game/README.md create mode 100644 projects/File_Organizer/README.md create mode 100644 projects/Find_imbd_rating/README.md create mode 100644 projects/Flappybird_game/README.md create mode 100644 projects/Full_Calendar/README.md create mode 100644 projects/GPA-Calculator/README.md create mode 100644 "projects/GUI based image display and transfer in\302\240python/README.md" create mode 100644 projects/Game of Cricket/README.md create mode 100644 projects/Gradient-Image/README.md create mode 100644 projects/Guess Number/README.md create mode 100644 projects/Guess The Word/README.md create mode 100644 projects/HandTrack/README.md create mode 100644 projects/Hangman/README.md create mode 100644 projects/Higher-Lower/README.md create mode 100644 projects/IP Blacklist Checker/README.md create mode 100644 projects/Image Manipulation/README.md create mode 100644 projects/Image Sketcher/README.md create mode 100644 projects/Image compressor/README.md create mode 100644 projects/Image to PDF Converter/README.md create mode 100644 projects/Image to text generation project/README.md create mode 100644 projects/Image-Upscale/README.md create mode 100644 projects/Instagram Post Creation/README.md create mode 100644 projects/Inverse Matrix Calculator/README.md create mode 100644 projects/Jokenpo/README.md create mode 100644 projects/Jokey/README.md create mode 100644 projects/KbdXylo/README.md create mode 100644 projects/Live AQI/README.md create mode 100644 projects/Location Search App (GUI)/README.md create mode 100644 projects/Lyrics-Extractor/README.md create mode 100644 projects/ML-Notebooks_Beginners/README.md create mode 100644 projects/Madlibs Generator/README.md create mode 100644 projects/Make-API/README.md create mode 100644 projects/Mastermind/README.md create mode 100644 projects/Medium Article Reader/README.md create mode 100644 projects/Merge PDFs/README.md create mode 100644 projects/Minecraft-in-Python-main/README.md create mode 100644 projects/Mobile Document Scanner/README.md create mode 100644 projects/Mongo CRUD/README.md create mode 100644 projects/Morse-Code-Translator/README.md create mode 100644 projects/MorseCode Translator/README.md create mode 100644 projects/MotivateBot/README.md create mode 100644 projects/Movie recommendation/README.md create mode 100644 projects/MovieApi_and_ML/README.md create mode 100644 projects/Music Player/README.md create mode 100644 projects/Neurons/README.md create mode 100644 projects/Number Guessing App/README.md create mode 100644 projects/OTP-Verfication-System/README.md create mode 100644 projects/Online Trivia/README.md create mode 100644 projects/OpenCV_color_detect_in_live_feed/README.md create mode 100644 projects/Organize_Directory/README.md create mode 100644 projects/Otp_Generator/README.md create mode 100644 projects/PDF_Reader/README.md create mode 100644 projects/PONG/README.md create mode 100644 projects/Password Projects/Password Generator/README.md create mode 100644 projects/Password Projects/Password Meter/README.md create mode 100644 projects/Password Projects/Password_manager/README.md create mode 100644 projects/Password Projects/WiFi Password Generator/README.md create mode 100644 projects/Password Projects/strong-password-detector/README.md create mode 100644 projects/Personal-Finance-tracker/README.md create mode 100644 projects/Pig_latin/README.md create mode 100644 projects/Pokemon Battle/README.md create mode 100644 projects/Print_Colored_Text/README.md create mode 100644 projects/Python story generator/README.md create mode 100644 projects/QRCode-Generator/README.md create mode 100644 projects/Qt5_YouTube/README.md create mode 100644 projects/Quiz Game/README.md create mode 100644 projects/Rename_Images/README.md create mode 100644 projects/Resize_Image/README.md create mode 100644 projects/Rock_Paper_Scissors/README.md create mode 100644 projects/Roll_A_dice/README.md create mode 100644 projects/Rubik-tracking/README.md create mode 100644 projects/Seek_with_hand_track/README.md create mode 100644 projects/Selfie_with_Python/README.md create mode 100644 projects/Send-Email/README.md create mode 100644 projects/Snake Game/README.md create mode 100644 projects/SongsMashup/README.md create mode 100644 projects/Space Shooter/README.md create mode 100644 projects/Speed-Type-test/README.md create mode 100644 projects/Split_Tip/README.md create mode 100644 projects/Spotify Player/README.md create mode 100644 projects/Stock-Market-Dashboard/README.md create mode 100644 projects/Subtitle_synchronizer.py/README.md create mode 100644 projects/Sudoku-Solver/README.md create mode 100644 projects/Sudoku_solver/README.md create mode 100644 projects/TennisTournamentSim/README.md create mode 100644 projects/Tesla/README.md create mode 100644 projects/Tetris Game/README.md create mode 100644 projects/Text Editor/README.md create mode 100644 projects/Text-to-Image Generation Project/README.md create mode 100644 projects/TextDetection/README.md create mode 100644 projects/Text_to_SpreadSheet/README.md create mode 100644 projects/Tic-Tac-Toe/README.md create mode 100644 projects/Tic-Tac-Toe/Tic-Tac-Toe-Terminal/README.md create mode 100644 projects/Tic-Tac-Toe/TicTacToe-GUI/README.md create mode 100644 projects/TicTacToe-TylerPear/README.md create mode 100644 projects/Tile Matching/README.md create mode 100644 projects/Timer/README.md create mode 100644 projects/Tkinter/README.md create mode 100644 projects/Turtle Pattern/README.md create mode 100644 projects/Turtle_Graphics/README.md create mode 100644 projects/Type Racer Game/README.md create mode 100644 projects/Voice-to-Text/README.md create mode 100644 projects/Weather/README.md create mode 100644 projects/Windows Logo/README.md create mode 100644 projects/Word_Predictor/README.md create mode 100644 projects/Worksheet_to_text/README.md create mode 100644 projects/World-Cup-Player-Comparison/README.md create mode 100644 projects/YouTube Video Downloader/README.md create mode 100644 projects/bittorrent-downloader/README.md create mode 100644 projects/caesar_cipher/README.md create mode 100644 projects/car game/README.md create mode 100644 projects/character-picture-grid/README.md create mode 100644 projects/chore-assignment-emailer/README.md create mode 100644 projects/comma-code/README.md create mode 100644 projects/currency converter/README.md create mode 100644 projects/custom-invitations/README.md create mode 100644 projects/custom-seating-cards/README.md create mode 100644 projects/dictionary.com-scraper/README.md create mode 100644 projects/duplicate_search/README.md create mode 100644 projects/excel-to-csv-converter/README.md create mode 100644 projects/facerecoginition/README.md create mode 100644 projects/fantasy-game-inventory/README.md create mode 100644 projects/fill-gaps/README.md create mode 100644 projects/find-unneeded-files/README.md create mode 100644 projects/goodreads-quotes-scraper/README.md create mode 100644 projects/healthmanagementsystem/README.md create mode 100644 projects/image-site-downloader/README.md create mode 100644 projects/indeed-scraper/README.md create mode 100644 projects/instant-messenger-bot/README.md create mode 100644 projects/inventory management/README.md create mode 100644 projects/link-verification/README.md create mode 100644 projects/linkedin-scrape/README.md create mode 100644 projects/looking-busy/README.md create mode 100644 projects/love-calculator/README.md create mode 100644 projects/maths/README.md create mode 100644 projects/minesweeper/README.md create mode 100644 projects/multiplayer_socket/README.md create mode 100644 projects/pdf-paranoia/README.md create mode 100644 projects/ping_pong/README.md create mode 100644 projects/prettified-stopwatch/README.md create mode 100644 projects/qr-code-generator-with-window-and-simple-ui/README.md create mode 100644 projects/reddit-scraper/README.md create mode 100644 projects/regex-search/README.md create mode 100644 projects/regex-strip/README.md create mode 100644 projects/reuters-scraper/README.md create mode 100644 projects/scheduledShutdown/README.md create mode 100644 projects/scrap-ycombinator/README.md create mode 100644 projects/snake_water_gun_game/README.md create mode 100644 projects/space_battle/README.md create mode 100644 projects/sponge-bob/README.md create mode 100644 projects/todo.md create mode 100644 projects/web-crawler(movie extract)/README.md create mode 100644 projects/what-for-dinner/README.md delete mode 100644 pyproject.toml delete mode 100644 python_modules/.synced delete mode 100644 python_modules/PIL/AvifImagePlugin.py delete mode 100644 python_modules/PIL/BdfFontFile.py delete mode 100644 python_modules/PIL/BlpImagePlugin.py delete mode 100644 python_modules/PIL/BmpImagePlugin.py delete mode 100644 python_modules/PIL/BufrStubImagePlugin.py delete mode 100644 python_modules/PIL/ContainerIO.py delete mode 100644 python_modules/PIL/CurImagePlugin.py delete mode 100644 python_modules/PIL/DcxImagePlugin.py delete mode 100644 python_modules/PIL/DdsImagePlugin.py delete mode 100644 python_modules/PIL/EpsImagePlugin.py delete mode 100644 python_modules/PIL/ExifTags.py delete mode 100644 python_modules/PIL/FitsImagePlugin.py delete mode 100644 python_modules/PIL/FliImagePlugin.py delete mode 100644 python_modules/PIL/FontFile.py delete mode 100644 python_modules/PIL/FpxImagePlugin.py delete mode 100644 python_modules/PIL/FtexImagePlugin.py delete mode 100644 python_modules/PIL/GbrImagePlugin.py delete mode 100644 python_modules/PIL/GdImageFile.py delete mode 100644 python_modules/PIL/GifImagePlugin.py delete mode 100644 python_modules/PIL/GimpGradientFile.py delete mode 100644 python_modules/PIL/GimpPaletteFile.py delete mode 100644 python_modules/PIL/GribStubImagePlugin.py delete mode 100644 python_modules/PIL/Hdf5StubImagePlugin.py delete mode 100644 python_modules/PIL/IcnsImagePlugin.py delete mode 100644 python_modules/PIL/IcoImagePlugin.py delete mode 100644 python_modules/PIL/ImImagePlugin.py delete mode 100644 python_modules/PIL/Image.py delete mode 100644 python_modules/PIL/ImageChops.py delete mode 100644 python_modules/PIL/ImageCms.py delete mode 100644 python_modules/PIL/ImageColor.py delete mode 100644 python_modules/PIL/ImageDraw.py delete mode 100644 python_modules/PIL/ImageDraw2.py delete mode 100644 python_modules/PIL/ImageEnhance.py delete mode 100644 python_modules/PIL/ImageFile.py delete mode 100644 python_modules/PIL/ImageFilter.py delete mode 100644 python_modules/PIL/ImageFont.py delete mode 100644 python_modules/PIL/ImageGrab.py delete mode 100644 python_modules/PIL/ImageMath.py delete mode 100644 python_modules/PIL/ImageMode.py delete mode 100644 python_modules/PIL/ImageMorph.py delete mode 100644 python_modules/PIL/ImageOps.py delete mode 100644 python_modules/PIL/ImagePalette.py delete mode 100644 python_modules/PIL/ImagePath.py delete mode 100644 python_modules/PIL/ImageQt.py delete mode 100644 python_modules/PIL/ImageSequence.py delete mode 100644 python_modules/PIL/ImageShow.py delete mode 100644 python_modules/PIL/ImageStat.py delete mode 100644 python_modules/PIL/ImageTk.py delete mode 100644 python_modules/PIL/ImageTransform.py delete mode 100644 python_modules/PIL/ImageWin.py delete mode 100644 python_modules/PIL/ImtImagePlugin.py delete mode 100644 python_modules/PIL/IptcImagePlugin.py delete mode 100644 python_modules/PIL/Jpeg2KImagePlugin.py delete mode 100644 python_modules/PIL/JpegImagePlugin.py delete mode 100644 python_modules/PIL/JpegPresets.py delete mode 100644 python_modules/PIL/McIdasImagePlugin.py delete mode 100644 python_modules/PIL/MicImagePlugin.py delete mode 100644 python_modules/PIL/MpegImagePlugin.py delete mode 100644 python_modules/PIL/MpoImagePlugin.py delete mode 100644 python_modules/PIL/MspImagePlugin.py delete mode 100644 python_modules/PIL/PSDraw.py delete mode 100644 python_modules/PIL/PaletteFile.py delete mode 100644 python_modules/PIL/PalmImagePlugin.py delete mode 100644 python_modules/PIL/PcdImagePlugin.py delete mode 100644 python_modules/PIL/PcfFontFile.py delete mode 100644 python_modules/PIL/PcxImagePlugin.py delete mode 100644 python_modules/PIL/PdfImagePlugin.py delete mode 100644 python_modules/PIL/PdfParser.py delete mode 100644 python_modules/PIL/PixarImagePlugin.py delete mode 100644 python_modules/PIL/PngImagePlugin.py delete mode 100644 python_modules/PIL/PpmImagePlugin.py delete mode 100644 python_modules/PIL/PsdImagePlugin.py delete mode 100644 python_modules/PIL/QoiImagePlugin.py delete mode 100644 python_modules/PIL/SgiImagePlugin.py delete mode 100644 python_modules/PIL/SpiderImagePlugin.py delete mode 100644 python_modules/PIL/SunImagePlugin.py delete mode 100644 python_modules/PIL/TarIO.py delete mode 100644 python_modules/PIL/TgaImagePlugin.py delete mode 100644 python_modules/PIL/TiffImagePlugin.py delete mode 100644 python_modules/PIL/TiffTags.py delete mode 100644 python_modules/PIL/WalImageFile.py delete mode 100644 python_modules/PIL/WebPImagePlugin.py delete mode 100644 python_modules/PIL/WmfImagePlugin.py delete mode 100644 python_modules/PIL/XVThumbImagePlugin.py delete mode 100644 python_modules/PIL/XbmImagePlugin.py delete mode 100644 python_modules/PIL/XpmImagePlugin.py delete mode 100644 python_modules/PIL/__init__.py delete mode 100644 python_modules/PIL/__main__.py delete mode 100644 python_modules/PIL/_avif.pyi delete mode 100644 python_modules/PIL/_binary.py delete mode 100644 python_modules/PIL/_deprecate.py delete mode 100644 python_modules/PIL/_imaging.pyi delete mode 100644 python_modules/PIL/_imagingcms.pyi delete mode 100644 python_modules/PIL/_imagingft.pyi delete mode 100644 python_modules/PIL/_imagingmath.pyi delete mode 100644 python_modules/PIL/_imagingmorph.pyi delete mode 100644 python_modules/PIL/_imagingtk.pyi delete mode 100644 python_modules/PIL/_tkinter_finder.py delete mode 100644 python_modules/PIL/_typing.py delete mode 100644 python_modules/PIL/_util.py delete mode 100644 python_modules/PIL/_version.py delete mode 100644 python_modules/PIL/_webp.pyi delete mode 100644 python_modules/PIL/features.py delete mode 100644 python_modules/PIL/py.typed delete mode 100644 python_modules/PIL/report.py delete mode 100644 python_modules/_cloudflare_compat_flags.pyi delete mode 100644 python_modules/_pyodide_entrypoint_helper.pyi delete mode 100644 python_modules/_workers_sdk_entropy_import_context.pth delete mode 100644 python_modules/_workers_sdk_entropy_import_context.py delete mode 100644 python_modules/_workers_sdk_entropy_import_context_loader.py delete mode 100644 python_modules/pillow-11.3.0.dist-info/INSTALLER delete mode 100644 python_modules/pillow-11.3.0.dist-info/METADATA delete mode 100644 python_modules/pillow-11.3.0.dist-info/RECORD delete mode 100644 python_modules/pillow-11.3.0.dist-info/REQUESTED delete mode 100644 python_modules/pillow-11.3.0.dist-info/WHEEL delete mode 100644 python_modules/pillow-11.3.0.dist-info/licenses/LICENSE delete mode 100644 python_modules/pillow-11.3.0.dist-info/top_level.txt delete mode 100644 python_modules/pillow-11.3.0.dist-info/zip-safe delete mode 100644 python_modules/pyvenv.cfg delete mode 100644 python_modules/qrcode-8.2.dist-info/INSTALLER delete mode 100644 python_modules/qrcode-8.2.dist-info/LICENSE delete mode 100644 python_modules/qrcode-8.2.dist-info/METADATA delete mode 100644 python_modules/qrcode-8.2.dist-info/RECORD delete mode 100644 python_modules/qrcode-8.2.dist-info/REQUESTED delete mode 100644 python_modules/qrcode-8.2.dist-info/WHEEL delete mode 100644 python_modules/qrcode-8.2.dist-info/entry_points.txt delete mode 100644 python_modules/qrcode/LUT.py delete mode 100644 python_modules/qrcode/__init__.py delete mode 100644 python_modules/qrcode/base.py delete mode 100644 python_modules/qrcode/compat/__init__.py delete mode 100644 python_modules/qrcode/compat/etree.py delete mode 100644 python_modules/qrcode/compat/png.py delete mode 100644 python_modules/qrcode/console_scripts.py delete mode 100644 python_modules/qrcode/constants.py delete mode 100644 python_modules/qrcode/exceptions.py delete mode 100644 python_modules/qrcode/image/__init__.py delete mode 100644 python_modules/qrcode/image/base.py delete mode 100644 python_modules/qrcode/image/pil.py delete mode 100644 python_modules/qrcode/image/pure.py delete mode 100644 python_modules/qrcode/image/styledpil.py delete mode 100644 python_modules/qrcode/image/styles/__init__.py delete mode 100644 python_modules/qrcode/image/styles/colormasks.py delete mode 100644 python_modules/qrcode/image/styles/moduledrawers/__init__.py delete mode 100644 python_modules/qrcode/image/styles/moduledrawers/base.py delete mode 100644 python_modules/qrcode/image/styles/moduledrawers/pil.py delete mode 100644 python_modules/qrcode/image/styles/moduledrawers/svg.py delete mode 100644 python_modules/qrcode/image/svg.py delete mode 100644 python_modules/qrcode/main.py delete mode 100644 python_modules/qrcode/release.py delete mode 100644 python_modules/qrcode/tests/__init__.py delete mode 100644 python_modules/qrcode/tests/consts.py delete mode 100644 python_modules/qrcode/tests/test_example.py delete mode 100644 python_modules/qrcode/tests/test_qrcode.py delete mode 100644 python_modules/qrcode/tests/test_qrcode_pil.py delete mode 100644 python_modules/qrcode/tests/test_qrcode_pypng.py delete mode 100644 python_modules/qrcode/tests/test_qrcode_svg.py delete mode 100644 python_modules/qrcode/tests/test_release.py delete mode 100644 python_modules/qrcode/tests/test_script.py delete mode 100644 python_modules/qrcode/tests/test_util.py delete mode 100644 python_modules/qrcode/util.py delete mode 100644 python_modules/workers/__init__.py delete mode 100644 python_modules/workers/_workers.py delete mode 100644 python_modules/workers/py.typed delete mode 100644 python_modules/workers/workflows.py delete mode 100644 python_modules/workers_runtime_sdk-1.1.5.dist-info/INSTALLER delete mode 100644 python_modules/workers_runtime_sdk-1.1.5.dist-info/METADATA delete mode 100644 python_modules/workers_runtime_sdk-1.1.5.dist-info/RECORD delete mode 100644 python_modules/workers_runtime_sdk-1.1.5.dist-info/REQUESTED delete mode 100644 python_modules/workers_runtime_sdk-1.1.5.dist-info/WHEEL delete mode 100644 src/handlers/__init__.py delete mode 100644 src/handlers/_shared.py delete mode 100644 src/handlers/bmi.py delete mode 100644 src/handlers/hangman.py delete mode 100644 src/handlers/madlibs.py delete mode 100644 src/handlers/qr.py delete mode 100644 src/handlers/rps.py delete mode 100644 src/handlers/tictactoe.py delete mode 100644 src/worker.py delete mode 100644 uv.lock delete mode 100644 web/package-lock.json create mode 100644 web/pnpm-lock.yaml create mode 100644 web/pnpm-workspace.yaml create mode 100644 web/public/playground/aes256.py create mode 100644 web/public/playground/battleship.py create mode 100644 web/public/playground/bigram-autocomplete.py create mode 100644 web/public/playground/bitcoin-mining.py create mode 100644 web/public/playground/blackjack.py create mode 100644 web/public/playground/blind-auction-2.py create mode 100644 web/public/playground/blind-auction.py create mode 100644 web/public/playground/caesar-cipher.py create mode 100644 web/public/playground/calculate-age.py create mode 100644 web/public/playground/calendar.py create mode 100644 web/public/playground/card-game.py create mode 100644 web/public/playground/character-picture-grid.py create mode 100644 web/public/playground/coin-flip.py create mode 100644 web/public/playground/collatz-conjecture.py create mode 100644 web/public/playground/comma-code.py create mode 100644 web/public/playground/countdown.py create mode 100644 web/public/playground/dice-roll-simulator.py create mode 100644 web/public/playground/dice-simulator.py create mode 100644 web/public/playground/dnd-dice.py create mode 100644 web/public/playground/fantasy-game-inventory.py create mode 100644 web/public/playground/game-of-cricket.py create mode 100644 web/public/playground/game-snake-water-gun.py create mode 100644 web/public/playground/gpa-calculator.py create mode 100644 web/public/playground/guess-the-word.py create mode 100644 web/public/playground/handcricket.py create mode 100644 web/public/playground/higher-lower.py create mode 100644 web/public/playground/inverse-matrix-calculator.py create mode 100644 web/public/playground/ipv4-calculator-main.py create mode 100644 web/public/playground/jokenpo.py create mode 100644 web/public/playground/loan-calculator.py create mode 100644 web/public/playground/love-calculator.py create mode 100644 web/public/playground/mastermind.py create mode 100644 web/public/playground/maths.py create mode 100644 web/public/playground/minesweeper.py create mode 100644 web/public/playground/morse-code-translator.py create mode 100644 web/public/playground/morsecode-translator.py create mode 100644 web/public/playground/neurons.py create mode 100644 web/public/playground/number-guessing-app.py create mode 100644 web/public/playground/otp-generator.py create mode 100644 web/public/playground/personal-finance-tracker.py create mode 100644 web/public/playground/pig-latin.py create mode 100644 web/public/playground/pokemon-battle.py create mode 100644 web/public/playground/projecteuler.py create mode 100644 web/public/playground/python-banking-system.py create mode 100644 web/public/playground/quiz-game.py create mode 100644 web/public/playground/regex-strip.py create mode 100644 web/public/playground/split-tip.py create mode 100644 web/public/playground/subnetting-flsm.py create mode 100644 web/public/playground/sudoku-solver.py create mode 100644 web/public/playground/tennistournamentsim.py create mode 100644 web/public/playground/tictactoe-tylerpear.py create mode 100644 web/public/playground/timer.py create mode 100644 web/scripts/gen-authors.mjs create mode 100644 web/scripts/gen-catalog.mjs create mode 100644 web/src/components/ResizeHandle.tsx create mode 100644 web/src/components/RunnableBadge.tsx delete mode 100644 web/src/components/TryItPanel.tsx delete mode 100644 web/src/lib/api.ts create mode 100644 web/src/lib/authors.json create mode 100644 web/src/lib/catalog.json create mode 100644 web/src/lib/catalog.ts delete mode 100644 wrangler.jsonc diff --git a/.gitignore b/.gitignore index becd20775..e8404512b 100644 --- a/.gitignore +++ b/.gitignore @@ -165,3 +165,5 @@ cython_debug/ web/node_modules/ web/.next/ web/out/ + +.claude \ No newline at end of file diff --git a/.wrangler/cache/pages.json b/.wrangler/cache/pages.json deleted file mode 100644 index 348673cfc..000000000 --- a/.wrangler/cache/pages.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "account_id": "51b5d4b1862c5d3379a7b244c5593875", - "project_name": "pybegin" -} \ No newline at end of file diff --git a/.wrangler/cache/wrangler-account.json b/.wrangler/cache/wrangler-account.json deleted file mode 100644 index e6c4434aa..000000000 --- a/.wrangler/cache/wrangler-account.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "account": { - "id": "51b5d4b1862c5d3379a7b244c5593875", - "name": "Bhowmickmrinank@gmail.com's Account" - } -} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..c819fdf11 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,48 @@ +# AGENTS.md + +## Repo layout + +``` +projects/ ~270 standalone beginner Python projects, one folder each +web/ Next.js website + in-browser Python playground +data/ static banner images +.github/ CI workflows +``` + +Two independent parts: the **Python projects** and the **website**. They don't +share code. + +## projects/ + +Each project is a self-contained folder (`projects/Hangman/`, `projects/BMI_calculator/`). +No shared package, no repo-wide dependencies — a project lists its own libs in a +local `requirements.txt`/README. Folder naming is inconsistent; match the existing +folder, don't rename. Python is auto-formatted with Black in CI. + +## web/ — system design + +Next.js 16 (App Router, TypeScript, React 19), **statically exported** (`output: 'export'`) +and hosted on **Cloudflare Pages**. Pages are pre-rendered HTML at build time for SEO. + +The **playground** runs real CPython in the browser — no server. Pyodide +(CPython → WebAssembly) loads in a **Web Worker** (`web/public/pyodide-worker.js`). +A `SharedArrayBuffer` + `Atomics` bridge lets blocking `input()` work, which is why +the playground needs cross-origin isolation (COOP/COEP headers in `web/public/_headers`). +The editor is CodeMirror 6; the terminal is xterm.js. + +There is **no backend**. A Cloudflare Worker once executed code server-side — it was +removed; everything runs client-side now. + +Key files: +- `web/src/app/` — routes (`/`, `/projects/[id]`, `/playground/[id]`) +- `web/src/lib/data.ts` — the project list shown on the site +- `web/public/playground/.py` — code preloaded into the playground per project +- `web/src/lib/pyodide/` + `web/public/pyodide-worker.js` — the Pyodide worker + +Only pure-stdlib console projects can run in the playground (`playground: true` in +`data.ts`); pygame/tkinter/network projects cannot. + +Commands (inside `web/`): `corepack pnpm dev` (localhost:3000), `corepack pnpm +run build` (static export → `web/out/`, also runs the type check). The project +uses **pnpm** (pinned via `packageManager` in `package.json`); supply-chain +settings are in `web/pnpm-workspace.yaml`. Do not reintroduce `package-lock.json`. diff --git a/projects/AES256/AES256.py b/projects/AES256/AES256.py index ffa635bcc..ef7e58ceb 100644 --- a/projects/AES256/AES256.py +++ b/projects/AES256/AES256.py @@ -1,16 +1,8 @@ # Imports import hashlib from base64 import b64encode, b64decode -import os -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes -import platform - -# Clear the console screen -if platform.system() == "Windows": - os.system("cls") -else: - os.system("clear") +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes # Start of Encryption Function diff --git a/projects/AES256/README.md b/projects/AES256/README.md index e90d22599..f22ddd9a7 100644 --- a/projects/AES256/README.md +++ b/projects/AES256/README.md @@ -1,19 +1,25 @@ -# AES 256 Encryption and Decryption Algorithm in Python +# AES 256 Encryption and Decryption -This is a beginner friendly project for understanding Encryption/Decryption using AES Encryption Cipher in Python +A beginner-friendly console project demonstrating AES-256 encryption and +decryption in Python. It uses AES in GCM mode (authenticated encryption) with a +key derived from your password via `scrypt`. -## Requirements -1. Cryptodome
-` pip install pycryptodome ` +Choose `1` to encrypt a message (prints the cipher text, salt, nonce and tag) or +`2` to decrypt by entering those values back. -2. PyCryptodomex
-`pip install pycryptodomex ` +## How to run -## Setup -1. Python3.x version should be installed in your system. -2. Install all requirements. -3. Clone the repository. -4. Run Python File `python3 AES256.py` +```bash +pip install pycryptodomex +python AES256.py +``` -## Demo! -![Screenshot 2022-10-20 134838](https://user-images.githubusercontent.com/99896373/196895153-c0e40bb4-95e8-4d98-86bd-eb0f051c218a.png) +## Dependencies + +- `pycryptodomex` — the `Cryptodome` AES implementation + +## Pyodide-runnable + +Yes. `pycryptodome` is available in the Pyodide playground, and the program is a +pure `input()`/`print()` console app. The screen-clearing `os.system` call was +removed so it runs cleanly in-browser. diff --git a/projects/API Based Weather Report/README.md b/projects/API Based Weather Report/README.md index 050d7b185..f56eb1d08 100644 --- a/projects/API Based Weather Report/README.md +++ b/projects/API Based Weather Report/README.md @@ -43,6 +43,11 @@ Welcome to the Weather Information App! This application allows users to fetch c - City Name with Spaces: Input the city name with spaces as it appears (e.g., "Los Angeles", "San Francisco"). - City District or Area (Optional): Specify a district or area within larger cities for more localized weather data (e.g., "Manhattan, New York", "Shinjuku, Tokyo"). +## Pyodide-runnable + +No. The app calls the OpenWeatherMap HTTP API at runtime, which is blocked from +the in-browser Pyodide playground. + ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/projects/AWS_s3_upload/readme.md b/projects/AWS_s3_upload/readme.md index ffdd64bfe..1b271c2a5 100644 --- a/projects/AWS_s3_upload/readme.md +++ b/projects/AWS_s3_upload/readme.md @@ -10,4 +10,8 @@ boto3 (pip install boto3)
- Run "python main.py"
### Author Name -Yashvardhan Singh https://github.com/pythonicboat \ No newline at end of file +Yashvardhan Singh https://github.com/pythonicboat + +## Pyodide-runnable + +No - uses boto3 to make network calls to the AWS S3 API. diff --git a/projects/Adjactive_compartive_superlative/README.md b/projects/Adjactive_compartive_superlative/README.md new file mode 100644 index 000000000..3ee95fa14 --- /dev/null +++ b/projects/Adjactive_compartive_superlative/README.md @@ -0,0 +1,23 @@ +# Adjective Comparative & Superlative + +A console tool that takes a comma-separated list of adjectives and prints the +comparative and superlative form of each, using WordNet (via NLTK) with a +JSON fallback list of irregular adjectives. + +## How to run + +```bash +pip install -r requirements.txt +python adjCS.py +``` + +On first run NLTK downloads the `wordnet` corpus. + +## Dependencies + +- `nltk` — and its `wordnet` corpus (downloaded at runtime) + +## Pyodide-runnable + +No. `nltk.download()` fetches the WordNet corpus over the network at runtime, +which is not available in the in-browser Pyodide playground. diff --git a/projects/Advisor/README.md b/projects/Advisor/README.md new file mode 100644 index 000000000..fab97ccb1 --- /dev/null +++ b/projects/Advisor/README.md @@ -0,0 +1,22 @@ +# Random Advisor + +A small Tkinter desktop app that fetches a random piece of advice from the +[Advice Slip API](https://api.adviceslip.com/) and shows it in a window. Click +**Get Advice** for a new one. + +## How to run + +```bash +pip install requests +python advisor.py +``` + +## Dependencies + +- `requests` — to call the Advice Slip API +- `tkinter` — GUI (ships with the standard Python installer) + +## Pyodide-runnable + +No. It uses a Tkinter window and a live HTTP request, neither of which works in +the in-browser Pyodide playground. diff --git a/projects/Alarm Clock/README.md b/projects/Alarm Clock/README.md new file mode 100644 index 000000000..1b275e952 --- /dev/null +++ b/projects/Alarm Clock/README.md @@ -0,0 +1,26 @@ +# Alarm Clock + +A Tkinter desktop alarm clock. Pick an hour, minute and second from the +drop-downs, press **Set Alarm**, and when the system clock reaches that time it +plays `sound.wav` on a loop until you press **Stop Alarm**. + +The folder also contains `clock.py`, a standalone digital clock that shows the +current time and greets you based on the time of day. + +## How to run + +```bash +pip install pygame +python alarm_clock.py # the alarm +python clock.py # the digital clock +``` + +## Dependencies + +- `tkinter` — GUI (ships with the standard Python installer) +- `pygame` — plays the alarm sound (`alarm_clock.py` only) + +## Pyodide-runnable + +No. Both scripts use a Tkinter window, and the alarm also uses `pygame` audio +and background threads — none of which work in the in-browser Pyodide playground. diff --git a/projects/Amazon Product Availbility Checker/README.md b/projects/Amazon Product Availbility Checker/README.md new file mode 100644 index 000000000..bdff5a8e0 --- /dev/null +++ b/projects/Amazon Product Availbility Checker/README.md @@ -0,0 +1,25 @@ +# Amazon Product Availability Checker + +A console script that fetches an Amazon product page, parses it with +BeautifulSoup, and reports whether the product is in stock. + +Before running, edit `amazon.py`: +- Set `product_url` to the Amazon product page you want to track. +- Set a valid `User-Agent` string in `headers` (Amazon blocks the default one). + +## How to run + +```bash +pip install requests beautifulsoup4 +python amazon.py +``` + +## Dependencies + +- `requests` — fetches the product page +- `beautifulsoup4` — parses the HTML + +## Pyodide-runnable + +No. It makes a live HTTP request to Amazon, and browsers block cross-origin +scraping requests, so it cannot run in the in-browser Pyodide playground. diff --git a/projects/AnalogClock/readme.md b/projects/AnalogClock/readme.md index 1903813bc..b98a53ea7 100644 --- a/projects/AnalogClock/readme.md +++ b/projects/AnalogClock/readme.md @@ -1,4 +1,23 @@ -## Analog Clock -1.This script Creates a popup with analog clock using tkinter -it's always start at 10 o'clock thats a fact -![img.png](img.png) \ No newline at end of file +# Analog Clock + +A Tkinter program that draws a working analog clock on a canvas — face, hour +numbers, and moving hour/minute/second hands. The clock always starts at +10 o'clock and ticks forward once per second. + +![img.png](img.png) + +## How to run + +```bash +python analog_clock.py +``` + +## Dependencies + +- `tkinter` — GUI (ships with the standard Python installer) +- `math` — standard library + +## Pyodide-runnable + +No. It draws to a Tkinter canvas window, which is not available in the +in-browser Pyodide playground. diff --git a/projects/Audio Converter/README.md b/projects/Audio Converter/README.md index 1bff5acaf..eda82c4a3 100644 --- a/projects/Audio Converter/README.md +++ b/projects/Audio Converter/README.md @@ -16,3 +16,8 @@ pip install pydub python audioconverter.py ``` Run `ffmpeg -formats` to view supported formats, since Pydub uses ffmpeg. + + +## Pyodide-runnable + +No - uses the pydub package and reads/converts audio files from disk via sys.argv, which is not available in a browser sandbox. diff --git a/projects/AudioAPI/ReadMe.md b/projects/AudioAPI/ReadMe.md index 71e78a9f5..0f9d6fc26 100644 --- a/projects/AudioAPI/ReadMe.md +++ b/projects/AudioAPI/ReadMe.md @@ -31,3 +31,8 @@ The Unofficial Whisper API provides transcription output in various formats: Text: The transcribed text as a plain text string. VTT (WebVTT): Transcribed text with timestamps in the WebVTT format for easy integration with video players. SRT (SubRip): Transcribed text with timestamps in the SubRip format, suitable for subtitles. + + +## Pyodide-runnable + +No - it is a Flask web server that also depends on OpenAI Whisper, neither of which runs under Pyodide. diff --git a/projects/AudioRecorder/README.md b/projects/AudioRecorder/README.md new file mode 100644 index 000000000..8d09e8a20 --- /dev/null +++ b/projects/AudioRecorder/README.md @@ -0,0 +1,19 @@ +# Audio Recorder + +Records audio from the system microphone and saves it to RAW, WAV, AIFF, and FLAC files. + +## How to run + +``` +pip install SpeechRecognition pyaudio +python main.py +``` + +## Dependencies + +- SpeechRecognition +- pyaudio (microphone input backend) + +## Pyodide-runnable + +No - it captures live audio from the microphone and writes audio files to disk. diff --git a/projects/Audiobook/Readme.md b/projects/Audiobook/Readme.md index e994be46f..0b4b83a7f 100644 --- a/projects/Audiobook/Readme.md +++ b/projects/Audiobook/Readme.md @@ -27,3 +27,8 @@ python pdf_text_to_speech.py ## How it Works The script takes a PDF file as input, extracts the text content from each page, and uses `pyttsx3` to read it out loud. The playback can be stopped at any time by pressing the 'q' key. + + +## Pyodide-runnable + +No - depends on pyttsx3 (offline TTS engine), the keyboard library, and reads PDFs from the local disk. diff --git a/projects/AutoGui/ReadMe.md b/projects/AutoGui/ReadMe.md index 370332a52..ed2b31bdf 100644 --- a/projects/AutoGui/ReadMe.md +++ b/projects/AutoGui/ReadMe.md @@ -7,3 +7,8 @@ This Python script periodically moves the mouse and performs a click action. It - Simulates a mouse click periodically - Prevents idle status on Microsoft Teams + + +## Pyodide-runnable + +No - relies on pyautogui to control the real mouse and keyboard, which has no meaning in a browser. diff --git a/projects/BFS visualizer/Readme.md b/projects/BFS visualizer/Readme.md index f77f229c7..c9cbe0700 100644 --- a/projects/BFS visualizer/Readme.md +++ b/projects/BFS visualizer/Readme.md @@ -13,4 +13,8 @@ # Output: -![output](./output.gif) \ No newline at end of file +![output](./output.gif) + +## Pyodide-runnable + +No - uses the curses library for terminal UI, which is not available in Pyodide. diff --git a/projects/BMI WebApp/README.md b/projects/BMI WebApp/README.md new file mode 100644 index 000000000..6e388833d --- /dev/null +++ b/projects/BMI WebApp/README.md @@ -0,0 +1,18 @@ +# BMI WebApp + +A small BMI calculator built with PyWebIO. It asks for height and weight in a browser form and reports the BMI along with a weight classification. + +## How to run + +``` +pip install pywebio +python main.py +``` + +## Dependencies + +- pywebio + +## Pyodide-runnable + +No - it depends on the PyWebIO framework, which runs its own web server and is not available under Pyodide. diff --git a/projects/BMI_calculator/Readme.md b/projects/BMI_calculator/Readme.md index e94470795..22b22a57d 100644 --- a/projects/BMI_calculator/Readme.md +++ b/projects/BMI_calculator/Readme.md @@ -55,3 +55,8 @@ The main function to execute the BMI calculation and interpretation. ## Error Handling The script handles invalid input, ensuring that only numerical values for height and weight are accepted. + + +## Pyodide-runnable + +No - it depends on the third-party tabulate package and reads the bmi.csv file from the local filesystem. diff --git a/projects/Battleship/README.md b/projects/Battleship/README.md index 3b8026773..f63c00907 100644 --- a/projects/Battleship/README.md +++ b/projects/Battleship/README.md @@ -57,3 +57,8 @@ Py-Battleship is built with the following tools and libraries:
  • Python Selenium_ _-> bs4_ _-> time_ -If you find any Difficulty in understanding things, then check for the docs of these library. \ No newline at end of file +If you find any Difficulty in understanding things, then check for the docs of these library. +## Pyodide-runnable + +No — it uses `selenium` to drive a Chrome browser, plus `requests` for web scraping. diff --git a/projects/Data_Abstractor/README.md b/projects/Data_Abstractor/README.md index fa67c6a2c..d2112a75a 100644 --- a/projects/Data_Abstractor/README.md +++ b/projects/Data_Abstractor/README.md @@ -12,3 +12,7 @@ Please note that this a sample project to understand the concepts of SQL and web 2. Run: `python3 main.py` If everything works successfully, the website will be available on the localhost server -> http://127.0.0.1:5000/ + +## Pyodide-runnable + +No — it runs a Flask web server, which requires a server process and cannot run in the Pyodide browser sandbox. diff --git a/projects/Desktop Weather Notifier/README.md b/projects/Desktop Weather Notifier/README.md new file mode 100644 index 000000000..b588cb27c --- /dev/null +++ b/projects/Desktop Weather Notifier/README.md @@ -0,0 +1,21 @@ +# Desktop Weather Notifier + +Fetches current weather for a city from weatherapi.com once per hour and shows it as a native desktop notification, including temperature, wind, and precipitation. + +## How to run + +``` +pip install plyer requests +python weather_notifier.py +``` + +Set your weatherapi.com API key in the `API_KEY` variable. + +## Dependencies + +- `plyer` +- `requests` + +## Pyodide-runnable + +No — it makes network requests to a weather API and uses `plyer` to display OS-level desktop notifications. diff --git a/projects/Desktop-Notification/README.md b/projects/Desktop-Notification/README.md new file mode 100644 index 000000000..cf27a510b --- /dev/null +++ b/projects/Desktop-Notification/README.md @@ -0,0 +1,18 @@ +# Desktop Notification + +Displays a simple Windows toast notification using the `win10toast` library. + +## How to run + +``` +pip install win10toast +python main.py +``` + +## Dependencies + +- `win10toast` (Windows only) + +## Pyodide-runnable + +No — it relies on `win10toast` to show native Windows toast notifications, which is not available in a browser. diff --git a/projects/Diabetes Monitoring Dashboard/README.md b/projects/Diabetes Monitoring Dashboard/README.md index 1da3c2e2b..54a4766c1 100644 --- a/projects/Diabetes Monitoring Dashboard/README.md +++ b/projects/Diabetes Monitoring Dashboard/README.md @@ -46,3 +46,7 @@ streamlit run webApp.py ``` The application will deploy a webapp on localhost which then can be accesed through web browsers (Chrome recommended!) by any client on that network. + +# Pyodide-runnable + +No — it runs a Streamlit web server, makes network requests, uses scikit-learn, and calls the OpenAI API. diff --git a/projects/Dice Simulator/Readme.md b/projects/Dice Simulator/Readme.md index d6c34856c..4e7baa339 100644 --- a/projects/Dice Simulator/Readme.md +++ b/projects/Dice Simulator/Readme.md @@ -17,3 +17,7 @@ A simple python program which uses basic concepts to show a simulation of two di - Very Useful when you doubt the judgement and choice of traditional physical dices. - Run the dice any number of times till the end of the game - Any further improvements in this project are fully welcomed. + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using only `random` and `input`/`print`. diff --git a/projects/Dice-Roll-Simulator/README.md b/projects/Dice-Roll-Simulator/README.md index b32ba2da0..54a43c78a 100644 --- a/projects/Dice-Roll-Simulator/README.md +++ b/projects/Dice-Roll-Simulator/README.md @@ -32,3 +32,7 @@ This script requires Python to be installed on your system. It also uses the `ra Contributions are welcome! If you find any bugs or have suggestions for improvement, please open an issue or create a pull request on the [GitHub repository](https://github.com/example/dice-roller). + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using only the `random` module and `input`/`print`. diff --git a/projects/Discord Bot/README.md b/projects/Discord Bot/README.md new file mode 100644 index 000000000..8205e19a4 --- /dev/null +++ b/projects/Discord Bot/README.md @@ -0,0 +1,20 @@ +# Discord Bot + +A simple Discord bot built with discord.py. It responds to `!hello` and `!random` messages and provides an `?ask` command that gives a random Magic 8-Ball style answer. + +## How to run + +``` +pip install discord.py +python main.py +``` + +Replace `"Your Token Here"` in the script with a valid Discord bot token. + +## Dependencies + +- `discord.py` + +## Pyodide-runnable + +No — it connects to Discord's gateway over the network and runs a long-lived bot event loop. diff --git a/projects/DnD Dice/README.md b/projects/DnD Dice/README.md new file mode 100644 index 000000000..e5b66eb62 --- /dev/null +++ b/projects/DnD Dice/README.md @@ -0,0 +1,19 @@ +# DnD Dice + +A console dice roller for tabletop role-playing games. Pick a dice type (D4, D6, D8, D10, D12, D20, or D100) and it prints a random roll. + +## How to run + +``` +python main.py +``` + +Requires Python 3.10+ (uses the `match` statement). + +## Dependencies + +Standard library only (`random`). + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using `input`, `print`, and `random`. diff --git a/projects/Drowsiness Detector/Readme.md b/projects/Drowsiness Detector/Readme.md index ff16d783b..8b1ef853c 100644 --- a/projects/Drowsiness Detector/Readme.md +++ b/projects/Drowsiness Detector/Readme.md @@ -63,3 +63,7 @@ If the eye is closed, the eye aspect ratio will again remain approximately const + +## Pyodide-runnable + +No — it uses `dlib`, `OpenCV`, a live webcam, audio playback, and threading. diff --git a/projects/Encryptor and Decryptor/README.md b/projects/Encryptor and Decryptor/README.md new file mode 100644 index 000000000..e6ff92966 --- /dev/null +++ b/projects/Encryptor and Decryptor/README.md @@ -0,0 +1,17 @@ +# Encryptor and Decryptor + +A Tkinter GUI application that encrypts and decrypts text using Base64 encoding, gated behind a secret key. + +## How to run + +``` +python encrypt.py +``` + +## Dependencies + +Standard library only (`tkinter`, `base64`). + +## Pyodide-runnable + +No — it builds a graphical interface with `tkinter`, which is unavailable in Pyodide. diff --git a/projects/English Thesaurus/README.md b/projects/English Thesaurus/README.md new file mode 100644 index 000000000..de33e98eb --- /dev/null +++ b/projects/English Thesaurus/README.md @@ -0,0 +1,19 @@ +# English Thesaurus + +A console thesaurus/dictionary that looks up word meanings from a local `data.json` file, suggesting close matches when a word is misspelled. + +## How to run + +``` +python App.py +``` + +Run from the repository root so the `projects\English Thesaurus\data.json` path resolves correctly. + +## Dependencies + +Standard library only (`json`, `difflib`). + +## Pyodide-runnable + +No — it loads `data.json` using a hard-coded Windows relative path; it could only run in Pyodide if that data file were loaded into the virtual filesystem first. diff --git a/projects/Expense-Tracker/README.md b/projects/Expense-Tracker/README.md index 8b2dac4b9..41eb68805 100644 --- a/projects/Expense-Tracker/README.md +++ b/projects/Expense-Tracker/README.md @@ -114,3 +114,7 @@ which would use dummy data to test the app. expenses to a preferred currency. + +## Pyodide-runnable + +No — it is a `customtkinter`/`tkinter` GUI application and also depends on `reportlab`, `pandas`, and `XlsxWriter`. diff --git a/projects/Eye Blink Detection/Readme.md b/projects/Eye Blink Detection/Readme.md index ec623e359..9a5c8ad1f 100644 --- a/projects/Eye Blink Detection/Readme.md +++ b/projects/Eye Blink Detection/Readme.md @@ -25,3 +25,7 @@ Each eye is represented by 6 (x, y)-coordinates, starting at the left-corner of + +## Pyodide-runnable + +No — it uses `dlib`, `OpenCV`, and a video/webcam stream, none of which are available in Pyodide. diff --git a/projects/Fidget Spinner Game/README.md b/projects/Fidget Spinner Game/README.md new file mode 100644 index 000000000..275e0202d --- /dev/null +++ b/projects/Fidget Spinner Game/README.md @@ -0,0 +1,17 @@ +# Fidget Spinner Game + +A small interactive toy built with Python's `turtle` module. Press the space bar to "flick" an on-screen fidget spinner; it spins and gradually slows down. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only (`turtle`). + +## Pyodide-runnable + +No — the `turtle` module depends on Tkinter, which has no browser support. diff --git a/projects/File_Organizer/README.md b/projects/File_Organizer/README.md new file mode 100644 index 000000000..b1721038d --- /dev/null +++ b/projects/File_Organizer/README.md @@ -0,0 +1,19 @@ +# File Organizer + +A command-line tool that organizes a directory by file type. It scans the given directory and moves files into category folders (Music, Videos, Documents, Pictures, etc.) based on their extensions. + +## How to run + +``` +python main.py [-v] +``` + +`-v` enables verbose output. + +## Dependencies + +Standard library only (`argparse`, `os`, `logging`, `shutil`). + +## Pyodide-runnable + +No — it walks and moves files on the real filesystem, which is not available in a browser sandbox. diff --git a/projects/Find_imbd_rating/README.md b/projects/Find_imbd_rating/README.md new file mode 100644 index 000000000..436018f61 --- /dev/null +++ b/projects/Find_imbd_rating/README.md @@ -0,0 +1,20 @@ +# Find IMDb Rating + +Reads the names of film files from a local directory, searches IMDb for each title, scrapes the rating and genre, and writes the results to `film_ratings.csv`. + +## How to run + +``` +pip install beautifulsoup4 requests pandas +python Find_imbd_rating.py +``` + +## Dependencies + +- beautifulsoup4 +- requests +- pandas + +## Pyodide-runnable + +No — it makes live HTTP requests to IMDb and reads a real directory listing, neither of which is available in a browser sandbox. diff --git a/projects/Flappybird_game/README.md b/projects/Flappybird_game/README.md new file mode 100644 index 000000000..c58c65af6 --- /dev/null +++ b/projects/Flappybird_game/README.md @@ -0,0 +1,18 @@ +# Flappy Bird Game + +A clone of the classic Flappy Bird game built with Pygame. Tap the space bar to flap the bird through gaps in the pipes; the game tracks score and high score. + +## How to run + +``` +pip install pygame +python main.py +``` + +## Dependencies + +- pygame + +## Pyodide-runnable + +No — it uses Pygame for graphics, sound, and an event loop, which is not available in a browser sandbox. diff --git a/projects/Full_Calendar/README.md b/projects/Full_Calendar/README.md new file mode 100644 index 000000000..f2d2c9363 --- /dev/null +++ b/projects/Full_Calendar/README.md @@ -0,0 +1,17 @@ +# Full Calendar + +A Tkinter GUI application that asks for a year and displays the full calendar for that year in a new window. + +## How to run + +``` +python GUI_calendar_generator.py +``` + +## Dependencies + +Standard library only (`tkinter`, `calendar`). + +## Pyodide-runnable + +No — it builds a Tkinter GUI, which has no browser support. diff --git a/projects/GPA-Calculator/README.md b/projects/GPA-Calculator/README.md new file mode 100644 index 000000000..dd15c526f --- /dev/null +++ b/projects/GPA-Calculator/README.md @@ -0,0 +1,17 @@ +# GPA Calculator + +A console tool that computes a semester GPA. It asks for the number of courses, each course's credits, and marks (either a total or split into mid-sem, internal and end-sem), then derives grade points and a weighted GPA. + +## How to run + +``` +python GPA-Calculator.py +``` + +## Dependencies + +Standard library only (`math`). + +## Pyodide-runnable + +Yes — console program using only `input()`/`print()` and the `math` module. diff --git "a/projects/GUI based image display and transfer in\302\240python/README.md" "b/projects/GUI based image display and transfer in\302\240python/README.md" new file mode 100644 index 000000000..75a027c96 --- /dev/null +++ "b/projects/GUI based image display and transfer in\302\240python/README.md" @@ -0,0 +1,18 @@ +# GUI Image Display and Transfer + +A Tkinter desktop application for viewing and copying images. It lets you open an image through a file dialog, displays it in the window, and saves (copies) it to a new location of your choice. + +## How to run + +``` +pip install pillow +python GUIimage.py +``` + +## Dependencies + +- pillow + +## Pyodide-runnable + +No — it builds a Tkinter GUI with file dialogs and copies files on disk, none of which are available in a browser. diff --git a/projects/Game of Cricket/README.md b/projects/Game of Cricket/README.md new file mode 100644 index 000000000..476eebbe8 --- /dev/null +++ b/projects/Game of Cricket/README.md @@ -0,0 +1,17 @@ +# Game of Cricket + +A text-based cricket game played against the computer. You pick a number from 1 to 6 each ball; matching the computer's number loses a wicket, otherwise you score runs. Includes a toss and a two-over innings for each side. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only (`random`). + +## Pyodide-runnable + +Yes — console game using only `input()`/`print()` and the `random` module. diff --git a/projects/Gpt-And-Langchain/README.md b/projects/Gpt-And-Langchain/README.md index 54f2f47dc..8d75b6ada 100644 --- a/projects/Gpt-And-Langchain/README.md +++ b/projects/Gpt-And-Langchain/README.md @@ -43,5 +43,9 @@ SERPAPI_API_KEY = 'SERP API Key' py telegram-bot.py ``` +## Pyodide-runnable + +No — it runs a Telegram bot and calls the OpenAI, Notion, and SerpAPI services over the network, none of which is available in a browser sandbox. + diff --git a/projects/Gradient-Image/README.md b/projects/Gradient-Image/README.md new file mode 100644 index 000000000..cfcde3c60 --- /dev/null +++ b/projects/Gradient-Image/README.md @@ -0,0 +1,19 @@ +# Gradient Image + +An OpenCV demo that loads an image and displays edge-detection results — Laplacian, combined Sobel gradient, and Canny — in separate windows for a pencil-shaded style representation. + +## How to run + +``` +pip install opencv-python numpy +python gradient.py +``` + +## Dependencies + +- opencv-python +- numpy + +## Pyodide-runnable + +No — it uses OpenCV GUI windows (`cv.imshow`), which are not available in a browser. diff --git a/projects/Guess Number/README.md b/projects/Guess Number/README.md new file mode 100644 index 000000000..39d51051f --- /dev/null +++ b/projects/Guess Number/README.md @@ -0,0 +1,26 @@ +# Guess Number + +A number guessing game. Two versions are included: + +- `guess_number.py` — a console version where the program picks a random number 1-10 and gives higher/lower hints until you find it, with replay support. +- `guess_num_v2.py` — a Tkinter GUI version of the same game. + +## How to run + +``` +python guess_number.py +``` + +or, for the GUI version: + +``` +python guess_num_v2.py +``` + +## Dependencies + +Standard library only (`random`, plus `tkinter` for the GUI version). + +## Pyodide-runnable + +Partial — `guess_number.py` runs in Pyodide (console, `input()`/`print()` only); `guess_num_v2.py` does not because it uses Tkinter, which has no browser support. diff --git a/projects/Guess The Word/README.md b/projects/Guess The Word/README.md new file mode 100644 index 000000000..d8e5f221d --- /dev/null +++ b/projects/Guess The Word/README.md @@ -0,0 +1,17 @@ +# Guess The Word + +A word guessing game (Hangman-style without the drawing). The program picks a random programming-language word and you have six attempts to reveal it one letter at a time. + +## How to run + +``` +python Guess_the_word.py +``` + +## Dependencies + +Standard library only (`random`). + +## Pyodide-runnable + +Yes — console game using only `input()`/`print()` and the `random` module. diff --git a/projects/HandCricket/Readme.md b/projects/HandCricket/Readme.md index 9115f83ee..1c49e372e 100644 --- a/projects/HandCricket/Readme.md +++ b/projects/HandCricket/Readme.md @@ -33,7 +33,7 @@ This is a simple command-line-based Hand Cricket game implemented in Python. It 3. Run the game using Python: ```bash - python hand_cricket.py + python main.py ``` 4. Follow the on-screen instructions to play the game. @@ -59,3 +59,7 @@ Contributions are welcome! If you'd like to contribute to this project, please f ## Acknowledgments - This game was inspired by the popular hand cricket game played by many cricket enthusiasts. - Special thanks to the Python community for providing helpful libraries and resources. + +## Pyodide-runnable + +Yes — console game using only `input()`/`print()` and the standard-library `random` and `time` modules. diff --git a/projects/HandTrack/README.md b/projects/HandTrack/README.md new file mode 100644 index 000000000..1fad71907 --- /dev/null +++ b/projects/HandTrack/README.md @@ -0,0 +1,21 @@ +# Hand Track + +Real-time hand tracking with OpenCV and MediaPipe. `base.py` and `HTrackMod.py` detect and draw hand landmarks from the webcam, and `VolumeControl.py` maps the pinch distance between fingers to the system volume. + +## How to run + +``` +pip install opencv-python mediapipe numpy pycaw comtypes +python base.py +``` + +## Dependencies + +- opencv-python +- mediapipe +- numpy +- pycaw, comtypes (for `VolumeControl.py`) + +## Pyodide-runnable + +No — it relies on webcam capture, OpenCV GUI windows, MediaPipe, and Windows audio APIs, none of which are available in a browser. diff --git a/projects/Hangman/README.md b/projects/Hangman/README.md new file mode 100644 index 000000000..a1406f50f --- /dev/null +++ b/projects/Hangman/README.md @@ -0,0 +1,17 @@ +# Hangman + +The classic Hangman game in the console. Pick a difficulty (easy/medium/hard), then guess the secret word letter by letter while ASCII art tracks your remaining tries. Word lists live in `RandomWords.py`. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only (`random`). + +## Pyodide-runnable + +Yes — console game using only `input()`/`print()` and the `random` module. diff --git a/projects/Higher-Lower/README.md b/projects/Higher-Lower/README.md new file mode 100644 index 000000000..d5efb3624 --- /dev/null +++ b/projects/Higher-Lower/README.md @@ -0,0 +1,17 @@ +# Higher-Lower Game + +A number guessing game. The program picks a random number between 0 and 100 and tells you whether to guess higher or lower until you find it. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only (`random`). + +## Pyodide-runnable + +Yes — console game using only `input()`/`print()` and the `random` module. diff --git a/projects/Historical Data Breaches/README.md b/projects/Historical Data Breaches/README.md index e9e0543e2..1181737c1 100644 --- a/projects/Historical Data Breaches/README.md +++ b/projects/Historical Data Breaches/README.md @@ -62,4 +62,8 @@ Data contained in breach: * Names * Social media profiles * Usernames -``` \ No newline at end of file +``` + +## Pyodide-runnable + +No — it fetches data from the HaveIBeenPwned API over the network, which is not available in a browser sandbox. \ No newline at end of file diff --git a/projects/IP Blacklist Checker/README.md b/projects/IP Blacklist Checker/README.md new file mode 100644 index 000000000..5f4dc4063 --- /dev/null +++ b/projects/IP Blacklist Checker/README.md @@ -0,0 +1,19 @@ +# IP Blacklist Checker + +A CLI script that checks whether given IP addresses are blacklisted by querying the blacklistchecker.com API. + +## How to run + +``` +pip install requests +python blacklist_checker.py [ip ...] +``` + +Requires a free API key from blacklistchecker.com. + +## Dependencies + +requests. + +## Pyodide-runnable +No — it makes live HTTP API requests to an external blacklist service. diff --git a/projects/IPv4_Calculator-main/README.md b/projects/IPv4_Calculator-main/README.md index 5fc1a6c44..001f3b3f4 100644 --- a/projects/IPv4_Calculator-main/README.md +++ b/projects/IPv4_Calculator-main/README.md @@ -4,3 +4,16 @@ IPv4_Calculator is developed for: 1.Determine the network class (A, B, C); 2.Determine which category the address belongs to (private, public); 3.Determine subnet attributes. + +## How to run + +``` +python ipv4_calc.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable +Yes — it is a pure-stdlib console program using only input() and print(). diff --git a/projects/Image Manipulation/README.md b/projects/Image Manipulation/README.md new file mode 100644 index 000000000..4d7472108 --- /dev/null +++ b/projects/Image Manipulation/README.md @@ -0,0 +1,18 @@ +# Image Manipulation + +A small Pillow script that opens a local image (`mars.jpg`), resizes it to 200x200 pixels, and saves the result as `newImage.jpg`. + +## How to run + +``` +pip install pillow +python resizingImage.py +``` + +## Dependencies + +- pillow + +## Pyodide-runnable + +No — it reads and writes image files on the real filesystem, which is not available in a browser sandbox. diff --git a/projects/Image Sketcher/README.md b/projects/Image Sketcher/README.md new file mode 100644 index 000000000..e2374cc44 --- /dev/null +++ b/projects/Image Sketcher/README.md @@ -0,0 +1,19 @@ +# Image Sketcher + +An OpenCV demo that captures live webcam frames and renders them as a pencil-sketch effect using grayscale conversion, Gaussian blur, and Canny edge detection. + +## How to run + +``` +pip install opencv-python numpy +python main.py +``` + +## Dependencies + +- opencv-python +- numpy + +## Pyodide-runnable + +No — it uses OpenCV webcam capture and GUI windows, which are not available in a browser. diff --git a/projects/Image Toonification/Readme.md b/projects/Image Toonification/Readme.md index 235438bc8..b8a54483d 100644 --- a/projects/Image Toonification/Readme.md +++ b/projects/Image Toonification/Readme.md @@ -18,3 +18,7 @@ Simple, Just clone this repository to your local storage and run the below comma `python3 main.py` ### If something does not work try changing the inner values such as blur value and line size. It should work now. + +## Pyodide-runnable + +No — it reads and writes image files on the real filesystem via OpenCV, which is not available in a browser sandbox. diff --git a/projects/Image compressor/README.md b/projects/Image compressor/README.md new file mode 100644 index 000000000..a1229a497 --- /dev/null +++ b/projects/Image compressor/README.md @@ -0,0 +1,18 @@ +# Image Compressor + +A command-line tool that compresses an image using Pillow. You pass an input image and optionally an output directory, quality, and format; it writes a compressed copy. + +## How to run + +``` +pip install pillow +python imagecompressor.py [-o output_dir] [-q quality] [-f format] +``` + +## Dependencies + +- pillow + +## Pyodide-runnable + +No — it reads and writes image files on the real filesystem and imports `tkinter.filedialog`, neither of which is available in a browser sandbox. diff --git a/projects/Image to PDF Converter/README.md b/projects/Image to PDF Converter/README.md new file mode 100644 index 000000000..1a0812db0 --- /dev/null +++ b/projects/Image to PDF Converter/README.md @@ -0,0 +1,18 @@ +# Image to PDF Converter + +Resizes every image in a `convert/` directory to A4 size and merges them into a single `output.pdf` file using FPDF. + +## How to run + +``` +pip install fpdf +python image_to_pdf_converter.py +``` + +## Dependencies + +- fpdf + +## Pyodide-runnable + +No — it reads images from a real directory and writes a PDF to disk, which is not available in a browser sandbox. diff --git a/projects/Image to text generation project/README.md b/projects/Image to text generation project/README.md new file mode 100644 index 000000000..0723810b3 --- /dev/null +++ b/projects/Image to text generation project/README.md @@ -0,0 +1,21 @@ +# Image to Text Generation + +A Jupyter notebook that generates a caption for an image. It loads a pretrained Vision Encoder-Decoder model (`bipin/image-caption-generator`) via Hugging Face Transformers and produces a text description of a given image. + +## How to run + +``` +pip install transformers torch pillow +``` + +Open `Image_to_text_generation.ipynb` in Jupyter (or Google Colab) and run the cells. + +## Dependencies + +- transformers +- torch +- pillow + +## Pyodide-runnable + +No — it requires PyTorch and Hugging Face Transformers (large native ML libraries) and is delivered as a notebook, none of which run under Pyodide. diff --git a/projects/Image-Upscale/README.md b/projects/Image-Upscale/README.md new file mode 100644 index 000000000..446c8b5ec --- /dev/null +++ b/projects/Image-Upscale/README.md @@ -0,0 +1,19 @@ +# Image Upscale + +Upscales an image using the ESRGAN super-resolution model built on PyTorch. + +## How to run + +``` +pip install torch torchvision pillow +python main.py +``` + +You also need a trained ESRGAN model file and an input image; update the paths in `main.py`. + +## Dependencies + +torch, torchvision, Pillow (and an ESRGAN module/model). + +## Pyodide-runnable +No — it relies on PyTorch and local image files, which are not available in the browser. diff --git a/projects/Image-to-art/Readme.md b/projects/Image-to-art/Readme.md index c6066e530..0e2665fa7 100644 --- a/projects/Image-to-art/Readme.md +++ b/projects/Image-to-art/Readme.md @@ -11,3 +11,6 @@ Don't forget to save image to be coverted to ascii art in this directory as 'tes ### How to run the script Open and run this script easily with the Python IDLE or the command line by running the following command on this directory: python3 ascii_art.py. + +## Pyodide-runnable +No — it depends on the Pillow imaging library and reads/writes image files from the local disk. diff --git a/projects/ImagegenChatbot/readme.md b/projects/ImagegenChatbot/readme.md index cecf8495a..21a875e9b 100644 --- a/projects/ImagegenChatbot/readme.md +++ b/projects/ImagegenChatbot/readme.md @@ -6,3 +6,7 @@ - Variable name: OPENAI_API_KEY , Variable value: your unique api key Now you'll be able to create your own AI 😎 + +## Pyodide-runnable + +No — it calls the OpenAI API over the network, downloads generated images, and opens them with Pillow, none of which is available in a browser sandbox. diff --git a/projects/Instagram Post Creation/README.md b/projects/Instagram Post Creation/README.md new file mode 100644 index 000000000..fef0bc4b9 --- /dev/null +++ b/projects/Instagram Post Creation/README.md @@ -0,0 +1,19 @@ +# Instagram Post Creation + +Generates an Instagram post image by drawing centered text onto a template image using Pillow. + +## How to run + +``` +pip install pillow +python main.py +``` + +Requires the bundled `templates/` and `fonts/` assets. + +## Dependencies + +Pillow. + +## Pyodide-runnable +No — it depends on Pillow and reads/writes image files from the local disk. diff --git a/projects/Internet Speed Tester/README.md b/projects/Internet Speed Tester/README.md index 35c0d12d5..a0b12be53 100644 --- a/projects/Internet Speed Tester/README.md +++ b/projects/Internet Speed Tester/README.md @@ -10,3 +10,6 @@ Check the Download and Upload Speed of your Internet Connection. ``` ./main.py ``` + +## Pyodide-runnable +No — it uses the speedtest library to measure real network speed against remote servers. diff --git a/projects/Internet-speed-test/README.md b/projects/Internet-speed-test/README.md index eb3c2d6a2..809ba321a 100644 --- a/projects/Internet-speed-test/README.md +++ b/projects/Internet-speed-test/README.md @@ -48,3 +48,6 @@ pip3 install speedtest-cli ![Shreejan-35](https://github.com/Shreejan-35) That's all. + +## Pyodide-runnable +No — it uses the speedtest library to measure real network speed against remote servers. diff --git a/projects/Inverse Matrix Calculator/README.md b/projects/Inverse Matrix Calculator/README.md new file mode 100644 index 000000000..1194944b0 --- /dev/null +++ b/projects/Inverse Matrix Calculator/README.md @@ -0,0 +1,16 @@ +# Inverse Matrix Calculator + +A console program that asks for the order and elements of a square matrix, then computes and prints its inverse using minors, cofactors, and the adjugate. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable +Yes — it is a pure-stdlib console program using only input() and print(). diff --git a/projects/JARVIS.PY/jarvisreadme.md b/projects/JARVIS.PY/jarvisreadme.md index d61ed2a5c..872e0db51 100644 --- a/projects/JARVIS.PY/jarvisreadme.md +++ b/projects/JARVIS.PY/jarvisreadme.md @@ -17,4 +17,7 @@ To get wikipedia data `pip install wikipedia` To get funny jokes -`pip install pyjokes` \ No newline at end of file +`pip install pyjokes` + +## Pyodide-runnable +No — it relies on a microphone, text-to-speech, web browser control, and file-system access. \ No newline at end of file diff --git a/projects/Jokenpo/README.md b/projects/Jokenpo/README.md new file mode 100644 index 000000000..e04cba7a3 --- /dev/null +++ b/projects/Jokenpo/README.md @@ -0,0 +1,16 @@ +# Jokenpo (Rock, Paper, Scissors) + +A console card-based rock-paper-scissors game (in Portuguese) where you can play against the computer or watch a simulated match between two players. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable +Yes — it is a pure-stdlib console game using only input() and print(). diff --git a/projects/Jokey/README.md b/projects/Jokey/README.md new file mode 100644 index 000000000..6a2381582 --- /dev/null +++ b/projects/Jokey/README.md @@ -0,0 +1,17 @@ +# Jokey + +A console joke teller that fetches jokes from the JokeAPI, with settings to change joke category and language. + +## How to run + +``` +pip install requests +python joketeller.py +``` + +## Dependencies + +requests. + +## Pyodide-runnable +No — it makes live HTTP requests to the JokeAPI to fetch jokes. diff --git a/projects/KbdXylo/README.md b/projects/KbdXylo/README.md new file mode 100644 index 000000000..ecb5c8182 --- /dev/null +++ b/projects/KbdXylo/README.md @@ -0,0 +1,17 @@ +# Keyboard Xylophone + +Turns your keyboard into a xylophone by listening for key presses and playing a tone based on each key's character value. + +## How to run + +``` +pip install -r requirements.txt +python main.py +``` + +## Dependencies + +boombox, pynput. + +## Pyodide-runnable +No — it uses pynput to capture global keyboard events and plays audio through the system sound device. diff --git a/projects/Language-Translate/README.md b/projects/Language-Translate/README.md index c0d4b9595..ad8ac4028 100644 --- a/projects/Language-Translate/README.md +++ b/projects/Language-Translate/README.md @@ -20,3 +20,6 @@ options: Language to translate from. --to_lang TO_LANG Language to translate to. (defaults to English) ``` + +## Pyodide-runnable +No — the `translate` library makes live HTTP requests to an online translation service. diff --git a/projects/Language_learning_assistant/README.md b/projects/Language_learning_assistant/README.md index b2c26392d..82a2b2b4e 100755 --- a/projects/Language_learning_assistant/README.md +++ b/projects/Language_learning_assistant/README.md @@ -47,3 +47,6 @@ The script offers a set of conversation questions and answers to engage in inter ## Grammar Exercises The grammar exercise section contains fill-in-the-blank sentences to help you improve your grasp of grammar rules. You can extend this section by adding more sentences and exercises to the `grammar_exercise` list in the script. + +## Pyodide-runnable +No — the pronunciation feature uses speech_recognition with a microphone, which is unavailable in Pyodide. diff --git a/projects/Live AQI/README.md b/projects/Live AQI/README.md new file mode 100644 index 000000000..5979d5346 --- /dev/null +++ b/projects/Live AQI/README.md @@ -0,0 +1,19 @@ +# Live AQI + +A Tkinter app that fetches the live Air Quality Index for a given city, state, and country from the AirVisual API. + +## How to run + +``` +pip install requests +python main.py +``` + +Requires an AirVisual API key set in `main.py`. + +## Dependencies + +tkinter (standard library), requests. + +## Pyodide-runnable +No — it uses a Tkinter GUI and makes live HTTP requests to the AirVisual API. diff --git a/projects/Loan Calculator/README.md b/projects/Loan Calculator/README.md index 483803b94..004db6b92 100644 --- a/projects/Loan Calculator/README.md +++ b/projects/Loan Calculator/README.md @@ -36,4 +36,7 @@ To use the Loan Calculator, follow these steps: - The program will calculate and display the monthly payment amount. -Enjoy using the Loan Calculator! If you have any questions or encounter any issues, please feel free to reach out or open an issue. \ No newline at end of file +Enjoy using the Loan Calculator! If you have any questions or encounter any issues, please feel free to reach out or open an issue. + +## Pyodide-runnable +Yes — it is a pure-stdlib console program using only input() and print(). \ No newline at end of file diff --git a/projects/Location Search App (GUI)/README.md b/projects/Location Search App (GUI)/README.md new file mode 100644 index 000000000..72706cd0d --- /dev/null +++ b/projects/Location Search App (GUI)/README.md @@ -0,0 +1,17 @@ +# Location Search App (GUI) + +A Tkinter app with a search box that opens the entered location in Google Maps in your web browser. + +## How to run + +``` +pip install pyperclip +python "Location_Search_GUI.py" +``` + +## Dependencies + +tkinter (standard library), pyperclip. + +## Pyodide-runnable +No — it uses a Tkinter GUI and opens an external web browser. diff --git a/projects/Lyrics-Extractor/README.md b/projects/Lyrics-Extractor/README.md new file mode 100644 index 000000000..6c7d0d359 --- /dev/null +++ b/projects/Lyrics-Extractor/README.md @@ -0,0 +1,19 @@ +# Lyrics Extractor + +A console program that takes a song name and fetches its title and lyrics using the lyrics-extractor library and Google Custom Search. + +## How to run + +``` +pip install lyrics-extractor +python main.py +``` + +Requires a Google Custom Search API key and engine ID. + +## Dependencies + +lyrics-extractor. + +## Pyodide-runnable +No — it makes live HTTP requests to the Google Custom Search API to fetch lyrics. diff --git a/projects/ML-Notebooks_Beginners/README.md b/projects/ML-Notebooks_Beginners/README.md new file mode 100644 index 000000000..c7e0e47c4 --- /dev/null +++ b/projects/ML-Notebooks_Beginners/README.md @@ -0,0 +1,22 @@ +# ML Notebooks for Beginners + +A collection of beginner-friendly machine learning / mathematics notes. `notebooks/maths/algebra.py` contains notes on algebra concepts for ML (functions, plotting, etc.) stored in Jupyter-notebook JSON form. + +## How to run + +The file is a Jupyter notebook saved with a `.py` extension. Open it with Jupyter: + +``` +pip install jupyter matplotlib numpy +jupyter notebook notebooks/maths/algebra.py +``` + +## Dependencies + +- jupyter +- matplotlib +- numpy + +## Pyodide-runnable + +No — the file is a Jupyter notebook document (JSON), not a runnable Python script, and it depends on a notebook environment to execute. diff --git a/projects/MQTT Client/README.md b/projects/MQTT Client/README.md index dd2fafeb9..0f0dfd882 100644 --- a/projects/MQTT Client/README.md +++ b/projects/MQTT Client/README.md @@ -30,4 +30,7 @@ python3 MqttClient.py ``` ## Code Running -![Running Code](docs/imgs/running_code.png) \ No newline at end of file +![Running Code](docs/imgs/running_code.png) +## Pyodide-runnable + +No — it connects over the network to an external MQTT broker, which is not possible in a browser sandbox. diff --git a/projects/Madlibs Generator/README.md b/projects/Madlibs Generator/README.md new file mode 100644 index 000000000..6a7a78817 --- /dev/null +++ b/projects/Madlibs Generator/README.md @@ -0,0 +1,16 @@ +# Madlibs Generator + +A Tkinter app that lets you pick one of three story templates and fill in a noun, adjective, verb, and adverb to generate a Mad Lib story. + +## How to run + +``` +python main.py +``` + +## Dependencies + +tkinter (standard library). + +## Pyodide-runnable +No — it uses a Tkinter GUI, which cannot run in the browser. diff --git a/projects/Make-API/README.md b/projects/Make-API/README.md new file mode 100644 index 000000000..13a26b98c --- /dev/null +++ b/projects/Make-API/README.md @@ -0,0 +1,19 @@ +# Make-API + +A small Flask web API that returns a random quote (read from `quote.txt`) as JSON on the root route. + +## How to run + +``` +pip install flask +python app.py +``` + +Then open http://127.0.0.1:5000/ in a browser. + +## Dependencies + +flask. + +## Pyodide-runnable +No — it runs a Flask web server, which requires a server process and networking. diff --git a/projects/Market Financial Sentiment Predictor/readme.md b/projects/Market Financial Sentiment Predictor/readme.md index 3de73868b..3258aed06 100644 --- a/projects/Market Financial Sentiment Predictor/readme.md +++ b/projects/Market Financial Sentiment Predictor/readme.md @@ -5,4 +5,16 @@ All day various news about companies comes and not all people can track this new ## Libraries Used: - Numpy - Pandas -- Scikit-learn \ No newline at end of file +- Scikit-learn + +## How to run + +``` +pip install numpy pandas scikit-learn +python main.py +``` + +Requires the bundled `Financial Market News.csv` dataset. + +## Pyodide-runnable +No — it depends on scikit-learn and reads a CSV dataset from the local disk. \ No newline at end of file diff --git a/projects/Mastermind/README.md b/projects/Mastermind/README.md new file mode 100644 index 000000000..bb316e533 --- /dev/null +++ b/projects/Mastermind/README.md @@ -0,0 +1,16 @@ +# Mastermind + +A console number-guessing game: the program picks a random 4-digit number and tells you how many digits you got right after each guess until you crack it. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable +Yes — it is a pure-stdlib console game using only input(), print(), and random. diff --git a/projects/Medium Article Reader/README.md b/projects/Medium Article Reader/README.md new file mode 100644 index 000000000..f26cd1be5 --- /dev/null +++ b/projects/Medium Article Reader/README.md @@ -0,0 +1,19 @@ +# Medium Article Reader + +Scrapes an article from a URL, summarizes it with an OpenAI LLM via LangChain, and reads the summary or full text aloud with text-to-speech. + +## How to run + +``` +pip install pyttsx3 requests beautifulsoup4 openai python-dotenv langchain +python main.py +``` + +Requires an OpenAI API key in a `.env` file. + +## Dependencies + +pyttsx3, requests, beautifulsoup4, openai, python-dotenv, langchain. + +## Pyodide-runnable +No — it makes live HTTP/API requests and uses pyttsx3 text-to-speech. diff --git a/projects/Merge PDFs/README.md b/projects/Merge PDFs/README.md new file mode 100644 index 000000000..ffe91a66b --- /dev/null +++ b/projects/Merge PDFs/README.md @@ -0,0 +1,17 @@ +# Merge PDFs + +A CLI tool that lists PDF files in a directory, lets you include/exclude files interactively, and merges the selected PDFs into a single file. + +## How to run + +``` +pip install -r requirements.txt +python main.py [directory] +``` + +## Dependencies + +pikepdf. + +## Pyodide-runnable +No — it walks the real file system and uses pikepdf to read and write PDF files on disk. diff --git a/projects/Message-Spam/README.md b/projects/Message-Spam/README.md index 42f980c50..f579119d6 100644 --- a/projects/Message-Spam/README.md +++ b/projects/Message-Spam/README.md @@ -18,4 +18,7 @@ ## Customization - Edit the `messages` variable to change the list of messages to send. -- Adjust the sleep interval for message timing.. \ No newline at end of file +- Adjust the sleep interval for message timing.. + +## Pyodide-runnable +No — it uses pyautogui to control the keyboard and opens WhatsApp Web in a desktop browser. \ No newline at end of file diff --git a/projects/Minecraft-in-Python-main/README.md b/projects/Minecraft-in-Python-main/README.md new file mode 100644 index 000000000..10f7cd83f --- /dev/null +++ b/projects/Minecraft-in-Python-main/README.md @@ -0,0 +1,20 @@ +# Minecraft in Python + +A simple Minecraft-style voxel sandbox built with the `ursina` 3D game engine. You can walk around in first-person, place and break blocks of several textures, and switch block types with the number keys. + +## How to run + +``` +pip install ursina +python "Minecraft-in-Python-main/UrsaCraft_video.py" +``` + +(`ursina_intro.py` is a smaller introductory example.) + +## Dependencies + +- ursina + +## Pyodide-runnable + +No — `ursina` is a desktop 3D game engine that requires OpenGL, a windowing system and local asset files, none of which are available in the browser. diff --git a/projects/Mineral Processing Technology-Image Analytics/README.md b/projects/Mineral Processing Technology-Image Analytics/README.md index 428b56bc5..f97ad976c 100644 --- a/projects/Mineral Processing Technology-Image Analytics/README.md +++ b/projects/Mineral Processing Technology-Image Analytics/README.md @@ -59,3 +59,7 @@ Read the input images for the challenge form the “input” folder, process eac + +## Pyodide-runnable + +No — it requires the OpenCV (`cv2`) library, which is not available in Pyodide, and reads/writes image files from the real filesystem. diff --git a/projects/Mobile Document Scanner/README.md b/projects/Mobile Document Scanner/README.md new file mode 100644 index 000000000..1489f98eb --- /dev/null +++ b/projects/Mobile Document Scanner/README.md @@ -0,0 +1,21 @@ +# Mobile Document Scanner + +Turns a photo of a document into a clean, top-down "scanned" image. It detects the document edges, finds its contour, applies a four-point perspective transform, and thresholds the result to a black-and-white scan. + +## How to run + +``` +pip install opencv-python numpy imutils scikit-image +python scan.py --image path/to/photo.jpg +``` + +## Dependencies + +- opencv-python +- numpy +- imutils +- scikit-image + +## Pyodide-runnable + +No — it relies on OpenCV (`cv2`) with `imshow`/`waitKey` GUI windows, which cannot run in a browser. diff --git a/projects/Mongo CRUD/README.md b/projects/Mongo CRUD/README.md new file mode 100644 index 000000000..176b3fdcd --- /dev/null +++ b/projects/Mongo CRUD/README.md @@ -0,0 +1,21 @@ +# Mongo CRUD + +A small example of create/read/update/delete operations against a MongoDB database using `pymongo`. `conexao.py` opens the database connection and `usuario.py` defines a `Usuario` class with methods to insert, search, update, list and delete user records. + +## How to run + +Requires a running MongoDB server on `localhost:27017`. + +``` +pip install pymongo pandas +python usuario.py +``` + +## Dependencies + +- pymongo +- pandas + +## Pyodide-runnable + +No — it needs a network connection to a MongoDB database server, which is unavailable in the browser. diff --git a/projects/Morse-Code-Translator/README.md b/projects/Morse-Code-Translator/README.md new file mode 100644 index 000000000..9652fe151 --- /dev/null +++ b/projects/Morse-Code-Translator/README.md @@ -0,0 +1,17 @@ +# Morse Code Translator + +An interactive Morse code translator. Choose `E` to encrypt text into Morse code or `D` to decrypt Morse code back into plain text. + +## How to run + +``` +python morse.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/MorseCode Translator/README.md b/projects/MorseCode Translator/README.md new file mode 100644 index 000000000..cdd2e0b27 --- /dev/null +++ b/projects/MorseCode Translator/README.md @@ -0,0 +1,17 @@ +# Morse Code Translator + +Encrypts plain text into Morse code and decrypts Morse code back into text using a Morse code dictionary. The `main()` function runs a hard-coded demonstration of both operations. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — it is a pure-stdlib program with hard-coded input and `print()` output. diff --git a/projects/MotivateBot/README.md b/projects/MotivateBot/README.md new file mode 100644 index 000000000..bf4c27ba6 --- /dev/null +++ b/projects/MotivateBot/README.md @@ -0,0 +1,23 @@ +# MotivateBot + +Generates an inspirational message using the OpenAI GPT API. Given a prompt, it calls the OpenAI completion endpoint and prints the generated motivational text. + +## How to run + +``` +pip install openai +``` + +Set your OpenAI API key in `main.py`, then: + +``` +python main.py +``` + +## Dependencies + +- openai + +## Pyodide-runnable + +No — it makes network requests to the OpenAI API, which is not possible from a browser sandbox. diff --git a/projects/Movie recommendation/README.md b/projects/Movie recommendation/README.md new file mode 100644 index 000000000..cac660f30 --- /dev/null +++ b/projects/Movie recommendation/README.md @@ -0,0 +1,22 @@ +# Movie Recommendation Engine + +A content-based movie recommender. It combines movie keywords, cast, genres and director into a single text feature, vectorises it with `CountVectorizer`, computes cosine similarity between all movies, and prints the top 5 movies most similar to a chosen film. + +## How to run + +``` +pip install pandas numpy scikit-learn +python movie_recommendation_engine.py +``` + +`movie_dataset.csv` must be present in the same folder. + +## Dependencies + +- pandas +- numpy +- scikit-learn + +## Pyodide-runnable + +No — it depends on `scikit-learn`, which is not available in Pyodide. diff --git a/projects/MovieApi_and_ML/README.md b/projects/MovieApi_and_ML/README.md new file mode 100644 index 000000000..5af11c813 --- /dev/null +++ b/projects/MovieApi_and_ML/README.md @@ -0,0 +1,20 @@ +# Movie API and ML + +A Django web application that exposes a movie API and a machine-learning recommendation system. It combines movie metadata with ML algorithms to provide personalised movie recommendations through a web interface. + +## How to run + +``` +pip install -r movieapi/requirements.txt +python movieapi/manage.py runserver +``` + +See `movieapi/README.md` for details on configuring an API key. + +## Dependencies + +- Django and the packages listed in `movieapi/requirements.txt` (includes scikit-learn, pandas) + +## Pyodide-runnable + +No — it is a Django web server project that depends on a server runtime, external APIs and scikit-learn. diff --git a/projects/Music Player/README.md b/projects/Music Player/README.md new file mode 100644 index 000000000..f30d9e64f --- /dev/null +++ b/projects/Music Player/README.md @@ -0,0 +1,18 @@ +# Music Player (Sangeet) + +A desktop music player built with Tkinter. It lets you load a directory of audio tracks into a playlist and play, pause, resume and stop songs using `pygame.mixer`. + +## How to run + +``` +pip install pygame +python main.py +``` + +## Dependencies + +- pygame + +## Pyodide-runnable + +No — it is a Tkinter GUI application that uses `pygame.mixer` for audio and reads tracks from the local filesystem. diff --git a/projects/NASA-APOD/README.md b/projects/NASA-APOD/README.md index 49585d542..8bea7f357 100644 --- a/projects/NASA-APOD/README.md +++ b/projects/NASA-APOD/README.md @@ -11,3 +11,7 @@ pip install -r requirements.txt 2. Pass it in `credentials.py` 3. Run the application `python main.py` + +## Pyodide-runnable + +No — it makes network requests to the NASA APOD API and downloads the image file with `wget`. diff --git a/projects/Neurons/README.md b/projects/Neurons/README.md new file mode 100644 index 000000000..0d6489702 --- /dev/null +++ b/projects/Neurons/README.md @@ -0,0 +1,17 @@ +# Neurons + +A terminal animation that simulates "neurons" on a grid. Each neuron randomly dies or moves in one of four directions on every tick, producing an evolving pattern printed to the terminal. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — after a small edit removing the `os.system` terminal-clear (and the now-unused `os`/`sys` imports), it is a pure-stdlib program that only prints to the console. diff --git a/projects/Neurons/main.py b/projects/Neurons/main.py index f93df61b3..6fe7abc4e 100644 --- a/projects/Neurons/main.py +++ b/projects/Neurons/main.py @@ -1,6 +1,4 @@ -import os import random -import sys import time from enum import Enum from shutil import get_terminal_size @@ -9,14 +7,6 @@ TICKS_PER_SECOND = 60 -def clear() -> None: - """Clear the terminal based on OS.""" - if sys.platform == "nt": - os.system("cls") - else: - os.system("clear") - - class Direction(Enum): """Mapping of direction to int.""" @@ -103,7 +93,6 @@ def main() -> None: grid.set_neuron(15, 15) # Example placement grid.set_neuron(35, 35) while grid.neurons: - clear() print(grid) # Output the string representation of the grid grid.tick() time.sleep(1 / TICKS_PER_SECOND) diff --git a/projects/Number Guessing App/README.md b/projects/Number Guessing App/README.md new file mode 100644 index 000000000..76db94d9e --- /dev/null +++ b/projects/Number Guessing App/README.md @@ -0,0 +1,17 @@ +# Number Guessing App + +A two-mode number guessing game. In mode 1 the player guesses a random number chosen by the computer; in mode 2 the computer guesses the number the player picked, using feedback (too high / too low / correct). + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only (`random`). + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/OTP-Verfication-System/README.md b/projects/OTP-Verfication-System/README.md new file mode 100644 index 000000000..ed4bd8691 --- /dev/null +++ b/projects/OTP-Verfication-System/README.md @@ -0,0 +1,19 @@ +# OTP Verification System + +Generates a random 6-digit OTP, emails it to a user via Gmail's SMTP server, and then verifies the code the user types back in. + +## How to run + +``` +python main.py +``` + +Edit `main.py` with valid Gmail credentials (an app password) before running. + +## Dependencies + +Standard library only (`os`, `math`, `random`, `smtplib`). + +## Pyodide-runnable + +No — it connects to Gmail's SMTP server over the network to send email, which is not possible in a browser sandbox. diff --git a/projects/Online Trivia/README.md b/projects/Online Trivia/README.md new file mode 100644 index 000000000..df5bad9e4 --- /dev/null +++ b/projects/Online Trivia/README.md @@ -0,0 +1,18 @@ +# Online Trivia + +A console trivia quiz that fetches true/false questions from the Open Trivia Database API. It displays each question, accepts the player's answer, keeps score, and shows a final score at the end. + +## How to run + +``` +pip install requests +python main.py +``` + +## Dependencies + +- requests + +## Pyodide-runnable + +No — it fetches quiz questions from the opentdb.com API over the network using `requests`. diff --git a/projects/OpenCV_color_detect_in_live_feed/README.md b/projects/OpenCV_color_detect_in_live_feed/README.md new file mode 100644 index 000000000..b346fa221 --- /dev/null +++ b/projects/OpenCV_color_detect_in_live_feed/README.md @@ -0,0 +1,19 @@ +# OpenCV Color Detection in Live Feed + +Captures a live video feed from a webcam and detects the colour at the centre of the frame. It converts each frame to HSV, reads the hue value at the centre pixel, classifies it into a named colour, and overlays the result on the video. + +## How to run + +``` +pip install opencv-python numpy +python Open_CV_color_detect_in_live_feed.py +``` + +## Dependencies + +- opencv-python +- numpy + +## Pyodide-runnable + +No — it requires webcam access via OpenCV's `VideoCapture` and `imshow` GUI windows, which are unavailable in the browser. diff --git a/projects/Organize_Directory/README.md b/projects/Organize_Directory/README.md new file mode 100644 index 000000000..b781f51f6 --- /dev/null +++ b/projects/Organize_Directory/README.md @@ -0,0 +1,17 @@ +# Organize Directory + +A file organiser. Given a directory path, it sorts the files inside it into category folders (images, music, video, executables, archives, torrent, documents, code, design files) based on their file extensions. + +## How to run + +``` +python organizer.py +``` + +## Dependencies + +Standard library only (`os`, `shutil`). + +## Pyodide-runnable + +No — it walks and moves files on the real filesystem, which the browser sandbox does not provide. diff --git a/projects/Othello/README.md b/projects/Othello/README.md index d3ec38f0f..f7e8eef85 100644 --- a/projects/Othello/README.md +++ b/projects/Othello/README.md @@ -55,3 +55,7 @@ Currently the algorithm makes use of the following heuristics for a position's e ``` 5. Navigate to the `src` directory, run the `main.py` file and play the game! > P.S. You can change color modes using the 'L' key! + +## Pyodide-runnable + +No — it is a `pygame` GUI game that requires a display window and local image/font assets. diff --git a/projects/Otp_Generator/README.md b/projects/Otp_Generator/README.md new file mode 100644 index 000000000..477e60b5e --- /dev/null +++ b/projects/Otp_Generator/README.md @@ -0,0 +1,17 @@ +# OTP Generator + +Generates one-time passwords (OTPs) of a given length. The `Otp` class can produce OTPs made of digits only, digits with uppercase letters, digits with lowercase letters, or all three combined. + +## How to run + +``` +python otpGen.py +``` + +## Dependencies + +Standard library only (`random`). + +## Pyodide-runnable + +Yes — it is a pure-stdlib program that simply prints a generated OTP. diff --git a/projects/PDF_Reader/README.md b/projects/PDF_Reader/README.md new file mode 100644 index 000000000..856de1be0 --- /dev/null +++ b/projects/PDF_Reader/README.md @@ -0,0 +1,23 @@ +# PDF Reader + +An AI-powered PDF question-answering tool. It loads a PDF with LangChain, splits and embeds the text into a Chroma vector store, summarises the document with an OpenAI model, and answers user questions about the content interactively. + +## How to run + +``` +pip install -r requirements.txt +``` + +Set your OpenAI API key in a `.env` file as `API_KEY`, place the PDF in the folder, then: + +``` +python PDF_Reader.py +``` + +## Dependencies + +- openai, langchain, chromadb, pypdf and others (see `requirements.txt`) + +## Pyodide-runnable + +No — it makes network calls to the OpenAI API and depends on LangChain/Chroma, none of which work in a browser sandbox. diff --git a/projects/PONG/README.md b/projects/PONG/README.md new file mode 100644 index 000000000..359c4a20f --- /dev/null +++ b/projects/PONG/README.md @@ -0,0 +1,20 @@ +# PONG + +A single-player Pong game built with `pygame`. It features a difficulty menu (Easy / Medium / Hard), an AI-controlled opponent paddle, sound effects, and a score counter. + +## How to run + +``` +pip install pygame +python main.py +``` + +Run from the `PONG` directory so the `assets/` files are found. + +## Dependencies + +- pygame + +## Pyodide-runnable + +No — it is a `pygame` GUI game that requires a display window, sound, and local image/font assets. diff --git a/projects/Password Projects/Password Breach Frequency/README.md b/projects/Password Projects/Password Breach Frequency/README.md index 43f9343ea..257a53bc4 100644 --- a/projects/Password Projects/Password Breach Frequency/README.md +++ b/projects/Password Projects/Password Breach Frequency/README.md @@ -14,4 +14,7 @@ This will launch the interactive program, which will request the password from t ``` Enter password: 12345678 Your password hash has appeared 5,172,909 times in known data breaches. -``` \ No newline at end of file +``` +## Pyodide-runnable + +No — it queries the HaveIBeenPwned API over the network using `requests`. diff --git a/projects/Password Projects/Password Generator/README.md b/projects/Password Projects/Password Generator/README.md new file mode 100644 index 000000000..1cf782fab --- /dev/null +++ b/projects/Password Projects/Password Generator/README.md @@ -0,0 +1,17 @@ +# Password Generator + +Generates a random password of a user-chosen length, drawn from lowercase and uppercase letters, digits and punctuation symbols. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only (`random`, `string`). + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/Password Projects/Password Hashing/readme.md b/projects/Password Projects/Password Hashing/readme.md index c6e44fbc6..4ebd99060 100644 --- a/projects/Password Projects/Password Hashing/readme.md +++ b/projects/Password Projects/Password Hashing/readme.md @@ -24,4 +24,7 @@ c53ced31f785a1888b348de05057011fedd3be48 $ python hashing_passwords.py Github < hash-type : sha256 > 1720d8eaff790da6af4406905ba663d0cc6a6cea2b3e54e7384ac334a037f59d -``` \ No newline at end of file +``` +## Pyodide-runnable + +Yes — it is a pure-stdlib program; the password is supplied via command-line arguments rather than `input()`. diff --git a/projects/Password Projects/Password Meter/README.md b/projects/Password Projects/Password Meter/README.md new file mode 100644 index 000000000..8be26dff3 --- /dev/null +++ b/projects/Password Projects/Password Meter/README.md @@ -0,0 +1,17 @@ +# Password Meter + +Scores the strength of a password using a set of rules. It awards points for character count, uppercase/lowercase letters, numbers and symbols, and deducts points for weaknesses such as letters-only passwords or consecutive/sequential characters. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only (`string`). + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/Password Projects/Password_manager/README.md b/projects/Password Projects/Password_manager/README.md new file mode 100644 index 000000000..e49ad058f --- /dev/null +++ b/projects/Password Projects/Password_manager/README.md @@ -0,0 +1,19 @@ +# Password Manager + +A desktop password manager built with Tkinter. Users can store a website, email and password, generate random passwords, save the entries to a `data.json` file, and search for stored credentials. + +## How to run + +``` +pip install scipy +python main.py +``` + +## Dependencies + +- scipy (imported by `passgen.py`) +- Tkinter (bundled with Python) + +## Pyodide-runnable + +No — it is a Tkinter GUI application that also reads and writes a JSON file on the local filesystem. diff --git a/projects/Password Projects/README.md b/projects/Password Projects/README.md index d01ff2d54..cee78ee3e 100644 --- a/projects/Password Projects/README.md +++ b/projects/Password Projects/README.md @@ -21,3 +21,7 @@ The projects are: 6. [WiFi Password Generator](https://github.com/u749929/python-beginner-projects/tree/main/projects/Password%20Projects/WiFi%20Password%20Generator) * This program will attempt to get the password for the conected wifi network and retunr this password to the user. If no password is found, the program will return nothing. + +## Pyodide-runnable + +Mixed — see each sub-project README. The console tools (Password Generator, Password Hashing, Password Meter, strong-password-detector) run in Pyodide; Password Breach Frequency needs network access, Password Manager is a tkinter GUI, and the WiFi Password Generator uses Windows-only OS commands. diff --git a/projects/Password Projects/WiFi Password Generator/README.md b/projects/Password Projects/WiFi Password Generator/README.md new file mode 100644 index 000000000..dc7a8a148 --- /dev/null +++ b/projects/Password Projects/WiFi Password Generator/README.md @@ -0,0 +1,19 @@ +# WiFi Password Generator + +Retrieves the saved Wi-Fi passwords on a Windows machine. It runs `netsh wlan` commands to list the stored wireless profiles and prints each network name alongside its password. + +## How to run + +``` +python wifi.py +``` + +(Windows only.) + +## Dependencies + +Standard library only (`subprocess`). + +## Pyodide-runnable + +No — it shells out to Windows-only `netsh` system commands via `subprocess`, which the browser sandbox cannot run. diff --git a/projects/Password Projects/strong-password-detector/README.md b/projects/Password Projects/strong-password-detector/README.md new file mode 100644 index 000000000..d47481106 --- /dev/null +++ b/projects/Password Projects/strong-password-detector/README.md @@ -0,0 +1,17 @@ +# Strong Password Detector + +Checks whether a password is "strong" using regular expressions: it must be at least eight characters long and contain uppercase letters, lowercase letters and at least one digit. + +## How to run + +``` +python strong-password.py +``` + +## Dependencies + +Standard library only (`re`). + +## Pyodide-runnable + +Yes — it is a pure-stdlib program with hard-coded input and `print()` output. diff --git a/projects/Personal-Finance-tracker/README.md b/projects/Personal-Finance-tracker/README.md new file mode 100644 index 000000000..79ec4e08b --- /dev/null +++ b/projects/Personal-Finance-tracker/README.md @@ -0,0 +1,17 @@ +# Personal Finance Tracker + +A console-based personal finance manager. It lets you add expenses (name, amount, category), list all expenses, and calculate the total spent, all from a simple text menu. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/Pig_latin/README.md b/projects/Pig_latin/README.md new file mode 100644 index 000000000..5355fc36d --- /dev/null +++ b/projects/Pig_latin/README.md @@ -0,0 +1,17 @@ +# Pig Latin Converter + +Converts an English sentence into Pig Latin. `function.py` holds the conversion logic and `main.py` prompts the user for a string and prints the converted result. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/Pokemon Battle/README.md b/projects/Pokemon Battle/README.md new file mode 100644 index 000000000..db106eaac --- /dev/null +++ b/projects/Pokemon Battle/README.md @@ -0,0 +1,18 @@ +# Pokemon Battle + +A turn-based Pokemon battle game in the console. Two Pokemon (with types, moves, attack and defense stats) fight each other; type advantages affect damage, health bars update each turn, and text is printed one character at a time for effect. + +## How to run + +``` +pip install numpy +python pokemon.py +``` + +## Dependencies + +- numpy + +## Pyodide-runnable + +Yes — `numpy` is available in Pyodide and the program is otherwise a console game using `input()` and `print()`. diff --git a/projects/Port_Scaner/README.md b/projects/Port_Scaner/README.md index a49a7aef1..0dd574388 100644 --- a/projects/Port_Scaner/README.md +++ b/projects/Port_Scaner/README.md @@ -1,4 +1,20 @@ # Port Scanner Port Scanner is developed for: -checking if the port is open \ No newline at end of file +checking if the port is open + +## How to run + +```sh +pip install pyfiglet +python port_scanner.py +``` + +## Dependencies + +- pyfiglet +- socket (standard library) + +## Pyodide-runnable + +No - it uses real TCP sockets (`socket.connect_ex`), which are not available in the browser sandbox. \ No newline at end of file diff --git a/projects/Print_Colored_Text/README.md b/projects/Print_Colored_Text/README.md new file mode 100644 index 000000000..b2e26bdf1 --- /dev/null +++ b/projects/Print_Colored_Text/README.md @@ -0,0 +1,18 @@ +# Print Colored Text + +Prints a few lines of text to the terminal in different foreground and background colors using the colorama library. + +## How to run + +```sh +pip install colorama +python print_coloured_text.py +``` + +## Dependencies + +- colorama + +## Pyodide-runnable + +No - colorama emits ANSI escape codes for a real terminal and is not bundled with Pyodide. diff --git a/projects/ProjectEuler/README.md b/projects/ProjectEuler/README.md index c64323e21..5a626ce0a 100644 --- a/projects/ProjectEuler/README.md +++ b/projects/ProjectEuler/README.md @@ -7,6 +7,22 @@ Project Euler is a series of challenging mathematical/computer programming probl ## Where should I start? That depends on your background. There are two tables containing problems. The Recent problems table lists the ten most recently published problems, so if you are new to Project Euler then you may prefer to start with the Archives to get a feel for the different types/difficulties of our problems. The first one-hundred or so problems are generally considered to be easier than the problems which follow. In the archives table you will be able to see how many people have solved each problem; as a general rule of thumb the more people that have solved it, the easier it is. To assist further there is a difficulty rating system which may also help you decide where to start. You are able to sort the problems in the archives table on ID, Solved By, or Difficulty. +## How to run + +Each problem lives in its own `Problem N` folder. Run a solution directly, e.g.: + +```sh +python "Problem 1/p1.py" +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes - every solution is a pure-stdlib mathematical computation that prints a result, with no I/O beyond `print`. + diff --git a/projects/Python Banking System/readme.md b/projects/Python Banking System/readme.md index eb4e31971..7d1c25e69 100644 --- a/projects/Python Banking System/readme.md +++ b/projects/Python Banking System/readme.md @@ -20,3 +20,17 @@ The Python Banking System is a simple console-based banking application that all - Check your account balance at any time. - View your transaction history. +## How to run + +```sh +python Banking.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes - it is a pure-stdlib console program using only `input()`/`print()` and the `random` module. + diff --git a/projects/Python story generator/README.md b/projects/Python story generator/README.md new file mode 100644 index 000000000..4810ef12f --- /dev/null +++ b/projects/Python story generator/README.md @@ -0,0 +1,19 @@ +# Python Story Generator + +A Tkinter desktop application that generates a random short story (at least 100 words) by combining randomly chosen phrases. Click the "Generate Story" button to produce a new story. + +## How to run + +```sh +pip install ttkthemes +python story.py +``` + +## Dependencies + +- ttkthemes +- tkinter (standard library) + +## Pyodide-runnable + +No - it builds a Tkinter GUI with `ttkthemes`, which is not available in the browser sandbox. diff --git a/projects/QRCode Scanner/Readme.md b/projects/QRCode Scanner/Readme.md index 8c925f696..49492eaea 100644 --- a/projects/QRCode Scanner/Readme.md +++ b/projects/QRCode Scanner/Readme.md @@ -13,3 +13,13 @@ Simple, Just clone this repository to your local storage and run the below comma ## Execution Run the below command in command prompt or terminal. `python3 main.py` + +## Dependencies + +- opencv-python +- numpy +- pyzbar + +## Pyodide-runnable + +No - it uses OpenCV (`cv2.imshow`, webcam capture) and `pyzbar`, none of which are available in the browser sandbox. diff --git a/projects/QRCode-Generator/README.md b/projects/QRCode-Generator/README.md new file mode 100644 index 000000000..3856f373d --- /dev/null +++ b/projects/QRCode-Generator/README.md @@ -0,0 +1,19 @@ +# QRCode Generator + +A console script that asks for a website/URL, encodes it into a QR code, and saves the image to the `static` folder as `qrcode.jpg`. + +## How to run + +```sh +pip install qrcode pillow +python main.py +``` + +## Dependencies + +- qrcode +- pillow + +## Pyodide-runnable + +No - it depends on the `qrcode` and `pillow` packages and writes an image file to a real disk path. diff --git a/projects/Qt5_YouTube/README.md b/projects/Qt5_YouTube/README.md new file mode 100644 index 000000000..698148fce --- /dev/null +++ b/projects/Qt5_YouTube/README.md @@ -0,0 +1,19 @@ +# Qt5 YouTube Player + +A PyQt5 desktop application that plays local video files and YouTube videos, with playback controls for position, volume, and speed. + +## How to run + +```sh +pip install PyQt5 youtube_dl +python main.py +``` + +## Dependencies + +- PyQt5 +- youtube_dl + +## Pyodide-runnable + +No - it builds a PyQt5 GUI and uses `youtube_dl` for network access, neither of which works in the browser sandbox. diff --git a/projects/QuickWordCloud/README.md b/projects/QuickWordCloud/README.md index 3f33216ae..5abe5b7c8 100644 --- a/projects/QuickWordCloud/README.md +++ b/projects/QuickWordCloud/README.md @@ -30,3 +30,7 @@ pytest From IDE right click and run.(As per the IDE options) From terminal python main.py + +## Pyodide-runnable + +No - it depends on the `wordcloud` package and opens a `matplotlib` GUI window, which are not supported in the Pyodide sandbox. diff --git a/projects/Quiz Game/README.md b/projects/Quiz Game/README.md new file mode 100644 index 000000000..d3124f1ce --- /dev/null +++ b/projects/Quiz Game/README.md @@ -0,0 +1,18 @@ +# Quiz Game + +A console-based quiz game that asks three questions, scores the player's answers, and stores each player's name and score in a local SQLite database (`quiz_game.db`). Previous scores are displayed after each game. Run `setup_db.py` once first to create the database and table. + +## How to run + +```sh +python setup_db.py +python main.py +``` + +## Dependencies + +Standard library only (uses `sqlite3`). + +## Pyodide-runnable + +Yes - it is a pure-stdlib console program; `sqlite3` is supported in Pyodide and the database is created in the in-memory virtual filesystem. diff --git a/projects/Random-Quote-Generator/README.md b/projects/Random-Quote-Generator/README.md index 804bd58fa..5abdaaaa8 100644 --- a/projects/Random-Quote-Generator/README.md +++ b/projects/Random-Quote-Generator/README.md @@ -78,6 +78,10 @@ We welcome contributions to improve and expand this Random Quote Generator proje 7. Wait for your pull request to be reviewed and merged. +## Pyodide-runnable + +No - it builds a Tkinter GUI and uses `requests` to call an external API, neither of which works in the browser sandbox. + ## Acknowledgments - Thanks to [Quotable API](https://quotable.io/) for providing the quotes. diff --git a/projects/Rename_Images/README.md b/projects/Rename_Images/README.md new file mode 100644 index 000000000..626408319 --- /dev/null +++ b/projects/Rename_Images/README.md @@ -0,0 +1,17 @@ +# Rename Images + +A console tool that walks a given directory and lets the user interactively rename each image file (jpg, png, jpeg). + +## How to run + +```sh +python rename_images.py +``` + +## Dependencies + +Standard library only (uses `os`). + +## Pyodide-runnable + +No - it walks and renames files on the real filesystem with `os.listdir`/`os.rename`, which is not available in the browser sandbox. diff --git a/projects/Resize_Image/README.md b/projects/Resize_Image/README.md new file mode 100644 index 000000000..e92816a21 --- /dev/null +++ b/projects/Resize_Image/README.md @@ -0,0 +1,18 @@ +# Resize Image + +A console tool that resizes one image or all images in a directory to a user-specified width and height, then saves the results to an output folder. + +## How to run + +```sh +pip install pillow +python resize.py +``` + +## Dependencies + +- pillow + +## Pyodide-runnable + +No - it uses the `pillow` package and reads/writes image files on the real filesystem, which is not available in the browser sandbox. diff --git a/projects/RestrauntAPI/README.md b/projects/RestrauntAPI/README.md index 80d7874ec..a14e41113 100644 --- a/projects/RestrauntAPI/README.md +++ b/projects/RestrauntAPI/README.md @@ -20,3 +20,22 @@ This endpoint is used to create orders. View Past Orders: - Endpoint: GET http://127.0.0.1:8000/orders// You can use this endpoint to view the previous orders of a user by providing their email address. + +## How to run + +```sh +pip install -r requirements.txt +cd Restraunt_API +python manage.py migrate +python manage.py runserver +``` + +## Dependencies + +- Django +- djangorestframework +- Pillow + +## Pyodide-runnable + +No - it is a Django web application that requires a running HTTP server and database, which cannot run in the browser sandbox. diff --git a/projects/Rock_Paper_Scissors/README.md b/projects/Rock_Paper_Scissors/README.md new file mode 100644 index 000000000..576353848 --- /dev/null +++ b/projects/Rock_Paper_Scissors/README.md @@ -0,0 +1,17 @@ +# Rock Paper Scissors + +A console Rock-Paper-Scissors game. The player picks a move each round, the computer picks randomly, and the score is tracked until the player chooses to exit. + +## How to run + +```sh +python main.py +``` + +## Dependencies + +Standard library only (uses `random`). + +## Pyodide-runnable + +Yes - it is a pure-stdlib console program using only `input()`/`print()` and the `random` module. diff --git a/projects/Roll_A_dice/README.md b/projects/Roll_A_dice/README.md new file mode 100644 index 000000000..72c2f8128 --- /dev/null +++ b/projects/Roll_A_dice/README.md @@ -0,0 +1,21 @@ +# Roll A Dice + +A console game where the player rolls a virtual dice and is given a task or riddle based on the number rolled. The player's name, roll, and answer are recorded in a MySQL database. + +## How to run + +```sh +pip install mysql-connector-python +python dice.py +``` + +You must have a MySQL server running and update the connection credentials in `dice.py`. + +## Dependencies + +- mysql-connector-python +- random (standard library) + +## Pyodide-runnable + +No - it connects to a MySQL database server, which cannot run in the browser sandbox. diff --git a/projects/Rubik-tracking/README.md b/projects/Rubik-tracking/README.md new file mode 100644 index 000000000..b6cdfb0c2 --- /dev/null +++ b/projects/Rubik-tracking/README.md @@ -0,0 +1,20 @@ +# Rubik Tracking + +Tracks a green-colored object (e.g., a Rubik's cube) in a live webcam feed using OpenCV color masking and draws its motion trail on screen. + +## How to run + +```sh +pip install opencv-python imutils numpy +python rubik-tracking.py +``` + +## Dependencies + +- opencv-python +- imutils +- numpy + +## Pyodide-runnable + +No - it captures live webcam video and shows OpenCV windows, neither of which works in the browser sandbox. diff --git a/projects/SMS_ChatBot/README.md b/projects/SMS_ChatBot/README.md index f03424557..0041294ed 100644 --- a/projects/SMS_ChatBot/README.md +++ b/projects/SMS_ChatBot/README.md @@ -92,4 +92,7 @@ python3 main.py Now take out your phone and text your Twilio Phone Number a question or prompt so that OpenAI can answer it or generate text! -![picture alt](https://assets.cdn.prod.twilio.com/images/Screenshot_2023-01-12_at_1.44.48_PM.format-webp.webp) \ No newline at end of file +![picture alt](https://assets.cdn.prod.twilio.com/images/Screenshot_2023-01-12_at_1.44.48_PM.format-webp.webp) +## Pyodide-runnable + +No — it is a Flask web server that integrates the Twilio SMS API and the OpenAI API, all requiring network access and a server that Pyodide cannot provide. diff --git a/projects/Sales Optimizer/Readme.md b/projects/Sales Optimizer/Readme.md index f4bad6180..6e7988e93 100644 --- a/projects/Sales Optimizer/Readme.md +++ b/projects/Sales Optimizer/Readme.md @@ -16,4 +16,15 @@ The retail stores always face a problem of tension between demand and supply. So - Numpy - Pandas - Seaborn -- Scikit-learn \ No newline at end of file +- Scikit-learn + +## How to run + +```sh +pip install numpy pandas seaborn scikit-learn matplotlib +python main.py +``` + +## Pyodide-runnable + +No - it depends on `seaborn`, `scikit-learn`, and `matplotlib` GUI plotting, which are not available in the Pyodide sandbox. \ No newline at end of file diff --git a/projects/Scientific-Calculator/ReadMe.md b/projects/Scientific-Calculator/ReadMe.md index d5244b448..898dd9abd 100644 --- a/projects/Scientific-Calculator/ReadMe.md +++ b/projects/Scientific-Calculator/ReadMe.md @@ -2,3 +2,17 @@

    ![image](https://user-images.githubusercontent.com/89387048/196713806-d9a3febc-5896-4ef7-a31d-373ee533347f.png) + +## How to run + +```sh +python Scientific_CalC.py +``` + +## Dependencies + +Standard library only (uses `tkinter` and `math`). + +## Pyodide-runnable + +No - it builds a Tkinter GUI, which is not available in the browser sandbox. diff --git a/projects/ScreenRecorder/README.md b/projects/ScreenRecorder/README.md index 55aae2d67..28d978cbe 100644 --- a/projects/ScreenRecorder/README.md +++ b/projects/ScreenRecorder/README.md @@ -13,3 +13,14 @@ pip install pyautogui # Now run the main.py ## Great.............You Did it, Check your folder and find "ScreenRecord.mp4" file + +## Dependencies + +- opencv-python +- pywin32 +- numpy +- pyautogui + +## Pyodide-runnable + +No - it captures the real screen with `pyautogui` and uses Windows-specific `win32api`, neither of which works in the browser sandbox. diff --git a/projects/Seek_with_hand_track/README.md b/projects/Seek_with_hand_track/README.md new file mode 100644 index 000000000..ec6c62056 --- /dev/null +++ b/projects/Seek_with_hand_track/README.md @@ -0,0 +1,21 @@ +# Seek with Hand Track + +Uses a webcam and MediaPipe hand tracking to detect hand gestures and send left/right arrow keypresses, allowing you to seek a video by tilting your hand. + +## How to run + +```sh +pip install opencv-python mediapipe pyautogui numpy +python main.py +``` + +## Dependencies + +- opencv-python +- mediapipe +- pyautogui +- numpy + +## Pyodide-runnable + +No - it captures live webcam video, runs MediaPipe, and simulates keypresses with `pyautogui`, none of which work in the browser sandbox. diff --git a/projects/Selfie_with_Python/README.md b/projects/Selfie_with_Python/README.md new file mode 100644 index 000000000..755456f3d --- /dev/null +++ b/projects/Selfie_with_Python/README.md @@ -0,0 +1,19 @@ +# Selfie with Python + +Opens the webcam in an OpenCV window; press Space to capture a selfie (saved as a JPG) and Escape to quit. + +## How to run + +```sh +pip install opencv-python +python Selfie_with_Python.py +``` + +## Dependencies + +- opencv-python +- time (standard library) + +## Pyodide-runnable + +No - it captures live webcam video and shows OpenCV windows, neither of which works in the browser sandbox. diff --git a/projects/Send-Email/README.md b/projects/Send-Email/README.md new file mode 100644 index 000000000..315ba63c2 --- /dev/null +++ b/projects/Send-Email/README.md @@ -0,0 +1,19 @@ +# Send Email + +A script that connects to Gmail's SMTP server and sends a plain-text email. The sender's username and password are read from environment variables. + +## How to run + +```sh +export username=your@gmail.com +export password=yourpassword +python main.py +``` + +## Dependencies + +Standard library only (uses `smtplib` and `os`). + +## Pyodide-runnable + +No - it opens an SMTP network connection to Gmail's mail server, which is blocked in the browser sandbox. diff --git a/projects/Simple-Plagiarism-Checker-Project/README.md b/projects/Simple-Plagiarism-Checker-Project/README.md index 2ab5a87eb..e137687ff 100644 --- a/projects/Simple-Plagiarism-Checker-Project/README.md +++ b/projects/Simple-Plagiarism-Checker-Project/README.md @@ -14,3 +14,7 @@ Web application of Plagiarism Checker using Python-Flask. TF-IDF and cosine simi 3. While Flask addresses itself as a "micro-framework", it is not lacking in features or power, especially with a clutch of extensions to support features such as authentication, databases and so on 4. Comprehensive documentation available + +## Pyodide-runnable + +No — it is a Flask web server application, and Flask cannot serve requests under Pyodide in the browser. diff --git a/projects/SketchifyMe/README.md b/projects/SketchifyMe/README.md index 4828b2502..d05c8fe61 100644 --- a/projects/SketchifyMe/README.md +++ b/projects/SketchifyMe/README.md @@ -1,3 +1,17 @@ # SketchifyMe -Seamlessly convert your images into pencil-sketch renditions, facilitating the effortless creation of your sketches. \ No newline at end of file +Seamlessly convert your images into pencil-sketch renditions, facilitating the effortless creation of your sketches. +## How to run + +``` +pip install opencv-python numpy +python image_to_sketch.py +``` + +## Dependencies + +opencv-python (cv2), numpy + +## Pyodide-runnable + +No — it relies on OpenCV's `cv2.imshow`/`waitKey` GUI windows and reads an image file from the local disk. diff --git a/projects/Skycast/README.md b/projects/Skycast/README.md index 360bfe833..29f6a399f 100644 --- a/projects/Skycast/README.md +++ b/projects/Skycast/README.md @@ -72,4 +72,7 @@ Contributions to SkyCast are welcome! Here are some ways you can contribute: - Refactor the code for better performance and maintainability. - Fix bugs and issues reported by users. -Thank you for your contributions to SkyCast! 🌤️ \ No newline at end of file +Thank you for your contributions to SkyCast! 🌤️ +## Pyodide-runnable + +No — it is a Streamlit web app that also makes live HTTP requests to the Weatherbit API; neither Streamlit's server nor network calls run under Pyodide. diff --git a/projects/Slice-Audio/README.md b/projects/Slice-Audio/README.md index ab9dc5987..e098b7420 100644 --- a/projects/Slice-Audio/README.md +++ b/projects/Slice-Audio/README.md @@ -21,3 +21,7 @@ You can use pip - pip install pydub More information on pydub here - https://pypi.org/project/pydub/ Use the python script slicingAudio.py + +## Pyodide-runnable + +No — pydub depends on ffmpeg and reads/writes mp3 files on the local disk, which is unavailable in Pyodide. diff --git a/projects/Snake Game/README.md b/projects/Snake Game/README.md new file mode 100644 index 000000000..015df7158 --- /dev/null +++ b/projects/Snake Game/README.md @@ -0,0 +1,18 @@ +# Snake Game + +A classic Snake game built with Pygame. The player controls a snake that grows as it eats food, tracking a score and a high score, with game-over and play-again screens. + +## How to run + +``` +pip install pygame +python src/main.py +``` + +## Dependencies + +pygame + +## Pyodide-runnable + +No — it uses Pygame for windowed graphics and real-time keyboard input, which require a desktop display. diff --git a/projects/Social_media_content_creation/README.md b/projects/Social_media_content_creation/README.md index 339e3d6ed..acaf749a8 100644 --- a/projects/Social_media_content_creation/README.md +++ b/projects/Social_media_content_creation/README.md @@ -14,3 +14,7 @@ To get started with these projects, simply clone this repository and navigate to We welcome contributions! If you have a project idea related to social media content creation, feel free to add it to this folder. Happy coding! + +## Pyodide-runnable + +No — this folder is a placeholder containing no Python code to run. diff --git a/projects/Socket/README.md b/projects/Socket/README.md index a342ef040..275125d10 100644 --- a/projects/Socket/README.md +++ b/projects/Socket/README.md @@ -14,3 +14,7 @@ python ./server.py # then run the client python ./client.py ``` + +## Pyodide-runnable + +No — it opens real TCP sockets for client/server networking, which is not available in the browser sandbox. diff --git a/projects/SongsMashup/README.md b/projects/SongsMashup/README.md new file mode 100644 index 000000000..24d466649 --- /dev/null +++ b/projects/SongsMashup/README.md @@ -0,0 +1,18 @@ +# Songs Mashup + +A Flask web app that downloads songs from YouTube for a given singer, slices each track into short snippets, merges them into a single mashup audio file, zips it, and emails the result to the user. + +## How to run + +``` +pip install flask numpy pandas pytube pydub youtube-search +python SongsMashup.py +``` + +## Dependencies + +flask, numpy, pandas, pytube, pydub, youtube-search, smtplib (standard library) + +## Pyodide-runnable + +No — it is a Flask server that downloads from YouTube, processes audio with pydub/ffmpeg, and sends email via SMTP, none of which work in Pyodide. diff --git a/projects/Space Shooter/README.md b/projects/Space Shooter/README.md new file mode 100644 index 000000000..691578918 --- /dev/null +++ b/projects/Space Shooter/README.md @@ -0,0 +1,18 @@ +# Space Shooter + +A 2D space shooter game built with Pygame. Pilot a spaceship, fire lasers at incoming enemy ships, dodge enemy lasers and homing missiles, and rack up a score, with menu and game-over screens. + +## How to run + +``` +pip install pygame +python main.py +``` + +## Dependencies + +pygame + +## Pyodide-runnable + +No — it uses Pygame for windowed graphics and loads image/font assets, which require a desktop display. diff --git a/projects/Speed-Type-test/README.md b/projects/Speed-Type-test/README.md new file mode 100644 index 000000000..a733972f2 --- /dev/null +++ b/projects/Speed-Type-test/README.md @@ -0,0 +1,18 @@ +# Speed Typing Test + +A typing speed test built with Pygame. A random sentence is displayed; type it as fast and accurately as you can, and the app reports your time, accuracy percentage, and words per minute. + +## How to run + +``` +pip install pygame +python "Speed Typing Test Python/speed typing.py" +``` + +## Dependencies + +pygame + +## Pyodide-runnable + +No — it uses Pygame for windowed graphics and loads image assets, which require a desktop display. diff --git a/projects/Split_Tip/README.md b/projects/Split_Tip/README.md new file mode 100644 index 000000000..c67044a6b --- /dev/null +++ b/projects/Split_Tip/README.md @@ -0,0 +1,17 @@ +# Split Tip Calculator + +A simple console tip calculator. Enter a bill amount, a tip percentage, and the number of people, and it prints how much each person owes in tip and in total. + +## How to run + +``` +python Tip_calculator.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program that only uses `input()`/`print()`. diff --git a/projects/Spotify Player/README.md b/projects/Spotify Player/README.md new file mode 100644 index 000000000..daa0adcac --- /dev/null +++ b/projects/Spotify Player/README.md @@ -0,0 +1,20 @@ +# Spotify Player + +A command-line Spotify tool that can play a searched song, recommend songs based on three seed tracks, and generate a new playlist similar to an existing one. It also texts the generated playlist link via Twilio. + +## How to run + +``` +pip install spotipy colorama twilio +python main.py +``` + +Configuration values must be placed in `utils/user_secrets.py` (see `utils/user_secrets_example.py`). + +## Dependencies + +spotipy, colorama, twilio + +## Pyodide-runnable + +No — it calls the Spotify Web API and Twilio API over the network, opens a browser, and clears the terminal with `os.system`. diff --git a/projects/Stock-Market-Dashboard/README.md b/projects/Stock-Market-Dashboard/README.md new file mode 100644 index 000000000..7b10b96df --- /dev/null +++ b/projects/Stock-Market-Dashboard/README.md @@ -0,0 +1,18 @@ +# Stock Market Dashboard + +A Streamlit dashboard that visualizes S&P 500 stock data with Bollinger Bands, MACD, and RSI indicators computed from closing prices. Companies are grouped by sector and selected by ticker. + +## How to run + +``` +pip install pandas yfinance ta streamlit +streamlit run dashboard.py +``` + +## Dependencies + +pandas, yfinance, ta, streamlit + +## Pyodide-runnable + +No — it is a Streamlit web app that downloads stock data and the S&P 500 company list over the network. diff --git a/projects/Subnetting Flsm/README.md b/projects/Subnetting Flsm/README.md index 0b44d1c03..fb5454f84 100644 --- a/projects/Subnetting Flsm/README.md +++ b/projects/Subnetting Flsm/README.md @@ -4,3 +4,17 @@ Subnetting_FLSM is developed for: 1.Calculation and analyzing number of hosts 2.Distribution hosts for each subnet by FLSM method 3.Transferring unique network address, broadcast address, usable hosts + +## How to run + +``` +python subnetting_flsm.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program that only uses `input()`/`print()`. diff --git a/projects/Subtitle_synchronizer.py/README.md b/projects/Subtitle_synchronizer.py/README.md new file mode 100644 index 000000000..695b93019 --- /dev/null +++ b/projects/Subtitle_synchronizer.py/README.md @@ -0,0 +1,19 @@ +# Subtitle Synchronizer + +A console tool that shifts all timestamps in an SRT subtitle file by a given number of milliseconds, reading from `input.srt` and writing the synchronized result to `output.srt`. + +## How to run + +``` +python main.py +``` + +Place an `input.srt` file in the same folder before running. + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +No — it reads `input.srt` and writes `output.srt` from the real local filesystem, which is not accessible under Pyodide. diff --git a/projects/Sudoku-Solver/README.md b/projects/Sudoku-Solver/README.md new file mode 100644 index 000000000..7bf654799 --- /dev/null +++ b/projects/Sudoku-Solver/README.md @@ -0,0 +1,18 @@ +# Sudoku Solver (GUI) + +A Pygame-based Sudoku game. It generates a random grid you can play interactively, supports hints, tracks wrong answers and elapsed time, and can visually solve the puzzle with a backtracking animation. + +## How to run + +``` +pip install pygame +python SudokuGUI.py +``` + +## Dependencies + +pygame + +## Pyodide-runnable + +No — it uses Pygame for windowed graphics, mouse/keyboard input, and loads image assets. diff --git a/projects/Sudoku_solver/README.md b/projects/Sudoku_solver/README.md new file mode 100644 index 000000000..6369052e5 --- /dev/null +++ b/projects/Sudoku_solver/README.md @@ -0,0 +1,17 @@ +# Sudoku Solver + +A console Sudoku generator and solver. It randomly generates a valid Sudoku board, removes cells to create a puzzle, then solves it with a backtracking algorithm, printing the full, unsolved, and solved boards. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program that only uses `random` and `print()`. diff --git a/projects/TennisTournamentSim/README.md b/projects/TennisTournamentSim/README.md new file mode 100644 index 000000000..bf742775d --- /dev/null +++ b/projects/TennisTournamentSim/README.md @@ -0,0 +1,17 @@ +# Tennis Tournament Simulator + +A console simulation of a four-player single-elimination tennis tournament. Players get random names, schools, and skill levels; match outcomes are decided by skill plus random variation, and the champion and runner-up are announced. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using only `random`, `time`, and `print()`. diff --git a/projects/Tesla/README.md b/projects/Tesla/README.md new file mode 100644 index 000000000..2bfa31f43 --- /dev/null +++ b/projects/Tesla/README.md @@ -0,0 +1,18 @@ +# Tesla Self-Driving Car + +A Pygame simulation of a self-driving car. A car follows a track image by sampling pixel colors ahead of it to decide when to drive straight or turn. + +## How to run + +``` +pip install pygame +python tesla.py +``` + +## Dependencies + +pygame + +## Pyodide-runnable + +No — it uses Pygame for windowed graphics and loads track/car image assets, which require a desktop display. diff --git a/projects/Tetris Game/README.md b/projects/Tetris Game/README.md new file mode 100644 index 000000000..2051e6028 --- /dev/null +++ b/projects/Tetris Game/README.md @@ -0,0 +1,18 @@ +# Tetris Game + +A classic Tetris game built with Pygame. Falling tetrominoes can be moved, rotated, and hard-dropped; full lines are cleared for points, and the game ends when pieces stack to the top. + +## How to run + +``` +pip install pygame +python tetrisGame.py +``` + +## Dependencies + +pygame + +## Pyodide-runnable + +No — it uses Pygame for windowed graphics and real-time keyboard input. diff --git a/projects/Text Editor/README.md b/projects/Text Editor/README.md new file mode 100644 index 000000000..e901beb3c --- /dev/null +++ b/projects/Text Editor/README.md @@ -0,0 +1,24 @@ +# Text Editor + +Notepad-style text editor applications built with Tkinter. `Small-version/txteditor.py` is a compact editor with open/save/close and light/dark themes; `Text-Editor-master/Text_Editor.py` is a fuller-featured editor. + +## How to run + +``` +pip install pygments +python Small-version/txteditor.py +``` + +or + +``` +python Text-Editor-master/Text_Editor.py +``` + +## Dependencies + +tkinter (standard library); the small version also imports pygments. + +## Pyodide-runnable + +No — both editors use the Tkinter GUI toolkit, which is not available in the browser sandbox. diff --git a/projects/Text Summarizer/READme.md b/projects/Text Summarizer/READme.md index b50e85e42..19a0fab90 100644 --- a/projects/Text Summarizer/READme.md +++ b/projects/Text Summarizer/READme.md @@ -32,3 +32,7 @@ That's it! You've successfully run the text summarizer project. It will tokenize Make sure to replace **"text_summarizer.py"** with the name you've given to your Python script if it's different. ### HAPPY CODING ALL ☺️ + +## Pyodide-runnable + +No — it depends on spaCy and the large `en_core_web_md` language model, which are not available in Pyodide. diff --git a/projects/Text to Speech/readme.md b/projects/Text to Speech/readme.md index b59be45e4..2f503699a 100644 --- a/projects/Text to Speech/readme.md +++ b/projects/Text to Speech/readme.md @@ -11,3 +11,7 @@ - gTTS - os + +## Pyodide-runnable + +No — gTTS sends text to Google's Translate TTS API over the network and writes/plays an mp3 file locally. diff --git a/projects/Text-to-Image Generation Project/README.md b/projects/Text-to-Image Generation Project/README.md new file mode 100644 index 000000000..48510735a --- /dev/null +++ b/projects/Text-to-Image Generation Project/README.md @@ -0,0 +1,21 @@ +# Text-to-Image Generation Project + +A Jupyter notebook (`Text_to_image.ipynb`) that demonstrates generating images from text prompts using a deep-learning text-to-image model. + +## How to run + +Open the notebook in Jupyter: + +```bash +jupyter notebook Text_to_image.ipynb +``` + +Install the deep-learning dependencies referenced inside the notebook (e.g. `diffusers`, `torch`, `transformers`). + +## Dependencies + +Deep-learning libraries such as `diffusers`, `torch`, and `transformers` (see the notebook cells). + +## Pyodide-runnable + +No — it is a Jupyter notebook relying on heavy GPU-oriented deep-learning libraries that cannot run in the browser. diff --git a/projects/TextDetection/README.md b/projects/TextDetection/README.md new file mode 100644 index 000000000..d7c28fa04 --- /dev/null +++ b/projects/TextDetection/README.md @@ -0,0 +1,20 @@ +# Text Detection + +A script that detects and prints text found in an image using the Google Cloud Vision API. + +## How to run + +``` +pip install google-cloud-vision +python TextDetection.py +``` + +Requires Google Cloud credentials configured in the environment. + +## Dependencies + +google-cloud-vision + +## Pyodide-runnable + +No — it calls the Google Cloud Vision API over the network and requires cloud authentication. diff --git a/projects/Text_to_SpreadSheet/README.md b/projects/Text_to_SpreadSheet/README.md new file mode 100644 index 000000000..f935696a5 --- /dev/null +++ b/projects/Text_to_SpreadSheet/README.md @@ -0,0 +1,18 @@ +# Text to Spreadsheet + +A script that reads every `.txt` file in a directory and writes each file's lines into a separate column of an Excel worksheet, saving the result as an `.xlsx` file. + +## How to run + +``` +pip install openpyxl +python textToSheet.py +``` + +## Dependencies + +openpyxl + +## Pyodide-runnable + +No — it walks the real local directory with `os.listdir()` and writes an `.xlsx` file to disk. diff --git a/projects/Tic-Tac-Toe/README.md b/projects/Tic-Tac-Toe/README.md new file mode 100644 index 000000000..eeed1c75c --- /dev/null +++ b/projects/Tic-Tac-Toe/README.md @@ -0,0 +1,24 @@ +# Tic-Tac-Toe + +Two implementations of the classic 3×3 game: + +- **`Tic-Tac-Toe-Terminal/`** — a console version played with `input()`/`print()`. +- **`TicTacToe-GUI/`** — a Tkinter version (`tic tac.py`, plus a one-player mode + in `oneplayermode.py`) using `X.png` / `O.jpg` images. + +## How to run + +```bash +python Tic-Tac-Toe-Terminal/main.py # terminal version +python TicTacToe-GUI/tic\ tac.py # GUI version +``` + +## Dependencies + +- Terminal version: standard library only. +- GUI version: `tkinter` (ships with the standard Python installer). + +## Pyodide-runnable + +Partly. The terminal version (`Tic-Tac-Toe-Terminal/main.py`) is pure-stdlib and +runs in the in-browser Pyodide playground. The Tkinter GUI version does not. diff --git a/projects/Tic-Tac-Toe/Tic-Tac-Toe-Terminal/README.md b/projects/Tic-Tac-Toe/Tic-Tac-Toe-Terminal/README.md new file mode 100644 index 000000000..b0f5f5a0e --- /dev/null +++ b/projects/Tic-Tac-Toe/Tic-Tac-Toe-Terminal/README.md @@ -0,0 +1,17 @@ +# Tic-Tac-Toe (Terminal) + +A console Tic-Tac-Toe game where a human plays against a simple AI computer opponent. The board uses numbers 1-9, the computer follows a basic win/block/corner/center strategy, and you can keep playing rounds until you choose to stop. + +## How to run + +```bash +python main.py +``` + +## Dependencies + +Standard library only (`random`). + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using `input()`/`print()`. diff --git a/projects/Tic-Tac-Toe/TicTacToe-GUI/README.md b/projects/Tic-Tac-Toe/TicTacToe-GUI/README.md new file mode 100644 index 000000000..6da2802f6 --- /dev/null +++ b/projects/Tic-Tac-Toe/TicTacToe-GUI/README.md @@ -0,0 +1,18 @@ +# Tic-Tac-Toe (GUI) + +A graphical Tic-Tac-Toe game built with `pygame`. `tic tac.py` provides a mouse-driven board with X/O image tiles and a computer opponent; `oneplayermode.py` is a terminal-driven variant of the same game logic. + +## How to run + +```bash +pip install pygame +python "tic tac.py" +``` + +## Dependencies + +- pygame + +## Pyodide-runnable + +No — it depends on `pygame`, which opens a desktop window and cannot run in a browser. diff --git a/projects/TicTacToe-TylerPear/README.md b/projects/TicTacToe-TylerPear/README.md new file mode 100644 index 000000000..f2ac60ae4 --- /dev/null +++ b/projects/TicTacToe-TylerPear/README.md @@ -0,0 +1,17 @@ +# Tic-Tac-Toe (TylerPear) + +A two-player console Tic-Tac-Toe game. Players enter moves using position codes (TL, TM, TR, ML, MM, MR, BL, BM, BR) and the board is redrawn after every turn until someone wins or the game ends in a cat's game. + +## How to run + +```bash +python main.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program using `input()`/`print()`. diff --git a/projects/Tile Matching/README.md b/projects/Tile Matching/README.md new file mode 100644 index 000000000..65f7f7720 --- /dev/null +++ b/projects/Tile Matching/README.md @@ -0,0 +1,17 @@ +# Tile Matching Game + +A memory tile-matching game built with Tkinter. Click pairs of tiles to reveal hidden colors and match them all before the 60-second timer runs out, with a live score and attempts counter. + +## How to run + +```bash +python tile_matching.py +``` + +## Dependencies + +Standard library only (`tkinter`, `random`). + +## Pyodide-runnable + +No — it uses `tkinter`, which is a desktop GUI toolkit unavailable in the browser. diff --git a/projects/Timer/README.md b/projects/Timer/README.md new file mode 100644 index 000000000..73d29286f --- /dev/null +++ b/projects/Timer/README.md @@ -0,0 +1,17 @@ +# Timer + +A simple countdown timer. It asks for a duration in seconds and counts up on screen, one second at a time, until the time is up. + +## How to run + +```bash +python main.py +``` + +## Dependencies + +Standard library only (`time`). + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program; note that `time.sleep` blocks the browser tab while counting. diff --git a/projects/Tkinter/README.md b/projects/Tkinter/README.md new file mode 100644 index 000000000..8a6800f6c --- /dev/null +++ b/projects/Tkinter/README.md @@ -0,0 +1,18 @@ +# Tkinter Demos + +A collection of Tkinter GUI examples. `1.py` is a multi-frame student/college login and account-creation interface, and `2.py` is a college-system window with a button menu and a database entry popup. + +## How to run + +```bash +python 1.py +python 2.py +``` + +## Dependencies + +Standard library only (`tkinter`). + +## Pyodide-runnable + +No — these scripts use `tkinter`, a desktop GUI toolkit unavailable in the browser. diff --git a/projects/ToDoList/README.md b/projects/ToDoList/README.md index 4a6aa9388..24c6a0027 100644 --- a/projects/ToDoList/README.md +++ b/projects/ToDoList/README.md @@ -18,4 +18,11 @@ To run the program, you will need Python installed on your computer. Once you ha ```bash python to_do_list.py -python3 to_do_list.py \ No newline at end of file +python3 to_do_list.py +``` + +## Pyodide-runnable + +No — `to_do_list.py` is a Telegram bot built on the `telebot` library; it +needs network access and a bot token, so it cannot run in the browser +playground. Run it on a desktop Python environment. diff --git a/projects/Turtle Pattern/README.md b/projects/Turtle Pattern/README.md new file mode 100644 index 000000000..6417e55c9 --- /dev/null +++ b/projects/Turtle Pattern/README.md @@ -0,0 +1,17 @@ +# Turtle Pattern + +Draws a random walking pattern using Python's `turtle` graphics. A turtle moves in randomly chosen directions with random pen colors for 200 steps, producing a colorful abstract pattern. + +## How to run + +```bash +python randomPattern.py +``` + +## Dependencies + +Standard library only (`turtle`, `random`, `time`). + +## Pyodide-runnable + +No — it uses the `turtle` module, which requires a desktop graphics window. diff --git a/projects/Turtle_Graphics/README.md b/projects/Turtle_Graphics/README.md new file mode 100644 index 000000000..14e9a50c4 --- /dev/null +++ b/projects/Turtle_Graphics/README.md @@ -0,0 +1,17 @@ +# Turtle Graphics + +Draws layered fractal tree patterns using Python's `turtle` graphics. Recursive `draw` calls render branching trees in multiple colors and sizes across the screen. + +## How to run + +```bash +python Turtle_Graphics.py +``` + +## Dependencies + +Standard library only (`turtle`). + +## Pyodide-runnable + +No — it uses the `turtle` module, which requires a desktop graphics window. diff --git a/projects/Twitter-Bot/README.md b/projects/Twitter-Bot/README.md index 8e74d6f14..dd5c8d89c 100644 --- a/projects/Twitter-Bot/README.md +++ b/projects/Twitter-Bot/README.md @@ -20,3 +20,7 @@ This file includes the detailed and concise explanation of the project code as f ## Note: Remember that running a Twitter bot should comply with Twitter's terms of service and automation rules. Be cautious not to exceed rate limits or engage in spammy behavior. + +## Pyodide-runnable + +No — it is a Telegram bot that connects to the Telegram API over the network. diff --git a/projects/Type Racer Game/README.md b/projects/Type Racer Game/README.md new file mode 100644 index 000000000..9b926bdc3 --- /dev/null +++ b/projects/Type Racer Game/README.md @@ -0,0 +1,17 @@ +# Type Racer Game + +A typing-speed game built with Tkinter. A random sentence appears and you type it against a countdown timer, with live progress, color feedback for correct/incorrect text, and a words-per-minute score. + +## How to run + +```bash +python type_racer.py +``` + +## Dependencies + +Standard library only (`tkinter`, `random`, `time`). + +## Pyodide-runnable + +No — it uses `tkinter`, a desktop GUI toolkit unavailable in the browser. diff --git a/projects/Video Reversal/README.md b/projects/Video Reversal/README.md index 415c176a3..2006462bd 100644 --- a/projects/Video Reversal/README.md +++ b/projects/Video Reversal/README.md @@ -5,3 +5,7 @@ This python project reverses an input video python **Note** Store the input video in this folder and update its path in the code. + +## Pyodide-runnable + +No — it uses OpenCV (`cv2`) to read a video file and display frames in a desktop window. diff --git a/projects/Video-subtitle-generator/readme.md b/projects/Video-subtitle-generator/readme.md index 1e7bfc726..f210f0293 100644 --- a/projects/Video-subtitle-generator/readme.md +++ b/projects/Video-subtitle-generator/readme.md @@ -22,4 +22,7 @@ choco install ffmpeg # on Windows using Scoop (https://scoop.sh/) scoop install ffmpeg -``` \ No newline at end of file +``` +## Pyodide-runnable + +No — it uses OpenAI Whisper and FFmpeg to transcribe local audio/video files. diff --git a/projects/Voice-to-Text/README.md b/projects/Voice-to-Text/README.md new file mode 100644 index 000000000..0ee128d33 --- /dev/null +++ b/projects/Voice-to-Text/README.md @@ -0,0 +1,20 @@ +# Voice-to-Text + +Captures audio from the microphone and converts it to text using Google's speech recognition service. + +## How to run + +```bash +pip install SpeechRecognition googletrans pyaudio +python main.py +``` + +## Dependencies + +- SpeechRecognition +- googletrans +- pyaudio (microphone access) + +## Pyodide-runnable + +No — it requires microphone hardware access and network speech-recognition calls, neither available in Pyodide. diff --git a/projects/Watermarker/README.md b/projects/Watermarker/README.md index f9df4db53..4b82b42b5 100644 --- a/projects/Watermarker/README.md +++ b/projects/Watermarker/README.md @@ -98,3 +98,7 @@ pip install -r requirements.txt ### **`If you are asked to make changes on the same feature, repeat steps 8 to 13 to add more commits to your pull request.`** "https://github.com/highb33kay/Watermarker.git" + +## Pyodide-runnable + +No — it reads local image files with Pillow and uses `matplotlib`, walking the real filesystem. diff --git a/projects/Weather/README.md b/projects/Weather/README.md new file mode 100644 index 000000000..18e9b6727 --- /dev/null +++ b/projects/Weather/README.md @@ -0,0 +1,20 @@ +# Weather + +A console weather app. Enter a city name and it fetches the current weather description and temperature from the OpenWeatherMap API. + +## How to run + +```bash +pip install requests +python main.py +``` + +You need an OpenWeatherMap API key; set it in the `API_KEY` variable. + +## Dependencies + +- requests + +## Pyodide-runnable + +No — it uses `requests` to call a live web API, which is not available in Pyodide. diff --git a/projects/Web Scraping Jujustu Kaisen Manga/README.md b/projects/Web Scraping Jujustu Kaisen Manga/README.md index e0b7cc1f4..50a4d374d 100644 --- a/projects/Web Scraping Jujustu Kaisen Manga/README.md +++ b/projects/Web Scraping Jujustu Kaisen Manga/README.md @@ -51,4 +51,7 @@ python app.py
    -**Made with 💙 by [Vishvam](https://github.com/Vishvam10)** \ No newline at end of file +**Made with 💙 by [Vishvam](https://github.com/Vishvam10)** +## Pyodide-runnable + +No — it uses Selenium and `requests` to scrape a live website and download images. diff --git a/projects/WebButtonSimpelGUI/README.md b/projects/WebButtonSimpelGUI/README.md index 7d8ae2ba0..e4dfc2032 100644 --- a/projects/WebButtonSimpelGUI/README.md +++ b/projects/WebButtonSimpelGUI/README.md @@ -9,3 +9,7 @@ The project demonstrates how to create a basic GUI application using Tkinter and To run the project, ensure you have the necessary dependencies installed (Tkinter and Pillow) and execute the script. The main window with the buttons will appear, allowing you to interact with the application. Feel free to clone or download the repository and modify the code according to your requirements. + +## Pyodide-runnable + +No — it uses `tkinter` and `webbrowser`, a desktop GUI that opens external browser pages. diff --git a/projects/Website Blocker/readme.md b/projects/Website Blocker/readme.md index 0a5b84b2f..59c5bea31 100644 --- a/projects/Website Blocker/readme.md +++ b/projects/Website Blocker/readme.md @@ -21,4 +21,7 @@ We need to know the following python modules to build the python website blocker 1. **file handling:** file handling is used to do the modifications to the hosts file. 2. **time:** The time module is used to control the frequency of the modifications to the hosts file. -3. **datetime:** The datetime module is used to keep track of the free time and working time. \ No newline at end of file +3. **datetime:** The datetime module is used to keep track of the free time and working time. +## Pyodide-runnable + +No — it modifies the operating system `hosts` file, which is not accessible in the browser sandbox. diff --git a/projects/Weights_in_different_planets_GUI/README.md b/projects/Weights_in_different_planets_GUI/README.md index 930617005..4c17e9031 100644 --- a/projects/Weights_in_different_planets_GUI/README.md +++ b/projects/Weights_in_different_planets_GUI/README.md @@ -27,3 +27,7 @@ This application allows users to input their weight on Earth and then calculates - Python 3 and above - Tkinter (Python GUI library) + +## Pyodide-runnable + +No — it uses `tkinter`, a desktop GUI toolkit unavailable in the browser. diff --git a/projects/Windows Logo/README.md b/projects/Windows Logo/README.md new file mode 100644 index 000000000..341fc6014 --- /dev/null +++ b/projects/Windows Logo/README.md @@ -0,0 +1,17 @@ +# Windows Logo + +Draws the classic Windows logo using Python's `turtle` graphics on a black background. + +## How to run + +```bash +python windows_logo.py +``` + +## Dependencies + +Standard library only (`turtle`). + +## Pyodide-runnable + +No — it uses the `turtle` module, which requires a desktop graphics window. diff --git a/projects/Wine_quality_predictor/Readme.md b/projects/Wine_quality_predictor/Readme.md index 86600948e..5cad94868 100644 --- a/projects/Wine_quality_predictor/Readme.md +++ b/projects/Wine_quality_predictor/Readme.md @@ -18,4 +18,7 @@ During production of wine there may occur an reduction in quality due to some er ## Libraries Used: - Numpy - Pandas -- Scikit-learn - Support Machine Vector \ No newline at end of file +- Scikit-learn - Support Machine Vector +## Pyodide-runnable + +No — it reads a local CSV with `pandas` and trains a `scikit-learn` model; scikit-learn is not available in Pyodide and the script reads from the real filesystem. diff --git a/projects/Word_Predictor/README.md b/projects/Word_Predictor/README.md new file mode 100644 index 000000000..0b9b9adc4 --- /dev/null +++ b/projects/Word_Predictor/README.md @@ -0,0 +1,20 @@ +# Word Predictor + +A next-word prediction tool that learns from an exported WhatsApp chat. It reads a `Chats.txt` file, builds word-frequency and next-word tables, and predicts the most likely words to follow a given word. + +## How to run + +```bash +pip install pandas +python main.py +``` + +Place an exported WhatsApp chat as `Chats.txt` in the same folder. + +## Dependencies + +- pandas + +## Pyodide-runnable + +No — although `pandas` is available in Pyodide, the script reads a local `Chats.txt` file from the real filesystem, which is not present in the browser sandbox. diff --git a/projects/Worksheet_to_text/README.md b/projects/Worksheet_to_text/README.md new file mode 100644 index 000000000..435219896 --- /dev/null +++ b/projects/Worksheet_to_text/README.md @@ -0,0 +1,18 @@ +# Worksheet to Text + +Reads an Excel workbook and writes each column of data into its own plain-text file (`text-1.txt`, `text-2.txt`, ...). + +## How to run + +```bash +pip install openpyxl +python sheetToTextFile.py +``` + +## Dependencies + +- openpyxl + +## Pyodide-runnable + +No — it reads a local `worksheet.xlsx` file from the real filesystem, which is not available in the Pyodide browser sandbox. diff --git a/projects/World-Cup-Player-Comparison/README.md b/projects/World-Cup-Player-Comparison/README.md new file mode 100644 index 000000000..07e66e839 --- /dev/null +++ b/projects/World-Cup-Player-Comparison/README.md @@ -0,0 +1,22 @@ +# World Cup Player Comparison + +Scrapes 2022 World Cup team statistics from fbref.com, lets you pick two players from two countries, and saves a bar chart comparing their combined goals and assists. + +## How to run + +```bash +pip install pandas requests beautifulsoup4 numpy matplotlib +python main.py +``` + +## Dependencies + +- pandas +- requests +- beautifulsoup4 +- numpy +- matplotlib + +## Pyodide-runnable + +No — it uses `requests` to scrape a live website, which is not available in Pyodide. diff --git a/projects/YouTube Video Downloader/README.md b/projects/YouTube Video Downloader/README.md new file mode 100644 index 000000000..095553472 --- /dev/null +++ b/projects/YouTube Video Downloader/README.md @@ -0,0 +1,18 @@ +# YouTube Video Downloader + +A console tool that downloads a YouTube video using `yt-dlp`. It lists the available formats and lets you pick one or auto-selects the highest resolution. + +## How to run + +```bash +pip install yt-dlp +python you_tube_analyzer.py +``` + +## Dependencies + +- yt-dlp + +## Pyodide-runnable + +No — it uses `yt-dlp` to download from the internet, which is not available in Pyodide. diff --git a/projects/bittorrent-downloader/README.md b/projects/bittorrent-downloader/README.md new file mode 100644 index 000000000..e6beb2d7e --- /dev/null +++ b/projects/bittorrent-downloader/README.md @@ -0,0 +1,20 @@ +# BitTorrent Downloader + +Checks an email inbox for torrent links sent from a verified account, launches a torrent client to download them, and sends an SMS notification when each download finishes. + +## How to run + +``` +pip install -r requirements.txt +python download_torrent.py +``` + +## Dependencies + +- imapclient +- pyzmail +- twilio + +## Pyodide-runnable + +No - it connects to an IMAP email server, launches an external torrent client via subprocess, and sends SMS through the Twilio API. diff --git a/projects/caesar_cipher/README.md b/projects/caesar_cipher/README.md new file mode 100644 index 000000000..6eda4909a --- /dev/null +++ b/projects/caesar_cipher/README.md @@ -0,0 +1,17 @@ +# Caesar Cipher + +A console implementation of the classic Caesar cipher. It encodes or decodes a message by shifting each letter a chosen number of positions through the alphabet. + +## How to run + +``` +python caesar-cipher.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes - it is a pure-stdlib console program using only `input()`/`print()`. diff --git a/projects/car game/README.md b/projects/car game/README.md new file mode 100644 index 000000000..a47111af5 --- /dev/null +++ b/projects/car game/README.md @@ -0,0 +1,18 @@ +# Car Game + +A 2D car-dodging arcade game built with Pygame. Steer your car to avoid oncoming traffic, with sound effects, background music, and a high-score tracker. + +## How to run + +``` +pip install pygame +python Game.py +``` + +## Dependencies + +- pygame + +## Pyodide-runnable + +No - it is a Pygame application that opens a desktop window and plays audio files. diff --git a/projects/career-guide-bot/readme.md b/projects/career-guide-bot/readme.md index 6106ab25a..10f3eec08 100644 --- a/projects/career-guide-bot/readme.md +++ b/projects/career-guide-bot/readme.md @@ -38,3 +38,8 @@ The Career Guide is a Python-based interactive program that helps users explore ## DEMO image + + +## Pyodide-runnable + +No - the carreer_guide.py source file is empty, so there is nothing to run. diff --git a/projects/character-picture-grid/README.md b/projects/character-picture-grid/README.md new file mode 100644 index 000000000..b5f7f6f26 --- /dev/null +++ b/projects/character-picture-grid/README.md @@ -0,0 +1,17 @@ +# Character Picture Grid + +A small utility that rotates a 2D character grid 90 degrees and prints the resulting picture to the console. + +## How to run + +``` +python character-picture-grid.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes - it is a pure-stdlib console program that only prints to standard output. diff --git a/projects/chore-assignment-emailer/README.md b/projects/chore-assignment-emailer/README.md new file mode 100644 index 000000000..4e7784760 --- /dev/null +++ b/projects/chore-assignment-emailer/README.md @@ -0,0 +1,19 @@ +# Chore Assignment Emailer + +Randomly assigns a list of chores among a list of email addresses (distributing them round-robin) and emails each person their assigned chores via Gmail's SMTP server. + +## How to run + +``` +python chore-emailer.py +``` + +You will be prompted for your Gmail address and password so the script can log in and send the emails. + +## Dependencies + +Standard library only (`random`, `smtplib`). + +## Pyodide-runnable + +No — it uses `smtplib` to make an outbound SMTP connection to Gmail, which is not possible in the browser sandbox. diff --git a/projects/comma-code/README.md b/projects/comma-code/README.md new file mode 100644 index 000000000..86980dfe1 --- /dev/null +++ b/projects/comma-code/README.md @@ -0,0 +1,17 @@ +# Comma Code + +Joins a list of strings into a single human-readable string with commas and the word "and" before the last item (e.g. `apples, bananas, tofu, and cats`). + +## How to run + +``` +python comma-code.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — it is a pure-stdlib script that only manipulates strings and prints output. diff --git a/projects/computer-algebra/README.md b/projects/computer-algebra/README.md index 14c95d5cd..2a0998a82 100644 --- a/projects/computer-algebra/README.md +++ b/projects/computer-algebra/README.md @@ -58,3 +58,7 @@ System (CAS) that allows users to perform various mathematical computations. The ## License [![License](https://img.shields.io/static/v1?label=Licence&message=GPL-3-0&color=blue)](https://opensource.org/license/GPL-3-0) + +## Pyodide-runnable + +No — it uses the `dearpygui` GUI toolkit and makes HTTP requests to the Newton API. diff --git a/projects/currency converter/README.md b/projects/currency converter/README.md new file mode 100644 index 000000000..30140530c --- /dev/null +++ b/projects/currency converter/README.md @@ -0,0 +1,19 @@ +# Currency Converter + +A real-time currency converter with a Tkinter GUI. It fetches the latest exchange rates from an online API and lets the user convert an amount between any two currencies via dropdown menus. + +## How to run + +``` +pip install requests +python "currency converter.py" +``` + +## Dependencies + +- `requests` +- `tkinter` (standard library) + +## Pyodide-runnable + +No — it builds a `tkinter` GUI and fetches live exchange rates over the network, neither of which works in the Pyodide browser environment. diff --git a/projects/custom-invitations/README.md b/projects/custom-invitations/README.md new file mode 100644 index 000000000..c46630c88 --- /dev/null +++ b/projects/custom-invitations/README.md @@ -0,0 +1,18 @@ +# Custom Invitations + +Generates a Word document (`invitations.docx`) containing a personalized party invitation page for each name listed in `guests.txt`. + +## How to run + +``` +pip install python-docx +python customInvitations.py +``` + +## Dependencies + +- `python-docx` + +## Pyodide-runnable + +No — it depends on `python-docx` and writes a `.docx` file to the filesystem. diff --git a/projects/custom-seating-cards/README.md b/projects/custom-seating-cards/README.md new file mode 100644 index 000000000..7c4fe2476 --- /dev/null +++ b/projects/custom-seating-cards/README.md @@ -0,0 +1,18 @@ +# Custom Seating Cards + +Creates a decorative PNG seating/place card for each guest listed in `guests.txt`, drawing their name over a flower image with a custom font. + +## How to run + +``` +pip install Pillow +python custom_cards.py +``` + +## Dependencies + +- `Pillow` (`PIL`) + +## Pyodide-runnable + +No — it reads image and font assets from disk and writes generated card images to an output folder. diff --git a/projects/dictionary.com-scraper/README.md b/projects/dictionary.com-scraper/README.md new file mode 100644 index 000000000..ba1347b6a --- /dev/null +++ b/projects/dictionary.com-scraper/README.md @@ -0,0 +1,19 @@ +# Dictionary.com Word of the Day Scraper + +Scrapes the "Word of the Day" from dictionary.com, can fetch all its meanings, generate a text-to-speech pronunciation, and open the word's page in a browser. + +## How to run + +``` +pip install beautifulsoup4 gtts +python wordOfTheDay.py +``` + +## Dependencies + +- `beautifulsoup4` (`bs4`) +- `gtts` + +## Pyodide-runnable + +No — it fetches pages from dictionary.com over the network, uses the online `gtts` service, and launches an external browser/media player. diff --git a/projects/duplicate_search/README.md b/projects/duplicate_search/README.md new file mode 100644 index 000000000..f38f280b2 --- /dev/null +++ b/projects/duplicate_search/README.md @@ -0,0 +1,19 @@ +# Duplicate File Search + +Recursively searches a directory for duplicate files by comparing MD5 checksums, then interactively lets you delete unwanted copies. + +## How to run + +``` +python main.py [directory] +``` + +If no directory is given, it searches the current directory. + +## Dependencies + +Standard library only (`hashlib`, `sys`, `collections`, `pathlib`). + +## Pyodide-runnable + +No — it walks and deletes files on the real filesystem, which is not meaningful in the Pyodide browser sandbox. diff --git a/projects/excel-to-csv-converter/README.md b/projects/excel-to-csv-converter/README.md new file mode 100644 index 000000000..69be07705 --- /dev/null +++ b/projects/excel-to-csv-converter/README.md @@ -0,0 +1,19 @@ +# Excel to CSV Converter + +Converts every sheet of every `.xlsx` file in a folder into separate CSV files. + +## How to run + +``` +pip install openpyxl +python excelToCsv.py +``` + +## Dependencies + +- `openpyxl` +- `csv`, `os` (standard library) + +## Pyodide-runnable + +No — it scans the real filesystem for Excel files and writes CSV files to disk. diff --git a/projects/facebook_video_downloader/README.md b/projects/facebook_video_downloader/README.md index 26dff6434..479dbd47d 100644 --- a/projects/facebook_video_downloader/README.md +++ b/projects/facebook_video_downloader/README.md @@ -15,3 +15,7 @@ python facebook_video_downloader.py ``` The script will save the video in the downloads folder of the current directory. + +## Pyodide-runnable + +No — it makes network requests to Facebook and downloads video files to disk using `requests` and `wget`. diff --git a/projects/facerecoginition/README.md b/projects/facerecoginition/README.md new file mode 100644 index 000000000..712bc874a --- /dev/null +++ b/projects/facerecoginition/README.md @@ -0,0 +1,20 @@ +# Face Recognition + +A two-part OpenCV face recognition demo. `FaceCapture.py` captures face crops from your webcam and saves them; `FaceDetection.py` trains an LBPH recognizer on those crops and live-detects faces, displaying an "Unlocked"/"locked" overlay based on confidence. + +## How to run + +``` +pip install opencv-contrib-python numpy +python FaceCapture.py +python FaceDetection.py +``` + +## Dependencies + +- opencv-contrib-python +- numpy + +## Pyodide-runnable + +No — it relies on OpenCV webcam capture (`cv2.VideoCapture`) and GUI windows, which are not available in a browser. diff --git a/projects/fantasy-game-inventory/README.md b/projects/fantasy-game-inventory/README.md new file mode 100644 index 000000000..3b9d805fc --- /dev/null +++ b/projects/fantasy-game-inventory/README.md @@ -0,0 +1,17 @@ +# Fantasy Game Inventory + +A small demo that manages a fantasy game player's inventory. It displays items and their counts, and supports adding loot (a list of items) to an existing inventory dictionary. + +## How to run + +``` +python game-inventory.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable + +Yes — pure-Python logic with only `print()` output, no I/O or external packages. diff --git a/projects/fill-gaps/README.md b/projects/fill-gaps/README.md new file mode 100644 index 000000000..fbfe07ec6 --- /dev/null +++ b/projects/fill-gaps/README.md @@ -0,0 +1,17 @@ +# Fill Gaps + +A utility for renaming numbered files in a folder. `fillGaps` closes gaps in a numbered sequence (e.g. `spam001`, `spam003` becomes `spam001`, `spam002`), and `insertGaps` opens a gap at a chosen index. + +## How to run + +``` +python fill_gaps.py +``` + +## Dependencies + +Standard library only (`os`, `re`, `shutil`). + +## Pyodide-runnable + +No — it lists, renames, and moves files on the real filesystem, which is not available in a browser sandbox. diff --git a/projects/find-unneeded-files/README.md b/projects/find-unneeded-files/README.md new file mode 100644 index 000000000..ac20b9456 --- /dev/null +++ b/projects/find-unneeded-files/README.md @@ -0,0 +1,17 @@ +# Find Unneeded Files + +Walks a folder tree and reports files or subfolders whose size exceeds a given threshold, to help locate large items that could be cleaned up. + +## How to run + +``` +python find_unneeded.py +``` + +## Dependencies + +Standard library only (`os`). + +## Pyodide-runnable + +No — it walks the real filesystem and reads file sizes, which is not available in a browser sandbox. diff --git a/projects/game-snake_water_gun/README.md b/projects/game-snake_water_gun/README.md index fd6282615..c3c9c54cc 100644 --- a/projects/game-snake_water_gun/README.md +++ b/projects/game-snake_water_gun/README.md @@ -14,3 +14,7 @@ 1. Python Should be installed in your system 2. Just download the game.py file and open it (bydefault it should be open in terminal). * If you do not have or not want to install python in your system, you can download the game.exe file and directly run that executable file. + +## Pyodide-runnable + +Yes — `game.py` is a console game using only `input()`/`print()` and the standard-library `random` module. diff --git a/projects/goodreads-quotes-scraper/README.md b/projects/goodreads-quotes-scraper/README.md new file mode 100644 index 000000000..13567e2be --- /dev/null +++ b/projects/goodreads-quotes-scraper/README.md @@ -0,0 +1,19 @@ +# Goodreads Quotes Scraper + +Scrapes quotes from Goodreads — top popular, recently added, by tag, or by page number — and saves the results to `temp.json`. + +## How to run + +``` +pip install bs4 selenium +python goodreadsScrape.py +``` + +## Dependencies + +- bs4 +- selenium + +## Pyodide-runnable + +No — it fetches live web pages over the network (and imports Selenium), which is not available in a browser sandbox. diff --git a/projects/healthmanagementsystem/README.md b/projects/healthmanagementsystem/README.md new file mode 100644 index 000000000..8d724c0c1 --- /dev/null +++ b/projects/healthmanagementsystem/README.md @@ -0,0 +1,17 @@ +# Health Management System + +A console-based health log. It lets you log exercise or food entries (timestamped) for one of three users into text files, and retrieve those logs later. + +## How to run + +``` +python health.py +``` + +## Dependencies + +Standard library only (`datetime`). + +## Pyodide-runnable + +No — it reads and appends to real files on disk, which is not available in a browser sandbox. diff --git a/projects/hill_cipher/Readme.md b/projects/hill_cipher/Readme.md index 440487449..fe6afac23 100644 --- a/projects/hill_cipher/Readme.md +++ b/projects/hill_cipher/Readme.md @@ -13,3 +13,7 @@

    Aid for Exceptions/Errors

    Try: pip intall mysql or/and pip install mysql-connector-python

    + +## Pyodide-runnable + +No — it connects to a local MySQL database via `mysql.connector`, which is not available in a browser sandbox. diff --git a/projects/image-site-downloader/README.md b/projects/image-site-downloader/README.md new file mode 100644 index 000000000..45510be15 --- /dev/null +++ b/projects/image-site-downloader/README.md @@ -0,0 +1,19 @@ +# Image Site Downloader + +Searches Imgur for a query term, scrapes the resulting image thumbnails, and downloads up to a chosen number of them into a local `results/` folder. + +## How to run + +``` +pip install requests bs4 +python imgur-downloader.py +``` + +## Dependencies + +- requests +- bs4 + +## Pyodide-runnable + +No — it makes live HTTP requests and writes downloaded files to disk, neither of which is available in a browser sandbox. diff --git a/projects/indeed-scraper/README.md b/projects/indeed-scraper/README.md new file mode 100644 index 000000000..3c921fb5a --- /dev/null +++ b/projects/indeed-scraper/README.md @@ -0,0 +1,17 @@ +# Indeed Job Scraper + +Scrapes job listings from Indeed for several cities and job titles, then saves the results to a CSV file. + +## How to run + +``` +pip install -r requirements.txt +python indeed-scraper.py +``` + +## Dependencies + +requests, beautifulsoup4, lxml, pandas. + +## Pyodide-runnable +No — it makes live HTTP requests to Indeed to scrape web pages, which Pyodide cannot do. diff --git a/projects/instant-messenger-bot/README.md b/projects/instant-messenger-bot/README.md new file mode 100644 index 000000000..2dd29d03a --- /dev/null +++ b/projects/instant-messenger-bot/README.md @@ -0,0 +1,17 @@ +# Instant Messenger Bot + +Automates sending messages to active Slack contacts by simulating keyboard and mouse input. + +## How to run + +``` +pip install -r requirements.txt +python slack_messenger.py +``` + +## Dependencies + +pyautogui. + +## Pyodide-runnable +No — it uses pyautogui to control the desktop keyboard, mouse, and screen. diff --git a/projects/inventory management/README.md b/projects/inventory management/README.md new file mode 100644 index 000000000..7b71dd64b --- /dev/null +++ b/projects/inventory management/README.md @@ -0,0 +1,19 @@ +# Inventory Management + +A simple supermarket inventory system with a Tkinter login window and add/display/search/update/delete operations backed by a MySQL database. + +## How to run + +``` +pip install -r requirements.txt +python main.py +``` + +Requires a local MySQL server with a `records` database and an `inventory` table. + +## Dependencies + +tkinter (standard library), mysql-connector-python. + +## Pyodide-runnable +No — it uses a Tkinter GUI and connects to a MySQL database. diff --git a/projects/link-verification/README.md b/projects/link-verification/README.md new file mode 100644 index 000000000..bf3ff18f0 --- /dev/null +++ b/projects/link-verification/README.md @@ -0,0 +1,17 @@ +# Link Verification + +Fetches a web page, extracts all of its links, and reports which ones are good or broken. + +## How to run + +``` +pip install -r requirements.txt +python verify_links.py +``` + +## Dependencies + +requests, beautifulsoup4. + +## Pyodide-runnable +No — it makes live HTTP requests to verify links across the internet. diff --git a/projects/linkedin-scrape/README.md b/projects/linkedin-scrape/README.md new file mode 100644 index 000000000..21633d0c2 --- /dev/null +++ b/projects/linkedin-scrape/README.md @@ -0,0 +1,19 @@ +# LinkedIn Profile Scraper + +Logs into LinkedIn with Selenium (Firefox) and scrapes contact information (email, phone, website) from a connection's profile. + +## How to run + +``` +pip install -r requirements.txt +python linkedin_profile.py +``` + +Requires Firefox and the bundled geckodriver. + +## Dependencies + +selenium, beautifulsoup4, requests, lxml. + +## Pyodide-runnable +No — it drives a real browser with Selenium to scrape a live website. diff --git a/projects/looking-busy/README.md b/projects/looking-busy/README.md new file mode 100644 index 000000000..a586e9612 --- /dev/null +++ b/projects/looking-busy/README.md @@ -0,0 +1,17 @@ +# Looking Busy + +Moves the mouse slightly every 10 seconds to keep your computer and apps from going idle. + +## How to run + +``` +pip install -r requirements.txt +python look_busy.py +``` + +## Dependencies + +pyautogui. + +## Pyodide-runnable +No — it uses pyautogui to control the desktop mouse. diff --git a/projects/love-calculator/README.md b/projects/love-calculator/README.md new file mode 100644 index 000000000..7cf9a7d30 --- /dev/null +++ b/projects/love-calculator/README.md @@ -0,0 +1,16 @@ +# Love Calculator + +A console program that takes two names and computes a playful "love score" percentage based on shared vowels, consonants, first letters, and name length, then describes the relationship. + +## How to run + +``` +python "Love Calculator.py" +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable +Yes — it is a pure-stdlib console program using only input(), print(), random, and time. diff --git a/projects/maths/README.md b/projects/maths/README.md new file mode 100644 index 000000000..ee3c070ef --- /dev/null +++ b/projects/maths/README.md @@ -0,0 +1,16 @@ +# 3D Shape Volume Calculator + +A console program that calculates the volume of various 3D shapes (cube, pyramid, cylinder, sphere, cone, ellipsoid, torus, hexagonal prism) from user-supplied dimensions. + +## How to run + +``` +python 3dShapeVolume.py +``` + +## Dependencies + +Standard library only. + +## Pyodide-runnable +Yes — it is a pure-stdlib console program using only input(), print(), and cmath. diff --git a/projects/minesweeper/README.md b/projects/minesweeper/README.md new file mode 100644 index 000000000..750558776 --- /dev/null +++ b/projects/minesweeper/README.md @@ -0,0 +1,17 @@ +# Minesweeper + +A console version of the classic Minesweeper game. It builds a board, randomly plants bombs, and asks the player where to dig (entered as `row,col`). Digging an empty cell recursively reveals neighbouring cells; hitting a bomb ends the game. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only (`random`, `re`). + +## Pyodide-runnable + +Yes — it is a pure-stdlib console program that only uses `input()` and `print()`. diff --git a/projects/movie-rater/README.md b/projects/movie-rater/README.md index ff47f5678..629ab5a23 100644 --- a/projects/movie-rater/README.md +++ b/projects/movie-rater/README.md @@ -35,4 +35,7 @@ Before you begin, ensure you have met the following requirements: ``` uvicorn main:app --reload - ``` \ No newline at end of file + ``` +## Pyodide-runnable + +No — it is a FastAPI web server that makes network requests to The Movie Database API. diff --git a/projects/multiplayer_socket/README.md b/projects/multiplayer_socket/README.md new file mode 100644 index 000000000..a774d12be --- /dev/null +++ b/projects/multiplayer_socket/README.md @@ -0,0 +1,20 @@ +# Multiplayer Socket Games + +A two-player networked game system using TCP sockets. `server.py` hosts the games and `client.py` connects to it. Players can choose between a multiplayer Hangman game and a Rock-Paper-Scissors game. + +## How to run + +Start the server first, then run a client on each player's machine: + +``` +python server.py +python client.py +``` + +## Dependencies + +Standard library only (`socket`, `_thread`). + +## Pyodide-runnable + +No — it uses raw TCP sockets and threading to communicate between machines, which is not possible in a browser sandbox. diff --git a/projects/pdf-paranoia/README.md b/projects/pdf-paranoia/README.md new file mode 100644 index 000000000..76a917735 --- /dev/null +++ b/projects/pdf-paranoia/README.md @@ -0,0 +1,18 @@ +# pdf-paranoia + +Bulk-encrypts and decrypts PDF files. It walks a directory tree, encrypts every unencrypted PDF with a user-supplied password (writing the result into an `untitled folder`), and can decrypt the `_encrypted` PDFs back again. + +## How to run + +``` +pip install PyPDF2 +python pdfParanoia.py +``` + +## Dependencies + +- PyPDF2 + +## Pyodide-runnable + +No — it walks the real filesystem with `os.walk` and reads/writes PDF files on disk, which is unavailable in the browser. diff --git a/projects/pdf_to_text/README.md b/projects/pdf_to_text/README.md index 297806052..8f377fdf5 100644 --- a/projects/pdf_to_text/README.md +++ b/projects/pdf_to_text/README.md @@ -34,4 +34,7 @@ This Python script is designed to convert a PDF file into a plain text (.txt) fi - [pi1814](https://github.com/pi1814) -This script simplifies the process of converting PDF files to text. If you encounter any issues or have suggestions for improvements, please don't hesitate to reach out to the author on their GitHub page. Feel free to customize and use this script for your PDF to text conversion needs. \ No newline at end of file +This script simplifies the process of converting PDF files to text. If you encounter any issues or have suggestions for improvements, please don't hesitate to reach out to the author on their GitHub page. Feel free to customize and use this script for your PDF to text conversion needs. +## Pyodide-runnable + +No — it is a PyQt5 desktop GUI application and reads PDF files from the local filesystem. diff --git a/projects/ping_pong/README.md b/projects/ping_pong/README.md new file mode 100644 index 000000000..6f0492004 --- /dev/null +++ b/projects/ping_pong/README.md @@ -0,0 +1,17 @@ +# Ping Pong + +A two-player Pong game built with Python's `turtle` graphics. Player A uses `W`/`S` and Player B uses the arrow keys to move their paddles; the ball bounces around the screen and the score updates on goals. + +## How to run + +``` +python ping_pong.py +``` + +## Dependencies + +Standard library only (`turtle`). + +## Pyodide-runnable + +No — it uses the `turtle` graphics module and keyboard events, which require a desktop windowing environment. diff --git a/projects/prettified-stopwatch/README.md b/projects/prettified-stopwatch/README.md new file mode 100644 index 000000000..3cd9609d3 --- /dev/null +++ b/projects/prettified-stopwatch/README.md @@ -0,0 +1,19 @@ +# Prettified Stopwatch + +A console stopwatch that records lap times. Press ENTER to start, ENTER again to mark each lap, and Ctrl-C to stop. Results are copied to the system clipboard. + +## How to run + +```sh +pip install pyperclip +python stopwatch.py +``` + +## Dependencies + +- pyperclip +- time (standard library) + +## Pyodide-runnable + +No - it relies on `pyperclip` for system clipboard access, which is not available in the Pyodide sandbox. diff --git a/projects/proxy-scrapper/README.md b/projects/proxy-scrapper/README.md index 4017a003c..8576c361c 100644 --- a/projects/proxy-scrapper/README.md +++ b/projects/proxy-scrapper/README.md @@ -8,4 +8,13 @@ pip3 install beautifulsoup4 Run script ```sh python3 proxy_scrapper.py -``` \ No newline at end of file +``` + +## Dependencies + +- requests +- beautifulsoup4 + +## Pyodide-runnable + +No - it uses `requests` to fetch a live website over the network, which is blocked in the browser sandbox. \ No newline at end of file diff --git a/projects/qr-code-generator-with-window-and-simple-ui/README.md b/projects/qr-code-generator-with-window-and-simple-ui/README.md new file mode 100644 index 000000000..db368a9fe --- /dev/null +++ b/projects/qr-code-generator-with-window-and-simple-ui/README.md @@ -0,0 +1,20 @@ +# QR Code Generator with Window and Simple UI + +A Tkinter desktop application that takes a text/URL, save location, file name, and size, then generates a QR code image and saves it to the chosen directory. + +## How to run + +```sh +pip install qrcode pillow +python main.py +``` + +## Dependencies + +- qrcode +- pillow +- tkinter (standard library) + +## Pyodide-runnable + +No - it builds a Tkinter GUI and writes image files to real disk paths, neither of which works in the browser sandbox. diff --git a/projects/reciept generator/README.md b/projects/reciept generator/README.md index e617d063f..80fe1f6ce 100644 --- a/projects/reciept generator/README.md +++ b/projects/reciept generator/README.md @@ -39,3 +39,11 @@ With more contribution, this can be a very memorable open source project. ### Contributor This project is contributed by [@Subha-5](https://github.com/Subha-5) + +## Dependencies + +- fpdf + +## Pyodide-runnable + +No - it depends on the `fpdf` package (not bundled with Pyodide) to render a PDF receipt. diff --git a/projects/reddit-scraper/README.md b/projects/reddit-scraper/README.md new file mode 100644 index 000000000..5358a7f49 --- /dev/null +++ b/projects/reddit-scraper/README.md @@ -0,0 +1,19 @@ +# Reddit Scraper + +Fetches the top posts from the r/python subreddit via Reddit's JSON API and stores them in a local SQLite database (`reddit_news.db`). + +## How to run + +```sh +pip install requests +python -c "import grabnews; grabnews.reddit_get()" +``` + +## Dependencies + +- requests +- sqlite3 (standard library) + +## Pyodide-runnable + +No - it uses `requests` to fetch live data from Reddit over the network, which is blocked in the browser sandbox. diff --git a/projects/regex-search/README.md b/projects/regex-search/README.md new file mode 100644 index 000000000..a392bfb9b --- /dev/null +++ b/projects/regex-search/README.md @@ -0,0 +1,17 @@ +# Regex Search + +A console tool that prompts for a regular expression and prints every line in the `.txt` files of a folder that matches it. + +## How to run + +```sh +python regex-search.py +``` + +## Dependencies + +Standard library only (uses `os` and `re`). + +## Pyodide-runnable + +No - it walks the real filesystem with `os.listdir` to find and open `.txt` files on disk, which is not available in the browser sandbox. diff --git a/projects/regex-strip/README.md b/projects/regex-strip/README.md new file mode 100644 index 000000000..0855bbb49 --- /dev/null +++ b/projects/regex-strip/README.md @@ -0,0 +1,17 @@ +# Regex Strip + +A small demonstration that reimplements Python's `str.strip()` method using regular expressions to remove leading and trailing whitespace. + +## How to run + +```sh +python regex-strip.py +``` + +## Dependencies + +Standard library only (uses `re`). + +## Pyodide-runnable + +Yes - it is pure-stdlib code using only the `re` module and `print`. diff --git a/projects/reuters-scraper/README.md b/projects/reuters-scraper/README.md new file mode 100644 index 000000000..8da0b695c --- /dev/null +++ b/projects/reuters-scraper/README.md @@ -0,0 +1,20 @@ +# Reuters Scraper + +Scrapes technology news headlines, summaries, times, and links from the Reuters website and writes them to `reuters_scrape.csv`. + +## How to run + +```sh +pip install beautifulsoup4 lxml +python scrape.py +``` + +## Dependencies + +- beautifulsoup4 +- lxml +- urllib, csv (standard library) + +## Pyodide-runnable + +No - it uses `urllib` to fetch a live website over the network, which is blocked in the browser sandbox. diff --git a/projects/scheduledShutdown/README.md b/projects/scheduledShutdown/README.md new file mode 100644 index 000000000..faa81e9d1 --- /dev/null +++ b/projects/scheduledShutdown/README.md @@ -0,0 +1,17 @@ +# Scheduled Shutdown + +A console utility that asks for a number of minutes and then shuts down the computer after that delay using a system shutdown command. + +## How to run + +```sh +python shutdown.py +``` + +## Dependencies + +Standard library only (uses `os` and `time`). + +## Pyodide-runnable + +No - it calls `os.system("shutdown ...")` to power off the host machine, which is not available in the browser sandbox. diff --git a/projects/scrap-ycombinator/README.md b/projects/scrap-ycombinator/README.md new file mode 100644 index 000000000..2d6e1629f --- /dev/null +++ b/projects/scrap-ycombinator/README.md @@ -0,0 +1,21 @@ +# Scrape Y Combinator + +Scrapes article titles and links from the Hacker News (Y Combinator) front page and writes them to `ycombinatornews.csv`. + +## How to run + +```sh +pip install requests beautifulsoup4 lxml +python main.py +``` + +## Dependencies + +- requests +- beautifulsoup4 +- lxml +- csv (standard library) + +## Pyodide-runnable + +No - it uses `requests` to fetch a live website over the network, which is blocked in the browser sandbox. diff --git a/projects/servo motor classifier/Readme.md b/projects/servo motor classifier/Readme.md index eda612c91..bcac8a031 100644 --- a/projects/servo motor classifier/Readme.md +++ b/projects/servo motor classifier/Readme.md @@ -10,4 +10,19 @@ Servos are basic components of many machines. There are different classes of ser ## Libraries Used: - Numpy - Pandas -- Scikit-learn \ No newline at end of file +- Scikit-learn + +## How to run + +``` +pip install pandas numpy scikit-learn matplotlib +python main.py +``` + +## Dependencies + +pandas, numpy, scikit-learn, matplotlib + +## Pyodide-runnable + +No — it downloads a CSV over the network with `pd.read_csv()` and renders a Matplotlib window, neither of which works in Pyodide. diff --git a/projects/snake_water_gun_game/README.md b/projects/snake_water_gun_game/README.md new file mode 100644 index 000000000..a53c86e39 --- /dev/null +++ b/projects/snake_water_gun_game/README.md @@ -0,0 +1,18 @@ +# Snake Water Gun Game + +A console version of the snake-water-gun game (a rock-paper-scissors variant). The snake drinks the water, the gun shoots the snake, and the gun has no effect on water. You play 10 rounds against the computer with colored terminal output. + +## How to run + +``` +pip install rich +python Snake_Water_Gun_Game.py +``` + +## Dependencies + +rich + +## Pyodide-runnable + +No — it depends on the `rich` package, which is not bundled with Pyodide. diff --git a/projects/space_battle/README.md b/projects/space_battle/README.md new file mode 100644 index 000000000..3eb52116f --- /dev/null +++ b/projects/space_battle/README.md @@ -0,0 +1,18 @@ +# Space Battle + +A Space Invaders style game built with Pygame. Move a UFO around the screen, shoot down descending enemies, and play with sound effects and background music. + +## How to run + +``` +pip install pygame +python main.py +``` + +## Dependencies + +pygame + +## Pyodide-runnable + +No — it uses Pygame for windowed graphics, audio playback, and real-time keyboard input. diff --git a/projects/sponge-bob/README.md b/projects/sponge-bob/README.md new file mode 100644 index 000000000..28e2734c1 --- /dev/null +++ b/projects/sponge-bob/README.md @@ -0,0 +1,17 @@ +# SpongeBob Turtle Drawing + +A turtle-graphics script that draws a SpongeBob SquarePants character (body, pants, shirt, tie, eyes, and smile) on screen. + +## How to run + +``` +python main.py +``` + +## Dependencies + +Standard library only (uses the `turtle` module). + +## Pyodide-runnable + +No — it uses the `turtle` module, which needs a Tkinter-backed desktop window. diff --git a/projects/student-management-system/README.md b/projects/student-management-system/README.md index df1d8508c..861cee9f1 100644 --- a/projects/student-management-system/README.md +++ b/projects/student-management-system/README.md @@ -60,3 +60,7 @@ To deploy this project just run .py files. - [@siddharth9300](https://github.com/siddharth9300) + +## Pyodide-runnable + +No — both scripts connect to a MySQL database via `mysql.connector`, which requires a live database server unavailable in Pyodide. diff --git a/projects/takeImage/README.md b/projects/takeImage/README.md index 15035303a..9c9e93c27 100644 --- a/projects/takeImage/README.md +++ b/projects/takeImage/README.md @@ -6,3 +6,7 @@ The default path would be your current directory. You can also give name to your image using following command: > python3 take_pictures_from_webcam.py --name ImageName + +## Pyodide-runnable + +No — it captures frames from a webcam with OpenCV (`cv2.VideoCapture`) and shows GUI windows, neither of which works in the browser sandbox. diff --git a/projects/text-translate/README.md b/projects/text-translate/README.md index 6c48c80b1..8cde428c8 100644 --- a/projects/text-translate/README.md +++ b/projects/text-translate/README.md @@ -3,4 +3,7 @@ ```pip install pygame``` ```pip install gTTS``` ```pip install tkinter``` -```pip install googletrans==4.0.0-rc1``` \ No newline at end of file +```pip install googletrans==4.0.0-rc1``` +## Pyodide-runnable + +No — `main.py` uses the `translate` package which makes network requests, and `translate _GUIaudio.py` additionally uses `tkinter`, `gtts`, and `pygame`. diff --git a/projects/todo.md b/projects/todo.md new file mode 100644 index 000000000..45343175b --- /dev/null +++ b/projects/todo.md @@ -0,0 +1,293 @@ +# Projects TODO + +Working checklist for processing every project folder, one at a time. + +## Task per project + +For each folder below: + +1. **Read the project** — understand what it does and what it depends on. +2. **Pyodide assessment** — decide if it can run in the browser playground + (Pyodide = CPython in WebAssembly: pure-stdlib console code works; no GUI, + no sockets, no threads-heavy or hardware/file-system-dependent code). + - If it **can run as-is or with small modifications** → make those edits so it + runs under Pyodide (keep behavior the same; prefer stdlib `input()`/`print()`). + - If it **cannot** (pygame, tkinter, OpenCV, network, OS automation, etc.) → + leave the code unchanged. +3. **Add `README.md`** to the folder — what the project does, how to run it, + dependencies, and whether it's Pyodide-runnable. +4. Tick the box here and note the outcome (`pyodide` / `modified` / `desktop-only`). + +> Process in order. Do not modify unrelated projects in the same change. + +## Checklist + +- [x] Adjactive_compartive_superlative — desktop-only (nltk corpus download) +- [x] Advisor — desktop-only (tkinter + network) +- [x] AES256 — modified (removed os.system clear; pyodide via pycryptodome) +- [x] Alarm Clock — desktop-only (tkinter + pygame + threads) +- [x] Amazon Product Availbility Checker — desktop-only (network scraping) +- [x] AnalogClock — desktop-only (tkinter canvas) +- [x] API Based Weather Report — desktop-only (OpenWeather HTTP API) +- [x] Audio Converter — desktop-only (pydub + disk files) +- [x] AudioAPI — desktop-only (Flask + Whisper) +- [x] Audiobook — desktop-only (pyttsx3 + PDFs) +- [x] AudioRecorder — desktop-only (microphone) +- [x] AutoGui — desktop-only (pyautogui) +- [x] AWS_s3_upload — desktop-only (boto3/AWS) +- [x] Battleship — modified (removed os.system clears; pyodide) +- [x] BFS visualizer — desktop-only (curses UI) +- [x] Bigram_Autocomplete — pyodide +- [x] Bitcoin Mining — pyodide +- [x] bittorrent-downloader — desktop-only (IMAP/subprocess/Twilio) +- [x] BlackJack — pyodide +- [x] Blender_tools — desktop-only (Blender bpy) +- [x] Blind Auction — pyodide +- [x] Blind_Auction — modified (removed unused distutils; pyodide) +- [x] BMI WebApp — desktop-only (PyWebIO) +- [x] BMI_calculator — desktop-only (tabulate + bmi.csv) +- [x] Book Data Extractor — desktop-only (scraping + xlsx) +- [x] Browser — desktop-only (PyQt5 WebEngine) +- [x] Budget-manager — desktop-only (tkinter + SQLite) +- [x] caesar_cipher — pyodide +- [x] Calculate Age — pyodide +- [x] Calculator — modified (removed os.system clears; pyodide) +- [x] Calculator-GUI-With-Python — desktop-only (Kivy) +- [x] Calendar — pyodide +- [x] Captcha_Genrator — desktop-only (tkinter) +- [x] car game — desktop-only (pygame) +- [x] Card Game — pyodide +- [x] career-guide-bot — desktop-only (source file empty) +- [x] character-picture-grid — pyodide +- [x] Chat Application — desktop-only (TCP sockets) +- [x] Chat-GPT-Discord-Bot — desktop-only (Discord + OpenAI) +- [x] Chess — desktop-only (pygame) +- [x] chore-assignment-emailer — desktop-only (smtplib) +- [x] Code_Reviewer — desktop-only (disk files + pycodestyle) +- [x] Coin Flip — modified (removed os.system clear; pyodide) +- [x] Collatz_Conjecture — pyodide +- [x] Comics_Scraper — desktop-only (xkcd scrape + files) +- [x] comma-code — pyodide +- [x] computer-algebra — desktop-only (dearpygui + API) +- [x] Connect Four — desktop-only (pygame) +- [x] Conway’s-Game-Of-Life — desktop-only (matplotlib animation) +- [x] Countdown — pyodide +- [x] CRUD-with-postgresql — desktop-only (PostgreSQL) +- [x] currency converter — desktop-only (tkinter + API) +- [x] CustomEncryptionDecryption — desktop-only (PyJWT + env/files) +- [x] custom-invitations — desktop-only (python-docx) +- [x] custom-seating-cards — desktop-only (Pillow assets) +- [x] Data Entry Automation — desktop-only (selenium) +- [x] Data_Abstractor — desktop-only (Flask server) +- [x] Desktop Weather Notifier — desktop-only (API + OS notifications) +- [x] Desktop-Notification — desktop-only (win10toast) +- [x] Diabetes Monitoring Dashboard — desktop-only (Streamlit + ML) +- [x] Dice Simulator — pyodide +- [x] Dice-Roll-Simulator — pyodide +- [x] dictionary.com-scraper — desktop-only (scrape + gtts) +- [x] Discord Bot — desktop-only (Discord gateway) +- [x] DnD Dice — pyodide +- [x] Drowsiness Detector — desktop-only (dlib/OpenCV/webcam) +- [x] duplicate_search — desktop-only (filesystem) +- [x] Encryptor and Decryptor — desktop-only (tkinter) +- [x] English Thesaurus — desktop-only (relative-path data.json) +- [x] excel-to-csv-converter — desktop-only (disk scan + openpyxl) +- [x] Expense-Tracker — desktop-only (tkinter + reportlab) +- [x] Eye Blink Detection — desktop-only (dlib/OpenCV/webcam) +- [x] facebook_video_downloader — desktop-only (network + downloads) +- [x] facerecoginition — desktop-only (OpenCV webcam) +- [x] fantasy-game-inventory — pyodide +- [x] Fidget Spinner Game — desktop-only (turtle) +- [x] File_Organizer — desktop-only (filesystem) +- [x] fill-gaps — desktop-only (filesystem) +- [x] Find_imbd_rating — desktop-only (IMDb scraping) +- [x] find-unneeded-files — desktop-only (filesystem walk) +- [x] Flappybird_game — desktop-only (pygame) +- [x] Full_Calendar — desktop-only (tkinter) +- [x] Game of Cricket — pyodide +- [x] game-snake_water_gun — pyodide +- [x] goodreads-quotes-scraper — desktop-only (Selenium scraping) +- [x] GPA-Calculator — pyodide +- [x] Gpt-And-Langchain — desktop-only (Telegram bot + APIs) +- [x] Gradient-Image — desktop-only (OpenCV GUI) +- [x] Guess Number — pyodide (v2 is tkinter) +- [x] Guess The Word — pyodide +- [x] GUI based image display and transfer in python — desktop-only (tkinter) +- [x] HandCricket — pyodide +- [x] HandTrack — desktop-only (webcam + MediaPipe) +- [x] Hangman — pyodide +- [x] healthmanagementsystem — desktop-only (disk files) +- [x] Higher-Lower — pyodide +- [x] hill_cipher — desktop-only (MySQL) +- [x] Historical Data Breaches — desktop-only (HIBP API) +- [x] Image compressor — desktop-only (Pillow + filedialog) +- [x] Image Manipulation — desktop-only (Pillow + disk) +- [x] Image Sketcher — desktop-only (OpenCV webcam) +- [x] Image to PDF Converter — desktop-only (disk I/O) +- [x] Image to text generation project — desktop-only (PyTorch notebook) +- [x] Image Toonification — desktop-only (OpenCV + disk) +- [x] ImagegenChatbot — desktop-only (OpenAI API) +- [x] image-site-downloader — desktop-only (live HTTP) +- [x] Image-to-art — desktop-only (Pillow + image files) +- [x] Image-Upscale — desktop-only (PyTorch + image files) +- [x] indeed-scraper — desktop-only (HTTP scraping) +- [x] Instagram Post Creation — desktop-only (Pillow + image I/O) +- [x] instant-messenger-bot — desktop-only (pyautogui automation) +- [x] Internet Speed Tester — desktop-only (network measurement) +- [x] Internet-speed-test — desktop-only (network measurement) +- [x] inventory management — desktop-only (tkinter + MySQL) +- [x] Inverse Matrix Calculator — pyodide +- [x] IP Blacklist Checker — desktop-only (live API) +- [x] IPv4_Calculator-main — pyodide +- [x] JARVIS.PY — desktop-only (mic/TTS/browser) +- [x] Jokenpo — pyodide +- [x] Jokey — desktop-only (JokeAPI HTTP) +- [x] KbdXylo — desktop-only (pynput + audio) +- [x] Language_learning_assistant — desktop-only (mic) +- [x] Language-Translate — desktop-only (HTTP) +- [x] linkedin-scrape — desktop-only (Selenium) +- [x] link-verification — desktop-only (HTTP) +- [x] Live AQI — desktop-only (tkinter + API) +- [x] Loan Calculator — pyodide +- [x] Location Search App (GUI) — desktop-only (tkinter) +- [x] looking-busy — desktop-only (pyautogui) +- [x] love-calculator — pyodide +- [x] Lyrics-Extractor — desktop-only (Search API) +- [x] Madlibs Generator — desktop-only (tkinter) +- [x] Make-API — desktop-only (Flask server) +- [x] Market Financial Sentiment Predictor — desktop-only (sklearn + CSV) +- [x] Mastermind — pyodide +- [x] maths — pyodide +- [x] Medium Article Reader — desktop-only (HTTP/OpenAI + TTS) +- [x] Merge PDFs — desktop-only (filesystem + pikepdf) +- [x] Message-Spam — desktop-only (pyautogui + WhatsApp Web) +- [x] Minecraft-in-Python-main — desktop-only (ursina 3D engine) +- [x] Mineral Processing Technology-Image Analytics — desktop-only (OpenCV + disk) +- [x] minesweeper — pyodide +- [x] ML-Notebooks_Beginners — desktop-only (Jupyter notebook) +- [x] Mobile Document Scanner — desktop-only (OpenCV GUI) +- [x] Mongo CRUD — desktop-only (MongoDB) +- [x] MorseCode Translator — pyodide +- [x] Morse-Code-Translator — pyodide +- [x] MotivateBot — desktop-only (OpenAI API) +- [x] Movie recommendation — desktop-only (scikit-learn) +- [x] MovieApi_and_ML — desktop-only (Django + sklearn) +- [x] movie-rater — desktop-only (FastAPI + TMDB) +- [x] MQTT Client — desktop-only (MQTT broker network) +- [x] multiplayer_socket — desktop-only (TCP sockets) +- [x] Music Player — desktop-only (tkinter + pygame) +- [x] NASA-APOD — desktop-only (network + download) +- [x] Neurons — modified (removed os.system clear; pyodide) +- [x] Number Guessing App — pyodide +- [x] Online Trivia — desktop-only (opentdb API) +- [x] OpenCV_color_detect_in_live_feed — desktop-only (webcam) +- [x] Organize_Directory — desktop-only (filesystem) +- [x] Othello — desktop-only (pygame) +- [x] Otp_Generator — pyodide +- [x] OTP-Verfication-System — desktop-only (Gmail SMTP) +- [x] Password Projects — mixed (Generator/Hashing/Meter/strong-detector pyodide; Breach/Manager/WiFi desktop-only) +- [x] PDF_Reader — desktop-only (OpenAI + LangChain) +- [x] pdf_to_text — desktop-only (PyQt5) +- [x] pdf-paranoia — desktop-only (filesystem PDFs) +- [x] Personal-Finance-tracker — pyodide +- [x] Pig_latin — pyodide +- [x] ping_pong — desktop-only (turtle) +- [x] Pokemon Battle — pyodide +- [x] PONG — desktop-only (pygame) +- [x] Port_Scaner — desktop-only (TCP sockets) +- [x] prettified-stopwatch — desktop-only (pyperclip clipboard) +- [x] Print_Colored_Text — desktop-only (colorama) +- [x] ProjectEuler — pyodide +- [x] proxy-scrapper — desktop-only (live HTTP) +- [x] Python Banking System — pyodide +- [x] Python story generator — desktop-only (tkinter) +- [x] QRCode Scanner — desktop-only (OpenCV webcam) +- [x] QRCode-Generator — desktop-only (qrcode + disk) +- [x] qr-code-generator-with-window-and-simple-ui — desktop-only (tkinter) +- [x] Qt5_YouTube — desktop-only (PyQt5 + network) +- [x] QuickWordCloud — desktop-only (wordcloud + matplotlib) +- [x] Quiz Game — pyodide +- [x] Random-Quote-Generator — desktop-only (tkinter + API) +- [x] reciept generator — desktop-only (fpdf) +- [x] reddit-scraper — desktop-only (live HTTP) +- [x] regex-search — desktop-only (filesystem walk) +- [x] regex-strip — pyodide +- [x] Rename_Images — desktop-only (filesystem) +- [x] Resize_Image — desktop-only (pillow + disk) +- [x] RestrauntAPI — desktop-only (Django) +- [x] reuters-scraper — desktop-only (live HTTP) +- [x] Rock_Paper_Scissors — pyodide +- [x] Roll_A_dice — desktop-only (MySQL) +- [x] Rubik-tracking — desktop-only (OpenCV webcam) +- [x] Sales Optimizer — desktop-only (sklearn + matplotlib) +- [x] scheduledShutdown — desktop-only (os.system shutdown) +- [x] Scientific-Calculator — desktop-only (tkinter) +- [x] scrap-ycombinator — desktop-only (live HTTP) +- [x] ScreenRecorder — desktop-only (pyautogui + win32) +- [x] Seek_with_hand_track — desktop-only (webcam + mediapipe) +- [x] Selfie_with_Python — desktop-only (OpenCV webcam) +- [x] Send-Email — desktop-only (SMTP) +- [x] servo motor classifier — desktop-only (network CSV + matplotlib) +- [x] Simple-Plagiarism-Checker-Project — desktop-only (Flask server) +- [x] SketchifyMe — desktop-only (OpenCV GUI) +- [x] Skycast — desktop-only (Streamlit + API) +- [x] Slice-Audio — desktop-only (pydub/ffmpeg) +- [x] SMS_ChatBot — desktop-only (Flask + Twilio/OpenAI) +- [x] Snake Game — desktop-only (pygame) +- [x] snake_water_gun_game — desktop-only (rich package) +- [x] Social_media_content_creation — desktop-only (no Python code) +- [x] Socket — desktop-only (TCP sockets) +- [x] SongsMashup — desktop-only (Flask + downloads) +- [x] Space Shooter — desktop-only (pygame) +- [x] space_battle — desktop-only (pygame) +- [x] Speed-Type-test — desktop-only (pygame) +- [x] Split_Tip — pyodide +- [x] sponge-bob — desktop-only (turtle) +- [x] Spotify Player — desktop-only (Spotify/Twilio APIs) +- [x] Stock-Market-Dashboard — desktop-only (Streamlit) +- [x] student-management-system — desktop-only (MySQL) +- [x] Subnetting Flsm — pyodide +- [x] Subtitle_synchronizer.py — desktop-only (disk .srt files) +- [x] Sudoku_solver — pyodide +- [x] Sudoku-Solver — desktop-only (pygame) +- [x] takeImage — desktop-only (OpenCV webcam) +- [x] TennisTournamentSim — pyodide +- [x] Tesla — desktop-only (pygame) +- [x] Tetris Game — desktop-only (pygame) +- [x] Text Editor — desktop-only (tkinter) +- [x] Text Summarizer — desktop-only (spaCy model) +- [x] Text to Speech — desktop-only (gTTS + playback) +- [x] Text_to_SpreadSheet — desktop-only (disk walk + xlsx) +- [x] TextDetection — desktop-only (Google Vision API) +- [x] Text-to-Image Generation Project — desktop-only (GPU DL notebook) +- [x] text-translate — desktop-only (network + tkinter/gtts) +- [x] Tic-Tac-Toe — pyodide +- [x] TicTacToe-TylerPear — pyodide +- [x] Tile Matching — desktop-only (tkinter) +- [x] Timer — pyodide +- [x] Tkinter — desktop-only (tkinter) +- [x] ToDoList — pyodide +- [x] Turtle Pattern — desktop-only (turtle) +- [x] Turtle_Graphics — desktop-only (turtle) +- [x] Twitter-Bot — desktop-only (Telegram API) +- [x] Type Racer Game — desktop-only (tkinter) +- [x] url_shortener — desktop-only (API + pywin32) +- [x] Video Reversal — desktop-only (OpenCV video) +- [x] video_transcoder_project — desktop-only (Flask + FFmpeg) +- [x] Video-subtitle-generator — desktop-only (Whisper + FFmpeg) +- [x] Voice-to-Text — desktop-only (microphone) +- [x] Watermarker — desktop-only (Pillow + matplotlib) +- [x] Weather — desktop-only (live web API) +- [x] Web Scraping Jujustu Kaisen Manga — desktop-only (Selenium) +- [x] WebButtonSimpelGUI — desktop-only (tkinter) +- [x] web-crawler(movie extract) — desktop-only (live HTTP) +- [x] Website Blocker — desktop-only (hosts file) +- [x] Weights_in_different_planets_GUI — desktop-only (tkinter) +- [x] what-for-dinner — desktop-only (TheMealDB API) +- [x] Windows Logo — desktop-only (turtle) +- [x] Wine_quality_predictor — desktop-only (sklearn + CSV) +- [x] Word_Predictor — desktop-only (reads local file) +- [x] Worksheet_to_text — desktop-only (reads local xlsx) +- [x] World-Cup-Player-Comparison — desktop-only (live HTTP) +- [x] xls_to_xlsx — desktop-only (pywin32 + Excel) +- [x] YouTube Video Downloader — desktop-only (yt-dlp) diff --git a/projects/url_shortener/README.md b/projects/url_shortener/README.md index 5d4dd3e21..64e8801b8 100644 --- a/projects/url_shortener/README.md +++ b/projects/url_shortener/README.md @@ -30,4 +30,7 @@ I built code quality tools into project (linting, formatting, type annotation ch Author: [justinjohnson-dev](https://github.com/justinjohnson-dev) -Last Update: 05/25/2024 \ No newline at end of file +Last Update: 05/25/2024 +## Pyodide-runnable + +No — it uses `pyshorteners`/`requests` to call a live URL-shortening API and depends on `pywin32`. diff --git a/projects/video_transcoder_project/README.md b/projects/video_transcoder_project/README.md index 8f71f3c7c..da616192a 100644 --- a/projects/video_transcoder_project/README.md +++ b/projects/video_transcoder_project/README.md @@ -45,3 +45,7 @@ This is a basic implementation; additional features like error handling, security measures, and frontend enhancements can be added for a more robust application. Feel free to modify the code and folder structure according to your project requirements. + +## Pyodide-runnable + +No — it is a Flask web server that transcodes videos with FFmpeg. diff --git a/projects/web-crawler(movie extract)/README.md b/projects/web-crawler(movie extract)/README.md new file mode 100644 index 000000000..ffc05a8a4 --- /dev/null +++ b/projects/web-crawler(movie extract)/README.md @@ -0,0 +1,23 @@ +# Web Crawler (Movie Extract) + +A web-scraping script intended to crawl Rotten Tomatoes' top-movies list, extract movie URLs, names, and synopses, and save them into an `.xls` spreadsheet. + +## How to run + +```bash +pip install requests lxml beautifulsoup4 xlwt +python main.py +``` + +Note: the source file currently contains syntax errors and would need fixing before it runs. + +## Dependencies + +- requests +- lxml +- beautifulsoup4 +- xlwt + +## Pyodide-runnable + +No — it uses `requests` to scrape live websites, which is not available in Pyodide. diff --git a/projects/what-for-dinner/README.md b/projects/what-for-dinner/README.md new file mode 100644 index 000000000..58f48adae --- /dev/null +++ b/projects/what-for-dinner/README.md @@ -0,0 +1,18 @@ +# What For Dinner + +A console app that suggests a random meal for dinner. It fetches a random recipe from TheMealDB API and prints the meal name, origin, category, cooking instructions, and a YouTube link in a colorized format. + +## How to run + +```bash +pip install requests +python main.py +``` + +## Dependencies + +- requests + +## Pyodide-runnable + +No — it uses `requests` to call the live TheMealDB API, which is not available in Pyodide. diff --git a/projects/xls_to_xlsx/README.md b/projects/xls_to_xlsx/README.md index 3d4f6acb5..0cc47b2ea 100644 --- a/projects/xls_to_xlsx/README.md +++ b/projects/xls_to_xlsx/README.md @@ -43,3 +43,7 @@ python xls_to_xlsx.py ## Author [odhy](https://github.com/odhyp) + +## Pyodide-runnable + +No — it depends on `pywin32` and automates the desktop Microsoft Excel application. diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index c0156247c..000000000 --- a/pyproject.toml +++ /dev/null @@ -1,9 +0,0 @@ -[project] -name = "pybegin-worker" -version = "0.1.0" -description = "Cloudflare Worker backend for the python-beginner-projects sticker site" -requires-python = ">=3.12" -dependencies = [ - "qrcode>=7.4", - "pillow", -] diff --git a/python_modules/.synced b/python_modules/.synced deleted file mode 100644 index 70b02ffc1..000000000 --- a/python_modules/.synced +++ /dev/null @@ -1 +0,0 @@ -1.9.4 \ No newline at end of file diff --git a/python_modules/PIL/AvifImagePlugin.py b/python_modules/PIL/AvifImagePlugin.py deleted file mode 100644 index 366e0c864..000000000 --- a/python_modules/PIL/AvifImagePlugin.py +++ /dev/null @@ -1,291 +0,0 @@ -from __future__ import annotations - -import os -from io import BytesIO -from typing import IO - -from . import ExifTags, Image, ImageFile - -try: - from . import _avif - - SUPPORTED = True -except ImportError: - SUPPORTED = False - -# Decoder options as module globals, until there is a way to pass parameters -# to Image.open (see https://github.com/python-pillow/Pillow/issues/569) -DECODE_CODEC_CHOICE = "auto" -DEFAULT_MAX_THREADS = 0 - - -def get_codec_version(codec_name: str) -> str | None: - versions = _avif.codec_versions() - for version in versions.split(", "): - if version.split(" [")[0] == codec_name: - return version.split(":")[-1].split(" ")[0] - return None - - -def _accept(prefix: bytes) -> bool | str: - if prefix[4:8] != b"ftyp": - return False - major_brand = prefix[8:12] - if major_brand in ( - # coding brands - b"avif", - b"avis", - # We accept files with AVIF container brands; we can't yet know if - # the ftyp box has the correct compatible brands, but if it doesn't - # then the plugin will raise a SyntaxError which Pillow will catch - # before moving on to the next plugin that accepts the file. - # - # Also, because this file might not actually be an AVIF file, we - # don't raise an error if AVIF support isn't properly compiled. - b"mif1", - b"msf1", - ): - if not SUPPORTED: - return ( - "image file could not be identified because AVIF support not installed" - ) - return True - return False - - -def _get_default_max_threads() -> int: - if DEFAULT_MAX_THREADS: - return DEFAULT_MAX_THREADS - if hasattr(os, "sched_getaffinity"): - return len(os.sched_getaffinity(0)) - else: - return os.cpu_count() or 1 - - -class AvifImageFile(ImageFile.ImageFile): - format = "AVIF" - format_description = "AVIF image" - __frame = -1 - - def _open(self) -> None: - if not SUPPORTED: - msg = "image file could not be opened because AVIF support not installed" - raise SyntaxError(msg) - - if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available( - DECODE_CODEC_CHOICE - ): - msg = "Invalid opening codec" - raise ValueError(msg) - self._decoder = _avif.AvifDecoder( - self.fp.read(), - DECODE_CODEC_CHOICE, - _get_default_max_threads(), - ) - - # Get info from decoder - self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = ( - self._decoder.get_info() - ) - self.is_animated = self.n_frames > 1 - - if icc: - self.info["icc_profile"] = icc - if xmp: - self.info["xmp"] = xmp - - if exif_orientation != 1 or exif: - exif_data = Image.Exif() - if exif: - exif_data.load(exif) - original_orientation = exif_data.get(ExifTags.Base.Orientation, 1) - else: - original_orientation = 1 - if exif_orientation != original_orientation: - exif_data[ExifTags.Base.Orientation] = exif_orientation - exif = exif_data.tobytes() - if exif: - self.info["exif"] = exif - self.seek(0) - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - - # Set tile - self.__frame = frame - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] - - def load(self) -> Image.core.PixelAccess | None: - if self.tile: - # We need to load the image data for this frame - data, timescale, pts_in_timescales, duration_in_timescales = ( - self._decoder.get_frame(self.__frame) - ) - self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale)) - self.info["duration"] = round(1000 * (duration_in_timescales / timescale)) - - if self.fp and self._exclusive_fp: - self.fp.close() - self.fp = BytesIO(data) - - return super().load() - - def load_seek(self, pos: int) -> None: - pass - - def tell(self) -> int: - return self.__frame - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, save_all=True) - - -def _save( - im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False -) -> None: - info = im.encoderinfo.copy() - if save_all: - append_images = list(info.get("append_images", [])) - else: - append_images = [] - - total = 0 - for ims in [im] + append_images: - total += getattr(ims, "n_frames", 1) - - quality = info.get("quality", 75) - if not isinstance(quality, int) or quality < 0 or quality > 100: - msg = "Invalid quality setting" - raise ValueError(msg) - - duration = info.get("duration", 0) - subsampling = info.get("subsampling", "4:2:0") - speed = info.get("speed", 6) - max_threads = info.get("max_threads", _get_default_max_threads()) - codec = info.get("codec", "auto") - if codec != "auto" and not _avif.encoder_codec_available(codec): - msg = "Invalid saving codec" - raise ValueError(msg) - range_ = info.get("range", "full") - tile_rows_log2 = info.get("tile_rows", 0) - tile_cols_log2 = info.get("tile_cols", 0) - alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) - autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) - - icc_profile = info.get("icc_profile", im.info.get("icc_profile")) - exif_orientation = 1 - if exif := info.get("exif"): - if isinstance(exif, Image.Exif): - exif_data = exif - else: - exif_data = Image.Exif() - exif_data.load(exif) - if ExifTags.Base.Orientation in exif_data: - exif_orientation = exif_data.pop(ExifTags.Base.Orientation) - exif = exif_data.tobytes() if exif_data else b"" - elif isinstance(exif, Image.Exif): - exif = exif_data.tobytes() - - xmp = info.get("xmp") - - if isinstance(xmp, str): - xmp = xmp.encode("utf-8") - - advanced = info.get("advanced") - if advanced is not None: - if isinstance(advanced, dict): - advanced = advanced.items() - try: - advanced = tuple(advanced) - except TypeError: - invalid = True - else: - invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced) - if invalid: - msg = ( - "advanced codec options must be a dict of key-value string " - "pairs or a series of key-value two-tuples" - ) - raise ValueError(msg) - - # Setup the AVIF encoder - enc = _avif.AvifEncoder( - im.size, - subsampling, - quality, - speed, - max_threads, - codec, - range_, - tile_rows_log2, - tile_cols_log2, - alpha_premultiplied, - autotiling, - icc_profile or b"", - exif or b"", - exif_orientation, - xmp or b"", - advanced, - ) - - # Add each frame - frame_idx = 0 - frame_duration = 0 - cur_idx = im.tell() - is_single_frame = total == 1 - try: - for ims in [im] + append_images: - # Get number of frames in this image - nfr = getattr(ims, "n_frames", 1) - - for idx in range(nfr): - ims.seek(idx) - - # Make sure image mode is supported - frame = ims - rawmode = ims.mode - if ims.mode not in {"RGB", "RGBA"}: - rawmode = "RGBA" if ims.has_transparency_data else "RGB" - frame = ims.convert(rawmode) - - # Update frame duration - if isinstance(duration, (list, tuple)): - frame_duration = duration[frame_idx] - else: - frame_duration = duration - - # Append the frame to the animation encoder - enc.add( - frame.tobytes("raw", rawmode), - frame_duration, - frame.size, - rawmode, - is_single_frame, - ) - - # Update frame index - frame_idx += 1 - - if not save_all: - break - - finally: - im.seek(cur_idx) - - # Get the final output from the encoder - data = enc.finish() - if data is None: - msg = "cannot write file as AVIF (encoder returned None)" - raise OSError(msg) - - fp.write(data) - - -Image.register_open(AvifImageFile.format, AvifImageFile, _accept) -if SUPPORTED: - Image.register_save(AvifImageFile.format, _save) - Image.register_save_all(AvifImageFile.format, _save_all) - Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"]) - Image.register_mime(AvifImageFile.format, "image/avif") diff --git a/python_modules/PIL/BdfFontFile.py b/python_modules/PIL/BdfFontFile.py deleted file mode 100644 index f175e2f4f..000000000 --- a/python_modules/PIL/BdfFontFile.py +++ /dev/null @@ -1,122 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# bitmap distribution font (bdf) file parser -# -# history: -# 1996-05-16 fl created (as bdf2pil) -# 1997-08-25 fl converted to FontFile driver -# 2001-05-25 fl removed bogus __init__ call -# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev) -# 2003-04-22 fl more robustification (from Graham Dumpleton) -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1997-2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -""" -Parse X Bitmap Distribution Format (BDF) -""" -from __future__ import annotations - -from typing import BinaryIO - -from . import FontFile, Image - - -def bdf_char( - f: BinaryIO, -) -> ( - tuple[ - str, - int, - tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]], - Image.Image, - ] - | None -): - # skip to STARTCHAR - while True: - s = f.readline() - if not s: - return None - if s.startswith(b"STARTCHAR"): - break - id = s[9:].strip().decode("ascii") - - # load symbol properties - props = {} - while True: - s = f.readline() - if not s or s.startswith(b"BITMAP"): - break - i = s.find(b" ") - props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") - - # load bitmap - bitmap = bytearray() - while True: - s = f.readline() - if not s or s.startswith(b"ENDCHAR"): - break - bitmap += s[:-1] - - # The word BBX - # followed by the width in x (BBw), height in y (BBh), - # and x and y displacement (BBxoff0, BByoff0) - # of the lower left corner from the origin of the character. - width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split()) - - # The word DWIDTH - # followed by the width in x and y of the character in device pixels. - dwx, dwy = (int(p) for p in props["DWIDTH"].split()) - - bbox = ( - (dwx, dwy), - (x_disp, -y_disp - height, width + x_disp, -y_disp), - (0, 0, width, height), - ) - - try: - im = Image.frombytes("1", (width, height), bitmap, "hex", "1") - except ValueError: - # deal with zero-width characters - im = Image.new("1", (width, height)) - - return id, int(props["ENCODING"]), bbox, im - - -class BdfFontFile(FontFile.FontFile): - """Font file plugin for the X11 BDF format.""" - - def __init__(self, fp: BinaryIO) -> None: - super().__init__() - - s = fp.readline() - if not s.startswith(b"STARTFONT 2.1"): - msg = "not a valid BDF file" - raise SyntaxError(msg) - - props = {} - comments = [] - - while True: - s = fp.readline() - if not s or s.startswith(b"ENDPROPERTIES"): - break - i = s.find(b" ") - props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") - if s[:i] in [b"COMMENT", b"COPYRIGHT"]: - if s.find(b"LogicalFontDescription") < 0: - comments.append(s[i + 1 : -1].decode("ascii")) - - while True: - c = bdf_char(fp) - if not c: - break - id, ch, (xy, dst, src), im = c - if 0 <= ch < len(self.glyph): - self.glyph[ch] = xy, dst, src, im diff --git a/python_modules/PIL/BlpImagePlugin.py b/python_modules/PIL/BlpImagePlugin.py deleted file mode 100644 index f7be7746d..000000000 --- a/python_modules/PIL/BlpImagePlugin.py +++ /dev/null @@ -1,497 +0,0 @@ -""" -Blizzard Mipmap Format (.blp) -Jerome Leclanche - -The contents of this file are hereby released in the public domain (CC0) -Full text of the CC0 license: - https://creativecommons.org/publicdomain/zero/1.0/ - -BLP1 files, used mostly in Warcraft III, are not fully supported. -All types of BLP2 files used in World of Warcraft are supported. - -The BLP file structure consists of a header, up to 16 mipmaps of the -texture - -Texture sizes must be powers of two, though the two dimensions do -not have to be equal; 512x256 is valid, but 512x200 is not. -The first mipmap (mipmap #0) is the full size image; each subsequent -mipmap halves both dimensions. The final mipmap should be 1x1. - -BLP files come in many different flavours: -* JPEG-compressed (type == 0) - only supported for BLP1. -* RAW images (type == 1, encoding == 1). Each mipmap is stored as an - array of 8-bit values, one per pixel, left to right, top to bottom. - Each value is an index to the palette. -* DXT-compressed (type == 1, encoding == 2): -- DXT1 compression is used if alpha_encoding == 0. - - An additional alpha bit is used if alpha_depth == 1. - - DXT3 compression is used if alpha_encoding == 1. - - DXT5 compression is used if alpha_encoding == 7. -""" - -from __future__ import annotations - -import abc -import os -import struct -from enum import IntEnum -from io import BytesIO -from typing import IO - -from . import Image, ImageFile - - -class Format(IntEnum): - JPEG = 0 - - -class Encoding(IntEnum): - UNCOMPRESSED = 1 - DXT = 2 - UNCOMPRESSED_RAW_BGRA = 3 - - -class AlphaEncoding(IntEnum): - DXT1 = 0 - DXT3 = 1 - DXT5 = 7 - - -def unpack_565(i: int) -> tuple[int, int, int]: - return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 - - -def decode_dxt1( - data: bytes, alpha: bool = False -) -> tuple[bytearray, bytearray, bytearray, bytearray]: - """ - input: one "row" of data (i.e. will produce 4*width pixels) - """ - - blocks = len(data) // 8 # number of blocks in row - ret = (bytearray(), bytearray(), bytearray(), bytearray()) - - for block_index in range(blocks): - # Decode next 8-byte block. - idx = block_index * 8 - color0, color1, bits = struct.unpack_from("> 2 - - a = 0xFF - if control == 0: - r, g, b = r0, g0, b0 - elif control == 1: - r, g, b = r1, g1, b1 - elif control == 2: - if color0 > color1: - r = (2 * r0 + r1) // 3 - g = (2 * g0 + g1) // 3 - b = (2 * b0 + b1) // 3 - else: - r = (r0 + r1) // 2 - g = (g0 + g1) // 2 - b = (b0 + b1) // 2 - elif control == 3: - if color0 > color1: - r = (2 * r1 + r0) // 3 - g = (2 * g1 + g0) // 3 - b = (2 * b1 + b0) // 3 - else: - r, g, b, a = 0, 0, 0, 0 - - if alpha: - ret[j].extend([r, g, b, a]) - else: - ret[j].extend([r, g, b]) - - return ret - - -def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: - """ - input: one "row" of data (i.e. will produce 4*width pixels) - """ - - blocks = len(data) // 16 # number of blocks in row - ret = (bytearray(), bytearray(), bytearray(), bytearray()) - - for block_index in range(blocks): - idx = block_index * 16 - block = data[idx : idx + 16] - # Decode next 16-byte block. - bits = struct.unpack_from("<8B", block) - color0, color1 = struct.unpack_from(">= 4 - else: - high = True - a &= 0xF - a *= 17 # We get a value between 0 and 15 - - color_code = (code >> 2 * (4 * j + i)) & 0x03 - - if color_code == 0: - r, g, b = r0, g0, b0 - elif color_code == 1: - r, g, b = r1, g1, b1 - elif color_code == 2: - r = (2 * r0 + r1) // 3 - g = (2 * g0 + g1) // 3 - b = (2 * b0 + b1) // 3 - elif color_code == 3: - r = (2 * r1 + r0) // 3 - g = (2 * g1 + g0) // 3 - b = (2 * b1 + b0) // 3 - - ret[j].extend([r, g, b, a]) - - return ret - - -def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: - """ - input: one "row" of data (i.e. will produce 4 * width pixels) - """ - - blocks = len(data) // 16 # number of blocks in row - ret = (bytearray(), bytearray(), bytearray(), bytearray()) - - for block_index in range(blocks): - idx = block_index * 16 - block = data[idx : idx + 16] - # Decode next 16-byte block. - a0, a1 = struct.unpack_from("> alphacode_index) & 0x07 - elif alphacode_index == 15: - alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06) - else: # alphacode_index >= 18 and alphacode_index <= 45 - alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07 - - if alphacode == 0: - a = a0 - elif alphacode == 1: - a = a1 - elif a0 > a1: - a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7 - elif alphacode == 6: - a = 0 - elif alphacode == 7: - a = 255 - else: - a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5 - - color_code = (code >> 2 * (4 * j + i)) & 0x03 - - if color_code == 0: - r, g, b = r0, g0, b0 - elif color_code == 1: - r, g, b = r1, g1, b1 - elif color_code == 2: - r = (2 * r0 + r1) // 3 - g = (2 * g0 + g1) // 3 - b = (2 * b0 + b1) // 3 - elif color_code == 3: - r = (2 * r1 + r0) // 3 - g = (2 * g1 + g0) // 3 - b = (2 * b1 + b0) // 3 - - ret[j].extend([r, g, b, a]) - - return ret - - -class BLPFormatError(NotImplementedError): - pass - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith((b"BLP1", b"BLP2")) - - -class BlpImageFile(ImageFile.ImageFile): - """ - Blizzard Mipmap Format - """ - - format = "BLP" - format_description = "Blizzard Mipmap Format" - - def _open(self) -> None: - self.magic = self.fp.read(4) - if not _accept(self.magic): - msg = f"Bad BLP magic {repr(self.magic)}" - raise BLPFormatError(msg) - - compression = struct.unpack(" tuple[int, int]: - try: - self._read_header() - self._load() - except struct.error as e: - msg = "Truncated BLP file" - raise OSError(msg) from e - return -1, 0 - - @abc.abstractmethod - def _load(self) -> None: - pass - - def _read_header(self) -> None: - self._offsets = struct.unpack("<16I", self._safe_read(16 * 4)) - self._lengths = struct.unpack("<16I", self._safe_read(16 * 4)) - - def _safe_read(self, length: int) -> bytes: - assert self.fd is not None - return ImageFile._safe_read(self.fd, length) - - def _read_palette(self) -> list[tuple[int, int, int, int]]: - ret = [] - for i in range(256): - try: - b, g, r, a = struct.unpack("<4B", self._safe_read(4)) - except struct.error: - break - ret.append((b, g, r, a)) - return ret - - def _read_bgra( - self, palette: list[tuple[int, int, int, int]], alpha: bool - ) -> bytearray: - data = bytearray() - _data = BytesIO(self._safe_read(self._lengths[0])) - while True: - try: - (offset,) = struct.unpack(" None: - self._compression, self._encoding, alpha = self.args - - if self._compression == Format.JPEG: - self._decode_jpeg_stream() - - elif self._compression == 1: - if self._encoding in (4, 5): - palette = self._read_palette() - data = self._read_bgra(palette, alpha) - self.set_as_raw(data) - else: - msg = f"Unsupported BLP encoding {repr(self._encoding)}" - raise BLPFormatError(msg) - else: - msg = f"Unsupported BLP compression {repr(self._encoding)}" - raise BLPFormatError(msg) - - def _decode_jpeg_stream(self) -> None: - from .JpegImagePlugin import JpegImageFile - - (jpeg_header_size,) = struct.unpack(" None: - self._compression, self._encoding, alpha, self._alpha_encoding = self.args - - palette = self._read_palette() - - assert self.fd is not None - self.fd.seek(self._offsets[0]) - - if self._compression == 1: - # Uncompressed or DirectX compression - - if self._encoding == Encoding.UNCOMPRESSED: - data = self._read_bgra(palette, alpha) - - elif self._encoding == Encoding.DXT: - data = bytearray() - if self._alpha_encoding == AlphaEncoding.DXT1: - linesize = (self.state.xsize + 3) // 4 * 8 - for yb in range((self.state.ysize + 3) // 4): - for d in decode_dxt1(self._safe_read(linesize), alpha): - data += d - - elif self._alpha_encoding == AlphaEncoding.DXT3: - linesize = (self.state.xsize + 3) // 4 * 16 - for yb in range((self.state.ysize + 3) // 4): - for d in decode_dxt3(self._safe_read(linesize)): - data += d - - elif self._alpha_encoding == AlphaEncoding.DXT5: - linesize = (self.state.xsize + 3) // 4 * 16 - for yb in range((self.state.ysize + 3) // 4): - for d in decode_dxt5(self._safe_read(linesize)): - data += d - else: - msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}" - raise BLPFormatError(msg) - else: - msg = f"Unknown BLP encoding {repr(self._encoding)}" - raise BLPFormatError(msg) - - else: - msg = f"Unknown BLP compression {repr(self._compression)}" - raise BLPFormatError(msg) - - self.set_as_raw(data) - - -class BLPEncoder(ImageFile.PyEncoder): - _pushes_fd = True - - def _write_palette(self) -> bytes: - data = b"" - assert self.im is not None - palette = self.im.getpalette("RGBA", "RGBA") - for i in range(len(palette) // 4): - r, g, b, a = palette[i * 4 : (i + 1) * 4] - data += struct.pack("<4B", b, g, r, a) - while len(data) < 256 * 4: - data += b"\x00" * 4 - return data - - def encode(self, bufsize: int) -> tuple[int, int, bytes]: - palette_data = self._write_palette() - - offset = 20 + 16 * 4 * 2 + len(palette_data) - data = struct.pack("<16I", offset, *((0,) * 15)) - - assert self.im is not None - w, h = self.im.size - data += struct.pack("<16I", w * h, *((0,) * 15)) - - data += palette_data - - for y in range(h): - for x in range(w): - data += struct.pack(" None: - if im.mode != "P": - msg = "Unsupported BLP image mode" - raise ValueError(msg) - - magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2" - fp.write(magic) - - assert im.palette is not None - fp.write(struct.pack(" mode, rawmode - 1: ("P", "P;1"), - 4: ("P", "P;4"), - 8: ("P", "P"), - 16: ("RGB", "BGR;15"), - 24: ("RGB", "BGR"), - 32: ("RGB", "BGRX"), -} - -USE_RAW_ALPHA = False - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"BM") - - -def _dib_accept(prefix: bytes) -> bool: - return i32(prefix) in [12, 40, 52, 56, 64, 108, 124] - - -# ============================================================================= -# Image plugin for the Windows BMP format. -# ============================================================================= -class BmpImageFile(ImageFile.ImageFile): - """Image plugin for the Windows Bitmap format (BMP)""" - - # ------------------------------------------------------------- Description - format_description = "Windows Bitmap" - format = "BMP" - - # -------------------------------------------------- BMP Compression values - COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5} - for k, v in COMPRESSIONS.items(): - vars()[k] = v - - def _bitmap(self, header: int = 0, offset: int = 0) -> None: - """Read relevant info about the BMP""" - read, seek = self.fp.read, self.fp.seek - if header: - seek(header) - # read bmp header size @offset 14 (this is part of the header size) - file_info: dict[str, bool | int | tuple[int, ...]] = { - "header_size": i32(read(4)), - "direction": -1, - } - - # -------------------- If requested, read header at a specific position - # read the rest of the bmp header, without its size - assert isinstance(file_info["header_size"], int) - header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) - - # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1 - # ----- This format has different offsets because of width/height types - # 12: BITMAPCOREHEADER/OS21XBITMAPHEADER - if file_info["header_size"] == 12: - file_info["width"] = i16(header_data, 0) - file_info["height"] = i16(header_data, 2) - file_info["planes"] = i16(header_data, 4) - file_info["bits"] = i16(header_data, 6) - file_info["compression"] = self.COMPRESSIONS["RAW"] - file_info["palette_padding"] = 3 - - # --------------------------------------------- Windows Bitmap v3 to v5 - # 40: BITMAPINFOHEADER - # 52: BITMAPV2HEADER - # 56: BITMAPV3HEADER - # 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER - # 108: BITMAPV4HEADER - # 124: BITMAPV5HEADER - elif file_info["header_size"] in (40, 52, 56, 64, 108, 124): - file_info["y_flip"] = header_data[7] == 0xFF - file_info["direction"] = 1 if file_info["y_flip"] else -1 - file_info["width"] = i32(header_data, 0) - file_info["height"] = ( - i32(header_data, 4) - if not file_info["y_flip"] - else 2**32 - i32(header_data, 4) - ) - file_info["planes"] = i16(header_data, 8) - file_info["bits"] = i16(header_data, 10) - file_info["compression"] = i32(header_data, 12) - # byte size of pixel data - file_info["data_size"] = i32(header_data, 16) - file_info["pixels_per_meter"] = ( - i32(header_data, 20), - i32(header_data, 24), - ) - file_info["colors"] = i32(header_data, 28) - file_info["palette_padding"] = 4 - assert isinstance(file_info["pixels_per_meter"], tuple) - self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) - if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: - masks = ["r_mask", "g_mask", "b_mask"] - if len(header_data) >= 48: - if len(header_data) >= 52: - masks.append("a_mask") - else: - file_info["a_mask"] = 0x0 - for idx, mask in enumerate(masks): - file_info[mask] = i32(header_data, 36 + idx * 4) - else: - # 40 byte headers only have the three components in the - # bitfields masks, ref: - # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx - # See also - # https://github.com/python-pillow/Pillow/issues/1293 - # There is a 4th component in the RGBQuad, in the alpha - # location, but it is listed as a reserved component, - # and it is not generally an alpha channel - file_info["a_mask"] = 0x0 - for mask in masks: - file_info[mask] = i32(read(4)) - assert isinstance(file_info["r_mask"], int) - assert isinstance(file_info["g_mask"], int) - assert isinstance(file_info["b_mask"], int) - assert isinstance(file_info["a_mask"], int) - file_info["rgb_mask"] = ( - file_info["r_mask"], - file_info["g_mask"], - file_info["b_mask"], - ) - file_info["rgba_mask"] = ( - file_info["r_mask"], - file_info["g_mask"], - file_info["b_mask"], - file_info["a_mask"], - ) - else: - msg = f"Unsupported BMP header type ({file_info['header_size']})" - raise OSError(msg) - - # ------------------ Special case : header is reported 40, which - # ---------------------- is shorter than real size for bpp >= 16 - assert isinstance(file_info["width"], int) - assert isinstance(file_info["height"], int) - self._size = file_info["width"], file_info["height"] - - # ------- If color count was not found in the header, compute from bits - assert isinstance(file_info["bits"], int) - file_info["colors"] = ( - file_info["colors"] - if file_info.get("colors", 0) - else (1 << file_info["bits"]) - ) - assert isinstance(file_info["colors"], int) - if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: - offset += 4 * file_info["colors"] - - # ---------------------- Check bit depth for unusual unsupported values - self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", "")) - if not self.mode: - msg = f"Unsupported BMP pixel depth ({file_info['bits']})" - raise OSError(msg) - - # ---------------- Process BMP with Bitfields compression (not palette) - decoder_name = "raw" - if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: - SUPPORTED: dict[int, list[tuple[int, ...]]] = { - 32: [ - (0xFF0000, 0xFF00, 0xFF, 0x0), - (0xFF000000, 0xFF0000, 0xFF00, 0x0), - (0xFF000000, 0xFF00, 0xFF, 0x0), - (0xFF000000, 0xFF0000, 0xFF00, 0xFF), - (0xFF, 0xFF00, 0xFF0000, 0xFF000000), - (0xFF0000, 0xFF00, 0xFF, 0xFF000000), - (0xFF000000, 0xFF00, 0xFF, 0xFF0000), - (0x0, 0x0, 0x0, 0x0), - ], - 24: [(0xFF0000, 0xFF00, 0xFF)], - 16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)], - } - MASK_MODES = { - (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", - (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", - (32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR", - (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", - (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", - (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", - (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR", - (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", - (24, (0xFF0000, 0xFF00, 0xFF)): "BGR", - (16, (0xF800, 0x7E0, 0x1F)): "BGR;16", - (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15", - } - if file_info["bits"] in SUPPORTED: - if ( - file_info["bits"] == 32 - and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] - ): - assert isinstance(file_info["rgba_mask"], tuple) - raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] - self._mode = "RGBA" if "A" in raw_mode else self.mode - elif ( - file_info["bits"] in (24, 16) - and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] - ): - assert isinstance(file_info["rgb_mask"], tuple) - raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] - else: - msg = "Unsupported BMP bitfields layout" - raise OSError(msg) - else: - msg = "Unsupported BMP bitfields layout" - raise OSError(msg) - elif file_info["compression"] == self.COMPRESSIONS["RAW"]: - if file_info["bits"] == 32 and ( - header == 22 or USE_RAW_ALPHA # 32-bit .cur offset - ): - raw_mode, self._mode = "BGRA", "RGBA" - elif file_info["compression"] in ( - self.COMPRESSIONS["RLE8"], - self.COMPRESSIONS["RLE4"], - ): - decoder_name = "bmp_rle" - else: - msg = f"Unsupported BMP compression ({file_info['compression']})" - raise OSError(msg) - - # --------------- Once the header is processed, process the palette/LUT - if self.mode == "P": # Paletted for 1, 4 and 8 bit images - # ---------------------------------------------------- 1-bit images - if not (0 < file_info["colors"] <= 65536): - msg = f"Unsupported BMP Palette size ({file_info['colors']})" - raise OSError(msg) - else: - assert isinstance(file_info["palette_padding"], int) - padding = file_info["palette_padding"] - palette = read(padding * file_info["colors"]) - grayscale = True - indices = ( - (0, 255) - if file_info["colors"] == 2 - else list(range(file_info["colors"])) - ) - - # ----------------- Check if grayscale and ignore palette if so - for ind, val in enumerate(indices): - rgb = palette[ind * padding : ind * padding + 3] - if rgb != o8(val) * 3: - grayscale = False - - # ------- If all colors are gray, white or black, ditch palette - if grayscale: - self._mode = "1" if file_info["colors"] == 2 else "L" - raw_mode = self.mode - else: - self._mode = "P" - self.palette = ImagePalette.raw( - "BGRX" if padding == 4 else "BGR", palette - ) - - # ---------------------------- Finally set the tile data for the plugin - self.info["compression"] = file_info["compression"] - args: list[Any] = [raw_mode] - if decoder_name == "bmp_rle": - args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"]) - else: - assert isinstance(file_info["width"], int) - args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) - args.append(file_info["direction"]) - self.tile = [ - ImageFile._Tile( - decoder_name, - (0, 0, file_info["width"], file_info["height"]), - offset or self.fp.tell(), - tuple(args), - ) - ] - - def _open(self) -> None: - """Open file, check magic number and read header""" - # read 14 bytes: magic number, filesize, reserved, header final offset - head_data = self.fp.read(14) - # choke if the file does not have the required magic bytes - if not _accept(head_data): - msg = "Not a BMP file" - raise SyntaxError(msg) - # read the start position of the BMP image data (u32) - offset = i32(head_data, 10) - # load bitmap information (offset=raster info) - self._bitmap(offset=offset) - - -class BmpRleDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - rle4 = self.args[1] - data = bytearray() - x = 0 - dest_length = self.state.xsize * self.state.ysize - while len(data) < dest_length: - pixels = self.fd.read(1) - byte = self.fd.read(1) - if not pixels or not byte: - break - num_pixels = pixels[0] - if num_pixels: - # encoded mode - if x + num_pixels > self.state.xsize: - # Too much data for row - num_pixels = max(0, self.state.xsize - x) - if rle4: - first_pixel = o8(byte[0] >> 4) - second_pixel = o8(byte[0] & 0x0F) - for index in range(num_pixels): - if index % 2 == 0: - data += first_pixel - else: - data += second_pixel - else: - data += byte * num_pixels - x += num_pixels - else: - if byte[0] == 0: - # end of line - while len(data) % self.state.xsize != 0: - data += b"\x00" - x = 0 - elif byte[0] == 1: - # end of bitmap - break - elif byte[0] == 2: - # delta - bytes_read = self.fd.read(2) - if len(bytes_read) < 2: - break - right, up = self.fd.read(2) - data += b"\x00" * (right + up * self.state.xsize) - x = len(data) % self.state.xsize - else: - # absolute mode - if rle4: - # 2 pixels per byte - byte_count = byte[0] // 2 - bytes_read = self.fd.read(byte_count) - for byte_read in bytes_read: - data += o8(byte_read >> 4) - data += o8(byte_read & 0x0F) - else: - byte_count = byte[0] - bytes_read = self.fd.read(byte_count) - data += bytes_read - if len(bytes_read) < byte_count: - break - x += byte[0] - - # align to 16-bit word boundary - if self.fd.tell() % 2 != 0: - self.fd.seek(1, os.SEEK_CUR) - rawmode = "L" if self.mode == "L" else "P" - self.set_as_raw(bytes(data), rawmode, (0, self.args[-1])) - return -1, 0 - - -# ============================================================================= -# Image plugin for the DIB format (BMP alias) -# ============================================================================= -class DibImageFile(BmpImageFile): - format = "DIB" - format_description = "Windows Bitmap" - - def _open(self) -> None: - self._bitmap() - - -# -# -------------------------------------------------------------------- -# Write BMP file - - -SAVE = { - "1": ("1", 1, 2), - "L": ("L", 8, 256), - "P": ("P", 8, 256), - "RGB": ("BGR", 24, 0), - "RGBA": ("BGRA", 32, 0), -} - - -def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, False) - - -def _save( - im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True -) -> None: - try: - rawmode, bits, colors = SAVE[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as BMP" - raise OSError(msg) from e - - info = im.encoderinfo - - dpi = info.get("dpi", (96, 96)) - - # 1 meter == 39.3701 inches - ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi) - - stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) - header = 40 # or 64 for OS/2 version 2 - image = stride * im.size[1] - - if im.mode == "1": - palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255)) - elif im.mode == "L": - palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256)) - elif im.mode == "P": - palette = im.im.getpalette("RGB", "BGRX") - colors = len(palette) // 4 - else: - palette = None - - # bitmap header - if bitmap_header: - offset = 14 + header + colors * 4 - file_size = offset + image - if file_size > 2**32 - 1: - msg = "File size is too large for the BMP format" - raise ValueError(msg) - fp.write( - b"BM" # file type (magic) - + o32(file_size) # file size - + o32(0) # reserved - + o32(offset) # image data offset - ) - - # bitmap info header - fp.write( - o32(header) # info header size - + o32(im.size[0]) # width - + o32(im.size[1]) # height - + o16(1) # planes - + o16(bits) # depth - + o32(0) # compression (0=uncompressed) - + o32(image) # size of bitmap - + o32(ppm[0]) # resolution - + o32(ppm[1]) # resolution - + o32(colors) # colors used - + o32(colors) # colors important - ) - - fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) - - if palette: - fp.write(palette) - - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))] - ) - - -# -# -------------------------------------------------------------------- -# Registry - - -Image.register_open(BmpImageFile.format, BmpImageFile, _accept) -Image.register_save(BmpImageFile.format, _save) - -Image.register_extension(BmpImageFile.format, ".bmp") - -Image.register_mime(BmpImageFile.format, "image/bmp") - -Image.register_decoder("bmp_rle", BmpRleDecoder) - -Image.register_open(DibImageFile.format, DibImageFile, _dib_accept) -Image.register_save(DibImageFile.format, _dib_save) - -Image.register_extension(DibImageFile.format, ".dib") - -Image.register_mime(DibImageFile.format, "image/bmp") diff --git a/python_modules/PIL/BufrStubImagePlugin.py b/python_modules/PIL/BufrStubImagePlugin.py deleted file mode 100644 index 8c5da14f5..000000000 --- a/python_modules/PIL/BufrStubImagePlugin.py +++ /dev/null @@ -1,75 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# BUFR stub adapter -# -# Copyright (c) 1996-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import IO - -from . import Image, ImageFile - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific BUFR image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -# -------------------------------------------------------------------- -# Image adapter - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith((b"BUFR", b"ZCZC")) - - -class BufrStubImageFile(ImageFile.StubImageFile): - format = "BUFR" - format_description = "BUFR" - - def _open(self) -> None: - if not _accept(self.fp.read(4)): - msg = "Not a BUFR file" - raise SyntaxError(msg) - - self.fp.seek(-4, os.SEEK_CUR) - - # make something up - self._mode = "F" - self._size = 1, 1 - - loader = self._load() - if loader: - loader.open(self) - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "BUFR save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept) -Image.register_save(BufrStubImageFile.format, _save) - -Image.register_extension(BufrStubImageFile.format, ".bufr") diff --git a/python_modules/PIL/ContainerIO.py b/python_modules/PIL/ContainerIO.py deleted file mode 100644 index ec9e66c71..000000000 --- a/python_modules/PIL/ContainerIO.py +++ /dev/null @@ -1,173 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a class to read from a container file -# -# History: -# 1995-06-18 fl Created -# 1995-09-07 fl Added readline(), readlines() -# -# Copyright (c) 1997-2001 by Secret Labs AB -# Copyright (c) 1995 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -from collections.abc import Iterable -from typing import IO, AnyStr, NoReturn - - -class ContainerIO(IO[AnyStr]): - """ - A file object that provides read access to a part of an existing - file (for example a TAR file). - """ - - def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None: - """ - Create file object. - - :param file: Existing file. - :param offset: Start of region, in bytes. - :param length: Size of region, in bytes. - """ - self.fh: IO[AnyStr] = file - self.pos = 0 - self.offset = offset - self.length = length - self.fh.seek(offset) - - ## - # Always false. - - def isatty(self) -> bool: - return False - - def seekable(self) -> bool: - return True - - def seek(self, offset: int, mode: int = io.SEEK_SET) -> int: - """ - Move file pointer. - - :param offset: Offset in bytes. - :param mode: Starting position. Use 0 for beginning of region, 1 - for current offset, and 2 for end of region. You cannot move - the pointer outside the defined region. - :returns: Offset from start of region, in bytes. - """ - if mode == 1: - self.pos = self.pos + offset - elif mode == 2: - self.pos = self.length + offset - else: - self.pos = offset - # clamp - self.pos = max(0, min(self.pos, self.length)) - self.fh.seek(self.offset + self.pos) - return self.pos - - def tell(self) -> int: - """ - Get current file pointer. - - :returns: Offset from start of region, in bytes. - """ - return self.pos - - def readable(self) -> bool: - return True - - def read(self, n: int = -1) -> AnyStr: - """ - Read data. - - :param n: Number of bytes to read. If omitted, zero or negative, - read until end of region. - :returns: An 8-bit string. - """ - if n > 0: - n = min(n, self.length - self.pos) - else: - n = self.length - self.pos - if n <= 0: # EOF - return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] - self.pos = self.pos + n - return self.fh.read(n) - - def readline(self, n: int = -1) -> AnyStr: - """ - Read a line of text. - - :param n: Number of bytes to read. If omitted, zero or negative, - read until end of line. - :returns: An 8-bit string. - """ - s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] - newline_character = b"\n" if "b" in self.fh.mode else "\n" - while True: - c = self.read(1) - if not c: - break - s = s + c - if c == newline_character or len(s) == n: - break - return s - - def readlines(self, n: int | None = -1) -> list[AnyStr]: - """ - Read multiple lines of text. - - :param n: Number of lines to read. If omitted, zero, negative or None, - read until end of region. - :returns: A list of 8-bit strings. - """ - lines = [] - while True: - s = self.readline() - if not s: - break - lines.append(s) - if len(lines) == n: - break - return lines - - def writable(self) -> bool: - return False - - def write(self, b: AnyStr) -> NoReturn: - raise NotImplementedError() - - def writelines(self, lines: Iterable[AnyStr]) -> NoReturn: - raise NotImplementedError() - - def truncate(self, size: int | None = None) -> int: - raise NotImplementedError() - - def __enter__(self) -> ContainerIO[AnyStr]: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def __iter__(self) -> ContainerIO[AnyStr]: - return self - - def __next__(self) -> AnyStr: - line = self.readline() - if not line: - msg = "end of region" - raise StopIteration(msg) - return line - - def fileno(self) -> int: - return self.fh.fileno() - - def flush(self) -> None: - self.fh.flush() - - def close(self) -> None: - self.fh.close() diff --git a/python_modules/PIL/CurImagePlugin.py b/python_modules/PIL/CurImagePlugin.py deleted file mode 100644 index b817dbc87..000000000 --- a/python_modules/PIL/CurImagePlugin.py +++ /dev/null @@ -1,75 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Windows Cursor support for PIL -# -# notes: -# uses BmpImagePlugin.py to read the bitmap data. -# -# history: -# 96-05-27 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import BmpImagePlugin, Image, ImageFile -from ._binary import i16le as i16 -from ._binary import i32le as i32 - -# -# -------------------------------------------------------------------- - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"\0\0\2\0") - - -## -# Image plugin for Windows Cursor files. - - -class CurImageFile(BmpImagePlugin.BmpImageFile): - format = "CUR" - format_description = "Windows Cursor" - - def _open(self) -> None: - offset = self.fp.tell() - - # check magic - s = self.fp.read(6) - if not _accept(s): - msg = "not a CUR file" - raise SyntaxError(msg) - - # pick the largest cursor in the file - m = b"" - for i in range(i16(s, 4)): - s = self.fp.read(16) - if not m: - m = s - elif s[0] > m[0] and s[1] > m[1]: - m = s - if not m: - msg = "No cursors were found" - raise TypeError(msg) - - # load as bitmap - self._bitmap(i32(m, 12) + offset) - - # patch up the bitmap height - self._size = self.size[0], self.size[1] // 2 - d, e, o, a = self.tile[0] - self.tile[0] = ImageFile._Tile(d, (0, 0) + self.size, o, a) - - -# -# -------------------------------------------------------------------- - -Image.register_open(CurImageFile.format, CurImageFile, _accept) - -Image.register_extension(CurImageFile.format, ".cur") diff --git a/python_modules/PIL/DcxImagePlugin.py b/python_modules/PIL/DcxImagePlugin.py deleted file mode 100644 index aea661b9c..000000000 --- a/python_modules/PIL/DcxImagePlugin.py +++ /dev/null @@ -1,83 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# DCX file handling -# -# DCX is a container file format defined by Intel, commonly used -# for fax applications. Each DCX file consists of a directory -# (a list of file offsets) followed by a set of (usually 1-bit) -# PCX files. -# -# History: -# 1995-09-09 fl Created -# 1996-03-20 fl Properly derived from PcxImageFile. -# 1998-07-15 fl Renamed offset attribute to avoid name clash -# 2002-07-30 fl Fixed file handling -# -# Copyright (c) 1997-98 by Secret Labs AB. -# Copyright (c) 1995-96 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image -from ._binary import i32le as i32 -from ._util import DeferredError -from .PcxImagePlugin import PcxImageFile - -MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 4 and i32(prefix) == MAGIC - - -## -# Image plugin for the Intel DCX format. - - -class DcxImageFile(PcxImageFile): - format = "DCX" - format_description = "Intel DCX" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # Header - s = self.fp.read(4) - if not _accept(s): - msg = "not a DCX file" - raise SyntaxError(msg) - - # Component directory - self._offset = [] - for i in range(1024): - offset = i32(self.fp.read(4)) - if not offset: - break - self._offset.append(offset) - - self._fp = self.fp - self.frame = -1 - self.n_frames = len(self._offset) - self.is_animated = self.n_frames > 1 - self.seek(0) - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self.frame = frame - self.fp = self._fp - self.fp.seek(self._offset[frame]) - PcxImageFile._open(self) - - def tell(self) -> int: - return self.frame - - -Image.register_open(DcxImageFile.format, DcxImageFile, _accept) - -Image.register_extension(DcxImageFile.format, ".dcx") diff --git a/python_modules/PIL/DdsImagePlugin.py b/python_modules/PIL/DdsImagePlugin.py deleted file mode 100644 index f9ade18f9..000000000 --- a/python_modules/PIL/DdsImagePlugin.py +++ /dev/null @@ -1,624 +0,0 @@ -""" -A Pillow plugin for .dds files (S3TC-compressed aka DXTC) -Jerome Leclanche - -Documentation: -https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt - -The contents of this file are hereby released in the public domain (CC0) -Full text of the CC0 license: -https://creativecommons.org/publicdomain/zero/1.0/ -""" - -from __future__ import annotations - -import io -import struct -import sys -from enum import IntEnum, IntFlag -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i32le as i32 -from ._binary import o8 -from ._binary import o32le as o32 - -# Magic ("DDS ") -DDS_MAGIC = 0x20534444 - - -# DDS flags -class DDSD(IntFlag): - CAPS = 0x1 - HEIGHT = 0x2 - WIDTH = 0x4 - PITCH = 0x8 - PIXELFORMAT = 0x1000 - MIPMAPCOUNT = 0x20000 - LINEARSIZE = 0x80000 - DEPTH = 0x800000 - - -# DDS caps -class DDSCAPS(IntFlag): - COMPLEX = 0x8 - TEXTURE = 0x1000 - MIPMAP = 0x400000 - - -class DDSCAPS2(IntFlag): - CUBEMAP = 0x200 - CUBEMAP_POSITIVEX = 0x400 - CUBEMAP_NEGATIVEX = 0x800 - CUBEMAP_POSITIVEY = 0x1000 - CUBEMAP_NEGATIVEY = 0x2000 - CUBEMAP_POSITIVEZ = 0x4000 - CUBEMAP_NEGATIVEZ = 0x8000 - VOLUME = 0x200000 - - -# Pixel Format -class DDPF(IntFlag): - ALPHAPIXELS = 0x1 - ALPHA = 0x2 - FOURCC = 0x4 - PALETTEINDEXED8 = 0x20 - RGB = 0x40 - LUMINANCE = 0x20000 - - -# dxgiformat.h -class DXGI_FORMAT(IntEnum): - UNKNOWN = 0 - R32G32B32A32_TYPELESS = 1 - R32G32B32A32_FLOAT = 2 - R32G32B32A32_UINT = 3 - R32G32B32A32_SINT = 4 - R32G32B32_TYPELESS = 5 - R32G32B32_FLOAT = 6 - R32G32B32_UINT = 7 - R32G32B32_SINT = 8 - R16G16B16A16_TYPELESS = 9 - R16G16B16A16_FLOAT = 10 - R16G16B16A16_UNORM = 11 - R16G16B16A16_UINT = 12 - R16G16B16A16_SNORM = 13 - R16G16B16A16_SINT = 14 - R32G32_TYPELESS = 15 - R32G32_FLOAT = 16 - R32G32_UINT = 17 - R32G32_SINT = 18 - R32G8X24_TYPELESS = 19 - D32_FLOAT_S8X24_UINT = 20 - R32_FLOAT_X8X24_TYPELESS = 21 - X32_TYPELESS_G8X24_UINT = 22 - R10G10B10A2_TYPELESS = 23 - R10G10B10A2_UNORM = 24 - R10G10B10A2_UINT = 25 - R11G11B10_FLOAT = 26 - R8G8B8A8_TYPELESS = 27 - R8G8B8A8_UNORM = 28 - R8G8B8A8_UNORM_SRGB = 29 - R8G8B8A8_UINT = 30 - R8G8B8A8_SNORM = 31 - R8G8B8A8_SINT = 32 - R16G16_TYPELESS = 33 - R16G16_FLOAT = 34 - R16G16_UNORM = 35 - R16G16_UINT = 36 - R16G16_SNORM = 37 - R16G16_SINT = 38 - R32_TYPELESS = 39 - D32_FLOAT = 40 - R32_FLOAT = 41 - R32_UINT = 42 - R32_SINT = 43 - R24G8_TYPELESS = 44 - D24_UNORM_S8_UINT = 45 - R24_UNORM_X8_TYPELESS = 46 - X24_TYPELESS_G8_UINT = 47 - R8G8_TYPELESS = 48 - R8G8_UNORM = 49 - R8G8_UINT = 50 - R8G8_SNORM = 51 - R8G8_SINT = 52 - R16_TYPELESS = 53 - R16_FLOAT = 54 - D16_UNORM = 55 - R16_UNORM = 56 - R16_UINT = 57 - R16_SNORM = 58 - R16_SINT = 59 - R8_TYPELESS = 60 - R8_UNORM = 61 - R8_UINT = 62 - R8_SNORM = 63 - R8_SINT = 64 - A8_UNORM = 65 - R1_UNORM = 66 - R9G9B9E5_SHAREDEXP = 67 - R8G8_B8G8_UNORM = 68 - G8R8_G8B8_UNORM = 69 - BC1_TYPELESS = 70 - BC1_UNORM = 71 - BC1_UNORM_SRGB = 72 - BC2_TYPELESS = 73 - BC2_UNORM = 74 - BC2_UNORM_SRGB = 75 - BC3_TYPELESS = 76 - BC3_UNORM = 77 - BC3_UNORM_SRGB = 78 - BC4_TYPELESS = 79 - BC4_UNORM = 80 - BC4_SNORM = 81 - BC5_TYPELESS = 82 - BC5_UNORM = 83 - BC5_SNORM = 84 - B5G6R5_UNORM = 85 - B5G5R5A1_UNORM = 86 - B8G8R8A8_UNORM = 87 - B8G8R8X8_UNORM = 88 - R10G10B10_XR_BIAS_A2_UNORM = 89 - B8G8R8A8_TYPELESS = 90 - B8G8R8A8_UNORM_SRGB = 91 - B8G8R8X8_TYPELESS = 92 - B8G8R8X8_UNORM_SRGB = 93 - BC6H_TYPELESS = 94 - BC6H_UF16 = 95 - BC6H_SF16 = 96 - BC7_TYPELESS = 97 - BC7_UNORM = 98 - BC7_UNORM_SRGB = 99 - AYUV = 100 - Y410 = 101 - Y416 = 102 - NV12 = 103 - P010 = 104 - P016 = 105 - OPAQUE_420 = 106 - YUY2 = 107 - Y210 = 108 - Y216 = 109 - NV11 = 110 - AI44 = 111 - IA44 = 112 - P8 = 113 - A8P8 = 114 - B4G4R4A4_UNORM = 115 - P208 = 130 - V208 = 131 - V408 = 132 - SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189 - SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190 - - -class D3DFMT(IntEnum): - UNKNOWN = 0 - R8G8B8 = 20 - A8R8G8B8 = 21 - X8R8G8B8 = 22 - R5G6B5 = 23 - X1R5G5B5 = 24 - A1R5G5B5 = 25 - A4R4G4B4 = 26 - R3G3B2 = 27 - A8 = 28 - A8R3G3B2 = 29 - X4R4G4B4 = 30 - A2B10G10R10 = 31 - A8B8G8R8 = 32 - X8B8G8R8 = 33 - G16R16 = 34 - A2R10G10B10 = 35 - A16B16G16R16 = 36 - A8P8 = 40 - P8 = 41 - L8 = 50 - A8L8 = 51 - A4L4 = 52 - V8U8 = 60 - L6V5U5 = 61 - X8L8V8U8 = 62 - Q8W8V8U8 = 63 - V16U16 = 64 - A2W10V10U10 = 67 - D16_LOCKABLE = 70 - D32 = 71 - D15S1 = 73 - D24S8 = 75 - D24X8 = 77 - D24X4S4 = 79 - D16 = 80 - D32F_LOCKABLE = 82 - D24FS8 = 83 - D32_LOCKABLE = 84 - S8_LOCKABLE = 85 - L16 = 81 - VERTEXDATA = 100 - INDEX16 = 101 - INDEX32 = 102 - Q16W16V16U16 = 110 - R16F = 111 - G16R16F = 112 - A16B16G16R16F = 113 - R32F = 114 - G32R32F = 115 - A32B32G32R32F = 116 - CxV8U8 = 117 - A1 = 118 - A2B10G10R10_XR_BIAS = 119 - BINARYBUFFER = 199 - - UYVY = i32(b"UYVY") - R8G8_B8G8 = i32(b"RGBG") - YUY2 = i32(b"YUY2") - G8R8_G8B8 = i32(b"GRGB") - DXT1 = i32(b"DXT1") - DXT2 = i32(b"DXT2") - DXT3 = i32(b"DXT3") - DXT4 = i32(b"DXT4") - DXT5 = i32(b"DXT5") - DX10 = i32(b"DX10") - BC4S = i32(b"BC4S") - BC4U = i32(b"BC4U") - BC5S = i32(b"BC5S") - BC5U = i32(b"BC5U") - ATI1 = i32(b"ATI1") - ATI2 = i32(b"ATI2") - MULTI2_ARGB8 = i32(b"MET1") - - -# Backward compatibility layer -module = sys.modules[__name__] -for item in DDSD: - assert item.name is not None - setattr(module, f"DDSD_{item.name}", item.value) -for item1 in DDSCAPS: - assert item1.name is not None - setattr(module, f"DDSCAPS_{item1.name}", item1.value) -for item2 in DDSCAPS2: - assert item2.name is not None - setattr(module, f"DDSCAPS2_{item2.name}", item2.value) -for item3 in DDPF: - assert item3.name is not None - setattr(module, f"DDPF_{item3.name}", item3.value) - -DDS_FOURCC = DDPF.FOURCC -DDS_RGB = DDPF.RGB -DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS -DDS_LUMINANCE = DDPF.LUMINANCE -DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS -DDS_ALPHA = DDPF.ALPHA -DDS_PAL8 = DDPF.PALETTEINDEXED8 - -DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT -DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT -DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH -DDS_HEADER_FLAGS_PITCH = DDSD.PITCH -DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE - -DDS_HEIGHT = DDSD.HEIGHT -DDS_WIDTH = DDSD.WIDTH - -DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE -DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP -DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX - -DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX -DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX -DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY -DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY -DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ -DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ - -DXT1_FOURCC = D3DFMT.DXT1 -DXT3_FOURCC = D3DFMT.DXT3 -DXT5_FOURCC = D3DFMT.DXT5 - -DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS -DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM -DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB -DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS -DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM -DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM -DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16 -DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16 -DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS -DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM -DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB - - -class DdsImageFile(ImageFile.ImageFile): - format = "DDS" - format_description = "DirectDraw Surface" - - def _open(self) -> None: - if not _accept(self.fp.read(4)): - msg = "not a DDS file" - raise SyntaxError(msg) - (header_size,) = struct.unpack(" None: - pass - - -class DdsRgbDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - bitcount, masks = self.args - - # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100 - # Calculate how many zeros each mask is padded with - mask_offsets = [] - # And the maximum value of each channel without the padding - mask_totals = [] - for mask in masks: - offset = 0 - if mask != 0: - while mask >> (offset + 1) << (offset + 1) == mask: - offset += 1 - mask_offsets.append(offset) - mask_totals.append(mask >> offset) - - data = bytearray() - bytecount = bitcount // 8 - dest_length = self.state.xsize * self.state.ysize * len(masks) - while len(data) < dest_length: - value = int.from_bytes(self.fd.read(bytecount), "little") - for i, mask in enumerate(masks): - masked_value = value & mask - # Remove the zero padding, and scale it to 8 bits - data += o8( - int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) - ) - self.set_as_raw(data) - return -1, 0 - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode not in ("RGB", "RGBA", "L", "LA"): - msg = f"cannot write mode {im.mode} as DDS" - raise OSError(msg) - - flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT - bitcount = len(im.getbands()) * 8 - pixel_format = im.encoderinfo.get("pixel_format") - args: tuple[int] | str - if pixel_format: - codec_name = "bcn" - flags |= DDSD.LINEARSIZE - pitch = (im.width + 3) * 4 - rgba_mask = [0, 0, 0, 0] - pixel_flags = DDPF.FOURCC - if pixel_format == "DXT1": - fourcc = D3DFMT.DXT1 - args = (1,) - elif pixel_format == "DXT3": - fourcc = D3DFMT.DXT3 - args = (2,) - elif pixel_format == "DXT5": - fourcc = D3DFMT.DXT5 - args = (3,) - else: - fourcc = D3DFMT.DX10 - if pixel_format == "BC2": - args = (2,) - dxgi_format = DXGI_FORMAT.BC2_TYPELESS - elif pixel_format == "BC3": - args = (3,) - dxgi_format = DXGI_FORMAT.BC3_TYPELESS - elif pixel_format == "BC5": - args = (5,) - dxgi_format = DXGI_FORMAT.BC5_TYPELESS - if im.mode != "RGB": - msg = "only RGB mode can be written as BC5" - raise OSError(msg) - else: - msg = f"cannot write pixel format {pixel_format}" - raise OSError(msg) - else: - codec_name = "raw" - flags |= DDSD.PITCH - pitch = (im.width * bitcount + 7) // 8 - - alpha = im.mode[-1] == "A" - if im.mode[0] == "L": - pixel_flags = DDPF.LUMINANCE - args = im.mode - if alpha: - rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] - else: - rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] - else: - pixel_flags = DDPF.RGB - args = im.mode[::-1] - rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] - - if alpha: - r, g, b, a = im.split() - im = Image.merge("RGBA", (a, r, g, b)) - if alpha: - pixel_flags |= DDPF.ALPHAPIXELS - rgba_mask.append(0xFF000000 if alpha else 0) - - fourcc = D3DFMT.UNKNOWN - fp.write( - o32(DDS_MAGIC) - + struct.pack( - "<7I", - 124, # header size - flags, # flags - im.height, - im.width, - pitch, - 0, # depth - 0, # mipmaps - ) - + struct.pack("11I", *((0,) * 11)) # reserved - # pfsize, pfflags, fourcc, bitcount - + struct.pack("<4I", 32, pixel_flags, fourcc, bitcount) - + struct.pack("<4I", *rgba_mask) # dwRGBABitMask - + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) - ) - if fourcc == D3DFMT.DX10: - fp.write( - # dxgi_format, 2D resource, misc, array size, straight alpha - struct.pack("<5I", dxgi_format, 3, 0, 0, 1) - ) - ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)]) - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"DDS ") - - -Image.register_open(DdsImageFile.format, DdsImageFile, _accept) -Image.register_decoder("dds_rgb", DdsRgbDecoder) -Image.register_save(DdsImageFile.format, _save) -Image.register_extension(DdsImageFile.format, ".dds") diff --git a/python_modules/PIL/EpsImagePlugin.py b/python_modules/PIL/EpsImagePlugin.py deleted file mode 100644 index 5e2ddad99..000000000 --- a/python_modules/PIL/EpsImagePlugin.py +++ /dev/null @@ -1,476 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# EPS file handling -# -# History: -# 1995-09-01 fl Created (0.1) -# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2) -# 1996-08-22 fl Don't choke on floating point BoundingBox values -# 1996-08-23 fl Handle files from Macintosh (0.3) -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) -# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5) -# 2014-05-07 e Handling of EPS with binary preview and fixed resolution -# resizing -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import re -import subprocess -import sys -import tempfile -from typing import IO - -from . import Image, ImageFile -from ._binary import i32le as i32 - -# -------------------------------------------------------------------- - - -split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") -field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") - -gs_binary: str | bool | None = None -gs_windows_binary = None - - -def has_ghostscript() -> bool: - global gs_binary, gs_windows_binary - if gs_binary is None: - if sys.platform.startswith("win"): - if gs_windows_binary is None: - import shutil - - for binary in ("gswin32c", "gswin64c", "gs"): - if shutil.which(binary) is not None: - gs_windows_binary = binary - break - else: - gs_windows_binary = False - gs_binary = gs_windows_binary - else: - try: - subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) - gs_binary = "gs" - except OSError: - gs_binary = False - return gs_binary is not False - - -def Ghostscript( - tile: list[ImageFile._Tile], - size: tuple[int, int], - fp: IO[bytes], - scale: int = 1, - transparency: bool = False, -) -> Image.core.ImagingCore: - """Render an image using Ghostscript""" - global gs_binary - if not has_ghostscript(): - msg = "Unable to locate Ghostscript on paths" - raise OSError(msg) - assert isinstance(gs_binary, str) - - # Unpack decoder tile - args = tile[0].args - assert isinstance(args, tuple) - length, bbox = args - - # Hack to support hi-res rendering - scale = int(scale) or 1 - width = size[0] * scale - height = size[1] * scale - # resolution is dependent on bbox and size - res_x = 72.0 * width / (bbox[2] - bbox[0]) - res_y = 72.0 * height / (bbox[3] - bbox[1]) - - out_fd, outfile = tempfile.mkstemp() - os.close(out_fd) - - infile_temp = None - if hasattr(fp, "name") and os.path.exists(fp.name): - infile = fp.name - else: - in_fd, infile_temp = tempfile.mkstemp() - os.close(in_fd) - infile = infile_temp - - # Ignore length and offset! - # Ghostscript can read it - # Copy whole file to read in Ghostscript - with open(infile_temp, "wb") as f: - # fetch length of fp - fp.seek(0, io.SEEK_END) - fsize = fp.tell() - # ensure start position - # go back - fp.seek(0) - lengthfile = fsize - while lengthfile > 0: - s = fp.read(min(lengthfile, 100 * 1024)) - if not s: - break - lengthfile -= len(s) - f.write(s) - - if transparency: - # "RGBA" - device = "pngalpha" - else: - # "pnmraw" automatically chooses between - # PBM ("1"), PGM ("L"), and PPM ("RGB"). - device = "pnmraw" - - # Build Ghostscript command - command = [ - gs_binary, - "-q", # quiet mode - f"-g{width:d}x{height:d}", # set output geometry (pixels) - f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch) - "-dBATCH", # exit after processing - "-dNOPAUSE", # don't pause between pages - "-dSAFER", # safe mode - f"-sDEVICE={device}", - f"-sOutputFile={outfile}", # output file - # adjust for image origin - "-c", - f"{-bbox[0]} {-bbox[1]} translate", - "-f", - infile, # input file - # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272) - "-c", - "showpage", - ] - - # push data through Ghostscript - try: - startupinfo = None - if sys.platform.startswith("win"): - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - subprocess.check_call(command, startupinfo=startupinfo) - with Image.open(outfile) as out_im: - out_im.load() - return out_im.im.copy() - finally: - try: - os.unlink(outfile) - if infile_temp: - os.unlink(infile_temp) - except OSError: - pass - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"%!PS") or ( - len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5 - ) - - -## -# Image plugin for Encapsulated PostScript. This plugin supports only -# a few variants of this format. - - -class EpsImageFile(ImageFile.ImageFile): - """EPS File Parser for the Python Imaging Library""" - - format = "EPS" - format_description = "Encapsulated Postscript" - - mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} - - def _open(self) -> None: - (length, offset) = self._find_offset(self.fp) - - # go to offset - start of "%!PS" - self.fp.seek(offset) - - self._mode = "RGB" - - # When reading header comments, the first comment is used. - # When reading trailer comments, the last comment is used. - bounding_box: list[int] | None = None - imagedata_size: tuple[int, int] | None = None - - byte_arr = bytearray(255) - bytes_mv = memoryview(byte_arr) - bytes_read = 0 - reading_header_comments = True - reading_trailer_comments = False - trailer_reached = False - - def check_required_header_comments() -> None: - """ - The EPS specification requires that some headers exist. - This should be checked when the header comments formally end, - when image data starts, or when the file ends, whichever comes first. - """ - if "PS-Adobe" not in self.info: - msg = 'EPS header missing "%!PS-Adobe" comment' - raise SyntaxError(msg) - if "BoundingBox" not in self.info: - msg = 'EPS header missing "%%BoundingBox" comment' - raise SyntaxError(msg) - - def read_comment(s: str) -> bool: - nonlocal bounding_box, reading_trailer_comments - try: - m = split.match(s) - except re.error as e: - msg = "not an EPS file" - raise SyntaxError(msg) from e - - if not m: - return False - - k, v = m.group(1, 2) - self.info[k] = v - if k == "BoundingBox": - if v == "(atend)": - reading_trailer_comments = True - elif not bounding_box or (trailer_reached and reading_trailer_comments): - try: - # Note: The DSC spec says that BoundingBox - # fields should be integers, but some drivers - # put floating point values there anyway. - bounding_box = [int(float(i)) for i in v.split()] - except Exception: - pass - return True - - while True: - byte = self.fp.read(1) - if byte == b"": - # if we didn't read a byte we must be at the end of the file - if bytes_read == 0: - if reading_header_comments: - check_required_header_comments() - break - elif byte in b"\r\n": - # if we read a line ending character, ignore it and parse what - # we have already read. if we haven't read any other characters, - # continue reading - if bytes_read == 0: - continue - else: - # ASCII/hexadecimal lines in an EPS file must not exceed - # 255 characters, not including line ending characters - if bytes_read >= 255: - # only enforce this for lines starting with a "%", - # otherwise assume it's binary data - if byte_arr[0] == ord("%"): - msg = "not an EPS file" - raise SyntaxError(msg) - else: - if reading_header_comments: - check_required_header_comments() - reading_header_comments = False - # reset bytes_read so we can keep reading - # data until the end of the line - bytes_read = 0 - byte_arr[bytes_read] = byte[0] - bytes_read += 1 - continue - - if reading_header_comments: - # Load EPS header - - # if this line doesn't start with a "%", - # or does start with "%%EndComments", - # then we've reached the end of the header/comments - if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": - check_required_header_comments() - reading_header_comments = False - continue - - s = str(bytes_mv[:bytes_read], "latin-1") - if not read_comment(s): - m = field.match(s) - if m: - k = m.group(1) - if k.startswith("PS-Adobe"): - self.info["PS-Adobe"] = k[9:] - else: - self.info[k] = "" - elif s[0] == "%": - # handle non-DSC PostScript comments that some - # tools mistakenly put in the Comments section - pass - else: - msg = "bad EPS header" - raise OSError(msg) - elif bytes_mv[:11] == b"%ImageData:": - # Check for an "ImageData" descriptor - # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 - - # If we've already read an "ImageData" descriptor, - # don't read another one. - if imagedata_size: - bytes_read = 0 - continue - - # Values: - # columns - # rows - # bit depth (1 or 8) - # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) - # number of padding channels - # block size (number of bytes per row per channel) - # binary/ascii (1: binary, 2: ascii) - # data start identifier (the image data follows after a single line - # consisting only of this quoted value) - image_data_values = byte_arr[11:bytes_read].split(None, 7) - columns, rows, bit_depth, mode_id = ( - int(value) for value in image_data_values[:4] - ) - - if bit_depth == 1: - self._mode = "1" - elif bit_depth == 8: - try: - self._mode = self.mode_map[mode_id] - except ValueError: - break - else: - break - - # Parse the columns and rows after checking the bit depth and mode - # in case the bit depth and/or mode are invalid. - imagedata_size = columns, rows - elif bytes_mv[:5] == b"%%EOF": - break - elif trailer_reached and reading_trailer_comments: - # Load EPS trailer - s = str(bytes_mv[:bytes_read], "latin-1") - read_comment(s) - elif bytes_mv[:9] == b"%%Trailer": - trailer_reached = True - bytes_read = 0 - - # A "BoundingBox" is always required, - # even if an "ImageData" descriptor size exists. - if not bounding_box: - msg = "cannot determine EPS bounding box" - raise OSError(msg) - - # An "ImageData" size takes precedence over the "BoundingBox". - self._size = imagedata_size or ( - bounding_box[2] - bounding_box[0], - bounding_box[3] - bounding_box[1], - ) - - self.tile = [ - ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box)) - ] - - def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: - s = fp.read(4) - - if s == b"%!PS": - # for HEAD without binary preview - fp.seek(0, io.SEEK_END) - length = fp.tell() - offset = 0 - elif i32(s) == 0xC6D3D0C5: - # FIX for: Some EPS file not handled correctly / issue #302 - # EPS can contain binary data - # or start directly with latin coding - # more info see: - # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf - s = fp.read(8) - offset = i32(s) - length = i32(s, 4) - else: - msg = "not an EPS file" - raise SyntaxError(msg) - - return length, offset - - def load( - self, scale: int = 1, transparency: bool = False - ) -> Image.core.PixelAccess | None: - # Load EPS via Ghostscript - if self.tile: - self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) - self._mode = self.im.mode - self._size = self.im.size - self.tile = [] - return Image.Image.load(self) - - def load_seek(self, pos: int) -> None: - # we can't incrementally load, so force ImageFile.parser to - # use our custom load method by defining this method. - pass - - -# -------------------------------------------------------------------- - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None: - """EPS Writer for the Python Imaging Library.""" - - # make sure image data is available - im.load() - - # determine PostScript image mode - if im.mode == "L": - operator = (8, 1, b"image") - elif im.mode == "RGB": - operator = (8, 3, b"false 3 colorimage") - elif im.mode == "CMYK": - operator = (8, 4, b"false 4 colorimage") - else: - msg = "image mode is not supported" - raise ValueError(msg) - - if eps: - # write EPS header - fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") - fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") - # fp.write("%%CreationDate: %s"...) - fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size) - fp.write(b"%%Pages: 1\n") - fp.write(b"%%EndComments\n") - fp.write(b"%%Page: 1 1\n") - fp.write(b"%%ImageData: %d %d " % im.size) - fp.write(b'%d %d 0 1 1 "%s"\n' % operator) - - # image header - fp.write(b"gsave\n") - fp.write(b"10 dict begin\n") - fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1])) - fp.write(b"%d %d scale\n" % im.size) - fp.write(b"%d %d 8\n" % im.size) # <= bits - fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) - fp.write(b"{ currentfile buf readhexstring pop } bind\n") - fp.write(operator[2] + b"\n") - if hasattr(fp, "flush"): - fp.flush() - - ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)]) - - fp.write(b"\n%%%%EndBinary\n") - fp.write(b"grestore end\n") - if hasattr(fp, "flush"): - fp.flush() - - -# -------------------------------------------------------------------- - - -Image.register_open(EpsImageFile.format, EpsImageFile, _accept) - -Image.register_save(EpsImageFile.format, _save) - -Image.register_extensions(EpsImageFile.format, [".ps", ".eps"]) - -Image.register_mime(EpsImageFile.format, "application/postscript") diff --git a/python_modules/PIL/ExifTags.py b/python_modules/PIL/ExifTags.py deleted file mode 100644 index 2280d5ce8..000000000 --- a/python_modules/PIL/ExifTags.py +++ /dev/null @@ -1,382 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# EXIF tags -# -# Copyright (c) 2003 by Secret Labs AB -# -# See the README file for information on usage and redistribution. -# - -""" -This module provides constants and clear-text names for various -well-known EXIF tags. -""" -from __future__ import annotations - -from enum import IntEnum - - -class Base(IntEnum): - # possibly incomplete - InteropIndex = 0x0001 - ProcessingSoftware = 0x000B - NewSubfileType = 0x00FE - SubfileType = 0x00FF - ImageWidth = 0x0100 - ImageLength = 0x0101 - BitsPerSample = 0x0102 - Compression = 0x0103 - PhotometricInterpretation = 0x0106 - Thresholding = 0x0107 - CellWidth = 0x0108 - CellLength = 0x0109 - FillOrder = 0x010A - DocumentName = 0x010D - ImageDescription = 0x010E - Make = 0x010F - Model = 0x0110 - StripOffsets = 0x0111 - Orientation = 0x0112 - SamplesPerPixel = 0x0115 - RowsPerStrip = 0x0116 - StripByteCounts = 0x0117 - MinSampleValue = 0x0118 - MaxSampleValue = 0x0119 - XResolution = 0x011A - YResolution = 0x011B - PlanarConfiguration = 0x011C - PageName = 0x011D - FreeOffsets = 0x0120 - FreeByteCounts = 0x0121 - GrayResponseUnit = 0x0122 - GrayResponseCurve = 0x0123 - T4Options = 0x0124 - T6Options = 0x0125 - ResolutionUnit = 0x0128 - PageNumber = 0x0129 - TransferFunction = 0x012D - Software = 0x0131 - DateTime = 0x0132 - Artist = 0x013B - HostComputer = 0x013C - Predictor = 0x013D - WhitePoint = 0x013E - PrimaryChromaticities = 0x013F - ColorMap = 0x0140 - HalftoneHints = 0x0141 - TileWidth = 0x0142 - TileLength = 0x0143 - TileOffsets = 0x0144 - TileByteCounts = 0x0145 - SubIFDs = 0x014A - InkSet = 0x014C - InkNames = 0x014D - NumberOfInks = 0x014E - DotRange = 0x0150 - TargetPrinter = 0x0151 - ExtraSamples = 0x0152 - SampleFormat = 0x0153 - SMinSampleValue = 0x0154 - SMaxSampleValue = 0x0155 - TransferRange = 0x0156 - ClipPath = 0x0157 - XClipPathUnits = 0x0158 - YClipPathUnits = 0x0159 - Indexed = 0x015A - JPEGTables = 0x015B - OPIProxy = 0x015F - JPEGProc = 0x0200 - JpegIFOffset = 0x0201 - JpegIFByteCount = 0x0202 - JpegRestartInterval = 0x0203 - JpegLosslessPredictors = 0x0205 - JpegPointTransforms = 0x0206 - JpegQTables = 0x0207 - JpegDCTables = 0x0208 - JpegACTables = 0x0209 - YCbCrCoefficients = 0x0211 - YCbCrSubSampling = 0x0212 - YCbCrPositioning = 0x0213 - ReferenceBlackWhite = 0x0214 - XMLPacket = 0x02BC - RelatedImageFileFormat = 0x1000 - RelatedImageWidth = 0x1001 - RelatedImageLength = 0x1002 - Rating = 0x4746 - RatingPercent = 0x4749 - ImageID = 0x800D - CFARepeatPatternDim = 0x828D - BatteryLevel = 0x828F - Copyright = 0x8298 - ExposureTime = 0x829A - FNumber = 0x829D - IPTCNAA = 0x83BB - ImageResources = 0x8649 - ExifOffset = 0x8769 - InterColorProfile = 0x8773 - ExposureProgram = 0x8822 - SpectralSensitivity = 0x8824 - GPSInfo = 0x8825 - ISOSpeedRatings = 0x8827 - OECF = 0x8828 - Interlace = 0x8829 - TimeZoneOffset = 0x882A - SelfTimerMode = 0x882B - SensitivityType = 0x8830 - StandardOutputSensitivity = 0x8831 - RecommendedExposureIndex = 0x8832 - ISOSpeed = 0x8833 - ISOSpeedLatitudeyyy = 0x8834 - ISOSpeedLatitudezzz = 0x8835 - ExifVersion = 0x9000 - DateTimeOriginal = 0x9003 - DateTimeDigitized = 0x9004 - OffsetTime = 0x9010 - OffsetTimeOriginal = 0x9011 - OffsetTimeDigitized = 0x9012 - ComponentsConfiguration = 0x9101 - CompressedBitsPerPixel = 0x9102 - ShutterSpeedValue = 0x9201 - ApertureValue = 0x9202 - BrightnessValue = 0x9203 - ExposureBiasValue = 0x9204 - MaxApertureValue = 0x9205 - SubjectDistance = 0x9206 - MeteringMode = 0x9207 - LightSource = 0x9208 - Flash = 0x9209 - FocalLength = 0x920A - Noise = 0x920D - ImageNumber = 0x9211 - SecurityClassification = 0x9212 - ImageHistory = 0x9213 - TIFFEPStandardID = 0x9216 - MakerNote = 0x927C - UserComment = 0x9286 - SubsecTime = 0x9290 - SubsecTimeOriginal = 0x9291 - SubsecTimeDigitized = 0x9292 - AmbientTemperature = 0x9400 - Humidity = 0x9401 - Pressure = 0x9402 - WaterDepth = 0x9403 - Acceleration = 0x9404 - CameraElevationAngle = 0x9405 - XPTitle = 0x9C9B - XPComment = 0x9C9C - XPAuthor = 0x9C9D - XPKeywords = 0x9C9E - XPSubject = 0x9C9F - FlashPixVersion = 0xA000 - ColorSpace = 0xA001 - ExifImageWidth = 0xA002 - ExifImageHeight = 0xA003 - RelatedSoundFile = 0xA004 - ExifInteroperabilityOffset = 0xA005 - FlashEnergy = 0xA20B - SpatialFrequencyResponse = 0xA20C - FocalPlaneXResolution = 0xA20E - FocalPlaneYResolution = 0xA20F - FocalPlaneResolutionUnit = 0xA210 - SubjectLocation = 0xA214 - ExposureIndex = 0xA215 - SensingMethod = 0xA217 - FileSource = 0xA300 - SceneType = 0xA301 - CFAPattern = 0xA302 - CustomRendered = 0xA401 - ExposureMode = 0xA402 - WhiteBalance = 0xA403 - DigitalZoomRatio = 0xA404 - FocalLengthIn35mmFilm = 0xA405 - SceneCaptureType = 0xA406 - GainControl = 0xA407 - Contrast = 0xA408 - Saturation = 0xA409 - Sharpness = 0xA40A - DeviceSettingDescription = 0xA40B - SubjectDistanceRange = 0xA40C - ImageUniqueID = 0xA420 - CameraOwnerName = 0xA430 - BodySerialNumber = 0xA431 - LensSpecification = 0xA432 - LensMake = 0xA433 - LensModel = 0xA434 - LensSerialNumber = 0xA435 - CompositeImage = 0xA460 - CompositeImageCount = 0xA461 - CompositeImageExposureTimes = 0xA462 - Gamma = 0xA500 - PrintImageMatching = 0xC4A5 - DNGVersion = 0xC612 - DNGBackwardVersion = 0xC613 - UniqueCameraModel = 0xC614 - LocalizedCameraModel = 0xC615 - CFAPlaneColor = 0xC616 - CFALayout = 0xC617 - LinearizationTable = 0xC618 - BlackLevelRepeatDim = 0xC619 - BlackLevel = 0xC61A - BlackLevelDeltaH = 0xC61B - BlackLevelDeltaV = 0xC61C - WhiteLevel = 0xC61D - DefaultScale = 0xC61E - DefaultCropOrigin = 0xC61F - DefaultCropSize = 0xC620 - ColorMatrix1 = 0xC621 - ColorMatrix2 = 0xC622 - CameraCalibration1 = 0xC623 - CameraCalibration2 = 0xC624 - ReductionMatrix1 = 0xC625 - ReductionMatrix2 = 0xC626 - AnalogBalance = 0xC627 - AsShotNeutral = 0xC628 - AsShotWhiteXY = 0xC629 - BaselineExposure = 0xC62A - BaselineNoise = 0xC62B - BaselineSharpness = 0xC62C - BayerGreenSplit = 0xC62D - LinearResponseLimit = 0xC62E - CameraSerialNumber = 0xC62F - LensInfo = 0xC630 - ChromaBlurRadius = 0xC631 - AntiAliasStrength = 0xC632 - ShadowScale = 0xC633 - DNGPrivateData = 0xC634 - MakerNoteSafety = 0xC635 - CalibrationIlluminant1 = 0xC65A - CalibrationIlluminant2 = 0xC65B - BestQualityScale = 0xC65C - RawDataUniqueID = 0xC65D - OriginalRawFileName = 0xC68B - OriginalRawFileData = 0xC68C - ActiveArea = 0xC68D - MaskedAreas = 0xC68E - AsShotICCProfile = 0xC68F - AsShotPreProfileMatrix = 0xC690 - CurrentICCProfile = 0xC691 - CurrentPreProfileMatrix = 0xC692 - ColorimetricReference = 0xC6BF - CameraCalibrationSignature = 0xC6F3 - ProfileCalibrationSignature = 0xC6F4 - AsShotProfileName = 0xC6F6 - NoiseReductionApplied = 0xC6F7 - ProfileName = 0xC6F8 - ProfileHueSatMapDims = 0xC6F9 - ProfileHueSatMapData1 = 0xC6FA - ProfileHueSatMapData2 = 0xC6FB - ProfileToneCurve = 0xC6FC - ProfileEmbedPolicy = 0xC6FD - ProfileCopyright = 0xC6FE - ForwardMatrix1 = 0xC714 - ForwardMatrix2 = 0xC715 - PreviewApplicationName = 0xC716 - PreviewApplicationVersion = 0xC717 - PreviewSettingsName = 0xC718 - PreviewSettingsDigest = 0xC719 - PreviewColorSpace = 0xC71A - PreviewDateTime = 0xC71B - RawImageDigest = 0xC71C - OriginalRawFileDigest = 0xC71D - SubTileBlockSize = 0xC71E - RowInterleaveFactor = 0xC71F - ProfileLookTableDims = 0xC725 - ProfileLookTableData = 0xC726 - OpcodeList1 = 0xC740 - OpcodeList2 = 0xC741 - OpcodeList3 = 0xC74E - NoiseProfile = 0xC761 - - -"""Maps EXIF tags to tag names.""" -TAGS = { - **{i.value: i.name for i in Base}, - 0x920C: "SpatialFrequencyResponse", - 0x9214: "SubjectLocation", - 0x9215: "ExposureIndex", - 0x828E: "CFAPattern", - 0x920B: "FlashEnergy", - 0x9216: "TIFF/EPStandardID", -} - - -class GPS(IntEnum): - GPSVersionID = 0x00 - GPSLatitudeRef = 0x01 - GPSLatitude = 0x02 - GPSLongitudeRef = 0x03 - GPSLongitude = 0x04 - GPSAltitudeRef = 0x05 - GPSAltitude = 0x06 - GPSTimeStamp = 0x07 - GPSSatellites = 0x08 - GPSStatus = 0x09 - GPSMeasureMode = 0x0A - GPSDOP = 0x0B - GPSSpeedRef = 0x0C - GPSSpeed = 0x0D - GPSTrackRef = 0x0E - GPSTrack = 0x0F - GPSImgDirectionRef = 0x10 - GPSImgDirection = 0x11 - GPSMapDatum = 0x12 - GPSDestLatitudeRef = 0x13 - GPSDestLatitude = 0x14 - GPSDestLongitudeRef = 0x15 - GPSDestLongitude = 0x16 - GPSDestBearingRef = 0x17 - GPSDestBearing = 0x18 - GPSDestDistanceRef = 0x19 - GPSDestDistance = 0x1A - GPSProcessingMethod = 0x1B - GPSAreaInformation = 0x1C - GPSDateStamp = 0x1D - GPSDifferential = 0x1E - GPSHPositioningError = 0x1F - - -"""Maps EXIF GPS tags to tag names.""" -GPSTAGS = {i.value: i.name for i in GPS} - - -class Interop(IntEnum): - InteropIndex = 0x0001 - InteropVersion = 0x0002 - RelatedImageFileFormat = 0x1000 - RelatedImageWidth = 0x1001 - RelatedImageHeight = 0x1002 - - -class IFD(IntEnum): - Exif = 0x8769 - GPSInfo = 0x8825 - MakerNote = 0x927C - Makernote = 0x927C # Deprecated - Interop = 0xA005 - IFD1 = -1 - - -class LightSource(IntEnum): - Unknown = 0x00 - Daylight = 0x01 - Fluorescent = 0x02 - Tungsten = 0x03 - Flash = 0x04 - Fine = 0x09 - Cloudy = 0x0A - Shade = 0x0B - DaylightFluorescent = 0x0C - DayWhiteFluorescent = 0x0D - CoolWhiteFluorescent = 0x0E - WhiteFluorescent = 0x0F - StandardLightA = 0x11 - StandardLightB = 0x12 - StandardLightC = 0x13 - D55 = 0x14 - D65 = 0x15 - D75 = 0x16 - D50 = 0x17 - ISO = 0x18 - Other = 0xFF diff --git a/python_modules/PIL/FitsImagePlugin.py b/python_modules/PIL/FitsImagePlugin.py deleted file mode 100644 index a3fdc0efe..000000000 --- a/python_modules/PIL/FitsImagePlugin.py +++ /dev/null @@ -1,152 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# FITS file handling -# -# Copyright (c) 1998-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import gzip -import math - -from . import Image, ImageFile - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"SIMPLE") - - -class FitsImageFile(ImageFile.ImageFile): - format = "FITS" - format_description = "FITS" - - def _open(self) -> None: - assert self.fp is not None - - headers: dict[bytes, bytes] = {} - header_in_progress = False - decoder_name = "" - while True: - header = self.fp.read(80) - if not header: - msg = "Truncated FITS file" - raise OSError(msg) - keyword = header[:8].strip() - if keyword in (b"SIMPLE", b"XTENSION"): - header_in_progress = True - elif headers and not header_in_progress: - # This is now a data unit - break - elif keyword == b"END": - # Seek to the end of the header unit - self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880) - if not decoder_name: - decoder_name, offset, args = self._parse_headers(headers) - - header_in_progress = False - continue - - if decoder_name: - # Keep going to read past the headers - continue - - value = header[8:].split(b"/")[0].strip() - if value.startswith(b"="): - value = value[1:].strip() - if not headers and (not _accept(keyword) or value != b"T"): - msg = "Not a FITS file" - raise SyntaxError(msg) - headers[keyword] = value - - if not decoder_name: - msg = "No image data" - raise ValueError(msg) - - offset += self.fp.tell() - 80 - self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)] - - def _get_size( - self, headers: dict[bytes, bytes], prefix: bytes - ) -> tuple[int, int] | None: - naxis = int(headers[prefix + b"NAXIS"]) - if naxis == 0: - return None - - if naxis == 1: - return 1, int(headers[prefix + b"NAXIS1"]) - else: - return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"]) - - def _parse_headers( - self, headers: dict[bytes, bytes] - ) -> tuple[str, int, tuple[str | int, ...]]: - prefix = b"" - decoder_name = "raw" - offset = 0 - if ( - headers.get(b"XTENSION") == b"'BINTABLE'" - and headers.get(b"ZIMAGE") == b"T" - and headers[b"ZCMPTYPE"] == b"'GZIP_1 '" - ): - no_prefix_size = self._get_size(headers, prefix) or (0, 0) - number_of_bits = int(headers[b"BITPIX"]) - offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8) - - prefix = b"Z" - decoder_name = "fits_gzip" - - size = self._get_size(headers, prefix) - if not size: - return "", 0, () - - self._size = size - - number_of_bits = int(headers[prefix + b"BITPIX"]) - if number_of_bits == 8: - self._mode = "L" - elif number_of_bits == 16: - self._mode = "I;16" - elif number_of_bits == 32: - self._mode = "I" - elif number_of_bits in (-32, -64): - self._mode = "F" - - args: tuple[str | int, ...] - if decoder_name == "raw": - args = (self.mode, 0, -1) - else: - args = (number_of_bits,) - return decoder_name, offset, args - - -class FitsGzipDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - value = gzip.decompress(self.fd.read()) - - rows = [] - offset = 0 - number_of_bits = min(self.args[0] // 8, 4) - for y in range(self.state.ysize): - row = bytearray() - for x in range(self.state.xsize): - row += value[offset + (4 - number_of_bits) : offset + 4] - offset += 4 - rows.append(row) - self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row])) - return -1, 0 - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(FitsImageFile.format, FitsImageFile, _accept) -Image.register_decoder("fits_gzip", FitsGzipDecoder) - -Image.register_extensions(FitsImageFile.format, [".fit", ".fits"]) diff --git a/python_modules/PIL/FliImagePlugin.py b/python_modules/PIL/FliImagePlugin.py deleted file mode 100644 index 7c5bfeefa..000000000 --- a/python_modules/PIL/FliImagePlugin.py +++ /dev/null @@ -1,178 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# FLI/FLC file handling. -# -# History: -# 95-09-01 fl Created -# 97-01-03 fl Fixed parser, setup decoder tile -# 98-07-15 fl Renamed offset attribute to avoid name clash -# -# Copyright (c) Secret Labs AB 1997-98. -# Copyright (c) Fredrik Lundh 1995-97. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os - -from . import Image, ImageFile, ImagePalette -from ._binary import i16le as i16 -from ._binary import i32le as i32 -from ._binary import o8 -from ._util import DeferredError - -# -# decoder - - -def _accept(prefix: bytes) -> bool: - return ( - len(prefix) >= 6 - and i16(prefix, 4) in [0xAF11, 0xAF12] - and i16(prefix, 14) in [0, 3] # flags - ) - - -## -# Image plugin for the FLI/FLC animation format. Use the seek -# method to load individual frames. - - -class FliImageFile(ImageFile.ImageFile): - format = "FLI" - format_description = "Autodesk FLI/FLC Animation" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # HEAD - s = self.fp.read(128) - if not (_accept(s) and s[20:22] == b"\x00\x00"): - msg = "not an FLI/FLC file" - raise SyntaxError(msg) - - # frames - self.n_frames = i16(s, 6) - self.is_animated = self.n_frames > 1 - - # image characteristics - self._mode = "P" - self._size = i16(s, 8), i16(s, 10) - - # animation speed - duration = i32(s, 16) - magic = i16(s, 4) - if magic == 0xAF11: - duration = (duration * 1000) // 70 - self.info["duration"] = duration - - # look for palette - palette = [(a, a, a) for a in range(256)] - - s = self.fp.read(16) - - self.__offset = 128 - - if i16(s, 4) == 0xF100: - # prefix chunk; ignore it - self.__offset = self.__offset + i32(s) - self.fp.seek(self.__offset) - s = self.fp.read(16) - - if i16(s, 4) == 0xF1FA: - # look for palette chunk - number_of_subchunks = i16(s, 6) - chunk_size: int | None = None - for _ in range(number_of_subchunks): - if chunk_size is not None: - self.fp.seek(chunk_size - 6, os.SEEK_CUR) - s = self.fp.read(6) - chunk_type = i16(s, 4) - if chunk_type in (4, 11): - self._palette(palette, 2 if chunk_type == 11 else 0) - break - chunk_size = i32(s) - if not chunk_size: - break - - self.palette = ImagePalette.raw( - "RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette) - ) - - # set things up to decode first frame - self.__frame = -1 - self._fp = self.fp - self.__rewind = self.fp.tell() - self.seek(0) - - def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None: - # load palette - - i = 0 - for e in range(i16(self.fp.read(2))): - s = self.fp.read(2) - i = i + s[0] - n = s[1] - if n == 0: - n = 256 - s = self.fp.read(n * 3) - for n in range(0, len(s), 3): - r = s[n] << shift - g = s[n + 1] << shift - b = s[n + 2] << shift - palette[i] = (r, g, b) - i += 1 - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if frame < self.__frame: - self._seek(0) - - for f in range(self.__frame + 1, frame + 1): - self._seek(f) - - def _seek(self, frame: int) -> None: - if isinstance(self._fp, DeferredError): - raise self._fp.ex - if frame == 0: - self.__frame = -1 - self._fp.seek(self.__rewind) - self.__offset = 128 - else: - # ensure that the previous frame was loaded - self.load() - - if frame != self.__frame + 1: - msg = f"cannot seek to frame {frame}" - raise ValueError(msg) - self.__frame = frame - - # move to next frame - self.fp = self._fp - self.fp.seek(self.__offset) - - s = self.fp.read(4) - if not s: - msg = "missing frame size" - raise EOFError(msg) - - framesize = i32(s) - - self.decodermaxblock = framesize - self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)] - - self.__offset += framesize - - def tell(self) -> int: - return self.__frame - - -# -# registry - -Image.register_open(FliImageFile.format, FliImageFile, _accept) - -Image.register_extensions(FliImageFile.format, [".fli", ".flc"]) diff --git a/python_modules/PIL/FontFile.py b/python_modules/PIL/FontFile.py deleted file mode 100644 index 1e0c1c166..000000000 --- a/python_modules/PIL/FontFile.py +++ /dev/null @@ -1,134 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# base class for raster font file parsers -# -# history: -# 1997-06-05 fl created -# 1997-08-19 fl restrict image width -# -# Copyright (c) 1997-1998 by Secret Labs AB -# Copyright (c) 1997-1998 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import BinaryIO - -from . import Image, _binary - -WIDTH = 800 - - -def puti16( - fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int] -) -> None: - """Write network order (big-endian) 16-bit sequence""" - for v in values: - if v < 0: - v += 65536 - fp.write(_binary.o16be(v)) - - -class FontFile: - """Base class for raster font file handlers.""" - - bitmap: Image.Image | None = None - - def __init__(self) -> None: - self.info: dict[bytes, bytes | int] = {} - self.glyph: list[ - tuple[ - tuple[int, int], - tuple[int, int, int, int], - tuple[int, int, int, int], - Image.Image, - ] - | None - ] = [None] * 256 - - def __getitem__(self, ix: int) -> ( - tuple[ - tuple[int, int], - tuple[int, int, int, int], - tuple[int, int, int, int], - Image.Image, - ] - | None - ): - return self.glyph[ix] - - def compile(self) -> None: - """Create metrics and bitmap""" - - if self.bitmap: - return - - # create bitmap large enough to hold all data - h = w = maxwidth = 0 - lines = 1 - for glyph in self.glyph: - if glyph: - d, dst, src, im = glyph - h = max(h, src[3] - src[1]) - w = w + (src[2] - src[0]) - if w > WIDTH: - lines += 1 - w = src[2] - src[0] - maxwidth = max(maxwidth, w) - - xsize = maxwidth - ysize = lines * h - - if xsize == 0 and ysize == 0: - return - - self.ysize = h - - # paste glyphs into bitmap - self.bitmap = Image.new("1", (xsize, ysize)) - self.metrics: list[ - tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]] - | None - ] = [None] * 256 - x = y = 0 - for i in range(256): - glyph = self[i] - if glyph: - d, dst, src, im = glyph - xx = src[2] - src[0] - x0, y0 = x, y - x = x + xx - if x > WIDTH: - x, y = 0, y + h - x0, y0 = x, y - x = xx - s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0 - self.bitmap.paste(im.crop(src), s) - self.metrics[i] = d, dst, s - - def save(self, filename: str) -> None: - """Save font""" - - self.compile() - - # font data - if not self.bitmap: - msg = "No bitmap created" - raise ValueError(msg) - self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG") - - # font metrics - with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: - fp.write(b"PILfont\n") - fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! - fp.write(b"DATA\n") - for id in range(256): - m = self.metrics[id] - if not m: - puti16(fp, (0,) * 10) - else: - puti16(fp, m[0] + m[1] + m[2]) diff --git a/python_modules/PIL/FpxImagePlugin.py b/python_modules/PIL/FpxImagePlugin.py deleted file mode 100644 index fd992cd9e..000000000 --- a/python_modules/PIL/FpxImagePlugin.py +++ /dev/null @@ -1,257 +0,0 @@ -# -# THIS IS WORK IN PROGRESS -# -# The Python Imaging Library. -# $Id$ -# -# FlashPix support for PIL -# -# History: -# 97-01-25 fl Created (reads uncompressed RGB images only) -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import olefile - -from . import Image, ImageFile -from ._binary import i32le as i32 - -# we map from colour field tuples to (mode, rawmode) descriptors -MODES = { - # opacity - (0x00007FFE,): ("A", "L"), - # monochrome - (0x00010000,): ("L", "L"), - (0x00018000, 0x00017FFE): ("RGBA", "LA"), - # photo YCC - (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"), - (0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"), - # standard RGB (NIFRGB) - (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"), - (0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"), -} - - -# -# -------------------------------------------------------------------- - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(olefile.MAGIC) - - -## -# Image plugin for the FlashPix images. - - -class FpxImageFile(ImageFile.ImageFile): - format = "FPX" - format_description = "FlashPix" - - def _open(self) -> None: - # - # read the OLE directory and see if this is a likely - # to be a FlashPix file - - try: - self.ole = olefile.OleFileIO(self.fp) - except OSError as e: - msg = "not an FPX file; invalid OLE file" - raise SyntaxError(msg) from e - - root = self.ole.root - if not root or root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": - msg = "not an FPX file; bad root CLSID" - raise SyntaxError(msg) - - self._open_index(1) - - def _open_index(self, index: int = 1) -> None: - # - # get the Image Contents Property Set - - prop = self.ole.getproperties( - [f"Data Object Store {index:06d}", "\005Image Contents"] - ) - - # size (highest resolution) - - assert isinstance(prop[0x1000002], int) - assert isinstance(prop[0x1000003], int) - self._size = prop[0x1000002], prop[0x1000003] - - size = max(self.size) - i = 1 - while size > 64: - size = size // 2 - i += 1 - self.maxid = i - 1 - - # mode. instead of using a single field for this, flashpix - # requires you to specify the mode for each channel in each - # resolution subimage, and leaves it to the decoder to make - # sure that they all match. for now, we'll cheat and assume - # that this is always the case. - - id = self.maxid << 16 - - s = prop[0x2000002 | id] - - if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4: - msg = "Invalid number of bands" - raise OSError(msg) - - # note: for now, we ignore the "uncalibrated" flag - colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands)) - - self._mode, self.rawmode = MODES[colors] - - # load JPEG tables, if any - self.jpeg = {} - for i in range(256): - id = 0x3000001 | (i << 16) - if id in prop: - self.jpeg[i] = prop[id] - - self._open_subimage(1, self.maxid) - - def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: - # - # setup tile descriptors for a given subimage - - stream = [ - f"Data Object Store {index:06d}", - f"Resolution {subimage:04d}", - "Subimage 0000 Header", - ] - - fp = self.ole.openstream(stream) - - # skip prefix - fp.read(28) - - # header stream - s = fp.read(36) - - size = i32(s, 4), i32(s, 8) - # tilecount = i32(s, 12) - tilesize = i32(s, 16), i32(s, 20) - # channels = i32(s, 24) - offset = i32(s, 28) - length = i32(s, 32) - - if size != self.size: - msg = "subimage mismatch" - raise OSError(msg) - - # get tile descriptors - fp.seek(28 + offset) - s = fp.read(i32(s, 12) * length) - - x = y = 0 - xsize, ysize = size - xtile, ytile = tilesize - self.tile = [] - - for i in range(0, len(s), length): - x1 = min(xsize, x + xtile) - y1 = min(ysize, y + ytile) - - compression = i32(s, i + 8) - - if compression == 0: - self.tile.append( - ImageFile._Tile( - "raw", - (x, y, x1, y1), - i32(s, i) + 28, - self.rawmode, - ) - ) - - elif compression == 1: - # FIXME: the fill decoder is not implemented - self.tile.append( - ImageFile._Tile( - "fill", - (x, y, x1, y1), - i32(s, i) + 28, - (self.rawmode, s[12:16]), - ) - ) - - elif compression == 2: - internal_color_conversion = s[14] - jpeg_tables = s[15] - rawmode = self.rawmode - - if internal_color_conversion: - # The image is stored as usual (usually YCbCr). - if rawmode == "RGBA": - # For "RGBA", data is stored as YCbCrA based on - # negative RGB. The following trick works around - # this problem : - jpegmode, rawmode = "YCbCrK", "CMYK" - else: - jpegmode = None # let the decoder decide - - else: - # The image is stored as defined by rawmode - jpegmode = rawmode - - self.tile.append( - ImageFile._Tile( - "jpeg", - (x, y, x1, y1), - i32(s, i) + 28, - (rawmode, jpegmode), - ) - ) - - # FIXME: jpeg tables are tile dependent; the prefix - # data must be placed in the tile descriptor itself! - - if jpeg_tables: - self.tile_prefix = self.jpeg[jpeg_tables] - - else: - msg = "unknown/invalid compression" - raise OSError(msg) - - x = x + xtile - if x >= xsize: - x, y = 0, y + ytile - if y >= ysize: - break # isn't really required - - self.stream = stream - self._fp = self.fp - self.fp = None - - def load(self) -> Image.core.PixelAccess | None: - if not self.fp: - self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) - - return ImageFile.ImageFile.load(self) - - def close(self) -> None: - self.ole.close() - super().close() - - def __exit__(self, *args: object) -> None: - self.ole.close() - super().__exit__() - - -# -# -------------------------------------------------------------------- - - -Image.register_open(FpxImageFile.format, FpxImageFile, _accept) - -Image.register_extension(FpxImageFile.format, ".fpx") diff --git a/python_modules/PIL/FtexImagePlugin.py b/python_modules/PIL/FtexImagePlugin.py deleted file mode 100644 index d60e75bb6..000000000 --- a/python_modules/PIL/FtexImagePlugin.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -A Pillow loader for .ftc and .ftu files (FTEX) -Jerome Leclanche - -The contents of this file are hereby released in the public domain (CC0) -Full text of the CC0 license: - https://creativecommons.org/publicdomain/zero/1.0/ - -Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001 - -The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a -packed custom format called FTEX. This file format uses file extensions FTC -and FTU. -* FTC files are compressed textures (using standard texture compression). -* FTU files are not compressed. -Texture File Format -The FTC and FTU texture files both use the same format. This -has the following structure: -{header} -{format_directory} -{data} -Where: -{header} = { - u32:magic, - u32:version, - u32:width, - u32:height, - u32:mipmap_count, - u32:format_count -} - -* The "magic" number is "FTEX". -* "width" and "height" are the dimensions of the texture. -* "mipmap_count" is the number of mipmaps in the texture. -* "format_count" is the number of texture formats (different versions of the -same texture) in this file. - -{format_directory} = format_count * { u32:format, u32:where } - -The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB -uncompressed textures. -The texture data for a format starts at the position "where" in the file. - -Each set of texture data in the file has the following structure: -{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } } -* "mipmap_size" is the number of bytes in that mip level. For compressed -textures this is the size of the texture data compressed with DXT1. For 24 bit -uncompressed textures, this is 3 * width * height. Following this are the image -bytes for that mipmap level. - -Note: All data is stored in little-Endian (Intel) byte order. -""" - -from __future__ import annotations - -import struct -from enum import IntEnum -from io import BytesIO - -from . import Image, ImageFile - -MAGIC = b"FTEX" - - -class Format(IntEnum): - DXT1 = 0 - UNCOMPRESSED = 1 - - -class FtexImageFile(ImageFile.ImageFile): - format = "FTEX" - format_description = "Texture File Format (IW2:EOC)" - - def _open(self) -> None: - if not _accept(self.fp.read(4)): - msg = "not an FTEX file" - raise SyntaxError(msg) - struct.unpack(" None: - pass - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(MAGIC) - - -Image.register_open(FtexImageFile.format, FtexImageFile, _accept) -Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"]) diff --git a/python_modules/PIL/GbrImagePlugin.py b/python_modules/PIL/GbrImagePlugin.py deleted file mode 100644 index f319d7e84..000000000 --- a/python_modules/PIL/GbrImagePlugin.py +++ /dev/null @@ -1,103 +0,0 @@ -# -# The Python Imaging Library -# -# load a GIMP brush file -# -# History: -# 96-03-14 fl Created -# 16-01-08 es Version 2 -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# Copyright (c) Eric Soroos 2016. -# -# See the README file for information on usage and redistribution. -# -# -# See https://github.com/GNOME/gimp/blob/mainline/devel-docs/gbr.txt for -# format documentation. -# -# This code Interprets version 1 and 2 .gbr files. -# Version 1 files are obsolete, and should not be used for new -# brushes. -# Version 2 files are saved by GIMP v2.8 (at least) -# Version 3 files have a format specifier of 18 for 16bit floats in -# the color depth field. This is currently unsupported by Pillow. -from __future__ import annotations - -from . import Image, ImageFile -from ._binary import i32be as i32 - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2) - - -## -# Image plugin for the GIMP brush format. - - -class GbrImageFile(ImageFile.ImageFile): - format = "GBR" - format_description = "GIMP brush file" - - def _open(self) -> None: - header_size = i32(self.fp.read(4)) - if header_size < 20: - msg = "not a GIMP brush" - raise SyntaxError(msg) - version = i32(self.fp.read(4)) - if version not in (1, 2): - msg = f"Unsupported GIMP brush version: {version}" - raise SyntaxError(msg) - - width = i32(self.fp.read(4)) - height = i32(self.fp.read(4)) - color_depth = i32(self.fp.read(4)) - if width <= 0 or height <= 0: - msg = "not a GIMP brush" - raise SyntaxError(msg) - if color_depth not in (1, 4): - msg = f"Unsupported GIMP brush color depth: {color_depth}" - raise SyntaxError(msg) - - if version == 1: - comment_length = header_size - 20 - else: - comment_length = header_size - 28 - magic_number = self.fp.read(4) - if magic_number != b"GIMP": - msg = "not a GIMP brush, bad magic number" - raise SyntaxError(msg) - self.info["spacing"] = i32(self.fp.read(4)) - - comment = self.fp.read(comment_length)[:-1] - - if color_depth == 1: - self._mode = "L" - else: - self._mode = "RGBA" - - self._size = width, height - - self.info["comment"] = comment - - # Image might not be small - Image._decompression_bomb_check(self.size) - - # Data is an uncompressed block of w * h * bytes/pixel - self._data_size = width * height * color_depth - - def load(self) -> Image.core.PixelAccess | None: - if self._im is None: - self.im = Image.core.new(self.mode, self.size) - self.frombytes(self.fp.read(self._data_size)) - return Image.Image.load(self) - - -# -# registry - - -Image.register_open(GbrImageFile.format, GbrImageFile, _accept) -Image.register_extension(GbrImageFile.format, ".gbr") diff --git a/python_modules/PIL/GdImageFile.py b/python_modules/PIL/GdImageFile.py deleted file mode 100644 index 891225ce2..000000000 --- a/python_modules/PIL/GdImageFile.py +++ /dev/null @@ -1,102 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# GD file handling -# -# History: -# 1996-04-12 fl Created -# -# Copyright (c) 1997 by Secret Labs AB. -# Copyright (c) 1996 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - - -""" -.. note:: - This format cannot be automatically recognized, so the - class is not registered for use with :py:func:`PIL.Image.open()`. To open a - gd file, use the :py:func:`PIL.GdImageFile.open()` function instead. - -.. warning:: - THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This - implementation is provided for convenience and demonstrational - purposes only. -""" -from __future__ import annotations - -from typing import IO - -from . import ImageFile, ImagePalette, UnidentifiedImageError -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._typing import StrOrBytesPath - - -class GdImageFile(ImageFile.ImageFile): - """ - Image plugin for the GD uncompressed format. Note that this format - is not supported by the standard :py:func:`PIL.Image.open()` function. To use - this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and - use the :py:func:`PIL.GdImageFile.open()` function. - """ - - format = "GD" - format_description = "GD uncompressed images" - - def _open(self) -> None: - # Header - assert self.fp is not None - - s = self.fp.read(1037) - - if i16(s) not in [65534, 65535]: - msg = "Not a valid GD 2.x .gd file" - raise SyntaxError(msg) - - self._mode = "P" - self._size = i16(s, 2), i16(s, 4) - - true_color = s[6] - true_color_offset = 2 if true_color else 0 - - # transparency index - tindex = i32(s, 7 + true_color_offset) - if tindex < 256: - self.info["transparency"] = tindex - - self.palette = ImagePalette.raw( - "RGBX", s[7 + true_color_offset + 6 : 7 + true_color_offset + 6 + 256 * 4] - ) - - self.tile = [ - ImageFile._Tile( - "raw", - (0, 0) + self.size, - 7 + true_color_offset + 6 + 256 * 4, - "L", - ) - ] - - -def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile: - """ - Load texture from a GD image file. - - :param fp: GD file name, or an opened file handle. - :param mode: Optional mode. In this version, if the mode argument - is given, it must be "r". - :returns: An image instance. - :raises OSError: If the image could not be read. - """ - if mode != "r": - msg = "bad mode" - raise ValueError(msg) - - try: - return GdImageFile(fp) - except SyntaxError as e: - msg = "cannot identify this image file" - raise UnidentifiedImageError(msg) from e diff --git a/python_modules/PIL/GifImagePlugin.py b/python_modules/PIL/GifImagePlugin.py deleted file mode 100644 index b03aa7f15..000000000 --- a/python_modules/PIL/GifImagePlugin.py +++ /dev/null @@ -1,1213 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# GIF file handling -# -# History: -# 1995-09-01 fl Created -# 1996-12-14 fl Added interlace support -# 1996-12-30 fl Added animation support -# 1997-01-05 fl Added write support, fixed local colour map bug -# 1997-02-23 fl Make sure to load raster data in getdata() -# 1997-07-05 fl Support external decoder (0.4) -# 1998-07-09 fl Handle all modes when saving (0.5) -# 1998-07-15 fl Renamed offset attribute to avoid name clash -# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6) -# 2001-04-17 fl Added palette optimization (0.7) -# 2002-06-06 fl Added transparency support for save (0.8) -# 2004-02-24 fl Disable interlacing for small images -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1995-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import itertools -import math -import os -import subprocess -from enum import IntEnum -from functools import cached_property -from typing import IO, Any, Literal, NamedTuple, Union, cast - -from . import ( - Image, - ImageChops, - ImageFile, - ImageMath, - ImageOps, - ImagePalette, - ImageSequence, -) -from ._binary import i16le as i16 -from ._binary import o8 -from ._binary import o16le as o16 -from ._util import DeferredError - -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import _imaging - from ._typing import Buffer - - -class LoadingStrategy(IntEnum): - """.. versionadded:: 9.1.0""" - - RGB_AFTER_FIRST = 0 - RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 - RGB_ALWAYS = 2 - - -#: .. versionadded:: 9.1.0 -LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST - -# -------------------------------------------------------------------- -# Identify/read GIF files - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith((b"GIF87a", b"GIF89a")) - - -## -# Image plugin for GIF images. This plugin supports both GIF87 and -# GIF89 images. - - -class GifImageFile(ImageFile.ImageFile): - format = "GIF" - format_description = "Compuserve GIF" - _close_exclusive_fp_after_loading = False - - global_palette = None - - def data(self) -> bytes | None: - s = self.fp.read(1) - if s and s[0]: - return self.fp.read(s[0]) - return None - - def _is_palette_needed(self, p: bytes) -> bool: - for i in range(0, len(p), 3): - if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): - return True - return False - - def _open(self) -> None: - # Screen - s = self.fp.read(13) - if not _accept(s): - msg = "not a GIF file" - raise SyntaxError(msg) - - self.info["version"] = s[:6] - self._size = i16(s, 6), i16(s, 8) - flags = s[10] - bits = (flags & 7) + 1 - - if flags & 128: - # get global palette - self.info["background"] = s[11] - # check if palette contains colour indices - p = self.fp.read(3 << bits) - if self._is_palette_needed(p): - p = ImagePalette.raw("RGB", p) - self.global_palette = self.palette = p - - self._fp = self.fp # FIXME: hack - self.__rewind = self.fp.tell() - self._n_frames: int | None = None - self._seek(0) # get ready to read first frame - - @property - def n_frames(self) -> int: - if self._n_frames is None: - current = self.tell() - try: - while True: - self._seek(self.tell() + 1, False) - except EOFError: - self._n_frames = self.tell() + 1 - self.seek(current) - return self._n_frames - - @cached_property - def is_animated(self) -> bool: - if self._n_frames is not None: - return self._n_frames != 1 - - current = self.tell() - if current: - return True - - try: - self._seek(1, False) - is_animated = True - except EOFError: - is_animated = False - - self.seek(current) - return is_animated - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if frame < self.__frame: - self._im = None - self._seek(0) - - last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: - self._seek(f) - except EOFError as e: - self.seek(last_frame) - msg = "no more images in GIF file" - raise EOFError(msg) from e - - def _seek(self, frame: int, update_image: bool = True) -> None: - if isinstance(self._fp, DeferredError): - raise self._fp.ex - if frame == 0: - # rewind - self.__offset = 0 - self.dispose: _imaging.ImagingCore | None = None - self.__frame = -1 - self._fp.seek(self.__rewind) - self.disposal_method = 0 - if "comment" in self.info: - del self.info["comment"] - else: - # ensure that the previous frame was loaded - if self.tile and update_image: - self.load() - - if frame != self.__frame + 1: - msg = f"cannot seek to frame {frame}" - raise ValueError(msg) - - self.fp = self._fp - if self.__offset: - # backup to last frame - self.fp.seek(self.__offset) - while self.data(): - pass - self.__offset = 0 - - s = self.fp.read(1) - if not s or s == b";": - msg = "no more images in GIF file" - raise EOFError(msg) - - palette: ImagePalette.ImagePalette | Literal[False] | None = None - - info: dict[str, Any] = {} - frame_transparency = None - interlace = None - frame_dispose_extent = None - while True: - if not s: - s = self.fp.read(1) - if not s or s == b";": - break - - elif s == b"!": - # - # extensions - # - s = self.fp.read(1) - block = self.data() - if s[0] == 249 and block is not None: - # - # graphic control extension - # - flags = block[0] - if flags & 1: - frame_transparency = block[3] - info["duration"] = i16(block, 1) * 10 - - # disposal method - find the value of bits 4 - 6 - dispose_bits = 0b00011100 & flags - dispose_bits = dispose_bits >> 2 - if dispose_bits: - # only set the dispose if it is not - # unspecified. I'm not sure if this is - # correct, but it seems to prevent the last - # frame from looking odd for some animations - self.disposal_method = dispose_bits - elif s[0] == 254: - # - # comment extension - # - comment = b"" - - # Read this comment block - while block: - comment += block - block = self.data() - - if "comment" in info: - # If multiple comment blocks in frame, separate with \n - info["comment"] += b"\n" + comment - else: - info["comment"] = comment - s = None - continue - elif s[0] == 255 and frame == 0 and block is not None: - # - # application extension - # - info["extension"] = block, self.fp.tell() - if block.startswith(b"NETSCAPE2.0"): - block = self.data() - if block and len(block) >= 3 and block[0] == 1: - self.info["loop"] = i16(block, 1) - while self.data(): - pass - - elif s == b",": - # - # local image - # - s = self.fp.read(9) - - # extent - x0, y0 = i16(s, 0), i16(s, 2) - x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6) - if (x1 > self.size[0] or y1 > self.size[1]) and update_image: - self._size = max(x1, self.size[0]), max(y1, self.size[1]) - Image._decompression_bomb_check(self._size) - frame_dispose_extent = x0, y0, x1, y1 - flags = s[8] - - interlace = (flags & 64) != 0 - - if flags & 128: - bits = (flags & 7) + 1 - p = self.fp.read(3 << bits) - if self._is_palette_needed(p): - palette = ImagePalette.raw("RGB", p) - else: - palette = False - - # image data - bits = self.fp.read(1)[0] - self.__offset = self.fp.tell() - break - s = None - - if interlace is None: - msg = "image not found in GIF frame" - raise EOFError(msg) - - self.__frame = frame - if not update_image: - return - - self.tile = [] - - if self.dispose: - self.im.paste(self.dispose, self.dispose_extent) - - self._frame_palette = palette if palette is not None else self.global_palette - self._frame_transparency = frame_transparency - if frame == 0: - if self._frame_palette: - if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: - self._mode = "RGBA" if frame_transparency is not None else "RGB" - else: - self._mode = "P" - else: - self._mode = "L" - - if palette: - self.palette = palette - elif self.global_palette: - from copy import copy - - self.palette = copy(self.global_palette) - else: - self.palette = None - else: - if self.mode == "P": - if ( - LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY - or palette - ): - if "transparency" in self.info: - self.im.putpalettealpha(self.info["transparency"], 0) - self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) - self._mode = "RGBA" - del self.info["transparency"] - else: - self._mode = "RGB" - self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) - - def _rgb(color: int) -> tuple[int, int, int]: - if self._frame_palette: - if color * 3 + 3 > len(self._frame_palette.palette): - color = 0 - return cast( - tuple[int, int, int], - tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]), - ) - else: - return (color, color, color) - - self.dispose = None - self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent - if self.dispose_extent and self.disposal_method >= 2: - try: - if self.disposal_method == 2: - # replace with background colour - - # only dispose the extent in this frame - x0, y0, x1, y1 = self.dispose_extent - dispose_size = (x1 - x0, y1 - y0) - - Image._decompression_bomb_check(dispose_size) - - # by convention, attempt to use transparency first - dispose_mode = "P" - color = self.info.get("transparency", frame_transparency) - if color is not None: - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGBA" - color = _rgb(color) + (0,) - else: - color = self.info.get("background", 0) - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGB" - color = _rgb(color) - self.dispose = Image.core.fill(dispose_mode, dispose_size, color) - else: - # replace with previous contents - if self._im is not None: - # only dispose the extent in this frame - self.dispose = self._crop(self.im, self.dispose_extent) - elif frame_transparency is not None: - x0, y0, x1, y1 = self.dispose_extent - dispose_size = (x1 - x0, y1 - y0) - - Image._decompression_bomb_check(dispose_size) - dispose_mode = "P" - color = frame_transparency - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGBA" - color = _rgb(frame_transparency) + (0,) - self.dispose = Image.core.fill( - dispose_mode, dispose_size, color - ) - except AttributeError: - pass - - if interlace is not None: - transparency = -1 - if frame_transparency is not None: - if frame == 0: - if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: - self.info["transparency"] = frame_transparency - elif self.mode not in ("RGB", "RGBA"): - transparency = frame_transparency - self.tile = [ - ImageFile._Tile( - "gif", - (x0, y0, x1, y1), - self.__offset, - (bits, interlace, transparency), - ) - ] - - if info.get("comment"): - self.info["comment"] = info["comment"] - for k in ["duration", "extension"]: - if k in info: - self.info[k] = info[k] - elif k in self.info: - del self.info[k] - - def load_prepare(self) -> None: - temp_mode = "P" if self._frame_palette else "L" - self._prev_im = None - if self.__frame == 0: - if self._frame_transparency is not None: - self.im = Image.core.fill( - temp_mode, self.size, self._frame_transparency - ) - elif self.mode in ("RGB", "RGBA"): - self._prev_im = self.im - if self._frame_palette: - self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) - self.im.putpalette("RGB", *self._frame_palette.getdata()) - else: - self._im = None - if not self._prev_im and self._im is not None and self.size != self.im.size: - expanded_im = Image.core.fill(self.im.mode, self.size) - if self._frame_palette: - expanded_im.putpalette("RGB", *self._frame_palette.getdata()) - expanded_im.paste(self.im, (0, 0) + self.im.size) - - self.im = expanded_im - self._mode = temp_mode - self._frame_palette = None - - super().load_prepare() - - def load_end(self) -> None: - if self.__frame == 0: - if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: - if self._frame_transparency is not None: - self.im.putpalettealpha(self._frame_transparency, 0) - self._mode = "RGBA" - else: - self._mode = "RGB" - self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) - return - if not self._prev_im: - return - if self.size != self._prev_im.size: - if self._frame_transparency is not None: - expanded_im = Image.core.fill("RGBA", self.size) - else: - expanded_im = Image.core.fill("P", self.size) - expanded_im.putpalette("RGB", "RGB", self.im.getpalette()) - expanded_im = expanded_im.convert("RGB") - expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size) - - self._prev_im = expanded_im - assert self._prev_im is not None - if self._frame_transparency is not None: - if self.mode == "L": - frame_im = self.im.convert_transparent("LA", self._frame_transparency) - else: - self.im.putpalettealpha(self._frame_transparency, 0) - frame_im = self.im.convert("RGBA") - else: - frame_im = self.im.convert("RGB") - - assert self.dispose_extent is not None - frame_im = self._crop(frame_im, self.dispose_extent) - - self.im = self._prev_im - self._mode = self.im.mode - if frame_im.mode in ("LA", "RGBA"): - self.im.paste(frame_im, self.dispose_extent, frame_im) - else: - self.im.paste(frame_im, self.dispose_extent) - - def tell(self) -> int: - return self.__frame - - -# -------------------------------------------------------------------- -# Write GIF files - - -RAWMODE = {"1": "L", "L": "L", "P": "P"} - - -def _normalize_mode(im: Image.Image) -> Image.Image: - """ - Takes an image (or frame), returns an image in a mode that is appropriate - for saving in a Gif. - - It may return the original image, or it may return an image converted to - palette or 'L' mode. - - :param im: Image object - :returns: Image object - """ - if im.mode in RAWMODE: - im.load() - return im - if Image.getmodebase(im.mode) == "RGB": - im = im.convert("P", palette=Image.Palette.ADAPTIVE) - assert im.palette is not None - if im.palette.mode == "RGBA": - for rgba in im.palette.colors: - if rgba[3] == 0: - im.info["transparency"] = im.palette.colors[rgba] - break - return im - return im.convert("L") - - -_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette] - - -def _normalize_palette( - im: Image.Image, palette: _Palette | None, info: dict[str, Any] -) -> Image.Image: - """ - Normalizes the palette for image. - - Sets the palette to the incoming palette, if provided. - - Ensures that there's a palette for L mode images - - Optimizes the palette if necessary/desired. - - :param im: Image object - :param palette: bytes object containing the source palette, or .... - :param info: encoderinfo - :returns: Image object - """ - source_palette = None - if palette: - # a bytes palette - if isinstance(palette, (bytes, bytearray, list)): - source_palette = bytearray(palette[:768]) - if isinstance(palette, ImagePalette.ImagePalette): - source_palette = bytearray(palette.palette) - - if im.mode == "P": - if not source_palette: - im_palette = im.getpalette(None) - assert im_palette is not None - source_palette = bytearray(im_palette) - else: # L-mode - if not source_palette: - source_palette = bytearray(i // 3 for i in range(768)) - im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) - assert source_palette is not None - - if palette: - used_palette_colors: list[int | None] = [] - assert im.palette is not None - for i in range(0, len(source_palette), 3): - source_color = tuple(source_palette[i : i + 3]) - index = im.palette.colors.get(source_color) - if index in used_palette_colors: - index = None - used_palette_colors.append(index) - for i, index in enumerate(used_palette_colors): - if index is None: - for j in range(len(used_palette_colors)): - if j not in used_palette_colors: - used_palette_colors[i] = j - break - dest_map: list[int] = [] - for index in used_palette_colors: - assert index is not None - dest_map.append(index) - im = im.remap_palette(dest_map) - else: - optimized_palette_colors = _get_optimize(im, info) - if optimized_palette_colors is not None: - im = im.remap_palette(optimized_palette_colors, source_palette) - if "transparency" in info: - try: - info["transparency"] = optimized_palette_colors.index( - info["transparency"] - ) - except ValueError: - del info["transparency"] - return im - - assert im.palette is not None - im.palette.palette = source_palette - return im - - -def _write_single_frame( - im: Image.Image, - fp: IO[bytes], - palette: _Palette | None, -) -> None: - im_out = _normalize_mode(im) - for k, v in im_out.info.items(): - if isinstance(k, str): - im.encoderinfo.setdefault(k, v) - im_out = _normalize_palette(im_out, palette, im.encoderinfo) - - for s in _get_global_header(im_out, im.encoderinfo): - fp.write(s) - - # local image header - flags = 0 - if get_interlace(im): - flags = flags | 64 - _write_local_header(fp, im, (0, 0), flags) - - im_out.encoderconfig = (8, get_interlace(im)) - ImageFile._save( - im_out, fp, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])] - ) - - fp.write(b"\0") # end of image data - - -def _getbbox( - base_im: Image.Image, im_frame: Image.Image -) -> tuple[Image.Image, tuple[int, int, int, int] | None]: - palette_bytes = [ - bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame) - ] - if palette_bytes[0] != palette_bytes[1]: - im_frame = im_frame.convert("RGBA") - base_im = base_im.convert("RGBA") - delta = ImageChops.subtract_modulo(im_frame, base_im) - return delta, delta.getbbox(alpha_only=False) - - -class _Frame(NamedTuple): - im: Image.Image - bbox: tuple[int, int, int, int] | None - encoderinfo: dict[str, Any] - - -def _write_multiple_frames( - im: Image.Image, fp: IO[bytes], palette: _Palette | None -) -> bool: - duration = im.encoderinfo.get("duration") - disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) - - im_frames: list[_Frame] = [] - previous_im: Image.Image | None = None - frame_count = 0 - background_im = None - for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): - for im_frame in ImageSequence.Iterator(imSequence): - # a copy is required here since seek can still mutate the image - im_frame = _normalize_mode(im_frame.copy()) - if frame_count == 0: - for k, v in im_frame.info.items(): - if k == "transparency": - continue - if isinstance(k, str): - im.encoderinfo.setdefault(k, v) - - encoderinfo = im.encoderinfo.copy() - if "transparency" in im_frame.info: - encoderinfo.setdefault("transparency", im_frame.info["transparency"]) - im_frame = _normalize_palette(im_frame, palette, encoderinfo) - if isinstance(duration, (list, tuple)): - encoderinfo["duration"] = duration[frame_count] - elif duration is None and "duration" in im_frame.info: - encoderinfo["duration"] = im_frame.info["duration"] - if isinstance(disposal, (list, tuple)): - encoderinfo["disposal"] = disposal[frame_count] - frame_count += 1 - - diff_frame = None - if im_frames and previous_im: - # delta frame - delta, bbox = _getbbox(previous_im, im_frame) - if not bbox: - # This frame is identical to the previous frame - if encoderinfo.get("duration"): - im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] - continue - if im_frames[-1].encoderinfo.get("disposal") == 2: - # To appear correctly in viewers using a convention, - # only consider transparency, and not background color - color = im.encoderinfo.get( - "transparency", im.info.get("transparency") - ) - if color is not None: - if background_im is None: - background = _get_background(im_frame, color) - background_im = Image.new("P", im_frame.size, background) - first_palette = im_frames[0].im.palette - assert first_palette is not None - background_im.putpalette(first_palette, first_palette.mode) - bbox = _getbbox(background_im, im_frame)[1] - else: - bbox = (0, 0) + im_frame.size - elif encoderinfo.get("optimize") and im_frame.mode != "1": - if "transparency" not in encoderinfo: - assert im_frame.palette is not None - try: - encoderinfo["transparency"] = ( - im_frame.palette._new_color_index(im_frame) - ) - except ValueError: - pass - if "transparency" in encoderinfo: - # When the delta is zero, fill the image with transparency - diff_frame = im_frame.copy() - fill = Image.new("P", delta.size, encoderinfo["transparency"]) - if delta.mode == "RGBA": - r, g, b, a = delta.split() - mask = ImageMath.lambda_eval( - lambda args: args["convert"]( - args["max"]( - args["max"]( - args["max"](args["r"], args["g"]), args["b"] - ), - args["a"], - ) - * 255, - "1", - ), - r=r, - g=g, - b=b, - a=a, - ) - else: - if delta.mode == "P": - # Convert to L without considering palette - delta_l = Image.new("L", delta.size) - delta_l.putdata(delta.getdata()) - delta = delta_l - mask = ImageMath.lambda_eval( - lambda args: args["convert"](args["im"] * 255, "1"), - im=delta, - ) - diff_frame.paste(fill, mask=ImageOps.invert(mask)) - else: - bbox = None - previous_im = im_frame - im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo)) - - if len(im_frames) == 1: - if "duration" in im.encoderinfo: - # Since multiple frames will not be written, use the combined duration - im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"] - return False - - for frame_data in im_frames: - im_frame = frame_data.im - if not frame_data.bbox: - # global header - for s in _get_global_header(im_frame, frame_data.encoderinfo): - fp.write(s) - offset = (0, 0) - else: - # compress difference - if not palette: - frame_data.encoderinfo["include_color_table"] = True - - if frame_data.bbox != (0, 0) + im_frame.size: - im_frame = im_frame.crop(frame_data.bbox) - offset = frame_data.bbox[:2] - _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo) - return True - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, save_all=True) - - -def _save( - im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False -) -> None: - # header - if "palette" in im.encoderinfo or "palette" in im.info: - palette = im.encoderinfo.get("palette", im.info.get("palette")) - else: - palette = None - im.encoderinfo.setdefault("optimize", True) - - if not save_all or not _write_multiple_frames(im, fp, palette): - _write_single_frame(im, fp, palette) - - fp.write(b";") # end of file - - if hasattr(fp, "flush"): - fp.flush() - - -def get_interlace(im: Image.Image) -> int: - interlace = im.encoderinfo.get("interlace", 1) - - # workaround for @PIL153 - if min(im.size) < 16: - interlace = 0 - - return interlace - - -def _write_local_header( - fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int -) -> None: - try: - transparency = im.encoderinfo["transparency"] - except KeyError: - transparency = None - - if "duration" in im.encoderinfo: - duration = int(im.encoderinfo["duration"] / 10) - else: - duration = 0 - - disposal = int(im.encoderinfo.get("disposal", 0)) - - if transparency is not None or duration != 0 or disposal: - packed_flag = 1 if transparency is not None else 0 - packed_flag |= disposal << 2 - - fp.write( - b"!" - + o8(249) # extension intro - + o8(4) # length - + o8(packed_flag) # packed fields - + o16(duration) # duration - + o8(transparency or 0) # transparency index - + o8(0) - ) - - include_color_table = im.encoderinfo.get("include_color_table") - if include_color_table: - palette_bytes = _get_palette_bytes(im) - color_table_size = _get_color_table_size(palette_bytes) - if color_table_size: - flags = flags | 128 # local color table flag - flags = flags | color_table_size - - fp.write( - b"," - + o16(offset[0]) # offset - + o16(offset[1]) - + o16(im.size[0]) # size - + o16(im.size[1]) - + o8(flags) # flags - ) - if include_color_table and color_table_size: - fp.write(_get_header_palette(palette_bytes)) - fp.write(o8(8)) # bits - - -def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # Unused by default. - # To use, uncomment the register_save call at the end of the file. - # - # If you need real GIF compression and/or RGB quantization, you - # can use the external NETPBM/PBMPLUS utilities. See comments - # below for information on how to enable this. - tempfile = im._dump() - - try: - with open(filename, "wb") as f: - if im.mode != "RGB": - subprocess.check_call( - ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL - ) - else: - # Pipe ppmquant output into ppmtogif - # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) - quant_cmd = ["ppmquant", "256", tempfile] - togif_cmd = ["ppmtogif"] - quant_proc = subprocess.Popen( - quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL - ) - togif_proc = subprocess.Popen( - togif_cmd, - stdin=quant_proc.stdout, - stdout=f, - stderr=subprocess.DEVNULL, - ) - - # Allow ppmquant to receive SIGPIPE if ppmtogif exits - assert quant_proc.stdout is not None - quant_proc.stdout.close() - - retcode = quant_proc.wait() - if retcode: - raise subprocess.CalledProcessError(retcode, quant_cmd) - - retcode = togif_proc.wait() - if retcode: - raise subprocess.CalledProcessError(retcode, togif_cmd) - finally: - try: - os.unlink(tempfile) - except OSError: - pass - - -# Force optimization so that we can test performance against -# cases where it took lots of memory and time previously. -_FORCE_OPTIMIZE = False - - -def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: - """ - Palette optimization is a potentially expensive operation. - - This function determines if the palette should be optimized using - some heuristics, then returns the list of palette entries in use. - - :param im: Image object - :param info: encoderinfo - :returns: list of indexes of palette entries in use, or None - """ - if im.mode in ("P", "L") and info and info.get("optimize"): - # Potentially expensive operation. - - # The palette saves 3 bytes per color not used, but palette - # lengths are restricted to 3*(2**N) bytes. Max saving would - # be 768 -> 6 bytes if we went all the way down to 2 colors. - # * If we're over 128 colors, we can't save any space. - # * If there aren't any holes, it's not worth collapsing. - # * If we have a 'large' image, the palette is in the noise. - - # create the new palette if not every color is used - optimise = _FORCE_OPTIMIZE or im.mode == "L" - if optimise or im.width * im.height < 512 * 512: - # check which colors are used - used_palette_colors = [] - for i, count in enumerate(im.histogram()): - if count: - used_palette_colors.append(i) - - if optimise or max(used_palette_colors) >= len(used_palette_colors): - return used_palette_colors - - assert im.palette is not None - num_palette_colors = len(im.palette.palette) // Image.getmodebands( - im.palette.mode - ) - current_palette_size = 1 << (num_palette_colors - 1).bit_length() - if ( - # check that the palette would become smaller when saved - len(used_palette_colors) <= current_palette_size // 2 - # check that the palette is not already the smallest possible size - and current_palette_size > 2 - ): - return used_palette_colors - return None - - -def _get_color_table_size(palette_bytes: bytes) -> int: - # calculate the palette size for the header - if not palette_bytes: - return 0 - elif len(palette_bytes) < 9: - return 1 - else: - return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 - - -def _get_header_palette(palette_bytes: bytes) -> bytes: - """ - Returns the palette, null padded to the next power of 2 (*3) bytes - suitable for direct inclusion in the GIF header - - :param palette_bytes: Unpadded palette bytes, in RGBRGB form - :returns: Null padded palette - """ - color_table_size = _get_color_table_size(palette_bytes) - - # add the missing amount of bytes - # the palette has to be 2< 0: - palette_bytes += o8(0) * 3 * actual_target_size_diff - return palette_bytes - - -def _get_palette_bytes(im: Image.Image) -> bytes: - """ - Gets the palette for inclusion in the gif header - - :param im: Image object - :returns: Bytes, len<=768 suitable for inclusion in gif header - """ - if not im.palette: - return b"" - - palette = bytes(im.palette.palette) - if im.palette.mode == "RGBA": - palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3)) - return palette - - -def _get_background( - im: Image.Image, - info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None, -) -> int: - background = 0 - if info_background: - if isinstance(info_background, tuple): - # WebPImagePlugin stores an RGBA value in info["background"] - # So it must be converted to the same format as GifImagePlugin's - # info["background"] - a global color table index - assert im.palette is not None - try: - background = im.palette.getcolor(info_background, im) - except ValueError as e: - if str(e) not in ( - # If all 256 colors are in use, - # then there is no need for the background color - "cannot allocate more than 256 colors", - # Ignore non-opaque WebP background - "cannot add non-opaque RGBA color to RGB palette", - ): - raise - else: - background = info_background - return background - - -def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]: - """Return a list of strings representing a GIF header""" - - # Header Block - # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp - - version = b"87a" - if im.info.get("version") == b"89a" or ( - info - and ( - "transparency" in info - or info.get("loop") is not None - or info.get("duration") - or info.get("comment") - ) - ): - version = b"89a" - - background = _get_background(im, info.get("background")) - - palette_bytes = _get_palette_bytes(im) - color_table_size = _get_color_table_size(palette_bytes) - - header = [ - b"GIF" # signature - + version # version - + o16(im.size[0]) # canvas width - + o16(im.size[1]), # canvas height - # Logical Screen Descriptor - # size of global color table + global color table flag - o8(color_table_size + 128), # packed fields - # background + reserved/aspect - o8(background) + o8(0), - # Global Color Table - _get_header_palette(palette_bytes), - ] - if info.get("loop") is not None: - header.append( - b"!" - + o8(255) # extension intro - + o8(11) - + b"NETSCAPE2.0" - + o8(3) - + o8(1) - + o16(info["loop"]) # number of loops - + o8(0) - ) - if info.get("comment"): - comment_block = b"!" + o8(254) # extension intro - - comment = info["comment"] - if isinstance(comment, str): - comment = comment.encode() - for i in range(0, len(comment), 255): - subblock = comment[i : i + 255] - comment_block += o8(len(subblock)) + subblock - - comment_block += o8(0) - header.append(comment_block) - return header - - -def _write_frame_data( - fp: IO[bytes], - im_frame: Image.Image, - offset: tuple[int, int], - params: dict[str, Any], -) -> None: - try: - im_frame.encoderinfo = params - - # local image header - _write_local_header(fp, im_frame, offset, 0) - - ImageFile._save( - im_frame, - fp, - [ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])], - ) - - fp.write(b"\0") # end of image data - finally: - del im_frame.encoderinfo - - -# -------------------------------------------------------------------- -# Legacy GIF utilities - - -def getheader( - im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None -) -> tuple[list[bytes], list[int] | None]: - """ - Legacy Method to get Gif data from image. - - Warning:: May modify image data. - - :param im: Image object - :param palette: bytes object containing the source palette, or .... - :param info: encoderinfo - :returns: tuple of(list of header items, optimized palette) - - """ - if info is None: - info = {} - - used_palette_colors = _get_optimize(im, info) - - if "background" not in info and "background" in im.info: - info["background"] = im.info["background"] - - im_mod = _normalize_palette(im, palette, info) - im.palette = im_mod.palette - im.im = im_mod.im - header = _get_global_header(im, info) - - return header, used_palette_colors - - -def getdata( - im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any -) -> list[bytes]: - """ - Legacy Method - - Return a list of strings representing this image. - The first string is a local image header, the rest contains - encoded image data. - - To specify duration, add the time in milliseconds, - e.g. ``getdata(im_frame, duration=1000)`` - - :param im: Image object - :param offset: Tuple of (x, y) pixels. Defaults to (0, 0) - :param \\**params: e.g. duration or other encoder info parameters - :returns: List of bytes containing GIF encoded frame data - - """ - from io import BytesIO - - class Collector(BytesIO): - data = [] - - def write(self, data: Buffer) -> int: - self.data.append(data) - return len(data) - - im.load() # make sure raster data is available - - fp = Collector() - - _write_frame_data(fp, im, offset, params) - - return fp.data - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(GifImageFile.format, GifImageFile, _accept) -Image.register_save(GifImageFile.format, _save) -Image.register_save_all(GifImageFile.format, _save_all) -Image.register_extension(GifImageFile.format, ".gif") -Image.register_mime(GifImageFile.format, "image/gif") - -# -# Uncomment the following line if you wish to use NETPBM/PBMPLUS -# instead of the built-in "uncompressed" GIF encoder - -# Image.register_save(GifImageFile.format, _save_netpbm) diff --git a/python_modules/PIL/GimpGradientFile.py b/python_modules/PIL/GimpGradientFile.py deleted file mode 100644 index ec62f8e4e..000000000 --- a/python_modules/PIL/GimpGradientFile.py +++ /dev/null @@ -1,149 +0,0 @@ -# -# Python Imaging Library -# $Id$ -# -# stuff to read (and render) GIMP gradient files -# -# History: -# 97-08-23 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# - -""" -Stuff to translate curve segments to palette values (derived from -the corresponding code in GIMP, written by Federico Mena Quintero. -See the GIMP distribution for more information.) -""" -from __future__ import annotations - -from math import log, pi, sin, sqrt -from typing import IO, Callable - -from ._binary import o8 - -EPSILON = 1e-10 -"""""" # Enable auto-doc for data member - - -def linear(middle: float, pos: float) -> float: - if pos <= middle: - if middle < EPSILON: - return 0.0 - else: - return 0.5 * pos / middle - else: - pos = pos - middle - middle = 1.0 - middle - if middle < EPSILON: - return 1.0 - else: - return 0.5 + 0.5 * pos / middle - - -def curved(middle: float, pos: float) -> float: - return pos ** (log(0.5) / log(max(middle, EPSILON))) - - -def sine(middle: float, pos: float) -> float: - return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 - - -def sphere_increasing(middle: float, pos: float) -> float: - return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) - - -def sphere_decreasing(middle: float, pos: float) -> float: - return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) - - -SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] -"""""" # Enable auto-doc for data member - - -class GradientFile: - gradient: ( - list[ - tuple[ - float, - float, - float, - list[float], - list[float], - Callable[[float, float], float], - ] - ] - | None - ) = None - - def getpalette(self, entries: int = 256) -> tuple[bytes, str]: - assert self.gradient is not None - palette = [] - - ix = 0 - x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] - - for i in range(entries): - x = i / (entries - 1) - - while x1 < x: - ix += 1 - x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] - - w = x1 - x0 - - if w < EPSILON: - scale = segment(0.5, 0.5) - else: - scale = segment((xm - x0) / w, (x - x0) / w) - - # expand to RGBA - r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5)) - g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5)) - b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5)) - a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5)) - - # add to palette - palette.append(r + g + b + a) - - return b"".join(palette), "RGBA" - - -class GimpGradientFile(GradientFile): - """File handler for GIMP's gradient format.""" - - def __init__(self, fp: IO[bytes]) -> None: - if not fp.readline().startswith(b"GIMP Gradient"): - msg = "not a GIMP gradient file" - raise SyntaxError(msg) - - line = fp.readline() - - # GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do - if line.startswith(b"Name: "): - line = fp.readline().strip() - - count = int(line) - - self.gradient = [] - - for i in range(count): - s = fp.readline().split() - w = [float(x) for x in s[:11]] - - x0, x1 = w[0], w[2] - xm = w[1] - rgb0 = w[3:7] - rgb1 = w[7:11] - - segment = SEGMENTS[int(s[11])] - cspace = int(s[12]) - - if cspace != 0: - msg = "cannot handle HSV colour space" - raise OSError(msg) - - self.gradient.append((x0, x1, xm, rgb0, rgb1, segment)) diff --git a/python_modules/PIL/GimpPaletteFile.py b/python_modules/PIL/GimpPaletteFile.py deleted file mode 100644 index 379ffd739..000000000 --- a/python_modules/PIL/GimpPaletteFile.py +++ /dev/null @@ -1,72 +0,0 @@ -# -# Python Imaging Library -# $Id$ -# -# stuff to read GIMP palette files -# -# History: -# 1997-08-23 fl Created -# 2004-09-07 fl Support GIMP 2.0 palette files. -# -# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. -# Copyright (c) Fredrik Lundh 1997-2004. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re -from io import BytesIO -from typing import IO - - -class GimpPaletteFile: - """File handler for GIMP's palette format.""" - - rawmode = "RGB" - - def _read(self, fp: IO[bytes], limit: bool = True) -> None: - if not fp.readline().startswith(b"GIMP Palette"): - msg = "not a GIMP palette file" - raise SyntaxError(msg) - - palette: list[int] = [] - i = 0 - while True: - if limit and i == 256 + 3: - break - - i += 1 - s = fp.readline() - if not s: - break - - # skip fields and comment lines - if re.match(rb"\w+:|#", s): - continue - if limit and len(s) > 100: - msg = "bad palette file" - raise SyntaxError(msg) - - v = s.split(maxsplit=3) - if len(v) < 3: - msg = "bad palette entry" - raise ValueError(msg) - - palette += (int(v[i]) for i in range(3)) - if limit and len(palette) == 768: - break - - self.palette = bytes(palette) - - def __init__(self, fp: IO[bytes]) -> None: - self._read(fp) - - @classmethod - def frombytes(cls, data: bytes) -> GimpPaletteFile: - self = cls.__new__(cls) - self._read(BytesIO(data), False) - return self - - def getpalette(self) -> tuple[bytes, str]: - return self.palette, self.rawmode diff --git a/python_modules/PIL/GribStubImagePlugin.py b/python_modules/PIL/GribStubImagePlugin.py deleted file mode 100644 index 439fc5a3e..000000000 --- a/python_modules/PIL/GribStubImagePlugin.py +++ /dev/null @@ -1,75 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# GRIB stub adapter -# -# Copyright (c) 1996-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import IO - -from . import Image, ImageFile - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific GRIB image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -# -------------------------------------------------------------------- -# Image adapter - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"GRIB") and prefix[7] == 1 - - -class GribStubImageFile(ImageFile.StubImageFile): - format = "GRIB" - format_description = "GRIB" - - def _open(self) -> None: - if not _accept(self.fp.read(8)): - msg = "Not a GRIB file" - raise SyntaxError(msg) - - self.fp.seek(-8, os.SEEK_CUR) - - # make something up - self._mode = "F" - self._size = 1, 1 - - loader = self._load() - if loader: - loader.open(self) - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "GRIB save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept) -Image.register_save(GribStubImageFile.format, _save) - -Image.register_extension(GribStubImageFile.format, ".grib") diff --git a/python_modules/PIL/Hdf5StubImagePlugin.py b/python_modules/PIL/Hdf5StubImagePlugin.py deleted file mode 100644 index 76e640f15..000000000 --- a/python_modules/PIL/Hdf5StubImagePlugin.py +++ /dev/null @@ -1,75 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# HDF5 stub adapter -# -# Copyright (c) 2000-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import IO - -from . import Image, ImageFile - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific HDF5 image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -# -------------------------------------------------------------------- -# Image adapter - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"\x89HDF\r\n\x1a\n") - - -class HDF5StubImageFile(ImageFile.StubImageFile): - format = "HDF5" - format_description = "HDF5" - - def _open(self) -> None: - if not _accept(self.fp.read(8)): - msg = "Not an HDF file" - raise SyntaxError(msg) - - self.fp.seek(-8, os.SEEK_CUR) - - # make something up - self._mode = "F" - self._size = 1, 1 - - loader = self._load() - if loader: - loader.open(self) - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "HDF5 save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept) -Image.register_save(HDF5StubImageFile.format, _save) - -Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"]) diff --git a/python_modules/PIL/IcnsImagePlugin.py b/python_modules/PIL/IcnsImagePlugin.py deleted file mode 100644 index 5a88429e5..000000000 --- a/python_modules/PIL/IcnsImagePlugin.py +++ /dev/null @@ -1,411 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# macOS icns file decoder, based on icns.py by Bob Ippolito. -# -# history: -# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies. -# 2020-04-04 Allow saving on all operating systems. -# -# Copyright (c) 2004 by Bob Ippolito. -# Copyright (c) 2004 by Secret Labs. -# Copyright (c) 2004 by Fredrik Lundh. -# Copyright (c) 2014 by Alastair Houghton. -# Copyright (c) 2020 by Pan Jing. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import struct -import sys -from typing import IO - -from . import Image, ImageFile, PngImagePlugin, features -from ._deprecate import deprecate - -enable_jpeg2k = features.check_codec("jpg_2000") -if enable_jpeg2k: - from . import Jpeg2KImagePlugin - -MAGIC = b"icns" -HEADERSIZE = 8 - - -def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]: - return struct.unpack(">4sI", fobj.read(HEADERSIZE)) - - -def read_32t( - fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] -) -> dict[str, Image.Image]: - # The 128x128 icon seems to have an extra header for some reason. - (start, length) = start_length - fobj.seek(start) - sig = fobj.read(4) - if sig != b"\x00\x00\x00\x00": - msg = "Unknown signature, expecting 0x00000000" - raise SyntaxError(msg) - return read_32(fobj, (start + 4, length - 4), size) - - -def read_32( - fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] -) -> dict[str, Image.Image]: - """ - Read a 32bit RGB icon resource. Seems to be either uncompressed or - an RLE packbits-like scheme. - """ - (start, length) = start_length - fobj.seek(start) - pixel_size = (size[0] * size[2], size[1] * size[2]) - sizesq = pixel_size[0] * pixel_size[1] - if length == sizesq * 3: - # uncompressed ("RGBRGBGB") - indata = fobj.read(length) - im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1) - else: - # decode image - im = Image.new("RGB", pixel_size, None) - for band_ix in range(3): - data = [] - bytesleft = sizesq - while bytesleft > 0: - byte = fobj.read(1) - if not byte: - break - byte_int = byte[0] - if byte_int & 0x80: - blocksize = byte_int - 125 - byte = fobj.read(1) - for i in range(blocksize): - data.append(byte) - else: - blocksize = byte_int + 1 - data.append(fobj.read(blocksize)) - bytesleft -= blocksize - if bytesleft <= 0: - break - if bytesleft != 0: - msg = f"Error reading channel [{repr(bytesleft)} left]" - raise SyntaxError(msg) - band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) - im.im.putband(band.im, band_ix) - return {"RGB": im} - - -def read_mk( - fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] -) -> dict[str, Image.Image]: - # Alpha masks seem to be uncompressed - start = start_length[0] - fobj.seek(start) - pixel_size = (size[0] * size[2], size[1] * size[2]) - sizesq = pixel_size[0] * pixel_size[1] - band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1) - return {"A": band} - - -def read_png_or_jpeg2000( - fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] -) -> dict[str, Image.Image]: - (start, length) = start_length - fobj.seek(start) - sig = fobj.read(12) - - im: Image.Image - if sig.startswith(b"\x89PNG\x0d\x0a\x1a\x0a"): - fobj.seek(start) - im = PngImagePlugin.PngImageFile(fobj) - Image._decompression_bomb_check(im.size) - return {"RGBA": im} - elif ( - sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a")) - or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" - ): - if not enable_jpeg2k: - msg = ( - "Unsupported icon subimage format (rebuild PIL " - "with JPEG 2000 support to fix this)" - ) - raise ValueError(msg) - # j2k, jpc or j2c - fobj.seek(start) - jp2kstream = fobj.read(length) - f = io.BytesIO(jp2kstream) - im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) - Image._decompression_bomb_check(im.size) - if im.mode != "RGBA": - im = im.convert("RGBA") - return {"RGBA": im} - else: - msg = "Unsupported icon subimage format" - raise ValueError(msg) - - -class IcnsFile: - SIZES = { - (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], - (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], - (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)], - (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)], - (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)], - (128, 128, 1): [ - (b"ic07", read_png_or_jpeg2000), - (b"it32", read_32t), - (b"t8mk", read_mk), - ], - (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)], - (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)], - (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)], - (32, 32, 1): [ - (b"icp5", read_png_or_jpeg2000), - (b"il32", read_32), - (b"l8mk", read_mk), - ], - (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)], - (16, 16, 1): [ - (b"icp4", read_png_or_jpeg2000), - (b"is32", read_32), - (b"s8mk", read_mk), - ], - } - - def __init__(self, fobj: IO[bytes]) -> None: - """ - fobj is a file-like object as an icns resource - """ - # signature : (start, length) - self.dct = {} - self.fobj = fobj - sig, filesize = nextheader(fobj) - if not _accept(sig): - msg = "not an icns file" - raise SyntaxError(msg) - i = HEADERSIZE - while i < filesize: - sig, blocksize = nextheader(fobj) - if blocksize <= 0: - msg = "invalid block header" - raise SyntaxError(msg) - i += HEADERSIZE - blocksize -= HEADERSIZE - self.dct[sig] = (i, blocksize) - fobj.seek(blocksize, io.SEEK_CUR) - i += blocksize - - def itersizes(self) -> list[tuple[int, int, int]]: - sizes = [] - for size, fmts in self.SIZES.items(): - for fmt, reader in fmts: - if fmt in self.dct: - sizes.append(size) - break - return sizes - - def bestsize(self) -> tuple[int, int, int]: - sizes = self.itersizes() - if not sizes: - msg = "No 32bit icon resources found" - raise SyntaxError(msg) - return max(sizes) - - def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]: - """ - Get an icon resource as {channel: array}. Note that - the arrays are bottom-up like windows bitmaps and will likely - need to be flipped or transposed in some way. - """ - dct = {} - for code, reader in self.SIZES[size]: - desc = self.dct.get(code) - if desc is not None: - dct.update(reader(self.fobj, desc, size)) - return dct - - def getimage( - self, size: tuple[int, int] | tuple[int, int, int] | None = None - ) -> Image.Image: - if size is None: - size = self.bestsize() - elif len(size) == 2: - size = (size[0], size[1], 1) - channels = self.dataforsize(size) - - im = channels.get("RGBA") - if im: - return im - - im = channels["RGB"].copy() - try: - im.putalpha(channels["A"]) - except KeyError: - pass - return im - - -## -# Image plugin for Mac OS icons. - - -class IcnsImageFile(ImageFile.ImageFile): - """ - PIL image support for Mac OS .icns files. - Chooses the best resolution, but will possibly load - a different size image if you mutate the size attribute - before calling 'load'. - - The info dictionary has a key 'sizes' that is a list - of sizes that the icns file has. - """ - - format = "ICNS" - format_description = "Mac OS icns resource" - - def _open(self) -> None: - self.icns = IcnsFile(self.fp) - self._mode = "RGBA" - self.info["sizes"] = self.icns.itersizes() - self.best_size = self.icns.bestsize() - self.size = ( - self.best_size[0] * self.best_size[2], - self.best_size[1] * self.best_size[2], - ) - - @property # type: ignore[override] - def size(self) -> tuple[int, int] | tuple[int, int, int]: - return self._size - - @size.setter - def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None: - if len(value) == 3: - deprecate("Setting size to (width, height, scale)", 12, "load(scale)") - if value in self.info["sizes"]: - self._size = value # type: ignore[assignment] - return - else: - # Check that a matching size exists, - # or that there is a scale that would create a size that matches - for size in self.info["sizes"]: - simple_size = size[0] * size[2], size[1] * size[2] - scale = simple_size[0] // value[0] - if simple_size[1] / value[1] == scale: - self._size = value - return - msg = "This is not one of the allowed sizes of this image" - raise ValueError(msg) - - def load(self, scale: int | None = None) -> Image.core.PixelAccess | None: - if scale is not None or len(self.size) == 3: - if scale is None and len(self.size) == 3: - scale = self.size[2] - assert scale is not None - width, height = self.size[:2] - self.size = width * scale, height * scale - self.best_size = width, height, scale - - px = Image.Image.load(self) - if self._im is not None and self.im.size == self.size: - # Already loaded - return px - self.load_prepare() - # This is likely NOT the best way to do it, but whatever. - im = self.icns.getimage(self.best_size) - - # If this is a PNG or JPEG 2000, it won't be loaded yet - px = im.load() - - self.im = im.im - self._mode = im.mode - self.size = im.size - - return px - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - """ - Saves the image as a series of PNG files, - that are then combined into a .icns file. - """ - if hasattr(fp, "flush"): - fp.flush() - - sizes = { - b"ic07": 128, - b"ic08": 256, - b"ic09": 512, - b"ic10": 1024, - b"ic11": 32, - b"ic12": 64, - b"ic13": 256, - b"ic14": 512, - } - provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])} - size_streams = {} - for size in set(sizes.values()): - image = ( - provided_images[size] - if size in provided_images - else im.resize((size, size)) - ) - - temp = io.BytesIO() - image.save(temp, "png") - size_streams[size] = temp.getvalue() - - entries = [] - for type, size in sizes.items(): - stream = size_streams[size] - entries.append((type, HEADERSIZE + len(stream), stream)) - - # Header - fp.write(MAGIC) - file_length = HEADERSIZE # Header - file_length += HEADERSIZE + 8 * len(entries) # TOC - file_length += sum(entry[1] for entry in entries) - fp.write(struct.pack(">i", file_length)) - - # TOC - fp.write(b"TOC ") - fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) - for entry in entries: - fp.write(entry[0]) - fp.write(struct.pack(">i", entry[1])) - - # Data - for entry in entries: - fp.write(entry[0]) - fp.write(struct.pack(">i", entry[1])) - fp.write(entry[2]) - - if hasattr(fp, "flush"): - fp.flush() - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(MAGIC) - - -Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) -Image.register_extension(IcnsImageFile.format, ".icns") - -Image.register_save(IcnsImageFile.format, _save) -Image.register_mime(IcnsImageFile.format, "image/icns") - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python3 IcnsImagePlugin.py [file]") - sys.exit() - - with open(sys.argv[1], "rb") as fp: - imf = IcnsImageFile(fp) - for size in imf.info["sizes"]: - width, height, scale = imf.size = size - imf.save(f"out-{width}-{height}-{scale}.png") - with Image.open(sys.argv[1]) as im: - im.save("out.png") - if sys.platform == "windows": - os.startfile("out.png") diff --git a/python_modules/PIL/IcoImagePlugin.py b/python_modules/PIL/IcoImagePlugin.py deleted file mode 100644 index bd35ac890..000000000 --- a/python_modules/PIL/IcoImagePlugin.py +++ /dev/null @@ -1,381 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Windows Icon support for PIL -# -# History: -# 96-05-27 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# - -# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis -# . -# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki -# -# Icon format references: -# * https://en.wikipedia.org/wiki/ICO_(file_format) -# * https://msdn.microsoft.com/en-us/library/ms997538.aspx -from __future__ import annotations - -import warnings -from io import BytesIO -from math import ceil, log -from typing import IO, NamedTuple - -from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin -from ._binary import i16le as i16 -from ._binary import i32le as i32 -from ._binary import o8 -from ._binary import o16le as o16 -from ._binary import o32le as o32 - -# -# -------------------------------------------------------------------- - -_MAGIC = b"\0\0\1\0" - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - fp.write(_MAGIC) # (2+2) - bmp = im.encoderinfo.get("bitmap_format") == "bmp" - sizes = im.encoderinfo.get( - "sizes", - [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], - ) - frames = [] - provided_ims = [im] + im.encoderinfo.get("append_images", []) - width, height = im.size - for size in sorted(set(sizes)): - if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256: - continue - - for provided_im in provided_ims: - if provided_im.size != size: - continue - frames.append(provided_im) - if bmp: - bits = BmpImagePlugin.SAVE[provided_im.mode][1] - bits_used = [bits] - for other_im in provided_ims: - if other_im.size != size: - continue - bits = BmpImagePlugin.SAVE[other_im.mode][1] - if bits not in bits_used: - # Another image has been supplied for this size - # with a different bit depth - frames.append(other_im) - bits_used.append(bits) - break - else: - # TODO: invent a more convenient method for proportional scalings - frame = provided_im.copy() - frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) - frames.append(frame) - fp.write(o16(len(frames))) # idCount(2) - offset = fp.tell() + len(frames) * 16 - for frame in frames: - width, height = frame.size - # 0 means 256 - fp.write(o8(width if width < 256 else 0)) # bWidth(1) - fp.write(o8(height if height < 256 else 0)) # bHeight(1) - - bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0) - fp.write(o8(colors)) # bColorCount(1) - fp.write(b"\0") # bReserved(1) - fp.write(b"\0\0") # wPlanes(2) - fp.write(o16(bits)) # wBitCount(2) - - image_io = BytesIO() - if bmp: - frame.save(image_io, "dib") - - if bits != 32: - and_mask = Image.new("1", size) - ImageFile._save( - and_mask, - image_io, - [ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))], - ) - else: - frame.save(image_io, "png") - image_io.seek(0) - image_bytes = image_io.read() - if bmp: - image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:] - bytes_len = len(image_bytes) - fp.write(o32(bytes_len)) # dwBytesInRes(4) - fp.write(o32(offset)) # dwImageOffset(4) - current = fp.tell() - fp.seek(offset) - fp.write(image_bytes) - offset = offset + bytes_len - fp.seek(current) - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(_MAGIC) - - -class IconHeader(NamedTuple): - width: int - height: int - nb_color: int - reserved: int - planes: int - bpp: int - size: int - offset: int - dim: tuple[int, int] - square: int - color_depth: int - - -class IcoFile: - def __init__(self, buf: IO[bytes]) -> None: - """ - Parse image from file-like object containing ico file data - """ - - # check magic - s = buf.read(6) - if not _accept(s): - msg = "not an ICO file" - raise SyntaxError(msg) - - self.buf = buf - self.entry = [] - - # Number of items in file - self.nb_items = i16(s, 4) - - # Get headers for each item - for i in range(self.nb_items): - s = buf.read(16) - - # See Wikipedia - width = s[0] or 256 - height = s[1] or 256 - - # No. of colors in image (0 if >=8bpp) - nb_color = s[2] - bpp = i16(s, 6) - icon_header = IconHeader( - width=width, - height=height, - nb_color=nb_color, - reserved=s[3], - planes=i16(s, 4), - bpp=i16(s, 6), - size=i32(s, 8), - offset=i32(s, 12), - dim=(width, height), - square=width * height, - # See Wikipedia notes about color depth. - # We need this just to differ images with equal sizes - color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256, - ) - - self.entry.append(icon_header) - - self.entry = sorted(self.entry, key=lambda x: x.color_depth) - # ICO images are usually squares - self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True) - - def sizes(self) -> set[tuple[int, int]]: - """ - Get a set of all available icon sizes and color depths. - """ - return {(h.width, h.height) for h in self.entry} - - def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int: - for i, h in enumerate(self.entry): - if size == h.dim and (bpp is False or bpp == h.color_depth): - return i - return 0 - - def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image: - """ - Get an image from the icon - """ - return self.frame(self.getentryindex(size, bpp)) - - def frame(self, idx: int) -> Image.Image: - """ - Get an image from frame idx - """ - - header = self.entry[idx] - - self.buf.seek(header.offset) - data = self.buf.read(8) - self.buf.seek(header.offset) - - im: Image.Image - if data[:8] == PngImagePlugin._MAGIC: - # png frame - im = PngImagePlugin.PngImageFile(self.buf) - Image._decompression_bomb_check(im.size) - else: - # XOR + AND mask bmp frame - im = BmpImagePlugin.DibImageFile(self.buf) - Image._decompression_bomb_check(im.size) - - # change tile dimension to only encompass XOR image - im._size = (im.size[0], int(im.size[1] / 2)) - d, e, o, a = im.tile[0] - im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a) - - # figure out where AND mask image starts - if header.bpp == 32: - # 32-bit color depth icon image allows semitransparent areas - # PIL's DIB format ignores transparency bits, recover them. - # The DIB is packed in BGRX byte order where X is the alpha - # channel. - - # Back up to start of bmp data - self.buf.seek(o) - # extract every 4th byte (eg. 3,7,11,15,...) - alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4] - - # convert to an 8bpp grayscale image - try: - mask = Image.frombuffer( - "L", # 8bpp - im.size, # (w, h) - alpha_bytes, # source chars - "raw", # raw decoder - ("L", 0, -1), # 8bpp inverted, unpadded, reversed - ) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - mask = None - else: - raise - else: - # get AND image from end of bitmap - w = im.size[0] - if (w % 32) > 0: - # bitmap row data is aligned to word boundaries - w += 32 - (im.size[0] % 32) - - # the total mask data is - # padded row size * height / bits per char - - total_bytes = int((w * im.size[1]) / 8) - and_mask_offset = header.offset + header.size - total_bytes - - self.buf.seek(and_mask_offset) - mask_data = self.buf.read(total_bytes) - - # convert raw data to image - try: - mask = Image.frombuffer( - "1", # 1 bpp - im.size, # (w, h) - mask_data, # source chars - "raw", # raw decoder - ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed - ) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - mask = None - else: - raise - - # now we have two images, im is XOR image and mask is AND image - - # apply mask image as alpha channel - if mask: - im = im.convert("RGBA") - im.putalpha(mask) - - return im - - -## -# Image plugin for Windows Icon files. - - -class IcoImageFile(ImageFile.ImageFile): - """ - PIL read-only image support for Microsoft Windows .ico files. - - By default the largest resolution image in the file will be loaded. This - can be changed by altering the 'size' attribute before calling 'load'. - - The info dictionary has a key 'sizes' that is a list of the sizes available - in the icon file. - - Handles classic, XP and Vista icon formats. - - When saving, PNG compression is used. Support for this was only added in - Windows Vista. If you are unable to view the icon in Windows, convert the - image to "RGBA" mode before saving. - - This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis - . - https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki - """ - - format = "ICO" - format_description = "Windows Icon" - - def _open(self) -> None: - self.ico = IcoFile(self.fp) - self.info["sizes"] = self.ico.sizes() - self.size = self.ico.entry[0].dim - self.load() - - @property - def size(self) -> tuple[int, int]: - return self._size - - @size.setter - def size(self, value: tuple[int, int]) -> None: - if value not in self.info["sizes"]: - msg = "This is not one of the allowed sizes of this image" - raise ValueError(msg) - self._size = value - - def load(self) -> Image.core.PixelAccess | None: - if self._im is not None and self.im.size == self.size: - # Already loaded - return Image.Image.load(self) - im = self.ico.getimage(self.size) - # if tile is PNG, it won't really be loaded yet - im.load() - self.im = im.im - self._mode = im.mode - if im.palette: - self.palette = im.palette - if im.size != self.size: - warnings.warn("Image was not the expected size") - - index = self.ico.getentryindex(self.size) - sizes = list(self.info["sizes"]) - sizes[index] = im.size - self.info["sizes"] = set(sizes) - - self.size = im.size - return Image.Image.load(self) - - def load_seek(self, pos: int) -> None: - # Flag the ImageFile.Parser so that it - # just does all the decode at the end. - pass - - -# -# -------------------------------------------------------------------- - - -Image.register_open(IcoImageFile.format, IcoImageFile, _accept) -Image.register_save(IcoImageFile.format, _save) -Image.register_extension(IcoImageFile.format, ".ico") - -Image.register_mime(IcoImageFile.format, "image/x-icon") diff --git a/python_modules/PIL/ImImagePlugin.py b/python_modules/PIL/ImImagePlugin.py deleted file mode 100644 index 71b999678..000000000 --- a/python_modules/PIL/ImImagePlugin.py +++ /dev/null @@ -1,389 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# IFUNC IM file handling for PIL -# -# history: -# 1995-09-01 fl Created. -# 1997-01-03 fl Save palette images -# 1997-01-08 fl Added sequence support -# 1997-01-23 fl Added P and RGB save support -# 1997-05-31 fl Read floating point images -# 1997-06-22 fl Save floating point images -# 1997-08-27 fl Read and save 1-bit images -# 1998-06-25 fl Added support for RGB+LUT images -# 1998-07-02 fl Added support for YCC images -# 1998-07-15 fl Renamed offset attribute to avoid name clash -# 1998-12-29 fl Added I;16 support -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) -# 2003-09-26 fl Added LA/PA support -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-2001 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -import re -from typing import IO, Any - -from . import Image, ImageFile, ImagePalette -from ._util import DeferredError - -# -------------------------------------------------------------------- -# Standard tags - -COMMENT = "Comment" -DATE = "Date" -EQUIPMENT = "Digitalization equipment" -FRAMES = "File size (no of images)" -LUT = "Lut" -NAME = "Name" -SCALE = "Scale (x,y)" -SIZE = "Image size (x*y)" -MODE = "Image type" - -TAGS = { - COMMENT: 0, - DATE: 0, - EQUIPMENT: 0, - FRAMES: 0, - LUT: 0, - NAME: 0, - SCALE: 0, - SIZE: 0, - MODE: 0, -} - -OPEN = { - # ifunc93/p3cfunc formats - "0 1 image": ("1", "1"), - "L 1 image": ("1", "1"), - "Greyscale image": ("L", "L"), - "Grayscale image": ("L", "L"), - "RGB image": ("RGB", "RGB;L"), - "RLB image": ("RGB", "RLB"), - "RYB image": ("RGB", "RLB"), - "B1 image": ("1", "1"), - "B2 image": ("P", "P;2"), - "B4 image": ("P", "P;4"), - "X 24 image": ("RGB", "RGB"), - "L 32 S image": ("I", "I;32"), - "L 32 F image": ("F", "F;32"), - # old p3cfunc formats - "RGB3 image": ("RGB", "RGB;T"), - "RYB3 image": ("RGB", "RYB;T"), - # extensions - "LA image": ("LA", "LA;L"), - "PA image": ("LA", "PA;L"), - "RGBA image": ("RGBA", "RGBA;L"), - "RGBX image": ("RGB", "RGBX;L"), - "CMYK image": ("CMYK", "CMYK;L"), - "YCC image": ("YCbCr", "YCbCr;L"), -} - -# ifunc95 extensions -for i in ["8", "8S", "16", "16S", "32", "32F"]: - OPEN[f"L {i} image"] = ("F", f"F;{i}") - OPEN[f"L*{i} image"] = ("F", f"F;{i}") -for i in ["16", "16L", "16B"]: - OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}") - OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}") -for i in ["32S"]: - OPEN[f"L {i} image"] = ("I", f"I;{i}") - OPEN[f"L*{i} image"] = ("I", f"I;{i}") -for j in range(2, 33): - OPEN[f"L*{j} image"] = ("F", f"F;{j}") - - -# -------------------------------------------------------------------- -# Read IM directory - -split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") - - -def number(s: Any) -> float: - try: - return int(s) - except ValueError: - return float(s) - - -## -# Image plugin for the IFUNC IM file format. - - -class ImImageFile(ImageFile.ImageFile): - format = "IM" - format_description = "IFUNC Image Memory" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # Quick rejection: if there's not an LF among the first - # 100 bytes, this is (probably) not a text header. - - if b"\n" not in self.fp.read(100): - msg = "not an IM file" - raise SyntaxError(msg) - self.fp.seek(0) - - n = 0 - - # Default values - self.info[MODE] = "L" - self.info[SIZE] = (512, 512) - self.info[FRAMES] = 1 - - self.rawmode = "L" - - while True: - s = self.fp.read(1) - - # Some versions of IFUNC uses \n\r instead of \r\n... - if s == b"\r": - continue - - if not s or s == b"\0" or s == b"\x1a": - break - - # FIXME: this may read whole file if not a text file - s = s + self.fp.readline() - - if len(s) > 100: - msg = "not an IM file" - raise SyntaxError(msg) - - if s.endswith(b"\r\n"): - s = s[:-2] - elif s.endswith(b"\n"): - s = s[:-1] - - try: - m = split.match(s) - except re.error as e: - msg = "not an IM file" - raise SyntaxError(msg) from e - - if m: - k, v = m.group(1, 2) - - # Don't know if this is the correct encoding, - # but a decent guess (I guess) - k = k.decode("latin-1", "replace") - v = v.decode("latin-1", "replace") - - # Convert value as appropriate - if k in [FRAMES, SCALE, SIZE]: - v = v.replace("*", ",") - v = tuple(map(number, v.split(","))) - if len(v) == 1: - v = v[0] - elif k == MODE and v in OPEN: - v, self.rawmode = OPEN[v] - - # Add to dictionary. Note that COMMENT tags are - # combined into a list of strings. - if k == COMMENT: - if k in self.info: - self.info[k].append(v) - else: - self.info[k] = [v] - else: - self.info[k] = v - - if k in TAGS: - n += 1 - - else: - msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}" - raise SyntaxError(msg) - - if not n: - msg = "Not an IM file" - raise SyntaxError(msg) - - # Basic attributes - self._size = self.info[SIZE] - self._mode = self.info[MODE] - - # Skip forward to start of image data - while s and not s.startswith(b"\x1a"): - s = self.fp.read(1) - if not s: - msg = "File truncated" - raise SyntaxError(msg) - - if LUT in self.info: - # convert lookup table to palette or lut attribute - palette = self.fp.read(768) - greyscale = 1 # greyscale palette - linear = 1 # linear greyscale palette - for i in range(256): - if palette[i] == palette[i + 256] == palette[i + 512]: - if palette[i] != i: - linear = 0 - else: - greyscale = 0 - if self.mode in ["L", "LA", "P", "PA"]: - if greyscale: - if not linear: - self.lut = list(palette[:256]) - else: - if self.mode in ["L", "P"]: - self._mode = self.rawmode = "P" - elif self.mode in ["LA", "PA"]: - self._mode = "PA" - self.rawmode = "PA;L" - self.palette = ImagePalette.raw("RGB;L", palette) - elif self.mode == "RGB": - if not greyscale or not linear: - self.lut = list(palette) - - self.frame = 0 - - self.__offset = offs = self.fp.tell() - - self._fp = self.fp # FIXME: hack - - if self.rawmode.startswith("F;"): - # ifunc95 formats - try: - # use bit decoder (if necessary) - bits = int(self.rawmode[2:]) - if bits not in [8, 16, 32]: - self.tile = [ - ImageFile._Tile( - "bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1) - ) - ] - return - except ValueError: - pass - - if self.rawmode in ["RGB;T", "RYB;T"]: - # Old LabEye/3PC files. Would be very surprised if anyone - # ever stumbled upon such a file ;-) - size = self.size[0] * self.size[1] - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)), - ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), - ImageFile._Tile( - "raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1) - ), - ] - else: - # LabEye/IFUNC files - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1)) - ] - - @property - def n_frames(self) -> int: - return self.info[FRAMES] - - @property - def is_animated(self) -> bool: - return self.info[FRAMES] > 1 - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if isinstance(self._fp, DeferredError): - raise self._fp.ex - - self.frame = frame - - if self.mode == "1": - bits = 1 - else: - bits = 8 * len(self.mode) - - size = ((self.size[0] * bits + 7) // 8) * self.size[1] - offs = self.__offset + frame * size - - self.fp = self._fp - - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1)) - ] - - def tell(self) -> int: - return self.frame - - -# -# -------------------------------------------------------------------- -# Save IM files - - -SAVE = { - # mode: (im type, raw mode) - "1": ("0 1", "1"), - "L": ("Greyscale", "L"), - "LA": ("LA", "LA;L"), - "P": ("Greyscale", "P"), - "PA": ("LA", "PA;L"), - "I": ("L 32S", "I;32S"), - "I;16": ("L 16", "I;16"), - "I;16L": ("L 16L", "I;16L"), - "I;16B": ("L 16B", "I;16B"), - "F": ("L 32F", "F;32F"), - "RGB": ("RGB", "RGB;L"), - "RGBA": ("RGBA", "RGBA;L"), - "RGBX": ("RGBX", "RGBX;L"), - "CMYK": ("CMYK", "CMYK;L"), - "YCbCr": ("YCC", "YCbCr;L"), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - image_type, rawmode = SAVE[im.mode] - except KeyError as e: - msg = f"Cannot save {im.mode} images as IM" - raise ValueError(msg) from e - - frames = im.encoderinfo.get("frames", 1) - - fp.write(f"Image type: {image_type} image\r\n".encode("ascii")) - if filename: - # Each line must be 100 characters or less, - # or: SyntaxError("not an IM file") - # 8 characters are used for "Name: " and "\r\n" - # Keep just the filename, ditch the potentially overlong path - if isinstance(filename, bytes): - filename = filename.decode("ascii") - name, ext = os.path.splitext(os.path.basename(filename)) - name = "".join([name[: 92 - len(ext)], ext]) - - fp.write(f"Name: {name}\r\n".encode("ascii")) - fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii")) - fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) - if im.mode in ["P", "PA"]: - fp.write(b"Lut: 1\r\n") - fp.write(b"\000" * (511 - fp.tell()) + b"\032") - if im.mode in ["P", "PA"]: - im_palette = im.im.getpalette("RGB", "RGB;L") - colors = len(im_palette) // 3 - palette = b"" - for i in range(3): - palette += im_palette[colors * i : colors * (i + 1)] - palette += b"\x00" * (256 - colors) - fp.write(palette) # 768 bytes - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))] - ) - - -# -# -------------------------------------------------------------------- -# Registry - - -Image.register_open(ImImageFile.format, ImImageFile) -Image.register_save(ImImageFile.format, _save) - -Image.register_extension(ImImageFile.format, ".im") diff --git a/python_modules/PIL/Image.py b/python_modules/PIL/Image.py deleted file mode 100644 index d209405c4..000000000 --- a/python_modules/PIL/Image.py +++ /dev/null @@ -1,4245 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# the Image class wrapper -# -# partial release history: -# 1995-09-09 fl Created -# 1996-03-11 fl PIL release 0.0 (proof of concept) -# 1996-04-30 fl PIL release 0.1b1 -# 1999-07-28 fl PIL release 1.0 final -# 2000-06-07 fl PIL release 1.1 -# 2000-10-20 fl PIL release 1.1.1 -# 2001-05-07 fl PIL release 1.1.2 -# 2002-03-15 fl PIL release 1.1.3 -# 2003-05-10 fl PIL release 1.1.4 -# 2005-03-28 fl PIL release 1.1.5 -# 2006-12-02 fl PIL release 1.1.6 -# 2009-11-15 fl PIL release 1.1.7 -# -# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-2009 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -from __future__ import annotations - -import abc -import atexit -import builtins -import io -import logging -import math -import os -import re -import struct -import sys -import tempfile -import warnings -from collections.abc import Callable, Iterator, MutableMapping, Sequence -from enum import IntEnum -from types import ModuleType -from typing import IO, Any, Literal, Protocol, cast - -# VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION was removed in Pillow 9.0.0. -# Use __version__ instead. -from . import ( - ExifTags, - ImageMode, - TiffTags, - UnidentifiedImageError, - __version__, - _plugins, -) -from ._binary import i32le, o32be, o32le -from ._deprecate import deprecate -from ._util import DeferredError, is_path - -ElementTree: ModuleType | None -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None - -logger = logging.getLogger(__name__) - - -class DecompressionBombWarning(RuntimeWarning): - pass - - -class DecompressionBombError(Exception): - pass - - -WARN_POSSIBLE_FORMATS: bool = False - -# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image -MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3) - - -try: - # If the _imaging C module is not present, Pillow will not load. - # Note that other modules should not refer to _imaging directly; - # import Image and use the Image.core variable instead. - # Also note that Image.core is not a publicly documented interface, - # and should be considered private and subject to change. - from . import _imaging as core - - if __version__ != getattr(core, "PILLOW_VERSION", None): - msg = ( - "The _imaging extension was built for another version of Pillow or PIL:\n" - f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n" - f"Pillow version: {__version__}" - ) - raise ImportError(msg) - -except ImportError as v: - core = DeferredError.new(ImportError("The _imaging C module is not installed.")) - # Explanations for ways that we know we might have an import error - if str(v).startswith("Module use of python"): - # The _imaging C module is present, but not compiled for - # the right version (windows only). Print a warning, if - # possible. - warnings.warn( - "The _imaging extension was built for another version of Python.", - RuntimeWarning, - ) - elif str(v).startswith("The _imaging extension"): - warnings.warn(str(v), RuntimeWarning) - # Fail here anyway. Don't let people run with a mostly broken Pillow. - # see docs/porting.rst - raise - - -def isImageType(t: Any) -> TypeGuard[Image]: - """ - Checks if an object is an image object. - - .. warning:: - - This function is for internal use only. - - :param t: object to check if it's an image - :returns: True if the object is an image - """ - deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)") - return hasattr(t, "im") - - -# -# Constants - - -# transpose -class Transpose(IntEnum): - FLIP_LEFT_RIGHT = 0 - FLIP_TOP_BOTTOM = 1 - ROTATE_90 = 2 - ROTATE_180 = 3 - ROTATE_270 = 4 - TRANSPOSE = 5 - TRANSVERSE = 6 - - -# transforms (also defined in Imaging.h) -class Transform(IntEnum): - AFFINE = 0 - EXTENT = 1 - PERSPECTIVE = 2 - QUAD = 3 - MESH = 4 - - -# resampling filters (also defined in Imaging.h) -class Resampling(IntEnum): - NEAREST = 0 - BOX = 4 - BILINEAR = 2 - HAMMING = 5 - BICUBIC = 3 - LANCZOS = 1 - - -_filters_support = { - Resampling.BOX: 0.5, - Resampling.BILINEAR: 1.0, - Resampling.HAMMING: 1.0, - Resampling.BICUBIC: 2.0, - Resampling.LANCZOS: 3.0, -} - - -# dithers -class Dither(IntEnum): - NONE = 0 - ORDERED = 1 # Not yet implemented - RASTERIZE = 2 # Not yet implemented - FLOYDSTEINBERG = 3 # default - - -# palettes/quantizers -class Palette(IntEnum): - WEB = 0 - ADAPTIVE = 1 - - -class Quantize(IntEnum): - MEDIANCUT = 0 - MAXCOVERAGE = 1 - FASTOCTREE = 2 - LIBIMAGEQUANT = 3 - - -module = sys.modules[__name__] -for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): - for item in enum: - setattr(module, item.name, item.value) - - -if hasattr(core, "DEFAULT_STRATEGY"): - DEFAULT_STRATEGY = core.DEFAULT_STRATEGY - FILTERED = core.FILTERED - HUFFMAN_ONLY = core.HUFFMAN_ONLY - RLE = core.RLE - FIXED = core.FIXED - - -# -------------------------------------------------------------------- -# Registries - -TYPE_CHECKING = False -if TYPE_CHECKING: - import mmap - from xml.etree.ElementTree import Element - - from IPython.lib.pretty import PrettyPrinter - - from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin - from ._typing import CapsuleType, NumpyArray, StrOrBytesPath, TypeGuard -ID: list[str] = [] -OPEN: dict[ - str, - tuple[ - Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], - Callable[[bytes], bool | str] | None, - ], -] = {} -MIME: dict[str, str] = {} -SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} -SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} -EXTENSION: dict[str, str] = {} -DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} -ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} - -# -------------------------------------------------------------------- -# Modes - -_ENDIAN = "<" if sys.byteorder == "little" else ">" - - -def _conv_type_shape(im: Image) -> tuple[tuple[int, ...], str]: - m = ImageMode.getmode(im.mode) - shape: tuple[int, ...] = (im.height, im.width) - extra = len(m.bands) - if extra != 1: - shape += (extra,) - return shape, m.typestr - - -MODES = [ - "1", - "CMYK", - "F", - "HSV", - "I", - "I;16", - "I;16B", - "I;16L", - "I;16N", - "L", - "LA", - "La", - "LAB", - "P", - "PA", - "RGB", - "RGBA", - "RGBa", - "RGBX", - "YCbCr", -] - -# raw modes that may be memory mapped. NOTE: if you change this, you -# may have to modify the stride calculation in map.c too! -_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") - - -def getmodebase(mode: str) -> str: - """ - Gets the "base" mode for given mode. This function returns "L" for - images that contain grayscale data, and "RGB" for images that - contain color data. - - :param mode: Input mode. - :returns: "L" or "RGB". - :exception KeyError: If the input mode was not a standard mode. - """ - return ImageMode.getmode(mode).basemode - - -def getmodetype(mode: str) -> str: - """ - Gets the storage type mode. Given a mode, this function returns a - single-layer mode suitable for storing individual bands. - - :param mode: Input mode. - :returns: "L", "I", or "F". - :exception KeyError: If the input mode was not a standard mode. - """ - return ImageMode.getmode(mode).basetype - - -def getmodebandnames(mode: str) -> tuple[str, ...]: - """ - Gets a list of individual band names. Given a mode, this function returns - a tuple containing the names of individual bands (use - :py:method:`~PIL.Image.getmodetype` to get the mode used to store each - individual band. - - :param mode: Input mode. - :returns: A tuple containing band names. The length of the tuple - gives the number of bands in an image of the given mode. - :exception KeyError: If the input mode was not a standard mode. - """ - return ImageMode.getmode(mode).bands - - -def getmodebands(mode: str) -> int: - """ - Gets the number of individual bands for this mode. - - :param mode: Input mode. - :returns: The number of bands in this mode. - :exception KeyError: If the input mode was not a standard mode. - """ - return len(ImageMode.getmode(mode).bands) - - -# -------------------------------------------------------------------- -# Helpers - -_initialized = 0 - - -def preinit() -> None: - """ - Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers. - - It is called when opening or saving images. - """ - - global _initialized - if _initialized >= 1: - return - - try: - from . import BmpImagePlugin - - assert BmpImagePlugin - except ImportError: - pass - try: - from . import GifImagePlugin - - assert GifImagePlugin - except ImportError: - pass - try: - from . import JpegImagePlugin - - assert JpegImagePlugin - except ImportError: - pass - try: - from . import PpmImagePlugin - - assert PpmImagePlugin - except ImportError: - pass - try: - from . import PngImagePlugin - - assert PngImagePlugin - except ImportError: - pass - - _initialized = 1 - - -def init() -> bool: - """ - Explicitly initializes the Python Imaging Library. This function - loads all available file format drivers. - - It is called when opening or saving images if :py:meth:`~preinit()` is - insufficient, and by :py:meth:`~PIL.features.pilinfo`. - """ - - global _initialized - if _initialized >= 2: - return False - - parent_name = __name__.rpartition(".")[0] - for plugin in _plugins: - try: - logger.debug("Importing %s", plugin) - __import__(f"{parent_name}.{plugin}", globals(), locals(), []) - except ImportError as e: - logger.debug("Image: failed to import %s: %s", plugin, e) - - if OPEN or SAVE: - _initialized = 2 - return True - return False - - -# -------------------------------------------------------------------- -# Codec factories (used by tobytes/frombytes and ImageFile.load) - - -def _getdecoder( - mode: str, decoder_name: str, args: Any, extra: tuple[Any, ...] = () -) -> core.ImagingDecoder | ImageFile.PyDecoder: - # tweak arguments - if args is None: - args = () - elif not isinstance(args, tuple): - args = (args,) - - try: - decoder = DECODERS[decoder_name] - except KeyError: - pass - else: - return decoder(mode, *args + extra) - - try: - # get decoder - decoder = getattr(core, f"{decoder_name}_decoder") - except AttributeError as e: - msg = f"decoder {decoder_name} not available" - raise OSError(msg) from e - return decoder(mode, *args + extra) - - -def _getencoder( - mode: str, encoder_name: str, args: Any, extra: tuple[Any, ...] = () -) -> core.ImagingEncoder | ImageFile.PyEncoder: - # tweak arguments - if args is None: - args = () - elif not isinstance(args, tuple): - args = (args,) - - try: - encoder = ENCODERS[encoder_name] - except KeyError: - pass - else: - return encoder(mode, *args + extra) - - try: - # get encoder - encoder = getattr(core, f"{encoder_name}_encoder") - except AttributeError as e: - msg = f"encoder {encoder_name} not available" - raise OSError(msg) from e - return encoder(mode, *args + extra) - - -# -------------------------------------------------------------------- -# Simple expression analyzer - - -class ImagePointTransform: - """ - Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than - 8 bits, this represents an affine transformation, where the value is multiplied by - ``scale`` and ``offset`` is added. - """ - - def __init__(self, scale: float, offset: float) -> None: - self.scale = scale - self.offset = offset - - def __neg__(self) -> ImagePointTransform: - return ImagePointTransform(-self.scale, -self.offset) - - def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform: - if isinstance(other, ImagePointTransform): - return ImagePointTransform( - self.scale + other.scale, self.offset + other.offset - ) - return ImagePointTransform(self.scale, self.offset + other) - - __radd__ = __add__ - - def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform: - return self + -other - - def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform: - return other + -self - - def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform: - if isinstance(other, ImagePointTransform): - return NotImplemented - return ImagePointTransform(self.scale * other, self.offset * other) - - __rmul__ = __mul__ - - def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform: - if isinstance(other, ImagePointTransform): - return NotImplemented - return ImagePointTransform(self.scale / other, self.offset / other) - - -def _getscaleoffset( - expr: Callable[[ImagePointTransform], ImagePointTransform | float], -) -> tuple[float, float]: - a = expr(ImagePointTransform(1, 0)) - return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) - - -# -------------------------------------------------------------------- -# Implementation wrapper - - -class SupportsGetData(Protocol): - def getdata( - self, - ) -> tuple[Transform, Sequence[int]]: ... - - -class Image: - """ - This class represents an image object. To create - :py:class:`~PIL.Image.Image` objects, use the appropriate factory - functions. There's hardly ever any reason to call the Image constructor - directly. - - * :py:func:`~PIL.Image.open` - * :py:func:`~PIL.Image.new` - * :py:func:`~PIL.Image.frombytes` - """ - - format: str | None = None - format_description: str | None = None - _close_exclusive_fp_after_loading = True - - def __init__(self) -> None: - # FIXME: take "new" parameters / other image? - self._im: core.ImagingCore | DeferredError | None = None - self._mode = "" - self._size = (0, 0) - self.palette: ImagePalette.ImagePalette | None = None - self.info: dict[str | tuple[int, int], Any] = {} - self.readonly = 0 - self._exif: Exif | None = None - - @property - def im(self) -> core.ImagingCore: - if isinstance(self._im, DeferredError): - raise self._im.ex - assert self._im is not None - return self._im - - @im.setter - def im(self, im: core.ImagingCore) -> None: - self._im = im - - @property - def width(self) -> int: - return self.size[0] - - @property - def height(self) -> int: - return self.size[1] - - @property - def size(self) -> tuple[int, int]: - return self._size - - @property - def mode(self) -> str: - return self._mode - - @property - def readonly(self) -> int: - return (self._im and self._im.readonly) or self._readonly - - @readonly.setter - def readonly(self, readonly: int) -> None: - self._readonly = readonly - - def _new(self, im: core.ImagingCore) -> Image: - new = Image() - new.im = im - new._mode = im.mode - new._size = im.size - if im.mode in ("P", "PA"): - if self.palette: - new.palette = self.palette.copy() - else: - from . import ImagePalette - - new.palette = ImagePalette.ImagePalette() - new.info = self.info.copy() - return new - - # Context manager support - def __enter__(self): - return self - - def __exit__(self, *args): - from . import ImageFile - - if isinstance(self, ImageFile.ImageFile): - if getattr(self, "_exclusive_fp", False): - self._close_fp() - self.fp = None - - def close(self) -> None: - """ - This operation will destroy the image core and release its memory. - The image data will be unusable afterward. - - This function is required to close images that have multiple frames or - have not had their file read and closed by the - :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for - more information. - """ - if getattr(self, "map", None): - if sys.platform == "win32" and hasattr(sys, "pypy_version_info"): - self.map.close() - self.map: mmap.mmap | None = None - - # Instead of simply setting to None, we're setting up a - # deferred error that will better explain that the core image - # object is gone. - self._im = DeferredError(ValueError("Operation on closed image")) - - def _copy(self) -> None: - self.load() - self.im = self.im.copy() - self.readonly = 0 - - def _ensure_mutable(self) -> None: - if self.readonly: - self._copy() - else: - self.load() - - def _dump( - self, file: str | None = None, format: str | None = None, **options: Any - ) -> str: - suffix = "" - if format: - suffix = f".{format}" - - if not file: - f, filename = tempfile.mkstemp(suffix) - os.close(f) - else: - filename = file - if not filename.endswith(suffix): - filename = filename + suffix - - self.load() - - if not format or format == "PPM": - self.im.save_ppm(filename) - else: - self.save(filename, format, **options) - - return filename - - def __eq__(self, other: object) -> bool: - if self.__class__ is not other.__class__: - return False - assert isinstance(other, Image) - return ( - self.mode == other.mode - and self.size == other.size - and self.info == other.info - and self.getpalette() == other.getpalette() - and self.tobytes() == other.tobytes() - ) - - def __repr__(self) -> str: - return ( - f"<{self.__class__.__module__}.{self.__class__.__name__} " - f"image mode={self.mode} size={self.size[0]}x{self.size[1]} " - f"at 0x{id(self):X}>" - ) - - def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: - """IPython plain text display support""" - - # Same as __repr__ but without unpredictable id(self), - # to keep Jupyter notebook `text/plain` output stable. - p.text( - f"<{self.__class__.__module__}.{self.__class__.__name__} " - f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>" - ) - - def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: - """Helper function for iPython display hook. - - :param image_format: Image format. - :returns: image as bytes, saved into the given format. - """ - b = io.BytesIO() - try: - self.save(b, image_format, **kwargs) - except Exception: - return None - return b.getvalue() - - def _repr_png_(self) -> bytes | None: - """iPython display hook support for PNG format. - - :returns: PNG version of the image as bytes - """ - return self._repr_image("PNG", compress_level=1) - - def _repr_jpeg_(self) -> bytes | None: - """iPython display hook support for JPEG format. - - :returns: JPEG version of the image as bytes - """ - return self._repr_image("JPEG") - - @property - def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: - # numpy array interface support - new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3} - if self.mode == "1": - # Binary images need to be extended from bits to bytes - # See: https://github.com/python-pillow/Pillow/issues/350 - new["data"] = self.tobytes("raw", "L") - else: - new["data"] = self.tobytes() - new["shape"], new["typestr"] = _conv_type_shape(self) - return new - - def __arrow_c_schema__(self) -> object: - self.load() - return self.im.__arrow_c_schema__() - - def __arrow_c_array__( - self, requested_schema: object | None = None - ) -> tuple[object, object]: - self.load() - return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__()) - - def __getstate__(self) -> list[Any]: - im_data = self.tobytes() # load image first - return [self.info, self.mode, self.size, self.getpalette(), im_data] - - def __setstate__(self, state: list[Any]) -> None: - Image.__init__(self) - info, mode, size, palette, data = state[:5] - self.info = info - self._mode = mode - self._size = size - self.im = core.new(mode, size) - if mode in ("L", "LA", "P", "PA") and palette: - self.putpalette(palette) - self.frombytes(data) - - def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes: - """ - Return image as a bytes object. - - .. warning:: - - This method returns raw image data derived from Pillow's internal - storage. For compressed image data (e.g. PNG, JPEG) use - :meth:`~.save`, with a BytesIO parameter for in-memory data. - - :param encoder_name: What encoder to use. - - The default is to use the standard "raw" encoder. - To see how this packs pixel data into the returned - bytes, see :file:`libImaging/Pack.c`. - - A list of C encoders can be seen under codecs - section of the function array in - :file:`_imaging.c`. Python encoders are registered - within the relevant plugins. - :param args: Extra arguments to the encoder. - :returns: A :py:class:`bytes` object. - """ - - encoder_args: Any = args - if len(encoder_args) == 1 and isinstance(encoder_args[0], tuple): - # may pass tuple instead of argument list - encoder_args = encoder_args[0] - - if encoder_name == "raw" and encoder_args == (): - encoder_args = self.mode - - self.load() - - if self.width == 0 or self.height == 0: - return b"" - - # unpack data - e = _getencoder(self.mode, encoder_name, encoder_args) - e.setimage(self.im) - - from . import ImageFile - - bufsize = max(ImageFile.MAXBLOCK, self.size[0] * 4) # see RawEncode.c - - output = [] - while True: - bytes_consumed, errcode, data = e.encode(bufsize) - output.append(data) - if errcode: - break - if errcode < 0: - msg = f"encoder error {errcode} in tobytes" - raise RuntimeError(msg) - - return b"".join(output) - - def tobitmap(self, name: str = "image") -> bytes: - """ - Returns the image converted to an X11 bitmap. - - .. note:: This method only works for mode "1" images. - - :param name: The name prefix to use for the bitmap variables. - :returns: A string containing an X11 bitmap. - :raises ValueError: If the mode is not "1" - """ - - self.load() - if self.mode != "1": - msg = "not a bitmap" - raise ValueError(msg) - data = self.tobytes("xbm") - return b"".join( - [ - f"#define {name}_width {self.size[0]}\n".encode("ascii"), - f"#define {name}_height {self.size[1]}\n".encode("ascii"), - f"static char {name}_bits[] = {{\n".encode("ascii"), - data, - b"};", - ] - ) - - def frombytes( - self, - data: bytes | bytearray | SupportsArrayInterface, - decoder_name: str = "raw", - *args: Any, - ) -> None: - """ - Loads this image with pixel data from a bytes object. - - This method is similar to the :py:func:`~PIL.Image.frombytes` function, - but loads data into this image instead of creating a new image object. - """ - - if self.width == 0 or self.height == 0: - return - - decoder_args: Any = args - if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple): - # may pass tuple instead of argument list - decoder_args = decoder_args[0] - - # default format - if decoder_name == "raw" and decoder_args == (): - decoder_args = self.mode - - # unpack data - d = _getdecoder(self.mode, decoder_name, decoder_args) - d.setimage(self.im) - s = d.decode(data) - - if s[0] >= 0: - msg = "not enough image data" - raise ValueError(msg) - if s[1] != 0: - msg = "cannot decode image data" - raise ValueError(msg) - - def load(self) -> core.PixelAccess | None: - """ - Allocates storage for the image and loads the pixel data. In - normal cases, you don't need to call this method, since the - Image class automatically loads an opened image when it is - accessed for the first time. - - If the file associated with the image was opened by Pillow, then this - method will close it. The exception to this is if the image has - multiple frames, in which case the file will be left open for seek - operations. See :ref:`file-handling` for more information. - - :returns: An image access object. - :rtype: :py:class:`.PixelAccess` - """ - if self._im is not None and self.palette and self.palette.dirty: - # realize palette - mode, arr = self.palette.getdata() - self.im.putpalette(self.palette.mode, mode, arr) - self.palette.dirty = 0 - self.palette.rawmode = None - if "transparency" in self.info and mode in ("LA", "PA"): - if isinstance(self.info["transparency"], int): - self.im.putpalettealpha(self.info["transparency"], 0) - else: - self.im.putpalettealphas(self.info["transparency"]) - self.palette.mode = "RGBA" - else: - self.palette.palette = self.im.getpalette( - self.palette.mode, self.palette.mode - ) - - if self._im is not None: - return self.im.pixel_access(self.readonly) - return None - - def verify(self) -> None: - """ - Verifies the contents of a file. For data read from a file, this - method attempts to determine if the file is broken, without - actually decoding the image data. If this method finds any - problems, it raises suitable exceptions. If you need to load - the image after using this method, you must reopen the image - file. - """ - pass - - def convert( - self, - mode: str | None = None, - matrix: tuple[float, ...] | None = None, - dither: Dither | None = None, - palette: Palette = Palette.WEB, - colors: int = 256, - ) -> Image: - """ - Returns a converted copy of this image. For the "P" mode, this - method translates pixels through the palette. If mode is - omitted, a mode is chosen so that all information in the image - and the palette can be represented without a palette. - - This supports all possible conversions between "L", "RGB" and "CMYK". The - ``matrix`` argument only supports "L" and "RGB". - - When translating a color image to grayscale (mode "L"), - the library uses the ITU-R 601-2 luma transform:: - - L = R * 299/1000 + G * 587/1000 + B * 114/1000 - - The default method of converting a grayscale ("L") or "RGB" - image into a bilevel (mode "1") image uses Floyd-Steinberg - dither to approximate the original image luminosity levels. If - dither is ``None``, all values larger than 127 are set to 255 (white), - all other values to 0 (black). To use other thresholds, use the - :py:meth:`~PIL.Image.Image.point` method. - - When converting from "RGBA" to "P" without a ``matrix`` argument, - this passes the operation to :py:meth:`~PIL.Image.Image.quantize`, - and ``dither`` and ``palette`` are ignored. - - When converting from "PA", if an "RGBA" palette is present, the alpha - channel from the image will be used instead of the values from the palette. - - :param mode: The requested mode. See: :ref:`concept-modes`. - :param matrix: An optional conversion matrix. If given, this - should be 4- or 12-tuple containing floating point values. - :param dither: Dithering method, used when converting from - mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` - (default). Note that this is not used when ``matrix`` is supplied. - :param palette: Palette to use when converting from mode "RGB" - to "P". Available palettes are :data:`Palette.WEB` or - :data:`Palette.ADAPTIVE`. - :param colors: Number of colors to use for the :data:`Palette.ADAPTIVE` - palette. Defaults to 256. - :rtype: :py:class:`~PIL.Image.Image` - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - - self.load() - - has_transparency = "transparency" in self.info - if not mode and self.mode == "P": - # determine default mode - if self.palette: - mode = self.palette.mode - else: - mode = "RGB" - if mode == "RGB" and has_transparency: - mode = "RGBA" - if not mode or (mode == self.mode and not matrix): - return self.copy() - - if matrix: - # matrix conversion - if mode not in ("L", "RGB"): - msg = "illegal conversion" - raise ValueError(msg) - im = self.im.convert_matrix(mode, matrix) - new_im = self._new(im) - if has_transparency and self.im.bands == 3: - transparency = new_im.info["transparency"] - - def convert_transparency( - m: tuple[float, ...], v: tuple[int, int, int] - ) -> int: - value = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 - return max(0, min(255, int(value))) - - if mode == "L": - transparency = convert_transparency(matrix, transparency) - elif len(mode) == 3: - transparency = tuple( - convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) - for i in range(len(transparency)) - ) - new_im.info["transparency"] = transparency - return new_im - - if mode == "P" and self.mode == "RGBA": - return self.quantize(colors) - - trns = None - delete_trns = False - # transparency handling - if has_transparency: - if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or ( - self.mode == "RGB" and mode in ("La", "LA", "RGBa", "RGBA") - ): - # Use transparent conversion to promote from transparent - # color to an alpha channel. - new_im = self._new( - self.im.convert_transparent(mode, self.info["transparency"]) - ) - del new_im.info["transparency"] - return new_im - elif self.mode in ("L", "RGB", "P") and mode in ("L", "RGB", "P"): - t = self.info["transparency"] - if isinstance(t, bytes): - # Dragons. This can't be represented by a single color - warnings.warn( - "Palette images with Transparency expressed in bytes should be " - "converted to RGBA images" - ) - delete_trns = True - else: - # get the new transparency color. - # use existing conversions - trns_im = new(self.mode, (1, 1)) - if self.mode == "P": - assert self.palette is not None - trns_im.putpalette(self.palette, self.palette.mode) - if isinstance(t, tuple): - err = "Couldn't allocate a palette color for transparency" - assert trns_im.palette is not None - try: - t = trns_im.palette.getcolor(t, self) - except ValueError as e: - if str(e) == "cannot allocate more than 256 colors": - # If all 256 colors are in use, - # then there is no need for transparency - t = None - else: - raise ValueError(err) from e - if t is None: - trns = None - else: - trns_im.putpixel((0, 0), t) - - if mode in ("L", "RGB"): - trns_im = trns_im.convert(mode) - else: - # can't just retrieve the palette number, got to do it - # after quantization. - trns_im = trns_im.convert("RGB") - trns = trns_im.getpixel((0, 0)) - - elif self.mode == "P" and mode in ("LA", "PA", "RGBA"): - t = self.info["transparency"] - delete_trns = True - - if isinstance(t, bytes): - self.im.putpalettealphas(t) - elif isinstance(t, int): - self.im.putpalettealpha(t, 0) - else: - msg = "Transparency for P mode should be bytes or int" - raise ValueError(msg) - - if mode == "P" and palette == Palette.ADAPTIVE: - im = self.im.quantize(colors) - new_im = self._new(im) - from . import ImagePalette - - new_im.palette = ImagePalette.ImagePalette( - "RGB", new_im.im.getpalette("RGB") - ) - if delete_trns: - # This could possibly happen if we requantize to fewer colors. - # The transparency would be totally off in that case. - del new_im.info["transparency"] - if trns is not None: - try: - new_im.info["transparency"] = new_im.palette.getcolor( - cast(tuple[int, ...], trns), # trns was converted to RGB - new_im, - ) - except Exception: - # if we can't make a transparent color, don't leave the old - # transparency hanging around to mess us up. - del new_im.info["transparency"] - warnings.warn("Couldn't allocate palette entry for transparency") - return new_im - - if "LAB" in (self.mode, mode): - im = self - if mode == "LAB": - if im.mode not in ("RGB", "RGBA", "RGBX"): - im = im.convert("RGBA") - other_mode = im.mode - else: - other_mode = mode - if other_mode in ("RGB", "RGBA", "RGBX"): - from . import ImageCms - - srgb = ImageCms.createProfile("sRGB") - lab = ImageCms.createProfile("LAB") - profiles = [lab, srgb] if im.mode == "LAB" else [srgb, lab] - transform = ImageCms.buildTransform( - profiles[0], profiles[1], im.mode, mode - ) - return transform.apply(im) - - # colorspace conversion - if dither is None: - dither = Dither.FLOYDSTEINBERG - - try: - im = self.im.convert(mode, dither) - except ValueError: - try: - # normalize source image and try again - modebase = getmodebase(self.mode) - if modebase == self.mode: - raise - im = self.im.convert(modebase) - im = im.convert(mode, dither) - except KeyError as e: - msg = "illegal conversion" - raise ValueError(msg) from e - - new_im = self._new(im) - if mode == "P" and palette != Palette.ADAPTIVE: - from . import ImagePalette - - new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB")) - if delete_trns: - # crash fail if we leave a bytes transparency in an rgb/l mode. - del new_im.info["transparency"] - if trns is not None: - if new_im.mode == "P" and new_im.palette: - try: - new_im.info["transparency"] = new_im.palette.getcolor( - cast(tuple[int, ...], trns), new_im # trns was converted to RGB - ) - except ValueError as e: - del new_im.info["transparency"] - if str(e) != "cannot allocate more than 256 colors": - # If all 256 colors are in use, - # then there is no need for transparency - warnings.warn( - "Couldn't allocate palette entry for transparency" - ) - else: - new_im.info["transparency"] = trns - return new_im - - def quantize( - self, - colors: int = 256, - method: int | None = None, - kmeans: int = 0, - palette: Image | None = None, - dither: Dither = Dither.FLOYDSTEINBERG, - ) -> Image: - """ - Convert the image to 'P' mode with the specified number - of colors. - - :param colors: The desired number of colors, <= 256 - :param method: :data:`Quantize.MEDIANCUT` (median cut), - :data:`Quantize.MAXCOVERAGE` (maximum coverage), - :data:`Quantize.FASTOCTREE` (fast octree), - :data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support - using :py:func:`PIL.features.check_feature` with - ``feature="libimagequant"``). - - By default, :data:`Quantize.MEDIANCUT` will be used. - - The exception to this is RGBA images. :data:`Quantize.MEDIANCUT` - and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so - :data:`Quantize.FASTOCTREE` is used by default instead. - :param kmeans: Integer greater than or equal to zero. - :param palette: Quantize to the palette of given - :py:class:`PIL.Image.Image`. - :param dither: Dithering method, used when converting from - mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` - (default). - :returns: A new image - """ - - self.load() - - if method is None: - # defaults: - method = Quantize.MEDIANCUT - if self.mode == "RGBA": - method = Quantize.FASTOCTREE - - if self.mode == "RGBA" and method not in ( - Quantize.FASTOCTREE, - Quantize.LIBIMAGEQUANT, - ): - # Caller specified an invalid mode. - msg = ( - "Fast Octree (method == 2) and libimagequant (method == 3) " - "are the only valid methods for quantizing RGBA images" - ) - raise ValueError(msg) - - if palette: - # use palette from reference image - palette.load() - if palette.mode != "P": - msg = "bad mode for palette image" - raise ValueError(msg) - if self.mode not in {"RGB", "L"}: - msg = "only RGB or L mode images can be quantized to a palette" - raise ValueError(msg) - im = self.im.convert("P", dither, palette.im) - new_im = self._new(im) - assert palette.palette is not None - new_im.palette = palette.palette.copy() - return new_im - - if kmeans < 0: - msg = "kmeans must not be negative" - raise ValueError(msg) - - im = self._new(self.im.quantize(colors, method, kmeans)) - - from . import ImagePalette - - mode = im.im.getpalettemode() - palette_data = im.im.getpalette(mode, mode)[: colors * len(mode)] - im.palette = ImagePalette.ImagePalette(mode, palette_data) - - return im - - def copy(self) -> Image: - """ - Copies this image. Use this method if you wish to paste things - into an image, but still retain the original. - - :rtype: :py:class:`~PIL.Image.Image` - :returns: An :py:class:`~PIL.Image.Image` object. - """ - self.load() - return self._new(self.im.copy()) - - __copy__ = copy - - def crop(self, box: tuple[float, float, float, float] | None = None) -> Image: - """ - Returns a rectangular region from this image. The box is a - 4-tuple defining the left, upper, right, and lower pixel - coordinate. See :ref:`coordinate-system`. - - Note: Prior to Pillow 3.4.0, this was a lazy operation. - - :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. - :rtype: :py:class:`~PIL.Image.Image` - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if box is None: - return self.copy() - - if box[2] < box[0]: - msg = "Coordinate 'right' is less than 'left'" - raise ValueError(msg) - elif box[3] < box[1]: - msg = "Coordinate 'lower' is less than 'upper'" - raise ValueError(msg) - - self.load() - return self._new(self._crop(self.im, box)) - - def _crop( - self, im: core.ImagingCore, box: tuple[float, float, float, float] - ) -> core.ImagingCore: - """ - Returns a rectangular region from the core image object im. - - This is equivalent to calling im.crop((x0, y0, x1, y1)), but - includes additional sanity checks. - - :param im: a core image object - :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. - :returns: A core image object. - """ - - x0, y0, x1, y1 = map(int, map(round, box)) - - absolute_values = (abs(x1 - x0), abs(y1 - y0)) - - _decompression_bomb_check(absolute_values) - - return im.crop((x0, y0, x1, y1)) - - def draft( - self, mode: str | None, size: tuple[int, int] | None - ) -> tuple[str, tuple[int, int, float, float]] | None: - """ - Configures the image file loader so it returns a version of the - image that as closely as possible matches the given mode and - size. For example, you can use this method to convert a color - JPEG to grayscale while loading it. - - If any changes are made, returns a tuple with the chosen ``mode`` and - ``box`` with coordinates of the original image within the altered one. - - Note that this method modifies the :py:class:`~PIL.Image.Image` object - in place. If the image has already been loaded, this method has no - effect. - - Note: This method is not implemented for most images. It is - currently implemented only for JPEG and MPO images. - - :param mode: The requested mode. - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - """ - pass - - def _expand(self, xmargin: int, ymargin: int | None = None) -> Image: - if ymargin is None: - ymargin = xmargin - self.load() - return self._new(self.im.expand(xmargin, ymargin)) - - def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: - """ - Filters this image using the given filter. For a list of - available filters, see the :py:mod:`~PIL.ImageFilter` module. - - :param filter: Filter kernel. - :returns: An :py:class:`~PIL.Image.Image` object.""" - - from . import ImageFilter - - self.load() - - if callable(filter): - filter = filter() - if not hasattr(filter, "filter"): - msg = "filter argument should be ImageFilter.Filter instance or class" - raise TypeError(msg) - - multiband = isinstance(filter, ImageFilter.MultibandFilter) - if self.im.bands == 1 or multiband: - return self._new(filter.filter(self.im)) - - ims = [ - self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands) - ] - return merge(self.mode, ims) - - def getbands(self) -> tuple[str, ...]: - """ - Returns a tuple containing the name of each band in this image. - For example, ``getbands`` on an RGB image returns ("R", "G", "B"). - - :returns: A tuple containing band names. - :rtype: tuple - """ - return ImageMode.getmode(self.mode).bands - - def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | None: - """ - Calculates the bounding box of the non-zero regions in the - image. - - :param alpha_only: Optional flag, defaulting to ``True``. - If ``True`` and the image has an alpha channel, trim transparent pixels. - Otherwise, trim pixels when all channels are zero. - Keyword-only argument. - :returns: The bounding box is returned as a 4-tuple defining the - left, upper, right, and lower pixel coordinate. See - :ref:`coordinate-system`. If the image is completely empty, this - method returns None. - - """ - - self.load() - return self.im.getbbox(alpha_only) - - def getcolors( - self, maxcolors: int = 256 - ) -> list[tuple[int, tuple[int, ...]]] | list[tuple[int, float]] | None: - """ - Returns a list of colors used in this image. - - The colors will be in the image's mode. For example, an RGB image will - return a tuple of (red, green, blue) color values, and a P image will - return the index of the color in the palette. - - :param maxcolors: Maximum number of colors. If this number is - exceeded, this method returns None. The default limit is - 256 colors. - :returns: An unsorted list of (count, pixel) values. - """ - - self.load() - if self.mode in ("1", "L", "P"): - h = self.im.histogram() - out: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]] - if len(out) > maxcolors: - return None - return out - return self.im.getcolors(maxcolors) - - def getdata(self, band: int | None = None) -> core.ImagingCore: - """ - Returns the contents of this image as a sequence object - containing pixel values. The sequence object is flattened, so - that values for line one follow directly after the values of - line zero, and so on. - - Note that the sequence object returned by this method is an - internal PIL data type, which only supports certain sequence - operations. To convert it to an ordinary sequence (e.g. for - printing), use ``list(im.getdata())``. - - :param band: What band to return. The default is to return - all bands. To return a single band, pass in the index - value (e.g. 0 to get the "R" band from an "RGB" image). - :returns: A sequence-like object. - """ - - self.load() - if band is not None: - return self.im.getband(band) - return self.im # could be abused - - def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: - """ - Gets the minimum and maximum pixel values for each band in - the image. - - :returns: For a single-band image, a 2-tuple containing the - minimum and maximum pixel value. For a multi-band image, - a tuple containing one 2-tuple for each band. - """ - - self.load() - if self.im.bands > 1: - return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) - return self.im.getextrema() - - def getxmp(self) -> dict[str, Any]: - """ - Returns a dictionary containing the XMP tags. - Requires defusedxml to be installed. - - :returns: XMP tags in a dictionary. - """ - - def get_name(tag: str) -> str: - return re.sub("^{[^}]+}", "", tag) - - def get_value(element: Element) -> str | dict[str, Any] | None: - value: dict[str, Any] = {get_name(k): v for k, v in element.attrib.items()} - children = list(element) - if children: - for child in children: - name = get_name(child.tag) - child_value = get_value(child) - if name in value: - if not isinstance(value[name], list): - value[name] = [value[name]] - value[name].append(child_value) - else: - value[name] = child_value - elif value: - if element.text: - value["text"] = element.text - else: - return element.text - return value - - if ElementTree is None: - warnings.warn("XMP data cannot be read without defusedxml dependency") - return {} - if "xmp" not in self.info: - return {} - root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 ")) - return {get_name(root.tag): get_value(root)} - - def getexif(self) -> Exif: - """ - Gets EXIF data from the image. - - :returns: an :py:class:`~PIL.Image.Exif` object. - """ - if self._exif is None: - self._exif = Exif() - elif self._exif._loaded: - return self._exif - self._exif._loaded = True - - exif_info = self.info.get("exif") - if exif_info is None: - if "Raw profile type exif" in self.info: - exif_info = bytes.fromhex( - "".join(self.info["Raw profile type exif"].split("\n")[3:]) - ) - elif hasattr(self, "tag_v2"): - self._exif.bigtiff = self.tag_v2._bigtiff - self._exif.endian = self.tag_v2._endian - self._exif.load_from_fp(self.fp, self.tag_v2._offset) - if exif_info is not None: - self._exif.load(exif_info) - - # XMP tags - if ExifTags.Base.Orientation not in self._exif: - xmp_tags = self.info.get("XML:com.adobe.xmp") - pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])' - if not xmp_tags and (xmp_tags := self.info.get("xmp")): - pattern = rb'tiff:Orientation(="|>)([0-9])' - if xmp_tags: - match = re.search(pattern, xmp_tags) - if match: - self._exif[ExifTags.Base.Orientation] = int(match[2]) - - return self._exif - - def _reload_exif(self) -> None: - if self._exif is None or not self._exif._loaded: - return - self._exif._loaded = False - self.getexif() - - def get_child_images(self) -> list[ImageFile.ImageFile]: - from . import ImageFile - - deprecate("Image.Image.get_child_images", 13) - return ImageFile.ImageFile.get_child_images(self) # type: ignore[arg-type] - - def getim(self) -> CapsuleType: - """ - Returns a capsule that points to the internal image memory. - - :returns: A capsule object. - """ - - self.load() - return self.im.ptr - - def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: - """ - Returns the image palette as a list. - - :param rawmode: The mode in which to return the palette. ``None`` will - return the palette in its current mode. - - .. versionadded:: 9.1.0 - - :returns: A list of color values [r, g, b, ...], or None if the - image has no palette. - """ - - self.load() - try: - mode = self.im.getpalettemode() - except ValueError: - return None # no palette - if rawmode is None: - rawmode = mode - return list(self.im.getpalette(mode, rawmode)) - - @property - def has_transparency_data(self) -> bool: - """ - Determine if an image has transparency data, whether in the form of an - alpha channel, a palette with an alpha channel, or a "transparency" key - in the info dictionary. - - Note the image might still appear solid, if all of the values shown - within are opaque. - - :returns: A boolean. - """ - if ( - self.mode in ("LA", "La", "PA", "RGBA", "RGBa") - or "transparency" in self.info - ): - return True - if self.mode == "P": - assert self.palette is not None - return self.palette.mode.endswith("A") - return False - - def apply_transparency(self) -> None: - """ - If a P mode image has a "transparency" key in the info dictionary, - remove the key and instead apply the transparency to the palette. - Otherwise, the image is unchanged. - """ - if self.mode != "P" or "transparency" not in self.info: - return - - from . import ImagePalette - - palette = self.getpalette("RGBA") - assert palette is not None - transparency = self.info["transparency"] - if isinstance(transparency, bytes): - for i, alpha in enumerate(transparency): - palette[i * 4 + 3] = alpha - else: - palette[transparency * 4 + 3] = 0 - self.palette = ImagePalette.ImagePalette("RGBA", bytes(palette)) - self.palette.dirty = 1 - - del self.info["transparency"] - - def getpixel( - self, xy: tuple[int, int] | list[int] - ) -> float | tuple[int, ...] | None: - """ - Returns the pixel value at a given position. - - :param xy: The coordinate, given as (x, y). See - :ref:`coordinate-system`. - :returns: The pixel value. If the image is a multi-layer image, - this method returns a tuple. - """ - - self.load() - return self.im.getpixel(tuple(xy)) - - def getprojection(self) -> tuple[list[int], list[int]]: - """ - Get projection to x and y axes - - :returns: Two sequences, indicating where there are non-zero - pixels along the X-axis and the Y-axis, respectively. - """ - - self.load() - x, y = self.im.getprojection() - return list(x), list(y) - - def histogram( - self, mask: Image | None = None, extrema: tuple[float, float] | None = None - ) -> list[int]: - """ - Returns a histogram for the image. The histogram is returned as a - list of pixel counts, one for each pixel value in the source - image. Counts are grouped into 256 bins for each band, even if - the image has more than 8 bits per band. If the image has more - than one band, the histograms for all bands are concatenated (for - example, the histogram for an "RGB" image contains 768 values). - - A bilevel image (mode "1") is treated as a grayscale ("L") image - by this method. - - If a mask is provided, the method returns a histogram for those - parts of the image where the mask image is non-zero. The mask - image must have the same size as the image, and be either a - bi-level image (mode "1") or a grayscale image ("L"). - - :param mask: An optional mask. - :param extrema: An optional tuple of manually-specified extrema. - :returns: A list containing pixel counts. - """ - self.load() - if mask: - mask.load() - return self.im.histogram((0, 0), mask.im) - if self.mode in ("I", "F"): - return self.im.histogram( - extrema if extrema is not None else self.getextrema() - ) - return self.im.histogram() - - def entropy( - self, mask: Image | None = None, extrema: tuple[float, float] | None = None - ) -> float: - """ - Calculates and returns the entropy for the image. - - A bilevel image (mode "1") is treated as a grayscale ("L") - image by this method. - - If a mask is provided, the method employs the histogram for - those parts of the image where the mask image is non-zero. - The mask image must have the same size as the image, and be - either a bi-level image (mode "1") or a grayscale image ("L"). - - :param mask: An optional mask. - :param extrema: An optional tuple of manually-specified extrema. - :returns: A float value representing the image entropy - """ - self.load() - if mask: - mask.load() - return self.im.entropy((0, 0), mask.im) - if self.mode in ("I", "F"): - return self.im.entropy( - extrema if extrema is not None else self.getextrema() - ) - return self.im.entropy() - - def paste( - self, - im: Image | str | float | tuple[float, ...], - box: Image | tuple[int, int, int, int] | tuple[int, int] | None = None, - mask: Image | None = None, - ) -> None: - """ - Pastes another image into this image. The box argument is either - a 2-tuple giving the upper left corner, a 4-tuple defining the - left, upper, right, and lower pixel coordinate, or None (same as - (0, 0)). See :ref:`coordinate-system`. If a 4-tuple is given, the size - of the pasted image must match the size of the region. - - If the modes don't match, the pasted image is converted to the mode of - this image (see the :py:meth:`~PIL.Image.Image.convert` method for - details). - - Instead of an image, the source can be a integer or tuple - containing pixel values. The method then fills the region - with the given color. When creating RGB images, you can - also use color strings as supported by the ImageColor module. - - If a mask is given, this method updates only the regions - indicated by the mask. You can use either "1", "L", "LA", "RGBA" - or "RGBa" images (if present, the alpha band is used as mask). - Where the mask is 255, the given image is copied as is. Where - the mask is 0, the current value is preserved. Intermediate - values will mix the two images together, including their alpha - channels if they have them. - - See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to - combine images with respect to their alpha channels. - - :param im: Source image or pixel value (integer, float or tuple). - :param box: An optional 4-tuple giving the region to paste into. - If a 2-tuple is used instead, it's treated as the upper left - corner. If omitted or None, the source is pasted into the - upper left corner. - - If an image is given as the second argument and there is no - third, the box defaults to (0, 0), and the second argument - is interpreted as a mask image. - :param mask: An optional mask image. - """ - - if isinstance(box, Image): - if mask is not None: - msg = "If using second argument as mask, third argument must be None" - raise ValueError(msg) - # abbreviated paste(im, mask) syntax - mask = box - box = None - - if box is None: - box = (0, 0) - - if len(box) == 2: - # upper left corner given; get size from image or mask - if isinstance(im, Image): - size = im.size - elif isinstance(mask, Image): - size = mask.size - else: - # FIXME: use self.size here? - msg = "cannot determine region size; use 4-item box" - raise ValueError(msg) - box += (box[0] + size[0], box[1] + size[1]) - - source: core.ImagingCore | str | float | tuple[float, ...] - if isinstance(im, str): - from . import ImageColor - - source = ImageColor.getcolor(im, self.mode) - elif isinstance(im, Image): - im.load() - if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): - # should use an adapter for this! - im = im.convert(self.mode) - source = im.im - else: - source = im - - self._ensure_mutable() - - if mask: - mask.load() - self.im.paste(source, box, mask.im) - else: - self.im.paste(source, box) - - def alpha_composite( - self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0) - ) -> None: - """'In-place' analog of Image.alpha_composite. Composites an image - onto this image. - - :param im: image to composite over this one - :param dest: Optional 2 tuple (left, top) specifying the upper - left corner in this (destination) image. - :param source: Optional 2 (left, top) tuple for the upper left - corner in the overlay source image, or 4 tuple (left, top, right, - bottom) for the bounds of the source rectangle - - Performance Note: Not currently implemented in-place in the core layer. - """ - - if not isinstance(source, (list, tuple)): - msg = "Source must be a list or tuple" - raise ValueError(msg) - if not isinstance(dest, (list, tuple)): - msg = "Destination must be a list or tuple" - raise ValueError(msg) - - if len(source) == 4: - overlay_crop_box = tuple(source) - elif len(source) == 2: - overlay_crop_box = tuple(source) + im.size - else: - msg = "Source must be a sequence of length 2 or 4" - raise ValueError(msg) - - if not len(dest) == 2: - msg = "Destination must be a sequence of length 2" - raise ValueError(msg) - if min(source) < 0: - msg = "Source must be non-negative" - raise ValueError(msg) - - # over image, crop if it's not the whole image. - if overlay_crop_box == (0, 0) + im.size: - overlay = im - else: - overlay = im.crop(overlay_crop_box) - - # target for the paste - box = tuple(dest) + (dest[0] + overlay.width, dest[1] + overlay.height) - - # destination image. don't copy if we're using the whole image. - if box == (0, 0) + self.size: - background = self - else: - background = self.crop(box) - - result = alpha_composite(background, overlay) - self.paste(result, box) - - def point( - self, - lut: ( - Sequence[float] - | NumpyArray - | Callable[[int], float] - | Callable[[ImagePointTransform], ImagePointTransform | float] - | ImagePointHandler - ), - mode: str | None = None, - ) -> Image: - """ - Maps this image through a lookup table or function. - - :param lut: A lookup table, containing 256 (or 65536 if - self.mode=="I" and mode == "L") values per band in the - image. A function can be used instead, it should take a - single argument. The function is called once for each - possible pixel value, and the resulting table is applied to - all bands of the image. - - It may also be an :py:class:`~PIL.Image.ImagePointHandler` - object:: - - class Example(Image.ImagePointHandler): - def point(self, im: Image) -> Image: - # Return result - :param mode: Output mode (default is same as input). This can only be used if - the source image has mode "L" or "P", and the output has mode "1" or the - source image mode is "I" and the output mode is "L". - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - self.load() - - if isinstance(lut, ImagePointHandler): - return lut.point(self) - - if callable(lut): - # if it isn't a list, it should be a function - if self.mode in ("I", "I;16", "F"): - # check if the function can be used with point_transform - # UNDONE wiredfool -- I think this prevents us from ever doing - # a gamma function point transform on > 8bit images. - scale, offset = _getscaleoffset(lut) # type: ignore[arg-type] - return self._new(self.im.point_transform(scale, offset)) - # for other modes, convert the function to a table - flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type] - else: - flatLut = lut - - if self.mode == "F": - # FIXME: _imaging returns a confusing error message for this case - msg = "point operation not supported for this mode" - raise ValueError(msg) - - if mode != "F": - flatLut = [round(i) for i in flatLut] - return self._new(self.im.point(flatLut, mode)) - - def putalpha(self, alpha: Image | int) -> None: - """ - Adds or replaces the alpha layer in this image. If the image - does not have an alpha layer, it's converted to "LA" or "RGBA". - The new layer must be either "L" or "1". - - :param alpha: The new alpha layer. This can either be an "L" or "1" - image having the same size as this image, or an integer. - """ - - self._ensure_mutable() - - if self.mode not in ("LA", "PA", "RGBA"): - # attempt to promote self to a matching alpha mode - try: - mode = getmodebase(self.mode) + "A" - try: - self.im.setmode(mode) - except (AttributeError, ValueError) as e: - # do things the hard way - im = self.im.convert(mode) - if im.mode not in ("LA", "PA", "RGBA"): - msg = "alpha channel could not be added" - raise ValueError(msg) from e # sanity check - self.im = im - self._mode = self.im.mode - except KeyError as e: - msg = "illegal image mode" - raise ValueError(msg) from e - - if self.mode in ("LA", "PA"): - band = 1 - else: - band = 3 - - if isinstance(alpha, Image): - # alpha layer - if alpha.mode not in ("1", "L"): - msg = "illegal image mode" - raise ValueError(msg) - alpha.load() - if alpha.mode == "1": - alpha = alpha.convert("L") - else: - # constant alpha - try: - self.im.fillband(band, alpha) - except (AttributeError, ValueError): - # do things the hard way - alpha = new("L", self.size, alpha) - else: - return - - self.im.putband(alpha.im, band) - - def putdata( - self, - data: Sequence[float] | Sequence[Sequence[int]] | core.ImagingCore | NumpyArray, - scale: float = 1.0, - offset: float = 0.0, - ) -> None: - """ - Copies pixel data from a flattened sequence object into the image. The - values should start at the upper left corner (0, 0), continue to the - end of the line, followed directly by the first value of the second - line, and so on. Data will be read until either the image or the - sequence ends. The scale and offset values are used to adjust the - sequence values: **pixel = value*scale + offset**. - - :param data: A flattened sequence object. - :param scale: An optional scale value. The default is 1.0. - :param offset: An optional offset value. The default is 0.0. - """ - - self._ensure_mutable() - - self.im.putdata(data, scale, offset) - - def putpalette( - self, - data: ImagePalette.ImagePalette | bytes | Sequence[int], - rawmode: str = "RGB", - ) -> None: - """ - Attaches a palette to this image. The image must be a "P", "PA", "L" - or "LA" image. - - The palette sequence must contain at most 256 colors, made up of one - integer value for each channel in the raw mode. - For example, if the raw mode is "RGB", then it can contain at most 768 - values, made up of red, green and blue values for the corresponding pixel - index in the 256 colors. - If the raw mode is "RGBA", then it can contain at most 1024 values, - containing red, green, blue and alpha values. - - Alternatively, an 8-bit string may be used instead of an integer sequence. - - :param data: A palette sequence (either a list or a string). - :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode - that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). - """ - from . import ImagePalette - - if self.mode not in ("L", "LA", "P", "PA"): - msg = "illegal image mode" - raise ValueError(msg) - if isinstance(data, ImagePalette.ImagePalette): - if data.rawmode is not None: - palette = ImagePalette.raw(data.rawmode, data.palette) - else: - palette = ImagePalette.ImagePalette(palette=data.palette) - palette.dirty = 1 - else: - if not isinstance(data, bytes): - data = bytes(data) - palette = ImagePalette.raw(rawmode, data) - self._mode = "PA" if "A" in self.mode else "P" - self.palette = palette - self.palette.mode = "RGBA" if "A" in rawmode else "RGB" - self.load() # install new palette - - def putpixel( - self, xy: tuple[int, int], value: float | tuple[int, ...] | list[int] - ) -> None: - """ - Modifies the pixel at the given position. The color is given as - a single numerical value for single-band images, and a tuple for - multi-band images. In addition to this, RGB and RGBA tuples are - accepted for P and PA images. - - Note that this method is relatively slow. For more extensive changes, - use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` - module instead. - - See: - - * :py:meth:`~PIL.Image.Image.paste` - * :py:meth:`~PIL.Image.Image.putdata` - * :py:mod:`~PIL.ImageDraw` - - :param xy: The pixel coordinate, given as (x, y). See - :ref:`coordinate-system`. - :param value: The pixel value. - """ - - if self.readonly: - self._copy() - self.load() - - if ( - self.mode in ("P", "PA") - and isinstance(value, (list, tuple)) - and len(value) in [3, 4] - ): - # RGB or RGBA value for a P or PA image - if self.mode == "PA": - alpha = value[3] if len(value) == 4 else 255 - value = value[:3] - assert self.palette is not None - palette_index = self.palette.getcolor(tuple(value), self) - value = (palette_index, alpha) if self.mode == "PA" else palette_index - return self.im.putpixel(xy, value) - - def remap_palette( - self, dest_map: list[int], source_palette: bytes | bytearray | None = None - ) -> Image: - """ - Rewrites the image to reorder the palette. - - :param dest_map: A list of indexes into the original palette. - e.g. ``[1,0]`` would swap a two item palette, and ``list(range(256))`` - is the identity transform. - :param source_palette: Bytes or None. - :returns: An :py:class:`~PIL.Image.Image` object. - - """ - from . import ImagePalette - - if self.mode not in ("L", "P"): - msg = "illegal image mode" - raise ValueError(msg) - - bands = 3 - palette_mode = "RGB" - if source_palette is None: - if self.mode == "P": - self.load() - palette_mode = self.im.getpalettemode() - if palette_mode == "RGBA": - bands = 4 - source_palette = self.im.getpalette(palette_mode, palette_mode) - else: # L-mode - source_palette = bytearray(i // 3 for i in range(768)) - elif len(source_palette) > 768: - bands = 4 - palette_mode = "RGBA" - - palette_bytes = b"" - new_positions = [0] * 256 - - # pick only the used colors from the palette - for i, oldPosition in enumerate(dest_map): - palette_bytes += source_palette[ - oldPosition * bands : oldPosition * bands + bands - ] - new_positions[oldPosition] = i - - # replace the palette color id of all pixel with the new id - - # Palette images are [0..255], mapped through a 1 or 3 - # byte/color map. We need to remap the whole image - # from palette 1 to palette 2. New_positions is - # an array of indexes into palette 1. Palette 2 is - # palette 1 with any holes removed. - - # We're going to leverage the convert mechanism to use the - # C code to remap the image from palette 1 to palette 2, - # by forcing the source image into 'L' mode and adding a - # mapping 'L' mode palette, then converting back to 'L' - # sans palette thus converting the image bytes, then - # assigning the optimized RGB palette. - - # perf reference, 9500x4000 gif, w/~135 colors - # 14 sec prepatch, 1 sec postpatch with optimization forced. - - mapping_palette = bytearray(new_positions) - - m_im = self.copy() - m_im._mode = "P" - - m_im.palette = ImagePalette.ImagePalette( - palette_mode, palette=mapping_palette * bands - ) - # possibly set palette dirty, then - # m_im.putpalette(mapping_palette, 'L') # converts to 'P' - # or just force it. - # UNDONE -- this is part of the general issue with palettes - m_im.im.putpalette(palette_mode, palette_mode + ";L", m_im.palette.tobytes()) - - m_im = m_im.convert("L") - - m_im.putpalette(palette_bytes, palette_mode) - m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes) - - if "transparency" in self.info: - try: - m_im.info["transparency"] = dest_map.index(self.info["transparency"]) - except ValueError: - if "transparency" in m_im.info: - del m_im.info["transparency"] - - return m_im - - def _get_safe_box( - self, - size: tuple[int, int], - resample: Resampling, - box: tuple[float, float, float, float], - ) -> tuple[int, int, int, int]: - """Expands the box so it includes adjacent pixels - that may be used by resampling with the given resampling filter. - """ - filter_support = _filters_support[resample] - 0.5 - scale_x = (box[2] - box[0]) / size[0] - scale_y = (box[3] - box[1]) / size[1] - support_x = filter_support * scale_x - support_y = filter_support * scale_y - - return ( - max(0, int(box[0] - support_x)), - max(0, int(box[1] - support_y)), - min(self.size[0], math.ceil(box[2] + support_x)), - min(self.size[1], math.ceil(box[3] + support_y)), - ) - - def resize( - self, - size: tuple[int, int] | list[int] | NumpyArray, - resample: int | None = None, - box: tuple[float, float, float, float] | None = None, - reducing_gap: float | None = None, - ) -> Image: - """ - Returns a resized copy of this image. - - :param size: The requested size in pixels, as a tuple or array: - (width, height). - :param resample: An optional resampling filter. This can be - one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, - :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, - :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. - If the image has mode "1" or "P", it is always set to - :py:data:`Resampling.NEAREST`. If the image mode is "BGR;15", - "BGR;16" or "BGR;24", then the default filter is - :py:data:`Resampling.NEAREST`. Otherwise, the default filter is - :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. - :param box: An optional 4-tuple of floats providing - the source image region to be scaled. - The values must be within (0, 0, width, height) rectangle. - If omitted or None, the entire source is used. - :param reducing_gap: Apply optimization by resizing the image - in two steps. First, reducing the image by integer times - using :py:meth:`~PIL.Image.Image.reduce`. - Second, resizing using regular resampling. The last step - changes size no less than by ``reducing_gap`` times. - ``reducing_gap`` may be None (no first step is performed) - or should be greater than 1.0. The bigger ``reducing_gap``, - the closer the result to the fair resampling. - The smaller ``reducing_gap``, the faster resizing. - With ``reducing_gap`` greater or equal to 3.0, the result is - indistinguishable from fair resampling in most cases. - The default value is None (no optimization). - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if resample is None: - bgr = self.mode.startswith("BGR;") - resample = Resampling.NEAREST if bgr else Resampling.BICUBIC - elif resample not in ( - Resampling.NEAREST, - Resampling.BILINEAR, - Resampling.BICUBIC, - Resampling.LANCZOS, - Resampling.BOX, - Resampling.HAMMING, - ): - msg = f"Unknown resampling filter ({resample})." - - filters = [ - f"{filter[1]} ({filter[0]})" - for filter in ( - (Resampling.NEAREST, "Image.Resampling.NEAREST"), - (Resampling.LANCZOS, "Image.Resampling.LANCZOS"), - (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), - (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), - (Resampling.BOX, "Image.Resampling.BOX"), - (Resampling.HAMMING, "Image.Resampling.HAMMING"), - ) - ] - msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" - raise ValueError(msg) - - if reducing_gap is not None and reducing_gap < 1.0: - msg = "reducing_gap must be 1.0 or greater" - raise ValueError(msg) - - if box is None: - box = (0, 0) + self.size - - size = tuple(size) - if self.size == size and box == (0, 0) + self.size: - return self.copy() - - if self.mode in ("1", "P"): - resample = Resampling.NEAREST - - if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST: - im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) - im = im.resize(size, resample, box) - return im.convert(self.mode) - - self.load() - - if reducing_gap is not None and resample != Resampling.NEAREST: - factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 - factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 - if factor_x > 1 or factor_y > 1: - reduce_box = self._get_safe_box(size, cast(Resampling, resample), box) - factor = (factor_x, factor_y) - self = ( - self.reduce(factor, box=reduce_box) - if callable(self.reduce) - else Image.reduce(self, factor, box=reduce_box) - ) - box = ( - (box[0] - reduce_box[0]) / factor_x, - (box[1] - reduce_box[1]) / factor_y, - (box[2] - reduce_box[0]) / factor_x, - (box[3] - reduce_box[1]) / factor_y, - ) - - return self._new(self.im.resize(size, resample, box)) - - def reduce( - self, - factor: int | tuple[int, int], - box: tuple[int, int, int, int] | None = None, - ) -> Image: - """ - Returns a copy of the image reduced ``factor`` times. - If the size of the image is not dividable by ``factor``, - the resulting size will be rounded up. - - :param factor: A greater than 0 integer or tuple of two integers - for width and height separately. - :param box: An optional 4-tuple of ints providing - the source image region to be reduced. - The values must be within ``(0, 0, width, height)`` rectangle. - If omitted or ``None``, the entire source is used. - """ - if not isinstance(factor, (list, tuple)): - factor = (factor, factor) - - if box is None: - box = (0, 0) + self.size - - if factor == (1, 1) and box == (0, 0) + self.size: - return self.copy() - - if self.mode in ["LA", "RGBA"]: - im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) - im = im.reduce(factor, box) - return im.convert(self.mode) - - self.load() - - return self._new(self.im.reduce(factor, box)) - - def rotate( - self, - angle: float, - resample: Resampling = Resampling.NEAREST, - expand: int | bool = False, - center: tuple[float, float] | None = None, - translate: tuple[int, int] | None = None, - fillcolor: float | tuple[float, ...] | str | None = None, - ) -> Image: - """ - Returns a rotated copy of this image. This method returns a - copy of this image, rotated the given number of degrees counter - clockwise around its centre. - - :param angle: In degrees counter clockwise. - :param resample: An optional resampling filter. This can be - one of :py:data:`Resampling.NEAREST` (use nearest neighbour), - :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`Resampling.BICUBIC` (cubic spline - interpolation in a 4x4 environment). If omitted, or if the image has - mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. - See :ref:`concept-filters`. - :param expand: Optional expansion flag. If true, expands the output - image to make it large enough to hold the entire rotated image. - If false or omitted, make the output image the same size as the - input image. Note that the expand flag assumes rotation around - the center and no translation. - :param center: Optional center of rotation (a 2-tuple). Origin is - the upper left corner. Default is the center of the image. - :param translate: An optional post-rotate translation (a 2-tuple). - :param fillcolor: An optional color for area outside the rotated image. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - angle = angle % 360.0 - - # Fast paths regardless of filter, as long as we're not - # translating or changing the center. - if not (center or translate): - if angle == 0: - return self.copy() - if angle == 180: - return self.transpose(Transpose.ROTATE_180) - if angle in (90, 270) and (expand or self.width == self.height): - return self.transpose( - Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270 - ) - - # Calculate the affine matrix. Note that this is the reverse - # transformation (from destination image to source) because we - # want to interpolate the (discrete) destination pixel from - # the local area around the (floating) source pixel. - - # The matrix we actually want (note that it operates from the right): - # (1, 0, tx) (1, 0, cx) ( cos a, sin a, 0) (1, 0, -cx) - # (0, 1, ty) * (0, 1, cy) * (-sin a, cos a, 0) * (0, 1, -cy) - # (0, 0, 1) (0, 0, 1) ( 0, 0, 1) (0, 0, 1) - - # The reverse matrix is thus: - # (1, 0, cx) ( cos -a, sin -a, 0) (1, 0, -cx) (1, 0, -tx) - # (0, 1, cy) * (-sin -a, cos -a, 0) * (0, 1, -cy) * (0, 1, -ty) - # (0, 0, 1) ( 0, 0, 1) (0, 0, 1) (0, 0, 1) - - # In any case, the final translation may be updated at the end to - # compensate for the expand flag. - - w, h = self.size - - if translate is None: - post_trans = (0, 0) - else: - post_trans = translate - if center is None: - center = (w / 2, h / 2) - - angle = -math.radians(angle) - matrix = [ - round(math.cos(angle), 15), - round(math.sin(angle), 15), - 0.0, - round(-math.sin(angle), 15), - round(math.cos(angle), 15), - 0.0, - ] - - def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]: - (a, b, c, d, e, f) = matrix - return a * x + b * y + c, d * x + e * y + f - - matrix[2], matrix[5] = transform( - -center[0] - post_trans[0], -center[1] - post_trans[1], matrix - ) - matrix[2] += center[0] - matrix[5] += center[1] - - if expand: - # calculate output size - xx = [] - yy = [] - for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - transformed_x, transformed_y = transform(x, y, matrix) - xx.append(transformed_x) - yy.append(transformed_y) - nw = math.ceil(max(xx)) - math.floor(min(xx)) - nh = math.ceil(max(yy)) - math.floor(min(yy)) - - # We multiply a translation matrix from the right. Because of its - # special form, this is the same as taking the image of the - # translation vector as new translation vector. - matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix) - w, h = nw, nh - - return self.transform( - (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor - ) - - def save( - self, fp: StrOrBytesPath | IO[bytes], format: str | None = None, **params: Any - ) -> None: - """ - Saves this image under the given filename. If no format is - specified, the format to use is determined from the filename - extension, if possible. - - Keyword options can be used to provide additional instructions - to the writer. If a writer doesn't recognise an option, it is - silently ignored. The available options are described in the - :doc:`image format documentation - <../handbook/image-file-formats>` for each writer. - - You can use a file object instead of a filename. In this case, - you must always specify the format. The file object must - implement the ``seek``, ``tell``, and ``write`` - methods, and be opened in binary mode. - - :param fp: A filename (string), os.PathLike object or file object. - :param format: Optional format override. If omitted, the - format to use is determined from the filename extension. - If a file object was used instead of a filename, this - parameter should always be used. - :param params: Extra parameters to the image writer. These can also be - set on the image itself through ``encoderinfo``. This is useful when - saving multiple images:: - - # Saving XMP data to a single image - from PIL import Image - red = Image.new("RGB", (1, 1), "#f00") - red.save("out.mpo", xmp=b"test") - - # Saving XMP data to the second frame of an image - from PIL import Image - black = Image.new("RGB", (1, 1)) - red = Image.new("RGB", (1, 1), "#f00") - red.encoderinfo = {"xmp": b"test"} - black.save("out.mpo", save_all=True, append_images=[red]) - :returns: None - :exception ValueError: If the output format could not be determined - from the file name. Use the format option to solve this. - :exception OSError: If the file could not be written. The file - may have been created, and may contain partial data. - """ - - filename: str | bytes = "" - open_fp = False - if is_path(fp): - filename = os.fspath(fp) - open_fp = True - elif fp == sys.stdout: - try: - fp = sys.stdout.buffer - except AttributeError: - pass - if not filename and hasattr(fp, "name") and is_path(fp.name): - # only set the name for metadata purposes - filename = os.fspath(fp.name) - - preinit() - - filename_ext = os.path.splitext(filename)[1].lower() - ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext - - if not format: - if ext not in EXTENSION: - init() - try: - format = EXTENSION[ext] - except KeyError as e: - msg = f"unknown file extension: {ext}" - raise ValueError(msg) from e - - from . import ImageFile - - # may mutate self! - if isinstance(self, ImageFile.ImageFile) and os.path.abspath( - filename - ) == os.path.abspath(self.filename): - self._ensure_mutable() - else: - self.load() - - save_all = params.pop("save_all", None) - self._default_encoderinfo = params - encoderinfo = getattr(self, "encoderinfo", {}) - self._attach_default_encoderinfo(self) - self.encoderconfig: tuple[Any, ...] = () - - if format.upper() not in SAVE: - init() - if save_all or ( - save_all is None - and params.get("append_images") - and format.upper() in SAVE_ALL - ): - save_handler = SAVE_ALL[format.upper()] - else: - save_handler = SAVE[format.upper()] - - created = False - if open_fp: - created = not os.path.exists(filename) - if params.get("append", False): - # Open also for reading ("+"), because TIFF save_all - # writer needs to go back and edit the written data. - fp = builtins.open(filename, "r+b") - else: - fp = builtins.open(filename, "w+b") - else: - fp = cast(IO[bytes], fp) - - try: - save_handler(self, fp, filename) - except Exception: - if open_fp: - fp.close() - if created: - try: - os.remove(filename) - except PermissionError: - pass - raise - finally: - self.encoderinfo = encoderinfo - if open_fp: - fp.close() - - def _attach_default_encoderinfo(self, im: Image) -> dict[str, Any]: - encoderinfo = getattr(self, "encoderinfo", {}) - self.encoderinfo = {**im._default_encoderinfo, **encoderinfo} - return encoderinfo - - def seek(self, frame: int) -> None: - """ - Seeks to the given frame in this sequence file. If you seek - beyond the end of the sequence, the method raises an - ``EOFError`` exception. When a sequence file is opened, the - library automatically seeks to frame 0. - - See :py:meth:`~PIL.Image.Image.tell`. - - If defined, :attr:`~PIL.Image.Image.n_frames` refers to the - number of available frames. - - :param frame: Frame number, starting at 0. - :exception EOFError: If the call attempts to seek beyond the end - of the sequence. - """ - - # overridden by file handlers - if frame != 0: - msg = "no more images in file" - raise EOFError(msg) - - def show(self, title: str | None = None) -> None: - """ - Displays this image. This method is mainly intended for debugging purposes. - - This method calls :py:func:`PIL.ImageShow.show` internally. You can use - :py:func:`PIL.ImageShow.register` to override its default behaviour. - - The image is first saved to a temporary file. By default, it will be in - PNG format. - - On Unix, the image is then opened using the **xdg-open**, **display**, - **gm**, **eog** or **xv** utility, depending on which one can be found. - - On macOS, the image is opened with the native Preview application. - - On Windows, the image is opened with the standard PNG display utility. - - :param title: Optional title to use for the image window, where possible. - """ - - _show(self, title=title) - - def split(self) -> tuple[Image, ...]: - """ - Split this image into individual bands. This method returns a - tuple of individual image bands from an image. For example, - splitting an "RGB" image creates three new images each - containing a copy of one of the original bands (red, green, - blue). - - If you need only one band, :py:meth:`~PIL.Image.Image.getchannel` - method can be more convenient and faster. - - :returns: A tuple containing bands. - """ - - self.load() - if self.im.bands == 1: - return (self.copy(),) - return tuple(map(self._new, self.im.split())) - - def getchannel(self, channel: int | str) -> Image: - """ - Returns an image containing a single channel of the source image. - - :param channel: What channel to return. Could be index - (0 for "R" channel of "RGB") or channel name - ("A" for alpha channel of "RGBA"). - :returns: An image in "L" mode. - - .. versionadded:: 4.3.0 - """ - self.load() - - if isinstance(channel, str): - try: - channel = self.getbands().index(channel) - except ValueError as e: - msg = f'The image has no channel "{channel}"' - raise ValueError(msg) from e - - return self._new(self.im.getband(channel)) - - def tell(self) -> int: - """ - Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. - - If defined, :attr:`~PIL.Image.Image.n_frames` refers to the - number of available frames. - - :returns: Frame number, starting with 0. - """ - return 0 - - def thumbnail( - self, - size: tuple[float, float], - resample: Resampling = Resampling.BICUBIC, - reducing_gap: float | None = 2.0, - ) -> None: - """ - Make this image into a thumbnail. This method modifies the - image to contain a thumbnail version of itself, no larger than - the given size. This method calculates an appropriate thumbnail - size to preserve the aspect of the image, calls the - :py:meth:`~PIL.Image.Image.draft` method to configure the file reader - (where applicable), and finally resizes the image. - - Note that this function modifies the :py:class:`~PIL.Image.Image` - object in place. If you need to use the full resolution image as well, - apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original - image. - - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - :param resample: Optional resampling filter. This can be one - of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, - :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, - :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. - If omitted, it defaults to :py:data:`Resampling.BICUBIC`. - (was :py:data:`Resampling.NEAREST` prior to version 2.5.0). - See: :ref:`concept-filters`. - :param reducing_gap: Apply optimization by resizing the image - in two steps. First, reducing the image by integer times - using :py:meth:`~PIL.Image.Image.reduce` or - :py:meth:`~PIL.Image.Image.draft` for JPEG images. - Second, resizing using regular resampling. The last step - changes size no less than by ``reducing_gap`` times. - ``reducing_gap`` may be None (no first step is performed) - or should be greater than 1.0. The bigger ``reducing_gap``, - the closer the result to the fair resampling. - The smaller ``reducing_gap``, the faster resizing. - With ``reducing_gap`` greater or equal to 3.0, the result is - indistinguishable from fair resampling in most cases. - The default value is 2.0 (very close to fair resampling - while still being faster in many cases). - :returns: None - """ - - provided_size = tuple(map(math.floor, size)) - - def preserve_aspect_ratio() -> tuple[int, int] | None: - def round_aspect(number: float, key: Callable[[int], float]) -> int: - return max(min(math.floor(number), math.ceil(number), key=key), 1) - - x, y = provided_size - if x >= self.width and y >= self.height: - return None - - aspect = self.width / self.height - if x / y >= aspect: - x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) - else: - y = round_aspect( - x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) - ) - return x, y - - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - final_size = preserved_size - - box = None - if reducing_gap is not None: - res = self.draft( - None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) - ) - if res is not None: - box = res[1] - - if self.size != final_size: - im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) - - self.im = im.im - self._size = final_size - self._mode = self.im.mode - - self.readonly = 0 - - # FIXME: the different transform methods need further explanation - # instead of bloating the method docs, add a separate chapter. - def transform( - self, - size: tuple[int, int], - method: Transform | ImageTransformHandler | SupportsGetData, - data: Sequence[Any] | None = None, - resample: int = Resampling.NEAREST, - fill: int = 1, - fillcolor: float | tuple[float, ...] | str | None = None, - ) -> Image: - """ - Transforms this image. This method creates a new image with the - given size, and the same mode as the original, and copies data - to the new image using the given transform. - - :param size: The output size in pixels, as a 2-tuple: - (width, height). - :param method: The transformation method. This is one of - :py:data:`Transform.EXTENT` (cut out a rectangular subregion), - :py:data:`Transform.AFFINE` (affine transform), - :py:data:`Transform.PERSPECTIVE` (perspective transform), - :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or - :py:data:`Transform.MESH` (map a number of source quadrilaterals - in one operation). - - It may also be an :py:class:`~PIL.Image.ImageTransformHandler` - object:: - - class Example(Image.ImageTransformHandler): - def transform(self, size, data, resample, fill=1): - # Return result - - Implementations of :py:class:`~PIL.Image.ImageTransformHandler` - for some of the :py:class:`Transform` methods are provided - in :py:mod:`~PIL.ImageTransform`. - - It may also be an object with a ``method.getdata`` method - that returns a tuple supplying new ``method`` and ``data`` values:: - - class Example: - def getdata(self): - method = Image.Transform.EXTENT - data = (0, 0, 100, 100) - return method, data - :param data: Extra data to the transformation method. - :param resample: Optional resampling filter. It can be one of - :py:data:`Resampling.NEAREST` (use nearest neighbour), - :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`Resampling.BICUBIC` (cubic spline - interpolation in a 4x4 environment). If omitted, or if the image - has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. - See: :ref:`concept-filters`. - :param fill: If ``method`` is an - :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of - the arguments passed to it. Otherwise, it is unused. - :param fillcolor: Optional fill color for the area outside the - transform in the output image. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST: - return ( - self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) - .transform(size, method, data, resample, fill, fillcolor) - .convert(self.mode) - ) - - if isinstance(method, ImageTransformHandler): - return method.transform(size, self, resample=resample, fill=fill) - - if hasattr(method, "getdata"): - # compatibility w. old-style transform objects - method, data = method.getdata() - - if data is None: - msg = "missing method data" - raise ValueError(msg) - - im = new(self.mode, size, fillcolor) - if self.mode == "P" and self.palette: - im.palette = self.palette.copy() - im.info = self.info.copy() - if method == Transform.MESH: - # list of quads - for box, quad in data: - im.__transformer( - box, self, Transform.QUAD, quad, resample, fillcolor is None - ) - else: - im.__transformer( - (0, 0) + size, self, method, data, resample, fillcolor is None - ) - - return im - - def __transformer( - self, - box: tuple[int, int, int, int], - image: Image, - method: Transform, - data: Sequence[float], - resample: int = Resampling.NEAREST, - fill: bool = True, - ) -> None: - w = box[2] - box[0] - h = box[3] - box[1] - - if method == Transform.AFFINE: - data = data[:6] - - elif method == Transform.EXTENT: - # convert extent to an affine transform - x0, y0, x1, y1 = data - xs = (x1 - x0) / w - ys = (y1 - y0) / h - method = Transform.AFFINE - data = (xs, 0, x0, 0, ys, y0) - - elif method == Transform.PERSPECTIVE: - data = data[:8] - - elif method == Transform.QUAD: - # quadrilateral warp. data specifies the four corners - # given as NW, SW, SE, and NE. - nw = data[:2] - sw = data[2:4] - se = data[4:6] - ne = data[6:8] - x0, y0 = nw - As = 1.0 / w - At = 1.0 / h - data = ( - x0, - (ne[0] - x0) * As, - (sw[0] - x0) * At, - (se[0] - sw[0] - ne[0] + x0) * As * At, - y0, - (ne[1] - y0) * As, - (sw[1] - y0) * At, - (se[1] - sw[1] - ne[1] + y0) * As * At, - ) - - else: - msg = "unknown transformation method" - raise ValueError(msg) - - if resample not in ( - Resampling.NEAREST, - Resampling.BILINEAR, - Resampling.BICUBIC, - ): - if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): - unusable: dict[int, str] = { - Resampling.BOX: "Image.Resampling.BOX", - Resampling.HAMMING: "Image.Resampling.HAMMING", - Resampling.LANCZOS: "Image.Resampling.LANCZOS", - } - msg = unusable[resample] + f" ({resample}) cannot be used." - else: - msg = f"Unknown resampling filter ({resample})." - - filters = [ - f"{filter[1]} ({filter[0]})" - for filter in ( - (Resampling.NEAREST, "Image.Resampling.NEAREST"), - (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), - (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), - ) - ] - msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" - raise ValueError(msg) - - image.load() - - self.load() - - if image.mode in ("1", "P"): - resample = Resampling.NEAREST - - self.im.transform(box, image.im, method, data, resample, fill) - - def transpose(self, method: Transpose) -> Image: - """ - Transpose image (flip or rotate in 90 degree steps) - - :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`, - :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`, - :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`, - :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`. - :returns: Returns a flipped or rotated copy of this image. - """ - - self.load() - return self._new(self.im.transpose(method)) - - def effect_spread(self, distance: int) -> Image: - """ - Randomly spread pixels in an image. - - :param distance: Distance to spread pixels. - """ - self.load() - return self._new(self.im.effect_spread(distance)) - - def toqimage(self) -> ImageQt.ImageQt: - """Returns a QImage copy of this image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.toqimage(self) - - def toqpixmap(self) -> ImageQt.QPixmap: - """Returns a QPixmap copy of this image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.toqpixmap(self) - - -# -------------------------------------------------------------------- -# Abstract handlers. - - -class ImagePointHandler(abc.ABC): - """ - Used as a mixin by point transforms - (for use with :py:meth:`~PIL.Image.Image.point`) - """ - - @abc.abstractmethod - def point(self, im: Image) -> Image: - pass - - -class ImageTransformHandler(abc.ABC): - """ - Used as a mixin by geometry transforms - (for use with :py:meth:`~PIL.Image.Image.transform`) - """ - - @abc.abstractmethod - def transform( - self, - size: tuple[int, int], - image: Image, - **options: Any, - ) -> Image: - pass - - -# -------------------------------------------------------------------- -# Factories - - -def _check_size(size: Any) -> None: - """ - Common check to enforce type and sanity check on size tuples - - :param size: Should be a 2 tuple of (width, height) - :returns: None, or raises a ValueError - """ - - if not isinstance(size, (list, tuple)): - msg = "Size must be a list or tuple" - raise ValueError(msg) - if len(size) != 2: - msg = "Size must be a sequence of length 2" - raise ValueError(msg) - if size[0] < 0 or size[1] < 0: - msg = "Width and height must be >= 0" - raise ValueError(msg) - - -def new( - mode: str, - size: tuple[int, int] | list[int], - color: float | tuple[float, ...] | str | None = 0, -) -> Image: - """ - Creates a new image with the given mode and size. - - :param mode: The mode to use for the new image. See: - :ref:`concept-modes`. - :param size: A 2-tuple, containing (width, height) in pixels. - :param color: What color to use for the image. Default is black. - If given, this should be a single integer or floating point value - for single-band modes, and a tuple for multi-band modes (one value - per band). When creating RGB or HSV images, you can also use color - strings as supported by the ImageColor module. If the color is - None, the image is not initialised. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - - _check_size(size) - - if color is None: - # don't initialize - return Image()._new(core.new(mode, size)) - - if isinstance(color, str): - # css3-style specifier - - from . import ImageColor - - color = ImageColor.getcolor(color, mode) - - im = Image() - if ( - mode == "P" - and isinstance(color, (list, tuple)) - and all(isinstance(i, int) for i in color) - ): - color_ints: tuple[int, ...] = cast(tuple[int, ...], tuple(color)) - if len(color_ints) == 3 or len(color_ints) == 4: - # RGB or RGBA value for a P image - from . import ImagePalette - - im.palette = ImagePalette.ImagePalette() - color = im.palette.getcolor(color_ints) - return im._new(core.fill(mode, size, color)) - - -def frombytes( - mode: str, - size: tuple[int, int], - data: bytes | bytearray | SupportsArrayInterface, - decoder_name: str = "raw", - *args: Any, -) -> Image: - """ - Creates a copy of an image memory from pixel data in a buffer. - - In its simplest form, this function takes three arguments - (mode, size, and unpacked pixel data). - - You can also use any pixel decoder supported by PIL. For more - information on available decoders, see the section - :ref:`Writing Your Own File Codec `. - - Note that this function decodes pixel data only, not entire images. - If you have an entire image in a string, wrap it in a - :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load - it. - - :param mode: The image mode. See: :ref:`concept-modes`. - :param size: The image size. - :param data: A byte buffer containing raw data for the given mode. - :param decoder_name: What decoder to use. - :param args: Additional parameters for the given decoder. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - _check_size(size) - - im = new(mode, size) - if im.width != 0 and im.height != 0: - decoder_args: Any = args - if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple): - # may pass tuple instead of argument list - decoder_args = decoder_args[0] - - if decoder_name == "raw" and decoder_args == (): - decoder_args = mode - - im.frombytes(data, decoder_name, decoder_args) - return im - - -def frombuffer( - mode: str, - size: tuple[int, int], - data: bytes | SupportsArrayInterface, - decoder_name: str = "raw", - *args: Any, -) -> Image: - """ - Creates an image memory referencing pixel data in a byte buffer. - - This function is similar to :py:func:`~PIL.Image.frombytes`, but uses data - in the byte buffer, where possible. This means that changes to the - original buffer object are reflected in this image). Not all modes can - share memory; supported modes include "L", "RGBX", "RGBA", and "CMYK". - - Note that this function decodes pixel data only, not entire images. - If you have an entire image file in a string, wrap it in a - :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it. - - The default parameters used for the "raw" decoder differs from that used for - :py:func:`~PIL.Image.frombytes`. This is a bug, and will probably be fixed in a - future release. The current release issues a warning if you do this; to disable - the warning, you should provide the full set of parameters. See below for details. - - :param mode: The image mode. See: :ref:`concept-modes`. - :param size: The image size. - :param data: A bytes or other buffer object containing raw - data for the given mode. - :param decoder_name: What decoder to use. - :param args: Additional parameters for the given decoder. For the - default encoder ("raw"), it's recommended that you provide the - full set of parameters:: - - frombuffer(mode, size, data, "raw", mode, 0, 1) - - :returns: An :py:class:`~PIL.Image.Image` object. - - .. versionadded:: 1.1.4 - """ - - _check_size(size) - - # may pass tuple instead of argument list - if len(args) == 1 and isinstance(args[0], tuple): - args = args[0] - - if decoder_name == "raw": - if args == (): - args = mode, 0, 1 - if args[0] in _MAPMODES: - im = new(mode, (0, 0)) - im = im._new(core.map_buffer(data, size, decoder_name, 0, args)) - if mode == "P": - from . import ImagePalette - - im.palette = ImagePalette.ImagePalette("RGB", im.im.getpalette("RGB")) - im.readonly = 1 - return im - - return frombytes(mode, size, data, decoder_name, args) - - -class SupportsArrayInterface(Protocol): - """ - An object that has an ``__array_interface__`` dictionary. - """ - - @property - def __array_interface__(self) -> dict[str, Any]: - raise NotImplementedError() - - -class SupportsArrowArrayInterface(Protocol): - """ - An object that has an ``__arrow_c_array__`` method corresponding to the arrow c - data interface. - """ - - def __arrow_c_array__( - self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037 - ) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037 - raise NotImplementedError() - - -def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: - """ - Creates an image memory from an object exporting the array interface - (using the buffer protocol):: - - from PIL import Image - import numpy as np - a = np.zeros((5, 5)) - im = Image.fromarray(a) - - If ``obj`` is not contiguous, then the ``tobytes`` method is called - and :py:func:`~PIL.Image.frombuffer` is used. - - In the case of NumPy, be aware that Pillow modes do not always correspond - to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, - 32-bit signed integer pixels, and 32-bit floating point pixels. - - Pillow images can also be converted to arrays:: - - from PIL import Image - import numpy as np - im = Image.open("hopper.jpg") - a = np.asarray(im) - - When converting Pillow images to arrays however, only pixel values are - transferred. This means that P and PA mode images will lose their palette. - - :param obj: Object with array interface - :param mode: Optional mode to use when reading ``obj``. Will be determined from - type if ``None``. Deprecated. - - This will not be used to convert the data after reading, but will be used to - change how the data is read:: - - from PIL import Image - import numpy as np - a = np.full((1, 1), 300) - im = Image.fromarray(a, mode="L") - im.getpixel((0, 0)) # 44 - im = Image.fromarray(a, mode="RGB") - im.getpixel((0, 0)) # (44, 1, 0) - - See: :ref:`concept-modes` for general information about modes. - :returns: An image object. - - .. versionadded:: 1.1.6 - """ - arr = obj.__array_interface__ - shape = arr["shape"] - ndim = len(shape) - strides = arr.get("strides", None) - if mode is None: - try: - typekey = (1, 1) + shape[2:], arr["typestr"] - except KeyError as e: - msg = "Cannot handle this data type" - raise TypeError(msg) from e - try: - mode, rawmode = _fromarray_typemap[typekey] - except KeyError as e: - typekey_shape, typestr = typekey - msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" - raise TypeError(msg) from e - else: - deprecate("'mode' parameter", 13) - rawmode = mode - if mode in ["1", "L", "I", "P", "F"]: - ndmax = 2 - elif mode == "RGB": - ndmax = 3 - else: - ndmax = 4 - if ndim > ndmax: - msg = f"Too many dimensions: {ndim} > {ndmax}." - raise ValueError(msg) - - size = 1 if ndim == 1 else shape[1], shape[0] - if strides is not None: - if hasattr(obj, "tobytes"): - obj = obj.tobytes() - elif hasattr(obj, "tostring"): - obj = obj.tostring() - else: - msg = "'strides' requires either tobytes() or tostring()" - raise ValueError(msg) - - return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) - - -def fromarrow( - obj: SupportsArrowArrayInterface, mode: str, size: tuple[int, int] -) -> Image: - """Creates an image with zero-copy shared memory from an object exporting - the arrow_c_array interface protocol:: - - from PIL import Image - import pyarrow as pa - arr = pa.array([0]*(5*5*4), type=pa.uint8()) - im = Image.fromarrow(arr, 'RGBA', (5, 5)) - - If the data representation of the ``obj`` is not compatible with - Pillow internal storage, a ValueError is raised. - - Pillow images can also be converted to Arrow objects:: - - from PIL import Image - import pyarrow as pa - im = Image.open('hopper.jpg') - arr = pa.array(im) - - As with array support, when converting Pillow images to arrays, - only pixel values are transferred. This means that P and PA mode - images will lose their palette. - - :param obj: Object with an arrow_c_array interface - :param mode: Image mode. - :param size: Image size. This must match the storage of the arrow object. - :returns: An Image object - - Note that according to the Arrow spec, both the producer and the - consumer should consider the exported array to be immutable, as - unsynchronized updates will potentially cause inconsistent data. - - See: :ref:`arrow-support` for more detailed information - - .. versionadded:: 11.2.1 - - """ - if not hasattr(obj, "__arrow_c_array__"): - msg = "arrow_c_array interface not found" - raise ValueError(msg) - - (schema_capsule, array_capsule) = obj.__arrow_c_array__() - _im = core.new_arrow(mode, size, schema_capsule, array_capsule) - if _im: - return Image()._new(_im) - - msg = "new_arrow returned None without an exception" - raise ValueError(msg) - - -def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: - """Creates an image instance from a QImage image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.fromqimage(im) - - -def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile: - """Creates an image instance from a QPixmap image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.fromqpixmap(im) - - -_fromarray_typemap = { - # (shape, typestr) => mode, rawmode - # first two members of shape are set to one - ((1, 1), "|b1"): ("1", "1;8"), - ((1, 1), "|u1"): ("L", "L"), - ((1, 1), "|i1"): ("I", "I;8"), - ((1, 1), "u2"): ("I", "I;16B"), - ((1, 1), "i2"): ("I", "I;16BS"), - ((1, 1), "u4"): ("I", "I;32B"), - ((1, 1), "i4"): ("I", "I;32BS"), - ((1, 1), "f4"): ("F", "F;32BF"), - ((1, 1), "f8"): ("F", "F;64BF"), - ((1, 1, 2), "|u1"): ("LA", "LA"), - ((1, 1, 3), "|u1"): ("RGB", "RGB"), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), - # shortcuts: - ((1, 1), f"{_ENDIAN}i4"): ("I", "I"), - ((1, 1), f"{_ENDIAN}f4"): ("F", "F"), -} - - -def _decompression_bomb_check(size: tuple[int, int]) -> None: - if MAX_IMAGE_PIXELS is None: - return - - pixels = max(1, size[0]) * max(1, size[1]) - - if pixels > 2 * MAX_IMAGE_PIXELS: - msg = ( - f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " - "pixels, could be decompression bomb DOS attack." - ) - raise DecompressionBombError(msg) - - if pixels > MAX_IMAGE_PIXELS: - warnings.warn( - f"Image size ({pixels} pixels) exceeds limit of {MAX_IMAGE_PIXELS} pixels, " - "could be decompression bomb DOS attack.", - DecompressionBombWarning, - ) - - -def open( - fp: StrOrBytesPath | IO[bytes], - mode: Literal["r"] = "r", - formats: list[str] | tuple[str, ...] | None = None, -) -> ImageFile.ImageFile: - """ - Opens and identifies the given image file. - - This is a lazy operation; this function identifies the file, but - the file remains open and the actual image data is not read from - the file until you try to process the data (or call the - :py:meth:`~PIL.Image.Image.load` method). See - :py:func:`~PIL.Image.new`. See :ref:`file-handling`. - - :param fp: A filename (string), os.PathLike object or a file object. - The file object must implement ``file.read``, - ``file.seek``, and ``file.tell`` methods, - and be opened in binary mode. The file object will also seek to zero - before reading. - :param mode: The mode. If given, this argument must be "r". - :param formats: A list or tuple of formats to attempt to load the file in. - This can be used to restrict the set of formats checked. - Pass ``None`` to try all supported formats. You can print the set of - available formats by running ``python3 -m PIL`` or using - the :py:func:`PIL.features.pilinfo` function. - :returns: An :py:class:`~PIL.Image.Image` object. - :exception FileNotFoundError: If the file cannot be found. - :exception PIL.UnidentifiedImageError: If the image cannot be opened and - identified. - :exception ValueError: If the ``mode`` is not "r", or if a ``StringIO`` - instance is used for ``fp``. - :exception TypeError: If ``formats`` is not ``None``, a list or a tuple. - """ - - if mode != "r": - msg = f"bad mode {repr(mode)}" # type: ignore[unreachable] - raise ValueError(msg) - elif isinstance(fp, io.StringIO): - msg = ( # type: ignore[unreachable] - "StringIO cannot be used to open an image. " - "Binary data must be used instead." - ) - raise ValueError(msg) - - if formats is None: - formats = ID - elif not isinstance(formats, (list, tuple)): - msg = "formats must be a list or tuple" # type: ignore[unreachable] - raise TypeError(msg) - - exclusive_fp = False - filename: str | bytes = "" - if is_path(fp): - filename = os.fspath(fp) - fp = builtins.open(filename, "rb") - exclusive_fp = True - else: - fp = cast(IO[bytes], fp) - - try: - fp.seek(0) - except (AttributeError, io.UnsupportedOperation): - fp = io.BytesIO(fp.read()) - exclusive_fp = True - - prefix = fp.read(16) - - preinit() - - warning_messages: list[str] = [] - - def _open_core( - fp: IO[bytes], - filename: str | bytes, - prefix: bytes, - formats: list[str] | tuple[str, ...], - ) -> ImageFile.ImageFile | None: - for i in formats: - i = i.upper() - if i not in OPEN: - init() - try: - factory, accept = OPEN[i] - result = not accept or accept(prefix) - if isinstance(result, str): - warning_messages.append(result) - elif result: - fp.seek(0) - im = factory(fp, filename) - _decompression_bomb_check(im.size) - return im - except (SyntaxError, IndexError, TypeError, struct.error) as e: - if WARN_POSSIBLE_FORMATS: - warning_messages.append(i + " opening failed. " + str(e)) - except BaseException: - if exclusive_fp: - fp.close() - raise - return None - - im = _open_core(fp, filename, prefix, formats) - - if im is None and formats is ID: - checked_formats = ID.copy() - if init(): - im = _open_core( - fp, - filename, - prefix, - tuple(format for format in formats if format not in checked_formats), - ) - - if im: - im._exclusive_fp = exclusive_fp - return im - - if exclusive_fp: - fp.close() - for message in warning_messages: - warnings.warn(message) - msg = "cannot identify image file %r" % (filename if filename else fp) - raise UnidentifiedImageError(msg) - - -# -# Image processing. - - -def alpha_composite(im1: Image, im2: Image) -> Image: - """ - Alpha composite im2 over im1. - - :param im1: The first image. Must have mode RGBA. - :param im2: The second image. Must have mode RGBA, and the same size as - the first image. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - im1.load() - im2.load() - return im1._new(core.alpha_composite(im1.im, im2.im)) - - -def blend(im1: Image, im2: Image, alpha: float) -> Image: - """ - Creates a new image by interpolating between two input images, using - a constant alpha:: - - out = image1 * (1.0 - alpha) + image2 * alpha - - :param im1: The first image. - :param im2: The second image. Must have the same mode and size as - the first image. - :param alpha: The interpolation alpha factor. If alpha is 0.0, a - copy of the first image is returned. If alpha is 1.0, a copy of - the second image is returned. There are no restrictions on the - alpha value. If necessary, the result is clipped to fit into - the allowed output range. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - im1.load() - im2.load() - return im1._new(core.blend(im1.im, im2.im, alpha)) - - -def composite(image1: Image, image2: Image, mask: Image) -> Image: - """ - Create composite image by blending images using a transparency mask. - - :param image1: The first image. - :param image2: The second image. Must have the same mode and - size as the first image. - :param mask: A mask image. This image can have mode - "1", "L", or "RGBA", and must have the same size as the - other two images. - """ - - image = image2.copy() - image.paste(image1, None, mask) - return image - - -def eval(image: Image, *args: Callable[[int], float]) -> Image: - """ - Applies the function (which should take one argument) to each pixel - in the given image. If the image has more than one band, the same - function is applied to each band. Note that the function is - evaluated once for each possible pixel value, so you cannot use - random components or other generators. - - :param image: The input image. - :param function: A function object, taking one integer argument. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - return image.point(args[0]) - - -def merge(mode: str, bands: Sequence[Image]) -> Image: - """ - Merge a set of single band images into a new multiband image. - - :param mode: The mode to use for the output image. See: - :ref:`concept-modes`. - :param bands: A sequence containing one single-band image for - each band in the output image. All bands must have the - same size. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if getmodebands(mode) != len(bands) or "*" in mode: - msg = "wrong number of bands" - raise ValueError(msg) - for band in bands[1:]: - if band.mode != getmodetype(mode): - msg = "mode mismatch" - raise ValueError(msg) - if band.size != bands[0].size: - msg = "size mismatch" - raise ValueError(msg) - for band in bands: - band.load() - return bands[0]._new(core.merge(mode, *[b.im for b in bands])) - - -# -------------------------------------------------------------------- -# Plugin registry - - -def register_open( - id: str, - factory: ( - Callable[[IO[bytes], str | bytes], ImageFile.ImageFile] - | type[ImageFile.ImageFile] - ), - accept: Callable[[bytes], bool | str] | None = None, -) -> None: - """ - Register an image file plugin. This function should not be used - in application code. - - :param id: An image format identifier. - :param factory: An image file factory method. - :param accept: An optional function that can be used to quickly - reject images having another format. - """ - id = id.upper() - if id not in ID: - ID.append(id) - OPEN[id] = factory, accept - - -def register_mime(id: str, mimetype: str) -> None: - """ - Registers an image MIME type by populating ``Image.MIME``. This function - should not be used in application code. - - ``Image.MIME`` provides a mapping from image format identifiers to mime - formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can - provide a different result for specific images. - - :param id: An image format identifier. - :param mimetype: The image MIME type for this format. - """ - MIME[id.upper()] = mimetype - - -def register_save( - id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] -) -> None: - """ - Registers an image save function. This function should not be - used in application code. - - :param id: An image format identifier. - :param driver: A function to save images in this format. - """ - SAVE[id.upper()] = driver - - -def register_save_all( - id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] -) -> None: - """ - Registers an image function to save all the frames - of a multiframe format. This function should not be - used in application code. - - :param id: An image format identifier. - :param driver: A function to save images in this format. - """ - SAVE_ALL[id.upper()] = driver - - -def register_extension(id: str, extension: str) -> None: - """ - Registers an image extension. This function should not be - used in application code. - - :param id: An image format identifier. - :param extension: An extension used for this format. - """ - EXTENSION[extension.lower()] = id.upper() - - -def register_extensions(id: str, extensions: list[str]) -> None: - """ - Registers image extensions. This function should not be - used in application code. - - :param id: An image format identifier. - :param extensions: A list of extensions used for this format. - """ - for extension in extensions: - register_extension(id, extension) - - -def registered_extensions() -> dict[str, str]: - """ - Returns a dictionary containing all file extensions belonging - to registered plugins - """ - init() - return EXTENSION - - -def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: - """ - Registers an image decoder. This function should not be - used in application code. - - :param name: The name of the decoder - :param decoder: An ImageFile.PyDecoder object - - .. versionadded:: 4.1.0 - """ - DECODERS[name] = decoder - - -def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: - """ - Registers an image encoder. This function should not be - used in application code. - - :param name: The name of the encoder - :param encoder: An ImageFile.PyEncoder object - - .. versionadded:: 4.1.0 - """ - ENCODERS[name] = encoder - - -# -------------------------------------------------------------------- -# Simple display support. - - -def _show(image: Image, **options: Any) -> None: - from . import ImageShow - - ImageShow.show(image, **options) - - -# -------------------------------------------------------------------- -# Effects - - -def effect_mandelbrot( - size: tuple[int, int], extent: tuple[float, float, float, float], quality: int -) -> Image: - """ - Generate a Mandelbrot set covering the given extent. - - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - :param extent: The extent to cover, as a 4-tuple: - (x0, y0, x1, y1). - :param quality: Quality. - """ - return Image()._new(core.effect_mandelbrot(size, extent, quality)) - - -def effect_noise(size: tuple[int, int], sigma: float) -> Image: - """ - Generate Gaussian noise centered around 128. - - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - :param sigma: Standard deviation of noise. - """ - return Image()._new(core.effect_noise(size, sigma)) - - -def linear_gradient(mode: str) -> Image: - """ - Generate 256x256 linear gradient from black to white, top to bottom. - - :param mode: Input mode. - """ - return Image()._new(core.linear_gradient(mode)) - - -def radial_gradient(mode: str) -> Image: - """ - Generate 256x256 radial gradient from black to white, centre to edge. - - :param mode: Input mode. - """ - return Image()._new(core.radial_gradient(mode)) - - -# -------------------------------------------------------------------- -# Resources - - -def _apply_env_variables(env: dict[str, str] | None = None) -> None: - env_dict = env if env is not None else os.environ - - for var_name, setter in [ - ("PILLOW_ALIGNMENT", core.set_alignment), - ("PILLOW_BLOCK_SIZE", core.set_block_size), - ("PILLOW_BLOCKS_MAX", core.set_blocks_max), - ]: - if var_name not in env_dict: - continue - - var = env_dict[var_name].lower() - - units = 1 - for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: - if var.endswith(postfix): - units = mul - var = var[: -len(postfix)] - - try: - var_int = int(var) * units - except ValueError: - warnings.warn(f"{var_name} is not int") - continue - - try: - setter(var_int) - except ValueError as e: - warnings.warn(f"{var_name}: {e}") - - -_apply_env_variables() -atexit.register(core.clear_cache) - - -if TYPE_CHECKING: - _ExifBase = MutableMapping[int, Any] -else: - _ExifBase = MutableMapping - - -class Exif(_ExifBase): - """ - This class provides read and write access to EXIF image data:: - - from PIL import Image - im = Image.open("exif.png") - exif = im.getexif() # Returns an instance of this class - - Information can be read and written, iterated over or deleted:: - - print(exif[274]) # 1 - exif[274] = 2 - for k, v in exif.items(): - print("Tag", k, "Value", v) # Tag 274 Value 2 - del exif[274] - - To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` - returns a dictionary:: - - from PIL import ExifTags - im = Image.open("exif_gps.jpg") - exif = im.getexif() - gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) - print(gps_ifd) - - Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``, - ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. - - :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: - - print(exif[ExifTags.Base.Software]) # PIL - print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 - """ - - endian: str | None = None - bigtiff = False - _loaded = False - - def __init__(self) -> None: - self._data: dict[int, Any] = {} - self._hidden_data: dict[int, Any] = {} - self._ifds: dict[int, dict[int, Any]] = {} - self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None - self._loaded_exif: bytes | None = None - - def _fixup(self, value: Any) -> Any: - try: - if len(value) == 1 and isinstance(value, tuple): - return value[0] - except Exception: - pass - return value - - def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]: - # Helper function - # returns a dict with any single item tuples/lists as individual values - return {k: self._fixup(v) for k, v in src_dict.items()} - - def _get_ifd_dict( - self, offset: int, group: int | None = None - ) -> dict[int, Any] | None: - try: - # an offset pointer to the location of the nested embedded IFD. - # It should be a long, but may be corrupted. - self.fp.seek(offset) - except (KeyError, TypeError): - return None - else: - from . import TiffImagePlugin - - info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) - info.load(self.fp) - return self._fixup_dict(dict(info)) - - def _get_head(self) -> bytes: - version = b"\x2b" if self.bigtiff else b"\x2a" - if self.endian == "<": - head = b"II" + version + b"\x00" + o32le(8) - else: - head = b"MM\x00" + version + o32be(8) - if self.bigtiff: - head += o32le(8) if self.endian == "<" else o32be(8) - head += b"\x00\x00\x00\x00" - return head - - def load(self, data: bytes) -> None: - # Extract EXIF information. This is highly experimental, - # and is likely to be replaced with something better in a future - # version. - - # The EXIF record consists of a TIFF file embedded in a JPEG - # application marker (!). - if data == self._loaded_exif: - return - self._loaded_exif = data - self._data.clear() - self._hidden_data.clear() - self._ifds.clear() - while data and data.startswith(b"Exif\x00\x00"): - data = data[6:] - if not data: - self._info = None - return - - self.fp: IO[bytes] = io.BytesIO(data) - self.head = self.fp.read(8) - # process dictionary - from . import TiffImagePlugin - - self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) - self.endian = self._info._endian - self.fp.seek(self._info.next) - self._info.load(self.fp) - - def load_from_fp(self, fp: IO[bytes], offset: int | None = None) -> None: - self._loaded_exif = None - self._data.clear() - self._hidden_data.clear() - self._ifds.clear() - - # process dictionary - from . import TiffImagePlugin - - self.fp = fp - if offset is not None: - self.head = self._get_head() - else: - self.head = self.fp.read(8) - self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) - if self.endian is None: - self.endian = self._info._endian - if offset is None: - offset = self._info.next - self.fp.tell() - self.fp.seek(offset) - self._info.load(self.fp) - - def _get_merged_dict(self) -> dict[int, Any]: - merged_dict = dict(self) - - # get EXIF extension - if ExifTags.IFD.Exif in self: - ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif], ExifTags.IFD.Exif) - if ifd: - merged_dict.update(ifd) - - # GPS - if ExifTags.IFD.GPSInfo in self: - merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( - self[ExifTags.IFD.GPSInfo], ExifTags.IFD.GPSInfo - ) - - return merged_dict - - def tobytes(self, offset: int = 8) -> bytes: - from . import TiffImagePlugin - - head = self._get_head() - ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) - for tag, ifd_dict in self._ifds.items(): - if tag not in self: - ifd[tag] = ifd_dict - for tag, value in self.items(): - if tag in [ - ExifTags.IFD.Exif, - ExifTags.IFD.GPSInfo, - ] and not isinstance(value, dict): - value = self.get_ifd(tag) - if ( - tag == ExifTags.IFD.Exif - and ExifTags.IFD.Interop in value - and not isinstance(value[ExifTags.IFD.Interop], dict) - ): - value = value.copy() - value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop) - ifd[tag] = value - return b"Exif\x00\x00" + head + ifd.tobytes(offset) - - def get_ifd(self, tag: int) -> dict[int, Any]: - if tag not in self._ifds: - if tag == ExifTags.IFD.IFD1: - if self._info is not None and self._info.next != 0: - ifd = self._get_ifd_dict(self._info.next) - if ifd is not None: - self._ifds[tag] = ifd - elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: - offset = self._hidden_data.get(tag, self.get(tag)) - if offset is not None: - ifd = self._get_ifd_dict(offset, tag) - if ifd is not None: - self._ifds[tag] = ifd - elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]: - if ExifTags.IFD.Exif not in self._ifds: - self.get_ifd(ExifTags.IFD.Exif) - tag_data = self._ifds[ExifTags.IFD.Exif][tag] - if tag == ExifTags.IFD.MakerNote: - from .TiffImagePlugin import ImageFileDirectory_v2 - - if tag_data.startswith(b"FUJIFILM"): - ifd_offset = i32le(tag_data, 8) - ifd_data = tag_data[ifd_offset:] - - makernote = {} - for i in range(struct.unpack(" 4: - (offset,) = struct.unpack("H", tag_data[:2])[0]): - ifd_tag, typ, count, data = struct.unpack( - ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] - ) - if ifd_tag == 0x1101: - # CameraInfo - (offset,) = struct.unpack(">L", data) - self.fp.seek(offset) - - camerainfo: dict[str, int | bytes] = { - "ModelID": self.fp.read(4) - } - - self.fp.read(4) - # Seconds since 2000 - camerainfo["TimeStamp"] = i32le(self.fp.read(12)) - - self.fp.read(4) - camerainfo["InternalSerialNumber"] = self.fp.read(4) - - self.fp.read(12) - parallax = self.fp.read(4) - handler = ImageFileDirectory_v2._load_dispatch[ - TiffTags.FLOAT - ][1] - camerainfo["Parallax"] = handler( - ImageFileDirectory_v2(), parallax, False - )[0] - - self.fp.read(4) - camerainfo["Category"] = self.fp.read(2) - - makernote = {0x1101: camerainfo} - self._ifds[tag] = makernote - else: - # Interop - ifd = self._get_ifd_dict(tag_data, tag) - if ifd is not None: - self._ifds[tag] = ifd - ifd = self._ifds.setdefault(tag, {}) - if tag == ExifTags.IFD.Exif and self._hidden_data: - ifd = { - k: v - for (k, v) in ifd.items() - if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote) - } - return ifd - - def hide_offsets(self) -> None: - for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo): - if tag in self: - self._hidden_data[tag] = self[tag] - del self[tag] - - def __str__(self) -> str: - if self._info is not None: - # Load all keys into self._data - for tag in self._info: - self[tag] - - return str(self._data) - - def __len__(self) -> int: - keys = set(self._data) - if self._info is not None: - keys.update(self._info) - return len(keys) - - def __getitem__(self, tag: int) -> Any: - if self._info is not None and tag not in self._data and tag in self._info: - self._data[tag] = self._fixup(self._info[tag]) - del self._info[tag] - return self._data[tag] - - def __contains__(self, tag: object) -> bool: - return tag in self._data or (self._info is not None and tag in self._info) - - def __setitem__(self, tag: int, value: Any) -> None: - if self._info is not None and tag in self._info: - del self._info[tag] - self._data[tag] = value - - def __delitem__(self, tag: int) -> None: - if self._info is not None and tag in self._info: - del self._info[tag] - else: - del self._data[tag] - - def __iter__(self) -> Iterator[int]: - keys = set(self._data) - if self._info is not None: - keys.update(self._info) - return iter(keys) diff --git a/python_modules/PIL/ImageChops.py b/python_modules/PIL/ImageChops.py deleted file mode 100644 index 29a5c995f..000000000 --- a/python_modules/PIL/ImageChops.py +++ /dev/null @@ -1,311 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard channel operations -# -# History: -# 1996-03-24 fl Created -# 1996-08-13 fl Added logical operations (for "1" images) -# 2000-10-12 fl Added offset method (from Image.py) -# -# Copyright (c) 1997-2000 by Secret Labs AB -# Copyright (c) 1996-2000 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -from __future__ import annotations - -from . import Image - - -def constant(image: Image.Image, value: int) -> Image.Image: - """Fill a channel with a given gray level. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return Image.new("L", image.size, value) - - -def duplicate(image: Image.Image) -> Image.Image: - """Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return image.copy() - - -def invert(image: Image.Image) -> Image.Image: - """ - Invert an image (channel). :: - - out = MAX - image - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image.load() - return image._new(image.im.chop_invert()) - - -def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. :: - - out = max(image1, image2) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_lighter(image2.im)) - - -def darker(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Compares the two images, pixel by pixel, and returns a new image containing - the darker values. :: - - out = min(image1, image2) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_darker(image2.im)) - - -def difference(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Returns the absolute value of the pixel-by-pixel difference between the two - images. :: - - out = abs(image1 - image2) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_difference(image2.im)) - - -def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other. - - If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. :: - - out = image1 * image2 / MAX - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_multiply(image2.im)) - - -def screen(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two inverted images on top of each other. :: - - out = MAX - ((MAX - image1) * (MAX - image2) / MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_screen(image2.im)) - - -def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other using the Soft Light algorithm - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_soft_light(image2.im)) - - -def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other using the Hard Light algorithm - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_hard_light(image2.im)) - - -def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other using the Overlay algorithm - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_overlay(image2.im)) - - -def add( - image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0 -) -> Image.Image: - """ - Adds two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: - - out = ((image1 + image2) / scale + offset) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_add(image2.im, scale, offset)) - - -def subtract( - image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0 -) -> Image.Image: - """ - Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. :: - - out = ((image1 - image2) / scale + offset) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_subtract(image2.im, scale, offset)) - - -def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Add two images, without clipping the result. :: - - out = ((image1 + image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_add_modulo(image2.im)) - - -def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Subtract two images, without clipping the result. :: - - out = ((image1 - image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_subtract_modulo(image2.im)) - - -def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Logical AND between two images. - - Both of the images must have mode "1". If you would like to perform a - logical AND on an image with a mode other than "1", try - :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask - as the second image. :: - - out = ((image1 and image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_and(image2.im)) - - -def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Logical OR between two images. - - Both of the images must have mode "1". :: - - out = ((image1 or image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_or(image2.im)) - - -def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Logical XOR between two images. - - Both of the images must have mode "1". :: - - out = ((bool(image1) != bool(image2)) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_xor(image2.im)) - - -def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image: - """Blend images using constant transparency weight. Alias for - :py:func:`PIL.Image.blend`. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return Image.blend(image1, image2, alpha) - - -def composite( - image1: Image.Image, image2: Image.Image, mask: Image.Image -) -> Image.Image: - """Create composite using transparency mask. Alias for - :py:func:`PIL.Image.composite`. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return Image.composite(image1, image2, mask) - - -def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image: - """Returns a copy of the image where data has been offset by the given - distances. Data wraps around the edges. If ``yoffset`` is omitted, it - is assumed to be equal to ``xoffset``. - - :param image: Input image. - :param xoffset: The horizontal distance. - :param yoffset: The vertical distance. If omitted, both - distances are set to the same value. - :rtype: :py:class:`~PIL.Image.Image` - """ - - if yoffset is None: - yoffset = xoffset - image.load() - return image._new(image.im.offset(xoffset, yoffset)) diff --git a/python_modules/PIL/ImageCms.py b/python_modules/PIL/ImageCms.py deleted file mode 100644 index a1584f111..000000000 --- a/python_modules/PIL/ImageCms.py +++ /dev/null @@ -1,1123 +0,0 @@ -# The Python Imaging Library. -# $Id$ - -# Optional color management support, based on Kevin Cazabon's PyCMS -# library. - -# Originally released under LGPL. Graciously donated to PIL in -# March 2009, for distribution under the standard PIL license - -# History: - -# 2009-03-08 fl Added to PIL. - -# Copyright (C) 2002-2003 Kevin Cazabon -# Copyright (c) 2009 by Fredrik Lundh -# Copyright (c) 2013 by Eric Soroos - -# See the README file for information on usage and redistribution. See -# below for the original description. -from __future__ import annotations - -import operator -import sys -from enum import IntEnum, IntFlag -from functools import reduce -from typing import Any, Literal, SupportsFloat, SupportsInt, Union - -from . import Image, __version__ -from ._deprecate import deprecate -from ._typing import SupportsRead - -try: - from . import _imagingcms as core - - _CmsProfileCompatible = Union[ - str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile" - ] -except ImportError as ex: - # Allow error import for doc purposes, but error out when accessing - # anything in core. - from ._util import DeferredError - - core = DeferredError.new(ex) - -_DESCRIPTION = """ -pyCMS - - a Python / PIL interface to the littleCMS ICC Color Management System - Copyright (C) 2002-2003 Kevin Cazabon - kevin@cazabon.com - https://www.cazabon.com - - pyCMS home page: https://www.cazabon.com/pyCMS - littleCMS home page: https://www.littlecms.com - (littleCMS is Copyright (C) 1998-2001 Marti Maria) - - Originally released under LGPL. Graciously donated to PIL in - March 2009, for distribution under the standard PIL license - - The pyCMS.py module provides a "clean" interface between Python/PIL and - pyCMSdll, taking care of some of the more complex handling of the direct - pyCMSdll functions, as well as error-checking and making sure that all - relevant data is kept together. - - While it is possible to call pyCMSdll functions directly, it's not highly - recommended. - - Version History: - - 1.0.0 pil Oct 2013 Port to LCMS 2. - - 0.1.0 pil mod March 10, 2009 - - Renamed display profile to proof profile. The proof - profile is the profile of the device that is being - simulated, not the profile of the device which is - actually used to display/print the final simulation - (that'd be the output profile) - also see LCMSAPI.txt - input colorspace -> using 'renderingIntent' -> proof - colorspace -> using 'proofRenderingIntent' -> output - colorspace - - Added LCMS FLAGS support. - Added FLAGS["SOFTPROOFING"] as default flag for - buildProofTransform (otherwise the proof profile/intent - would be ignored). - - 0.1.0 pil March 2009 - added to PIL, as PIL.ImageCms - - 0.0.2 alpha Jan 6, 2002 - - Added try/except statements around type() checks of - potential CObjects... Python won't let you use type() - on them, and raises a TypeError (stupid, if you ask - me!) - - Added buildProofTransformFromOpenProfiles() function. - Additional fixes in DLL, see DLL code for details. - - 0.0.1 alpha first public release, Dec. 26, 2002 - - Known to-do list with current version (of Python interface, not pyCMSdll): - - none - -""" - -_VERSION = "1.0.0 pil" - - -def __getattr__(name: str) -> Any: - if name == "DESCRIPTION": - deprecate("PIL.ImageCms.DESCRIPTION", 12) - return _DESCRIPTION - elif name == "VERSION": - deprecate("PIL.ImageCms.VERSION", 12) - return _VERSION - elif name == "FLAGS": - deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") - return _FLAGS - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - -# --------------------------------------------------------------------. - - -# -# intent/direction values - - -class Intent(IntEnum): - PERCEPTUAL = 0 - RELATIVE_COLORIMETRIC = 1 - SATURATION = 2 - ABSOLUTE_COLORIMETRIC = 3 - - -class Direction(IntEnum): - INPUT = 0 - OUTPUT = 1 - PROOF = 2 - - -# -# flags - - -class Flags(IntFlag): - """Flags and documentation are taken from ``lcms2.h``.""" - - NONE = 0 - NOCACHE = 0x0040 - """Inhibit 1-pixel cache""" - NOOPTIMIZE = 0x0100 - """Inhibit optimizations""" - NULLTRANSFORM = 0x0200 - """Don't transform anyway""" - GAMUTCHECK = 0x1000 - """Out of Gamut alarm""" - SOFTPROOFING = 0x4000 - """Do softproofing""" - BLACKPOINTCOMPENSATION = 0x2000 - NOWHITEONWHITEFIXUP = 0x0004 - """Don't fix scum dot""" - HIGHRESPRECALC = 0x0400 - """Use more memory to give better accuracy""" - LOWRESPRECALC = 0x0800 - """Use less memory to minimize resources""" - # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: - USE_8BITS_DEVICELINK = 0x0008 - """Create 8 bits devicelinks""" - GUESSDEVICECLASS = 0x0020 - """Guess device class (for ``transform2devicelink``)""" - KEEP_SEQUENCE = 0x0080 - """Keep profile sequence for devicelink creation""" - FORCE_CLUT = 0x0002 - """Force CLUT optimization""" - CLUT_POST_LINEARIZATION = 0x0001 - """create postlinearization tables if possible""" - CLUT_PRE_LINEARIZATION = 0x0010 - """create prelinearization tables if possible""" - NONEGATIVES = 0x8000 - """Prevent negative numbers in floating point transforms""" - COPY_ALPHA = 0x04000000 - """Alpha channels are copied on ``cmsDoTransform()``""" - NODEFAULTRESOURCEDEF = 0x01000000 - - _GRIDPOINTS_1 = 1 << 16 - _GRIDPOINTS_2 = 2 << 16 - _GRIDPOINTS_4 = 4 << 16 - _GRIDPOINTS_8 = 8 << 16 - _GRIDPOINTS_16 = 16 << 16 - _GRIDPOINTS_32 = 32 << 16 - _GRIDPOINTS_64 = 64 << 16 - _GRIDPOINTS_128 = 128 << 16 - - @staticmethod - def GRIDPOINTS(n: int) -> Flags: - """ - Fine-tune control over number of gridpoints - - :param n: :py:class:`int` in range ``0 <= n <= 255`` - """ - return Flags.NONE | ((n & 0xFF) << 16) - - -_MAX_FLAG = reduce(operator.or_, Flags) - - -_FLAGS = { - "MATRIXINPUT": 1, - "MATRIXOUTPUT": 2, - "MATRIXONLY": (1 | 2), - "NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot - # Don't create prelinearization tables on precalculated transforms - # (internal use): - "NOPRELINEARIZATION": 16, - "GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink) - "NOTCACHE": 64, # Inhibit 1-pixel cache - "NOTPRECALC": 256, - "NULLTRANSFORM": 512, # Don't transform anyway - "HIGHRESPRECALC": 1024, # Use more memory to give better accuracy - "LOWRESPRECALC": 2048, # Use less memory to minimize resources - "WHITEBLACKCOMPENSATION": 8192, - "BLACKPOINTCOMPENSATION": 8192, - "GAMUTCHECK": 4096, # Out of Gamut alarm - "SOFTPROOFING": 16384, # Do softproofing - "PRESERVEBLACK": 32768, # Black preservation - "NODEFAULTRESOURCEDEF": 16777216, # CRD special - "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints -} - - -# --------------------------------------------------------------------. -# Experimental PIL-level API -# --------------------------------------------------------------------. - -## -# Profile. - - -class ImageCmsProfile: - def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None: - """ - :param profile: Either a string representing a filename, - a file like object containing a profile or a - low-level profile object - - """ - self.filename = None - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info - - if isinstance(profile, str): - if sys.platform == "win32": - profile_bytes_path = profile.encode() - try: - profile_bytes_path.decode("ascii") - except UnicodeDecodeError: - with open(profile, "rb") as f: - self.profile = core.profile_frombytes(f.read()) - return - self.filename = profile - self.profile = core.profile_open(profile) - elif hasattr(profile, "read"): - self.profile = core.profile_frombytes(profile.read()) - elif isinstance(profile, core.CmsProfile): - self.profile = profile - else: - msg = "Invalid type for Profile" # type: ignore[unreachable] - raise TypeError(msg) - - def tobytes(self) -> bytes: - """ - Returns the profile in a format suitable for embedding in - saved images. - - :returns: a bytes object containing the ICC profile. - """ - - return core.profile_tobytes(self.profile) - - -class ImageCmsTransform(Image.ImagePointHandler): - """ - Transform. This can be used with the procedural API, or with the standard - :py:func:`~PIL.Image.Image.point` method. - - Will return the output profile in the ``output.info['icc_profile']``. - """ - - def __init__( - self, - input: ImageCmsProfile, - output: ImageCmsProfile, - input_mode: str, - output_mode: str, - intent: Intent = Intent.PERCEPTUAL, - proof: ImageCmsProfile | None = None, - proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, - flags: Flags = Flags.NONE, - ): - supported_modes = ( - "RGB", - "RGBA", - "RGBX", - "CMYK", - "I;16", - "I;16L", - "I;16B", - "YCbCr", - "LAB", - "L", - "1", - ) - for mode in (input_mode, output_mode): - if mode not in supported_modes: - deprecate( - mode, - 12, - { - "L;16": "I;16 or I;16L", - "L:16B": "I;16B", - "YCCA": "YCbCr", - "YCC": "YCbCr", - }.get(mode), - ) - if proof is None: - self.transform = core.buildTransform( - input.profile, output.profile, input_mode, output_mode, intent, flags - ) - else: - self.transform = core.buildProofTransform( - input.profile, - output.profile, - proof.profile, - input_mode, - output_mode, - intent, - proof_intent, - flags, - ) - # Note: inputMode and outputMode are for pyCMS compatibility only - self.input_mode = self.inputMode = input_mode - self.output_mode = self.outputMode = output_mode - - self.output_profile = output - - def point(self, im: Image.Image) -> Image.Image: - return self.apply(im) - - def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image: - if imOut is None: - imOut = Image.new(self.output_mode, im.size, None) - self.transform.apply(im.getim(), imOut.getim()) - imOut.info["icc_profile"] = self.output_profile.tobytes() - return imOut - - def apply_in_place(self, im: Image.Image) -> Image.Image: - if im.mode != self.output_mode: - msg = "mode mismatch" - raise ValueError(msg) # wrong output mode - self.transform.apply(im.getim(), im.getim()) - im.info["icc_profile"] = self.output_profile.tobytes() - return im - - -def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | None: - """ - (experimental) Fetches the profile for the current display device. - - :returns: ``None`` if the profile is not known. - """ - - if sys.platform != "win32": - return None - - from . import ImageWin # type: ignore[unused-ignore, unreachable] - - if isinstance(handle, ImageWin.HDC): - profile = core.get_display_profile_win32(int(handle), 1) - else: - profile = core.get_display_profile_win32(int(handle or 0)) - if profile is None: - return None - return ImageCmsProfile(profile) - - -# --------------------------------------------------------------------. -# pyCMS compatible layer -# --------------------------------------------------------------------. - - -class PyCMSError(Exception): - """(pyCMS) Exception class. - This is used for all errors in the pyCMS API.""" - - pass - - -def profileToProfile( - im: Image.Image, - inputProfile: _CmsProfileCompatible, - outputProfile: _CmsProfileCompatible, - renderingIntent: Intent = Intent.PERCEPTUAL, - outputMode: str | None = None, - inPlace: bool = False, - flags: Flags = Flags.NONE, -) -> Image.Image | None: - """ - (pyCMS) Applies an ICC transformation to a given image, mapping from - ``inputProfile`` to ``outputProfile``. - - If the input or output profiles specified are not valid filenames, a - :exc:`PyCMSError` will be raised. If ``inPlace`` is ``True`` and - ``outputMode != im.mode``, a :exc:`PyCMSError` will be raised. - If an error occurs during application of the profiles, - a :exc:`PyCMSError` will be raised. - If ``outputMode`` is not a mode supported by the ``outputProfile`` (or by pyCMS), - a :exc:`PyCMSError` will be raised. - - This function applies an ICC transformation to im from ``inputProfile``'s - color space to ``outputProfile``'s color space using the specified rendering - intent to decide how to handle out-of-gamut colors. - - ``outputMode`` can be used to specify that a color mode conversion is to - be done using these profiles, but the specified profiles must be able - to handle that mode. I.e., if converting im from RGB to CMYK using - profiles, the input profile must handle RGB data, and the output - profile must handle CMYK data. - - :param im: An open :py:class:`~PIL.Image.Image` object (i.e. Image.new(...) - or Image.open(...), etc.) - :param inputProfile: String, as a valid filename path to the ICC input - profile you wish to use for this image, or a profile object - :param outputProfile: String, as a valid filename path to the ICC output - profile you wish to use for this image, or a profile object - :param renderingIntent: Integer (0-3) specifying the rendering intent you - wish to use for the transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param outputMode: A valid PIL mode for the output image (i.e. "RGB", - "CMYK", etc.). Note: if rendering the image "inPlace", outputMode - MUST be the same mode as the input, or omitted completely. If - omitted, the outputMode will be the same as the mode of the input - image (im.mode) - :param inPlace: Boolean. If ``True``, the original image is modified in-place, - and ``None`` is returned. If ``False`` (default), a new - :py:class:`~PIL.Image.Image` object is returned with the transform applied. - :param flags: Integer (0-...) specifying additional flags - :returns: Either None or a new :py:class:`~PIL.Image.Image` object, depending on - the value of ``inPlace`` - :exception PyCMSError: - """ - - if outputMode is None: - outputMode = im.mode - - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - msg = "renderingIntent must be an integer between 0 and 3" - raise PyCMSError(msg) - - if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = f"flags must be an integer between 0 and {_MAX_FLAG}" - raise PyCMSError(msg) - - try: - if not isinstance(inputProfile, ImageCmsProfile): - inputProfile = ImageCmsProfile(inputProfile) - if not isinstance(outputProfile, ImageCmsProfile): - outputProfile = ImageCmsProfile(outputProfile) - transform = ImageCmsTransform( - inputProfile, - outputProfile, - im.mode, - outputMode, - renderingIntent, - flags=flags, - ) - if inPlace: - transform.apply_in_place(im) - imOut = None - else: - imOut = transform.apply(im) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - return imOut - - -def getOpenProfile( - profileFilename: str | SupportsRead[bytes] | core.CmsProfile, -) -> ImageCmsProfile: - """ - (pyCMS) Opens an ICC profile file. - - The PyCMSProfile object can be passed back into pyCMS for use in creating - transforms and such (as in ImageCms.buildTransformFromOpenProfiles()). - - If ``profileFilename`` is not a valid filename for an ICC profile, - a :exc:`PyCMSError` will be raised. - - :param profileFilename: String, as a valid filename path to the ICC profile - you wish to open, or a file-like object. - :returns: A CmsProfile class object. - :exception PyCMSError: - """ - - try: - return ImageCmsProfile(profileFilename) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def buildTransform( - inputProfile: _CmsProfileCompatible, - outputProfile: _CmsProfileCompatible, - inMode: str, - outMode: str, - renderingIntent: Intent = Intent.PERCEPTUAL, - flags: Flags = Flags.NONE, -) -> ImageCmsTransform: - """ - (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the - ``outputProfile``. Use applyTransform to apply the transform to a given - image. - - If the input or output profiles specified are not valid filenames, a - :exc:`PyCMSError` will be raised. If an error occurs during creation - of the transform, a :exc:`PyCMSError` will be raised. - - If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` - (or by pyCMS), a :exc:`PyCMSError` will be raised. - - This function builds and returns an ICC transform from the ``inputProfile`` - to the ``outputProfile`` using the ``renderingIntent`` to determine what to do - with out-of-gamut colors. It will ONLY work for converting images that - are in ``inMode`` to images that are in ``outMode`` color format (PIL mode, - i.e. "RGB", "RGBA", "CMYK", etc.). - - Building the transform is a fair part of the overhead in - ImageCms.profileToProfile(), so if you're planning on converting multiple - images using the same input/output settings, this can save you time. - Once you have a transform object, it can be used with - ImageCms.applyProfile() to convert images without the need to re-compute - the lookup table for the transform. - - The reason pyCMS returns a class object rather than a handle directly - to the transform is that it needs to keep track of the PIL input/output - modes that the transform is meant for. These attributes are stored in - the ``inMode`` and ``outMode`` attributes of the object (which can be - manually overridden if you really want to, but I don't know of any - time that would be of use, or would even work). - - :param inputProfile: String, as a valid filename path to the ICC input - profile you wish to use for this transform, or a profile object - :param outputProfile: String, as a valid filename path to the ICC output - profile you wish to use for this transform, or a profile object - :param inMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param outMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param renderingIntent: Integer (0-3) specifying the rendering intent you - wish to use for the transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param flags: Integer (0-...) specifying additional flags - :returns: A CmsTransform class object. - :exception PyCMSError: - """ - - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - msg = "renderingIntent must be an integer between 0 and 3" - raise PyCMSError(msg) - - if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = f"flags must be an integer between 0 and {_MAX_FLAG}" - raise PyCMSError(msg) - - try: - if not isinstance(inputProfile, ImageCmsProfile): - inputProfile = ImageCmsProfile(inputProfile) - if not isinstance(outputProfile, ImageCmsProfile): - outputProfile = ImageCmsProfile(outputProfile) - return ImageCmsTransform( - inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags - ) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def buildProofTransform( - inputProfile: _CmsProfileCompatible, - outputProfile: _CmsProfileCompatible, - proofProfile: _CmsProfileCompatible, - inMode: str, - outMode: str, - renderingIntent: Intent = Intent.PERCEPTUAL, - proofRenderingIntent: Intent = Intent.ABSOLUTE_COLORIMETRIC, - flags: Flags = Flags.SOFTPROOFING, -) -> ImageCmsTransform: - """ - (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the - ``outputProfile``, but tries to simulate the result that would be - obtained on the ``proofProfile`` device. - - If the input, output, or proof profiles specified are not valid - filenames, a :exc:`PyCMSError` will be raised. - - If an error occurs during creation of the transform, - a :exc:`PyCMSError` will be raised. - - If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` - (or by pyCMS), a :exc:`PyCMSError` will be raised. - - This function builds and returns an ICC transform from the ``inputProfile`` - to the ``outputProfile``, but tries to simulate the result that would be - obtained on the ``proofProfile`` device using ``renderingIntent`` and - ``proofRenderingIntent`` to determine what to do with out-of-gamut - colors. This is known as "soft-proofing". It will ONLY work for - converting images that are in ``inMode`` to images that are in outMode - color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.). - - Usage of the resulting transform object is exactly the same as with - ImageCms.buildTransform(). - - Proof profiling is generally used when using an output device to get a - good idea of what the final printed/displayed image would look like on - the ``proofProfile`` device when it's quicker and easier to use the - output device for judging color. Generally, this means that the - output device is a monitor, or a dye-sub printer (etc.), and the simulated - device is something more expensive, complicated, or time consuming - (making it difficult to make a real print for color judgement purposes). - - Soft-proofing basically functions by adjusting the colors on the - output device to match the colors of the device being simulated. However, - when the simulated device has a much wider gamut than the output - device, you may obtain marginal results. - - :param inputProfile: String, as a valid filename path to the ICC input - profile you wish to use for this transform, or a profile object - :param outputProfile: String, as a valid filename path to the ICC output - (monitor, usually) profile you wish to use for this transform, or a - profile object - :param proofProfile: String, as a valid filename path to the ICC proof - profile you wish to use for this transform, or a profile object - :param inMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param outMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param renderingIntent: Integer (0-3) specifying the rendering intent you - wish to use for the input->proof (simulated) transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param proofRenderingIntent: Integer (0-3) specifying the rendering intent - you wish to use for proof->output transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param flags: Integer (0-...) specifying additional flags - :returns: A CmsTransform class object. - :exception PyCMSError: - """ - - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - msg = "renderingIntent must be an integer between 0 and 3" - raise PyCMSError(msg) - - if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = f"flags must be an integer between 0 and {_MAX_FLAG}" - raise PyCMSError(msg) - - try: - if not isinstance(inputProfile, ImageCmsProfile): - inputProfile = ImageCmsProfile(inputProfile) - if not isinstance(outputProfile, ImageCmsProfile): - outputProfile = ImageCmsProfile(outputProfile) - if not isinstance(proofProfile, ImageCmsProfile): - proofProfile = ImageCmsProfile(proofProfile) - return ImageCmsTransform( - inputProfile, - outputProfile, - inMode, - outMode, - renderingIntent, - proofProfile, - proofRenderingIntent, - flags, - ) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -buildTransformFromOpenProfiles = buildTransform -buildProofTransformFromOpenProfiles = buildProofTransform - - -def applyTransform( - im: Image.Image, transform: ImageCmsTransform, inPlace: bool = False -) -> Image.Image | None: - """ - (pyCMS) Applies a transform to a given image. - - If ``im.mode != transform.input_mode``, a :exc:`PyCMSError` is raised. - - If ``inPlace`` is ``True`` and ``transform.input_mode != transform.output_mode``, a - :exc:`PyCMSError` is raised. - - If ``im.mode``, ``transform.input_mode`` or ``transform.output_mode`` is not - supported by pyCMSdll or the profiles you used for the transform, a - :exc:`PyCMSError` is raised. - - If an error occurs while the transform is being applied, - a :exc:`PyCMSError` is raised. - - This function applies a pre-calculated transform (from - ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles()) - to an image. The transform can be used for multiple images, saving - considerable calculation time if doing the same conversion multiple times. - - If you want to modify im in-place instead of receiving a new image as - the return value, set ``inPlace`` to ``True``. This can only be done if - ``transform.input_mode`` and ``transform.output_mode`` are the same, because we - can't change the mode in-place (the buffer sizes for some modes are - different). The default behavior is to return a new :py:class:`~PIL.Image.Image` - object of the same dimensions in mode ``transform.output_mode``. - - :param im: An :py:class:`~PIL.Image.Image` object, and ``im.mode`` must be the same - as the ``input_mode`` supported by the transform. - :param transform: A valid CmsTransform class object - :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is - returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the - transform applied is returned (and ``im`` is not changed). The default is - ``False``. - :returns: Either ``None``, or a new :py:class:`~PIL.Image.Image` object, - depending on the value of ``inPlace``. The profile will be returned in - the image's ``info['icc_profile']``. - :exception PyCMSError: - """ - - try: - if inPlace: - transform.apply_in_place(im) - imOut = None - else: - imOut = transform.apply(im) - except (TypeError, ValueError) as v: - raise PyCMSError(v) from v - - return imOut - - -def createProfile( - colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0 -) -> core.CmsProfile: - """ - (pyCMS) Creates a profile. - - If colorSpace not in ``["LAB", "XYZ", "sRGB"]``, - a :exc:`PyCMSError` is raised. - - If using LAB and ``colorTemp`` is not a positive integer, - a :exc:`PyCMSError` is raised. - - If an error occurs while creating the profile, - a :exc:`PyCMSError` is raised. - - Use this function to create common profiles on-the-fly instead of - having to supply a profile on disk and knowing the path to it. It - returns a normal CmsProfile object that can be passed to - ImageCms.buildTransformFromOpenProfiles() to create a transform to apply - to images. - - :param colorSpace: String, the color space of the profile you wish to - create. - Currently only "LAB", "XYZ", and "sRGB" are supported. - :param colorTemp: Positive number for the white point for the profile, in - degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50 - illuminant if omitted (5000k). colorTemp is ONLY applied to LAB - profiles, and is ignored for XYZ and sRGB. - :returns: A CmsProfile class object - :exception PyCMSError: - """ - - if colorSpace not in ["LAB", "XYZ", "sRGB"]: - msg = ( - f"Color space not supported for on-the-fly profile creation ({colorSpace})" - ) - raise PyCMSError(msg) - - if colorSpace == "LAB": - try: - colorTemp = float(colorTemp) - except (TypeError, ValueError) as e: - msg = f'Color temperature must be numeric, "{colorTemp}" not valid' - raise PyCMSError(msg) from e - - try: - return core.createProfile(colorSpace, colorTemp) - except (TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileName(profile: _CmsProfileCompatible) -> str: - """ - - (pyCMS) Gets the internal product name for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, - a :exc:`PyCMSError` is raised If an error occurs while trying - to obtain the name tag, a :exc:`PyCMSError` is raised. - - Use this function to obtain the INTERNAL name of the profile (stored - in an ICC tag in the profile itself), usually the one used when the - profile was originally created. Sometimes this tag also contains - additional information supplied by the creator. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal name of the profile as stored - in an ICC tag. - :exception PyCMSError: - """ - - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - # do it in python, not c. - # // name was "%s - %s" (model, manufacturer) || Description , - # // but if the Model and Manufacturer were the same or the model - # // was long, Just the model, in 1.x - model = profile.profile.model - manufacturer = profile.profile.manufacturer - - if not (model or manufacturer): - return (profile.profile.profile_description or "") + "\n" - if not manufacturer or (model and len(model) > 30): - return f"{model}\n" - return f"{model} - {manufacturer}\n" - - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileInfo(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the internal product information for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, - a :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the info tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - info tag. This often contains details about the profile, and how it - was created, as supplied by the creator. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - - try: - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - # add an extra newline to preserve pyCMS compatibility - # Python, not C. the white point bits weren't working well, - # so skipping. - # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint - description = profile.profile.profile_description - cpright = profile.profile.copyright - elements = [element for element in (description, cpright) if element] - return "\r\n\r\n".join(elements) + "\r\n\r\n" - - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileCopyright(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the copyright for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the copyright tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - copyright tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.copyright or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileManufacturer(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the manufacturer for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the manufacturer tag, a - :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - manufacturer tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.manufacturer or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileModel(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the model for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the model tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - model tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.model or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileDescription(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the description for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the description tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - description tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in an - ICC tag. - :exception PyCMSError: - """ - - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.profile_description or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getDefaultIntent(profile: _CmsProfileCompatible) -> int: - """ - (pyCMS) Gets the default intent name for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the default intent, a - :exc:`PyCMSError` is raised. - - Use this function to determine the default (and usually best optimized) - rendering intent for this profile. Most profiles support multiple - rendering intents, but are intended mostly for one type of conversion. - If you wish to use a different intent than returned, use - ImageCms.isIntentSupported() to verify it will work first. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: Integer 0-3 specifying the default rendering intent for this - profile. - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :exception PyCMSError: - """ - - try: - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return profile.profile.rendering_intent - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def isIntentSupported( - profile: _CmsProfileCompatible, intent: Intent, direction: Direction -) -> Literal[-1, 1]: - """ - (pyCMS) Checks if a given intent is supported. - - Use this function to verify that you can use your desired - ``intent`` with ``profile``, and that ``profile`` can be used for the - input/output/proof profile as you desire. - - Some profiles are created specifically for one "direction", can cannot - be used for others. Some profiles can only be used for certain - rendering intents, so it's best to either verify this before trying - to create a transform with them (using this function), or catch the - potential :exc:`PyCMSError` that will occur if they don't - support the modes you select. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :param intent: Integer (0-3) specifying the rendering intent you wish to - use with this profile - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param direction: Integer specifying if the profile is to be used for - input, output, or proof - - INPUT = 0 (or use ImageCms.Direction.INPUT) - OUTPUT = 1 (or use ImageCms.Direction.OUTPUT) - PROOF = 2 (or use ImageCms.Direction.PROOF) - - :returns: 1 if the intent/direction are supported, -1 if they are not. - :exception PyCMSError: - """ - - try: - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - # FIXME: I get different results for the same data w. different - # compilers. Bug in LittleCMS or in the binding? - if profile.profile.is_intent_supported(intent, direction): - return 1 - else: - return -1 - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def versions() -> tuple[str, str | None, str, str]: - """ - (pyCMS) Fetches versions. - """ - - deprecate( - "PIL.ImageCms.versions()", - 12, - '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', - ) - return _VERSION, core.littlecms_version, sys.version.split()[0], __version__ diff --git a/python_modules/PIL/ImageColor.py b/python_modules/PIL/ImageColor.py deleted file mode 100644 index 9a15a8eb7..000000000 --- a/python_modules/PIL/ImageColor.py +++ /dev/null @@ -1,320 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# map CSS3-style colour description strings to RGB -# -# History: -# 2002-10-24 fl Added support for CSS-style color strings -# 2002-12-15 fl Added RGBA support -# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2 -# 2004-07-19 fl Fixed gray/grey spelling issues -# 2009-03-05 fl Fixed rounding error in grayscale calculation -# -# Copyright (c) 2002-2004 by Secret Labs AB -# Copyright (c) 2002-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re -from functools import lru_cache - -from . import Image - - -@lru_cache -def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]: - """ - Convert a color string to an RGB or RGBA tuple. If the string cannot be - parsed, this function raises a :py:exc:`ValueError` exception. - - .. versionadded:: 1.1.4 - - :param color: A color string - :return: ``(red, green, blue[, alpha])`` - """ - if len(color) > 100: - msg = "color specifier is too long" - raise ValueError(msg) - color = color.lower() - - rgb = colormap.get(color, None) - if rgb: - if isinstance(rgb, tuple): - return rgb - rgb_tuple = getrgb(rgb) - assert len(rgb_tuple) == 3 - colormap[color] = rgb_tuple - return rgb_tuple - - # check for known string formats - if re.match("#[a-f0-9]{3}$", color): - return int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16) - - if re.match("#[a-f0-9]{4}$", color): - return ( - int(color[1] * 2, 16), - int(color[2] * 2, 16), - int(color[3] * 2, 16), - int(color[4] * 2, 16), - ) - - if re.match("#[a-f0-9]{6}$", color): - return int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16) - - if re.match("#[a-f0-9]{8}$", color): - return ( - int(color[1:3], 16), - int(color[3:5], 16), - int(color[5:7], 16), - int(color[7:9], 16), - ) - - m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) - if m: - return int(m.group(1)), int(m.group(2)), int(m.group(3)) - - m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) - if m: - return ( - int((int(m.group(1)) * 255) / 100.0 + 0.5), - int((int(m.group(2)) * 255) / 100.0 + 0.5), - int((int(m.group(3)) * 255) / 100.0 + 0.5), - ) - - m = re.match( - r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color - ) - if m: - from colorsys import hls_to_rgb - - rgb_floats = hls_to_rgb( - float(m.group(1)) / 360.0, - float(m.group(3)) / 100.0, - float(m.group(2)) / 100.0, - ) - return ( - int(rgb_floats[0] * 255 + 0.5), - int(rgb_floats[1] * 255 + 0.5), - int(rgb_floats[2] * 255 + 0.5), - ) - - m = re.match( - r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color - ) - if m: - from colorsys import hsv_to_rgb - - rgb_floats = hsv_to_rgb( - float(m.group(1)) / 360.0, - float(m.group(2)) / 100.0, - float(m.group(3)) / 100.0, - ) - return ( - int(rgb_floats[0] * 255 + 0.5), - int(rgb_floats[1] * 255 + 0.5), - int(rgb_floats[2] * 255 + 0.5), - ) - - m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) - if m: - return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)) - msg = f"unknown color specifier: {repr(color)}" - raise ValueError(msg) - - -@lru_cache -def getcolor(color: str, mode: str) -> int | tuple[int, ...]: - """ - Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if - ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is - not color or a palette image, converts the RGB value to a grayscale value. - If the string cannot be parsed, this function raises a :py:exc:`ValueError` - exception. - - .. versionadded:: 1.1.4 - - :param color: A color string - :param mode: Convert result to this mode - :return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])`` - """ - # same as getrgb, but converts the result to the given mode - rgb, alpha = getrgb(color), 255 - if len(rgb) == 4: - alpha = rgb[3] - rgb = rgb[:3] - - if mode == "HSV": - from colorsys import rgb_to_hsv - - r, g, b = rgb - h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) - return int(h * 255), int(s * 255), int(v * 255) - elif Image.getmodebase(mode) == "L": - r, g, b = rgb - # ITU-R Recommendation 601-2 for nonlinear RGB - # scaled to 24 bits to match the convert's implementation. - graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 - if mode[-1] == "A": - return graylevel, alpha - return graylevel - elif mode[-1] == "A": - return rgb + (alpha,) - return rgb - - -colormap: dict[str, str | tuple[int, int, int]] = { - # X11 colour table from https://drafts.csswg.org/css-color-4/, with - # gray/grey spelling issues fixed. This is a superset of HTML 4.0 - # colour names used in CSS 1. - "aliceblue": "#f0f8ff", - "antiquewhite": "#faebd7", - "aqua": "#00ffff", - "aquamarine": "#7fffd4", - "azure": "#f0ffff", - "beige": "#f5f5dc", - "bisque": "#ffe4c4", - "black": "#000000", - "blanchedalmond": "#ffebcd", - "blue": "#0000ff", - "blueviolet": "#8a2be2", - "brown": "#a52a2a", - "burlywood": "#deb887", - "cadetblue": "#5f9ea0", - "chartreuse": "#7fff00", - "chocolate": "#d2691e", - "coral": "#ff7f50", - "cornflowerblue": "#6495ed", - "cornsilk": "#fff8dc", - "crimson": "#dc143c", - "cyan": "#00ffff", - "darkblue": "#00008b", - "darkcyan": "#008b8b", - "darkgoldenrod": "#b8860b", - "darkgray": "#a9a9a9", - "darkgrey": "#a9a9a9", - "darkgreen": "#006400", - "darkkhaki": "#bdb76b", - "darkmagenta": "#8b008b", - "darkolivegreen": "#556b2f", - "darkorange": "#ff8c00", - "darkorchid": "#9932cc", - "darkred": "#8b0000", - "darksalmon": "#e9967a", - "darkseagreen": "#8fbc8f", - "darkslateblue": "#483d8b", - "darkslategray": "#2f4f4f", - "darkslategrey": "#2f4f4f", - "darkturquoise": "#00ced1", - "darkviolet": "#9400d3", - "deeppink": "#ff1493", - "deepskyblue": "#00bfff", - "dimgray": "#696969", - "dimgrey": "#696969", - "dodgerblue": "#1e90ff", - "firebrick": "#b22222", - "floralwhite": "#fffaf0", - "forestgreen": "#228b22", - "fuchsia": "#ff00ff", - "gainsboro": "#dcdcdc", - "ghostwhite": "#f8f8ff", - "gold": "#ffd700", - "goldenrod": "#daa520", - "gray": "#808080", - "grey": "#808080", - "green": "#008000", - "greenyellow": "#adff2f", - "honeydew": "#f0fff0", - "hotpink": "#ff69b4", - "indianred": "#cd5c5c", - "indigo": "#4b0082", - "ivory": "#fffff0", - "khaki": "#f0e68c", - "lavender": "#e6e6fa", - "lavenderblush": "#fff0f5", - "lawngreen": "#7cfc00", - "lemonchiffon": "#fffacd", - "lightblue": "#add8e6", - "lightcoral": "#f08080", - "lightcyan": "#e0ffff", - "lightgoldenrodyellow": "#fafad2", - "lightgreen": "#90ee90", - "lightgray": "#d3d3d3", - "lightgrey": "#d3d3d3", - "lightpink": "#ffb6c1", - "lightsalmon": "#ffa07a", - "lightseagreen": "#20b2aa", - "lightskyblue": "#87cefa", - "lightslategray": "#778899", - "lightslategrey": "#778899", - "lightsteelblue": "#b0c4de", - "lightyellow": "#ffffe0", - "lime": "#00ff00", - "limegreen": "#32cd32", - "linen": "#faf0e6", - "magenta": "#ff00ff", - "maroon": "#800000", - "mediumaquamarine": "#66cdaa", - "mediumblue": "#0000cd", - "mediumorchid": "#ba55d3", - "mediumpurple": "#9370db", - "mediumseagreen": "#3cb371", - "mediumslateblue": "#7b68ee", - "mediumspringgreen": "#00fa9a", - "mediumturquoise": "#48d1cc", - "mediumvioletred": "#c71585", - "midnightblue": "#191970", - "mintcream": "#f5fffa", - "mistyrose": "#ffe4e1", - "moccasin": "#ffe4b5", - "navajowhite": "#ffdead", - "navy": "#000080", - "oldlace": "#fdf5e6", - "olive": "#808000", - "olivedrab": "#6b8e23", - "orange": "#ffa500", - "orangered": "#ff4500", - "orchid": "#da70d6", - "palegoldenrod": "#eee8aa", - "palegreen": "#98fb98", - "paleturquoise": "#afeeee", - "palevioletred": "#db7093", - "papayawhip": "#ffefd5", - "peachpuff": "#ffdab9", - "peru": "#cd853f", - "pink": "#ffc0cb", - "plum": "#dda0dd", - "powderblue": "#b0e0e6", - "purple": "#800080", - "rebeccapurple": "#663399", - "red": "#ff0000", - "rosybrown": "#bc8f8f", - "royalblue": "#4169e1", - "saddlebrown": "#8b4513", - "salmon": "#fa8072", - "sandybrown": "#f4a460", - "seagreen": "#2e8b57", - "seashell": "#fff5ee", - "sienna": "#a0522d", - "silver": "#c0c0c0", - "skyblue": "#87ceeb", - "slateblue": "#6a5acd", - "slategray": "#708090", - "slategrey": "#708090", - "snow": "#fffafa", - "springgreen": "#00ff7f", - "steelblue": "#4682b4", - "tan": "#d2b48c", - "teal": "#008080", - "thistle": "#d8bfd8", - "tomato": "#ff6347", - "turquoise": "#40e0d0", - "violet": "#ee82ee", - "wheat": "#f5deb3", - "white": "#ffffff", - "whitesmoke": "#f5f5f5", - "yellow": "#ffff00", - "yellowgreen": "#9acd32", -} diff --git a/python_modules/PIL/ImageDraw.py b/python_modules/PIL/ImageDraw.py deleted file mode 100644 index 6cf1ee626..000000000 --- a/python_modules/PIL/ImageDraw.py +++ /dev/null @@ -1,1232 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# drawing interface operations -# -# History: -# 1996-04-13 fl Created (experimental) -# 1996-08-07 fl Filled polygons, ellipses. -# 1996-08-13 fl Added text support -# 1998-06-28 fl Handle I and F images -# 1998-12-29 fl Added arc; use arc primitive to draw ellipses -# 1999-01-10 fl Added shape stuff (experimental) -# 1999-02-06 fl Added bitmap support -# 1999-02-11 fl Changed all primitives to take options -# 1999-02-20 fl Fixed backwards compatibility -# 2000-10-12 fl Copy on write, when necessary -# 2001-02-18 fl Use default ink for bitmap/text also in fill mode -# 2002-10-24 fl Added support for CSS-style color strings -# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing -# 2002-12-11 fl Refactored low-level drawing API (work in progress) -# 2004-08-26 fl Made Draw() a factory function, added getdraw() support -# 2004-09-04 fl Added width support to line primitive -# 2004-09-10 fl Added font mode handling -# 2006-06-19 fl Added font bearing support (getmask2) -# -# Copyright (c) 1997-2006 by Secret Labs AB -# Copyright (c) 1996-2006 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import math -import struct -from collections.abc import Sequence -from types import ModuleType -from typing import Any, AnyStr, Callable, Union, cast - -from . import Image, ImageColor -from ._deprecate import deprecate -from ._typing import Coords - -# experimental access to the outline API -Outline: Callable[[], Image.core._Outline] = Image.core.outline - -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import ImageDraw2, ImageFont - -_Ink = Union[float, tuple[int, ...], str] - -""" -A simple 2D drawing interface for PIL images. -

    -Application code should use the Draw factory, instead of -directly. -""" - - -class ImageDraw: - font: ( - ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None - ) = None - - def __init__(self, im: Image.Image, mode: str | None = None) -> None: - """ - Create a drawing instance. - - :param im: The image to draw in. - :param mode: Optional mode to use for color values. For RGB - images, this argument can be RGB or RGBA (to blend the - drawing into the image). For all other modes, this argument - must be the same as the image mode. If omitted, the mode - defaults to the mode of the image. - """ - im.load() - if im.readonly: - im._copy() # make it writeable - blend = 0 - if mode is None: - mode = im.mode - if mode != im.mode: - if mode == "RGBA" and im.mode == "RGB": - blend = 1 - else: - msg = "mode mismatch" - raise ValueError(msg) - if mode == "P": - self.palette = im.palette - else: - self.palette = None - self._image = im - self.im = im.im - self.draw = Image.core.draw(self.im, blend) - self.mode = mode - if mode in ("I", "F"): - self.ink = self.draw.draw_ink(1) - else: - self.ink = self.draw.draw_ink(-1) - if mode in ("1", "P", "I", "F"): - # FIXME: fix Fill2 to properly support matte for I+F images - self.fontmode = "1" - else: - self.fontmode = "L" # aliasing is okay for other modes - self.fill = False - - def getfont( - self, - ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: - """ - Get the current default font. - - To set the default font for this ImageDraw instance:: - - from PIL import ImageDraw, ImageFont - draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - To set the default font for all future ImageDraw instances:: - - from PIL import ImageDraw, ImageFont - ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - If the current default font is ``None``, - it is initialized with ``ImageFont.load_default()``. - - :returns: An image font.""" - if not self.font: - # FIXME: should add a font repository - from . import ImageFont - - self.font = ImageFont.load_default() - return self.font - - def _getfont( - self, font_size: float | None - ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: - if font_size is not None: - from . import ImageFont - - return ImageFont.load_default(font_size) - else: - return self.getfont() - - def _getink( - self, ink: _Ink | None, fill: _Ink | None = None - ) -> tuple[int | None, int | None]: - result_ink = None - result_fill = None - if ink is None and fill is None: - if self.fill: - result_fill = self.ink - else: - result_ink = self.ink - else: - if ink is not None: - if isinstance(ink, str): - ink = ImageColor.getcolor(ink, self.mode) - if self.palette and isinstance(ink, tuple): - ink = self.palette.getcolor(ink, self._image) - result_ink = self.draw.draw_ink(ink) - if fill is not None: - if isinstance(fill, str): - fill = ImageColor.getcolor(fill, self.mode) - if self.palette and isinstance(fill, tuple): - fill = self.palette.getcolor(fill, self._image) - result_fill = self.draw.draw_ink(fill) - return result_ink, result_fill - - def arc( - self, - xy: Coords, - start: float, - end: float, - fill: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw an arc.""" - ink, fill = self._getink(fill) - if ink is not None: - self.draw.draw_arc(xy, start, end, ink, width) - - def bitmap( - self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None - ) -> None: - """Draw a bitmap.""" - bitmap.load() - ink, fill = self._getink(fill) - if ink is None: - ink = fill - if ink is not None: - self.draw.draw_bitmap(xy, bitmap.im, ink) - - def chord( - self, - xy: Coords, - start: float, - end: float, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a chord.""" - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_chord(xy, start, end, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - self.draw.draw_chord(xy, start, end, ink, 0, width) - - def ellipse( - self, - xy: Coords, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw an ellipse.""" - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_ellipse(xy, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - self.draw.draw_ellipse(xy, ink, 0, width) - - def circle( - self, - xy: Sequence[float], - radius: float, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a circle given center coordinates and a radius.""" - ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius) - self.ellipse(ellipse_xy, fill, outline, width) - - def line( - self, - xy: Coords, - fill: _Ink | None = None, - width: int = 0, - joint: str | None = None, - ) -> None: - """Draw a line, or a connected sequence of line segments.""" - ink = self._getink(fill)[0] - if ink is not None: - self.draw.draw_lines(xy, ink, width) - if joint == "curve" and width > 4: - points: Sequence[Sequence[float]] - if isinstance(xy[0], (list, tuple)): - points = cast(Sequence[Sequence[float]], xy) - else: - points = [ - cast(Sequence[float], tuple(xy[i : i + 2])) - for i in range(0, len(xy), 2) - ] - for i in range(1, len(points) - 1): - point = points[i] - angles = [ - math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) - % 360 - for start, end in ( - (points[i - 1], point), - (point, points[i + 1]), - ) - ] - if angles[0] == angles[1]: - # This is a straight line, so no joint is required - continue - - def coord_at_angle( - coord: Sequence[float], angle: float - ) -> tuple[float, ...]: - x, y = coord - angle -= 90 - distance = width / 2 - 1 - return tuple( - p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) - for p, p_d in ( - (x, distance * math.cos(math.radians(angle))), - (y, distance * math.sin(math.radians(angle))), - ) - ) - - flipped = ( - angles[1] > angles[0] and angles[1] - 180 > angles[0] - ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0]) - coords = [ - (point[0] - width / 2 + 1, point[1] - width / 2 + 1), - (point[0] + width / 2 - 1, point[1] + width / 2 - 1), - ] - if flipped: - start, end = (angles[1] + 90, angles[0] + 90) - else: - start, end = (angles[0] - 90, angles[1] - 90) - self.pieslice(coords, start - 90, end - 90, fill) - - if width > 8: - # Cover potential gaps between the line and the joint - if flipped: - gap_coords = [ - coord_at_angle(point, angles[0] + 90), - point, - coord_at_angle(point, angles[1] + 90), - ] - else: - gap_coords = [ - coord_at_angle(point, angles[0] - 90), - point, - coord_at_angle(point, angles[1] - 90), - ] - self.line(gap_coords, fill, width=3) - - def shape( - self, - shape: Image.core._Outline, - fill: _Ink | None = None, - outline: _Ink | None = None, - ) -> None: - """(Experimental) Draw a shape.""" - shape.close() - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_outline(shape, fill_ink, 1) - if ink is not None and ink != fill_ink: - self.draw.draw_outline(shape, ink, 0) - - def pieslice( - self, - xy: Coords, - start: float, - end: float, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a pieslice.""" - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_pieslice(xy, start, end, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - self.draw.draw_pieslice(xy, start, end, ink, 0, width) - - def point(self, xy: Coords, fill: _Ink | None = None) -> None: - """Draw one or more individual pixels.""" - ink, fill = self._getink(fill) - if ink is not None: - self.draw.draw_points(xy, ink) - - def polygon( - self, - xy: Coords, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a polygon.""" - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_polygon(xy, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - if width == 1: - self.draw.draw_polygon(xy, ink, 0, width) - elif self.im is not None: - # To avoid expanding the polygon outwards, - # use the fill as a mask - mask = Image.new("1", self.im.size) - mask_ink = self._getink(1)[0] - draw = Draw(mask) - draw.draw.draw_polygon(xy, mask_ink, 1) - - self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im) - - def regular_polygon( - self, - bounding_circle: Sequence[Sequence[float] | float], - n_sides: int, - rotation: float = 0, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a regular polygon.""" - xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) - self.polygon(xy, fill, outline, width) - - def rectangle( - self, - xy: Coords, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a rectangle.""" - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_rectangle(xy, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - self.draw.draw_rectangle(xy, ink, 0, width) - - def rounded_rectangle( - self, - xy: Coords, - radius: float = 0, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - *, - corners: tuple[bool, bool, bool, bool] | None = None, - ) -> None: - """Draw a rounded rectangle.""" - if isinstance(xy[0], (list, tuple)): - (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) - else: - x0, y0, x1, y1 = cast(Sequence[float], xy) - if x1 < x0: - msg = "x1 must be greater than or equal to x0" - raise ValueError(msg) - if y1 < y0: - msg = "y1 must be greater than or equal to y0" - raise ValueError(msg) - if corners is None: - corners = (True, True, True, True) - - d = radius * 2 - - x0 = round(x0) - y0 = round(y0) - x1 = round(x1) - y1 = round(y1) - full_x, full_y = False, False - if all(corners): - full_x = d >= x1 - x0 - 1 - if full_x: - # The two left and two right corners are joined - d = x1 - x0 - full_y = d >= y1 - y0 - 1 - if full_y: - # The two top and two bottom corners are joined - d = y1 - y0 - if full_x and full_y: - # If all corners are joined, that is a circle - return self.ellipse(xy, fill, outline, width) - - if d == 0 or not any(corners): - # If the corners have no curve, - # or there are no corners, - # that is a rectangle - return self.rectangle(xy, fill, outline, width) - - r = int(d // 2) - ink, fill_ink = self._getink(outline, fill) - - def draw_corners(pieslice: bool) -> None: - parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] - if full_x: - # Draw top and bottom halves - parts = ( - ((x0, y0, x0 + d, y0 + d), 180, 360), - ((x0, y1 - d, x0 + d, y1), 0, 180), - ) - elif full_y: - # Draw left and right halves - parts = ( - ((x0, y0, x0 + d, y0 + d), 90, 270), - ((x1 - d, y0, x1, y0 + d), 270, 90), - ) - else: - # Draw four separate corners - parts = tuple( - part - for i, part in enumerate( - ( - ((x0, y0, x0 + d, y0 + d), 180, 270), - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), - ) - ) - if corners[i] - ) - for part in parts: - if pieslice: - self.draw.draw_pieslice(*(part + (fill_ink, 1))) - else: - self.draw.draw_arc(*(part + (ink, width))) - - if fill_ink is not None: - draw_corners(True) - - if full_x: - self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) - elif x1 - r - 1 > x0 + r + 1: - self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) - if not full_x and not full_y: - left = [x0, y0, x0 + r, y1] - if corners[0]: - left[1] += r + 1 - if corners[3]: - left[3] -= r + 1 - self.draw.draw_rectangle(left, fill_ink, 1) - - right = [x1 - r, y0, x1, y1] - if corners[1]: - right[1] += r + 1 - if corners[2]: - right[3] -= r + 1 - self.draw.draw_rectangle(right, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - draw_corners(False) - - if not full_x: - top = [x0, y0, x1, y0 + width - 1] - if corners[0]: - top[0] += r + 1 - if corners[1]: - top[2] -= r + 1 - self.draw.draw_rectangle(top, ink, 1) - - bottom = [x0, y1 - width + 1, x1, y1] - if corners[3]: - bottom[0] += r + 1 - if corners[2]: - bottom[2] -= r + 1 - self.draw.draw_rectangle(bottom, ink, 1) - if not full_y: - left = [x0, y0, x0 + width - 1, y1] - if corners[0]: - left[1] += r + 1 - if corners[3]: - left[3] -= r + 1 - self.draw.draw_rectangle(left, ink, 1) - - right = [x1 - width + 1, y0, x1, y1] - if corners[1]: - right[1] += r + 1 - if corners[2]: - right[3] -= r + 1 - self.draw.draw_rectangle(right, ink, 1) - - def _multiline_check(self, text: AnyStr) -> bool: - split_character = "\n" if isinstance(text, str) else b"\n" - - return split_character in text - - def text( - self, - xy: tuple[float, float], - text: AnyStr, - fill: _Ink | None = None, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - anchor: str | None = None, - spacing: float = 4, - align: str = "left", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - stroke_fill: _Ink | None = None, - embedded_color: bool = False, - *args: Any, - **kwargs: Any, - ) -> None: - """Draw text.""" - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - - if font is None: - font = self._getfont(kwargs.get("font_size")) - - if self._multiline_check(text): - return self.multiline_text( - xy, - text, - fill, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - stroke_fill, - embedded_color, - ) - - def getink(fill: _Ink | None) -> int: - ink, fill_ink = self._getink(fill) - if ink is None: - assert fill_ink is not None - return fill_ink - return ink - - def draw_text(ink: int, stroke_width: float = 0) -> None: - mode = self.fontmode - if stroke_width == 0 and embedded_color: - mode = "RGBA" - coord = [] - for i in range(2): - coord.append(int(xy[i])) - start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) - try: - mask, offset = font.getmask2( # type: ignore[union-attr,misc] - text, - mode, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - stroke_filled=True, - anchor=anchor, - ink=ink, - start=start, - *args, - **kwargs, - ) - coord = [coord[0] + offset[0], coord[1] + offset[1]] - except AttributeError: - try: - mask = font.getmask( # type: ignore[misc] - text, - mode, - direction, - features, - language, - stroke_width, - anchor, - ink, - start=start, - *args, - **kwargs, - ) - except TypeError: - mask = font.getmask(text) - if mode == "RGBA": - # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A - # extract mask and set text alpha - color, mask = mask, mask.getband(3) - ink_alpha = struct.pack("i", ink)[3] - color.fillband(3, ink_alpha) - x, y = coord - if self.im is not None: - self.im.paste( - color, (x, y, x + mask.size[0], y + mask.size[1]), mask - ) - else: - self.draw.draw_bitmap(coord, mask, ink) - - ink = getink(fill) - if ink is not None: - stroke_ink = None - if stroke_width: - stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink - - if stroke_ink is not None: - # Draw stroked text - draw_text(stroke_ink, stroke_width) - - # Draw normal text - if ink != stroke_ink: - draw_text(ink) - else: - # Only draw normal text - draw_text(ink) - - def _prepare_multiline_text( - self, - xy: tuple[float, float], - text: AnyStr, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ), - anchor: str | None, - spacing: float, - align: str, - direction: str | None, - features: list[str] | None, - language: str | None, - stroke_width: float, - embedded_color: bool, - font_size: float | None, - ) -> tuple[ - ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, - list[tuple[tuple[float, float], str, AnyStr]], - ]: - if anchor is None: - anchor = "lt" if direction == "ttb" else "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - elif anchor[1] in "tb" and direction != "ttb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - lines = text.split("\n" if isinstance(text, str) else b"\n") - line_spacing = ( - self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] - + stroke_width - + spacing - ) - - top = xy[1] - parts = [] - if direction == "ttb": - left = xy[0] - for line in lines: - parts.append(((left, top), anchor, line)) - left += line_spacing - else: - widths = [] - max_width: float = 0 - for line in lines: - line_width = self.textlength( - line, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - widths.append(line_width) - max_width = max(max_width, line_width) - - if anchor[1] == "m": - top -= (len(lines) - 1) * line_spacing / 2.0 - elif anchor[1] == "d": - top -= (len(lines) - 1) * line_spacing - - for idx, line in enumerate(lines): - left = xy[0] - width_difference = max_width - widths[idx] - - # align by align parameter - if align in ("left", "justify"): - pass - elif align == "center": - left += width_difference / 2.0 - elif align == "right": - left += width_difference - else: - msg = 'align must be "left", "center", "right" or "justify"' - raise ValueError(msg) - - if ( - align == "justify" - and width_difference != 0 - and idx != len(lines) - 1 - ): - words = line.split(" " if isinstance(text, str) else b" ") - if len(words) > 1: - # align left by anchor - if anchor[0] == "m": - left -= max_width / 2.0 - elif anchor[0] == "r": - left -= max_width - - word_widths = [ - self.textlength( - word, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - for word in words - ] - word_anchor = "l" + anchor[1] - width_difference = max_width - sum(word_widths) - for i, word in enumerate(words): - parts.append(((left, top), word_anchor, word)) - left += word_widths[i] + width_difference / (len(words) - 1) - top += line_spacing - continue - - # align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - parts.append(((left, top), anchor, line)) - top += line_spacing - - return font, parts - - def multiline_text( - self, - xy: tuple[float, float], - text: AnyStr, - fill: _Ink | None = None, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - anchor: str | None = None, - spacing: float = 4, - align: str = "left", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - stroke_fill: _Ink | None = None, - embedded_color: bool = False, - *, - font_size: float | None = None, - ) -> None: - font, lines = self._prepare_multiline_text( - xy, - text, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - embedded_color, - font_size, - ) - - for xy, anchor, line in lines: - self.text( - xy, - line, - fill, - font, - anchor, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - stroke_fill=stroke_fill, - embedded_color=embedded_color, - ) - - def textlength( - self, - text: AnyStr, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - embedded_color: bool = False, - *, - font_size: float | None = None, - ) -> float: - """Get the length of a given string, in pixels with 1/64 precision.""" - if self._multiline_check(text): - msg = "can't measure length of multiline text" - raise ValueError(msg) - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - mode = "RGBA" if embedded_color else self.fontmode - return font.getlength(text, mode, direction, features, language) - - def textbbox( - self, - xy: tuple[float, float], - text: AnyStr, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - anchor: str | None = None, - spacing: float = 4, - align: str = "left", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - embedded_color: bool = False, - *, - font_size: float | None = None, - ) -> tuple[float, float, float, float]: - """Get the bounding box of a given string, in pixels.""" - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - if self._multiline_check(text): - return self.multiline_textbbox( - xy, - text, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - embedded_color, - ) - - mode = "RGBA" if embedded_color else self.fontmode - bbox = font.getbbox( - text, mode, direction, features, language, stroke_width, anchor - ) - return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] - - def multiline_textbbox( - self, - xy: tuple[float, float], - text: AnyStr, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - anchor: str | None = None, - spacing: float = 4, - align: str = "left", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - embedded_color: bool = False, - *, - font_size: float | None = None, - ) -> tuple[float, float, float, float]: - font, lines = self._prepare_multiline_text( - xy, - text, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - embedded_color, - font_size, - ) - - bbox: tuple[float, float, float, float] | None = None - - for xy, anchor, line in lines: - bbox_line = self.textbbox( - xy, - line, - font, - anchor, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - embedded_color=embedded_color, - ) - if bbox is None: - bbox = bbox_line - else: - bbox = ( - min(bbox[0], bbox_line[0]), - min(bbox[1], bbox_line[1]), - max(bbox[2], bbox_line[2]), - max(bbox[3], bbox_line[3]), - ) - - if bbox is None: - return xy[0], xy[1], xy[0], xy[1] - return bbox - - -def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: - """ - A simple 2D drawing interface for PIL images. - - :param im: The image to draw in. - :param mode: Optional mode to use for color values. For RGB - images, this argument can be RGB or RGBA (to blend the - drawing into the image). For all other modes, this argument - must be the same as the image mode. If omitted, the mode - defaults to the mode of the image. - """ - try: - return getattr(im, "getdraw")(mode) - except AttributeError: - return ImageDraw(im, mode) - - -def getdraw( - im: Image.Image | None = None, hints: list[str] | None = None -) -> tuple[ImageDraw2.Draw | None, ModuleType]: - """ - :param im: The image to draw in. - :param hints: An optional list of hints. Deprecated. - :returns: A (drawing context, drawing resource factory) tuple. - """ - if hints is not None: - deprecate("'hints' parameter", 12) - from . import ImageDraw2 - - draw = ImageDraw2.Draw(im) if im is not None else None - return draw, ImageDraw2 - - -def floodfill( - image: Image.Image, - xy: tuple[int, int], - value: float | tuple[int, ...], - border: float | tuple[int, ...] | None = None, - thresh: float = 0, -) -> None: - """ - .. warning:: This method is experimental. - - Fills a bounded region with a given color. - - :param image: Target image. - :param xy: Seed position (a 2-item coordinate tuple). See - :ref:`coordinate-system`. - :param value: Fill color. - :param border: Optional border value. If given, the region consists of - pixels with a color different from the border color. If not given, - the region consists of pixels having the same color as the seed - pixel. - :param thresh: Optional threshold value which specifies a maximum - tolerable difference of a pixel value from the 'background' in - order for it to be replaced. Useful for filling regions of - non-homogeneous, but similar, colors. - """ - # based on an implementation by Eric S. Raymond - # amended by yo1995 @20180806 - pixel = image.load() - assert pixel is not None - x, y = xy - try: - background = pixel[x, y] - if _color_diff(value, background) <= thresh: - return # seed point already has fill color - pixel[x, y] = value - except (ValueError, IndexError): - return # seed point outside image - edge = {(x, y)} - # use a set to keep record of current and previous edge pixels - # to reduce memory consumption - full_edge = set() - while edge: - new_edge = set() - for x, y in edge: # 4 adjacent method - for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): - # If already processed, or if a coordinate is negative, skip - if (s, t) in full_edge or s < 0 or t < 0: - continue - try: - p = pixel[s, t] - except (ValueError, IndexError): - pass - else: - full_edge.add((s, t)) - if border is None: - fill = _color_diff(p, background) <= thresh - else: - fill = p not in (value, border) - if fill: - pixel[s, t] = value - new_edge.add((s, t)) - full_edge = edge # discard pixels processed - edge = new_edge - - -def _compute_regular_polygon_vertices( - bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float -) -> list[tuple[float, float]]: - """ - Generate a list of vertices for a 2D regular polygon. - - :param bounding_circle: The bounding circle is a sequence defined - by a point and radius. The polygon is inscribed in this circle. - (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) - :param n_sides: Number of sides - (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) - :param rotation: Apply an arbitrary rotation to the polygon - (e.g. ``rotation=90``, applies a 90 degree rotation) - :return: List of regular polygon vertices - (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) - - How are the vertices computed? - 1. Compute the following variables - - theta: Angle between the apothem & the nearest polygon vertex - - side_length: Length of each polygon edge - - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) - - polygon_radius: Polygon radius (last element of bounding_circle) - - angles: Location of each polygon vertex in polar grid - (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) - - 2. For each angle in angles, get the polygon vertex at that angle - The vertex is computed using the equation below. - X= xcos(φ) + ysin(φ) - Y= −xsin(φ) + ycos(φ) - - Note: - φ = angle in degrees - x = 0 - y = polygon_radius - - The formula above assumes rotation around the origin. - In our case, we are rotating around the centroid. - To account for this, we use the formula below - X = xcos(φ) + ysin(φ) + centroid_x - Y = −xsin(φ) + ycos(φ) + centroid_y - """ - # 1. Error Handling - # 1.1 Check `n_sides` has an appropriate value - if not isinstance(n_sides, int): - msg = "n_sides should be an int" # type: ignore[unreachable] - raise TypeError(msg) - if n_sides < 3: - msg = "n_sides should be an int > 2" - raise ValueError(msg) - - # 1.2 Check `bounding_circle` has an appropriate value - if not isinstance(bounding_circle, (list, tuple)): - msg = "bounding_circle should be a sequence" - raise TypeError(msg) - - if len(bounding_circle) == 3: - if not all(isinstance(i, (int, float)) for i in bounding_circle): - msg = "bounding_circle should only contain numeric data" - raise ValueError(msg) - - *centroid, polygon_radius = cast(list[float], list(bounding_circle)) - elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)): - if not all( - isinstance(i, (int, float)) for i in bounding_circle[0] - ) or not isinstance(bounding_circle[1], (int, float)): - msg = "bounding_circle should only contain numeric data" - raise ValueError(msg) - - if len(bounding_circle[0]) != 2: - msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" - raise ValueError(msg) - - centroid = cast(list[float], list(bounding_circle[0])) - polygon_radius = cast(float, bounding_circle[1]) - else: - msg = ( - "bounding_circle should contain 2D coordinates " - "and a radius (e.g. (x, y, r) or ((x, y), r) )" - ) - raise ValueError(msg) - - if polygon_radius <= 0: - msg = "bounding_circle radius should be > 0" - raise ValueError(msg) - - # 1.3 Check `rotation` has an appropriate value - if not isinstance(rotation, (int, float)): - msg = "rotation should be an int or float" # type: ignore[unreachable] - raise ValueError(msg) - - # 2. Define Helper Functions - def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]: - return ( - round( - point[0] * math.cos(math.radians(360 - degrees)) - - point[1] * math.sin(math.radians(360 - degrees)) - + centroid[0], - 2, - ), - round( - point[1] * math.cos(math.radians(360 - degrees)) - + point[0] * math.sin(math.radians(360 - degrees)) - + centroid[1], - 2, - ), - ) - - def _compute_polygon_vertex(angle: float) -> tuple[float, float]: - start_point = [polygon_radius, 0] - return _apply_rotation(start_point, angle) - - def _get_angles(n_sides: int, rotation: float) -> list[float]: - angles = [] - degrees = 360 / n_sides - # Start with the bottom left polygon vertex - current_angle = (270 - 0.5 * degrees) + rotation - for _ in range(n_sides): - angles.append(current_angle) - current_angle += degrees - if current_angle > 360: - current_angle -= 360 - return angles - - # 3. Variable Declarations - angles = _get_angles(n_sides, rotation) - - # 4. Compute Vertices - return [_compute_polygon_vertex(angle) for angle in angles] - - -def _color_diff( - color1: float | tuple[int, ...], color2: float | tuple[int, ...] -) -> float: - """ - Uses 1-norm distance to calculate difference between two values. - """ - first = color1 if isinstance(color1, tuple) else (color1,) - second = color2 if isinstance(color2, tuple) else (color2,) - - return sum(abs(first[i] - second[i]) for i in range(len(second))) diff --git a/python_modules/PIL/ImageDraw2.py b/python_modules/PIL/ImageDraw2.py deleted file mode 100644 index 3d68658ed..000000000 --- a/python_modules/PIL/ImageDraw2.py +++ /dev/null @@ -1,243 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# WCK-style drawing interface operations -# -# History: -# 2003-12-07 fl created -# 2005-05-15 fl updated; added to PIL as ImageDraw2 -# 2005-05-15 fl added text support -# 2005-05-20 fl added arc/chord/pieslice support -# -# Copyright (c) 2003-2005 by Secret Labs AB -# Copyright (c) 2003-2005 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - - -""" -(Experimental) WCK-style drawing interface operations - -.. seealso:: :py:mod:`PIL.ImageDraw` -""" -from __future__ import annotations - -from typing import Any, AnyStr, BinaryIO - -from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath -from ._typing import Coords, StrOrBytesPath - - -class Pen: - """Stores an outline color and width.""" - - def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None: - self.color = ImageColor.getrgb(color) - self.width = width - - -class Brush: - """Stores a fill color""" - - def __init__(self, color: str, opacity: int = 255) -> None: - self.color = ImageColor.getrgb(color) - - -class Font: - """Stores a TrueType font and color""" - - def __init__( - self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12 - ) -> None: - # FIXME: add support for bitmap fonts - self.color = ImageColor.getrgb(color) - self.font = ImageFont.truetype(file, size) - - -class Draw: - """ - (Experimental) WCK-style drawing interface - """ - - def __init__( - self, - image: Image.Image | str, - size: tuple[int, int] | list[int] | None = None, - color: float | tuple[float, ...] | str | None = None, - ) -> None: - if isinstance(image, str): - if size is None: - msg = "If image argument is mode string, size must be a list or tuple" - raise ValueError(msg) - image = Image.new(image, size, color) - self.draw = ImageDraw.Draw(image) - self.image = image - self.transform: tuple[float, float, float, float, float, float] | None = None - - def flush(self) -> Image.Image: - return self.image - - def render( - self, - op: str, - xy: Coords, - pen: Pen | Brush | None, - brush: Brush | Pen | None = None, - **kwargs: Any, - ) -> None: - # handle color arguments - outline = fill = None - width = 1 - if isinstance(pen, Pen): - outline = pen.color - width = pen.width - elif isinstance(brush, Pen): - outline = brush.color - width = brush.width - if isinstance(brush, Brush): - fill = brush.color - elif isinstance(pen, Brush): - fill = pen.color - # handle transformation - if self.transform: - path = ImagePath.Path(xy) - path.transform(self.transform) - xy = path - # render the item - if op in ("arc", "line"): - kwargs.setdefault("fill", outline) - else: - kwargs.setdefault("fill", fill) - kwargs.setdefault("outline", outline) - if op == "line": - kwargs.setdefault("width", width) - getattr(self.draw, op)(xy, **kwargs) - - def settransform(self, offset: tuple[float, float]) -> None: - """Sets a transformation offset.""" - (xoffset, yoffset) = offset - self.transform = (1, 0, xoffset, 0, 1, yoffset) - - def arc( - self, - xy: Coords, - pen: Pen | Brush | None, - start: float, - end: float, - *options: Any, - ) -> None: - """ - Draws an arc (a portion of a circle outline) between the start and end - angles, inside the given bounding box. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` - """ - self.render("arc", xy, pen, *options, start=start, end=end) - - def chord( - self, - xy: Coords, - pen: Pen | Brush | None, - start: float, - end: float, - *options: Any, - ) -> None: - """ - Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points - with a straight line. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` - """ - self.render("chord", xy, pen, *options, start=start, end=end) - - def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: - """ - Draws an ellipse inside the given bounding box. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` - """ - self.render("ellipse", xy, pen, *options) - - def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: - """ - Draws a line between the coordinates in the ``xy`` list. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` - """ - self.render("line", xy, pen, *options) - - def pieslice( - self, - xy: Coords, - pen: Pen | Brush | None, - start: float, - end: float, - *options: Any, - ) -> None: - """ - Same as arc, but also draws straight lines between the end points and the - center of the bounding box. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` - """ - self.render("pieslice", xy, pen, *options, start=start, end=end) - - def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: - """ - Draws a polygon. - - The polygon outline consists of straight lines between the given - coordinates, plus a straight line between the last and the first - coordinate. - - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` - """ - self.render("polygon", xy, pen, *options) - - def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: - """ - Draws a rectangle. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` - """ - self.render("rectangle", xy, pen, *options) - - def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None: - """ - Draws the string at the given position. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` - """ - if self.transform: - path = ImagePath.Path(xy) - path.transform(self.transform) - xy = path - self.draw.text(xy, text, font=font.font, fill=font.color) - - def textbbox( - self, xy: tuple[float, float], text: AnyStr, font: Font - ) -> tuple[float, float, float, float]: - """ - Returns bounding box (in pixels) of given text. - - :return: ``(left, top, right, bottom)`` bounding box - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` - """ - if self.transform: - path = ImagePath.Path(xy) - path.transform(self.transform) - xy = path - return self.draw.textbbox(xy, text, font=font.font) - - def textlength(self, text: AnyStr, font: Font) -> float: - """ - Returns length (in pixels) of given text. - This is the amount by which following text should be offset. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength` - """ - return self.draw.textlength(text, font=font.font) diff --git a/python_modules/PIL/ImageEnhance.py b/python_modules/PIL/ImageEnhance.py deleted file mode 100644 index 0e7e6dd8a..000000000 --- a/python_modules/PIL/ImageEnhance.py +++ /dev/null @@ -1,113 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# image enhancement classes -# -# For a background, see "Image Processing By Interpolation and -# Extrapolation", Paul Haeberli and Douglas Voorhies. Available -# at http://www.graficaobscura.com/interp/index.html -# -# History: -# 1996-03-23 fl Created -# 2009-06-16 fl Fixed mean calculation -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFilter, ImageStat - - -class _Enhance: - image: Image.Image - degenerate: Image.Image - - def enhance(self, factor: float) -> Image.Image: - """ - Returns an enhanced image. - - :param factor: A floating point value controlling the enhancement. - Factor 1.0 always returns a copy of the original image, - lower factors mean less color (brightness, contrast, - etc), and higher values more. There are no restrictions - on this value. - :rtype: :py:class:`~PIL.Image.Image` - """ - return Image.blend(self.degenerate, self.image, factor) - - -class Color(_Enhance): - """Adjust image color balance. - - This class can be used to adjust the colour balance of an image, in - a manner similar to the controls on a colour TV set. An enhancement - factor of 0.0 gives a black and white image. A factor of 1.0 gives - the original image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - self.intermediate_mode = "L" - if "A" in image.getbands(): - self.intermediate_mode = "LA" - - if self.intermediate_mode != image.mode: - image = image.convert(self.intermediate_mode).convert(image.mode) - self.degenerate = image - - -class Contrast(_Enhance): - """Adjust image contrast. - - This class can be used to control the contrast of an image, similar - to the contrast control on a TV set. An enhancement factor of 0.0 - gives a solid gray image. A factor of 1.0 gives the original image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - if image.mode != "L": - image = image.convert("L") - mean = int(ImageStat.Stat(image).mean[0] + 0.5) - self.degenerate = Image.new("L", image.size, mean) - if self.degenerate.mode != self.image.mode: - self.degenerate = self.degenerate.convert(self.image.mode) - - if "A" in self.image.getbands(): - self.degenerate.putalpha(self.image.getchannel("A")) - - -class Brightness(_Enhance): - """Adjust image brightness. - - This class can be used to control the brightness of an image. An - enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the - original image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - self.degenerate = Image.new(image.mode, image.size, 0) - - if "A" in image.getbands(): - self.degenerate.putalpha(image.getchannel("A")) - - -class Sharpness(_Enhance): - """Adjust image sharpness. - - This class can be used to adjust the sharpness of an image. An - enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the - original image, and a factor of 2.0 gives a sharpened image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - self.degenerate = image.filter(ImageFilter.SMOOTH) - - if "A" in image.getbands(): - self.degenerate.putalpha(image.getchannel("A")) diff --git a/python_modules/PIL/ImageFile.py b/python_modules/PIL/ImageFile.py deleted file mode 100644 index bf556a2c6..000000000 --- a/python_modules/PIL/ImageFile.py +++ /dev/null @@ -1,922 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# base class for image file handlers -# -# history: -# 1995-09-09 fl Created -# 1996-03-11 fl Fixed load mechanism. -# 1996-04-15 fl Added pcx/xbm decoders. -# 1996-04-30 fl Added encoders. -# 1996-12-14 fl Added load helpers -# 1997-01-11 fl Use encode_to_file where possible -# 1997-08-27 fl Flush output in _save -# 1998-03-05 fl Use memory mapping for some modes -# 1999-02-04 fl Use memory mapping also for "I;16" and "I;16B" -# 1999-05-31 fl Added image parser -# 2000-10-12 fl Set readonly flag on memory-mapped images -# 2002-03-20 fl Use better messages for common decoder errors -# 2003-04-21 fl Fall back on mmap/map_buffer if map is not available -# 2003-10-30 fl Added StubImageFile class -# 2004-02-25 fl Made incremental parser more robust -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1995-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import abc -import io -import itertools -import logging -import os -import struct -from typing import IO, Any, NamedTuple, cast - -from . import ExifTags, Image -from ._deprecate import deprecate -from ._util import DeferredError, is_path - -TYPE_CHECKING = False -if TYPE_CHECKING: - from ._typing import StrOrBytesPath - -logger = logging.getLogger(__name__) - -MAXBLOCK = 65536 - -SAFEBLOCK = 1024 * 1024 - -LOAD_TRUNCATED_IMAGES = False -"""Whether or not to load truncated image files. User code may change this.""" - -ERRORS = { - -1: "image buffer overrun error", - -2: "decoding error", - -3: "unknown error", - -8: "bad configuration", - -9: "out of memory error", -} -""" -Dict of known error codes returned from :meth:`.PyDecoder.decode`, -:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and -:meth:`.PyEncoder.encode_to_file`. -""" - - -# -# -------------------------------------------------------------------- -# Helpers - - -def _get_oserror(error: int, *, encoder: bool) -> OSError: - try: - msg = Image.core.getcodecstatus(error) - except AttributeError: - msg = ERRORS.get(error) - if not msg: - msg = f"{'encoder' if encoder else 'decoder'} error {error}" - msg += f" when {'writing' if encoder else 'reading'} image file" - return OSError(msg) - - -def raise_oserror(error: int) -> OSError: - deprecate( - "raise_oserror", - 12, - action="It is only useful for translating error codes returned by a codec's " - "decode() method, which ImageFile already does automatically.", - ) - raise _get_oserror(error, encoder=False) - - -def _tilesort(t: _Tile) -> int: - # sort on offset - return t[2] - - -class _Tile(NamedTuple): - codec_name: str - extents: tuple[int, int, int, int] | None - offset: int = 0 - args: tuple[Any, ...] | str | None = None - - -# -# -------------------------------------------------------------------- -# ImageFile base class - - -class ImageFile(Image.Image): - """Base class for image file format handlers.""" - - def __init__( - self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None - ) -> None: - super().__init__() - - self._min_frame = 0 - - self.custom_mimetype: str | None = None - - self.tile: list[_Tile] = [] - """ A list of tile descriptors """ - - self.readonly = 1 # until we know better - - self.decoderconfig: tuple[Any, ...] = () - self.decodermaxblock = MAXBLOCK - - if is_path(fp): - # filename - self.fp = open(fp, "rb") - self.filename = os.fspath(fp) - self._exclusive_fp = True - else: - # stream - self.fp = cast(IO[bytes], fp) - self.filename = filename if filename is not None else "" - # can be overridden - self._exclusive_fp = False - - try: - try: - self._open() - except ( - IndexError, # end of data - TypeError, # end of data (ord) - KeyError, # unsupported mode - EOFError, # got header but not the first frame - struct.error, - ) as v: - raise SyntaxError(v) from v - - if not self.mode or self.size[0] <= 0 or self.size[1] <= 0: - msg = "not identified by this driver" - raise SyntaxError(msg) - except BaseException: - # close the file only if we have opened it this constructor - if self._exclusive_fp: - self.fp.close() - raise - - def _open(self) -> None: - pass - - def _close_fp(self): - if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() - - def close(self) -> None: - """ - Closes the file pointer, if possible. - - This operation will destroy the image core and release its memory. - The image data will be unusable afterward. - - This function is required to close images that have multiple frames or - have not had their file read and closed by the - :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for - more information. - """ - try: - self._close_fp() - self.fp = None - except Exception as msg: - logger.debug("Error closing: %s", msg) - - super().close() - - def get_child_images(self) -> list[ImageFile]: - child_images = [] - exif = self.getexif() - ifds = [] - if ExifTags.Base.SubIFDs in exif: - subifd_offsets = exif[ExifTags.Base.SubIFDs] - if subifd_offsets: - if not isinstance(subifd_offsets, tuple): - subifd_offsets = (subifd_offsets,) - for subifd_offset in subifd_offsets: - ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) - ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) - if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): - assert exif._info is not None - ifds.append((ifd1, exif._info.next)) - - offset = None - for ifd, ifd_offset in ifds: - assert self.fp is not None - current_offset = self.fp.tell() - if offset is None: - offset = current_offset - - fp = self.fp - if ifd is not None: - thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset) - if thumbnail_offset is not None: - thumbnail_offset += getattr(self, "_exif_offset", 0) - self.fp.seek(thumbnail_offset) - - length = ifd.get(ExifTags.Base.JpegIFByteCount) - assert isinstance(length, int) - data = self.fp.read(length) - fp = io.BytesIO(data) - - with Image.open(fp) as im: - from . import TiffImagePlugin - - if thumbnail_offset is None and isinstance( - im, TiffImagePlugin.TiffImageFile - ): - im._frame_pos = [ifd_offset] - im._seek(0) - im.load() - child_images.append(im) - - if offset is not None: - assert self.fp is not None - self.fp.seek(offset) - return child_images - - def get_format_mimetype(self) -> str | None: - if self.custom_mimetype: - return self.custom_mimetype - if self.format is not None: - return Image.MIME.get(self.format.upper()) - return None - - def __getstate__(self) -> list[Any]: - return super().__getstate__() + [self.filename] - - def __setstate__(self, state: list[Any]) -> None: - self.tile = [] - if len(state) > 5: - self.filename = state[5] - super().__setstate__(state) - - def verify(self) -> None: - """Check file integrity""" - - # raise exception if something's wrong. must be called - # directly after open, and closes file when finished. - if self._exclusive_fp: - self.fp.close() - self.fp = None - - def load(self) -> Image.core.PixelAccess | None: - """Load image data based on tile list""" - - if not self.tile and self._im is None: - msg = "cannot load this image" - raise OSError(msg) - - pixel = Image.Image.load(self) - if not self.tile: - return pixel - - self.map: mmap.mmap | None = None - use_mmap = self.filename and len(self.tile) == 1 - - readonly = 0 - - # look for read/seek overrides - if hasattr(self, "load_read"): - read = self.load_read - # don't use mmap if there are custom read/seek functions - use_mmap = False - else: - read = self.fp.read - - if hasattr(self, "load_seek"): - seek = self.load_seek - use_mmap = False - else: - seek = self.fp.seek - - if use_mmap: - # try memory mapping - decoder_name, extents, offset, args = self.tile[0] - if isinstance(args, str): - args = (args, 0, 1) - if ( - decoder_name == "raw" - and isinstance(args, tuple) - and len(args) >= 3 - and args[0] == self.mode - and args[0] in Image._MAPMODES - ): - try: - # use mmap, if possible - import mmap - - with open(self.filename) as fp: - self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) - if offset + self.size[1] * args[1] > self.map.size(): - msg = "buffer is not large enough" - raise OSError(msg) - self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, offset, args - ) - readonly = 1 - # After trashing self.im, - # we might need to reload the palette data. - if self.palette: - self.palette.dirty = 1 - except (AttributeError, OSError, ImportError): - self.map = None - - self.load_prepare() - err_code = -3 # initialize to unknown error - if not self.map: - # sort tiles in file order - self.tile.sort(key=_tilesort) - - # FIXME: This is a hack to handle TIFF's JpegTables tag. - prefix = getattr(self, "tile_prefix", b"") - - # Remove consecutive duplicates that only differ by their offset - self.tile = [ - list(tiles)[-1] - for _, tiles in itertools.groupby( - self.tile, lambda tile: (tile[0], tile[1], tile[3]) - ) - ] - for i, (decoder_name, extents, offset, args) in enumerate(self.tile): - seek(offset) - decoder = Image._getdecoder( - self.mode, decoder_name, args, self.decoderconfig - ) - try: - decoder.setimage(self.im, extents) - if decoder.pulls_fd: - decoder.setfd(self.fp) - err_code = decoder.decode(b"")[1] - else: - b = prefix - while True: - read_bytes = self.decodermaxblock - if i + 1 < len(self.tile): - next_offset = self.tile[i + 1].offset - if next_offset > offset: - read_bytes = next_offset - offset - try: - s = read(read_bytes) - except (IndexError, struct.error) as e: - # truncated png/gif - if LOAD_TRUNCATED_IMAGES: - break - else: - msg = "image file is truncated" - raise OSError(msg) from e - - if not s: # truncated jpeg - if LOAD_TRUNCATED_IMAGES: - break - else: - msg = ( - "image file is truncated " - f"({len(b)} bytes not processed)" - ) - raise OSError(msg) - - b = b + s - n, err_code = decoder.decode(b) - if n < 0: - break - b = b[n:] - finally: - # Need to cleanup here to prevent leaks - decoder.cleanup() - - self.tile = [] - self.readonly = readonly - - self.load_end() - - if self._exclusive_fp and self._close_exclusive_fp_after_loading: - self.fp.close() - self.fp = None - - if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0: - # still raised if decoder fails to return anything - raise _get_oserror(err_code, encoder=False) - - return Image.Image.load(self) - - def load_prepare(self) -> None: - # create image memory if necessary - if self._im is None: - self.im = Image.core.new(self.mode, self.size) - # create palette (optional) - if self.mode == "P": - Image.Image.load(self) - - def load_end(self) -> None: - # may be overridden - pass - - # may be defined for contained formats - # def load_seek(self, pos: int) -> None: - # pass - - # may be defined for blocked formats (e.g. PNG) - # def load_read(self, read_bytes: int) -> bytes: - # pass - - def _seek_check(self, frame: int) -> bool: - if ( - frame < self._min_frame - # Only check upper limit on frames if additional seek operations - # are not required to do so - or ( - not (hasattr(self, "_n_frames") and self._n_frames is None) - and frame >= getattr(self, "n_frames") + self._min_frame - ) - ): - msg = "attempt to seek outside sequence" - raise EOFError(msg) - - return self.tell() != frame - - -class StubHandler(abc.ABC): - def open(self, im: StubImageFile) -> None: - pass - - @abc.abstractmethod - def load(self, im: StubImageFile) -> Image.Image: - pass - - -class StubImageFile(ImageFile, metaclass=abc.ABCMeta): - """ - Base class for stub image loaders. - - A stub loader is an image loader that can identify files of a - certain format, but relies on external code to load the file. - """ - - @abc.abstractmethod - def _open(self) -> None: - pass - - def load(self) -> Image.core.PixelAccess | None: - loader = self._load() - if loader is None: - msg = f"cannot find loader for this {self.format} file" - raise OSError(msg) - image = loader.load(self) - assert image is not None - # become the other object (!) - self.__class__ = image.__class__ # type: ignore[assignment] - self.__dict__ = image.__dict__ - return image.load() - - @abc.abstractmethod - def _load(self) -> StubHandler | None: - """(Hook) Find actual image loader.""" - pass - - -class Parser: - """ - Incremental image parser. This class implements the standard - feed/close consumer interface. - """ - - incremental = None - image: Image.Image | None = None - data: bytes | None = None - decoder: Image.core.ImagingDecoder | PyDecoder | None = None - offset = 0 - finished = 0 - - def reset(self) -> None: - """ - (Consumer) Reset the parser. Note that you can only call this - method immediately after you've created a parser; parser - instances cannot be reused. - """ - assert self.data is None, "cannot reuse parsers" - - def feed(self, data: bytes) -> None: - """ - (Consumer) Feed data to the parser. - - :param data: A string buffer. - :exception OSError: If the parser failed to parse the image file. - """ - # collect data - - if self.finished: - return - - if self.data is None: - self.data = data - else: - self.data = self.data + data - - # parse what we have - if self.decoder: - if self.offset > 0: - # skip header - skip = min(len(self.data), self.offset) - self.data = self.data[skip:] - self.offset = self.offset - skip - if self.offset > 0 or not self.data: - return - - n, e = self.decoder.decode(self.data) - - if n < 0: - # end of stream - self.data = None - self.finished = 1 - if e < 0: - # decoding error - self.image = None - raise _get_oserror(e, encoder=False) - else: - # end of image - return - self.data = self.data[n:] - - elif self.image: - # if we end up here with no decoder, this file cannot - # be incrementally parsed. wait until we've gotten all - # available data - pass - - else: - # attempt to open this file - try: - with io.BytesIO(self.data) as fp: - im = Image.open(fp) - except OSError: - pass # not enough data - else: - flag = hasattr(im, "load_seek") or hasattr(im, "load_read") - if flag or len(im.tile) != 1: - # custom load code, or multiple tiles - self.decode = None - else: - # initialize decoder - im.load_prepare() - d, e, o, a = im.tile[0] - im.tile = [] - self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig) - self.decoder.setimage(im.im, e) - - # calculate decoder offset - self.offset = o - if self.offset <= len(self.data): - self.data = self.data[self.offset :] - self.offset = 0 - - self.image = im - - def __enter__(self) -> Parser: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def close(self) -> Image.Image: - """ - (Consumer) Close the stream. - - :returns: An image object. - :exception OSError: If the parser failed to parse the image file either - because it cannot be identified or cannot be - decoded. - """ - # finish decoding - if self.decoder: - # get rid of what's left in the buffers - self.feed(b"") - self.data = self.decoder = None - if not self.finished: - msg = "image was incomplete" - raise OSError(msg) - if not self.image: - msg = "cannot parse this image" - raise OSError(msg) - if self.data: - # incremental parsing not possible; reopen the file - # not that we have all data - with io.BytesIO(self.data) as fp: - try: - self.image = Image.open(fp) - finally: - self.image.load() - return self.image - - -# -------------------------------------------------------------------- - - -def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None: - """Helper to save image based on tile list - - :param im: Image object. - :param fp: File object. - :param tile: Tile list. - :param bufsize: Optional buffer size - """ - - im.load() - if not hasattr(im, "encoderconfig"): - im.encoderconfig = () - tile.sort(key=_tilesort) - # FIXME: make MAXBLOCK a configuration parameter - # It would be great if we could have the encoder specify what it needs - # But, it would need at least the image size in most cases. RawEncode is - # a tricky case. - bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c - try: - fh = fp.fileno() - fp.flush() - _encode_tile(im, fp, tile, bufsize, fh) - except (AttributeError, io.UnsupportedOperation) as exc: - _encode_tile(im, fp, tile, bufsize, None, exc) - if hasattr(fp, "flush"): - fp.flush() - - -def _encode_tile( - im: Image.Image, - fp: IO[bytes], - tile: list[_Tile], - bufsize: int, - fh: int | None, - exc: BaseException | None = None, -) -> None: - for encoder_name, extents, offset, args in tile: - if offset > 0: - fp.seek(offset) - encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig) - try: - encoder.setimage(im.im, extents) - if encoder.pushes_fd: - encoder.setfd(fp) - errcode = encoder.encode_to_pyfd()[1] - else: - if exc: - # compress to Python file-compatible object - while True: - errcode, data = encoder.encode(bufsize)[1:] - fp.write(data) - if errcode: - break - else: - # slight speedup: compress to real file object - assert fh is not None - errcode = encoder.encode_to_file(fh, bufsize) - if errcode < 0: - raise _get_oserror(errcode, encoder=True) from exc - finally: - encoder.cleanup() - - -def _safe_read(fp: IO[bytes], size: int) -> bytes: - """ - Reads large blocks in a safe way. Unlike fp.read(n), this function - doesn't trust the user. If the requested size is larger than - SAFEBLOCK, the file is read block by block. - - :param fp: File handle. Must implement a read method. - :param size: Number of bytes to read. - :returns: A string containing size bytes of data. - - Raises an OSError if the file is truncated and the read cannot be completed - - """ - if size <= 0: - return b"" - if size <= SAFEBLOCK: - data = fp.read(size) - if len(data) < size: - msg = "Truncated File Read" - raise OSError(msg) - return data - blocks: list[bytes] = [] - remaining_size = size - while remaining_size > 0: - block = fp.read(min(remaining_size, SAFEBLOCK)) - if not block: - break - blocks.append(block) - remaining_size -= len(block) - if sum(len(block) for block in blocks) < size: - msg = "Truncated File Read" - raise OSError(msg) - return b"".join(blocks) - - -class PyCodecState: - def __init__(self) -> None: - self.xsize = 0 - self.ysize = 0 - self.xoff = 0 - self.yoff = 0 - - def extents(self) -> tuple[int, int, int, int]: - return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize - - -class PyCodec: - fd: IO[bytes] | None - - def __init__(self, mode: str, *args: Any) -> None: - self.im: Image.core.ImagingCore | None = None - self.state = PyCodecState() - self.fd = None - self.mode = mode - self.init(args) - - def init(self, args: tuple[Any, ...]) -> None: - """ - Override to perform codec specific initialization - - :param args: Tuple of arg items from the tile entry - :returns: None - """ - self.args = args - - def cleanup(self) -> None: - """ - Override to perform codec specific cleanup - - :returns: None - """ - pass - - def setfd(self, fd: IO[bytes]) -> None: - """ - Called from ImageFile to set the Python file-like object - - :param fd: A Python file-like object - :returns: None - """ - self.fd = fd - - def setimage( - self, - im: Image.core.ImagingCore, - extents: tuple[int, int, int, int] | None = None, - ) -> None: - """ - Called from ImageFile to set the core output image for the codec - - :param im: A core image object - :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle - for this tile - :returns: None - """ - - # following c code - self.im = im - - if extents: - (x0, y0, x1, y1) = extents - else: - (x0, y0, x1, y1) = (0, 0, 0, 0) - - if x0 == 0 and x1 == 0: - self.state.xsize, self.state.ysize = self.im.size - else: - self.state.xoff = x0 - self.state.yoff = y0 - self.state.xsize = x1 - x0 - self.state.ysize = y1 - y0 - - if self.state.xsize <= 0 or self.state.ysize <= 0: - msg = "Size cannot be negative" - raise ValueError(msg) - - if ( - self.state.xsize + self.state.xoff > self.im.size[0] - or self.state.ysize + self.state.yoff > self.im.size[1] - ): - msg = "Tile cannot extend outside image" - raise ValueError(msg) - - -class PyDecoder(PyCodec): - """ - Python implementation of a format decoder. Override this class and - add the decoding logic in the :meth:`decode` method. - - See :ref:`Writing Your Own File Codec in Python` - """ - - _pulls_fd = False - - @property - def pulls_fd(self) -> bool: - return self._pulls_fd - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - """ - Override to perform the decoding process. - - :param buffer: A bytes object with the data to be decoded. - :returns: A tuple of ``(bytes consumed, errcode)``. - If finished with decoding return -1 for the bytes consumed. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - msg = "unavailable in base decoder" - raise NotImplementedError(msg) - - def set_as_raw( - self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = () - ) -> None: - """ - Convenience method to set the internal image from a stream of raw data - - :param data: Bytes to be set - :param rawmode: The rawmode to be used for the decoder. - If not specified, it will default to the mode of the image - :param extra: Extra arguments for the decoder. - :returns: None - """ - - if not rawmode: - rawmode = self.mode - d = Image._getdecoder(self.mode, "raw", rawmode, extra) - assert self.im is not None - d.setimage(self.im, self.state.extents()) - s = d.decode(data) - - if s[0] >= 0: - msg = "not enough image data" - raise ValueError(msg) - if s[1] != 0: - msg = "cannot decode image data" - raise ValueError(msg) - - -class PyEncoder(PyCodec): - """ - Python implementation of a format encoder. Override this class and - add the decoding logic in the :meth:`encode` method. - - See :ref:`Writing Your Own File Codec in Python` - """ - - _pushes_fd = False - - @property - def pushes_fd(self) -> bool: - return self._pushes_fd - - def encode(self, bufsize: int) -> tuple[int, int, bytes]: - """ - Override to perform the encoding process. - - :param bufsize: Buffer size. - :returns: A tuple of ``(bytes encoded, errcode, bytes)``. - If finished with encoding return 1 for the error code. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - msg = "unavailable in base encoder" - raise NotImplementedError(msg) - - def encode_to_pyfd(self) -> tuple[int, int]: - """ - If ``pushes_fd`` is ``True``, then this method will be used, - and ``encode()`` will only be called once. - - :returns: A tuple of ``(bytes consumed, errcode)``. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - if not self.pushes_fd: - return 0, -8 # bad configuration - bytes_consumed, errcode, data = self.encode(0) - if data: - assert self.fd is not None - self.fd.write(data) - return bytes_consumed, errcode - - def encode_to_file(self, fh: int, bufsize: int) -> int: - """ - :param fh: File handle. - :param bufsize: Buffer size. - - :returns: If finished successfully, return 0. - Otherwise, return an error code. Err codes are from - :data:`.ImageFile.ERRORS`. - """ - errcode = 0 - while errcode == 0: - status, errcode, buf = self.encode(bufsize) - if status > 0: - os.write(fh, buf[status:]) - return errcode diff --git a/python_modules/PIL/ImageFilter.py b/python_modules/PIL/ImageFilter.py deleted file mode 100644 index b9ed54ab2..000000000 --- a/python_modules/PIL/ImageFilter.py +++ /dev/null @@ -1,604 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard filters -# -# History: -# 1995-11-27 fl Created -# 2002-06-08 fl Added rank and mode filters -# 2003-09-15 fl Fixed rank calculation in rank filter; added expand call -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-2002 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import abc -import functools -from collections.abc import Sequence -from types import ModuleType -from typing import Any, Callable, cast - -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import _imaging - from ._typing import NumpyArray - - -class Filter(abc.ABC): - @abc.abstractmethod - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - pass - - -class MultibandFilter(Filter): - pass - - -class BuiltinFilter(MultibandFilter): - filterargs: tuple[Any, ...] - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - if image.mode == "P": - msg = "cannot filter palette images" - raise ValueError(msg) - return image.filter(*self.filterargs) - - -class Kernel(BuiltinFilter): - """ - Create a convolution kernel. This only supports 3x3 and 5x5 integer and floating - point kernels. - - Kernels can only be applied to "L" and "RGB" images. - - :param size: Kernel size, given as (width, height). This must be (3,3) or (5,5). - :param kernel: A sequence containing kernel weights. The kernel will be flipped - vertically before being applied to the image. - :param scale: Scale factor. If given, the result for each pixel is divided by this - value. The default is the sum of the kernel weights. - :param offset: Offset. If given, this value is added to the result, after it has - been divided by the scale factor. - """ - - name = "Kernel" - - def __init__( - self, - size: tuple[int, int], - kernel: Sequence[float], - scale: float | None = None, - offset: float = 0, - ) -> None: - if scale is None: - # default scale is sum of kernel - scale = functools.reduce(lambda a, b: a + b, kernel) - if size[0] * size[1] != len(kernel): - msg = "not enough coefficients in kernel" - raise ValueError(msg) - self.filterargs = size, scale, offset, kernel - - -class RankFilter(Filter): - """ - Create a rank filter. The rank filter sorts all pixels in - a window of the given size, and returns the ``rank``'th value. - - :param size: The kernel size, in pixels. - :param rank: What pixel value to pick. Use 0 for a min filter, - ``size * size / 2`` for a median filter, ``size * size - 1`` - for a max filter, etc. - """ - - name = "Rank" - - def __init__(self, size: int, rank: int) -> None: - self.size = size - self.rank = rank - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - if image.mode == "P": - msg = "cannot filter palette images" - raise ValueError(msg) - image = image.expand(self.size // 2, self.size // 2) - return image.rankfilter(self.size, self.rank) - - -class MedianFilter(RankFilter): - """ - Create a median filter. Picks the median pixel value in a window with the - given size. - - :param size: The kernel size, in pixels. - """ - - name = "Median" - - def __init__(self, size: int = 3) -> None: - self.size = size - self.rank = size * size // 2 - - -class MinFilter(RankFilter): - """ - Create a min filter. Picks the lowest pixel value in a window with the - given size. - - :param size: The kernel size, in pixels. - """ - - name = "Min" - - def __init__(self, size: int = 3) -> None: - self.size = size - self.rank = 0 - - -class MaxFilter(RankFilter): - """ - Create a max filter. Picks the largest pixel value in a window with the - given size. - - :param size: The kernel size, in pixels. - """ - - name = "Max" - - def __init__(self, size: int = 3) -> None: - self.size = size - self.rank = size * size - 1 - - -class ModeFilter(Filter): - """ - Create a mode filter. Picks the most frequent pixel value in a box with the - given size. Pixel values that occur only once or twice are ignored; if no - pixel value occurs more than twice, the original pixel value is preserved. - - :param size: The kernel size, in pixels. - """ - - name = "Mode" - - def __init__(self, size: int = 3) -> None: - self.size = size - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - return image.modefilter(self.size) - - -class GaussianBlur(MultibandFilter): - """Blurs the image with a sequence of extended box filters, which - approximates a Gaussian kernel. For details on accuracy see - - - :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two - numbers for x and y, or a single number for both. - """ - - name = "GaussianBlur" - - def __init__(self, radius: float | Sequence[float] = 2) -> None: - self.radius = radius - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - xy = self.radius - if isinstance(xy, (int, float)): - xy = (xy, xy) - if xy == (0, 0): - return image.copy() - return image.gaussian_blur(xy) - - -class BoxBlur(MultibandFilter): - """Blurs the image by setting each pixel to the average value of the pixels - in a square box extending radius pixels in each direction. - Supports float radius of arbitrary size. Uses an optimized implementation - which runs in linear time relative to the size of the image - for any radius value. - - :param radius: Size of the box in a direction. Either a sequence of two numbers for - x and y, or a single number for both. - - Radius 0 does not blur, returns an identical image. - Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total. - """ - - name = "BoxBlur" - - def __init__(self, radius: float | Sequence[float]) -> None: - xy = radius if isinstance(radius, (tuple, list)) else (radius, radius) - if xy[0] < 0 or xy[1] < 0: - msg = "radius must be >= 0" - raise ValueError(msg) - self.radius = radius - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - xy = self.radius - if isinstance(xy, (int, float)): - xy = (xy, xy) - if xy == (0, 0): - return image.copy() - return image.box_blur(xy) - - -class UnsharpMask(MultibandFilter): - """Unsharp mask filter. - - See Wikipedia's entry on `digital unsharp masking`_ for an explanation of - the parameters. - - :param radius: Blur Radius - :param percent: Unsharp strength, in percent - :param threshold: Threshold controls the minimum brightness change that - will be sharpened - - .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking - - """ - - name = "UnsharpMask" - - def __init__( - self, radius: float = 2, percent: int = 150, threshold: int = 3 - ) -> None: - self.radius = radius - self.percent = percent - self.threshold = threshold - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - return image.unsharp_mask(self.radius, self.percent, self.threshold) - - -class BLUR(BuiltinFilter): - name = "Blur" - # fmt: off - filterargs = (5, 5), 16, 0, ( - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - ) - # fmt: on - - -class CONTOUR(BuiltinFilter): - name = "Contour" - # fmt: off - filterargs = (3, 3), 1, 255, ( - -1, -1, -1, - -1, 8, -1, - -1, -1, -1, - ) - # fmt: on - - -class DETAIL(BuiltinFilter): - name = "Detail" - # fmt: off - filterargs = (3, 3), 6, 0, ( - 0, -1, 0, - -1, 10, -1, - 0, -1, 0, - ) - # fmt: on - - -class EDGE_ENHANCE(BuiltinFilter): - name = "Edge-enhance" - # fmt: off - filterargs = (3, 3), 2, 0, ( - -1, -1, -1, - -1, 10, -1, - -1, -1, -1, - ) - # fmt: on - - -class EDGE_ENHANCE_MORE(BuiltinFilter): - name = "Edge-enhance More" - # fmt: off - filterargs = (3, 3), 1, 0, ( - -1, -1, -1, - -1, 9, -1, - -1, -1, -1, - ) - # fmt: on - - -class EMBOSS(BuiltinFilter): - name = "Emboss" - # fmt: off - filterargs = (3, 3), 1, 128, ( - -1, 0, 0, - 0, 1, 0, - 0, 0, 0, - ) - # fmt: on - - -class FIND_EDGES(BuiltinFilter): - name = "Find Edges" - # fmt: off - filterargs = (3, 3), 1, 0, ( - -1, -1, -1, - -1, 8, -1, - -1, -1, -1, - ) - # fmt: on - - -class SHARPEN(BuiltinFilter): - name = "Sharpen" - # fmt: off - filterargs = (3, 3), 16, 0, ( - -2, -2, -2, - -2, 32, -2, - -2, -2, -2, - ) - # fmt: on - - -class SMOOTH(BuiltinFilter): - name = "Smooth" - # fmt: off - filterargs = (3, 3), 13, 0, ( - 1, 1, 1, - 1, 5, 1, - 1, 1, 1, - ) - # fmt: on - - -class SMOOTH_MORE(BuiltinFilter): - name = "Smooth More" - # fmt: off - filterargs = (5, 5), 100, 0, ( - 1, 1, 1, 1, 1, - 1, 5, 5, 5, 1, - 1, 5, 44, 5, 1, - 1, 5, 5, 5, 1, - 1, 1, 1, 1, 1, - ) - # fmt: on - - -class Color3DLUT(MultibandFilter): - """Three-dimensional color lookup table. - - Transforms 3-channel pixels using the values of the channels as coordinates - in the 3D lookup table and interpolating the nearest elements. - - This method allows you to apply almost any color transformation - in constant time by using pre-calculated decimated tables. - - .. versionadded:: 5.2.0 - - :param size: Size of the table. One int or tuple of (int, int, int). - Minimal size in any dimension is 2, maximum is 65. - :param table: Flat lookup table. A list of ``channels * size**3`` - float elements or a list of ``size**3`` channels-sized - tuples with floats. Channels are changed first, - then first dimension, then second, then third. - Value 0.0 corresponds lowest value of output, 1.0 highest. - :param channels: Number of channels in the table. Could be 3 or 4. - Default is 3. - :param target_mode: A mode for the result image. Should have not less - than ``channels`` channels. Default is ``None``, - which means that mode wouldn't be changed. - """ - - name = "Color 3D LUT" - - def __init__( - self, - size: int | tuple[int, int, int], - table: Sequence[float] | Sequence[Sequence[int]] | NumpyArray, - channels: int = 3, - target_mode: str | None = None, - **kwargs: bool, - ) -> None: - if channels not in (3, 4): - msg = "Only 3 or 4 output channels are supported" - raise ValueError(msg) - self.size = size = self._check_size(size) - self.channels = channels - self.mode = target_mode - - # Hidden flag `_copy_table=False` could be used to avoid extra copying - # of the table if the table is specially made for the constructor. - copy_table = kwargs.get("_copy_table", True) - items = size[0] * size[1] * size[2] - wrong_size = False - - numpy: ModuleType | None = None - if hasattr(table, "shape"): - try: - import numpy - except ImportError: - pass - - if numpy and isinstance(table, numpy.ndarray): - numpy_table: NumpyArray = table - if copy_table: - numpy_table = numpy_table.copy() - - if numpy_table.shape in [ - (items * channels,), - (items, channels), - (size[2], size[1], size[0], channels), - ]: - table = numpy_table.reshape(items * channels) - else: - wrong_size = True - - else: - if copy_table: - table = list(table) - - # Convert to a flat list - if table and isinstance(table[0], (list, tuple)): - raw_table = cast(Sequence[Sequence[int]], table) - flat_table: list[int] = [] - for pixel in raw_table: - if len(pixel) != channels: - msg = ( - "The elements of the table should " - f"have a length of {channels}." - ) - raise ValueError(msg) - flat_table.extend(pixel) - table = flat_table - - if wrong_size or len(table) != items * channels: - msg = ( - "The table should have either channels * size**3 float items " - "or size**3 items of channels-sized tuples with floats. " - f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. " - f"Actual length: {len(table)}" - ) - raise ValueError(msg) - self.table = table - - @staticmethod - def _check_size(size: Any) -> tuple[int, int, int]: - try: - _, _, _ = size - except ValueError as e: - msg = "Size should be either an integer or a tuple of three integers." - raise ValueError(msg) from e - except TypeError: - size = (size, size, size) - size = tuple(int(x) for x in size) - for size_1d in size: - if not 2 <= size_1d <= 65: - msg = "Size should be in [2, 65] range." - raise ValueError(msg) - return size - - @classmethod - def generate( - cls, - size: int | tuple[int, int, int], - callback: Callable[[float, float, float], tuple[float, ...]], - channels: int = 3, - target_mode: str | None = None, - ) -> Color3DLUT: - """Generates new LUT using provided callback. - - :param size: Size of the table. Passed to the constructor. - :param callback: Function with three parameters which correspond - three color channels. Will be called ``size**3`` - times with values from 0.0 to 1.0 and should return - a tuple with ``channels`` elements. - :param channels: The number of channels which should return callback. - :param target_mode: Passed to the constructor of the resulting - lookup table. - """ - size_1d, size_2d, size_3d = cls._check_size(size) - if channels not in (3, 4): - msg = "Only 3 or 4 output channels are supported" - raise ValueError(msg) - - table: list[float] = [0] * (size_1d * size_2d * size_3d * channels) - idx_out = 0 - for b in range(size_3d): - for g in range(size_2d): - for r in range(size_1d): - table[idx_out : idx_out + channels] = callback( - r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1) - ) - idx_out += channels - - return cls( - (size_1d, size_2d, size_3d), - table, - channels=channels, - target_mode=target_mode, - _copy_table=False, - ) - - def transform( - self, - callback: Callable[..., tuple[float, ...]], - with_normals: bool = False, - channels: int | None = None, - target_mode: str | None = None, - ) -> Color3DLUT: - """Transforms the table values using provided callback and returns - a new LUT with altered values. - - :param callback: A function which takes old lookup table values - and returns a new set of values. The number - of arguments which function should take is - ``self.channels`` or ``3 + self.channels`` - if ``with_normals`` flag is set. - Should return a tuple of ``self.channels`` or - ``channels`` elements if it is set. - :param with_normals: If true, ``callback`` will be called with - coordinates in the color cube as the first - three arguments. Otherwise, ``callback`` - will be called only with actual color values. - :param channels: The number of channels in the resulting lookup table. - :param target_mode: Passed to the constructor of the resulting - lookup table. - """ - if channels not in (None, 3, 4): - msg = "Only 3 or 4 output channels are supported" - raise ValueError(msg) - ch_in = self.channels - ch_out = channels or ch_in - size_1d, size_2d, size_3d = self.size - - table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out) - idx_in = 0 - idx_out = 0 - for b in range(size_3d): - for g in range(size_2d): - for r in range(size_1d): - values = self.table[idx_in : idx_in + ch_in] - if with_normals: - values = callback( - r / (size_1d - 1), - g / (size_2d - 1), - b / (size_3d - 1), - *values, - ) - else: - values = callback(*values) - table[idx_out : idx_out + ch_out] = values - idx_in += ch_in - idx_out += ch_out - - return type(self)( - self.size, - table, - channels=ch_out, - target_mode=target_mode or self.mode, - _copy_table=False, - ) - - def __repr__(self) -> str: - r = [ - f"{self.__class__.__name__} from {self.table.__class__.__name__}", - "size={:d}x{:d}x{:d}".format(*self.size), - f"channels={self.channels:d}", - ] - if self.mode: - r.append(f"target_mode={self.mode}") - return "<{}>".format(" ".join(r)) - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - from . import Image - - return image.color_lut_3d( - self.mode or image.mode, - Image.Resampling.BILINEAR, - self.channels, - self.size, - self.table, - ) diff --git a/python_modules/PIL/ImageFont.py b/python_modules/PIL/ImageFont.py deleted file mode 100644 index 329c463ff..000000000 --- a/python_modules/PIL/ImageFont.py +++ /dev/null @@ -1,1339 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PIL raster font management -# -# History: -# 1996-08-07 fl created (experimental) -# 1997-08-25 fl minor adjustments to handle fonts from pilfont 0.3 -# 1999-02-06 fl rewrote most font management stuff in C -# 1999-03-17 fl take pth files into account in load_path (from Richard Jones) -# 2001-02-17 fl added freetype support -# 2001-05-09 fl added TransposedFont wrapper class -# 2002-03-04 fl make sure we have a "L" or "1" font -# 2002-12-04 fl skip non-directory entries in the system path -# 2003-04-29 fl add embedded default font -# 2003-09-27 fl added support for truetype charmap encodings -# -# Todo: -# Adapt to PILFONT2 format (16-bit fonts, compressed, single file) -# -# Copyright (c) 1997-2003 by Secret Labs AB -# Copyright (c) 1996-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -from __future__ import annotations - -import base64 -import os -import sys -import warnings -from enum import IntEnum -from io import BytesIO -from types import ModuleType -from typing import IO, Any, BinaryIO, TypedDict, cast - -from . import Image, features -from ._typing import StrOrBytesPath -from ._util import DeferredError, is_path - -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import ImageFile - from ._imaging import ImagingFont - from ._imagingft import Font - - -class Axis(TypedDict): - minimum: int | None - default: int | None - maximum: int | None - name: bytes | None - - -class Layout(IntEnum): - BASIC = 0 - RAQM = 1 - - -MAX_STRING_LENGTH = 1_000_000 - - -core: ModuleType | DeferredError -try: - from . import _imagingft as core -except ImportError as ex: - core = DeferredError.new(ex) - - -def _string_length_check(text: str | bytes | bytearray) -> None: - if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: - msg = "too many characters in string" - raise ValueError(msg) - - -# FIXME: add support for pilfont2 format (see FontFile.py) - -# -------------------------------------------------------------------- -# Font metrics format: -# "PILfont" LF -# fontdescriptor LF -# (optional) key=value... LF -# "DATA" LF -# binary data: 256*10*2 bytes (dx, dy, dstbox, srcbox) -# -# To place a character, cut out srcbox and paste at dstbox, -# relative to the character position. Then move the character -# position according to dx, dy. -# -------------------------------------------------------------------- - - -class ImageFont: - """PIL font wrapper""" - - font: ImagingFont - - def _load_pilfont(self, filename: str) -> None: - with open(filename, "rb") as fp: - image: ImageFile.ImageFile | None = None - root = os.path.splitext(filename)[0] - - for ext in (".png", ".gif", ".pbm"): - if image: - image.close() - try: - fullname = root + ext - image = Image.open(fullname) - except Exception: - pass - else: - if image and image.mode in ("1", "L"): - break - else: - if image: - image.close() - - msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}" - raise OSError(msg) - - self.file = fullname - - self._load_pilfont_data(fp, image) - image.close() - - def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: - # read PILfont header - if file.readline() != b"PILfont\n": - msg = "Not a PILfont file" - raise SyntaxError(msg) - file.readline().split(b";") - self.info = [] # FIXME: should be a dictionary - while True: - s = file.readline() - if not s or s == b"DATA\n": - break - self.info.append(s) - - # read PILfont metrics - data = file.read(256 * 20) - - # check image - if image.mode not in ("1", "L"): - msg = "invalid font image mode" - raise TypeError(msg) - - image.load() - - self.font = Image.core.font(image.im, data) - - def getmask( - self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any - ) -> Image.core.ImagingCore: - """ - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. Otherwise, it should have mode ``1``. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 - - :return: An internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module. - """ - _string_length_check(text) - Image._decompression_bomb_check(self.font.getsize(text)) - return self.font.getmask(text, mode) - - def getbbox( - self, text: str | bytes | bytearray, *args: Any, **kwargs: Any - ) -> tuple[int, int, int, int]: - """ - Returns bounding box (in pixels) of given text. - - .. versionadded:: 9.2.0 - - :param text: Text to render. - - :return: ``(left, top, right, bottom)`` bounding box - """ - _string_length_check(text) - width, height = self.font.getsize(text) - return 0, 0, width, height - - def getlength( - self, text: str | bytes | bytearray, *args: Any, **kwargs: Any - ) -> int: - """ - Returns length (in pixels) of given text. - This is the amount by which following text should be offset. - - .. versionadded:: 9.2.0 - """ - _string_length_check(text) - width, height = self.font.getsize(text) - return width - - -## -# Wrapper for FreeType fonts. Application code should use the -# truetype factory function to create font objects. - - -class FreeTypeFont: - """FreeType font wrapper (requires _imagingft service)""" - - font: Font - font_bytes: bytes - - def __init__( - self, - font: StrOrBytesPath | BinaryIO, - size: float = 10, - index: int = 0, - encoding: str = "", - layout_engine: Layout | None = None, - ) -> None: - # FIXME: use service provider instead - - if isinstance(core, DeferredError): - raise core.ex - - if size <= 0: - msg = f"font size must be greater than 0, not {size}" - raise ValueError(msg) - - self.path = font - self.size = size - self.index = index - self.encoding = encoding - - try: - from packaging.version import parse as parse_version - except ImportError: - pass - else: - if freetype_version := features.version_module("freetype2"): - if parse_version(freetype_version) < parse_version("2.9.1"): - warnings.warn( - "Support for FreeType 2.9.0 is deprecated and will be removed " - "in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 " - "or newer, preferably FreeType 2.10.4 which fixes " - "CVE-2020-15999.", - DeprecationWarning, - ) - - if layout_engine not in (Layout.BASIC, Layout.RAQM): - layout_engine = Layout.BASIC - if core.HAVE_RAQM: - layout_engine = Layout.RAQM - elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: - warnings.warn( - "Raqm layout was requested, but Raqm is not available. " - "Falling back to basic layout." - ) - layout_engine = Layout.BASIC - - self.layout_engine = layout_engine - - def load_from_bytes(f: IO[bytes]) -> None: - self.font_bytes = f.read() - self.font = core.getfont( - "", size, index, encoding, self.font_bytes, layout_engine - ) - - if is_path(font): - font = os.fspath(font) - if sys.platform == "win32": - font_bytes_path = font if isinstance(font, bytes) else font.encode() - try: - font_bytes_path.decode("ascii") - except UnicodeDecodeError: - # FreeType cannot load fonts with non-ASCII characters on Windows - # So load it into memory first - with open(font, "rb") as f: - load_from_bytes(f) - return - self.font = core.getfont( - font, size, index, encoding, layout_engine=layout_engine - ) - else: - load_from_bytes(cast(IO[bytes], font)) - - def __getstate__(self) -> list[Any]: - return [self.path, self.size, self.index, self.encoding, self.layout_engine] - - def __setstate__(self, state: list[Any]) -> None: - path, size, index, encoding, layout_engine = state - FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine) - - def getname(self) -> tuple[str | None, str | None]: - """ - :return: A tuple of the font family (e.g. Helvetica) and the font style - (e.g. Bold) - """ - return self.font.family, self.font.style - - def getmetrics(self) -> tuple[int, int]: - """ - :return: A tuple of the font ascent (the distance from the baseline to - the highest outline point) and descent (the distance from the - baseline to the lowest outline point, a negative value) - """ - return self.font.ascent, self.font.descent - - def getlength( - self, - text: str | bytes, - mode: str = "", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - ) -> float: - """ - Returns length (in pixels with 1/64 precision) of given text when rendered - in font with provided direction, features, and language. - - This is the amount by which following text should be offset. - Text bounding box may extend past the length in some fonts, - e.g. when using italics or accents. - - The result is returned as a float; it is a whole number if using basic layout. - - Note that the sum of two lengths may not equal the length of a concatenated - string due to kerning. If you need to adjust for kerning, include the following - character and subtract its length. - - For example, instead of :: - - hello = font.getlength("Hello") - world = font.getlength("World") - hello_world = hello + world # not adjusted for kerning - assert hello_world == font.getlength("HelloWorld") # may fail - - use :: - - hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning - world = font.getlength("World") - hello_world = hello + world # adjusted for kerning - assert hello_world == font.getlength("HelloWorld") # True - - or disable kerning with (requires libraqm) :: - - hello = draw.textlength("Hello", font, features=["-kern"]) - world = draw.textlength("World", font, features=["-kern"]) - hello_world = hello + world # kerning is disabled, no need to adjust - assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) - - .. versionadded:: 8.0.0 - - :param text: Text to measure. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - :return: Either width for horizontal text, or height for vertical text. - """ - _string_length_check(text) - return self.font.getlength(text, mode, direction, features, language) / 64 - - def getbbox( - self, - text: str | bytes, - mode: str = "", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - anchor: str | None = None, - ) -> tuple[float, float, float, float]: - """ - Returns bounding box (in pixels) of given text relative to given anchor - when rendered in font with provided direction, features, and language. - - Use :py:meth:`getlength()` to get the offset of following text with - 1/64 pixel precision. The bounding box includes extra margins for - some fonts, e.g. italics or accents. - - .. versionadded:: 8.0.0 - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - :param stroke_width: The width of the text stroke. - - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - - :return: ``(left, top, right, bottom)`` bounding box - """ - _string_length_check(text) - size, offset = self.font.getsize( - text, mode, direction, features, language, anchor - ) - left, top = offset[0] - stroke_width, offset[1] - stroke_width - width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width - return left, top, left + width, top + height - - def getmask( - self, - text: str | bytes, - mode: str = "", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - anchor: str | None = None, - ink: int = 0, - start: tuple[float, float] | None = None, - ) -> Image.core.ImagingCore: - """ - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. If the font has embedded color data, the bitmap - should have mode ``RGBA``. Otherwise, it should have mode ``1``. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - - .. versionadded:: 8.0.0 - - :param ink: Foreground ink for rendering in RGBA mode. - - .. versionadded:: 8.0.0 - - :param start: Tuple of horizontal and vertical offset, as text may render - differently when starting at fractional coordinates. - - .. versionadded:: 9.4.0 - - :return: An internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module. - """ - return self.getmask2( - text, - mode, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - anchor=anchor, - ink=ink, - start=start, - )[0] - - def getmask2( - self, - text: str | bytes, - mode: str = "", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - anchor: str | None = None, - ink: int = 0, - start: tuple[float, float] | None = None, - *args: Any, - **kwargs: Any, - ) -> tuple[Image.core.ImagingCore, tuple[int, int]]: - """ - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. If the font has embedded color data, the bitmap - should have mode ``RGBA``. Otherwise, it should have mode ``1``. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - - .. versionadded:: 8.0.0 - - :param ink: Foreground ink for rendering in RGBA mode. - - .. versionadded:: 8.0.0 - - :param start: Tuple of horizontal and vertical offset, as text may render - differently when starting at fractional coordinates. - - .. versionadded:: 9.4.0 - - :return: A tuple of an internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module, and the text offset, the - gap between the starting coordinate and the first marking - """ - _string_length_check(text) - if start is None: - start = (0, 0) - - def fill(width: int, height: int) -> Image.core.ImagingCore: - size = (width, height) - Image._decompression_bomb_check(size) - return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - - return self.font.render( - text, - fill, - mode, - direction, - features, - language, - stroke_width, - kwargs.get("stroke_filled", False), - anchor, - ink, - start, - ) - - def font_variant( - self, - font: StrOrBytesPath | BinaryIO | None = None, - size: float | None = None, - index: int | None = None, - encoding: str | None = None, - layout_engine: Layout | None = None, - ) -> FreeTypeFont: - """ - Create a copy of this FreeTypeFont object, - using any specified arguments to override the settings. - - Parameters are identical to the parameters used to initialize this - object. - - :return: A FreeTypeFont object. - """ - if font is None: - try: - font = BytesIO(self.font_bytes) - except AttributeError: - font = self.path - return FreeTypeFont( - font=font, - size=self.size if size is None else size, - index=self.index if index is None else index, - encoding=self.encoding if encoding is None else encoding, - layout_engine=layout_engine or self.layout_engine, - ) - - def get_variation_names(self) -> list[bytes]: - """ - :returns: A list of the named styles in a variation font. - :exception OSError: If the font is not a variation font. - """ - try: - names = self.font.getvarnames() - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e - return [name.replace(b"\x00", b"") for name in names] - - def set_variation_by_name(self, name: str | bytes) -> None: - """ - :param name: The name of the style. - :exception OSError: If the font is not a variation font. - """ - names = self.get_variation_names() - if not isinstance(name, bytes): - name = name.encode() - index = names.index(name) + 1 - - if index == getattr(self, "_last_variation_index", None): - # When the same name is set twice in a row, - # there is an 'unknown freetype error' - # https://savannah.nongnu.org/bugs/?56186 - return - self._last_variation_index = index - - self.font.setvarname(index) - - def get_variation_axes(self) -> list[Axis]: - """ - :returns: A list of the axes in a variation font. - :exception OSError: If the font is not a variation font. - """ - try: - axes = self.font.getvaraxes() - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e - for axis in axes: - if axis["name"]: - axis["name"] = axis["name"].replace(b"\x00", b"") - return axes - - def set_variation_by_axes(self, axes: list[float]) -> None: - """ - :param axes: A list of values for each axis. - :exception OSError: If the font is not a variation font. - """ - try: - self.font.setvaraxes(axes) - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e - - -class TransposedFont: - """Wrapper for writing rotated or mirrored text""" - - def __init__( - self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None - ): - """ - Wrapper that creates a transposed font from any existing font - object. - - :param font: A font object. - :param orientation: An optional orientation. If given, this should - be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM, - Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or - Image.Transpose.ROTATE_270. - """ - self.font = font - self.orientation = orientation # any 'transpose' argument, or None - - def getmask( - self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any - ) -> Image.core.ImagingCore: - im = self.font.getmask(text, mode, *args, **kwargs) - if self.orientation is not None: - return im.transpose(self.orientation) - return im - - def getbbox( - self, text: str | bytes, *args: Any, **kwargs: Any - ) -> tuple[int, int, float, float]: - # TransposedFont doesn't support getmask2, move top-left point to (0, 0) - # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont - left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) - width = right - left - height = bottom - top - if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - return 0, 0, height, width - return 0, 0, width, height - - def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float: - if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - msg = "text length is undefined for text rotated by 90 or 270 degrees" - raise ValueError(msg) - return self.font.getlength(text, *args, **kwargs) - - -def load(filename: str) -> ImageFont: - """ - Load a font file. This function loads a font object from the given - bitmap font file, and returns the corresponding font object. For loading TrueType - or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`. - - :param filename: Name of font file. - :return: A font object. - :exception OSError: If the file could not be read. - """ - f = ImageFont() - f._load_pilfont(filename) - return f - - -def truetype( - font: StrOrBytesPath | BinaryIO, - size: float = 10, - index: int = 0, - encoding: str = "", - layout_engine: Layout | None = None, -) -> FreeTypeFont: - """ - Load a TrueType or OpenType font from a file or file-like object, - and create a font object. This function loads a font object from the given - file or file-like object, and creates a font object for a font of the given - size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load` - and :py:func:`~PIL.ImageFont.load_path`. - - Pillow uses FreeType to open font files. On Windows, be aware that FreeType - will keep the file open as long as the FreeTypeFont object exists. Windows - limits the number of files that can be open in C at once to 512, so if many - fonts are opened simultaneously and that limit is approached, an - ``OSError`` may be thrown, reporting that FreeType "cannot open resource". - A workaround would be to copy the file(s) into memory, and open that instead. - - This function requires the _imagingft service. - - :param font: A filename or file-like object containing a TrueType font. - If the file is not found in this filename, the loader may also - search in other directories, such as: - - * The :file:`fonts/` directory on Windows, - * :file:`/Library/Fonts/`, :file:`/System/Library/Fonts/` - and :file:`~/Library/Fonts/` on macOS. - * :file:`~/.local/share/fonts`, :file:`/usr/local/share/fonts`, - and :file:`/usr/share/fonts` on Linux; or those specified by - the ``XDG_DATA_HOME`` and ``XDG_DATA_DIRS`` environment variables - for user-installed and system-wide fonts, respectively. - - :param size: The requested size, in pixels. - :param index: Which font face to load (default is first available face). - :param encoding: Which font encoding to use (default is Unicode). Possible - encodings include (see the FreeType documentation for more - information): - - * "unic" (Unicode) - * "symb" (Microsoft Symbol) - * "ADOB" (Adobe Standard) - * "ADBE" (Adobe Expert) - * "ADBC" (Adobe Custom) - * "armn" (Apple Roman) - * "sjis" (Shift JIS) - * "gb " (PRC) - * "big5" - * "wans" (Extended Wansung) - * "joha" (Johab) - * "lat1" (Latin-1) - - This specifies the character set to use. It does not alter the - encoding of any text provided in subsequent operations. - :param layout_engine: Which layout engine to use, if available: - :attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`. - If it is available, Raqm layout will be used by default. - Otherwise, basic layout will be used. - - Raqm layout is recommended for all non-English text. If Raqm layout - is not required, basic layout will have better performance. - - You can check support for Raqm layout using - :py:func:`PIL.features.check_feature` with ``feature="raqm"``. - - .. versionadded:: 4.2.0 - :return: A font object. - :exception OSError: If the file could not be read. - :exception ValueError: If the font size is not greater than zero. - """ - - def freetype(font: StrOrBytesPath | BinaryIO) -> FreeTypeFont: - return FreeTypeFont(font, size, index, encoding, layout_engine) - - try: - return freetype(font) - except OSError: - if not is_path(font): - raise - ttf_filename = os.path.basename(font) - - dirs = [] - if sys.platform == "win32": - # check the windows font repository - # NOTE: must use uppercase WINDIR, to work around bugs in - # 1.5.2's os.environ.get() - windir = os.environ.get("WINDIR") - if windir: - dirs.append(os.path.join(windir, "fonts")) - elif sys.platform in ("linux", "linux2"): - data_home = os.environ.get("XDG_DATA_HOME") - if not data_home: - # The freedesktop spec defines the following default directory for - # when XDG_DATA_HOME is unset or empty. This user-level directory - # takes precedence over system-level directories. - data_home = os.path.expanduser("~/.local/share") - xdg_dirs = [data_home] - - data_dirs = os.environ.get("XDG_DATA_DIRS") - if not data_dirs: - # Similarly, defaults are defined for the system-level directories - data_dirs = "/usr/local/share:/usr/share" - xdg_dirs += data_dirs.split(":") - - dirs += [os.path.join(xdg_dir, "fonts") for xdg_dir in xdg_dirs] - elif sys.platform == "darwin": - dirs += [ - "/Library/Fonts", - "/System/Library/Fonts", - os.path.expanduser("~/Library/Fonts"), - ] - - ext = os.path.splitext(ttf_filename)[1] - first_font_with_a_different_extension = None - for directory in dirs: - for walkroot, walkdir, walkfilenames in os.walk(directory): - for walkfilename in walkfilenames: - if ext and walkfilename == ttf_filename: - return freetype(os.path.join(walkroot, walkfilename)) - elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: - fontpath = os.path.join(walkroot, walkfilename) - if os.path.splitext(fontpath)[1] == ".ttf": - return freetype(fontpath) - if not ext and first_font_with_a_different_extension is None: - first_font_with_a_different_extension = fontpath - if first_font_with_a_different_extension: - return freetype(first_font_with_a_different_extension) - raise - - -def load_path(filename: str | bytes) -> ImageFont: - """ - Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a - bitmap font along the Python path. - - :param filename: Name of font file. - :return: A font object. - :exception OSError: If the file could not be read. - """ - if not isinstance(filename, str): - filename = filename.decode("utf-8") - for directory in sys.path: - try: - return load(os.path.join(directory, filename)) - except OSError: - pass - msg = f'cannot find font file "{filename}" in sys.path' - if os.path.exists(filename): - msg += f', did you mean ImageFont.load("{filename}") instead?' - - raise OSError(msg) - - -def load_default_imagefont() -> ImageFont: - f = ImageFont() - f._load_pilfont_data( - # courB08 - BytesIO( - base64.b64decode( - b""" -UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA -BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL -AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA -AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB -ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A -BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB -//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA -AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH -AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA -ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv -AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/ -/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5 -AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA -AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG -AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA -BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA -AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA -2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF -AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA//// -+gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA -////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA -BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv -AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA -AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA -AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA -BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP// -//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA -AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF -AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB -mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn -AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA -AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7 -AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA -Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB -//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA -AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ -AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC -DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ -AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/ -+wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5 -AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/ -///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG -AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA -BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA -Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC -eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG -AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA//// -+gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA -////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA -BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT -AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A -AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA -Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA -Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP// -//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA -AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ -AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA -LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5 -AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA -AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5 -AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA -AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG -AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA -EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK -AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA -pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG -AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// -+QAGAAIAzgAKANUAEw== -""" - ) - ), - Image.open( - BytesIO( - base64.b64decode( - b""" -iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u -Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 -M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g -LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F -IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA -Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791 -NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx -in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9 -SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY -AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt -y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG -ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY -lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H -/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3 -AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47 -c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/ -/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw -pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv -oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR -evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA -AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// -Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR -w7IkEbzhVQAAAABJRU5ErkJggg== -""" - ) - ) - ), - ) - return f - - -def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: - """If FreeType support is available, load a version of Aileron Regular, - https://dotcolon.net/fonts/aileron, with a more limited character set. - - Otherwise, load a "better than nothing" font. - - .. versionadded:: 1.1.4 - - :param size: The font size of Aileron Regular. - - .. versionadded:: 10.1.0 - - :return: A font object. - """ - if isinstance(core, ModuleType) or size is not None: - return truetype( - BytesIO( - base64.b64decode( - b""" -AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA -AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA -MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh -tdHjjERZ8AAAB2AAAAdBsb2NhuOexrgAABVQAAADqbWF4cAC7AEYAAAFYAAAAIG5hbWUr+h5lAAAk -OAAAA6Jwb3N0D3oPTQAAJ9wAAAEKAAEAAAABGhxJDqIhXw889QALA+gAAAAA0Bqf2QAAAADhCh2h/ -2r/LgOxAyAAAAAIAAIAAAAAAAAAAQAAA8r/GgAAA7j/av9qA7EAAQAAAAAAAAAAAAAAAAAAAHQAAQ -AAAHQAQwAFAAAAAAACAAAAAQABAAAAQAAAAAAAAAADAfoBkAAFAAgCigJYAAAASwKKAlgAAAFeADI -BPgAAAAAFAAAAAAAAAAAAAAcAAAAAAAAAAAAAAABVS1dOAEAAIPsCAwL/GgDIA8oA5iAAAJMAAAAA -AhICsgAAACAAAwH0AAAAAAAAAU0AAADYAAAA8gA5AVMAVgJEAEYCRAA1AuQAKQKOAEAAsAArATsAZ -AE7AB4CMABVAkQAUADc/+EBEgAgANwAJQEv//sCRAApAkQAggJEADwCRAAtAkQAIQJEADkCRAArAk -QAMgJEACwCRAAxANwAJQDc/+ECRABnAkQAUAJEAEQB8wAjA1QANgJ/AB0CcwBkArsALwLFAGQCSwB -kAjcAZALGAC8C2gBkAQgAZAIgADcCYQBkAj8AZANiAGQCzgBkAuEALwJWAGQC3QAvAmsAZAJJADQC -ZAAiAqoAXgJuACADuAAaAnEAGQJFABMCTwAuATMAYgEv//sBJwAiAkQAUAH0ADIBLAApAhMAJAJjA -EoCEQAeAmcAHgIlAB4BIgAVAmcAHgJRAEoA7gA+AOn/8wIKAEoA9wBGA1cASgJRAEoCSgAeAmMASg -JnAB4BSgBKAcsAGAE5ABQCUABCAgIAAQMRAAEB4v/6AgEAAQHOABQBLwBAAPoAYAEvACECRABNA0Y -AJAItAHgBKgAcAkQAUAEsAHQAygAgAi0AOQD3ADYA9wAWAaEANgGhABYCbAAlAYMAeAGDADkA6/9q -AhsAFAIKABUB/QAVAAAAAwAAAAMAAAAcAAEAAAAAAKQAAwABAAAAHAAEAIgAAAAeABAAAwAOAH4Aq -QCrALEAtAC3ALsgGSAdICYgOiBEISL7Av//AAAAIACpAKsAsAC0ALcAuyAYIBwgJiA5IEQhIvsB// -//4/+5/7j/tP+y/7D/reBR4E/gR+A14CzfTwVxAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAEGAAABAAAAAAAAAAECAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAMEBQYHCAkKCwwNDg8QERIT -FBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMT -U5PUFFSU1RVVldYWVpbXF1eX2BhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAA -AAAAAAYnFmAAAAAABlAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2htAAAAAAAAAABrbGlqAAAAAHAAbm9 -ycwBnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACYAJgAmAD4AUgCCAMoBCgFO -AVwBcgGIAaYBvAHKAdYB6AH2AgwCIAJKAogCpgLWAw4DIgNkA5wDugPUA+gD/AQQBEYEogS8BPoFJ -gVSBWoFgAWwBcoF1gX6BhQGJAZMBmgGiga0BuIHGgdUB2YHkAeiB8AH3AfyCAoIHAgqCDoITghcCG -oIogjSCPoJKglYCXwJwgnqCgIKKApACl4Klgq8CtwLDAs8C1YLjAuyC9oL7gwMDCYMSAxgDKAMrAz -qDQoNTA1mDYQNoA2uDcAN2g3oDfYODA4iDkoOXA5sDnoOnA7EDvwAAAAFAAAAAAH0ArwAAwAGAAkA -DAAPAAAxESERAxMhExcRASELARETAfT6qv6syKr+jgFUqsiqArz9RAGLAP/+1P8B/v3VAP8BLP4CA -P8AAgA5//IAuQKyAAMACwAANyMDMwIyFhQGIiY0oE4MZk84JCQ4JLQB/v3AJDgkJDgAAgBWAeUBPA -LfAAMABwAAEyMnMxcjJzOmRgpagkYKWgHl+vr6AAAAAAIARgAAAf4CsgAbAB8AAAEHMxUjByM3Iwc -jNyM1MzcjNTM3MwczNzMHMxUrAQczAZgdZXEvOi9bLzovWmYdZXEvOi9bLzovWp9bHlsBn4w429vb -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 -EQQ/BCQvEjQCFQZWFSEWQv40AcxCDiUblhP9uSEAAAAAAAAWAQ4AAQAAAAAAAAATACgAAQAAAAAAA -QAHAEwAAQAAAAAAAgAHAGQAAQAAAAAAAwAaAKIAAQAAAAAABAAHAM0AAQAAAAAABQA8AU8AAQAAAA -AABgAPAawAAQAAAAAACAALAdQAAQAAAAAACQALAfgAAQAAAAAACwAXAjQAAQAAAAAADAAXAnwAAwA -BBAkAAAAmAAAAAwABBAkAAQAOADwAAwABBAkAAgAOAFQAAwABBAkAAwA0AGwAAwABBAkABAAOAL0A -AwABBAkABQB4ANUAAwABBAkABgAeAYwAAwABBAkACAAWAbwAAwABBAkACQAWAeAAAwABBAkACwAuA -gQAAwABBAkADAAuAkwATgBvACAAUgBpAGcAaAB0AHMAIABSAGUAcwBlAHIAdgBlAGQALgAATm8gUm -lnaHRzIFJlc2VydmVkLgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAUgBlAGcAdQBsAGEAcgAAUmV -ndWxhcgAAMQAuADEAMAAyADsAVQBLAFcATgA7AEEAaQBsAGUAcgBvAG4ALQBSAGUAZwB1AGwAYQBy -AAAxLjEwMjtVS1dOO0FpbGVyb24tUmVndWxhcgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAVgBlA -HIAcwBpAG8AbgAgADEALgAxADAAMgA7AFAAUwAgADAAMAAxAC4AMQAwADIAOwBoAG8AdABjAG8Abg -B2ACAAMQAuADAALgA3ADAAOwBtAGEAawBlAG8AdABmAC4AbABpAGIAMgAuADUALgA1ADgAMwAyADk -AAFZlcnNpb24gMS4xMDI7UFMgMDAxLjEwMjtob3Rjb252IDEuMC43MDttYWtlb3RmLmxpYjIuNS41 -ODMyOQAAQQBpAGwAZQByAG8AbgAtAFIAZQBnAHUAbABhAHIAAEFpbGVyb24tUmVndWxhcgAAUwBvA -HIAYQAgAFMAYQBnAGEAbgBvAABTb3JhIFNhZ2FubwAAUwBvAHIAYQAgAFMAYQBnAGEAbgBvAABTb3 -JhIFNhZ2FubwAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBsAG8AbgAuAG4AZQB0AAB -odHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBs -AG8AbgAuAG4AZQB0AABodHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAAAACAAAAAAAA/4MAMgAAAAAAA -AAAAAAAAAAAAAAAAAAAAHQAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATAB -QAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAA -xADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0A -TgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAIsAqQCDAJMAjQDDAKoAtgC3A -LQAtQCrAL4AvwC8AIwAwADBAAAAAAAB//8AAgABAAAADAAAABwAAAACAAIAAwBxAAEAcgBzAAIABA -AAAAIAAAABAAAACgBMAGYAAkRGTFQADmxhdG4AGgAEAAAAAP//AAEAAAAWAANDQVQgAB5NT0wgABZ -ST00gABYAAP//AAEAAAAA//8AAgAAAAEAAmxpZ2EADmxvY2wAFAAAAAEAAQAAAAEAAAACAAYAEAAG -AAAAAgASADQABAAAAAEATAADAAAAAgAQABYAAQAcAAAAAQABAE8AAQABAGcAAQABAE8AAwAAAAIAE -AAWAAEAHAAAAAEAAQAvAAEAAQBnAAEAAQAvAAEAGgABAAgAAgAGAAwAcwACAE8AcgACAEwAAQABAE -kAAAABAAAACgBGAGAAAkRGTFQADmxhdG4AHAAEAAAAAP//AAIAAAABABYAA0NBVCAAFk1PTCAAFlJ -PTSAAFgAA//8AAgAAAAEAAmNwc3AADmtlcm4AFAAAAAEAAAAAAAEAAQACAAYADgABAAAAAQASAAIA -AAACAB4ANgABAAoABQAFAAoAAgABACQAPQAAAAEAEgAEAAAAAQAMAAEAOP/nAAEAAQAkAAIGigAEA -AAFJAXKABoAGQAA//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAD/sv+4/+z/7v/MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAD/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9T/6AAAAAD/8QAA -ABD/vQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7gAAAAAAAAAAAAAAAAAA//MAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAP/5AAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAD/4AAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//L/9AAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAA/+gAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/mAAAAAAAAAAAAAAAAAAD -/4gAA//AAAAAA//YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAP/OAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/zv/qAAAAAP/0AAAACAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZAAD/egAA/1kAAAAA/5D/rgAAAAAAAAAAAA -AAAAAAAAAAAAAAAAD/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAD/8AAA/7b/8P+wAAD/8P/E/98AAAAA/8P/+P/0//oAAAAAAAAAAAAA//gA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/w//C/9MAAP/SAAD/9wAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/yAAA/+kAAAAA//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9wAAAAD//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAP/2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAP/cAAAAAAAAAAAAAAAA/7YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/6AAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAkAFAAEAAAAAQACwAAABcA -BgAAAAAAAAAIAA4AAAAAAAsAEgAAAAAAAAATABkAAwANAAAAAQAJAAAAAAAAAAAAAAAAAAAAGAAAA -AAABwAAAAAAAAAAAAAAFQAFAAAAAAAYABgAAAAUAAAACgAAAAwAAgAPABEAFgAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAEAEQBdAAYAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAABwAAAAAACAAAAAAAAAAAAAcAAAAHAAAAEwAJ -ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA -gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC -YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA -AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ== -""" - ) - ), - 10 if size is None else size, - layout_engine=Layout.BASIC, - ) - return load_default_imagefont() diff --git a/python_modules/PIL/ImageGrab.py b/python_modules/PIL/ImageGrab.py deleted file mode 100644 index 1eb450734..000000000 --- a/python_modules/PIL/ImageGrab.py +++ /dev/null @@ -1,196 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# screen grabber -# -# History: -# 2001-04-26 fl created -# 2001-09-17 fl use builtin driver, if present -# 2002-11-19 fl added grabclipboard support -# -# Copyright (c) 2001-2002 by Secret Labs AB -# Copyright (c) 2001-2002 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import shutil -import subprocess -import sys -import tempfile - -from . import Image - -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import ImageWin - - -def grab( - bbox: tuple[int, int, int, int] | None = None, - include_layered_windows: bool = False, - all_screens: bool = False, - xdisplay: str | None = None, - window: int | ImageWin.HWND | None = None, -) -> Image.Image: - im: Image.Image - if xdisplay is None: - if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - args = ["screencapture"] - if bbox: - left, top, right, bottom = bbox - args += ["-R", f"{left},{top},{right-left},{bottom-top}"] - subprocess.call(args + ["-x", filepath]) - im = Image.open(filepath) - im.load() - os.unlink(filepath) - if bbox: - im_resized = im.resize((right - left, bottom - top)) - im.close() - return im_resized - return im - elif sys.platform == "win32": - if window is not None: - all_screens = -1 - offset, size, data = Image.core.grabscreen_win32( - include_layered_windows, - all_screens, - int(window) if window is not None else 0, - ) - im = Image.frombytes( - "RGB", - size, - data, - # RGB, 32-bit line padding, origin lower left corner - "raw", - "BGR", - (size[0] * 3 + 3) & -4, - -1, - ) - if bbox: - x0, y0 = offset - left, top, right, bottom = bbox - im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) - return im - # Cast to Optional[str] needed for Windows and macOS. - display_name: str | None = xdisplay - try: - if not Image.core.HAVE_XCB: - msg = "Pillow was built without XCB support" - raise OSError(msg) - size, data = Image.core.grabscreen_x11(display_name) - except OSError: - if display_name is None and sys.platform not in ("darwin", "win32"): - if shutil.which("gnome-screenshot"): - args = ["gnome-screenshot", "-f"] - elif shutil.which("grim"): - args = ["grim"] - elif shutil.which("spectacle"): - args = ["spectacle", "-n", "-b", "-f", "-o"] - else: - raise - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - subprocess.call(args + [filepath]) - im = Image.open(filepath) - im.load() - os.unlink(filepath) - if bbox: - im_cropped = im.crop(bbox) - im.close() - return im_cropped - return im - else: - raise - else: - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im - - -def grabclipboard() -> Image.Image | list[str] | None: - if sys.platform == "darwin": - p = subprocess.run( - ["osascript", "-e", "get the clipboard as «class PNGf»"], - capture_output=True, - ) - if p.returncode != 0: - return None - - import binascii - - data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3])) - return Image.open(data) - elif sys.platform == "win32": - fmt, data = Image.core.grabclipboard_win32() - if fmt == "file": # CF_HDROP - import struct - - o = struct.unpack_from("I", data)[0] - if data[16] == 0: - files = data[o:].decode("mbcs").split("\0") - else: - files = data[o:].decode("utf-16le").split("\0") - return files[: files.index("")] - if isinstance(data, bytes): - data = io.BytesIO(data) - if fmt == "png": - from . import PngImagePlugin - - return PngImagePlugin.PngImageFile(data) - elif fmt == "DIB": - from . import BmpImagePlugin - - return BmpImagePlugin.DibImageFile(data) - return None - else: - if os.getenv("WAYLAND_DISPLAY"): - session_type = "wayland" - elif os.getenv("DISPLAY"): - session_type = "x11" - else: # Session type check failed - session_type = None - - if shutil.which("wl-paste") and session_type in ("wayland", None): - args = ["wl-paste", "-t", "image"] - elif shutil.which("xclip") and session_type in ("x11", None): - args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] - else: - msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" - raise NotImplementedError(msg) - - p = subprocess.run(args, capture_output=True) - if p.returncode != 0: - err = p.stderr - for silent_error in [ - # wl-paste, when the clipboard is empty - b"Nothing is copied", - # Ubuntu/Debian wl-paste, when the clipboard is empty - b"No selection", - # Ubuntu/Debian wl-paste, when an image isn't available - b"No suitable type of content copied", - # wl-paste or Ubuntu/Debian xclip, when an image isn't available - b" not available", - # xclip, when an image isn't available - b"cannot convert ", - # xclip, when the clipboard isn't initialized - b"xclip: Error: There is no owner for the ", - ]: - if silent_error in err: - return None - msg = f"{args[0]} error" - if err: - msg += f": {err.strip().decode()}" - raise ChildProcessError(msg) - - data = io.BytesIO(p.stdout) - im = Image.open(data) - im.load() - return im diff --git a/python_modules/PIL/ImageMath.py b/python_modules/PIL/ImageMath.py deleted file mode 100644 index c33809ced..000000000 --- a/python_modules/PIL/ImageMath.py +++ /dev/null @@ -1,368 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# a simple math add-on for the Python Imaging Library -# -# History: -# 1999-02-15 fl Original PIL Plus release -# 2005-05-05 fl Simplified and cleaned up for PIL 1.1.6 -# 2005-09-12 fl Fixed int() and float() for Python 2.4.1 -# -# Copyright (c) 1999-2005 by Secret Labs AB -# Copyright (c) 2005 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import builtins -from types import CodeType -from typing import Any, Callable - -from . import Image, _imagingmath -from ._deprecate import deprecate - - -class _Operand: - """Wraps an image operand, providing standard operators""" - - def __init__(self, im: Image.Image): - self.im = im - - def __fixup(self, im1: _Operand | float) -> Image.Image: - # convert image to suitable mode - if isinstance(im1, _Operand): - # argument was an image. - if im1.im.mode in ("1", "L"): - return im1.im.convert("I") - elif im1.im.mode in ("I", "F"): - return im1.im - else: - msg = f"unsupported mode: {im1.im.mode}" - raise ValueError(msg) - else: - # argument was a constant - if isinstance(im1, (int, float)) and self.im.mode in ("1", "L", "I"): - return Image.new("I", self.im.size, im1) - else: - return Image.new("F", self.im.size, im1) - - def apply( - self, - op: str, - im1: _Operand | float, - im2: _Operand | float | None = None, - mode: str | None = None, - ) -> _Operand: - im_1 = self.__fixup(im1) - if im2 is None: - # unary operation - out = Image.new(mode or im_1.mode, im_1.size, None) - try: - op = getattr(_imagingmath, f"{op}_{im_1.mode}") - except AttributeError as e: - msg = f"bad operand type for '{op}'" - raise TypeError(msg) from e - _imagingmath.unop(op, out.getim(), im_1.getim()) - else: - # binary operation - im_2 = self.__fixup(im2) - if im_1.mode != im_2.mode: - # convert both arguments to floating point - if im_1.mode != "F": - im_1 = im_1.convert("F") - if im_2.mode != "F": - im_2 = im_2.convert("F") - if im_1.size != im_2.size: - # crop both arguments to a common size - size = ( - min(im_1.size[0], im_2.size[0]), - min(im_1.size[1], im_2.size[1]), - ) - if im_1.size != size: - im_1 = im_1.crop((0, 0) + size) - if im_2.size != size: - im_2 = im_2.crop((0, 0) + size) - out = Image.new(mode or im_1.mode, im_1.size, None) - try: - op = getattr(_imagingmath, f"{op}_{im_1.mode}") - except AttributeError as e: - msg = f"bad operand type for '{op}'" - raise TypeError(msg) from e - _imagingmath.binop(op, out.getim(), im_1.getim(), im_2.getim()) - return _Operand(out) - - # unary operators - def __bool__(self) -> bool: - # an image is "true" if it contains at least one non-zero pixel - return self.im.getbbox() is not None - - def __abs__(self) -> _Operand: - return self.apply("abs", self) - - def __pos__(self) -> _Operand: - return self - - def __neg__(self) -> _Operand: - return self.apply("neg", self) - - # binary operators - def __add__(self, other: _Operand | float) -> _Operand: - return self.apply("add", self, other) - - def __radd__(self, other: _Operand | float) -> _Operand: - return self.apply("add", other, self) - - def __sub__(self, other: _Operand | float) -> _Operand: - return self.apply("sub", self, other) - - def __rsub__(self, other: _Operand | float) -> _Operand: - return self.apply("sub", other, self) - - def __mul__(self, other: _Operand | float) -> _Operand: - return self.apply("mul", self, other) - - def __rmul__(self, other: _Operand | float) -> _Operand: - return self.apply("mul", other, self) - - def __truediv__(self, other: _Operand | float) -> _Operand: - return self.apply("div", self, other) - - def __rtruediv__(self, other: _Operand | float) -> _Operand: - return self.apply("div", other, self) - - def __mod__(self, other: _Operand | float) -> _Operand: - return self.apply("mod", self, other) - - def __rmod__(self, other: _Operand | float) -> _Operand: - return self.apply("mod", other, self) - - def __pow__(self, other: _Operand | float) -> _Operand: - return self.apply("pow", self, other) - - def __rpow__(self, other: _Operand | float) -> _Operand: - return self.apply("pow", other, self) - - # bitwise - def __invert__(self) -> _Operand: - return self.apply("invert", self) - - def __and__(self, other: _Operand | float) -> _Operand: - return self.apply("and", self, other) - - def __rand__(self, other: _Operand | float) -> _Operand: - return self.apply("and", other, self) - - def __or__(self, other: _Operand | float) -> _Operand: - return self.apply("or", self, other) - - def __ror__(self, other: _Operand | float) -> _Operand: - return self.apply("or", other, self) - - def __xor__(self, other: _Operand | float) -> _Operand: - return self.apply("xor", self, other) - - def __rxor__(self, other: _Operand | float) -> _Operand: - return self.apply("xor", other, self) - - def __lshift__(self, other: _Operand | float) -> _Operand: - return self.apply("lshift", self, other) - - def __rshift__(self, other: _Operand | float) -> _Operand: - return self.apply("rshift", self, other) - - # logical - def __eq__(self, other: _Operand | float) -> _Operand: # type: ignore[override] - return self.apply("eq", self, other) - - def __ne__(self, other: _Operand | float) -> _Operand: # type: ignore[override] - return self.apply("ne", self, other) - - def __lt__(self, other: _Operand | float) -> _Operand: - return self.apply("lt", self, other) - - def __le__(self, other: _Operand | float) -> _Operand: - return self.apply("le", self, other) - - def __gt__(self, other: _Operand | float) -> _Operand: - return self.apply("gt", self, other) - - def __ge__(self, other: _Operand | float) -> _Operand: - return self.apply("ge", self, other) - - -# conversions -def imagemath_int(self: _Operand) -> _Operand: - return _Operand(self.im.convert("I")) - - -def imagemath_float(self: _Operand) -> _Operand: - return _Operand(self.im.convert("F")) - - -# logical -def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("eq", self, other, mode="I") - - -def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("ne", self, other, mode="I") - - -def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("min", self, other) - - -def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("max", self, other) - - -def imagemath_convert(self: _Operand, mode: str) -> _Operand: - return _Operand(self.im.convert(mode)) - - -ops = { - "int": imagemath_int, - "float": imagemath_float, - "equal": imagemath_equal, - "notequal": imagemath_notequal, - "min": imagemath_min, - "max": imagemath_max, - "convert": imagemath_convert, -} - - -def lambda_eval( - expression: Callable[[dict[str, Any]], Any], - options: dict[str, Any] = {}, - **kw: Any, -) -> Any: - """ - Returns the result of an image function. - - :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band - images, use the :py:meth:`~PIL.Image.Image.split` method or - :py:func:`~PIL.Image.merge` function. - - :param expression: A function that receives a dictionary. - :param options: Values to add to the function's dictionary. Deprecated. - You can instead use one or more keyword arguments. - :param **kw: Values to add to the function's dictionary. - :return: The expression result. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - """ - - if options: - deprecate( - "ImageMath.lambda_eval options", - 12, - "ImageMath.lambda_eval keyword arguments", - ) - - args: dict[str, Any] = ops.copy() - args.update(options) - args.update(kw) - for k, v in args.items(): - if isinstance(v, Image.Image): - args[k] = _Operand(v) - - out = expression(args) - try: - return out.im - except AttributeError: - return out - - -def unsafe_eval( - expression: str, - options: dict[str, Any] = {}, - **kw: Any, -) -> Any: - """ - Evaluates an image expression. This uses Python's ``eval()`` function to process - the expression string, and carries the security risks of doing so. It is not - recommended to process expressions without considering this. - :py:meth:`~lambda_eval` is a more secure alternative. - - :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band - images, use the :py:meth:`~PIL.Image.Image.split` method or - :py:func:`~PIL.Image.merge` function. - - :param expression: A string containing a Python-style expression. - :param options: Values to add to the evaluation context. Deprecated. - You can instead use one or more keyword arguments. - :param **kw: Values to add to the evaluation context. - :return: The evaluated expression. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - """ - - if options: - deprecate( - "ImageMath.unsafe_eval options", - 12, - "ImageMath.unsafe_eval keyword arguments", - ) - - # build execution namespace - args: dict[str, Any] = ops.copy() - for k in [*options, *kw]: - if "__" in k or hasattr(builtins, k): - msg = f"'{k}' not allowed" - raise ValueError(msg) - - args.update(options) - args.update(kw) - for k, v in args.items(): - if isinstance(v, Image.Image): - args[k] = _Operand(v) - - compiled_code = compile(expression, "", "eval") - - def scan(code: CodeType) -> None: - for const in code.co_consts: - if type(const) is type(compiled_code): - scan(const) - - for name in code.co_names: - if name not in args and name != "abs": - msg = f"'{name}' not allowed" - raise ValueError(msg) - - scan(compiled_code) - out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) - try: - return out.im - except AttributeError: - return out - - -def eval( - expression: str, - _dict: dict[str, Any] = {}, - **kw: Any, -) -> Any: - """ - Evaluates an image expression. - - Deprecated. Use lambda_eval() or unsafe_eval() instead. - - :param expression: A string containing a Python-style expression. - :param _dict: Values to add to the evaluation context. You - can either use a dictionary, or one or more keyword - arguments. - :return: The evaluated expression. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - - .. deprecated:: 10.3.0 - """ - - deprecate( - "ImageMath.eval", - 12, - "ImageMath.lambda_eval or ImageMath.unsafe_eval", - ) - return unsafe_eval(expression, _dict, **kw) diff --git a/python_modules/PIL/ImageMode.py b/python_modules/PIL/ImageMode.py deleted file mode 100644 index 92a08d2cb..000000000 --- a/python_modules/PIL/ImageMode.py +++ /dev/null @@ -1,92 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard mode descriptors -# -# History: -# 2006-03-20 fl Added -# -# Copyright (c) 2006 by Secret Labs AB. -# Copyright (c) 2006 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import sys -from functools import lru_cache -from typing import NamedTuple - -from ._deprecate import deprecate - - -class ModeDescriptor(NamedTuple): - """Wrapper for mode strings.""" - - mode: str - bands: tuple[str, ...] - basemode: str - basetype: str - typestr: str - - def __str__(self) -> str: - return self.mode - - -@lru_cache -def getmode(mode: str) -> ModeDescriptor: - """Gets a mode descriptor for the given mode.""" - endian = "<" if sys.byteorder == "little" else ">" - - modes = { - # core modes - # Bits need to be extended to bytes - "1": ("L", "L", ("1",), "|b1"), - "L": ("L", "L", ("L",), "|u1"), - "I": ("L", "I", ("I",), f"{endian}i4"), - "F": ("L", "F", ("F",), f"{endian}f4"), - "P": ("P", "L", ("P",), "|u1"), - "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), - "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), - "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), - # UNDONE - unsigned |u1i1i1 - "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), - "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), - # extra experimental modes - "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), - "LA": ("L", "L", ("L", "A"), "|u1"), - "La": ("L", "L", ("L", "a"), "|u1"), - "PA": ("RGB", "L", ("P", "A"), "|u1"), - } - if mode in modes: - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - base_mode, base_type, bands, type_str = modes[mode] - return ModeDescriptor(mode, bands, base_mode, base_type, type_str) - - mapping_modes = { - # I;16 == I;16L, and I;32 == I;32L - "I;16": "u2", - "I;16BS": ">i2", - "I;16N": f"{endian}u2", - "I;16NS": f"{endian}i2", - "I;32": "u4", - "I;32L": "i4", - "I;32LS": " -from __future__ import annotations - -import re - -from . import Image, _imagingmorph - -LUT_SIZE = 1 << 9 - -# fmt: off -ROTATION_MATRIX = [ - 6, 3, 0, - 7, 4, 1, - 8, 5, 2, -] -MIRROR_MATRIX = [ - 2, 1, 0, - 5, 4, 3, - 8, 7, 6, -] -# fmt: on - - -class LutBuilder: - """A class for building a MorphLut from a descriptive language - - The input patterns is a list of a strings sequences like these:: - - 4:(... - .1. - 111)->1 - - (whitespaces including linebreaks are ignored). The option 4 - describes a series of symmetry operations (in this case a - 4-rotation), the pattern is described by: - - - . or X - Ignore - - 1 - Pixel is on - - 0 - Pixel is off - - The result of the operation is described after "->" string. - - The default is to return the current pixel value, which is - returned if no other match is found. - - Operations: - - - 4 - 4 way rotation - - N - Negate - - 1 - Dummy op for no other operation (an op must always be given) - - M - Mirroring - - Example:: - - lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) - lut = lb.build_lut() - - """ - - def __init__( - self, patterns: list[str] | None = None, op_name: str | None = None - ) -> None: - if patterns is not None: - self.patterns = patterns - else: - self.patterns = [] - self.lut: bytearray | None = None - if op_name is not None: - known_patterns = { - "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], - "dilation4": ["4:(... .0. .1.)->1"], - "dilation8": ["4:(... .0. .1.)->1", "4:(... .0. ..1)->1"], - "erosion4": ["4:(... .1. .0.)->0"], - "erosion8": ["4:(... .1. .0.)->0", "4:(... .1. ..0)->0"], - "edge": [ - "1:(... ... ...)->0", - "4:(.0. .1. ...)->1", - "4:(01. .1. ...)->1", - ], - } - if op_name not in known_patterns: - msg = f"Unknown pattern {op_name}!" - raise Exception(msg) - - self.patterns = known_patterns[op_name] - - def add_patterns(self, patterns: list[str]) -> None: - self.patterns += patterns - - def build_default_lut(self) -> None: - symbols = [0, 1] - m = 1 << 4 # pos of current pixel - self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) - - def get_lut(self) -> bytearray | None: - return self.lut - - def _string_permute(self, pattern: str, permutation: list[int]) -> str: - """string_permute takes a pattern and a permutation and returns the - string permuted according to the permutation list. - """ - assert len(permutation) == 9 - return "".join(pattern[p] for p in permutation) - - def _pattern_permute( - self, basic_pattern: str, options: str, basic_result: int - ) -> list[tuple[str, int]]: - """pattern_permute takes a basic pattern and its result and clones - the pattern according to the modifications described in the $options - parameter. It returns a list of all cloned patterns.""" - patterns = [(basic_pattern, basic_result)] - - # rotations - if "4" in options: - res = patterns[-1][1] - for i in range(4): - patterns.append( - (self._string_permute(patterns[-1][0], ROTATION_MATRIX), res) - ) - # mirror - if "M" in options: - n = len(patterns) - for pattern, res in patterns[:n]: - patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res)) - - # negate - if "N" in options: - n = len(patterns) - for pattern, res in patterns[:n]: - # Swap 0 and 1 - pattern = pattern.replace("0", "Z").replace("1", "0").replace("Z", "1") - res = 1 - int(res) - patterns.append((pattern, res)) - - return patterns - - def build_lut(self) -> bytearray: - """Compile all patterns into a morphology lut. - - TBD :Build based on (file) morphlut:modify_lut - """ - self.build_default_lut() - assert self.lut is not None - patterns = [] - - # Parse and create symmetries of the patterns strings - for p in self.patterns: - m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) - if not m: - msg = 'Syntax error in pattern "' + p + '"' - raise Exception(msg) - options = m.group(1) - pattern = m.group(2) - result = int(m.group(3)) - - # Get rid of spaces - pattern = pattern.replace(" ", "").replace("\n", "") - - patterns += self._pattern_permute(pattern, options, result) - - # compile the patterns into regular expressions for speed - compiled_patterns = [] - for pattern in patterns: - p = pattern[0].replace(".", "X").replace("X", "[01]") - compiled_patterns.append((re.compile(p), pattern[1])) - - # Step through table and find patterns that match. - # Note that all the patterns are searched. The last one - # caught overrides - for i in range(LUT_SIZE): - # Build the bit pattern - bitpattern = bin(i)[2:] - bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] - - for pattern, r in compiled_patterns: - if pattern.match(bitpattern): - self.lut[i] = [0, 1][r] - - return self.lut - - -class MorphOp: - """A class for binary morphological operators""" - - def __init__( - self, - lut: bytearray | None = None, - op_name: str | None = None, - patterns: list[str] | None = None, - ) -> None: - """Create a binary morphological operator""" - self.lut = lut - if op_name is not None: - self.lut = LutBuilder(op_name=op_name).build_lut() - elif patterns is not None: - self.lut = LutBuilder(patterns=patterns).build_lut() - - def apply(self, image: Image.Image) -> tuple[int, Image.Image]: - """Run a single morphological operation on an image - - Returns a tuple of the number of changed pixels and the - morphed image""" - if self.lut is None: - msg = "No operator loaded" - raise Exception(msg) - - if image.mode != "L": - msg = "Image mode must be L" - raise ValueError(msg) - outimage = Image.new(image.mode, image.size, None) - count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim()) - return count, outimage - - def match(self, image: Image.Image) -> list[tuple[int, int]]: - """Get a list of coordinates matching the morphological operation on - an image. - - Returns a list of tuples of (x,y) coordinates - of all matching pixels. See :ref:`coordinate-system`.""" - if self.lut is None: - msg = "No operator loaded" - raise Exception(msg) - - if image.mode != "L": - msg = "Image mode must be L" - raise ValueError(msg) - return _imagingmorph.match(bytes(self.lut), image.getim()) - - def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: - """Get a list of all turned on pixels in a binary image - - Returns a list of tuples of (x,y) coordinates - of all matching pixels. See :ref:`coordinate-system`.""" - - if image.mode != "L": - msg = "Image mode must be L" - raise ValueError(msg) - return _imagingmorph.get_on_pixels(image.getim()) - - def load_lut(self, filename: str) -> None: - """Load an operator from an mrl file""" - with open(filename, "rb") as f: - self.lut = bytearray(f.read()) - - if len(self.lut) != LUT_SIZE: - self.lut = None - msg = "Wrong size operator file!" - raise Exception(msg) - - def save_lut(self, filename: str) -> None: - """Save an operator to an mrl file""" - if self.lut is None: - msg = "No operator loaded" - raise Exception(msg) - with open(filename, "wb") as f: - f.write(self.lut) - - def set_lut(self, lut: bytearray | None) -> None: - """Set the lut from an external source""" - self.lut = lut diff --git a/python_modules/PIL/ImageOps.py b/python_modules/PIL/ImageOps.py deleted file mode 100644 index da28854b5..000000000 --- a/python_modules/PIL/ImageOps.py +++ /dev/null @@ -1,745 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard image operations -# -# History: -# 2001-10-20 fl Created -# 2001-10-23 fl Added autocontrast operator -# 2001-12-18 fl Added Kevin's fit operator -# 2004-03-14 fl Fixed potential division by zero in equalize -# 2005-05-05 fl Fixed equalize for low number of values -# -# Copyright (c) 2001-2004 by Secret Labs AB -# Copyright (c) 2001-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import functools -import operator -import re -from collections.abc import Sequence -from typing import Literal, Protocol, cast, overload - -from . import ExifTags, Image, ImagePalette - -# -# helpers - - -def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: - if isinstance(border, tuple): - if len(border) == 2: - left, top = right, bottom = border - elif len(border) == 4: - left, top, right, bottom = border - else: - left = top = right = bottom = border - return left, top, right, bottom - - -def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: - if isinstance(color, str): - from . import ImageColor - - color = ImageColor.getcolor(color, mode) - return color - - -def _lut(image: Image.Image, lut: list[int]) -> Image.Image: - if image.mode == "P": - # FIXME: apply to lookup table, not image data - msg = "mode P support coming soon" - raise NotImplementedError(msg) - elif image.mode in ("L", "RGB"): - if image.mode == "RGB" and len(lut) == 256: - lut = lut + lut + lut - return image.point(lut) - else: - msg = f"not supported for mode {image.mode}" - raise OSError(msg) - - -# -# actions - - -def autocontrast( - image: Image.Image, - cutoff: float | tuple[float, float] = 0, - ignore: int | Sequence[int] | None = None, - mask: Image.Image | None = None, - preserve_tone: bool = False, -) -> Image.Image: - """ - Maximize (normalize) image contrast. This function calculates a - histogram of the input image (or mask region), removes ``cutoff`` percent of the - lightest and darkest pixels from the histogram, and remaps the image - so that the darkest pixel becomes black (0), and the lightest - becomes white (255). - - :param image: The image to process. - :param cutoff: The percent to cut off from the histogram on the low and - high ends. Either a tuple of (low, high), or a single - number for both. - :param ignore: The background pixel value (use None for no background). - :param mask: Histogram used in contrast operation is computed using pixels - within the mask. If no mask is given the entire image is used - for histogram computation. - :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast. - - .. versionadded:: 8.2.0 - - :return: An image. - """ - if preserve_tone: - histogram = image.convert("L").histogram(mask) - else: - histogram = image.histogram(mask) - - lut = [] - for layer in range(0, len(histogram), 256): - h = histogram[layer : layer + 256] - if ignore is not None: - # get rid of outliers - if isinstance(ignore, int): - h[ignore] = 0 - else: - for ix in ignore: - h[ix] = 0 - if cutoff: - # cut off pixels from both ends of the histogram - if not isinstance(cutoff, tuple): - cutoff = (cutoff, cutoff) - # get number of pixels - n = 0 - for ix in range(256): - n = n + h[ix] - # remove cutoff% pixels from the low end - cut = int(n * cutoff[0] // 100) - for lo in range(256): - if cut > h[lo]: - cut = cut - h[lo] - h[lo] = 0 - else: - h[lo] -= cut - cut = 0 - if cut <= 0: - break - # remove cutoff% samples from the high end - cut = int(n * cutoff[1] // 100) - for hi in range(255, -1, -1): - if cut > h[hi]: - cut = cut - h[hi] - h[hi] = 0 - else: - h[hi] -= cut - cut = 0 - if cut <= 0: - break - # find lowest/highest samples after preprocessing - for lo in range(256): - if h[lo]: - break - for hi in range(255, -1, -1): - if h[hi]: - break - if hi <= lo: - # don't bother - lut.extend(list(range(256))) - else: - scale = 255.0 / (hi - lo) - offset = -lo * scale - for ix in range(256): - ix = int(ix * scale + offset) - if ix < 0: - ix = 0 - elif ix > 255: - ix = 255 - lut.append(ix) - return _lut(image, lut) - - -def colorize( - image: Image.Image, - black: str | tuple[int, ...], - white: str | tuple[int, ...], - mid: str | int | tuple[int, ...] | None = None, - blackpoint: int = 0, - whitepoint: int = 255, - midpoint: int = 127, -) -> Image.Image: - """ - Colorize grayscale image. - This function calculates a color wedge which maps all black pixels in - the source image to the first color and all white pixels to the - second color. If ``mid`` is specified, it uses three-color mapping. - The ``black`` and ``white`` arguments should be RGB tuples or color names; - optionally you can use three-color mapping by also specifying ``mid``. - Mapping positions for any of the colors can be specified - (e.g. ``blackpoint``), where these parameters are the integer - value corresponding to where the corresponding color should be mapped. - These parameters must have logical order, such that - ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified). - - :param image: The image to colorize. - :param black: The color to use for black input pixels. - :param white: The color to use for white input pixels. - :param mid: The color to use for midtone input pixels. - :param blackpoint: an int value [0, 255] for the black mapping. - :param whitepoint: an int value [0, 255] for the white mapping. - :param midpoint: an int value [0, 255] for the midtone mapping. - :return: An image. - """ - - # Initial asserts - assert image.mode == "L" - if mid is None: - assert 0 <= blackpoint <= whitepoint <= 255 - else: - assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 - - # Define colors from arguments - rgb_black = cast(Sequence[int], _color(black, "RGB")) - rgb_white = cast(Sequence[int], _color(white, "RGB")) - rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None - - # Empty lists for the mapping - red = [] - green = [] - blue = [] - - # Create the low-end values - for i in range(blackpoint): - red.append(rgb_black[0]) - green.append(rgb_black[1]) - blue.append(rgb_black[2]) - - # Create the mapping (2-color) - if rgb_mid is None: - range_map = range(whitepoint - blackpoint) - - for i in range_map: - red.append( - rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) - ) - green.append( - rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) - ) - blue.append( - rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) - ) - - # Create the mapping (3-color) - else: - range_map1 = range(midpoint - blackpoint) - range_map2 = range(whitepoint - midpoint) - - for i in range_map1: - red.append( - rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) - ) - green.append( - rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) - ) - blue.append( - rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) - ) - for i in range_map2: - red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) - green.append( - rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) - ) - blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) - - # Create the high-end values - for i in range(256 - whitepoint): - red.append(rgb_white[0]) - green.append(rgb_white[1]) - blue.append(rgb_white[2]) - - # Return converted image - image = image.convert("RGB") - return _lut(image, red + green + blue) - - -def contain( - image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC -) -> Image.Image: - """ - Returns a resized version of the image, set to the maximum width and height - within the requested size, while maintaining the original aspect ratio. - - :param image: The image to resize. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :return: An image. - """ - - im_ratio = image.width / image.height - dest_ratio = size[0] / size[1] - - if im_ratio != dest_ratio: - if im_ratio > dest_ratio: - new_height = round(image.height / image.width * size[0]) - if new_height != size[1]: - size = (size[0], new_height) - else: - new_width = round(image.width / image.height * size[1]) - if new_width != size[0]: - size = (new_width, size[1]) - return image.resize(size, resample=method) - - -def cover( - image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC -) -> Image.Image: - """ - Returns a resized version of the image, so that the requested size is - covered, while maintaining the original aspect ratio. - - :param image: The image to resize. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :return: An image. - """ - - im_ratio = image.width / image.height - dest_ratio = size[0] / size[1] - - if im_ratio != dest_ratio: - if im_ratio < dest_ratio: - new_height = round(image.height / image.width * size[0]) - if new_height != size[1]: - size = (size[0], new_height) - else: - new_width = round(image.width / image.height * size[1]) - if new_width != size[0]: - size = (new_width, size[1]) - return image.resize(size, resample=method) - - -def pad( - image: Image.Image, - size: tuple[int, int], - method: int = Image.Resampling.BICUBIC, - color: str | int | tuple[int, ...] | None = None, - centering: tuple[float, float] = (0.5, 0.5), -) -> Image.Image: - """ - Returns a resized and padded version of the image, expanded to fill the - requested aspect ratio and size. - - :param image: The image to resize and crop. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :param color: The background color of the padded image. - :param centering: Control the position of the original image within the - padded version. - - (0.5, 0.5) will keep the image centered - (0, 0) will keep the image aligned to the top left - (1, 1) will keep the image aligned to the bottom - right - :return: An image. - """ - - resized = contain(image, size, method) - if resized.size == size: - out = resized - else: - out = Image.new(image.mode, size, color) - if resized.palette: - palette = resized.getpalette() - if palette is not None: - out.putpalette(palette) - if resized.width != size[0]: - x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) - out.paste(resized, (x, 0)) - else: - y = round((size[1] - resized.height) * max(0, min(centering[1], 1))) - out.paste(resized, (0, y)) - return out - - -def crop(image: Image.Image, border: int = 0) -> Image.Image: - """ - Remove border from image. The same amount of pixels are removed - from all four sides. This function works on all image modes. - - .. seealso:: :py:meth:`~PIL.Image.Image.crop` - - :param image: The image to crop. - :param border: The number of pixels to remove. - :return: An image. - """ - left, top, right, bottom = _border(border) - return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) - - -def scale( - image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC -) -> Image.Image: - """ - Returns a rescaled image by a specific factor given in parameter. - A factor greater than 1 expands the image, between 0 and 1 contracts the - image. - - :param image: The image to rescale. - :param factor: The expansion factor, as a float. - :param resample: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - if factor == 1: - return image.copy() - elif factor <= 0: - msg = "the factor must be greater than 0" - raise ValueError(msg) - else: - size = (round(factor * image.width), round(factor * image.height)) - return image.resize(size, resample) - - -class SupportsGetMesh(Protocol): - """ - An object that supports the ``getmesh`` method, taking an image as an - argument, and returning a list of tuples. Each tuple contains two tuples, - the source box as a tuple of 4 integers, and a tuple of 8 integers for the - final quadrilateral, in order of top left, bottom left, bottom right, top - right. - """ - - def getmesh( - self, image: Image.Image - ) -> list[ - tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] - ]: ... - - -def deform( - image: Image.Image, - deformer: SupportsGetMesh, - resample: int = Image.Resampling.BILINEAR, -) -> Image.Image: - """ - Deform the image. - - :param image: The image to deform. - :param deformer: A deformer object. Any object that implements a - ``getmesh`` method can be used. - :param resample: An optional resampling filter. Same values possible as - in the PIL.Image.transform function. - :return: An image. - """ - return image.transform( - image.size, Image.Transform.MESH, deformer.getmesh(image), resample - ) - - -def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: - """ - Equalize the image histogram. This function applies a non-linear - mapping to the input image, in order to create a uniform - distribution of grayscale values in the output image. - - :param image: The image to equalize. - :param mask: An optional mask. If given, only the pixels selected by - the mask are included in the analysis. - :return: An image. - """ - if image.mode == "P": - image = image.convert("RGB") - h = image.histogram(mask) - lut = [] - for b in range(0, len(h), 256): - histo = [_f for _f in h[b : b + 256] if _f] - if len(histo) <= 1: - lut.extend(list(range(256))) - else: - step = (functools.reduce(operator.add, histo) - histo[-1]) // 255 - if not step: - lut.extend(list(range(256))) - else: - n = step // 2 - for i in range(256): - lut.append(n // step) - n = n + h[i + b] - return _lut(image, lut) - - -def expand( - image: Image.Image, - border: int | tuple[int, ...] = 0, - fill: str | int | tuple[int, ...] = 0, -) -> Image.Image: - """ - Add border to the image - - :param image: The image to expand. - :param border: Border width, in pixels. - :param fill: Pixel fill value (a color value). Default is 0 (black). - :return: An image. - """ - left, top, right, bottom = _border(border) - width = left + image.size[0] + right - height = top + image.size[1] + bottom - color = _color(fill, image.mode) - if image.palette: - palette = ImagePalette.ImagePalette(palette=image.getpalette()) - if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4): - color = palette.getcolor(color) - else: - palette = None - out = Image.new(image.mode, (width, height), color) - if palette: - out.putpalette(palette.palette) - out.paste(image, (left, top)) - return out - - -def fit( - image: Image.Image, - size: tuple[int, int], - method: int = Image.Resampling.BICUBIC, - bleed: float = 0.0, - centering: tuple[float, float] = (0.5, 0.5), -) -> Image.Image: - """ - Returns a resized and cropped version of the image, cropped to the - requested aspect ratio and size. - - This function was contributed by Kevin Cazabon. - - :param image: The image to resize and crop. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :param bleed: Remove a border around the outside of the image from all - four edges. The value is a decimal percentage (use 0.01 for - one percent). The default value is 0 (no border). - Cannot be greater than or equal to 0.5. - :param centering: Control the cropping position. Use (0.5, 0.5) for - center cropping (e.g. if cropping the width, take 50% off - of the left side, and therefore 50% off the right side). - (0.0, 0.0) will crop from the top left corner (i.e. if - cropping the width, take all of the crop off of the right - side, and if cropping the height, take all of it off the - bottom). (1.0, 0.0) will crop from the bottom left - corner, etc. (i.e. if cropping the width, take all of the - crop off the left side, and if cropping the height take - none from the top, and therefore all off the bottom). - :return: An image. - """ - - # by Kevin Cazabon, Feb 17/2000 - # kevin@cazabon.com - # https://www.cazabon.com - - centering_x, centering_y = centering - - if not 0.0 <= centering_x <= 1.0: - centering_x = 0.5 - if not 0.0 <= centering_y <= 1.0: - centering_y = 0.5 - - if not 0.0 <= bleed < 0.5: - bleed = 0.0 - - # calculate the area to use for resizing and cropping, subtracting - # the 'bleed' around the edges - - # number of pixels to trim off on Top and Bottom, Left and Right - bleed_pixels = (bleed * image.size[0], bleed * image.size[1]) - - live_size = ( - image.size[0] - bleed_pixels[0] * 2, - image.size[1] - bleed_pixels[1] * 2, - ) - - # calculate the aspect ratio of the live_size - live_size_ratio = live_size[0] / live_size[1] - - # calculate the aspect ratio of the output image - output_ratio = size[0] / size[1] - - # figure out if the sides or top/bottom will be cropped off - if live_size_ratio == output_ratio: - # live_size is already the needed ratio - crop_width = live_size[0] - crop_height = live_size[1] - elif live_size_ratio >= output_ratio: - # live_size is wider than what's needed, crop the sides - crop_width = output_ratio * live_size[1] - crop_height = live_size[1] - else: - # live_size is taller than what's needed, crop the top and bottom - crop_width = live_size[0] - crop_height = live_size[0] / output_ratio - - # make the crop - crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x - crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y - - crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) - - # resize the image and return it - return image.resize(size, method, box=crop) - - -def flip(image: Image.Image) -> Image.Image: - """ - Flip the image vertically (top to bottom). - - :param image: The image to flip. - :return: An image. - """ - return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) - - -def grayscale(image: Image.Image) -> Image.Image: - """ - Convert the image to grayscale. - - :param image: The image to convert. - :return: An image. - """ - return image.convert("L") - - -def invert(image: Image.Image) -> Image.Image: - """ - Invert (negate) the image. - - :param image: The image to invert. - :return: An image. - """ - lut = list(range(255, -1, -1)) - return image.point(lut) if image.mode == "1" else _lut(image, lut) - - -def mirror(image: Image.Image) -> Image.Image: - """ - Flip image horizontally (left to right). - - :param image: The image to mirror. - :return: An image. - """ - return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - - -def posterize(image: Image.Image, bits: int) -> Image.Image: - """ - Reduce the number of bits for each color channel. - - :param image: The image to posterize. - :param bits: The number of bits to keep for each channel (1-8). - :return: An image. - """ - mask = ~(2 ** (8 - bits) - 1) - lut = [i & mask for i in range(256)] - return _lut(image, lut) - - -def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: - """ - Invert all pixel values above a threshold. - - :param image: The image to solarize. - :param threshold: All pixels above this grayscale level are inverted. - :return: An image. - """ - lut = [] - for i in range(256): - if i < threshold: - lut.append(i) - else: - lut.append(255 - i) - return _lut(image, lut) - - -@overload -def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ... - - -@overload -def exif_transpose( - image: Image.Image, *, in_place: Literal[False] = False -) -> Image.Image: ... - - -def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: - """ - If an image has an EXIF Orientation tag, other than 1, transpose the image - accordingly, and remove the orientation data. - - :param image: The image to transpose. - :param in_place: Boolean. Keyword-only argument. - If ``True``, the original image is modified in-place, and ``None`` is returned. - If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned - with the transposition applied. If there is no transposition, a copy of the - image will be returned. - """ - image.load() - image_exif = image.getexif() - orientation = image_exif.get(ExifTags.Base.Orientation, 1) - method = { - 2: Image.Transpose.FLIP_LEFT_RIGHT, - 3: Image.Transpose.ROTATE_180, - 4: Image.Transpose.FLIP_TOP_BOTTOM, - 5: Image.Transpose.TRANSPOSE, - 6: Image.Transpose.ROTATE_270, - 7: Image.Transpose.TRANSVERSE, - 8: Image.Transpose.ROTATE_90, - }.get(orientation) - if method is not None: - if in_place: - image.im = image.im.transpose(method) - image._size = image.im.size - else: - transposed_image = image.transpose(method) - exif_image = image if in_place else transposed_image - - exif = exif_image.getexif() - if ExifTags.Base.Orientation in exif: - del exif[ExifTags.Base.Orientation] - if "exif" in exif_image.info: - exif_image.info["exif"] = exif.tobytes() - elif "Raw profile type exif" in exif_image.info: - exif_image.info["Raw profile type exif"] = exif.tobytes().hex() - for key in ("XML:com.adobe.xmp", "xmp"): - if key in exif_image.info: - for pattern in ( - r'tiff:Orientation="([0-9])"', - r"([0-9])", - ): - value = exif_image.info[key] - if isinstance(value, str): - value = re.sub(pattern, "", value) - elif isinstance(value, tuple): - value = tuple( - re.sub(pattern.encode(), b"", v) for v in value - ) - else: - value = re.sub(pattern.encode(), b"", value) - exif_image.info[key] = value - if not in_place: - return transposed_image - elif not in_place: - return image.copy() - return None diff --git a/python_modules/PIL/ImagePalette.py b/python_modules/PIL/ImagePalette.py deleted file mode 100644 index 103697117..000000000 --- a/python_modules/PIL/ImagePalette.py +++ /dev/null @@ -1,286 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# image palette object -# -# History: -# 1996-03-11 fl Rewritten. -# 1997-01-03 fl Up and running. -# 1997-08-23 fl Added load hack -# 2001-04-16 fl Fixed randint shadow bug in random() -# -# Copyright (c) 1997-2001 by Secret Labs AB -# Copyright (c) 1996-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import array -from collections.abc import Sequence -from typing import IO - -from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile - -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import Image - - -class ImagePalette: - """ - Color palette for palette mapped images - - :param mode: The mode to use for the palette. See: - :ref:`concept-modes`. Defaults to "RGB" - :param palette: An optional palette. If given, it must be a bytearray, - an array or a list of ints between 0-255. The list must consist of - all channels for one color followed by the next color (e.g. RGBRGBRGB). - Defaults to an empty palette. - """ - - def __init__( - self, - mode: str = "RGB", - palette: Sequence[int] | bytes | bytearray | None = None, - ) -> None: - self.mode = mode - self.rawmode: str | None = None # if set, palette contains raw data - self.palette = palette or bytearray() - self.dirty: int | None = None - - @property - def palette(self) -> Sequence[int] | bytes | bytearray: - return self._palette - - @palette.setter - def palette(self, palette: Sequence[int] | bytes | bytearray) -> None: - self._colors: dict[tuple[int, ...], int] | None = None - self._palette = palette - - @property - def colors(self) -> dict[tuple[int, ...], int]: - if self._colors is None: - mode_len = len(self.mode) - self._colors = {} - for i in range(0, len(self.palette), mode_len): - color = tuple(self.palette[i : i + mode_len]) - if color in self._colors: - continue - self._colors[color] = i // mode_len - return self._colors - - @colors.setter - def colors(self, colors: dict[tuple[int, ...], int]) -> None: - self._colors = colors - - def copy(self) -> ImagePalette: - new = ImagePalette() - - new.mode = self.mode - new.rawmode = self.rawmode - if self.palette is not None: - new.palette = self.palette[:] - new.dirty = self.dirty - - return new - - def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]: - """ - Get palette contents in format suitable for the low-level - ``im.putpalette`` primitive. - - .. warning:: This method is experimental. - """ - if self.rawmode: - return self.rawmode, self.palette - return self.mode, self.tobytes() - - def tobytes(self) -> bytes: - """Convert palette to bytes. - - .. warning:: This method is experimental. - """ - if self.rawmode: - msg = "palette contains raw palette data" - raise ValueError(msg) - if isinstance(self.palette, bytes): - return self.palette - arr = array.array("B", self.palette) - return arr.tobytes() - - # Declare tostring as an alias for tobytes - tostring = tobytes - - def _new_color_index( - self, image: Image.Image | None = None, e: Exception | None = None - ) -> int: - if not isinstance(self.palette, bytearray): - self._palette = bytearray(self.palette) - index = len(self.palette) // 3 - special_colors: tuple[int | tuple[int, ...] | None, ...] = () - if image: - special_colors = ( - image.info.get("background"), - image.info.get("transparency"), - ) - while index in special_colors: - index += 1 - if index >= 256: - if image: - # Search for an unused index - for i, count in reversed(list(enumerate(image.histogram()))): - if count == 0 and i not in special_colors: - index = i - break - if index >= 256: - msg = "cannot allocate more than 256 colors" - raise ValueError(msg) from e - return index - - def getcolor( - self, - color: tuple[int, ...], - image: Image.Image | None = None, - ) -> int: - """Given an rgb tuple, allocate palette entry. - - .. warning:: This method is experimental. - """ - if self.rawmode: - msg = "palette contains raw palette data" - raise ValueError(msg) - if isinstance(color, tuple): - if self.mode == "RGB": - if len(color) == 4: - if color[3] != 255: - msg = "cannot add non-opaque RGBA color to RGB palette" - raise ValueError(msg) - color = color[:3] - elif self.mode == "RGBA": - if len(color) == 3: - color += (255,) - try: - return self.colors[color] - except KeyError as e: - # allocate new color slot - index = self._new_color_index(image, e) - assert isinstance(self._palette, bytearray) - self.colors[color] = index - if index * 3 < len(self.palette): - self._palette = ( - self._palette[: index * 3] - + bytes(color) - + self._palette[index * 3 + 3 :] - ) - else: - self._palette += bytes(color) - self.dirty = 1 - return index - else: - msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable] - raise ValueError(msg) - - def save(self, fp: str | IO[str]) -> None: - """Save palette to text file. - - .. warning:: This method is experimental. - """ - if self.rawmode: - msg = "palette contains raw palette data" - raise ValueError(msg) - if isinstance(fp, str): - fp = open(fp, "w") - fp.write("# Palette\n") - fp.write(f"# Mode: {self.mode}\n") - for i in range(256): - fp.write(f"{i}") - for j in range(i * len(self.mode), (i + 1) * len(self.mode)): - try: - fp.write(f" {self.palette[j]}") - except IndexError: - fp.write(" 0") - fp.write("\n") - fp.close() - - -# -------------------------------------------------------------------- -# Internal - - -def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette: - palette = ImagePalette() - palette.rawmode = rawmode - palette.palette = data - palette.dirty = 1 - return palette - - -# -------------------------------------------------------------------- -# Factories - - -def make_linear_lut(black: int, white: float) -> list[int]: - if black == 0: - return [int(white * i // 255) for i in range(256)] - - msg = "unavailable when black is non-zero" - raise NotImplementedError(msg) # FIXME - - -def make_gamma_lut(exp: float) -> list[int]: - return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)] - - -def negative(mode: str = "RGB") -> ImagePalette: - palette = list(range(256 * len(mode))) - palette.reverse() - return ImagePalette(mode, [i // len(mode) for i in palette]) - - -def random(mode: str = "RGB") -> ImagePalette: - from random import randint - - palette = [randint(0, 255) for _ in range(256 * len(mode))] - return ImagePalette(mode, palette) - - -def sepia(white: str = "#fff0c0") -> ImagePalette: - bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] - return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) - - -def wedge(mode: str = "RGB") -> ImagePalette: - palette = list(range(256 * len(mode))) - return ImagePalette(mode, [i // len(mode) for i in palette]) - - -def load(filename: str) -> tuple[bytes, str]: - # FIXME: supports GIMP gradients only - - with open(filename, "rb") as fp: - paletteHandlers: list[ - type[ - GimpPaletteFile.GimpPaletteFile - | GimpGradientFile.GimpGradientFile - | PaletteFile.PaletteFile - ] - ] = [ - GimpPaletteFile.GimpPaletteFile, - GimpGradientFile.GimpGradientFile, - PaletteFile.PaletteFile, - ] - for paletteHandler in paletteHandlers: - try: - fp.seek(0) - lut = paletteHandler(fp).getpalette() - if lut: - break - except (SyntaxError, ValueError): - pass - else: - msg = "cannot load palette" - raise OSError(msg) - - return lut # data, rawmode diff --git a/python_modules/PIL/ImagePath.py b/python_modules/PIL/ImagePath.py deleted file mode 100644 index 77e8a609a..000000000 --- a/python_modules/PIL/ImagePath.py +++ /dev/null @@ -1,20 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# path interface -# -# History: -# 1996-11-04 fl Created -# 2002-04-14 fl Added documentation stub class -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image - -Path = Image.core.path diff --git a/python_modules/PIL/ImageQt.py b/python_modules/PIL/ImageQt.py deleted file mode 100644 index df7a57b65..000000000 --- a/python_modules/PIL/ImageQt.py +++ /dev/null @@ -1,220 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a simple Qt image interface. -# -# history: -# 2006-06-03 fl: created -# 2006-06-04 fl: inherit from QImage instead of wrapping it -# 2006-06-05 fl: removed toimage helper; move string support to ImageQt -# 2013-11-13 fl: add support for Qt5 (aurelien.ballier@cyclonit.com) -# -# Copyright (c) 2006 by Secret Labs AB -# Copyright (c) 2006 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import sys -from io import BytesIO -from typing import Any, Callable, Union - -from . import Image -from ._util import is_path - -TYPE_CHECKING = False -if TYPE_CHECKING: - import PyQt6 - import PySide6 - - from . import ImageFile - - QBuffer: type - QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray] - QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice] - QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] - QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] - -qt_version: str | None -qt_versions = [ - ["6", "PyQt6"], - ["side6", "PySide6"], -] - -# If a version has already been imported, attempt it first -qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) -for version, qt_module in qt_versions: - try: - qRgba: Callable[[int, int, int, int], int] - if qt_module == "PyQt6": - from PyQt6.QtCore import QBuffer, QIODevice - from PyQt6.QtGui import QImage, QPixmap, qRgba - elif qt_module == "PySide6": - from PySide6.QtCore import QBuffer, QIODevice - from PySide6.QtGui import QImage, QPixmap, qRgba - except (ImportError, RuntimeError): - continue - qt_is_installed = True - qt_version = version - break -else: - qt_is_installed = False - qt_version = None - - -def rgb(r: int, g: int, b: int, a: int = 255) -> int: - """(Internal) Turns an RGB color into a Qt compatible color integer.""" - # use qRgb to pack the colors, and then turn the resulting long - # into a negative integer with the same bitpattern. - return qRgba(r, g, b, a) & 0xFFFFFFFF - - -def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile: - """ - :param im: QImage or PIL ImageQt object - """ - buffer = QBuffer() - qt_openmode: object - if qt_version == "6": - try: - qt_openmode = getattr(QIODevice, "OpenModeFlag") - except AttributeError: - qt_openmode = getattr(QIODevice, "OpenMode") - else: - qt_openmode = QIODevice - buffer.open(getattr(qt_openmode, "ReadWrite")) - # preserve alpha channel with png - # otherwise ppm is more friendly with Image.open - if im.hasAlphaChannel(): - im.save(buffer, "png") - else: - im.save(buffer, "ppm") - - b = BytesIO() - b.write(buffer.data()) - buffer.close() - b.seek(0) - - return Image.open(b) - - -def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile: - return fromqimage(im) - - -def align8to32(bytes: bytes, width: int, mode: str) -> bytes: - """ - converts each scanline of data from 8 bit to 32 bit aligned - """ - - bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode] - - # calculate bytes per line and the extra padding if needed - bits_per_line = bits_per_pixel * width - full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8) - bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0) - - extra_padding = -bytes_per_line % 4 - - # already 32 bit aligned by luck - if not extra_padding: - return bytes - - new_data = [ - bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding - for i in range(len(bytes) // bytes_per_line) - ] - - return b"".join(new_data) - - -def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]: - data = None - colortable = None - exclusive_fp = False - - # handle filename, if given instead of image name - if hasattr(im, "toUtf8"): - # FIXME - is this really the best way to do this? - im = str(im.toUtf8(), "utf-8") - if is_path(im): - im = Image.open(im) - exclusive_fp = True - assert isinstance(im, Image.Image) - - qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage - if im.mode == "1": - format = getattr(qt_format, "Format_Mono") - elif im.mode == "L": - format = getattr(qt_format, "Format_Indexed8") - colortable = [rgb(i, i, i) for i in range(256)] - elif im.mode == "P": - format = getattr(qt_format, "Format_Indexed8") - palette = im.getpalette() - assert palette is not None - colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)] - elif im.mode == "RGB": - # Populate the 4th channel with 255 - im = im.convert("RGBA") - - data = im.tobytes("raw", "BGRA") - format = getattr(qt_format, "Format_RGB32") - elif im.mode == "RGBA": - data = im.tobytes("raw", "BGRA") - format = getattr(qt_format, "Format_ARGB32") - elif im.mode == "I;16": - im = im.point(lambda i: i * 256) - - format = getattr(qt_format, "Format_Grayscale16") - else: - if exclusive_fp: - im.close() - msg = f"unsupported image mode {repr(im.mode)}" - raise ValueError(msg) - - size = im.size - __data = data or align8to32(im.tobytes(), size[0], im.mode) - if exclusive_fp: - im.close() - return {"data": __data, "size": size, "format": format, "colortable": colortable} - - -if qt_is_installed: - - class ImageQt(QImage): # type: ignore[misc] - def __init__(self, im: Image.Image | str | QByteArray) -> None: - """ - An PIL image wrapper for Qt. This is a subclass of PyQt's QImage - class. - - :param im: A PIL Image object, or a file name (given either as - Python string or a PyQt string object). - """ - im_data = _toqclass_helper(im) - # must keep a reference, or Qt will crash! - # All QImage constructors that take data operate on an existing - # buffer, so this buffer has to hang on for the life of the image. - # Fixes https://github.com/python-pillow/Pillow/issues/1370 - self.__data = im_data["data"] - super().__init__( - self.__data, - im_data["size"][0], - im_data["size"][1], - im_data["format"], - ) - if im_data["colortable"]: - self.setColorTable(im_data["colortable"]) - - -def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: - return ImageQt(im) - - -def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: - qimage = toqimage(im) - pixmap = getattr(QPixmap, "fromImage")(qimage) - if qt_version == "6": - pixmap.detach() - return pixmap diff --git a/python_modules/PIL/ImageSequence.py b/python_modules/PIL/ImageSequence.py deleted file mode 100644 index a6fc340d5..000000000 --- a/python_modules/PIL/ImageSequence.py +++ /dev/null @@ -1,86 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# sequence support classes -# -# history: -# 1997-02-20 fl Created -# -# Copyright (c) 1997 by Secret Labs AB. -# Copyright (c) 1997 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -## -from __future__ import annotations - -from typing import Callable - -from . import Image - - -class Iterator: - """ - This class implements an iterator object that can be used to loop - over an image sequence. - - You can use the ``[]`` operator to access elements by index. This operator - will raise an :py:exc:`IndexError` if you try to access a nonexistent - frame. - - :param im: An image object. - """ - - def __init__(self, im: Image.Image) -> None: - if not hasattr(im, "seek"): - msg = "im must have seek method" - raise AttributeError(msg) - self.im = im - self.position = getattr(self.im, "_min_frame", 0) - - def __getitem__(self, ix: int) -> Image.Image: - try: - self.im.seek(ix) - return self.im - except EOFError as e: - msg = "end of sequence" - raise IndexError(msg) from e - - def __iter__(self) -> Iterator: - return self - - def __next__(self) -> Image.Image: - try: - self.im.seek(self.position) - self.position += 1 - return self.im - except EOFError as e: - msg = "end of sequence" - raise StopIteration(msg) from e - - -def all_frames( - im: Image.Image | list[Image.Image], - func: Callable[[Image.Image], Image.Image] | None = None, -) -> list[Image.Image]: - """ - Applies a given function to all frames in an image or a list of images. - The frames are returned as a list of separate images. - - :param im: An image, or a list of images. - :param func: The function to apply to all of the image frames. - :returns: A list of images. - """ - if not isinstance(im, list): - im = [im] - - ims = [] - for imSequence in im: - current = imSequence.tell() - - ims += [im_frame.copy() for im_frame in Iterator(imSequence)] - - imSequence.seek(current) - return [func(im) for im in ims] if func else ims diff --git a/python_modules/PIL/ImageShow.py b/python_modules/PIL/ImageShow.py deleted file mode 100644 index 7705608e3..000000000 --- a/python_modules/PIL/ImageShow.py +++ /dev/null @@ -1,362 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# im.show() drivers -# -# History: -# 2008-04-06 fl Created -# -# Copyright (c) Secret Labs AB 2008. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import abc -import os -import shutil -import subprocess -import sys -from shlex import quote -from typing import Any - -from . import Image - -_viewers = [] - - -def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None: - """ - The :py:func:`register` function is used to register additional viewers:: - - from PIL import ImageShow - ImageShow.register(MyViewer()) # MyViewer will be used as a last resort - ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised - ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised - - :param viewer: The viewer to be registered. - :param order: - Zero or a negative integer to prepend this viewer to the list, - a positive integer to append it. - """ - if isinstance(viewer, type) and issubclass(viewer, Viewer): - viewer = viewer() - if order > 0: - _viewers.append(viewer) - else: - _viewers.insert(0, viewer) - - -def show(image: Image.Image, title: str | None = None, **options: Any) -> bool: - r""" - Display a given image. - - :param image: An image object. - :param title: Optional title. Not all viewers can display the title. - :param \**options: Additional viewer options. - :returns: ``True`` if a suitable viewer was found, ``False`` otherwise. - """ - for viewer in _viewers: - if viewer.show(image, title=title, **options): - return True - return False - - -class Viewer: - """Base class for viewers.""" - - # main api - - def show(self, image: Image.Image, **options: Any) -> int: - """ - The main function for displaying an image. - Converts the given image to the target format and displays it. - """ - - if not ( - image.mode in ("1", "RGBA") - or (self.format == "PNG" and image.mode in ("I;16", "LA")) - ): - base = Image.getmodebase(image.mode) - if image.mode != base: - image = image.convert(base) - - return self.show_image(image, **options) - - # hook methods - - format: str | None = None - """The format to convert the image into.""" - options: dict[str, Any] = {} - """Additional options used to convert the image.""" - - def get_format(self, image: Image.Image) -> str | None: - """Return format name, or ``None`` to save as PGM/PPM.""" - return self.format - - def get_command(self, file: str, **options: Any) -> str: - """ - Returns the command used to display the file. - Not implemented in the base class. - """ - msg = "unavailable in base viewer" - raise NotImplementedError(msg) - - def save_image(self, image: Image.Image) -> str: - """Save to temporary file and return filename.""" - return image._dump(format=self.get_format(image), **self.options) - - def show_image(self, image: Image.Image, **options: Any) -> int: - """Display the given image.""" - return self.show_file(self.save_image(image), **options) - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - os.system(self.get_command(path, **options)) # nosec - return 1 - - -# -------------------------------------------------------------------- - - -class WindowsViewer(Viewer): - """The default viewer on Windows is the default system application for PNG files.""" - - format = "PNG" - options = {"compress_level": 1, "save_all": True} - - def get_command(self, file: str, **options: Any) -> str: - return ( - f'start "Pillow" /WAIT "{file}" ' - "&& ping -n 4 127.0.0.1 >NUL " - f'&& del /f "{file}"' - ) - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - subprocess.Popen( - self.get_command(path, **options), - shell=True, - creationflags=getattr(subprocess, "CREATE_NO_WINDOW"), - ) # nosec - return 1 - - -if sys.platform == "win32": - register(WindowsViewer) - - -class MacViewer(Viewer): - """The default viewer on macOS using ``Preview.app``.""" - - format = "PNG" - options = {"compress_level": 1, "save_all": True} - - def get_command(self, file: str, **options: Any) -> str: - # on darwin open returns immediately resulting in the temp - # file removal while app is opening - command = "open -a Preview.app" - command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" - return command - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - subprocess.call(["open", "-a", "Preview.app", path]) - - pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") - executable = (not pyinstaller and sys.executable) or shutil.which("python3") - if executable: - subprocess.Popen( - [ - executable, - "-c", - "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", - path, - ] - ) - return 1 - - -if sys.platform == "darwin": - register(MacViewer) - - -class UnixViewer(abc.ABC, Viewer): - format = "PNG" - options = {"compress_level": 1, "save_all": True} - - @abc.abstractmethod - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - pass - - def get_command(self, file: str, **options: Any) -> str: - command = self.get_command_ex(file, **options)[0] - return f"{command} {quote(file)}" - - -class XDGViewer(UnixViewer): - """ - The freedesktop.org ``xdg-open`` command. - """ - - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - command = executable = "xdg-open" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - subprocess.Popen(["xdg-open", path]) - return 1 - - -class DisplayViewer(UnixViewer): - """ - The ImageMagick ``display`` command. - This viewer supports the ``title`` parameter. - """ - - def get_command_ex( - self, file: str, title: str | None = None, **options: Any - ) -> tuple[str, str]: - command = executable = "display" - if title: - command += f" -title {quote(title)}" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - args = ["display"] - title = options.get("title") - if title: - args += ["-title", title] - args.append(path) - - subprocess.Popen(args) - return 1 - - -class GmDisplayViewer(UnixViewer): - """The GraphicsMagick ``gm display`` command.""" - - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - executable = "gm" - command = "gm display" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - subprocess.Popen(["gm", "display", path]) - return 1 - - -class EogViewer(UnixViewer): - """The GNOME Image Viewer ``eog`` command.""" - - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - executable = "eog" - command = "eog -n" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - subprocess.Popen(["eog", "-n", path]) - return 1 - - -class XVViewer(UnixViewer): - """ - The X Viewer ``xv`` command. - This viewer supports the ``title`` parameter. - """ - - def get_command_ex( - self, file: str, title: str | None = None, **options: Any - ) -> tuple[str, str]: - # note: xv is pretty outdated. most modern systems have - # imagemagick's display command instead. - command = executable = "xv" - if title: - command += f" -name {quote(title)}" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - args = ["xv"] - title = options.get("title") - if title: - args += ["-name", title] - args.append(path) - - subprocess.Popen(args) - return 1 - - -if sys.platform not in ("win32", "darwin"): # unixoids - if shutil.which("xdg-open"): - register(XDGViewer) - if shutil.which("display"): - register(DisplayViewer) - if shutil.which("gm"): - register(GmDisplayViewer) - if shutil.which("eog"): - register(EogViewer) - if shutil.which("xv"): - register(XVViewer) - - -class IPythonViewer(Viewer): - """The viewer for IPython frontends.""" - - def show_image(self, image: Image.Image, **options: Any) -> int: - ipython_display(image) - return 1 - - -try: - from IPython.display import display as ipython_display -except ImportError: - pass -else: - register(IPythonViewer) - - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python3 ImageShow.py imagefile [title]") - sys.exit() - - with Image.open(sys.argv[1]) as im: - print(show(im, *sys.argv[2:])) diff --git a/python_modules/PIL/ImageStat.py b/python_modules/PIL/ImageStat.py deleted file mode 100644 index 8bc504526..000000000 --- a/python_modules/PIL/ImageStat.py +++ /dev/null @@ -1,160 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# global image statistics -# -# History: -# 1996-04-05 fl Created -# 1997-05-21 fl Added mask; added rms, var, stddev attributes -# 1997-08-05 fl Added median -# 1998-07-05 hk Fixed integer overflow error -# -# Notes: -# This class shows how to implement delayed evaluation of attributes. -# To get a certain value, simply access the corresponding attribute. -# The __getattr__ dispatcher takes care of the rest. -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996-97. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import math -from functools import cached_property - -from . import Image - - -class Stat: - def __init__( - self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None - ) -> None: - """ - Calculate statistics for the given image. If a mask is included, - only the regions covered by that mask are included in the - statistics. You can also pass in a previously calculated histogram. - - :param image: A PIL image, or a precalculated histogram. - - .. note:: - - For a PIL image, calculations rely on the - :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are - grouped into 256 bins, even if the image has more than 8 bits per - channel. So ``I`` and ``F`` mode images have a maximum ``mean``, - ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum - of more than 255. - - :param mask: An optional mask. - """ - if isinstance(image_or_list, Image.Image): - self.h = image_or_list.histogram(mask) - elif isinstance(image_or_list, list): - self.h = image_or_list - else: - msg = "first argument must be image or list" # type: ignore[unreachable] - raise TypeError(msg) - self.bands = list(range(len(self.h) // 256)) - - @cached_property - def extrema(self) -> list[tuple[int, int]]: - """ - Min/max values for each band in the image. - - .. note:: - This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and - simply returns the low and high bins used. This is correct for - images with 8 bits per channel, but fails for other modes such as - ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to - return per-band extrema for the image. This is more correct and - efficient because, for non-8-bit modes, the histogram method uses - :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used. - """ - - def minmax(histogram: list[int]) -> tuple[int, int]: - res_min, res_max = 255, 0 - for i in range(256): - if histogram[i]: - res_min = i - break - for i in range(255, -1, -1): - if histogram[i]: - res_max = i - break - return res_min, res_max - - return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)] - - @cached_property - def count(self) -> list[int]: - """Total number of pixels for each band in the image.""" - return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)] - - @cached_property - def sum(self) -> list[float]: - """Sum of all pixels for each band in the image.""" - - v = [] - for i in range(0, len(self.h), 256): - layer_sum = 0.0 - for j in range(256): - layer_sum += j * self.h[i + j] - v.append(layer_sum) - return v - - @cached_property - def sum2(self) -> list[float]: - """Squared sum of all pixels for each band in the image.""" - - v = [] - for i in range(0, len(self.h), 256): - sum2 = 0.0 - for j in range(256): - sum2 += (j**2) * float(self.h[i + j]) - v.append(sum2) - return v - - @cached_property - def mean(self) -> list[float]: - """Average (arithmetic mean) pixel level for each band in the image.""" - return [self.sum[i] / self.count[i] for i in self.bands] - - @cached_property - def median(self) -> list[int]: - """Median pixel level for each band in the image.""" - - v = [] - for i in self.bands: - s = 0 - half = self.count[i] // 2 - b = i * 256 - for j in range(256): - s = s + self.h[b + j] - if s > half: - break - v.append(j) - return v - - @cached_property - def rms(self) -> list[float]: - """RMS (root-mean-square) for each band in the image.""" - return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands] - - @cached_property - def var(self) -> list[float]: - """Variance for each band in the image.""" - return [ - (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] - for i in self.bands - ] - - @cached_property - def stddev(self) -> list[float]: - """Standard deviation for each band in the image.""" - return [math.sqrt(self.var[i]) for i in self.bands] - - -Global = Stat # compatibility diff --git a/python_modules/PIL/ImageTk.py b/python_modules/PIL/ImageTk.py deleted file mode 100644 index 3a4cb81e9..000000000 --- a/python_modules/PIL/ImageTk.py +++ /dev/null @@ -1,266 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a Tk display interface -# -# History: -# 96-04-08 fl Created -# 96-09-06 fl Added getimage method -# 96-11-01 fl Rewritten, removed image attribute and crop method -# 97-05-09 fl Use PyImagingPaste method instead of image type -# 97-05-12 fl Minor tweaks to match the IFUNC95 interface -# 97-05-17 fl Support the "pilbitmap" booster patch -# 97-06-05 fl Added file= and data= argument to image constructors -# 98-03-09 fl Added width and height methods to Image classes -# 98-07-02 fl Use default mode for "P" images without palette attribute -# 98-07-02 fl Explicitly destroy Tkinter image objects -# 99-07-24 fl Support multiple Tk interpreters (from Greg Couch) -# 99-07-26 fl Automatically hook into Tkinter (if possible) -# 99-08-15 fl Hook uses _imagingtk instead of _imaging -# -# Copyright (c) 1997-1999 by Secret Labs AB -# Copyright (c) 1996-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import tkinter -from io import BytesIO -from typing import Any - -from . import Image, ImageFile - -TYPE_CHECKING = False -if TYPE_CHECKING: - from ._typing import CapsuleType - -# -------------------------------------------------------------------- -# Check for Tkinter interface hooks - - -def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: - source = None - if "file" in kw: - source = kw.pop("file") - elif "data" in kw: - source = BytesIO(kw.pop("data")) - if not source: - return None - return Image.open(source) - - -def _pyimagingtkcall( - command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType -) -> None: - tk = photo.tk - try: - tk.call(command, photo, repr(ptr)) - except tkinter.TclError: - # activate Tkinter hook - # may raise an error if it cannot attach to Tkinter - from . import _imagingtk - - _imagingtk.tkinit(tk.interpaddr()) - tk.call(command, photo, repr(ptr)) - - -# -------------------------------------------------------------------- -# PhotoImage - - -class PhotoImage: - """ - A Tkinter-compatible photo image. This can be used - everywhere Tkinter expects an image object. If the image is an RGBA - image, pixels having alpha 0 are treated as transparent. - - The constructor takes either a PIL image, or a mode and a size. - Alternatively, you can use the ``file`` or ``data`` options to initialize - the photo image object. - - :param image: Either a PIL image, or a mode string. If a mode string is - used, a size must also be given. - :param size: If the first argument is a mode string, this defines the size - of the image. - :keyword file: A filename to load the image from (using - ``Image.open(file)``). - :keyword data: An 8-bit string containing image data (as loaded from an - image file). - """ - - def __init__( - self, - image: Image.Image | str | None = None, - size: tuple[int, int] | None = None, - **kw: Any, - ) -> None: - # Tk compatibility: file or data - if image is None: - image = _get_image_from_kw(kw) - - if image is None: - msg = "Image is required" - raise ValueError(msg) - elif isinstance(image, str): - mode = image - image = None - - if size is None: - msg = "If first argument is mode, size is required" - raise ValueError(msg) - else: - # got an image instead of a mode - mode = image.mode - if mode == "P": - # palette mapped data - image.apply_transparency() - image.load() - mode = image.palette.mode if image.palette else "RGB" - size = image.size - kw["width"], kw["height"] = size - - if mode not in ["1", "L", "RGB", "RGBA"]: - mode = Image.getmodebase(mode) - - self.__mode = mode - self.__size = size - self.__photo = tkinter.PhotoImage(**kw) - self.tk = self.__photo.tk - if image: - self.paste(image) - - def __del__(self) -> None: - try: - name = self.__photo.name - except AttributeError: - return - self.__photo.name = None - try: - self.__photo.tk.call("image", "delete", name) - except Exception: - pass # ignore internal errors - - def __str__(self) -> str: - """ - Get the Tkinter photo image identifier. This method is automatically - called by Tkinter whenever a PhotoImage object is passed to a Tkinter - method. - - :return: A Tkinter photo image identifier (a string). - """ - return str(self.__photo) - - def width(self) -> int: - """ - Get the width of the image. - - :return: The width, in pixels. - """ - return self.__size[0] - - def height(self) -> int: - """ - Get the height of the image. - - :return: The height, in pixels. - """ - return self.__size[1] - - def paste(self, im: Image.Image) -> None: - """ - Paste a PIL image into the photo image. Note that this can - be very slow if the photo image is displayed. - - :param im: A PIL image. The size must match the target region. If the - mode does not match, the image is converted to the mode of - the bitmap image. - """ - # convert to blittable - ptr = im.getim() - image = im.im - if not image.isblock() or im.mode != self.__mode: - block = Image.core.new_block(self.__mode, im.size) - image.convert2(block, image) # convert directly between buffers - ptr = block.ptr - - _pyimagingtkcall("PyImagingPhoto", self.__photo, ptr) - - -# -------------------------------------------------------------------- -# BitmapImage - - -class BitmapImage: - """ - A Tkinter-compatible bitmap image. This can be used everywhere Tkinter - expects an image object. - - The given image must have mode "1". Pixels having value 0 are treated as - transparent. Options, if any, are passed on to Tkinter. The most commonly - used option is ``foreground``, which is used to specify the color for the - non-transparent parts. See the Tkinter documentation for information on - how to specify colours. - - :param image: A PIL image. - """ - - def __init__(self, image: Image.Image | None = None, **kw: Any) -> None: - # Tk compatibility: file or data - if image is None: - image = _get_image_from_kw(kw) - - if image is None: - msg = "Image is required" - raise ValueError(msg) - self.__mode = image.mode - self.__size = image.size - - self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw) - - def __del__(self) -> None: - try: - name = self.__photo.name - except AttributeError: - return - self.__photo.name = None - try: - self.__photo.tk.call("image", "delete", name) - except Exception: - pass # ignore internal errors - - def width(self) -> int: - """ - Get the width of the image. - - :return: The width, in pixels. - """ - return self.__size[0] - - def height(self) -> int: - """ - Get the height of the image. - - :return: The height, in pixels. - """ - return self.__size[1] - - def __str__(self) -> str: - """ - Get the Tkinter bitmap image identifier. This method is automatically - called by Tkinter whenever a BitmapImage object is passed to a Tkinter - method. - - :return: A Tkinter bitmap image identifier (a string). - """ - return str(self.__photo) - - -def getimage(photo: PhotoImage) -> Image.Image: - """Copies the contents of a PhotoImage to a PIL image memory.""" - im = Image.new("RGBA", (photo.width(), photo.height())) - - _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim()) - - return im diff --git a/python_modules/PIL/ImageTransform.py b/python_modules/PIL/ImageTransform.py deleted file mode 100644 index fb144ff38..000000000 --- a/python_modules/PIL/ImageTransform.py +++ /dev/null @@ -1,136 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# transform wrappers -# -# History: -# 2002-04-08 fl Created -# -# Copyright (c) 2002 by Secret Labs AB -# Copyright (c) 2002 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from collections.abc import Sequence -from typing import Any - -from . import Image - - -class Transform(Image.ImageTransformHandler): - """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`.""" - - method: Image.Transform - - def __init__(self, data: Sequence[Any]) -> None: - self.data = data - - def getdata(self) -> tuple[Image.Transform, Sequence[int]]: - return self.method, self.data - - def transform( - self, - size: tuple[int, int], - image: Image.Image, - **options: Any, - ) -> Image.Image: - """Perform the transform. Called from :py:meth:`.Image.transform`.""" - # can be overridden - method, data = self.getdata() - return image.transform(size, method, data, **options) - - -class AffineTransform(Transform): - """ - Define an affine image transform. - - This function takes a 6-tuple (a, b, c, d, e, f) which contain the first - two rows from the inverse of an affine transform matrix. For each pixel - (x, y) in the output image, the new value is taken from a position (a x + - b y + c, d x + e y + f) in the input image, rounded to nearest pixel. - - This function can be used to scale, translate, rotate, and shear the - original image. - - See :py:meth:`.Image.transform` - - :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows - from the inverse of an affine transform matrix. - """ - - method = Image.Transform.AFFINE - - -class PerspectiveTransform(Transform): - """ - Define a perspective image transform. - - This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel - (x, y) in the output image, the new value is taken from a position - ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in - the input image, rounded to nearest pixel. - - This function can be used to scale, translate, rotate, and shear the - original image. - - See :py:meth:`.Image.transform` - - :param matrix: An 8-tuple (a, b, c, d, e, f, g, h). - """ - - method = Image.Transform.PERSPECTIVE - - -class ExtentTransform(Transform): - """ - Define a transform to extract a subregion from an image. - - Maps a rectangle (defined by two corners) from the image to a rectangle of - the given size. The resulting image will contain data sampled from between - the corners, such that (x0, y0) in the input image will end up at (0,0) in - the output image, and (x1, y1) at size. - - This method can be used to crop, stretch, shrink, or mirror an arbitrary - rectangle in the current image. It is slightly slower than crop, but about - as fast as a corresponding resize operation. - - See :py:meth:`.Image.transform` - - :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the - input image's coordinate system. See :ref:`coordinate-system`. - """ - - method = Image.Transform.EXTENT - - -class QuadTransform(Transform): - """ - Define a quad image transform. - - Maps a quadrilateral (a region defined by four corners) from the image to a - rectangle of the given size. - - See :py:meth:`.Image.transform` - - :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the - upper left, lower left, lower right, and upper right corner of the - source quadrilateral. - """ - - method = Image.Transform.QUAD - - -class MeshTransform(Transform): - """ - Define a mesh image transform. A mesh transform consists of one or more - individual quad transforms. - - See :py:meth:`.Image.transform` - - :param data: A list of (bbox, quad) tuples. - """ - - method = Image.Transform.MESH diff --git a/python_modules/PIL/ImageWin.py b/python_modules/PIL/ImageWin.py deleted file mode 100644 index 98c28f29f..000000000 --- a/python_modules/PIL/ImageWin.py +++ /dev/null @@ -1,247 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a Windows DIB display interface -# -# History: -# 1996-05-20 fl Created -# 1996-09-20 fl Fixed subregion exposure -# 1997-09-21 fl Added draw primitive (for tzPrint) -# 2003-05-21 fl Added experimental Window/ImageWindow classes -# 2003-09-05 fl Added fromstring/tostring methods -# -# Copyright (c) Secret Labs AB 1997-2003. -# Copyright (c) Fredrik Lundh 1996-2003. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image - - -class HDC: - """ - Wraps an HDC integer. The resulting object can be passed to the - :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` - methods. - """ - - def __init__(self, dc: int) -> None: - self.dc = dc - - def __int__(self) -> int: - return self.dc - - -class HWND: - """ - Wraps an HWND integer. The resulting object can be passed to the - :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` - methods, instead of a DC. - """ - - def __init__(self, wnd: int) -> None: - self.wnd = wnd - - def __int__(self) -> int: - return self.wnd - - -class Dib: - """ - A Windows bitmap with the given mode and size. The mode can be one of "1", - "L", "P", or "RGB". - - If the display requires a palette, this constructor creates a suitable - palette and associates it with the image. For an "L" image, 128 graylevels - are allocated. For an "RGB" image, a 6x6x6 colour cube is used, together - with 20 graylevels. - - To make sure that palettes work properly under Windows, you must call the - ``palette`` method upon certain events from Windows. - - :param image: Either a PIL image, or a mode string. If a mode string is - used, a size must also be given. The mode can be one of "1", - "L", "P", or "RGB". - :param size: If the first argument is a mode string, this - defines the size of the image. - """ - - def __init__( - self, image: Image.Image | str, size: tuple[int, int] | None = None - ) -> None: - if isinstance(image, str): - mode = image - image = "" - if size is None: - msg = "If first argument is mode, size is required" - raise ValueError(msg) - else: - mode = image.mode - size = image.size - if mode not in ["1", "L", "P", "RGB"]: - mode = Image.getmodebase(mode) - self.image = Image.core.display(mode, size) - self.mode = mode - self.size = size - if image: - assert not isinstance(image, str) - self.paste(image) - - def expose(self, handle: int | HDC | HWND) -> None: - """ - Copy the bitmap contents to a device context. - - :param handle: Device context (HDC), cast to a Python integer, or an - HDC or HWND instance. In PythonWin, you can use - ``CDC.GetHandleAttrib()`` to get a suitable handle. - """ - handle_int = int(handle) - if isinstance(handle, HWND): - dc = self.image.getdc(handle_int) - try: - self.image.expose(dc) - finally: - self.image.releasedc(handle_int, dc) - else: - self.image.expose(handle_int) - - def draw( - self, - handle: int | HDC | HWND, - dst: tuple[int, int, int, int], - src: tuple[int, int, int, int] | None = None, - ) -> None: - """ - Same as expose, but allows you to specify where to draw the image, and - what part of it to draw. - - The destination and source areas are given as 4-tuple rectangles. If - the source is omitted, the entire image is copied. If the source and - the destination have different sizes, the image is resized as - necessary. - """ - if src is None: - src = (0, 0) + self.size - handle_int = int(handle) - if isinstance(handle, HWND): - dc = self.image.getdc(handle_int) - try: - self.image.draw(dc, dst, src) - finally: - self.image.releasedc(handle_int, dc) - else: - self.image.draw(handle_int, dst, src) - - def query_palette(self, handle: int | HDC | HWND) -> int: - """ - Installs the palette associated with the image in the given device - context. - - This method should be called upon **QUERYNEWPALETTE** and - **PALETTECHANGED** events from Windows. If this method returns a - non-zero value, one or more display palette entries were changed, and - the image should be redrawn. - - :param handle: Device context (HDC), cast to a Python integer, or an - HDC or HWND instance. - :return: The number of entries that were changed (if one or more entries, - this indicates that the image should be redrawn). - """ - handle_int = int(handle) - if isinstance(handle, HWND): - handle = self.image.getdc(handle_int) - try: - result = self.image.query_palette(handle) - finally: - self.image.releasedc(handle, handle) - else: - result = self.image.query_palette(handle_int) - return result - - def paste( - self, im: Image.Image, box: tuple[int, int, int, int] | None = None - ) -> None: - """ - Paste a PIL image into the bitmap image. - - :param im: A PIL image. The size must match the target region. - If the mode does not match, the image is converted to the - mode of the bitmap image. - :param box: A 4-tuple defining the left, upper, right, and - lower pixel coordinate. See :ref:`coordinate-system`. If - None is given instead of a tuple, all of the image is - assumed. - """ - im.load() - if self.mode != im.mode: - im = im.convert(self.mode) - if box: - self.image.paste(im.im, box) - else: - self.image.paste(im.im) - - def frombytes(self, buffer: bytes) -> None: - """ - Load display memory contents from byte data. - - :param buffer: A buffer containing display data (usually - data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`) - """ - self.image.frombytes(buffer) - - def tobytes(self) -> bytes: - """ - Copy display memory contents to bytes object. - - :return: A bytes object containing display data. - """ - return self.image.tobytes() - - -class Window: - """Create a Window with the given title size.""" - - def __init__( - self, title: str = "PIL", width: int | None = None, height: int | None = None - ) -> None: - self.hwnd = Image.core.createwindow( - title, self.__dispatcher, width or 0, height or 0 - ) - - def __dispatcher(self, action: str, *args: int) -> None: - getattr(self, f"ui_handle_{action}")(*args) - - def ui_handle_clear(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: - pass - - def ui_handle_damage(self, x0: int, y0: int, x1: int, y1: int) -> None: - pass - - def ui_handle_destroy(self) -> None: - pass - - def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: - pass - - def ui_handle_resize(self, width: int, height: int) -> None: - pass - - def mainloop(self) -> None: - Image.core.eventloop() - - -class ImageWindow(Window): - """Create an image window which displays the given image.""" - - def __init__(self, image: Image.Image | Dib, title: str = "PIL") -> None: - if not isinstance(image, Dib): - image = Dib(image) - self.image = image - width, height = image.size - super().__init__(title, width=width, height=height) - - def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: - self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/python_modules/PIL/ImtImagePlugin.py b/python_modules/PIL/ImtImagePlugin.py deleted file mode 100644 index c4eccee34..000000000 --- a/python_modules/PIL/ImtImagePlugin.py +++ /dev/null @@ -1,103 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# IM Tools support for PIL -# -# history: -# 1996-05-27 fl Created (read 8-bit images only) -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.2) -# -# Copyright (c) Secret Labs AB 1997-2001. -# Copyright (c) Fredrik Lundh 1996-2001. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re - -from . import Image, ImageFile - -# -# -------------------------------------------------------------------- - -field = re.compile(rb"([a-z]*) ([^ \r\n]*)") - - -## -# Image plugin for IM Tools images. - - -class ImtImageFile(ImageFile.ImageFile): - format = "IMT" - format_description = "IM Tools" - - def _open(self) -> None: - # Quick rejection: if there's not a LF among the first - # 100 bytes, this is (probably) not a text header. - - assert self.fp is not None - - buffer = self.fp.read(100) - if b"\n" not in buffer: - msg = "not an IM file" - raise SyntaxError(msg) - - xsize = ysize = 0 - - while True: - if buffer: - s = buffer[:1] - buffer = buffer[1:] - else: - s = self.fp.read(1) - if not s: - break - - if s == b"\x0c": - # image data begins - self.tile = [ - ImageFile._Tile( - "raw", - (0, 0) + self.size, - self.fp.tell() - len(buffer), - self.mode, - ) - ] - - break - - else: - # read key/value pair - if b"\n" not in buffer: - buffer += self.fp.read(100) - lines = buffer.split(b"\n") - s += lines.pop(0) - buffer = b"\n".join(lines) - if len(s) == 1 or len(s) > 100: - break - if s[0] == ord(b"*"): - continue # comment - - m = field.match(s) - if not m: - break - k, v = m.group(1, 2) - if k == b"width": - xsize = int(v) - self._size = xsize, ysize - elif k == b"height": - ysize = int(v) - self._size = xsize, ysize - elif k == b"pixel" and v == b"n8": - self._mode = "L" - - -# -# -------------------------------------------------------------------- - -Image.register_open(ImtImageFile.format, ImtImageFile) - -# -# no extension registered (".im" is simply too common) diff --git a/python_modules/PIL/IptcImagePlugin.py b/python_modules/PIL/IptcImagePlugin.py deleted file mode 100644 index fc024d668..000000000 --- a/python_modules/PIL/IptcImagePlugin.py +++ /dev/null @@ -1,250 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# IPTC/NAA file handling -# -# history: -# 1995-10-01 fl Created -# 1998-03-09 fl Cleaned up and added to PIL -# 2002-06-18 fl Added getiptcinfo helper -# -# Copyright (c) Secret Labs AB 1997-2002. -# Copyright (c) Fredrik Lundh 1995. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from collections.abc import Sequence -from io import BytesIO -from typing import cast - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._deprecate import deprecate - -COMPRESSION = {1: "raw", 5: "jpeg"} - - -def __getattr__(name: str) -> bytes: - if name == "PAD": - deprecate("IptcImagePlugin.PAD", 12) - return b"\0\0\0\0" - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - -# -# Helpers - - -def _i(c: bytes) -> int: - return i32((b"\0\0\0\0" + c)[-4:]) - - -def _i8(c: int | bytes) -> int: - return c if isinstance(c, int) else c[0] - - -def i(c: bytes) -> int: - """.. deprecated:: 10.2.0""" - deprecate("IptcImagePlugin.i", 12) - return _i(c) - - -def dump(c: Sequence[int | bytes]) -> None: - """.. deprecated:: 10.2.0""" - deprecate("IptcImagePlugin.dump", 12) - for i in c: - print(f"{_i8(i):02x}", end=" ") - print() - - -## -# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields -# from TIFF and JPEG files, use the getiptcinfo function. - - -class IptcImageFile(ImageFile.ImageFile): - format = "IPTC" - format_description = "IPTC/NAA" - - def getint(self, key: tuple[int, int]) -> int: - return _i(self.info[key]) - - def field(self) -> tuple[tuple[int, int] | None, int]: - # - # get a IPTC field header - s = self.fp.read(5) - if not s.strip(b"\x00"): - return None, 0 - - tag = s[1], s[2] - - # syntax - if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]: - msg = "invalid IPTC/NAA file" - raise SyntaxError(msg) - - # field size - size = s[3] - if size > 132: - msg = "illegal field length in IPTC/NAA file" - raise OSError(msg) - elif size == 128: - size = 0 - elif size > 128: - size = _i(self.fp.read(size - 128)) - else: - size = i16(s, 3) - - return tag, size - - def _open(self) -> None: - # load descriptive fields - while True: - offset = self.fp.tell() - tag, size = self.field() - if not tag or tag == (8, 10): - break - if size: - tagdata = self.fp.read(size) - else: - tagdata = None - if tag in self.info: - if isinstance(self.info[tag], list): - self.info[tag].append(tagdata) - else: - self.info[tag] = [self.info[tag], tagdata] - else: - self.info[tag] = tagdata - - # mode - layers = self.info[(3, 60)][0] - component = self.info[(3, 60)][1] - if (3, 65) in self.info: - id = self.info[(3, 65)][0] - 1 - else: - id = 0 - if layers == 1 and not component: - self._mode = "L" - elif layers == 3 and component: - self._mode = "RGB"[id] - elif layers == 4 and component: - self._mode = "CMYK"[id] - - # size - self._size = self.getint((3, 20)), self.getint((3, 30)) - - # compression - try: - compression = COMPRESSION[self.getint((3, 120))] - except KeyError as e: - msg = "Unknown IPTC image compression" - raise OSError(msg) from e - - # tile - if tag == (8, 10): - self.tile = [ - ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression) - ] - - def load(self) -> Image.core.PixelAccess | None: - if len(self.tile) != 1 or self.tile[0][0] != "iptc": - return ImageFile.ImageFile.load(self) - - offset, compression = self.tile[0][2:] - - self.fp.seek(offset) - - # Copy image data to temporary file - o = BytesIO() - if compression == "raw": - # To simplify access to the extracted file, - # prepend a PPM header - o.write(b"P5\n%d %d\n255\n" % self.size) - while True: - type, size = self.field() - if type != (8, 10): - break - while size > 0: - s = self.fp.read(min(size, 8192)) - if not s: - break - o.write(s) - size -= len(s) - - with Image.open(o) as _im: - _im.load() - self.im = _im.im - self.tile = [] - return Image.Image.load(self) - - -Image.register_open(IptcImageFile.format, IptcImageFile) - -Image.register_extension(IptcImageFile.format, ".iim") - - -def getiptcinfo( - im: ImageFile.ImageFile, -) -> dict[tuple[int, int], bytes | list[bytes]] | None: - """ - Get IPTC information from TIFF, JPEG, or IPTC file. - - :param im: An image containing IPTC data. - :returns: A dictionary containing IPTC information, or None if - no IPTC information block was found. - """ - from . import JpegImagePlugin, TiffImagePlugin - - data = None - - info: dict[tuple[int, int], bytes | list[bytes]] = {} - if isinstance(im, IptcImageFile): - # return info dictionary right away - for k, v in im.info.items(): - if isinstance(k, tuple): - info[k] = v - return info - - elif isinstance(im, JpegImagePlugin.JpegImageFile): - # extract the IPTC/NAA resource - photoshop = im.info.get("photoshop") - if photoshop: - data = photoshop.get(0x0404) - - elif isinstance(im, TiffImagePlugin.TiffImageFile): - # get raw data from the IPTC/NAA tag (PhotoShop tags the data - # as 4-byte integers, so we cannot use the get method...) - try: - data = im.tag_v2._tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] - except KeyError: - pass - - if data is None: - return None # no properties - - # create an IptcImagePlugin object without initializing it - class FakeImage: - pass - - fake_im = FakeImage() - fake_im.__class__ = IptcImageFile # type: ignore[assignment] - iptc_im = cast(IptcImageFile, fake_im) - - # parse the IPTC information chunk - iptc_im.info = {} - iptc_im.fp = BytesIO(data) - - try: - iptc_im._open() - except (IndexError, KeyError): - pass # expected failure - - for k, v in iptc_im.info.items(): - if isinstance(k, tuple): - info[k] = v - return info diff --git a/python_modules/PIL/Jpeg2KImagePlugin.py b/python_modules/PIL/Jpeg2KImagePlugin.py deleted file mode 100644 index e0f4ecae5..000000000 --- a/python_modules/PIL/Jpeg2KImagePlugin.py +++ /dev/null @@ -1,442 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# JPEG2000 file handling -# -# History: -# 2014-03-12 ajh Created -# 2021-06-30 rogermb Extract dpi information from the 'resc' header box -# -# Copyright (c) 2014 Coriolis Systems Limited -# Copyright (c) 2014 Alastair Houghton -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import struct -from collections.abc import Callable -from typing import IO, cast - -from . import Image, ImageFile, ImagePalette, _binary - - -class BoxReader: - """ - A small helper class to read fields stored in JPEG2000 header boxes - and to easily step into and read sub-boxes. - """ - - def __init__(self, fp: IO[bytes], length: int = -1) -> None: - self.fp = fp - self.has_length = length >= 0 - self.length = length - self.remaining_in_box = -1 - - def _can_read(self, num_bytes: int) -> bool: - if self.has_length and self.fp.tell() + num_bytes > self.length: - # Outside box: ensure we don't read past the known file length - return False - if self.remaining_in_box >= 0: - # Inside box contents: ensure read does not go past box boundaries - return num_bytes <= self.remaining_in_box - else: - return True # No length known, just read - - def _read_bytes(self, num_bytes: int) -> bytes: - if not self._can_read(num_bytes): - msg = "Not enough data in header" - raise SyntaxError(msg) - - data = self.fp.read(num_bytes) - if len(data) < num_bytes: - msg = f"Expected to read {num_bytes} bytes but only got {len(data)}." - raise OSError(msg) - - if self.remaining_in_box > 0: - self.remaining_in_box -= num_bytes - return data - - def read_fields(self, field_format: str) -> tuple[int | bytes, ...]: - size = struct.calcsize(field_format) - data = self._read_bytes(size) - return struct.unpack(field_format, data) - - def read_boxes(self) -> BoxReader: - size = self.remaining_in_box - data = self._read_bytes(size) - return BoxReader(io.BytesIO(data), size) - - def has_next_box(self) -> bool: - if self.has_length: - return self.fp.tell() + self.remaining_in_box < self.length - else: - return True - - def next_box_type(self) -> bytes: - # Skip the rest of the box if it has not been read - if self.remaining_in_box > 0: - self.fp.seek(self.remaining_in_box, os.SEEK_CUR) - self.remaining_in_box = -1 - - # Read the length and type of the next box - lbox, tbox = cast(tuple[int, bytes], self.read_fields(">I4s")) - if lbox == 1: - lbox = cast(int, self.read_fields(">Q")[0]) - hlen = 16 - else: - hlen = 8 - - if lbox < hlen or not self._can_read(lbox - hlen): - msg = "Invalid header length" - raise SyntaxError(msg) - - self.remaining_in_box = lbox - hlen - return tbox - - -def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]: - """Parse the JPEG 2000 codestream to extract the size and component - count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" - - hdr = fp.read(2) - lsiz = _binary.i16be(hdr) - siz = hdr + fp.read(lsiz - 2) - lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( - ">HHIIIIIIIIH", siz - ) - - size = (xsiz - xosiz, ysiz - yosiz) - if csiz == 1: - ssiz = struct.unpack_from(">B", siz, 38) - if (ssiz[0] & 0x7F) + 1 > 8: - mode = "I;16" - else: - mode = "L" - elif csiz == 2: - mode = "LA" - elif csiz == 3: - mode = "RGB" - elif csiz == 4: - mode = "RGBA" - else: - msg = "unable to determine J2K image mode" - raise SyntaxError(msg) - - return size, mode - - -def _res_to_dpi(num: int, denom: int, exp: int) -> float | None: - """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, - calculated as (num / denom) * 10^exp and stored in dots per meter, - to floating-point dots per inch.""" - if denom == 0: - return None - return (254 * num * (10**exp)) / (10000 * denom) - - -def _parse_jp2_header( - fp: IO[bytes], -) -> tuple[ - tuple[int, int], - str, - str | None, - tuple[float, float] | None, - ImagePalette.ImagePalette | None, -]: - """Parse the JP2 header box to extract size, component count, - color space information, and optionally DPI information, - returning a (size, mode, mimetype, dpi) tuple.""" - - # Find the JP2 header box - reader = BoxReader(fp) - header = None - mimetype = None - while reader.has_next_box(): - tbox = reader.next_box_type() - - if tbox == b"jp2h": - header = reader.read_boxes() - break - elif tbox == b"ftyp": - if reader.read_fields(">4s")[0] == b"jpx ": - mimetype = "image/jpx" - assert header is not None - - size = None - mode = None - bpc = None - nc = None - dpi = None # 2-tuple of DPI info, or None - palette = None - - while header.has_next_box(): - tbox = header.next_box_type() - - if tbox == b"ihdr": - height, width, nc, bpc = header.read_fields(">IIHB") - assert isinstance(height, int) - assert isinstance(width, int) - assert isinstance(bpc, int) - size = (width, height) - if nc == 1 and (bpc & 0x7F) > 8: - mode = "I;16" - elif nc == 1: - mode = "L" - elif nc == 2: - mode = "LA" - elif nc == 3: - mode = "RGB" - elif nc == 4: - mode = "RGBA" - elif tbox == b"colr" and nc == 4: - meth, _, _, enumcs = header.read_fields(">BBBI") - if meth == 1 and enumcs == 12: - mode = "CMYK" - elif tbox == b"pclr" and mode in ("L", "LA"): - ne, npc = header.read_fields(">HB") - assert isinstance(ne, int) - assert isinstance(npc, int) - max_bitdepth = 0 - for bitdepth in header.read_fields(">" + ("B" * npc)): - assert isinstance(bitdepth, int) - if bitdepth > max_bitdepth: - max_bitdepth = bitdepth - if max_bitdepth <= 8: - palette = ImagePalette.ImagePalette("RGBA" if npc == 4 else "RGB") - for i in range(ne): - color: list[int] = [] - for value in header.read_fields(">" + ("B" * npc)): - assert isinstance(value, int) - color.append(value) - palette.getcolor(tuple(color)) - mode = "P" if mode == "L" else "PA" - elif tbox == b"res ": - res = header.read_boxes() - while res.has_next_box(): - tres = res.next_box_type() - if tres == b"resc": - vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") - assert isinstance(vrcn, int) - assert isinstance(vrcd, int) - assert isinstance(hrcn, int) - assert isinstance(hrcd, int) - assert isinstance(vrce, int) - assert isinstance(hrce, int) - hres = _res_to_dpi(hrcn, hrcd, hrce) - vres = _res_to_dpi(vrcn, vrcd, vrce) - if hres is not None and vres is not None: - dpi = (hres, vres) - break - - if size is None or mode is None: - msg = "Malformed JP2 header" - raise SyntaxError(msg) - - return size, mode, mimetype, dpi, palette - - -## -# Image plugin for JPEG2000 images. - - -class Jpeg2KImageFile(ImageFile.ImageFile): - format = "JPEG2000" - format_description = "JPEG 2000 (ISO 15444)" - - def _open(self) -> None: - sig = self.fp.read(4) - if sig == b"\xff\x4f\xff\x51": - self.codec = "j2k" - self._size, self._mode = _parse_codestream(self.fp) - self._parse_comment() - else: - sig = sig + self.fp.read(8) - - if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a": - self.codec = "jp2" - header = _parse_jp2_header(self.fp) - self._size, self._mode, self.custom_mimetype, dpi, self.palette = header - if dpi is not None: - self.info["dpi"] = dpi - if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): - hdr = self.fp.read(2) - length = _binary.i16be(hdr) - self.fp.seek(length - 2, os.SEEK_CUR) - self._parse_comment() - else: - msg = "not a JPEG 2000 file" - raise SyntaxError(msg) - - self._reduce = 0 - self.layers = 0 - - fd = -1 - length = -1 - - try: - fd = self.fp.fileno() - length = os.fstat(fd).st_size - except Exception: - fd = -1 - try: - pos = self.fp.tell() - self.fp.seek(0, io.SEEK_END) - length = self.fp.tell() - self.fp.seek(pos) - except Exception: - length = -1 - - self.tile = [ - ImageFile._Tile( - "jpeg2k", - (0, 0) + self.size, - 0, - (self.codec, self._reduce, self.layers, fd, length), - ) - ] - - def _parse_comment(self) -> None: - while True: - marker = self.fp.read(2) - if not marker: - break - typ = marker[1] - if typ in (0x90, 0xD9): - # Start of tile or end of codestream - break - hdr = self.fp.read(2) - length = _binary.i16be(hdr) - if typ == 0x64: - # Comment - self.info["comment"] = self.fp.read(length - 2)[2:] - break - else: - self.fp.seek(length - 2, os.SEEK_CUR) - - @property # type: ignore[override] - def reduce( - self, - ) -> ( - Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image] - | int - ): - # https://github.com/python-pillow/Pillow/issues/4343 found that the - # new Image 'reduce' method was shadowed by this plugin's 'reduce' - # property. This attempts to allow for both scenarios - return self._reduce or super().reduce - - @reduce.setter - def reduce(self, value: int) -> None: - self._reduce = value - - def load(self) -> Image.core.PixelAccess | None: - if self.tile and self._reduce: - power = 1 << self._reduce - adjust = power >> 1 - self._size = ( - int((self.size[0] + adjust) / power), - int((self.size[1] + adjust) / power), - ) - - # Update the reduce and layers settings - t = self.tile[0] - assert isinstance(t[3], tuple) - t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4]) - self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)] - - return ImageFile.ImageFile.load(self) - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith( - (b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a") - ) - - -# ------------------------------------------------------------ -# Save support - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # Get the keyword arguments - info = im.encoderinfo - - if isinstance(filename, str): - filename = filename.encode() - if filename.endswith(b".j2k") or info.get("no_jp2", False): - kind = "j2k" - else: - kind = "jp2" - - offset = info.get("offset", None) - tile_offset = info.get("tile_offset", None) - tile_size = info.get("tile_size", None) - quality_mode = info.get("quality_mode", "rates") - quality_layers = info.get("quality_layers", None) - if quality_layers is not None and not ( - isinstance(quality_layers, (list, tuple)) - and all( - isinstance(quality_layer, (int, float)) for quality_layer in quality_layers - ) - ): - msg = "quality_layers must be a sequence of numbers" - raise ValueError(msg) - - num_resolutions = info.get("num_resolutions", 0) - cblk_size = info.get("codeblock_size", None) - precinct_size = info.get("precinct_size", None) - irreversible = info.get("irreversible", False) - progression = info.get("progression", "LRCP") - cinema_mode = info.get("cinema_mode", "no") - mct = info.get("mct", 0) - signed = info.get("signed", False) - comment = info.get("comment") - if isinstance(comment, str): - comment = comment.encode() - plt = info.get("plt", False) - - fd = -1 - if hasattr(fp, "fileno"): - try: - fd = fp.fileno() - except Exception: - fd = -1 - - im.encoderconfig = ( - offset, - tile_offset, - tile_size, - quality_mode, - quality_layers, - num_resolutions, - cblk_size, - precinct_size, - irreversible, - progression, - cinema_mode, - mct, - signed, - fd, - comment, - plt, - ) - - ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)]) - - -# ------------------------------------------------------------ -# Registry stuff - - -Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept) -Image.register_save(Jpeg2KImageFile.format, _save) - -Image.register_extensions( - Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"] -) - -Image.register_mime(Jpeg2KImageFile.format, "image/jp2") diff --git a/python_modules/PIL/JpegImagePlugin.py b/python_modules/PIL/JpegImagePlugin.py deleted file mode 100644 index defe9f773..000000000 --- a/python_modules/PIL/JpegImagePlugin.py +++ /dev/null @@ -1,902 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# JPEG (JFIF) file handling -# -# See "Digital Compression and Coding of Continuous-Tone Still Images, -# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1) -# -# History: -# 1995-09-09 fl Created -# 1995-09-13 fl Added full parser -# 1996-03-25 fl Added hack to use the IJG command line utilities -# 1996-05-05 fl Workaround Photoshop 2.5 CMYK polarity bug -# 1996-05-28 fl Added draft support, JFIF version (0.1) -# 1996-12-30 fl Added encoder options, added progression property (0.2) -# 1997-08-27 fl Save mode 1 images as BW (0.3) -# 1998-07-12 fl Added YCbCr to draft and save methods (0.4) -# 1998-10-19 fl Don't hang on files using 16-bit DQT's (0.4.1) -# 2001-04-16 fl Extract DPI settings from JFIF files (0.4.2) -# 2002-07-01 fl Skip pad bytes before markers; identify Exif files (0.4.3) -# 2003-04-25 fl Added experimental EXIF decoder (0.5) -# 2003-06-06 fl Added experimental EXIF GPSinfo decoder -# 2003-09-13 fl Extract COM markers -# 2009-09-06 fl Added icc_profile support (from Florian Hoech) -# 2009-03-06 fl Changed CMYK handling; always use Adobe polarity (0.6) -# 2009-03-08 fl Added subsampling support (from Justin Huff). -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-1996 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import array -import io -import math -import os -import struct -import subprocess -import sys -import tempfile -import warnings -from typing import IO, Any - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import o8 -from ._binary import o16be as o16 -from ._deprecate import deprecate -from .JpegPresets import presets - -TYPE_CHECKING = False -if TYPE_CHECKING: - from .MpoImagePlugin import MpoImageFile - -# -# Parser - - -def Skip(self: JpegImageFile, marker: int) -> None: - n = i16(self.fp.read(2)) - 2 - ImageFile._safe_read(self.fp, n) - - -def APP(self: JpegImageFile, marker: int) -> None: - # - # Application marker. Store these in the APP dictionary. - # Also look for well-known application markers. - - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - - app = f"APP{marker & 15}" - - self.app[app] = s # compatibility - self.applist.append((app, s)) - - if marker == 0xFFE0 and s.startswith(b"JFIF"): - # extract JFIF information - self.info["jfif"] = version = i16(s, 5) # version - self.info["jfif_version"] = divmod(version, 256) - # extract JFIF properties - try: - jfif_unit = s[7] - jfif_density = i16(s, 8), i16(s, 10) - except Exception: - pass - else: - if jfif_unit == 1: - self.info["dpi"] = jfif_density - elif jfif_unit == 2: # cm - # 1 dpcm = 2.54 dpi - self.info["dpi"] = tuple(d * 2.54 for d in jfif_density) - self.info["jfif_unit"] = jfif_unit - self.info["jfif_density"] = jfif_density - elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"): - # extract EXIF information - if "exif" in self.info: - self.info["exif"] += s[6:] - else: - self.info["exif"] = s - self._exif_offset = self.fp.tell() - n + 6 - elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"): - self.info["xmp"] = s.split(b"\x00", 1)[1] - elif marker == 0xFFE2 and s.startswith(b"FPXR\0"): - # extract FlashPix information (incomplete) - self.info["flashpix"] = s # FIXME: value will change - elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"): - # Since an ICC profile can be larger than the maximum size of - # a JPEG marker (64K), we need provisions to split it into - # multiple markers. The format defined by the ICC specifies - # one or more APP2 markers containing the following data: - # Identifying string ASCII "ICC_PROFILE\0" (12 bytes) - # Marker sequence number 1, 2, etc (1 byte) - # Number of markers Total of APP2's used (1 byte) - # Profile data (remainder of APP2 data) - # Decoders should use the marker sequence numbers to - # reassemble the profile, rather than assuming that the APP2 - # markers appear in the correct sequence. - self.icclist.append(s) - elif marker == 0xFFED and s.startswith(b"Photoshop 3.0\x00"): - # parse the image resource block - offset = 14 - photoshop = self.info.setdefault("photoshop", {}) - while s[offset : offset + 4] == b"8BIM": - try: - offset += 4 - # resource code - code = i16(s, offset) - offset += 2 - # resource name (usually empty) - name_len = s[offset] - # name = s[offset+1:offset+1+name_len] - offset += 1 + name_len - offset += offset & 1 # align - # resource data block - size = i32(s, offset) - offset += 4 - data = s[offset : offset + size] - if code == 0x03ED: # ResolutionInfo - photoshop[code] = { - "XResolution": i32(data, 0) / 65536, - "DisplayedUnitsX": i16(data, 4), - "YResolution": i32(data, 8) / 65536, - "DisplayedUnitsY": i16(data, 12), - } - else: - photoshop[code] = data - offset += size - offset += offset & 1 # align - except struct.error: - break # insufficient data - - elif marker == 0xFFEE and s.startswith(b"Adobe"): - self.info["adobe"] = i16(s, 5) - # extract Adobe custom properties - try: - adobe_transform = s[11] - except IndexError: - pass - else: - self.info["adobe_transform"] = adobe_transform - elif marker == 0xFFE2 and s.startswith(b"MPF\0"): - # extract MPO information - self.info["mp"] = s[4:] - # offset is current location minus buffer size - # plus constant header size - self.info["mpoffset"] = self.fp.tell() - n + 4 - - -def COM(self: JpegImageFile, marker: int) -> None: - # - # Comment marker. Store these in the APP dictionary. - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - - self.info["comment"] = s - self.app["COM"] = s # compatibility - self.applist.append(("COM", s)) - - -def SOF(self: JpegImageFile, marker: int) -> None: - # - # Start of frame marker. Defines the size and mode of the - # image. JPEG is colour blind, so we use some simple - # heuristics to map the number of layers to an appropriate - # mode. Note that this could be made a bit brighter, by - # looking for JFIF and Adobe APP markers. - - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - self._size = i16(s, 3), i16(s, 1) - - self.bits = s[0] - if self.bits != 8: - msg = f"cannot handle {self.bits}-bit layers" - raise SyntaxError(msg) - - self.layers = s[5] - if self.layers == 1: - self._mode = "L" - elif self.layers == 3: - self._mode = "RGB" - elif self.layers == 4: - self._mode = "CMYK" - else: - msg = f"cannot handle {self.layers}-layer images" - raise SyntaxError(msg) - - if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: - self.info["progressive"] = self.info["progression"] = 1 - - if self.icclist: - # fixup icc profile - self.icclist.sort() # sort by sequence number - if self.icclist[0][13] == len(self.icclist): - profile = [p[14:] for p in self.icclist] - icc_profile = b"".join(profile) - else: - icc_profile = None # wrong number of fragments - self.info["icc_profile"] = icc_profile - self.icclist = [] - - for i in range(6, len(s), 3): - t = s[i : i + 3] - # 4-tuples: id, vsamp, hsamp, qtable - self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2])) - - -def DQT(self: JpegImageFile, marker: int) -> None: - # - # Define quantization table. Note that there might be more - # than one table in each marker. - - # FIXME: The quantization tables can be used to estimate the - # compression quality. - - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - while len(s): - v = s[0] - precision = 1 if (v // 16 == 0) else 2 # in bytes - qt_length = 1 + precision * 64 - if len(s) < qt_length: - msg = "bad quantization table marker" - raise SyntaxError(msg) - data = array.array("B" if precision == 1 else "H", s[1:qt_length]) - if sys.byteorder == "little" and precision > 1: - data.byteswap() # the values are always big-endian - self.quantization[v & 15] = [data[i] for i in zigzag_index] - s = s[qt_length:] - - -# -# JPEG marker table - -MARKER = { - 0xFFC0: ("SOF0", "Baseline DCT", SOF), - 0xFFC1: ("SOF1", "Extended Sequential DCT", SOF), - 0xFFC2: ("SOF2", "Progressive DCT", SOF), - 0xFFC3: ("SOF3", "Spatial lossless", SOF), - 0xFFC4: ("DHT", "Define Huffman table", Skip), - 0xFFC5: ("SOF5", "Differential sequential DCT", SOF), - 0xFFC6: ("SOF6", "Differential progressive DCT", SOF), - 0xFFC7: ("SOF7", "Differential spatial", SOF), - 0xFFC8: ("JPG", "Extension", None), - 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF), - 0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF), - 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF), - 0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip), - 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF), - 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF), - 0xFFCF: ("SOF15", "Differential spatial (AC)", SOF), - 0xFFD0: ("RST0", "Restart 0", None), - 0xFFD1: ("RST1", "Restart 1", None), - 0xFFD2: ("RST2", "Restart 2", None), - 0xFFD3: ("RST3", "Restart 3", None), - 0xFFD4: ("RST4", "Restart 4", None), - 0xFFD5: ("RST5", "Restart 5", None), - 0xFFD6: ("RST6", "Restart 6", None), - 0xFFD7: ("RST7", "Restart 7", None), - 0xFFD8: ("SOI", "Start of image", None), - 0xFFD9: ("EOI", "End of image", None), - 0xFFDA: ("SOS", "Start of scan", Skip), - 0xFFDB: ("DQT", "Define quantization table", DQT), - 0xFFDC: ("DNL", "Define number of lines", Skip), - 0xFFDD: ("DRI", "Define restart interval", Skip), - 0xFFDE: ("DHP", "Define hierarchical progression", SOF), - 0xFFDF: ("EXP", "Expand reference component", Skip), - 0xFFE0: ("APP0", "Application segment 0", APP), - 0xFFE1: ("APP1", "Application segment 1", APP), - 0xFFE2: ("APP2", "Application segment 2", APP), - 0xFFE3: ("APP3", "Application segment 3", APP), - 0xFFE4: ("APP4", "Application segment 4", APP), - 0xFFE5: ("APP5", "Application segment 5", APP), - 0xFFE6: ("APP6", "Application segment 6", APP), - 0xFFE7: ("APP7", "Application segment 7", APP), - 0xFFE8: ("APP8", "Application segment 8", APP), - 0xFFE9: ("APP9", "Application segment 9", APP), - 0xFFEA: ("APP10", "Application segment 10", APP), - 0xFFEB: ("APP11", "Application segment 11", APP), - 0xFFEC: ("APP12", "Application segment 12", APP), - 0xFFED: ("APP13", "Application segment 13", APP), - 0xFFEE: ("APP14", "Application segment 14", APP), - 0xFFEF: ("APP15", "Application segment 15", APP), - 0xFFF0: ("JPG0", "Extension 0", None), - 0xFFF1: ("JPG1", "Extension 1", None), - 0xFFF2: ("JPG2", "Extension 2", None), - 0xFFF3: ("JPG3", "Extension 3", None), - 0xFFF4: ("JPG4", "Extension 4", None), - 0xFFF5: ("JPG5", "Extension 5", None), - 0xFFF6: ("JPG6", "Extension 6", None), - 0xFFF7: ("JPG7", "Extension 7", None), - 0xFFF8: ("JPG8", "Extension 8", None), - 0xFFF9: ("JPG9", "Extension 9", None), - 0xFFFA: ("JPG10", "Extension 10", None), - 0xFFFB: ("JPG11", "Extension 11", None), - 0xFFFC: ("JPG12", "Extension 12", None), - 0xFFFD: ("JPG13", "Extension 13", None), - 0xFFFE: ("COM", "Comment", COM), -} - - -def _accept(prefix: bytes) -> bool: - # Magic number was taken from https://en.wikipedia.org/wiki/JPEG - return prefix.startswith(b"\xff\xd8\xff") - - -## -# Image plugin for JPEG and JFIF images. - - -class JpegImageFile(ImageFile.ImageFile): - format = "JPEG" - format_description = "JPEG (ISO 10918)" - - def _open(self) -> None: - s = self.fp.read(3) - - if not _accept(s): - msg = "not a JPEG file" - raise SyntaxError(msg) - s = b"\xff" - - # Create attributes - self.bits = self.layers = 0 - self._exif_offset = 0 - - # JPEG specifics (internal) - self.layer: list[tuple[int, int, int, int]] = [] - self._huffman_dc: dict[Any, Any] = {} - self._huffman_ac: dict[Any, Any] = {} - self.quantization: dict[int, list[int]] = {} - self.app: dict[str, bytes] = {} # compatibility - self.applist: list[tuple[str, bytes]] = [] - self.icclist: list[bytes] = [] - - while True: - i = s[0] - if i == 0xFF: - s = s + self.fp.read(1) - i = i16(s) - else: - # Skip non-0xFF junk - s = self.fp.read(1) - continue - - if i in MARKER: - name, description, handler = MARKER[i] - if handler is not None: - handler(self, i) - if i == 0xFFDA: # start of scan - rawmode = self.mode - if self.mode == "CMYK": - rawmode = "CMYK;I" # assume adobe conventions - self.tile = [ - ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, "")) - ] - # self.__offset = self.fp.tell() - break - s = self.fp.read(1) - elif i in {0, 0xFFFF}: - # padded marker or junk; move on - s = b"\xff" - elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) - s = self.fp.read(1) - else: - msg = "no marker found" - raise SyntaxError(msg) - - self._read_dpi_from_exif() - - def __getattr__(self, name: str) -> Any: - if name in ("huffman_ac", "huffman_dc"): - deprecate(name, 12) - return getattr(self, "_" + name) - raise AttributeError(name) - - def __getstate__(self) -> list[Any]: - return super().__getstate__() + [self.layers, self.layer] - - def __setstate__(self, state: list[Any]) -> None: - self.layers, self.layer = state[6:] - super().__setstate__(state) - - def load_read(self, read_bytes: int) -> bytes: - """ - internal: read more image data - For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker - so libjpeg can finish decoding - """ - s = self.fp.read(read_bytes) - - if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"): - # Premature EOF. - # Pretend file is finished adding EOI marker - self._ended = True - return b"\xff\xd9" - - return s - - def draft( - self, mode: str | None, size: tuple[int, int] | None - ) -> tuple[str, tuple[int, int, float, float]] | None: - if len(self.tile) != 1: - return None - - # Protect from second call - if self.decoderconfig: - return None - - d, e, o, a = self.tile[0] - scale = 1 - original_size = self.size - - assert isinstance(a, tuple) - if a[0] == "RGB" and mode in ["L", "YCbCr"]: - self._mode = mode - a = mode, "" - - if size: - scale = min(self.size[0] // size[0], self.size[1] // size[1]) - for s in [8, 4, 2, 1]: - if scale >= s: - break - assert e is not None - e = ( - e[0], - e[1], - (e[2] - e[0] + s - 1) // s + e[0], - (e[3] - e[1] + s - 1) // s + e[1], - ) - self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) - scale = s - - self.tile = [ImageFile._Tile(d, e, o, a)] - self.decoderconfig = (scale, 0) - - box = (0, 0, original_size[0] / scale, original_size[1] / scale) - return self.mode, box - - def load_djpeg(self) -> None: - # ALTERNATIVE: handle JPEGs via the IJG command line utilities - - f, path = tempfile.mkstemp() - os.close(f) - if os.path.exists(self.filename): - subprocess.check_call(["djpeg", "-outfile", path, self.filename]) - else: - try: - os.unlink(path) - except OSError: - pass - - msg = "Invalid Filename" - raise ValueError(msg) - - try: - with Image.open(path) as _im: - _im.load() - self.im = _im.im - finally: - try: - os.unlink(path) - except OSError: - pass - - self._mode = self.im.mode - self._size = self.im.size - - self.tile = [] - - def _getexif(self) -> dict[int, Any] | None: - return _getexif(self) - - def _read_dpi_from_exif(self) -> None: - # If DPI isn't in JPEG header, fetch from EXIF - if "dpi" in self.info or "exif" not in self.info: - return - try: - exif = self.getexif() - resolution_unit = exif[0x0128] - x_resolution = exif[0x011A] - try: - dpi = float(x_resolution[0]) / x_resolution[1] - except TypeError: - dpi = x_resolution - if math.isnan(dpi): - msg = "DPI is not a number" - raise ValueError(msg) - if resolution_unit == 3: # cm - # 1 dpcm = 2.54 dpi - dpi *= 2.54 - self.info["dpi"] = dpi, dpi - except ( - struct.error, # truncated EXIF - KeyError, # dpi not included - SyntaxError, # invalid/unreadable EXIF - TypeError, # dpi is an invalid float - ValueError, # dpi is an invalid float - ZeroDivisionError, # invalid dpi rational value - ): - self.info["dpi"] = 72, 72 - - def _getmp(self) -> dict[int, Any] | None: - return _getmp(self) - - -def _getexif(self: JpegImageFile) -> dict[int, Any] | None: - if "exif" not in self.info: - return None - return self.getexif()._get_merged_dict() - - -def _getmp(self: JpegImageFile) -> dict[int, Any] | None: - # Extract MP information. This method was inspired by the "highly - # experimental" _getexif version that's been in use for years now, - # itself based on the ImageFileDirectory class in the TIFF plugin. - - # The MP record essentially consists of a TIFF file embedded in a JPEG - # application marker. - try: - data = self.info["mp"] - except KeyError: - return None - file_contents = io.BytesIO(data) - head = file_contents.read(8) - endianness = ">" if head.startswith(b"\x4d\x4d\x00\x2a") else "<" - # process dictionary - from . import TiffImagePlugin - - try: - info = TiffImagePlugin.ImageFileDirectory_v2(head) - file_contents.seek(info.next) - info.load(file_contents) - mp = dict(info) - except Exception as e: - msg = "malformed MP Index (unreadable directory)" - raise SyntaxError(msg) from e - # it's an error not to have a number of images - try: - quant = mp[0xB001] - except KeyError as e: - msg = "malformed MP Index (no number of images)" - raise SyntaxError(msg) from e - # get MP entries - mpentries = [] - try: - rawmpentries = mp[0xB002] - for entrynum in range(quant): - unpackedentry = struct.unpack_from( - f"{endianness}LLLHH", rawmpentries, entrynum * 16 - ) - labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2") - mpentry = dict(zip(labels, unpackedentry)) - mpentryattr = { - "DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)), - "DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)), - "RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)), - "Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27, - "ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24, - "MPType": mpentry["Attribute"] & 0x00FFFFFF, - } - if mpentryattr["ImageDataFormat"] == 0: - mpentryattr["ImageDataFormat"] = "JPEG" - else: - msg = "unsupported picture format in MPO" - raise SyntaxError(msg) - mptypemap = { - 0x000000: "Undefined", - 0x010001: "Large Thumbnail (VGA Equivalent)", - 0x010002: "Large Thumbnail (Full HD Equivalent)", - 0x020001: "Multi-Frame Image (Panorama)", - 0x020002: "Multi-Frame Image: (Disparity)", - 0x020003: "Multi-Frame Image: (Multi-Angle)", - 0x030000: "Baseline MP Primary Image", - } - mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown") - mpentry["Attribute"] = mpentryattr - mpentries.append(mpentry) - mp[0xB002] = mpentries - except KeyError as e: - msg = "malformed MP Index (bad MP Entry)" - raise SyntaxError(msg) from e - # Next we should try and parse the individual image unique ID list; - # we don't because I've never seen this actually used in a real MPO - # file and so can't test it. - return mp - - -# -------------------------------------------------------------------- -# stuff to save JPEG files - -RAWMODE = { - "1": "L", - "L": "L", - "RGB": "RGB", - "RGBX": "RGB", - "CMYK": "CMYK;I", # assume adobe conventions - "YCbCr": "YCbCr", -} - -# fmt: off -zigzag_index = ( - 0, 1, 5, 6, 14, 15, 27, 28, - 2, 4, 7, 13, 16, 26, 29, 42, - 3, 8, 12, 17, 25, 30, 41, 43, - 9, 11, 18, 24, 31, 40, 44, 53, - 10, 19, 23, 32, 39, 45, 52, 54, - 20, 22, 33, 38, 46, 51, 55, 60, - 21, 34, 37, 47, 50, 56, 59, 61, - 35, 36, 48, 49, 57, 58, 62, 63, -) - -samplings = { - (1, 1, 1, 1, 1, 1): 0, - (2, 1, 1, 1, 1, 1): 1, - (2, 2, 1, 1, 1, 1): 2, -} -# fmt: on - - -def get_sampling(im: Image.Image) -> int: - # There's no subsampling when images have only 1 layer - # (grayscale images) or when they are CMYK (4 layers), - # so set subsampling to the default value. - # - # NOTE: currently Pillow can't encode JPEG to YCCK format. - # If YCCK support is added in the future, subsampling code will have - # to be updated (here and in JpegEncode.c) to deal with 4 layers. - if not isinstance(im, JpegImageFile) or im.layers in (1, 4): - return -1 - sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] - return samplings.get(sampling, -1) - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.width == 0 or im.height == 0: - msg = "cannot write empty image as JPEG" - raise ValueError(msg) - - try: - rawmode = RAWMODE[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as JPEG" - raise OSError(msg) from e - - info = im.encoderinfo - - dpi = [round(x) for x in info.get("dpi", (0, 0))] - - quality = info.get("quality", -1) - subsampling = info.get("subsampling", -1) - qtables = info.get("qtables") - - if quality == "keep": - quality = -1 - subsampling = "keep" - qtables = "keep" - elif quality in presets: - preset = presets[quality] - quality = -1 - subsampling = preset.get("subsampling", -1) - qtables = preset.get("quantization") - elif not isinstance(quality, int): - msg = "Invalid quality setting" - raise ValueError(msg) - else: - if subsampling in presets: - subsampling = presets[subsampling].get("subsampling", -1) - if isinstance(qtables, str) and qtables in presets: - qtables = presets[qtables].get("quantization") - - if subsampling == "4:4:4": - subsampling = 0 - elif subsampling == "4:2:2": - subsampling = 1 - elif subsampling == "4:2:0": - subsampling = 2 - elif subsampling == "4:1:1": - # For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0. - # Set 4:2:0 if someone is still using that value. - subsampling = 2 - elif subsampling == "keep": - if im.format != "JPEG": - msg = "Cannot use 'keep' when original image is not a JPEG" - raise ValueError(msg) - subsampling = get_sampling(im) - - def validate_qtables( - qtables: ( - str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None - ), - ) -> list[list[int]] | None: - if qtables is None: - return qtables - if isinstance(qtables, str): - try: - lines = [ - int(num) - for line in qtables.splitlines() - for num in line.split("#", 1)[0].split() - ] - except ValueError as e: - msg = "Invalid quantization table" - raise ValueError(msg) from e - else: - qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] - if isinstance(qtables, (tuple, list, dict)): - if isinstance(qtables, dict): - qtables = [ - qtables[key] for key in range(len(qtables)) if key in qtables - ] - elif isinstance(qtables, tuple): - qtables = list(qtables) - if not (0 < len(qtables) < 5): - msg = "None or too many quantization tables" - raise ValueError(msg) - for idx, table in enumerate(qtables): - try: - if len(table) != 64: - msg = "Invalid quantization table" - raise TypeError(msg) - table_array = array.array("H", table) - except TypeError as e: - msg = "Invalid quantization table" - raise ValueError(msg) from e - else: - qtables[idx] = list(table_array) - return qtables - - if qtables == "keep": - if im.format != "JPEG": - msg = "Cannot use 'keep' when original image is not a JPEG" - raise ValueError(msg) - qtables = getattr(im, "quantization", None) - qtables = validate_qtables(qtables) - - extra = info.get("extra", b"") - - MAX_BYTES_IN_MARKER = 65533 - if xmp := info.get("xmp"): - overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" - max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len - if len(xmp) > max_data_bytes_in_marker: - msg = "XMP data is too long" - raise ValueError(msg) - size = o16(2 + overhead_len + len(xmp)) - extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp - - if icc_profile := info.get("icc_profile"): - overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) - max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len - markers = [] - while icc_profile: - markers.append(icc_profile[:max_data_bytes_in_marker]) - icc_profile = icc_profile[max_data_bytes_in_marker:] - i = 1 - for marker in markers: - size = o16(2 + overhead_len + len(marker)) - extra += ( - b"\xff\xe2" - + size - + b"ICC_PROFILE\0" - + o8(i) - + o8(len(markers)) - + marker - ) - i += 1 - - comment = info.get("comment", im.info.get("comment")) - - # "progressive" is the official name, but older documentation - # says "progression" - # FIXME: issue a warning if the wrong form is used (post-1.1.7) - progressive = info.get("progressive", False) or info.get("progression", False) - - optimize = info.get("optimize", False) - - exif = info.get("exif", b"") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - if len(exif) > MAX_BYTES_IN_MARKER: - msg = "EXIF data is too long" - raise ValueError(msg) - - # get keyword arguments - im.encoderconfig = ( - quality, - progressive, - info.get("smooth", 0), - optimize, - info.get("keep_rgb", False), - info.get("streamtype", 0), - dpi, - subsampling, - info.get("restart_marker_blocks", 0), - info.get("restart_marker_rows", 0), - qtables, - comment, - extra, - exif, - ) - - # if we optimize, libjpeg needs a buffer big enough to hold the whole image - # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is - # channels*size, this is a value that's been used in a django patch. - # https://github.com/matthewwithanm/django-imagekit/issues/50 - if optimize or progressive: - # CMYK can be bigger - if im.mode == "CMYK": - bufsize = 4 * im.size[0] * im.size[1] - # keep sets quality to -1, but the actual value may be high. - elif quality >= 95 or quality == -1: - bufsize = 2 * im.size[0] * im.size[1] - else: - bufsize = im.size[0] * im.size[1] - if exif: - bufsize += len(exif) + 5 - if extra: - bufsize += len(extra) + 1 - else: - # The EXIF info needs to be written as one block, + APP1, + one spare byte. - # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(len(exif) + 5, len(extra) + 1) - - ImageFile._save( - im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize - ) - - -def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # ALTERNATIVE: handle JPEGs via the IJG command line utilities. - tempfile = im._dump() - subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) - try: - os.unlink(tempfile) - except OSError: - pass - - -## -# Factory for making JPEG and MPO instances -def jpeg_factory( - fp: IO[bytes], filename: str | bytes | None = None -) -> JpegImageFile | MpoImageFile: - im = JpegImageFile(fp, filename) - try: - mpheader = im._getmp() - if mpheader is not None and mpheader[45057] > 1: - for segment, content in im.applist: - if segment == "APP1" and b' hdrgm:Version="' in content: - # Ultra HDR images are not yet supported - return im - # It's actually an MPO - from .MpoImagePlugin import MpoImageFile - - # Don't reload everything, just convert it. - im = MpoImageFile.adopt(im, mpheader) - except (TypeError, IndexError): - # It is really a JPEG - pass - except SyntaxError: - warnings.warn( - "Image appears to be a malformed MPO file, it will be " - "interpreted as a base JPEG file" - ) - return im - - -# --------------------------------------------------------------------- -# Registry stuff - -Image.register_open(JpegImageFile.format, jpeg_factory, _accept) -Image.register_save(JpegImageFile.format, _save) - -Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"]) - -Image.register_mime(JpegImageFile.format, "image/jpeg") diff --git a/python_modules/PIL/JpegPresets.py b/python_modules/PIL/JpegPresets.py deleted file mode 100644 index d0e64a35e..000000000 --- a/python_modules/PIL/JpegPresets.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -JPEG quality settings equivalent to the Photoshop settings. -Can be used when saving JPEG files. - -The following presets are available by default: -``web_low``, ``web_medium``, ``web_high``, ``web_very_high``, ``web_maximum``, -``low``, ``medium``, ``high``, ``maximum``. -More presets can be added to the :py:data:`presets` dict if needed. - -To apply the preset, specify:: - - quality="preset_name" - -To apply only the quantization table:: - - qtables="preset_name" - -To apply only the subsampling setting:: - - subsampling="preset_name" - -Example:: - - im.save("image_name.jpg", quality="web_high") - -Subsampling ------------ - -Subsampling is the practice of encoding images by implementing less resolution -for chroma information than for luma information. -(ref.: https://en.wikipedia.org/wiki/Chroma_subsampling) - -Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and -4:2:0. - -You can get the subsampling of a JPEG with the -:func:`.JpegImagePlugin.get_sampling` function. - -In JPEG compressed data a JPEG marker is used instead of an EXIF tag. -(ref.: https://exiv2.org/tags.html) - - -Quantization tables -------------------- - -They are values use by the DCT (Discrete cosine transform) to remove -*unnecessary* information from the image (the lossy part of the compression). -(ref.: https://en.wikipedia.org/wiki/Quantization_matrix#Quantization_matrices, -https://en.wikipedia.org/wiki/JPEG#Quantization) - -You can get the quantization tables of a JPEG with:: - - im.quantization - -This will return a dict with a number of lists. You can pass this dict -directly as the qtables argument when saving a JPEG. - -The quantization table format in presets is a list with sublists. These formats -are interchangeable. - -Libjpeg ref.: -https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html - -""" - -from __future__ import annotations - -# fmt: off -presets = { - 'web_low': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [20, 16, 25, 39, 50, 46, 62, 68, - 16, 18, 23, 38, 38, 53, 65, 68, - 25, 23, 31, 38, 53, 65, 68, 68, - 39, 38, 38, 53, 65, 68, 68, 68, - 50, 38, 53, 65, 68, 68, 68, 68, - 46, 53, 65, 68, 68, 68, 68, 68, - 62, 65, 68, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68], - [21, 25, 32, 38, 54, 68, 68, 68, - 25, 28, 24, 38, 54, 68, 68, 68, - 32, 24, 32, 43, 66, 68, 68, 68, - 38, 38, 43, 53, 68, 68, 68, 68, - 54, 54, 66, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68] - ]}, - 'web_medium': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [16, 11, 11, 16, 23, 27, 31, 30, - 11, 12, 12, 15, 20, 23, 23, 30, - 11, 12, 13, 16, 23, 26, 35, 47, - 16, 15, 16, 23, 26, 37, 47, 64, - 23, 20, 23, 26, 39, 51, 64, 64, - 27, 23, 26, 37, 51, 64, 64, 64, - 31, 23, 35, 47, 64, 64, 64, 64, - 30, 30, 47, 64, 64, 64, 64, 64], - [17, 15, 17, 21, 20, 26, 38, 48, - 15, 19, 18, 17, 20, 26, 35, 43, - 17, 18, 20, 22, 26, 30, 46, 53, - 21, 17, 22, 28, 30, 39, 53, 64, - 20, 20, 26, 30, 39, 48, 64, 64, - 26, 26, 30, 39, 48, 63, 64, 64, - 38, 35, 46, 53, 64, 64, 64, 64, - 48, 43, 53, 64, 64, 64, 64, 64] - ]}, - 'web_high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [6, 4, 4, 6, 9, 11, 12, 16, - 4, 5, 5, 6, 8, 10, 12, 12, - 4, 5, 5, 6, 10, 12, 14, 19, - 6, 6, 6, 11, 12, 15, 19, 28, - 9, 8, 10, 12, 16, 20, 27, 31, - 11, 10, 12, 15, 20, 27, 31, 31, - 12, 12, 14, 19, 27, 31, 31, 31, - 16, 12, 19, 28, 31, 31, 31, 31], - [7, 7, 13, 24, 26, 31, 31, 31, - 7, 12, 16, 21, 31, 31, 31, 31, - 13, 16, 17, 31, 31, 31, 31, 31, - 24, 21, 31, 31, 31, 31, 31, 31, - 26, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31] - ]}, - 'web_very_high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 4, 5, 7, 9, - 2, 2, 2, 4, 5, 7, 9, 12, - 3, 3, 4, 5, 8, 10, 12, 12, - 4, 4, 5, 7, 10, 12, 12, 12, - 5, 5, 7, 9, 12, 12, 12, 12, - 6, 6, 9, 12, 12, 12, 12, 12], - [3, 3, 5, 9, 13, 15, 15, 15, - 3, 4, 6, 11, 14, 12, 12, 12, - 5, 6, 9, 14, 12, 12, 12, 12, - 9, 11, 14, 12, 12, 12, 12, 12, - 13, 14, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'web_maximum': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 2, - 1, 1, 1, 1, 1, 1, 2, 2, - 1, 1, 1, 1, 1, 2, 2, 3, - 1, 1, 1, 1, 2, 2, 3, 3, - 1, 1, 1, 2, 2, 3, 3, 3, - 1, 1, 2, 2, 3, 3, 3, 3], - [1, 1, 1, 2, 2, 3, 3, 3, - 1, 1, 1, 2, 3, 3, 3, 3, - 1, 1, 1, 3, 3, 3, 3, 3, - 2, 2, 3, 3, 3, 3, 3, 3, - 2, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3] - ]}, - 'low': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [18, 14, 14, 21, 30, 35, 34, 17, - 14, 16, 16, 19, 26, 23, 12, 12, - 14, 16, 17, 21, 23, 12, 12, 12, - 21, 19, 21, 23, 12, 12, 12, 12, - 30, 26, 23, 12, 12, 12, 12, 12, - 35, 23, 12, 12, 12, 12, 12, 12, - 34, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12], - [20, 19, 22, 27, 20, 20, 17, 17, - 19, 25, 23, 14, 14, 12, 12, 12, - 22, 23, 14, 14, 12, 12, 12, 12, - 27, 14, 14, 12, 12, 12, 12, 12, - 20, 14, 12, 12, 12, 12, 12, 12, - 20, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'medium': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [12, 8, 8, 12, 17, 21, 24, 17, - 8, 9, 9, 11, 15, 19, 12, 12, - 8, 9, 10, 12, 19, 12, 12, 12, - 12, 11, 12, 21, 12, 12, 12, 12, - 17, 15, 19, 12, 12, 12, 12, 12, - 21, 19, 12, 12, 12, 12, 12, 12, - 24, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12], - [13, 11, 13, 16, 20, 20, 17, 17, - 11, 14, 14, 14, 14, 12, 12, 12, - 13, 14, 14, 14, 12, 12, 12, 12, - 16, 14, 14, 12, 12, 12, 12, 12, - 20, 14, 12, 12, 12, 12, 12, 12, - 20, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [6, 4, 4, 6, 9, 11, 12, 16, - 4, 5, 5, 6, 8, 10, 12, 12, - 4, 5, 5, 6, 10, 12, 12, 12, - 6, 6, 6, 11, 12, 12, 12, 12, - 9, 8, 10, 12, 12, 12, 12, 12, - 11, 10, 12, 12, 12, 12, 12, 12, - 12, 12, 12, 12, 12, 12, 12, 12, - 16, 12, 12, 12, 12, 12, 12, 12], - [7, 7, 13, 24, 20, 20, 17, 17, - 7, 12, 16, 14, 14, 12, 12, 12, - 13, 16, 14, 14, 12, 12, 12, 12, - 24, 14, 14, 12, 12, 12, 12, 12, - 20, 14, 12, 12, 12, 12, 12, 12, - 20, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'maximum': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 4, 5, 7, 9, - 2, 2, 2, 4, 5, 7, 9, 12, - 3, 3, 4, 5, 8, 10, 12, 12, - 4, 4, 5, 7, 10, 12, 12, 12, - 5, 5, 7, 9, 12, 12, 12, 12, - 6, 6, 9, 12, 12, 12, 12, 12], - [3, 3, 5, 9, 13, 15, 15, 15, - 3, 4, 6, 10, 14, 12, 12, 12, - 5, 6, 9, 14, 12, 12, 12, 12, - 9, 10, 14, 12, 12, 12, 12, 12, - 13, 14, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12] - ]}, -} -# fmt: on diff --git a/python_modules/PIL/McIdasImagePlugin.py b/python_modules/PIL/McIdasImagePlugin.py deleted file mode 100644 index 9a47933b6..000000000 --- a/python_modules/PIL/McIdasImagePlugin.py +++ /dev/null @@ -1,78 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Basic McIdas support for PIL -# -# History: -# 1997-05-05 fl Created (8-bit images only) -# 2009-03-08 fl Added 16/32-bit support. -# -# Thanks to Richard Jones and Craig Swank for specs and samples. -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import struct - -from . import Image, ImageFile - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04") - - -## -# Image plugin for McIdas area images. - - -class McIdasImageFile(ImageFile.ImageFile): - format = "MCIDAS" - format_description = "McIdas area file" - - def _open(self) -> None: - # parse area file directory - assert self.fp is not None - - s = self.fp.read(256) - if not _accept(s) or len(s) != 256: - msg = "not an McIdas area file" - raise SyntaxError(msg) - - self.area_descriptor_raw = s - self.area_descriptor = w = [0, *struct.unpack("!64i", s)] - - # get mode - if w[11] == 1: - mode = rawmode = "L" - elif w[11] == 2: - mode = rawmode = "I;16B" - elif w[11] == 4: - # FIXME: add memory map support - mode = "I" - rawmode = "I;32B" - else: - msg = "unsupported McIdas format" - raise SyntaxError(msg) - - self._mode = mode - self._size = w[10], w[9] - - offset = w[34] + w[15] - stride = w[15] + w[10] * w[11] * w[14] - - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1)) - ] - - -# -------------------------------------------------------------------- -# registry - -Image.register_open(McIdasImageFile.format, McIdasImageFile, _accept) - -# no default extension diff --git a/python_modules/PIL/MicImagePlugin.py b/python_modules/PIL/MicImagePlugin.py deleted file mode 100644 index 9ce38c427..000000000 --- a/python_modules/PIL/MicImagePlugin.py +++ /dev/null @@ -1,102 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Microsoft Image Composer support for PIL -# -# Notes: -# uses TiffImagePlugin.py to read the actual image streams -# -# History: -# 97-01-20 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import olefile - -from . import Image, TiffImagePlugin - -# -# -------------------------------------------------------------------- - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(olefile.MAGIC) - - -## -# Image plugin for Microsoft's Image Composer file format. - - -class MicImageFile(TiffImagePlugin.TiffImageFile): - format = "MIC" - format_description = "Microsoft Image Composer" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # read the OLE directory and see if this is a likely - # to be a Microsoft Image Composer file - - try: - self.ole = olefile.OleFileIO(self.fp) - except OSError as e: - msg = "not an MIC file; invalid OLE file" - raise SyntaxError(msg) from e - - # find ACI subfiles with Image members (maybe not the - # best way to identify MIC files, but what the... ;-) - - self.images = [ - path - for path in self.ole.listdir() - if path[1:] and path[0].endswith(".ACI") and path[1] == "Image" - ] - - # if we didn't find any images, this is probably not - # an MIC file. - if not self.images: - msg = "not an MIC file; no image entries" - raise SyntaxError(msg) - - self.frame = -1 - self._n_frames = len(self.images) - self.is_animated = self._n_frames > 1 - - self.__fp = self.fp - self.seek(0) - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - filename = self.images[frame] - self.fp = self.ole.openstream(filename) - - TiffImagePlugin.TiffImageFile._open(self) - - self.frame = frame - - def tell(self) -> int: - return self.frame - - def close(self) -> None: - self.__fp.close() - self.ole.close() - super().close() - - def __exit__(self, *args: object) -> None: - self.__fp.close() - self.ole.close() - super().__exit__() - - -# -# -------------------------------------------------------------------- - -Image.register_open(MicImageFile.format, MicImageFile, _accept) - -Image.register_extension(MicImageFile.format, ".mic") diff --git a/python_modules/PIL/MpegImagePlugin.py b/python_modules/PIL/MpegImagePlugin.py deleted file mode 100644 index 47ebe9d62..000000000 --- a/python_modules/PIL/MpegImagePlugin.py +++ /dev/null @@ -1,84 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# MPEG file handling -# -# History: -# 95-09-09 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1995. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile -from ._binary import i8 -from ._typing import SupportsRead - -# -# Bitstream parser - - -class BitStream: - def __init__(self, fp: SupportsRead[bytes]) -> None: - self.fp = fp - self.bits = 0 - self.bitbuffer = 0 - - def next(self) -> int: - return i8(self.fp.read(1)) - - def peek(self, bits: int) -> int: - while self.bits < bits: - self.bitbuffer = (self.bitbuffer << 8) + self.next() - self.bits += 8 - return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 - - def skip(self, bits: int) -> None: - while self.bits < bits: - self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) - self.bits += 8 - self.bits = self.bits - bits - - def read(self, bits: int) -> int: - v = self.peek(bits) - self.bits = self.bits - bits - return v - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"\x00\x00\x01\xb3") - - -## -# Image plugin for MPEG streams. This plugin can identify a stream, -# but it cannot read it. - - -class MpegImageFile(ImageFile.ImageFile): - format = "MPEG" - format_description = "MPEG" - - def _open(self) -> None: - assert self.fp is not None - - s = BitStream(self.fp) - if s.read(32) != 0x1B3: - msg = "not an MPEG file" - raise SyntaxError(msg) - - self._mode = "RGB" - self._size = s.read(12), s.read(12) - - -# -------------------------------------------------------------------- -# Registry stuff - -Image.register_open(MpegImageFile.format, MpegImageFile, _accept) - -Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"]) - -Image.register_mime(MpegImageFile.format, "video/mpeg") diff --git a/python_modules/PIL/MpoImagePlugin.py b/python_modules/PIL/MpoImagePlugin.py deleted file mode 100644 index b1ae07873..000000000 --- a/python_modules/PIL/MpoImagePlugin.py +++ /dev/null @@ -1,202 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# MPO file handling -# -# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the -# Camera & Imaging Products Association) -# -# The multi-picture object combines multiple JPEG images (with a modified EXIF -# data format) into a single file. While it can theoretically be used much like -# a GIF animation, it is commonly used to represent 3D photographs and is (as -# of this writing) the most commonly used format by 3D cameras. -# -# History: -# 2014-03-13 Feneric Created -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -import struct -from typing import IO, Any, cast - -from . import ( - Image, - ImageFile, - ImageSequence, - JpegImagePlugin, - TiffImagePlugin, -) -from ._binary import o32le -from ._util import DeferredError - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - JpegImagePlugin._save(im, fp, filename) - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - append_images = im.encoderinfo.get("append_images", []) - if not append_images and not getattr(im, "is_animated", False): - _save(im, fp, filename) - return - - mpf_offset = 28 - offsets: list[int] = [] - im_sequences = [im, *append_images] - total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences) - for im_sequence in im_sequences: - for im_frame in ImageSequence.Iterator(im_sequence): - if not offsets: - # APP2 marker - ifd_length = 66 + 16 * total - im_frame.encoderinfo["extra"] = ( - b"\xff\xe2" - + struct.pack(">H", 6 + ifd_length) - + b"MPF\0" - + b" " * ifd_length - ) - exif = im_frame.encoderinfo.get("exif") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - im_frame.encoderinfo["exif"] = exif - if exif: - mpf_offset += 4 + len(exif) - - JpegImagePlugin._save(im_frame, fp, filename) - offsets.append(fp.tell()) - else: - encoderinfo = im_frame._attach_default_encoderinfo(im) - im_frame.save(fp, "JPEG") - im_frame.encoderinfo = encoderinfo - offsets.append(fp.tell() - offsets[-1]) - - ifd = TiffImagePlugin.ImageFileDirectory_v2() - ifd[0xB000] = b"0100" - ifd[0xB001] = len(offsets) - - mpentries = b"" - data_offset = 0 - for i, size in enumerate(offsets): - if i == 0: - mptype = 0x030000 # Baseline MP Primary Image - else: - mptype = 0x000000 # Undefined - mpentries += struct.pack(" None: - self.fp.seek(0) # prep the fp in order to pass the JPEG test - JpegImagePlugin.JpegImageFile._open(self) - self._after_jpeg_open() - - def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None: - self.mpinfo = mpheader if mpheader is not None else self._getmp() - if self.mpinfo is None: - msg = "Image appears to be a malformed MPO file" - raise ValueError(msg) - self.n_frames = self.mpinfo[0xB001] - self.__mpoffsets = [ - mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] - ] - self.__mpoffsets[0] = 0 - # Note that the following assertion will only be invalid if something - # gets broken within JpegImagePlugin. - assert self.n_frames == len(self.__mpoffsets) - del self.info["mpoffset"] # no longer needed - self.is_animated = self.n_frames > 1 - self._fp = self.fp # FIXME: hack - self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame - self.__frame = 0 - self.offset = 0 - # for now we can only handle reading and individual frame extraction - self.readonly = 1 - - def load_seek(self, pos: int) -> None: - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self._fp.seek(pos) - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self.fp = self._fp - self.offset = self.__mpoffsets[frame] - - original_exif = self.info.get("exif") - if "exif" in self.info: - del self.info["exif"] - - self.fp.seek(self.offset + 2) # skip SOI marker - if not self.fp.read(2): - msg = "No data found for frame" - raise ValueError(msg) - self.fp.seek(self.offset) - JpegImagePlugin.JpegImageFile._open(self) - if self.info.get("exif") != original_exif: - self._reload_exif() - - self.tile = [ - ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1]) - ] - self.__frame = frame - - def tell(self) -> int: - return self.__frame - - @staticmethod - def adopt( - jpeg_instance: JpegImagePlugin.JpegImageFile, - mpheader: dict[int, Any] | None = None, - ) -> MpoImageFile: - """ - Transform the instance of JpegImageFile into - an instance of MpoImageFile. - After the call, the JpegImageFile is extended - to be an MpoImageFile. - - This is essentially useful when opening a JPEG - file that reveals itself as an MPO, to avoid - double call to _open. - """ - jpeg_instance.__class__ = MpoImageFile - mpo_instance = cast(MpoImageFile, jpeg_instance) - mpo_instance._after_jpeg_open(mpheader) - return mpo_instance - - -# --------------------------------------------------------------------- -# Registry stuff - -# Note that since MPO shares a factory with JPEG, we do not need to do a -# separate registration for it here. -# Image.register_open(MpoImageFile.format, -# JpegImagePlugin.jpeg_factory, _accept) -Image.register_save(MpoImageFile.format, _save) -Image.register_save_all(MpoImageFile.format, _save_all) - -Image.register_extension(MpoImageFile.format, ".mpo") - -Image.register_mime(MpoImageFile.format, "image/mpo") diff --git a/python_modules/PIL/MspImagePlugin.py b/python_modules/PIL/MspImagePlugin.py deleted file mode 100644 index 277087a86..000000000 --- a/python_modules/PIL/MspImagePlugin.py +++ /dev/null @@ -1,200 +0,0 @@ -# -# The Python Imaging Library. -# -# MSP file handling -# -# This is the format used by the Paint program in Windows 1 and 2. -# -# History: -# 95-09-05 fl Created -# 97-01-03 fl Read/write MSP images -# 17-02-21 es Fixed RLE interpretation -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1995-97. -# Copyright (c) Eric Soroos 2017. -# -# See the README file for information on usage and redistribution. -# -# More info on this format: https://archive.org/details/gg243631 -# Page 313: -# Figure 205. Windows Paint Version 1: "DanM" Format -# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03 -# -# See also: https://www.fileformat.info/format/mspaint/egff.htm -from __future__ import annotations - -import io -import struct -from typing import IO - -from . import Image, ImageFile -from ._binary import i16le as i16 -from ._binary import o16le as o16 - -# -# read MSP files - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith((b"DanM", b"LinS")) - - -## -# Image plugin for Windows MSP images. This plugin supports both -# uncompressed (Windows 1.0). - - -class MspImageFile(ImageFile.ImageFile): - format = "MSP" - format_description = "Windows Paint" - - def _open(self) -> None: - # Header - assert self.fp is not None - - s = self.fp.read(32) - if not _accept(s): - msg = "not an MSP file" - raise SyntaxError(msg) - - # Header checksum - checksum = 0 - for i in range(0, 32, 2): - checksum = checksum ^ i16(s, i) - if checksum != 0: - msg = "bad MSP checksum" - raise SyntaxError(msg) - - self._mode = "1" - self._size = i16(s, 4), i16(s, 6) - - if s.startswith(b"DanM"): - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")] - else: - self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] - - -class MspDecoder(ImageFile.PyDecoder): - # The algo for the MSP decoder is from - # https://www.fileformat.info/format/mspaint/egff.htm - # cc-by-attribution -- That page references is taken from the - # Encyclopedia of Graphics File Formats and is licensed by - # O'Reilly under the Creative Common/Attribution license - # - # For RLE encoded files, the 32byte header is followed by a scan - # line map, encoded as one 16bit word of encoded byte length per - # line. - # - # NOTE: the encoded length of the line can be 0. This was not - # handled in the previous version of this encoder, and there's no - # mention of how to handle it in the documentation. From the few - # examples I've seen, I've assumed that it is a fill of the - # background color, in this case, white. - # - # - # Pseudocode of the decoder: - # Read a BYTE value as the RunType - # If the RunType value is zero - # Read next byte as the RunCount - # Read the next byte as the RunValue - # Write the RunValue byte RunCount times - # If the RunType value is non-zero - # Use this value as the RunCount - # Read and write the next RunCount bytes literally - # - # e.g.: - # 0x00 03 ff 05 00 01 02 03 04 - # would yield the bytes: - # 0xff ff ff 00 01 02 03 04 - # - # which are then interpreted as a bit packed mode '1' image - - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - - img = io.BytesIO() - blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) - try: - self.fd.seek(32) - rowmap = struct.unpack_from( - f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2) - ) - except struct.error as e: - msg = "Truncated MSP file in row map" - raise OSError(msg) from e - - for x, rowlen in enumerate(rowmap): - try: - if rowlen == 0: - img.write(blank_line) - continue - row = self.fd.read(rowlen) - if len(row) != rowlen: - msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}" - raise OSError(msg) - idx = 0 - while idx < rowlen: - runtype = row[idx] - idx += 1 - if runtype == 0: - (runcount, runval) = struct.unpack_from("Bc", row, idx) - img.write(runval * runcount) - idx += 2 - else: - runcount = runtype - img.write(row[idx : idx + runcount]) - idx += runcount - - except struct.error as e: - msg = f"Corrupted MSP file in row {x}" - raise OSError(msg) from e - - self.set_as_raw(img.getvalue(), "1") - - return -1, 0 - - -Image.register_decoder("MSP", MspDecoder) - - -# -# write MSP files (uncompressed only) - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode != "1": - msg = f"cannot write mode {im.mode} as MSP" - raise OSError(msg) - - # create MSP header - header = [0] * 16 - - header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1 - header[2], header[3] = im.size - header[4], header[5] = 1, 1 - header[6], header[7] = 1, 1 - header[8], header[9] = im.size - - checksum = 0 - for h in header: - checksum = checksum ^ h - header[12] = checksum # FIXME: is this the right field? - - # header - for h in header: - fp.write(o16(h)) - - # image body - ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")]) - - -# -# registry - -Image.register_open(MspImageFile.format, MspImageFile, _accept) -Image.register_save(MspImageFile.format, _save) - -Image.register_extension(MspImageFile.format, ".msp") diff --git a/python_modules/PIL/PSDraw.py b/python_modules/PIL/PSDraw.py deleted file mode 100644 index 7fd4c5c94..000000000 --- a/python_modules/PIL/PSDraw.py +++ /dev/null @@ -1,237 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# Simple PostScript graphics interface -# -# History: -# 1996-04-20 fl Created -# 1999-01-10 fl Added gsave/grestore to image method -# 2005-05-04 fl Fixed floating point issue in image (from Eric Etheridge) -# -# Copyright (c) 1997-2005 by Secret Labs AB. All rights reserved. -# Copyright (c) 1996 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import sys -from typing import IO - -from . import EpsImagePlugin - -TYPE_CHECKING = False - - -## -# Simple PostScript graphics interface. - - -class PSDraw: - """ - Sets up printing to the given file. If ``fp`` is omitted, - ``sys.stdout.buffer`` is assumed. - """ - - def __init__(self, fp: IO[bytes] | None = None) -> None: - if not fp: - fp = sys.stdout.buffer - self.fp = fp - - def begin_document(self, id: str | None = None) -> None: - """Set up printing of a document. (Write PostScript DSC header.)""" - # FIXME: incomplete - self.fp.write( - b"%!PS-Adobe-3.0\n" - b"save\n" - b"/showpage { } def\n" - b"%%EndComments\n" - b"%%BeginDocument\n" - ) - # self.fp.write(ERROR_PS) # debugging! - self.fp.write(EDROFF_PS) - self.fp.write(VDI_PS) - self.fp.write(b"%%EndProlog\n") - self.isofont: dict[bytes, int] = {} - - def end_document(self) -> None: - """Ends printing. (Write PostScript DSC footer.)""" - self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") - if hasattr(self.fp, "flush"): - self.fp.flush() - - def setfont(self, font: str, size: int) -> None: - """ - Selects which font to use. - - :param font: A PostScript font name - :param size: Size in points. - """ - font_bytes = bytes(font, "UTF-8") - if font_bytes not in self.isofont: - # reencode font - self.fp.write( - b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes) - ) - self.isofont[font_bytes] = 1 - # rough - self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font_bytes)) - - def line(self, xy0: tuple[int, int], xy1: tuple[int, int]) -> None: - """ - Draws a line between the two points. Coordinates are given in - PostScript point coordinates (72 points per inch, (0, 0) is the lower - left corner of the page). - """ - self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1)) - - def rectangle(self, box: tuple[int, int, int, int]) -> None: - """ - Draws a rectangle. - - :param box: A tuple of four integers, specifying left, bottom, width and - height. - """ - self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) - - def text(self, xy: tuple[int, int], text: str) -> None: - """ - Draws text at the given position. You must use - :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. - """ - text_bytes = bytes(text, "UTF-8") - text_bytes = b"\\(".join(text_bytes.split(b"(")) - text_bytes = b"\\)".join(text_bytes.split(b")")) - self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,))) - - if TYPE_CHECKING: - from . import Image - - def image( - self, box: tuple[int, int, int, int], im: Image.Image, dpi: int | None = None - ) -> None: - """Draw a PIL image, centered in the given box.""" - # default resolution depends on mode - if not dpi: - if im.mode == "1": - dpi = 200 # fax - else: - dpi = 100 # grayscale - # image size (on paper) - x = im.size[0] * 72 / dpi - y = im.size[1] * 72 / dpi - # max allowed size - xmax = float(box[2] - box[0]) - ymax = float(box[3] - box[1]) - if x > xmax: - y = y * xmax / x - x = xmax - if y > ymax: - x = x * ymax / y - y = ymax - dx = (xmax - x) / 2 + box[0] - dy = (ymax - y) / 2 + box[1] - self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy)) - if (x, y) != im.size: - # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) - sx = x / im.size[0] - sy = y / im.size[1] - self.fp.write(b"%f %f scale\n" % (sx, sy)) - EpsImagePlugin._save(im, self.fp, "", 0) - self.fp.write(b"\ngrestore\n") - - -# -------------------------------------------------------------------- -# PostScript driver - -# -# EDROFF.PS -- PostScript driver for Edroff 2 -# -# History: -# 94-01-25 fl: created (edroff 2.04) -# -# Copyright (c) Fredrik Lundh 1994. -# - - -EDROFF_PS = b"""\ -/S { show } bind def -/P { moveto show } bind def -/M { moveto } bind def -/X { 0 rmoveto } bind def -/Y { 0 exch rmoveto } bind def -/E { findfont - dup maxlength dict begin - { - 1 index /FID ne { def } { pop pop } ifelse - } forall - /Encoding exch def - dup /FontName exch def - currentdict end definefont pop -} bind def -/F { findfont exch scalefont dup setfont - [ exch /setfont cvx ] cvx bind def -} bind def -""" - -# -# VDI.PS -- PostScript driver for VDI meta commands -# -# History: -# 94-01-25 fl: created (edroff 2.04) -# -# Copyright (c) Fredrik Lundh 1994. -# - -VDI_PS = b"""\ -/Vm { moveto } bind def -/Va { newpath arcn stroke } bind def -/Vl { moveto lineto stroke } bind def -/Vc { newpath 0 360 arc closepath } bind def -/Vr { exch dup 0 rlineto - exch dup 0 exch rlineto - exch neg 0 rlineto - 0 exch neg rlineto - setgray fill } bind def -/Tm matrix def -/Ve { Tm currentmatrix pop - translate scale newpath 0 0 .5 0 360 arc closepath - Tm setmatrix -} bind def -/Vf { currentgray exch setgray fill setgray } bind def -""" - -# -# ERROR.PS -- Error handler -# -# History: -# 89-11-21 fl: created (pslist 1.10) -# - -ERROR_PS = b"""\ -/landscape false def -/errorBUF 200 string def -/errorNL { currentpoint 10 sub exch pop 72 exch moveto } def -errordict begin /handleerror { - initmatrix /Courier findfont 10 scalefont setfont - newpath 72 720 moveto $error begin /newerror false def - (PostScript Error) show errorNL errorNL - (Error: ) show - /errorname load errorBUF cvs show errorNL errorNL - (Command: ) show - /command load dup type /stringtype ne { errorBUF cvs } if show - errorNL errorNL - (VMstatus: ) show - vmstatus errorBUF cvs show ( bytes available, ) show - errorBUF cvs show ( bytes used at level ) show - errorBUF cvs show errorNL errorNL - (Operand stargck: ) show errorNL /ostargck load { - dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL - } forall errorNL - (Execution stargck: ) show errorNL /estargck load { - dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL - } forall - end showpage -} def end -""" diff --git a/python_modules/PIL/PaletteFile.py b/python_modules/PIL/PaletteFile.py deleted file mode 100644 index 2a26e5d4e..000000000 --- a/python_modules/PIL/PaletteFile.py +++ /dev/null @@ -1,54 +0,0 @@ -# -# Python Imaging Library -# $Id$ -# -# stuff to read simple, teragon-style palette files -# -# History: -# 97-08-23 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from typing import IO - -from ._binary import o8 - - -class PaletteFile: - """File handler for Teragon-style palette files.""" - - rawmode = "RGB" - - def __init__(self, fp: IO[bytes]) -> None: - palette = [o8(i) * 3 for i in range(256)] - - while True: - s = fp.readline() - - if not s: - break - if s.startswith(b"#"): - continue - if len(s) > 100: - msg = "bad palette file" - raise SyntaxError(msg) - - v = [int(x) for x in s.split()] - try: - [i, r, g, b] = v - except ValueError: - [i, r] = v - g = b = r - - if 0 <= i <= 255: - palette[i] = o8(r) + o8(g) + o8(b) - - self.palette = b"".join(palette) - - def getpalette(self) -> tuple[bytes, str]: - return self.palette, self.rawmode diff --git a/python_modules/PIL/PalmImagePlugin.py b/python_modules/PIL/PalmImagePlugin.py deleted file mode 100644 index 15f712908..000000000 --- a/python_modules/PIL/PalmImagePlugin.py +++ /dev/null @@ -1,217 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# - -## -# Image plugin for Palm pixmap images (output only). -## -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile -from ._binary import o8 -from ._binary import o16be as o16b - -# fmt: off -_Palm8BitColormapValues = ( - (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255), - (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204), - (255, 153, 204), (255, 102, 204), (255, 51, 204), (255, 0, 204), - (255, 255, 153), (255, 204, 153), (255, 153, 153), (255, 102, 153), - (255, 51, 153), (255, 0, 153), (204, 255, 255), (204, 204, 255), - (204, 153, 255), (204, 102, 255), (204, 51, 255), (204, 0, 255), - (204, 255, 204), (204, 204, 204), (204, 153, 204), (204, 102, 204), - (204, 51, 204), (204, 0, 204), (204, 255, 153), (204, 204, 153), - (204, 153, 153), (204, 102, 153), (204, 51, 153), (204, 0, 153), - (153, 255, 255), (153, 204, 255), (153, 153, 255), (153, 102, 255), - (153, 51, 255), (153, 0, 255), (153, 255, 204), (153, 204, 204), - (153, 153, 204), (153, 102, 204), (153, 51, 204), (153, 0, 204), - (153, 255, 153), (153, 204, 153), (153, 153, 153), (153, 102, 153), - (153, 51, 153), (153, 0, 153), (102, 255, 255), (102, 204, 255), - (102, 153, 255), (102, 102, 255), (102, 51, 255), (102, 0, 255), - (102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204), - (102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153), - (102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153), - (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255), - (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204), - (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204), - (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153), - (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255), - (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255), - (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204), - (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153), - (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153), - (255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102), - (255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51), - (255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51), - (255, 255, 0), (255, 204, 0), (255, 153, 0), (255, 102, 0), - (255, 51, 0), (255, 0, 0), (204, 255, 102), (204, 204, 102), - (204, 153, 102), (204, 102, 102), (204, 51, 102), (204, 0, 102), - (204, 255, 51), (204, 204, 51), (204, 153, 51), (204, 102, 51), - (204, 51, 51), (204, 0, 51), (204, 255, 0), (204, 204, 0), - (204, 153, 0), (204, 102, 0), (204, 51, 0), (204, 0, 0), - (153, 255, 102), (153, 204, 102), (153, 153, 102), (153, 102, 102), - (153, 51, 102), (153, 0, 102), (153, 255, 51), (153, 204, 51), - (153, 153, 51), (153, 102, 51), (153, 51, 51), (153, 0, 51), - (153, 255, 0), (153, 204, 0), (153, 153, 0), (153, 102, 0), - (153, 51, 0), (153, 0, 0), (102, 255, 102), (102, 204, 102), - (102, 153, 102), (102, 102, 102), (102, 51, 102), (102, 0, 102), - (102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51), - (102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0), - (102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0), - (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102), - (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51), - (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51), - (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0), - (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102), - (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102), - (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51), - (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0), - (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17), - (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119), - (136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221), - (238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128), - (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) -# fmt: on - - -# so build a prototype image to be used for palette resampling -def build_prototype_image() -> Image.Image: - image = Image.new("L", (1, len(_Palm8BitColormapValues))) - image.putdata(list(range(len(_Palm8BitColormapValues)))) - palettedata: tuple[int, ...] = () - for colormapValue in _Palm8BitColormapValues: - palettedata += colormapValue - palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) - image.putpalette(palettedata) - return image - - -Palm8BitColormapImage = build_prototype_image() - -# OK, we now have in Palm8BitColormapImage, -# a "P"-mode image with the right palette -# -# -------------------------------------------------------------------- - -_FLAGS = {"custom-colormap": 0x4000, "is-compressed": 0x8000, "has-transparent": 0x2000} - -_COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} - - -# -# -------------------------------------------------------------------- - -## -# (Internal) Image save plugin for the Palm format. - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode == "P": - rawmode = "P" - bpp = 8 - version = 1 - - elif im.mode == "L": - if im.encoderinfo.get("bpp") in (1, 2, 4): - # this is 8-bit grayscale, so we shift it to get the high-order bits, - # and invert it because - # Palm does grayscale from white (0) to black (1) - bpp = im.encoderinfo["bpp"] - maxval = (1 << bpp) - 1 - shift = 8 - bpp - im = im.point(lambda x: maxval - (x >> shift)) - elif im.info.get("bpp") in (1, 2, 4): - # here we assume that even though the inherent mode is 8-bit grayscale, - # only the lower bpp bits are significant. - # We invert them to match the Palm. - bpp = im.info["bpp"] - maxval = (1 << bpp) - 1 - im = im.point(lambda x: maxval - (x & maxval)) - else: - msg = f"cannot write mode {im.mode} as Palm" - raise OSError(msg) - - # we ignore the palette here - im._mode = "P" - rawmode = f"P;{bpp}" - version = 1 - - elif im.mode == "1": - # monochrome -- write it inverted, as is the Palm standard - rawmode = "1;I" - bpp = 1 - version = 0 - - else: - msg = f"cannot write mode {im.mode} as Palm" - raise OSError(msg) - - # - # make sure image data is available - im.load() - - # write header - - cols = im.size[0] - rows = im.size[1] - - rowbytes = int((cols + (16 // bpp - 1)) / (16 // bpp)) * 2 - transparent_index = 0 - compression_type = _COMPRESSION_TYPES["none"] - - flags = 0 - if im.mode == "P": - flags |= _FLAGS["custom-colormap"] - colormap = im.im.getpalette() - colors = len(colormap) // 3 - colormapsize = 4 * colors + 2 - else: - colormapsize = 0 - - if "offset" in im.info: - offset = (rowbytes * rows + 16 + 3 + colormapsize) // 4 - else: - offset = 0 - - fp.write(o16b(cols) + o16b(rows) + o16b(rowbytes) + o16b(flags)) - fp.write(o8(bpp)) - fp.write(o8(version)) - fp.write(o16b(offset)) - fp.write(o8(transparent_index)) - fp.write(o8(compression_type)) - fp.write(o16b(0)) # reserved by Palm - - # now write colormap if necessary - - if colormapsize: - fp.write(o16b(colors)) - for i in range(colors): - fp.write(o8(i)) - fp.write(colormap[3 * i : 3 * i + 3]) - - # now convert data to raw form - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))] - ) - - if hasattr(fp, "flush"): - fp.flush() - - -# -# -------------------------------------------------------------------- - -Image.register_save("Palm", _save) - -Image.register_extension("Palm", ".palm") - -Image.register_mime("Palm", "image/palm") diff --git a/python_modules/PIL/PcdImagePlugin.py b/python_modules/PIL/PcdImagePlugin.py deleted file mode 100644 index 3aa249988..000000000 --- a/python_modules/PIL/PcdImagePlugin.py +++ /dev/null @@ -1,64 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PCD file handling -# -# History: -# 96-05-10 fl Created -# 96-05-27 fl Added draft mode (128x192, 256x384) -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile - -## -# Image plugin for PhotoCD images. This plugin only reads the 768x512 -# image from the file; higher resolutions are encoded in a proprietary -# encoding. - - -class PcdImageFile(ImageFile.ImageFile): - format = "PCD" - format_description = "Kodak PhotoCD" - - def _open(self) -> None: - # rough - assert self.fp is not None - - self.fp.seek(2048) - s = self.fp.read(2048) - - if not s.startswith(b"PCD_"): - msg = "not a PCD file" - raise SyntaxError(msg) - - orientation = s[1538] & 3 - self.tile_post_rotate = None - if orientation == 1: - self.tile_post_rotate = 90 - elif orientation == 3: - self.tile_post_rotate = -90 - - self._mode = "RGB" - self._size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)] - - def load_end(self) -> None: - if self.tile_post_rotate: - # Handle rotated PCDs - self.im = self.im.rotate(self.tile_post_rotate) - self._size = self.im.size - - -# -# registry - -Image.register_open(PcdImageFile.format, PcdImageFile) - -Image.register_extension(PcdImageFile.format, ".pcd") diff --git a/python_modules/PIL/PcfFontFile.py b/python_modules/PIL/PcfFontFile.py deleted file mode 100644 index 0d1968b14..000000000 --- a/python_modules/PIL/PcfFontFile.py +++ /dev/null @@ -1,254 +0,0 @@ -# -# THIS IS WORK IN PROGRESS -# -# The Python Imaging Library -# $Id$ -# -# portable compiled font file parser -# -# history: -# 1997-08-19 fl created -# 2003-09-13 fl fixed loading of unicode fonts -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1997-2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -from typing import BinaryIO, Callable - -from . import FontFile, Image -from ._binary import i8 -from ._binary import i16be as b16 -from ._binary import i16le as l16 -from ._binary import i32be as b32 -from ._binary import i32le as l32 - -# -------------------------------------------------------------------- -# declarations - -PCF_MAGIC = 0x70636601 # "\x01fcp" - -PCF_PROPERTIES = 1 << 0 -PCF_ACCELERATORS = 1 << 1 -PCF_METRICS = 1 << 2 -PCF_BITMAPS = 1 << 3 -PCF_INK_METRICS = 1 << 4 -PCF_BDF_ENCODINGS = 1 << 5 -PCF_SWIDTHS = 1 << 6 -PCF_GLYPH_NAMES = 1 << 7 -PCF_BDF_ACCELERATORS = 1 << 8 - -BYTES_PER_ROW: list[Callable[[int], int]] = [ - lambda bits: ((bits + 7) >> 3), - lambda bits: ((bits + 15) >> 3) & ~1, - lambda bits: ((bits + 31) >> 3) & ~3, - lambda bits: ((bits + 63) >> 3) & ~7, -] - - -def sz(s: bytes, o: int) -> bytes: - return s[o : s.index(b"\0", o)] - - -class PcfFontFile(FontFile.FontFile): - """Font file plugin for the X11 PCF format.""" - - name = "name" - - def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"): - self.charset_encoding = charset_encoding - - magic = l32(fp.read(4)) - if magic != PCF_MAGIC: - msg = "not a PCF file" - raise SyntaxError(msg) - - super().__init__() - - count = l32(fp.read(4)) - self.toc = {} - for i in range(count): - type = l32(fp.read(4)) - self.toc[type] = l32(fp.read(4)), l32(fp.read(4)), l32(fp.read(4)) - - self.fp = fp - - self.info = self._load_properties() - - metrics = self._load_metrics() - bitmaps = self._load_bitmaps(metrics) - encoding = self._load_encoding() - - # - # create glyph structure - - for ch, ix in enumerate(encoding): - if ix is not None: - ( - xsize, - ysize, - left, - right, - width, - ascent, - descent, - attributes, - ) = metrics[ix] - self.glyph[ch] = ( - (width, 0), - (left, descent - ysize, xsize + left, descent), - (0, 0, xsize, ysize), - bitmaps[ix], - ) - - def _getformat( - self, tag: int - ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]: - format, size, offset = self.toc[tag] - - fp = self.fp - fp.seek(offset) - - format = l32(fp.read(4)) - - if format & 4: - i16, i32 = b16, b32 - else: - i16, i32 = l16, l32 - - return fp, format, i16, i32 - - def _load_properties(self) -> dict[bytes, bytes | int]: - # - # font properties - - properties = {} - - fp, format, i16, i32 = self._getformat(PCF_PROPERTIES) - - nprops = i32(fp.read(4)) - - # read property description - p = [(i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))) for _ in range(nprops)] - - if nprops & 3: - fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad - - data = fp.read(i32(fp.read(4))) - - for k, s, v in p: - property_value: bytes | int = sz(data, v) if s else v - properties[sz(data, k)] = property_value - - return properties - - def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]: - # - # font metrics - - metrics: list[tuple[int, int, int, int, int, int, int, int]] = [] - - fp, format, i16, i32 = self._getformat(PCF_METRICS) - - append = metrics.append - - if (format & 0xFF00) == 0x100: - # "compressed" metrics - for i in range(i16(fp.read(2))): - left = i8(fp.read(1)) - 128 - right = i8(fp.read(1)) - 128 - width = i8(fp.read(1)) - 128 - ascent = i8(fp.read(1)) - 128 - descent = i8(fp.read(1)) - 128 - xsize = right - left - ysize = ascent + descent - append((xsize, ysize, left, right, width, ascent, descent, 0)) - - else: - # "jumbo" metrics - for i in range(i32(fp.read(4))): - left = i16(fp.read(2)) - right = i16(fp.read(2)) - width = i16(fp.read(2)) - ascent = i16(fp.read(2)) - descent = i16(fp.read(2)) - attributes = i16(fp.read(2)) - xsize = right - left - ysize = ascent + descent - append((xsize, ysize, left, right, width, ascent, descent, attributes)) - - return metrics - - def _load_bitmaps( - self, metrics: list[tuple[int, int, int, int, int, int, int, int]] - ) -> list[Image.Image]: - # - # bitmap data - - fp, format, i16, i32 = self._getformat(PCF_BITMAPS) - - nbitmaps = i32(fp.read(4)) - - if nbitmaps != len(metrics): - msg = "Wrong number of bitmaps" - raise OSError(msg) - - offsets = [i32(fp.read(4)) for _ in range(nbitmaps)] - - bitmap_sizes = [i32(fp.read(4)) for _ in range(4)] - - # byteorder = format & 4 # non-zero => MSB - bitorder = format & 8 # non-zero => MSB - padindex = format & 3 - - bitmapsize = bitmap_sizes[padindex] - offsets.append(bitmapsize) - - data = fp.read(bitmapsize) - - pad = BYTES_PER_ROW[padindex] - mode = "1;R" - if bitorder: - mode = "1" - - bitmaps = [] - for i in range(nbitmaps): - xsize, ysize = metrics[i][:2] - b, e = offsets[i : i + 2] - bitmaps.append( - Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) - ) - - return bitmaps - - def _load_encoding(self) -> list[int | None]: - fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) - - first_col, last_col = i16(fp.read(2)), i16(fp.read(2)) - first_row, last_row = i16(fp.read(2)), i16(fp.read(2)) - - i16(fp.read(2)) # default - - nencoding = (last_col - first_col + 1) * (last_row - first_row + 1) - - # map character code to bitmap index - encoding: list[int | None] = [None] * min(256, nencoding) - - encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)] - - for i in range(first_col, len(encoding)): - try: - encoding_offset = encoding_offsets[ - ord(bytearray([i]).decode(self.charset_encoding)) - ] - if encoding_offset != 0xFFFF: - encoding[i] = encoding_offset - except UnicodeDecodeError: - # character is not supported in selected encoding - pass - - return encoding diff --git a/python_modules/PIL/PcxImagePlugin.py b/python_modules/PIL/PcxImagePlugin.py deleted file mode 100644 index 458d586c4..000000000 --- a/python_modules/PIL/PcxImagePlugin.py +++ /dev/null @@ -1,228 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PCX file handling -# -# This format was originally used by ZSoft's popular PaintBrush -# program for the IBM PC. It is also supported by many MS-DOS and -# Windows applications, including the Windows PaintBrush program in -# Windows 3. -# -# history: -# 1995-09-01 fl Created -# 1996-05-20 fl Fixed RGB support -# 1997-01-03 fl Fixed 2-bit and 4-bit support -# 1999-02-03 fl Fixed 8-bit support (broken in 1.0b1) -# 1999-02-07 fl Added write support -# 2002-06-09 fl Made 2-bit and 4-bit support a bit more robust -# 2002-07-30 fl Seek from to current position, not beginning of file -# 2003-06-03 fl Extract DPI settings (info["dpi"]) -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import logging -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i16le as i16 -from ._binary import o8 -from ._binary import o16le as o16 - -logger = logging.getLogger(__name__) - - -def _accept(prefix: bytes) -> bool: - return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] - - -## -# Image plugin for Paintbrush images. - - -class PcxImageFile(ImageFile.ImageFile): - format = "PCX" - format_description = "Paintbrush" - - def _open(self) -> None: - # header - assert self.fp is not None - - s = self.fp.read(68) - if not _accept(s): - msg = "not a PCX file" - raise SyntaxError(msg) - - # image - bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1 - if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: - msg = "bad PCX image size" - raise SyntaxError(msg) - logger.debug("BBox: %s %s %s %s", *bbox) - - offset = self.fp.tell() + 60 - - # format - version = s[1] - bits = s[3] - planes = s[65] - provided_stride = i16(s, 66) - logger.debug( - "PCX version %s, bits %s, planes %s, stride %s", - version, - bits, - planes, - provided_stride, - ) - - self.info["dpi"] = i16(s, 12), i16(s, 14) - - if bits == 1 and planes == 1: - mode = rawmode = "1" - - elif bits == 1 and planes in (2, 4): - mode = "P" - rawmode = f"P;{planes}L" - self.palette = ImagePalette.raw("RGB", s[16:64]) - - elif version == 5 and bits == 8 and planes == 1: - mode = rawmode = "L" - # FIXME: hey, this doesn't work with the incremental loader !!! - self.fp.seek(-769, io.SEEK_END) - s = self.fp.read(769) - if len(s) == 769 and s[0] == 12: - # check if the palette is linear grayscale - for i in range(256): - if s[i * 3 + 1 : i * 3 + 4] != o8(i) * 3: - mode = rawmode = "P" - break - if mode == "P": - self.palette = ImagePalette.raw("RGB", s[1:]) - - elif version == 5 and bits == 8 and planes == 3: - mode = "RGB" - rawmode = "RGB;L" - - else: - msg = "unknown PCX mode" - raise OSError(msg) - - self._mode = mode - self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] - - # Don't trust the passed in stride. - # Calculate the approximate position for ourselves. - # CVE-2020-35653 - stride = (self._size[0] * bits + 7) // 8 - - # While the specification states that this must be even, - # not all images follow this - if provided_stride != stride: - stride += stride % 2 - - bbox = (0, 0) + self.size - logger.debug("size: %sx%s", *self.size) - - self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))] - - -# -------------------------------------------------------------------- -# save PCX files - - -SAVE = { - # mode: (version, bits, planes, raw mode) - "1": (2, 1, 1, "1"), - "L": (5, 8, 1, "L"), - "P": (5, 8, 1, "P"), - "RGB": (5, 8, 3, "RGB;L"), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - version, bits, planes, rawmode = SAVE[im.mode] - except KeyError as e: - msg = f"Cannot save {im.mode} images as PCX" - raise ValueError(msg) from e - - # bytes per plane - stride = (im.size[0] * bits + 7) // 8 - # stride should be even - stride += stride % 2 - # Stride needs to be kept in sync with the PcxEncode.c version. - # Ideally it should be passed in in the state, but the bytes value - # gets overwritten. - - logger.debug( - "PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", - im.size[0], - bits, - stride, - ) - - # under windows, we could determine the current screen size with - # "Image.core.display_mode()[1]", but I think that's overkill... - - screen = im.size - - dpi = 100, 100 - - # PCX header - fp.write( - o8(10) - + o8(version) - + o8(1) - + o8(bits) - + o16(0) - + o16(0) - + o16(im.size[0] - 1) - + o16(im.size[1] - 1) - + o16(dpi[0]) - + o16(dpi[1]) - + b"\0" * 24 - + b"\xff" * 24 - + b"\0" - + o8(planes) - + o16(stride) - + o16(1) - + o16(screen[0]) - + o16(screen[1]) - + b"\0" * 54 - ) - - assert fp.tell() == 128 - - ImageFile._save( - im, fp, [ImageFile._Tile("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))] - ) - - if im.mode == "P": - # colour palette - fp.write(o8(12)) - palette = im.im.getpalette("RGB", "RGB") - palette += b"\x00" * (768 - len(palette)) - fp.write(palette) # 768 bytes - elif im.mode == "L": - # grayscale palette - fp.write(o8(12)) - for i in range(256): - fp.write(o8(i) * 3) - - -# -------------------------------------------------------------------- -# registry - - -Image.register_open(PcxImageFile.format, PcxImageFile, _accept) -Image.register_save(PcxImageFile.format, _save) - -Image.register_extension(PcxImageFile.format, ".pcx") - -Image.register_mime(PcxImageFile.format, "image/x-pcx") diff --git a/python_modules/PIL/PdfImagePlugin.py b/python_modules/PIL/PdfImagePlugin.py deleted file mode 100644 index e9c20ddc1..000000000 --- a/python_modules/PIL/PdfImagePlugin.py +++ /dev/null @@ -1,311 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PDF (Acrobat) file handling -# -# History: -# 1996-07-16 fl Created -# 1997-01-18 fl Fixed header -# 2004-02-21 fl Fixes for 1/L/CMYK images, etc. -# 2004-02-24 fl Fixes for 1 and P images. -# -# Copyright (c) 1997-2004 by Secret Labs AB. All rights reserved. -# Copyright (c) 1996-1997 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -## -# Image plugin for PDF images (output only). -## -from __future__ import annotations - -import io -import math -import os -import time -from typing import IO, Any - -from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features - -# -# -------------------------------------------------------------------- - -# object ids: -# 1. catalogue -# 2. pages -# 3. image -# 4. page -# 5. page contents - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, save_all=True) - - -## -# (Internal) Image save plugin for the PDF format. - - -def _write_image( - im: Image.Image, - filename: str | bytes, - existing_pdf: PdfParser.PdfParser, - image_refs: list[PdfParser.IndirectReference], -) -> tuple[PdfParser.IndirectReference, str]: - # FIXME: Should replace ASCIIHexDecode with RunLengthDecode - # (packbits) or LZWDecode (tiff/lzw compression). Note that - # PDF 1.2 also supports Flatedecode (zip compression). - - params = None - decode = None - - # - # Get image characteristics - - width, height = im.size - - dict_obj: dict[str, Any] = {"BitsPerComponent": 8} - if im.mode == "1": - if features.check("libtiff"): - decode_filter = "CCITTFaxDecode" - dict_obj["BitsPerComponent"] = 1 - params = PdfParser.PdfArray( - [ - PdfParser.PdfDict( - { - "K": -1, - "BlackIs1": True, - "Columns": width, - "Rows": height, - } - ) - ] - ) - else: - decode_filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") - procset = "ImageB" # grayscale - elif im.mode == "L": - decode_filter = "DCTDecode" - # params = f"<< /Predictor 15 /Columns {width-2} >>" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") - procset = "ImageB" # grayscale - elif im.mode == "LA": - decode_filter = "JPXDecode" - # params = f"<< /Predictor 15 /Columns {width-2} >>" - procset = "ImageB" # grayscale - dict_obj["SMaskInData"] = 1 - elif im.mode == "P": - decode_filter = "ASCIIHexDecode" - palette = im.getpalette() - assert palette is not None - dict_obj["ColorSpace"] = [ - PdfParser.PdfName("Indexed"), - PdfParser.PdfName("DeviceRGB"), - len(palette) // 3 - 1, - PdfParser.PdfBinary(palette), - ] - procset = "ImageI" # indexed color - - if "transparency" in im.info: - smask = im.convert("LA").getchannel("A") - smask.encoderinfo = {} - - image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] - dict_obj["SMask"] = image_ref - elif im.mode == "RGB": - decode_filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") - procset = "ImageC" # color images - elif im.mode == "RGBA": - decode_filter = "JPXDecode" - procset = "ImageC" # color images - dict_obj["SMaskInData"] = 1 - elif im.mode == "CMYK": - decode_filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") - procset = "ImageC" # color images - decode = [1, 0, 1, 0, 1, 0, 1, 0] - else: - msg = f"cannot save mode {im.mode}" - raise ValueError(msg) - - # - # image - - op = io.BytesIO() - - if decode_filter == "ASCIIHexDecode": - ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)]) - elif decode_filter == "CCITTFaxDecode": - im.save( - op, - "TIFF", - compression="group4", - # use a single strip - strip_size=math.ceil(width / 8) * height, - ) - elif decode_filter == "DCTDecode": - Image.SAVE["JPEG"](im, op, filename) - elif decode_filter == "JPXDecode": - del dict_obj["BitsPerComponent"] - Image.SAVE["JPEG2000"](im, op, filename) - else: - msg = f"unsupported PDF filter ({decode_filter})" - raise ValueError(msg) - - stream = op.getvalue() - filter: PdfParser.PdfArray | PdfParser.PdfName - if decode_filter == "CCITTFaxDecode": - stream = stream[8:] - filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)]) - else: - filter = PdfParser.PdfName(decode_filter) - - image_ref = image_refs.pop(0) - existing_pdf.write_obj( - image_ref, - stream=stream, - Type=PdfParser.PdfName("XObject"), - Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / x_resolution, - Height=height, # * 72.0 / y_resolution, - Filter=filter, - Decode=decode, - DecodeParms=params, - **dict_obj, - ) - - return image_ref, procset - - -def _save( - im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False -) -> None: - is_appending = im.encoderinfo.get("append", False) - filename_str = filename.decode() if isinstance(filename, bytes) else filename - if is_appending: - existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="r+b") - else: - existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="w+b") - - dpi = im.encoderinfo.get("dpi") - if dpi: - x_resolution = dpi[0] - y_resolution = dpi[1] - else: - x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) - - info = { - "title": ( - None if is_appending else os.path.splitext(os.path.basename(filename))[0] - ), - "author": None, - "subject": None, - "keywords": None, - "creator": None, - "producer": None, - "creationDate": None if is_appending else time.gmtime(), - "modDate": None if is_appending else time.gmtime(), - } - for k, default in info.items(): - v = im.encoderinfo.get(k) if k in im.encoderinfo else default - if v: - existing_pdf.info[k[0].upper() + k[1:]] = v - - # - # make sure image data is available - im.load() - - existing_pdf.start_writing() - existing_pdf.write_header() - existing_pdf.write_comment(f"created by Pillow {__version__} PDF driver") - - # - # pages - ims = [im] - if save_all: - append_images = im.encoderinfo.get("append_images", []) - for append_im in append_images: - append_im.encoderinfo = im.encoderinfo.copy() - ims.append(append_im) - number_of_pages = 0 - image_refs = [] - page_refs = [] - contents_refs = [] - for im in ims: - im_number_of_pages = 1 - if save_all: - im_number_of_pages = getattr(im, "n_frames", 1) - number_of_pages += im_number_of_pages - for i in range(im_number_of_pages): - image_refs.append(existing_pdf.next_object_id(0)) - if im.mode == "P" and "transparency" in im.info: - image_refs.append(existing_pdf.next_object_id(0)) - - page_refs.append(existing_pdf.next_object_id(0)) - contents_refs.append(existing_pdf.next_object_id(0)) - existing_pdf.pages.append(page_refs[-1]) - - # - # catalog and list of pages - existing_pdf.write_catalog() - - page_number = 0 - for im_sequence in ims: - im_pages: ImageSequence.Iterator | list[Image.Image] = ( - ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] - ) - for im in im_pages: - image_ref, procset = _write_image(im, filename, existing_pdf, image_refs) - - # - # page - - existing_pdf.write_page( - page_refs[page_number], - Resources=PdfParser.PdfDict( - ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], - XObject=PdfParser.PdfDict(image=image_ref), - ), - MediaBox=[ - 0, - 0, - im.width * 72.0 / x_resolution, - im.height * 72.0 / y_resolution, - ], - Contents=contents_refs[page_number], - ) - - # - # page contents - - page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - im.width * 72.0 / x_resolution, - im.height * 72.0 / y_resolution, - ) - - existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) - - page_number += 1 - - # - # trailer - existing_pdf.write_xref_and_trailer() - if hasattr(fp, "flush"): - fp.flush() - existing_pdf.close() - - -# -# -------------------------------------------------------------------- - - -Image.register_save("PDF", _save) -Image.register_save_all("PDF", _save_all) - -Image.register_extension("PDF", ".pdf") - -Image.register_mime("PDF", "application/pdf") diff --git a/python_modules/PIL/PdfParser.py b/python_modules/PIL/PdfParser.py deleted file mode 100644 index 73d8c21c0..000000000 --- a/python_modules/PIL/PdfParser.py +++ /dev/null @@ -1,1074 +0,0 @@ -from __future__ import annotations - -import calendar -import codecs -import collections -import mmap -import os -import re -import time -import zlib -from typing import IO, Any, NamedTuple, Union - - -# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set -# on page 656 -def encode_text(s: str) -> bytes: - return codecs.BOM_UTF16_BE + s.encode("utf_16_be") - - -PDFDocEncoding = { - 0x16: "\u0017", - 0x18: "\u02d8", - 0x19: "\u02c7", - 0x1A: "\u02c6", - 0x1B: "\u02d9", - 0x1C: "\u02dd", - 0x1D: "\u02db", - 0x1E: "\u02da", - 0x1F: "\u02dc", - 0x80: "\u2022", - 0x81: "\u2020", - 0x82: "\u2021", - 0x83: "\u2026", - 0x84: "\u2014", - 0x85: "\u2013", - 0x86: "\u0192", - 0x87: "\u2044", - 0x88: "\u2039", - 0x89: "\u203a", - 0x8A: "\u2212", - 0x8B: "\u2030", - 0x8C: "\u201e", - 0x8D: "\u201c", - 0x8E: "\u201d", - 0x8F: "\u2018", - 0x90: "\u2019", - 0x91: "\u201a", - 0x92: "\u2122", - 0x93: "\ufb01", - 0x94: "\ufb02", - 0x95: "\u0141", - 0x96: "\u0152", - 0x97: "\u0160", - 0x98: "\u0178", - 0x99: "\u017d", - 0x9A: "\u0131", - 0x9B: "\u0142", - 0x9C: "\u0153", - 0x9D: "\u0161", - 0x9E: "\u017e", - 0xA0: "\u20ac", -} - - -def decode_text(b: bytes) -> str: - if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: - return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be") - else: - return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b) - - -class PdfFormatError(RuntimeError): - """An error that probably indicates a syntactic or semantic error in the - PDF file structure""" - - pass - - -def check_format_condition(condition: bool, error_message: str) -> None: - if not condition: - raise PdfFormatError(error_message) - - -class IndirectReferenceTuple(NamedTuple): - object_id: int - generation: int - - -class IndirectReference(IndirectReferenceTuple): - def __str__(self) -> str: - return f"{self.object_id} {self.generation} R" - - def __bytes__(self) -> bytes: - return self.__str__().encode("us-ascii") - - def __eq__(self, other: object) -> bool: - if self.__class__ is not other.__class__: - return False - assert isinstance(other, IndirectReference) - return other.object_id == self.object_id and other.generation == self.generation - - def __ne__(self, other: object) -> bool: - return not (self == other) - - def __hash__(self) -> int: - return hash((self.object_id, self.generation)) - - -class IndirectObjectDef(IndirectReference): - def __str__(self) -> str: - return f"{self.object_id} {self.generation} obj" - - -class XrefTable: - def __init__(self) -> None: - self.existing_entries: dict[int, tuple[int, int]] = ( - {} - ) # object ID => (offset, generation) - self.new_entries: dict[int, tuple[int, int]] = ( - {} - ) # object ID => (offset, generation) - self.deleted_entries = {0: 65536} # object ID => generation - self.reading_finished = False - - def __setitem__(self, key: int, value: tuple[int, int]) -> None: - if self.reading_finished: - self.new_entries[key] = value - else: - self.existing_entries[key] = value - if key in self.deleted_entries: - del self.deleted_entries[key] - - def __getitem__(self, key: int) -> tuple[int, int]: - try: - return self.new_entries[key] - except KeyError: - return self.existing_entries[key] - - def __delitem__(self, key: int) -> None: - if key in self.new_entries: - generation = self.new_entries[key][1] + 1 - del self.new_entries[key] - self.deleted_entries[key] = generation - elif key in self.existing_entries: - generation = self.existing_entries[key][1] + 1 - self.deleted_entries[key] = generation - elif key in self.deleted_entries: - generation = self.deleted_entries[key] - else: - msg = f"object ID {key} cannot be deleted because it doesn't exist" - raise IndexError(msg) - - def __contains__(self, key: int) -> bool: - return key in self.existing_entries or key in self.new_entries - - def __len__(self) -> int: - return len( - set(self.existing_entries.keys()) - | set(self.new_entries.keys()) - | set(self.deleted_entries.keys()) - ) - - def keys(self) -> set[int]: - return ( - set(self.existing_entries.keys()) - set(self.deleted_entries.keys()) - ) | set(self.new_entries.keys()) - - def write(self, f: IO[bytes]) -> int: - keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys())) - deleted_keys = sorted(set(self.deleted_entries.keys())) - startxref = f.tell() - f.write(b"xref\n") - while keys: - # find a contiguous sequence of object IDs - prev: int | None = None - for index, key in enumerate(keys): - if prev is None or prev + 1 == key: - prev = key - else: - contiguous_keys = keys[:index] - keys = keys[index:] - break - else: - contiguous_keys = keys - keys = [] - f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys))) - for object_id in contiguous_keys: - if object_id in self.new_entries: - f.write(b"%010d %05d n \n" % self.new_entries[object_id]) - else: - this_deleted_object_id = deleted_keys.pop(0) - check_format_condition( - object_id == this_deleted_object_id, - f"expected the next deleted object ID to be {object_id}, " - f"instead found {this_deleted_object_id}", - ) - try: - next_in_linked_list = deleted_keys[0] - except IndexError: - next_in_linked_list = 0 - f.write( - b"%010d %05d f \n" - % (next_in_linked_list, self.deleted_entries[object_id]) - ) - return startxref - - -class PdfName: - name: bytes - - def __init__(self, name: PdfName | bytes | str) -> None: - if isinstance(name, PdfName): - self.name = name.name - elif isinstance(name, bytes): - self.name = name - else: - self.name = name.encode("us-ascii") - - def name_as_str(self) -> str: - return self.name.decode("us-ascii") - - def __eq__(self, other: object) -> bool: - return ( - isinstance(other, PdfName) and other.name == self.name - ) or other == self.name - - def __hash__(self) -> int: - return hash(self.name) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({repr(self.name)})" - - @classmethod - def from_pdf_stream(cls, data: bytes) -> PdfName: - return cls(PdfParser.interpret_name(data)) - - allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} - - def __bytes__(self) -> bytes: - result = bytearray(b"/") - for b in self.name: - if b in self.allowed_chars: - result.append(b) - else: - result.extend(b"#%02X" % b) - return bytes(result) - - -class PdfArray(list[Any]): - def __bytes__(self) -> bytes: - return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" - - -TYPE_CHECKING = False -if TYPE_CHECKING: - _DictBase = collections.UserDict[Union[str, bytes], Any] -else: - _DictBase = collections.UserDict - - -class PdfDict(_DictBase): - def __setattr__(self, key: str, value: Any) -> None: - if key == "data": - collections.UserDict.__setattr__(self, key, value) - else: - self[key.encode("us-ascii")] = value - - def __getattr__(self, key: str) -> str | time.struct_time: - try: - value = self[key.encode("us-ascii")] - except KeyError as e: - raise AttributeError(key) from e - if isinstance(value, bytes): - value = decode_text(value) - if key.endswith("Date"): - if value.startswith("D:"): - value = value[2:] - - relationship = "Z" - if len(value) > 17: - relationship = value[14] - offset = int(value[15:17]) * 60 - if len(value) > 20: - offset += int(value[18:20]) - - format = "%Y%m%d%H%M%S"[: len(value) - 2] - value = time.strptime(value[: len(format) + 2], format) - if relationship in ["+", "-"]: - offset *= 60 - if relationship == "+": - offset *= -1 - value = time.gmtime(calendar.timegm(value) + offset) - return value - - def __bytes__(self) -> bytes: - out = bytearray(b"<<") - for key, value in self.items(): - if value is None: - continue - value = pdf_repr(value) - out.extend(b"\n") - out.extend(bytes(PdfName(key))) - out.extend(b" ") - out.extend(value) - out.extend(b"\n>>") - return bytes(out) - - -class PdfBinary: - def __init__(self, data: list[int] | bytes) -> None: - self.data = data - - def __bytes__(self) -> bytes: - return b"<%s>" % b"".join(b"%02X" % b for b in self.data) - - -class PdfStream: - def __init__(self, dictionary: PdfDict, buf: bytes) -> None: - self.dictionary = dictionary - self.buf = buf - - def decode(self) -> bytes: - try: - filter = self.dictionary[b"Filter"] - except KeyError: - return self.buf - if filter == b"FlateDecode": - try: - expected_length = self.dictionary[b"DL"] - except KeyError: - expected_length = self.dictionary[b"Length"] - return zlib.decompress(self.buf, bufsize=int(expected_length)) - else: - msg = f"stream filter {repr(filter)} unknown/unsupported" - raise NotImplementedError(msg) - - -def pdf_repr(x: Any) -> bytes: - if x is True: - return b"true" - elif x is False: - return b"false" - elif x is None: - return b"null" - elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): - return bytes(x) - elif isinstance(x, (int, float)): - return str(x).encode("us-ascii") - elif isinstance(x, time.struct_time): - return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" - elif isinstance(x, dict): - return bytes(PdfDict(x)) - elif isinstance(x, list): - return bytes(PdfArray(x)) - elif isinstance(x, str): - return pdf_repr(encode_text(x)) - elif isinstance(x, bytes): - # XXX escape more chars? handle binary garbage - x = x.replace(b"\\", b"\\\\") - x = x.replace(b"(", b"\\(") - x = x.replace(b")", b"\\)") - return b"(" + x + b")" - else: - return bytes(x) - - -class PdfParser: - """Based on - https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf - Supports PDF up to 1.4 - """ - - def __init__( - self, - filename: str | None = None, - f: IO[bytes] | None = None, - buf: bytes | bytearray | None = None, - start_offset: int = 0, - mode: str = "rb", - ) -> None: - if buf and f: - msg = "specify buf or f or filename, but not both buf and f" - raise RuntimeError(msg) - self.filename = filename - self.buf: bytes | bytearray | mmap.mmap | None = buf - self.f = f - self.start_offset = start_offset - self.should_close_buf = False - self.should_close_file = False - if filename is not None and f is None: - self.f = f = open(filename, mode) - self.should_close_file = True - if f is not None: - self.buf = self.get_buf_from_file(f) - self.should_close_buf = True - if not filename and hasattr(f, "name"): - self.filename = f.name - self.cached_objects: dict[IndirectReference, Any] = {} - self.root_ref: IndirectReference | None - self.info_ref: IndirectReference | None - self.pages_ref: IndirectReference | None - self.last_xref_section_offset: int | None - if self.buf: - self.read_pdf_info() - else: - self.file_size_total = self.file_size_this = 0 - self.root = PdfDict() - self.root_ref = None - self.info = PdfDict() - self.info_ref = None - self.page_tree_root = PdfDict() - self.pages: list[IndirectReference] = [] - self.orig_pages: list[IndirectReference] = [] - self.pages_ref = None - self.last_xref_section_offset = None - self.trailer_dict: dict[bytes, Any] = {} - self.xref_table = XrefTable() - self.xref_table.reading_finished = True - if f: - self.seek_end() - - def __enter__(self) -> PdfParser: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def start_writing(self) -> None: - self.close_buf() - self.seek_end() - - def close_buf(self) -> None: - if isinstance(self.buf, mmap.mmap): - self.buf.close() - self.buf = None - - def close(self) -> None: - if self.should_close_buf: - self.close_buf() - if self.f is not None and self.should_close_file: - self.f.close() - self.f = None - - def seek_end(self) -> None: - assert self.f is not None - self.f.seek(0, os.SEEK_END) - - def write_header(self) -> None: - assert self.f is not None - self.f.write(b"%PDF-1.4\n") - - def write_comment(self, s: str) -> None: - assert self.f is not None - self.f.write(f"% {s}\n".encode()) - - def write_catalog(self) -> IndirectReference: - assert self.f is not None - self.del_root() - self.root_ref = self.next_object_id(self.f.tell()) - self.pages_ref = self.next_object_id(0) - self.rewrite_pages() - self.write_obj(self.root_ref, Type=PdfName(b"Catalog"), Pages=self.pages_ref) - self.write_obj( - self.pages_ref, - Type=PdfName(b"Pages"), - Count=len(self.pages), - Kids=self.pages, - ) - return self.root_ref - - def rewrite_pages(self) -> None: - pages_tree_nodes_to_delete = [] - for i, page_ref in enumerate(self.orig_pages): - page_info = self.cached_objects[page_ref] - del self.xref_table[page_ref.object_id] - pages_tree_nodes_to_delete.append(page_info[PdfName(b"Parent")]) - if page_ref not in self.pages: - # the page has been deleted - continue - # make dict keys into strings for passing to write_page - stringified_page_info = {} - for key, value in page_info.items(): - # key should be a PdfName - stringified_page_info[key.name_as_str()] = value - stringified_page_info["Parent"] = self.pages_ref - new_page_ref = self.write_page(None, **stringified_page_info) - for j, cur_page_ref in enumerate(self.pages): - if cur_page_ref == page_ref: - # replace the page reference with the new one - self.pages[j] = new_page_ref - # delete redundant Pages tree nodes from xref table - for pages_tree_node_ref in pages_tree_nodes_to_delete: - while pages_tree_node_ref: - pages_tree_node = self.cached_objects[pages_tree_node_ref] - if pages_tree_node_ref.object_id in self.xref_table: - del self.xref_table[pages_tree_node_ref.object_id] - pages_tree_node_ref = pages_tree_node.get(b"Parent", None) - self.orig_pages = [] - - def write_xref_and_trailer( - self, new_root_ref: IndirectReference | None = None - ) -> None: - assert self.f is not None - if new_root_ref: - self.del_root() - self.root_ref = new_root_ref - if self.info: - self.info_ref = self.write_obj(None, self.info) - start_xref = self.xref_table.write(self.f) - num_entries = len(self.xref_table) - trailer_dict: dict[str | bytes, Any] = { - b"Root": self.root_ref, - b"Size": num_entries, - } - if self.last_xref_section_offset is not None: - trailer_dict[b"Prev"] = self.last_xref_section_offset - if self.info: - trailer_dict[b"Info"] = self.info_ref - self.last_xref_section_offset = start_xref - self.f.write( - b"trailer\n" - + bytes(PdfDict(trailer_dict)) - + b"\nstartxref\n%d\n%%%%EOF" % start_xref - ) - - def write_page( - self, ref: int | IndirectReference | None, *objs: Any, **dict_obj: Any - ) -> IndirectReference: - obj_ref = self.pages[ref] if isinstance(ref, int) else ref - if "Type" not in dict_obj: - dict_obj["Type"] = PdfName(b"Page") - if "Parent" not in dict_obj: - dict_obj["Parent"] = self.pages_ref - return self.write_obj(obj_ref, *objs, **dict_obj) - - def write_obj( - self, ref: IndirectReference | None, *objs: Any, **dict_obj: Any - ) -> IndirectReference: - assert self.f is not None - f = self.f - if ref is None: - ref = self.next_object_id(f.tell()) - else: - self.xref_table[ref.object_id] = (f.tell(), ref.generation) - f.write(bytes(IndirectObjectDef(*ref))) - stream = dict_obj.pop("stream", None) - if stream is not None: - dict_obj["Length"] = len(stream) - if dict_obj: - f.write(pdf_repr(dict_obj)) - for obj in objs: - f.write(pdf_repr(obj)) - if stream is not None: - f.write(b"stream\n") - f.write(stream) - f.write(b"\nendstream\n") - f.write(b"endobj\n") - return ref - - def del_root(self) -> None: - if self.root_ref is None: - return - del self.xref_table[self.root_ref.object_id] - del self.xref_table[self.root[b"Pages"].object_id] - - @staticmethod - def get_buf_from_file(f: IO[bytes]) -> bytes | mmap.mmap: - if hasattr(f, "getbuffer"): - return f.getbuffer() - elif hasattr(f, "getvalue"): - return f.getvalue() - else: - try: - return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) - except ValueError: # cannot mmap an empty file - return b"" - - def read_pdf_info(self) -> None: - assert self.buf is not None - self.file_size_total = len(self.buf) - self.file_size_this = self.file_size_total - self.start_offset - self.read_trailer() - check_format_condition( - self.trailer_dict.get(b"Root") is not None, "Root is missing" - ) - self.root_ref = self.trailer_dict[b"Root"] - assert self.root_ref is not None - self.info_ref = self.trailer_dict.get(b"Info", None) - self.root = PdfDict(self.read_indirect(self.root_ref)) - if self.info_ref is None: - self.info = PdfDict() - else: - self.info = PdfDict(self.read_indirect(self.info_ref)) - check_format_condition(b"Type" in self.root, "/Type missing in Root") - check_format_condition( - self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog" - ) - check_format_condition( - self.root.get(b"Pages") is not None, "/Pages missing in Root" - ) - check_format_condition( - isinstance(self.root[b"Pages"], IndirectReference), - "/Pages in Root is not an indirect reference", - ) - self.pages_ref = self.root[b"Pages"] - assert self.pages_ref is not None - self.page_tree_root = self.read_indirect(self.pages_ref) - self.pages = self.linearize_page_tree(self.page_tree_root) - # save the original list of page references - # in case the user modifies, adds or deletes some pages - # and we need to rewrite the pages and their list - self.orig_pages = self.pages[:] - - def next_object_id(self, offset: int | None = None) -> IndirectReference: - try: - # TODO: support reuse of deleted objects - reference = IndirectReference(max(self.xref_table.keys()) + 1, 0) - except ValueError: - reference = IndirectReference(1, 0) - if offset is not None: - self.xref_table[reference.object_id] = (offset, 0) - return reference - - delimiter = rb"[][()<>{}/%]" - delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]" - whitespace = rb"[\000\011\012\014\015\040]" - whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]" - whitespace_optional = whitespace + b"*" - whitespace_mandatory = whitespace + b"+" - # No "\012" aka "\n" or "\015" aka "\r": - whitespace_optional_no_nl = rb"[\000\011\014\040]*" - newline_only = rb"[\r\n]+" - newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl - re_trailer_end = re.compile( - whitespace_mandatory - + rb"trailer" - + whitespace_optional - + rb"<<(.*>>)" - + newline - + rb"startxref" - + newline - + rb"([0-9]+)" - + newline - + rb"%%EOF" - + whitespace_optional - + rb"$", - re.DOTALL, - ) - re_trailer_prev = re.compile( - whitespace_optional - + rb"trailer" - + whitespace_optional - + rb"<<(.*?>>)" - + newline - + rb"startxref" - + newline - + rb"([0-9]+)" - + newline - + rb"%%EOF" - + whitespace_optional, - re.DOTALL, - ) - - def read_trailer(self) -> None: - assert self.buf is not None - search_start_offset = len(self.buf) - 16384 - if search_start_offset < self.start_offset: - search_start_offset = self.start_offset - m = self.re_trailer_end.search(self.buf, search_start_offset) - check_format_condition(m is not None, "trailer end not found") - # make sure we found the LAST trailer - last_match = m - while m: - last_match = m - m = self.re_trailer_end.search(self.buf, m.start() + 16) - if not m: - m = last_match - assert m is not None - trailer_data = m.group(1) - self.last_xref_section_offset = int(m.group(2)) - self.trailer_dict = self.interpret_trailer(trailer_data) - self.xref_table = XrefTable() - self.read_xref_table(xref_section_offset=self.last_xref_section_offset) - if b"Prev" in self.trailer_dict: - self.read_prev_trailer(self.trailer_dict[b"Prev"]) - - def read_prev_trailer(self, xref_section_offset: int) -> None: - assert self.buf is not None - trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset) - m = self.re_trailer_prev.search( - self.buf[trailer_offset : trailer_offset + 16384] - ) - check_format_condition(m is not None, "previous trailer not found") - assert m is not None - trailer_data = m.group(1) - check_format_condition( - int(m.group(2)) == xref_section_offset, - "xref section offset in previous trailer doesn't match what was expected", - ) - trailer_dict = self.interpret_trailer(trailer_data) - if b"Prev" in trailer_dict: - self.read_prev_trailer(trailer_dict[b"Prev"]) - - re_whitespace_optional = re.compile(whitespace_optional) - re_name = re.compile( - whitespace_optional - + rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" - + delimiter_or_ws - + rb")" - ) - re_dict_start = re.compile(whitespace_optional + rb"<<") - re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional) - - @classmethod - def interpret_trailer(cls, trailer_data: bytes) -> dict[bytes, Any]: - trailer = {} - offset = 0 - while True: - m = cls.re_name.match(trailer_data, offset) - if not m: - m = cls.re_dict_end.match(trailer_data, offset) - check_format_condition( - m is not None and m.end() == len(trailer_data), - "name not found in trailer, remaining data: " - + repr(trailer_data[offset:]), - ) - break - key = cls.interpret_name(m.group(1)) - assert isinstance(key, bytes) - value, value_offset = cls.get_value(trailer_data, m.end()) - trailer[key] = value - if value_offset is None: - break - offset = value_offset - check_format_condition( - b"Size" in trailer and isinstance(trailer[b"Size"], int), - "/Size not in trailer or not an integer", - ) - check_format_condition( - b"Root" in trailer and isinstance(trailer[b"Root"], IndirectReference), - "/Root not in trailer or not an indirect reference", - ) - return trailer - - re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?") - - @classmethod - def interpret_name(cls, raw: bytes, as_text: bool = False) -> str | bytes: - name = b"" - for m in cls.re_hashes_in_name.finditer(raw): - if m.group(3): - name += m.group(1) + bytearray.fromhex(m.group(3).decode("us-ascii")) - else: - name += m.group(1) - if as_text: - return name.decode("utf-8") - else: - return bytes(name) - - re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")") - re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")") - re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")") - re_int = re.compile( - whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")" - ) - re_real = re.compile( - whitespace_optional - + rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" - + delimiter_or_ws - + rb")" - ) - re_array_start = re.compile(whitespace_optional + rb"\[") - re_array_end = re.compile(whitespace_optional + rb"]") - re_string_hex = re.compile( - whitespace_optional + rb"<(" + whitespace_or_hex + rb"*)>" - ) - re_string_lit = re.compile(whitespace_optional + rb"\(") - re_indirect_reference = re.compile( - whitespace_optional - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"R(?=" - + delimiter_or_ws - + rb")" - ) - re_indirect_def_start = re.compile( - whitespace_optional - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"obj(?=" - + delimiter_or_ws - + rb")" - ) - re_indirect_def_end = re.compile( - whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")" - ) - re_comment = re.compile( - rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*" - ) - re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n") - re_stream_end = re.compile( - whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")" - ) - - @classmethod - def get_value( - cls, - data: bytes | bytearray | mmap.mmap, - offset: int, - expect_indirect: IndirectReference | None = None, - max_nesting: int = -1, - ) -> tuple[Any, int | None]: - if max_nesting == 0: - return None, None - m = cls.re_comment.match(data, offset) - if m: - offset = m.end() - m = cls.re_indirect_def_start.match(data, offset) - if m: - check_format_condition( - int(m.group(1)) > 0, - "indirect object definition: object ID must be greater than 0", - ) - check_format_condition( - int(m.group(2)) >= 0, - "indirect object definition: generation must be non-negative", - ) - check_format_condition( - expect_indirect is None - or expect_indirect - == IndirectReference(int(m.group(1)), int(m.group(2))), - "indirect object definition different than expected", - ) - object, object_offset = cls.get_value( - data, m.end(), max_nesting=max_nesting - 1 - ) - if object_offset is None: - return object, None - m = cls.re_indirect_def_end.match(data, object_offset) - check_format_condition( - m is not None, "indirect object definition end not found" - ) - assert m is not None - return object, m.end() - check_format_condition( - not expect_indirect, "indirect object definition not found" - ) - m = cls.re_indirect_reference.match(data, offset) - if m: - check_format_condition( - int(m.group(1)) > 0, - "indirect object reference: object ID must be greater than 0", - ) - check_format_condition( - int(m.group(2)) >= 0, - "indirect object reference: generation must be non-negative", - ) - return IndirectReference(int(m.group(1)), int(m.group(2))), m.end() - m = cls.re_dict_start.match(data, offset) - if m: - offset = m.end() - result: dict[Any, Any] = {} - m = cls.re_dict_end.match(data, offset) - current_offset: int | None = offset - while not m: - assert current_offset is not None - key, current_offset = cls.get_value( - data, current_offset, max_nesting=max_nesting - 1 - ) - if current_offset is None: - return result, None - value, current_offset = cls.get_value( - data, current_offset, max_nesting=max_nesting - 1 - ) - result[key] = value - if current_offset is None: - return result, None - m = cls.re_dict_end.match(data, current_offset) - current_offset = m.end() - m = cls.re_stream_start.match(data, current_offset) - if m: - stream_len = result.get(b"Length") - if stream_len is None or not isinstance(stream_len, int): - msg = f"bad or missing Length in stream dict ({stream_len})" - raise PdfFormatError(msg) - stream_data = data[m.end() : m.end() + stream_len] - m = cls.re_stream_end.match(data, m.end() + stream_len) - check_format_condition(m is not None, "stream end not found") - assert m is not None - current_offset = m.end() - return PdfStream(PdfDict(result), stream_data), current_offset - return PdfDict(result), current_offset - m = cls.re_array_start.match(data, offset) - if m: - offset = m.end() - results = [] - m = cls.re_array_end.match(data, offset) - current_offset = offset - while not m: - assert current_offset is not None - value, current_offset = cls.get_value( - data, current_offset, max_nesting=max_nesting - 1 - ) - results.append(value) - if current_offset is None: - return results, None - m = cls.re_array_end.match(data, current_offset) - return results, m.end() - m = cls.re_null.match(data, offset) - if m: - return None, m.end() - m = cls.re_true.match(data, offset) - if m: - return True, m.end() - m = cls.re_false.match(data, offset) - if m: - return False, m.end() - m = cls.re_name.match(data, offset) - if m: - return PdfName(cls.interpret_name(m.group(1))), m.end() - m = cls.re_int.match(data, offset) - if m: - return int(m.group(1)), m.end() - m = cls.re_real.match(data, offset) - if m: - # XXX Decimal instead of float??? - return float(m.group(1)), m.end() - m = cls.re_string_hex.match(data, offset) - if m: - # filter out whitespace - hex_string = bytearray( - b for b in m.group(1) if b in b"0123456789abcdefABCDEF" - ) - if len(hex_string) % 2 == 1: - # append a 0 if the length is not even - yes, at the end - hex_string.append(ord(b"0")) - return bytearray.fromhex(hex_string.decode("us-ascii")), m.end() - m = cls.re_string_lit.match(data, offset) - if m: - return cls.get_literal_string(data, m.end()) - # return None, offset # fallback (only for debugging) - msg = f"unrecognized object: {repr(data[offset : offset + 32])}" - raise PdfFormatError(msg) - - re_lit_str_token = re.compile( - rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" - ) - escaped_chars = { - b"n": b"\n", - b"r": b"\r", - b"t": b"\t", - b"b": b"\b", - b"f": b"\f", - b"(": b"(", - b")": b")", - b"\\": b"\\", - ord(b"n"): b"\n", - ord(b"r"): b"\r", - ord(b"t"): b"\t", - ord(b"b"): b"\b", - ord(b"f"): b"\f", - ord(b"("): b"(", - ord(b")"): b")", - ord(b"\\"): b"\\", - } - - @classmethod - def get_literal_string( - cls, data: bytes | bytearray | mmap.mmap, offset: int - ) -> tuple[bytes, int]: - nesting_depth = 0 - result = bytearray() - for m in cls.re_lit_str_token.finditer(data, offset): - result.extend(data[offset : m.start()]) - if m.group(1): - result.extend(cls.escaped_chars[m.group(1)[1]]) - elif m.group(2): - result.append(int(m.group(2)[1:], 8)) - elif m.group(3): - pass - elif m.group(5): - result.extend(b"\n") - elif m.group(6): - result.extend(b"(") - nesting_depth += 1 - elif m.group(7): - if nesting_depth == 0: - return bytes(result), m.end() - result.extend(b")") - nesting_depth -= 1 - offset = m.end() - msg = "unfinished literal string" - raise PdfFormatError(msg) - - re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) - re_xref_subsection_start = re.compile( - whitespace_optional - + rb"([0-9]+)" - + whitespace_mandatory - + rb"([0-9]+)" - + whitespace_optional - + newline_only - ) - re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") - - def read_xref_table(self, xref_section_offset: int) -> int: - assert self.buf is not None - subsection_found = False - m = self.re_xref_section_start.match( - self.buf, xref_section_offset + self.start_offset - ) - check_format_condition(m is not None, "xref section start not found") - assert m is not None - offset = m.end() - while True: - m = self.re_xref_subsection_start.match(self.buf, offset) - if not m: - check_format_condition( - subsection_found, "xref subsection start not found" - ) - break - subsection_found = True - offset = m.end() - first_object = int(m.group(1)) - num_objects = int(m.group(2)) - for i in range(first_object, first_object + num_objects): - m = self.re_xref_entry.match(self.buf, offset) - check_format_condition(m is not None, "xref entry not found") - assert m is not None - offset = m.end() - is_free = m.group(3) == b"f" - if not is_free: - generation = int(m.group(2)) - new_entry = (int(m.group(1)), generation) - if i not in self.xref_table: - self.xref_table[i] = new_entry - return offset - - def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any: - offset, generation = self.xref_table[ref[0]] - check_format_condition( - generation == ref[1], - f"expected to find generation {ref[1]} for object ID {ref[0]} in xref " - f"table, instead found generation {generation} at offset {offset}", - ) - assert self.buf is not None - value = self.get_value( - self.buf, - offset + self.start_offset, - expect_indirect=IndirectReference(*ref), - max_nesting=max_nesting, - )[0] - self.cached_objects[ref] = value - return value - - def linearize_page_tree( - self, node: PdfDict | None = None - ) -> list[IndirectReference]: - page_node = node if node is not None else self.page_tree_root - check_format_condition( - page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" - ) - pages = [] - for kid in page_node[b"Kids"]: - kid_object = self.read_indirect(kid) - if kid_object[b"Type"] == b"Page": - pages.append(kid) - else: - pages.extend(self.linearize_page_tree(node=kid_object)) - return pages diff --git a/python_modules/PIL/PixarImagePlugin.py b/python_modules/PIL/PixarImagePlugin.py deleted file mode 100644 index d2b6d0a97..000000000 --- a/python_modules/PIL/PixarImagePlugin.py +++ /dev/null @@ -1,72 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PIXAR raster support for PIL -# -# history: -# 97-01-29 fl Created -# -# notes: -# This is incomplete; it is based on a few samples created with -# Photoshop 2.5 and 3.0, and a summary description provided by -# Greg Coats . Hopefully, "L" and -# "RGBA" support will be added in future versions. -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile -from ._binary import i16le as i16 - -# -# helpers - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"\200\350\000\000") - - -## -# Image plugin for PIXAR raster images. - - -class PixarImageFile(ImageFile.ImageFile): - format = "PIXAR" - format_description = "PIXAR raster image" - - def _open(self) -> None: - # assuming a 4-byte magic label - assert self.fp is not None - - s = self.fp.read(4) - if not _accept(s): - msg = "not a PIXAR file" - raise SyntaxError(msg) - - # read rest of header - s = s + self.fp.read(508) - - self._size = i16(s, 418), i16(s, 416) - - # get channel/depth descriptions - mode = i16(s, 424), i16(s, 426) - - if mode == (14, 2): - self._mode = "RGB" - # FIXME: to be continued... - - # create tile descriptor (assuming "dumped") - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)] - - -# -# -------------------------------------------------------------------- - -Image.register_open(PixarImageFile.format, PixarImageFile, _accept) - -Image.register_extension(PixarImageFile.format, ".pxr") diff --git a/python_modules/PIL/PngImagePlugin.py b/python_modules/PIL/PngImagePlugin.py deleted file mode 100644 index 1b9a89aef..000000000 --- a/python_modules/PIL/PngImagePlugin.py +++ /dev/null @@ -1,1551 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PNG support code -# -# See "PNG (Portable Network Graphics) Specification, version 1.0; -# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.). -# -# history: -# 1996-05-06 fl Created (couldn't resist it) -# 1996-12-14 fl Upgraded, added read and verify support (0.2) -# 1996-12-15 fl Separate PNG stream parser -# 1996-12-29 fl Added write support, added getchunks -# 1996-12-30 fl Eliminated circular references in decoder (0.3) -# 1998-07-12 fl Read/write 16-bit images as mode I (0.4) -# 2001-02-08 fl Added transparency support (from Zircon) (0.5) -# 2001-04-16 fl Don't close data source in "open" method (0.6) -# 2004-02-24 fl Don't even pretend to support interlaced files (0.7) -# 2004-08-31 fl Do basic sanity check on chunk identifiers (0.8) -# 2004-09-20 fl Added PngInfo chunk container -# 2004-12-18 fl Added DPI read support (based on code by Niki Spahiev) -# 2008-08-13 fl Added tRNS support for RGB images -# 2009-03-06 fl Support for preserving ICC profiles (by Florian Hoech) -# 2009-03-08 fl Added zTXT support (from Lowell Alleman) -# 2009-03-29 fl Read interlaced PNG files (from Conrado Porto Lopes Gouvua) -# -# Copyright (c) 1997-2009 by Secret Labs AB -# Copyright (c) 1996 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import itertools -import logging -import re -import struct -import warnings -import zlib -from collections.abc import Callable -from enum import IntEnum -from typing import IO, Any, NamedTuple, NoReturn, cast - -from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import o8 -from ._binary import o16be as o16 -from ._binary import o32be as o32 -from ._deprecate import deprecate -from ._util import DeferredError - -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import _imaging - -logger = logging.getLogger(__name__) - -is_cid = re.compile(rb"\w\w\w\w").match - - -_MAGIC = b"\211PNG\r\n\032\n" - - -_MODES = { - # supported bits/color combinations, and corresponding modes/rawmodes - # Grayscale - (1, 0): ("1", "1"), - (2, 0): ("L", "L;2"), - (4, 0): ("L", "L;4"), - (8, 0): ("L", "L"), - (16, 0): ("I;16", "I;16B"), - # Truecolour - (8, 2): ("RGB", "RGB"), - (16, 2): ("RGB", "RGB;16B"), - # Indexed-colour - (1, 3): ("P", "P;1"), - (2, 3): ("P", "P;2"), - (4, 3): ("P", "P;4"), - (8, 3): ("P", "P"), - # Grayscale with alpha - (8, 4): ("LA", "LA"), - (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available - # Truecolour with alpha - (8, 6): ("RGBA", "RGBA"), - (16, 6): ("RGBA", "RGBA;16B"), -} - - -_simple_palette = re.compile(b"^\xff*\x00\xff*$") - -MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK -""" -Maximum decompressed size for a iTXt or zTXt chunk. -Eliminates decompression bombs where compressed chunks can expand 1000x. -See :ref:`Text in PNG File Format`. -""" -MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK -""" -Set the maximum total text chunk size. -See :ref:`Text in PNG File Format`. -""" - - -# APNG frame disposal modes -class Disposal(IntEnum): - OP_NONE = 0 - """ - No disposal is done on this frame before rendering the next frame. - See :ref:`Saving APNG sequences`. - """ - OP_BACKGROUND = 1 - """ - This frame’s modified region is cleared to fully transparent black before rendering - the next frame. - See :ref:`Saving APNG sequences`. - """ - OP_PREVIOUS = 2 - """ - This frame’s modified region is reverted to the previous frame’s contents before - rendering the next frame. - See :ref:`Saving APNG sequences`. - """ - - -# APNG frame blend modes -class Blend(IntEnum): - OP_SOURCE = 0 - """ - All color components of this frame, including alpha, overwrite the previous output - image contents. - See :ref:`Saving APNG sequences`. - """ - OP_OVER = 1 - """ - This frame should be alpha composited with the previous output image contents. - See :ref:`Saving APNG sequences`. - """ - - -def _safe_zlib_decompress(s: bytes) -> bytes: - dobj = zlib.decompressobj() - plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) - if dobj.unconsumed_tail: - msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK" - raise ValueError(msg) - return plaintext - - -def _crc32(data: bytes, seed: int = 0) -> int: - return zlib.crc32(data, seed) & 0xFFFFFFFF - - -# -------------------------------------------------------------------- -# Support classes. Suitable for PNG and related formats like MNG etc. - - -class ChunkStream: - def __init__(self, fp: IO[bytes]) -> None: - self.fp: IO[bytes] | None = fp - self.queue: list[tuple[bytes, int, int]] | None = [] - - def read(self) -> tuple[bytes, int, int]: - """Fetch a new chunk. Returns header information.""" - cid = None - - assert self.fp is not None - if self.queue: - cid, pos, length = self.queue.pop() - self.fp.seek(pos) - else: - s = self.fp.read(8) - cid = s[4:] - pos = self.fp.tell() - length = i32(s) - - if not is_cid(cid): - if not ImageFile.LOAD_TRUNCATED_IMAGES: - msg = f"broken PNG file (chunk {repr(cid)})" - raise SyntaxError(msg) - - return cid, pos, length - - def __enter__(self) -> ChunkStream: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def close(self) -> None: - self.queue = self.fp = None - - def push(self, cid: bytes, pos: int, length: int) -> None: - assert self.queue is not None - self.queue.append((cid, pos, length)) - - def call(self, cid: bytes, pos: int, length: int) -> bytes: - """Call the appropriate chunk handler""" - - logger.debug("STREAM %r %s %s", cid, pos, length) - return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length) - - def crc(self, cid: bytes, data: bytes) -> None: - """Read and verify checksum""" - - # Skip CRC checks for ancillary chunks if allowed to load truncated - # images - # 5th byte of first char is 1 [specs, section 5.4] - if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1): - self.crc_skip(cid, data) - return - - assert self.fp is not None - try: - crc1 = _crc32(data, _crc32(cid)) - crc2 = i32(self.fp.read(4)) - if crc1 != crc2: - msg = f"broken PNG file (bad header checksum in {repr(cid)})" - raise SyntaxError(msg) - except struct.error as e: - msg = f"broken PNG file (incomplete checksum in {repr(cid)})" - raise SyntaxError(msg) from e - - def crc_skip(self, cid: bytes, data: bytes) -> None: - """Read checksum""" - - assert self.fp is not None - self.fp.read(4) - - def verify(self, endchunk: bytes = b"IEND") -> list[bytes]: - # Simple approach; just calculate checksum for all remaining - # blocks. Must be called directly after open. - - cids = [] - - assert self.fp is not None - while True: - try: - cid, pos, length = self.read() - except struct.error as e: - msg = "truncated PNG file" - raise OSError(msg) from e - - if cid == endchunk: - break - self.crc(cid, ImageFile._safe_read(self.fp, length)) - cids.append(cid) - - return cids - - -class iTXt(str): - """ - Subclass of string to allow iTXt chunks to look like strings while - keeping their extra information - - """ - - lang: str | bytes | None - tkey: str | bytes | None - - @staticmethod - def __new__( - cls, text: str, lang: str | None = None, tkey: str | None = None - ) -> iTXt: - """ - :param cls: the class to use when creating the instance - :param text: value for this key - :param lang: language code - :param tkey: UTF-8 version of the key name - """ - - self = str.__new__(cls, text) - self.lang = lang - self.tkey = tkey - return self - - -class PngInfo: - """ - PNG chunk container (for use with save(pnginfo=)) - - """ - - def __init__(self) -> None: - self.chunks: list[tuple[bytes, bytes, bool]] = [] - - def add(self, cid: bytes, data: bytes, after_idat: bool = False) -> None: - """Appends an arbitrary chunk. Use with caution. - - :param cid: a byte string, 4 bytes long. - :param data: a byte string of the encoded data - :param after_idat: for use with private chunks. Whether the chunk - should be written after IDAT - - """ - - self.chunks.append((cid, data, after_idat)) - - def add_itxt( - self, - key: str | bytes, - value: str | bytes, - lang: str | bytes = "", - tkey: str | bytes = "", - zip: bool = False, - ) -> None: - """Appends an iTXt chunk. - - :param key: latin-1 encodable text key name - :param value: value for this key - :param lang: language code - :param tkey: UTF-8 version of the key name - :param zip: compression flag - - """ - - if not isinstance(key, bytes): - key = key.encode("latin-1", "strict") - if not isinstance(value, bytes): - value = value.encode("utf-8", "strict") - if not isinstance(lang, bytes): - lang = lang.encode("utf-8", "strict") - if not isinstance(tkey, bytes): - tkey = tkey.encode("utf-8", "strict") - - if zip: - self.add( - b"iTXt", - key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value), - ) - else: - self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) - - def add_text( - self, key: str | bytes, value: str | bytes | iTXt, zip: bool = False - ) -> None: - """Appends a text chunk. - - :param key: latin-1 encodable text key name - :param value: value for this key, text or an - :py:class:`PIL.PngImagePlugin.iTXt` instance - :param zip: compression flag - - """ - if isinstance(value, iTXt): - return self.add_itxt( - key, - value, - value.lang if value.lang is not None else b"", - value.tkey if value.tkey is not None else b"", - zip=zip, - ) - - # The tEXt chunk stores latin-1 text - if not isinstance(value, bytes): - try: - value = value.encode("latin-1", "strict") - except UnicodeError: - return self.add_itxt(key, value, zip=zip) - - if not isinstance(key, bytes): - key = key.encode("latin-1", "strict") - - if zip: - self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) - else: - self.add(b"tEXt", key + b"\0" + value) - - -# -------------------------------------------------------------------- -# PNG image stream (IHDR/IEND) - - -class _RewindState(NamedTuple): - info: dict[str | tuple[int, int], Any] - tile: list[ImageFile._Tile] - seq_num: int | None - - -class PngStream(ChunkStream): - def __init__(self, fp: IO[bytes]) -> None: - super().__init__(fp) - - # local copies of Image attributes - self.im_info: dict[str | tuple[int, int], Any] = {} - self.im_text: dict[str, str | iTXt] = {} - self.im_size = (0, 0) - self.im_mode = "" - self.im_tile: list[ImageFile._Tile] = [] - self.im_palette: tuple[str, bytes] | None = None - self.im_custom_mimetype: str | None = None - self.im_n_frames: int | None = None - self._seq_num: int | None = None - self.rewind_state = _RewindState({}, [], None) - - self.text_memory = 0 - - def check_text_memory(self, chunklen: int) -> None: - self.text_memory += chunklen - if self.text_memory > MAX_TEXT_MEMORY: - msg = ( - "Too much memory used in text chunks: " - f"{self.text_memory}>MAX_TEXT_MEMORY" - ) - raise ValueError(msg) - - def save_rewind(self) -> None: - self.rewind_state = _RewindState( - self.im_info.copy(), - self.im_tile, - self._seq_num, - ) - - def rewind(self) -> None: - self.im_info = self.rewind_state.info.copy() - self.im_tile = self.rewind_state.tile - self._seq_num = self.rewind_state.seq_num - - def chunk_iCCP(self, pos: int, length: int) -> bytes: - # ICC profile - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - # according to PNG spec, the iCCP chunk contains: - # Profile name 1-79 bytes (character string) - # Null separator 1 byte (null character) - # Compression method 1 byte (0) - # Compressed profile n bytes (zlib with deflate compression) - i = s.find(b"\0") - logger.debug("iCCP profile name %r", s[:i]) - comp_method = s[i + 1] - logger.debug("Compression method %s", comp_method) - if comp_method != 0: - msg = f"Unknown compression method {comp_method} in iCCP chunk" - raise SyntaxError(msg) - try: - icc_profile = _safe_zlib_decompress(s[i + 2 :]) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - icc_profile = None - else: - raise - except zlib.error: - icc_profile = None # FIXME - self.im_info["icc_profile"] = icc_profile - return s - - def chunk_IHDR(self, pos: int, length: int) -> bytes: - # image header - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if length < 13: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "Truncated IHDR chunk" - raise ValueError(msg) - self.im_size = i32(s, 0), i32(s, 4) - try: - self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])] - except Exception: - pass - if s[12]: - self.im_info["interlace"] = 1 - if s[11]: - msg = "unknown filter category" - raise SyntaxError(msg) - return s - - def chunk_IDAT(self, pos: int, length: int) -> NoReturn: - # image data - if "bbox" in self.im_info: - tile = [ImageFile._Tile("zip", self.im_info["bbox"], pos, self.im_rawmode)] - else: - if self.im_n_frames is not None: - self.im_info["default_image"] = True - tile = [ImageFile._Tile("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] - self.im_tile = tile - self.im_idat = length - msg = "image data found" - raise EOFError(msg) - - def chunk_IEND(self, pos: int, length: int) -> NoReturn: - msg = "end of PNG image" - raise EOFError(msg) - - def chunk_PLTE(self, pos: int, length: int) -> bytes: - # palette - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if self.im_mode == "P": - self.im_palette = "RGB", s - return s - - def chunk_tRNS(self, pos: int, length: int) -> bytes: - # transparency - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if self.im_mode == "P": - if _simple_palette.match(s): - # tRNS contains only one full-transparent entry, - # other entries are full opaque - i = s.find(b"\0") - if i >= 0: - self.im_info["transparency"] = i - else: - # otherwise, we have a byte string with one alpha value - # for each palette entry - self.im_info["transparency"] = s - elif self.im_mode in ("1", "L", "I;16"): - self.im_info["transparency"] = i16(s) - elif self.im_mode == "RGB": - self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) - return s - - def chunk_gAMA(self, pos: int, length: int) -> bytes: - # gamma setting - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - self.im_info["gamma"] = i32(s) / 100000.0 - return s - - def chunk_cHRM(self, pos: int, length: int) -> bytes: - # chromaticity, 8 unsigned ints, actual value is scaled by 100,000 - # WP x,y, Red x,y, Green x,y Blue x,y - - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - raw_vals = struct.unpack(f">{len(s) // 4}I", s) - self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) - return s - - def chunk_sRGB(self, pos: int, length: int) -> bytes: - # srgb rendering intent, 1 byte - # 0 perceptual - # 1 relative colorimetric - # 2 saturation - # 3 absolute colorimetric - - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if length < 1: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "Truncated sRGB chunk" - raise ValueError(msg) - self.im_info["srgb"] = s[0] - return s - - def chunk_pHYs(self, pos: int, length: int) -> bytes: - # pixels per unit - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if length < 9: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "Truncated pHYs chunk" - raise ValueError(msg) - px, py = i32(s, 0), i32(s, 4) - unit = s[8] - if unit == 1: # meter - dpi = px * 0.0254, py * 0.0254 - self.im_info["dpi"] = dpi - elif unit == 0: - self.im_info["aspect"] = px, py - return s - - def chunk_tEXt(self, pos: int, length: int) -> bytes: - # text - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - try: - k, v = s.split(b"\0", 1) - except ValueError: - # fallback for broken tEXt tags - k = s - v = b"" - if k: - k_str = k.decode("latin-1", "strict") - v_str = v.decode("latin-1", "replace") - - self.im_info[k_str] = v if k == b"exif" else v_str - self.im_text[k_str] = v_str - self.check_text_memory(len(v_str)) - - return s - - def chunk_zTXt(self, pos: int, length: int) -> bytes: - # compressed text - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - try: - k, v = s.split(b"\0", 1) - except ValueError: - k = s - v = b"" - if v: - comp_method = v[0] - else: - comp_method = 0 - if comp_method != 0: - msg = f"Unknown compression method {comp_method} in zTXt chunk" - raise SyntaxError(msg) - try: - v = _safe_zlib_decompress(v[1:]) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - v = b"" - else: - raise - except zlib.error: - v = b"" - - if k: - k_str = k.decode("latin-1", "strict") - v_str = v.decode("latin-1", "replace") - - self.im_info[k_str] = self.im_text[k_str] = v_str - self.check_text_memory(len(v_str)) - - return s - - def chunk_iTXt(self, pos: int, length: int) -> bytes: - # international text - assert self.fp is not None - r = s = ImageFile._safe_read(self.fp, length) - try: - k, r = r.split(b"\0", 1) - except ValueError: - return s - if len(r) < 2: - return s - cf, cm, r = r[0], r[1], r[2:] - try: - lang, tk, v = r.split(b"\0", 2) - except ValueError: - return s - if cf != 0: - if cm == 0: - try: - v = _safe_zlib_decompress(v) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - else: - raise - except zlib.error: - return s - else: - return s - if k == b"XML:com.adobe.xmp": - self.im_info["xmp"] = v - try: - k_str = k.decode("latin-1", "strict") - lang_str = lang.decode("utf-8", "strict") - tk_str = tk.decode("utf-8", "strict") - v_str = v.decode("utf-8", "strict") - except UnicodeError: - return s - - self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str) - self.check_text_memory(len(v_str)) - - return s - - def chunk_eXIf(self, pos: int, length: int) -> bytes: - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - self.im_info["exif"] = b"Exif\x00\x00" + s - return s - - # APNG chunks - def chunk_acTL(self, pos: int, length: int) -> bytes: - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if length < 8: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "APNG contains truncated acTL chunk" - raise ValueError(msg) - if self.im_n_frames is not None: - self.im_n_frames = None - warnings.warn("Invalid APNG, will use default PNG image if possible") - return s - n_frames = i32(s) - if n_frames == 0 or n_frames > 0x80000000: - warnings.warn("Invalid APNG, will use default PNG image if possible") - return s - self.im_n_frames = n_frames - self.im_info["loop"] = i32(s, 4) - self.im_custom_mimetype = "image/apng" - return s - - def chunk_fcTL(self, pos: int, length: int) -> bytes: - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if length < 26: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "APNG contains truncated fcTL chunk" - raise ValueError(msg) - seq = i32(s) - if (self._seq_num is None and seq != 0) or ( - self._seq_num is not None and self._seq_num != seq - 1 - ): - msg = "APNG contains frame sequence errors" - raise SyntaxError(msg) - self._seq_num = seq - width, height = i32(s, 4), i32(s, 8) - px, py = i32(s, 12), i32(s, 16) - im_w, im_h = self.im_size - if px + width > im_w or py + height > im_h: - msg = "APNG contains invalid frames" - raise SyntaxError(msg) - self.im_info["bbox"] = (px, py, px + width, py + height) - delay_num, delay_den = i16(s, 20), i16(s, 22) - if delay_den == 0: - delay_den = 100 - self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000 - self.im_info["disposal"] = s[24] - self.im_info["blend"] = s[25] - return s - - def chunk_fdAT(self, pos: int, length: int) -> bytes: - assert self.fp is not None - if length < 4: - if ImageFile.LOAD_TRUNCATED_IMAGES: - s = ImageFile._safe_read(self.fp, length) - return s - msg = "APNG contains truncated fDAT chunk" - raise ValueError(msg) - s = ImageFile._safe_read(self.fp, 4) - seq = i32(s) - if self._seq_num != seq - 1: - msg = "APNG contains frame sequence errors" - raise SyntaxError(msg) - self._seq_num = seq - return self.chunk_IDAT(pos + 4, length - 4) - - -# -------------------------------------------------------------------- -# PNG reader - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(_MAGIC) - - -## -# Image plugin for PNG images. - - -class PngImageFile(ImageFile.ImageFile): - format = "PNG" - format_description = "Portable network graphics" - - def _open(self) -> None: - if not _accept(self.fp.read(8)): - msg = "not a PNG file" - raise SyntaxError(msg) - self._fp = self.fp - self.__frame = 0 - - # - # Parse headers up to the first IDAT or fDAT chunk - - self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = [] - self.png: PngStream | None = PngStream(self.fp) - - while True: - # - # get next chunk - - cid, pos, length = self.png.read() - - try: - s = self.png.call(cid, pos, length) - except EOFError: - break - except AttributeError: - logger.debug("%r %s %s (unknown)", cid, pos, length) - s = ImageFile._safe_read(self.fp, length) - if cid[1:2].islower(): - self.private_chunks.append((cid, s)) - - self.png.crc(cid, s) - - # - # Copy relevant attributes from the PngStream. An alternative - # would be to let the PngStream class modify these attributes - # directly, but that introduces circular references which are - # difficult to break if things go wrong in the decoder... - # (believe me, I've tried ;-) - - self._mode = self.png.im_mode - self._size = self.png.im_size - self.info = self.png.im_info - self._text: dict[str, str | iTXt] | None = None - self.tile = self.png.im_tile - self.custom_mimetype = self.png.im_custom_mimetype - self.n_frames = self.png.im_n_frames or 1 - self.default_image = self.info.get("default_image", False) - - if self.png.im_palette: - rawmode, data = self.png.im_palette - self.palette = ImagePalette.raw(rawmode, data) - - if cid == b"fdAT": - self.__prepare_idat = length - 4 - else: - self.__prepare_idat = length # used by load_prepare() - - if self.png.im_n_frames is not None: - self._close_exclusive_fp_after_loading = False - self.png.save_rewind() - self.__rewind_idat = self.__prepare_idat - self.__rewind = self._fp.tell() - if self.default_image: - # IDAT chunk contains default image and not first animation frame - self.n_frames += 1 - self._seek(0) - self.is_animated = self.n_frames > 1 - - @property - def text(self) -> dict[str, str | iTXt]: - # experimental - if self._text is None: - # iTxt, tEXt and zTXt chunks may appear at the end of the file - # So load the file to ensure that they are read - if self.is_animated: - frame = self.__frame - # for APNG, seek to the final frame before loading - self.seek(self.n_frames - 1) - self.load() - if self.is_animated: - self.seek(frame) - assert self._text is not None - return self._text - - def verify(self) -> None: - """Verify PNG file""" - - if self.fp is None: - msg = "verify must be called directly after open" - raise RuntimeError(msg) - - # back up to beginning of IDAT block - self.fp.seek(self.tile[0][2] - 8) - - assert self.png is not None - self.png.verify() - self.png.close() - - if self._exclusive_fp: - self.fp.close() - self.fp = None - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if frame < self.__frame: - self._seek(0, True) - - last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: - self._seek(f) - except EOFError as e: - self.seek(last_frame) - msg = "no more images in APNG file" - raise EOFError(msg) from e - - def _seek(self, frame: int, rewind: bool = False) -> None: - assert self.png is not None - if isinstance(self._fp, DeferredError): - raise self._fp.ex - - self.dispose: _imaging.ImagingCore | None - dispose_extent = None - if frame == 0: - if rewind: - self._fp.seek(self.__rewind) - self.png.rewind() - self.__prepare_idat = self.__rewind_idat - self._im = None - self.info = self.png.im_info - self.tile = self.png.im_tile - self.fp = self._fp - self._prev_im = None - self.dispose = None - self.default_image = self.info.get("default_image", False) - self.dispose_op = self.info.get("disposal") - self.blend_op = self.info.get("blend") - dispose_extent = self.info.get("bbox") - self.__frame = 0 - else: - if frame != self.__frame + 1: - msg = f"cannot seek to frame {frame}" - raise ValueError(msg) - - # ensure previous frame was loaded - self.load() - - if self.dispose: - self.im.paste(self.dispose, self.dispose_extent) - self._prev_im = self.im.copy() - - self.fp = self._fp - - # advance to the next frame - if self.__prepare_idat: - ImageFile._safe_read(self.fp, self.__prepare_idat) - self.__prepare_idat = 0 - frame_start = False - while True: - self.fp.read(4) # CRC - - try: - cid, pos, length = self.png.read() - except (struct.error, SyntaxError): - break - - if cid == b"IEND": - msg = "No more images in APNG file" - raise EOFError(msg) - if cid == b"fcTL": - if frame_start: - # there must be at least one fdAT chunk between fcTL chunks - msg = "APNG missing frame data" - raise SyntaxError(msg) - frame_start = True - - try: - self.png.call(cid, pos, length) - except UnicodeDecodeError: - break - except EOFError: - if cid == b"fdAT": - length -= 4 - if frame_start: - self.__prepare_idat = length - break - ImageFile._safe_read(self.fp, length) - except AttributeError: - logger.debug("%r %s %s (unknown)", cid, pos, length) - ImageFile._safe_read(self.fp, length) - - self.__frame = frame - self.tile = self.png.im_tile - self.dispose_op = self.info.get("disposal") - self.blend_op = self.info.get("blend") - dispose_extent = self.info.get("bbox") - - if not self.tile: - msg = "image not found in APNG frame" - raise EOFError(msg) - if dispose_extent: - self.dispose_extent: tuple[float, float, float, float] = dispose_extent - - # setup frame disposal (actual disposal done when needed in the next _seek()) - if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: - self.dispose_op = Disposal.OP_BACKGROUND - - self.dispose = None - if self.dispose_op == Disposal.OP_PREVIOUS: - if self._prev_im: - self.dispose = self._prev_im.copy() - self.dispose = self._crop(self.dispose, self.dispose_extent) - elif self.dispose_op == Disposal.OP_BACKGROUND: - self.dispose = Image.core.fill(self.mode, self.size) - self.dispose = self._crop(self.dispose, self.dispose_extent) - - def tell(self) -> int: - return self.__frame - - def load_prepare(self) -> None: - """internal: prepare to read PNG file""" - - if self.info.get("interlace"): - self.decoderconfig = self.decoderconfig + (1,) - - self.__idat = self.__prepare_idat # used by load_read() - ImageFile.ImageFile.load_prepare(self) - - def load_read(self, read_bytes: int) -> bytes: - """internal: read more image data""" - - assert self.png is not None - while self.__idat == 0: - # end of chunk, skip forward to next one - - self.fp.read(4) # CRC - - cid, pos, length = self.png.read() - - if cid not in [b"IDAT", b"DDAT", b"fdAT"]: - self.png.push(cid, pos, length) - return b"" - - if cid == b"fdAT": - try: - self.png.call(cid, pos, length) - except EOFError: - pass - self.__idat = length - 4 # sequence_num has already been read - else: - self.__idat = length # empty chunks are allowed - - # read more data from this chunk - if read_bytes <= 0: - read_bytes = self.__idat - else: - read_bytes = min(read_bytes, self.__idat) - - self.__idat = self.__idat - read_bytes - - return self.fp.read(read_bytes) - - def load_end(self) -> None: - """internal: finished reading image data""" - assert self.png is not None - if self.__idat != 0: - self.fp.read(self.__idat) - while True: - self.fp.read(4) # CRC - - try: - cid, pos, length = self.png.read() - except (struct.error, SyntaxError): - break - - if cid == b"IEND": - break - elif cid == b"fcTL" and self.is_animated: - # start of the next frame, stop reading - self.__prepare_idat = 0 - self.png.push(cid, pos, length) - break - - try: - self.png.call(cid, pos, length) - except UnicodeDecodeError: - break - except EOFError: - if cid == b"fdAT": - length -= 4 - try: - ImageFile._safe_read(self.fp, length) - except OSError as e: - if ImageFile.LOAD_TRUNCATED_IMAGES: - break - else: - raise e - except AttributeError: - logger.debug("%r %s %s (unknown)", cid, pos, length) - s = ImageFile._safe_read(self.fp, length) - if cid[1:2].islower(): - self.private_chunks.append((cid, s, True)) - self._text = self.png.im_text - if not self.is_animated: - self.png.close() - self.png = None - else: - if self._prev_im and self.blend_op == Blend.OP_OVER: - updated = self._crop(self.im, self.dispose_extent) - if self.im.mode == "RGB" and "transparency" in self.info: - mask = updated.convert_transparent( - "RGBA", self.info["transparency"] - ) - else: - if self.im.mode == "P" and "transparency" in self.info: - t = self.info["transparency"] - if isinstance(t, bytes): - updated.putpalettealphas(t) - elif isinstance(t, int): - updated.putpalettealpha(t) - mask = updated.convert("RGBA") - self._prev_im.paste(updated, self.dispose_extent, mask) - self.im = self._prev_im - - def _getexif(self) -> dict[int, Any] | None: - if "exif" not in self.info: - self.load() - if "exif" not in self.info and "Raw profile type exif" not in self.info: - return None - return self.getexif()._get_merged_dict() - - def getexif(self) -> Image.Exif: - if "exif" not in self.info: - self.load() - - return super().getexif() - - -# -------------------------------------------------------------------- -# PNG writer - -_OUTMODES = { - # supported PIL modes, and corresponding rawmode, bit depth and color type - "1": ("1", b"\x01", b"\x00"), - "L;1": ("L;1", b"\x01", b"\x00"), - "L;2": ("L;2", b"\x02", b"\x00"), - "L;4": ("L;4", b"\x04", b"\x00"), - "L": ("L", b"\x08", b"\x00"), - "LA": ("LA", b"\x08", b"\x04"), - "I": ("I;16B", b"\x10", b"\x00"), - "I;16": ("I;16B", b"\x10", b"\x00"), - "I;16B": ("I;16B", b"\x10", b"\x00"), - "P;1": ("P;1", b"\x01", b"\x03"), - "P;2": ("P;2", b"\x02", b"\x03"), - "P;4": ("P;4", b"\x04", b"\x03"), - "P": ("P", b"\x08", b"\x03"), - "RGB": ("RGB", b"\x08", b"\x02"), - "RGBA": ("RGBA", b"\x08", b"\x06"), -} - - -def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None: - """Write a PNG chunk (including CRC field)""" - - byte_data = b"".join(data) - - fp.write(o32(len(byte_data)) + cid) - fp.write(byte_data) - crc = _crc32(byte_data, _crc32(cid)) - fp.write(o32(crc)) - - -class _idat: - # wrap output from the encoder in IDAT chunks - - def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None: - self.fp = fp - self.chunk = chunk - - def write(self, data: bytes) -> None: - self.chunk(self.fp, b"IDAT", data) - - -class _fdat: - # wrap encoder output in fdAT chunks - - def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None: - self.fp = fp - self.chunk = chunk - self.seq_num = seq_num - - def write(self, data: bytes) -> None: - self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) - self.seq_num += 1 - - -class _Frame(NamedTuple): - im: Image.Image - bbox: tuple[int, int, int, int] | None - encoderinfo: dict[str, Any] - - -def _write_multiple_frames( - im: Image.Image, - fp: IO[bytes], - chunk: Callable[..., None], - mode: str, - rawmode: str, - default_image: Image.Image | None, - append_images: list[Image.Image], -) -> Image.Image | None: - duration = im.encoderinfo.get("duration") - loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) - disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) - blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) - - if default_image: - chain = itertools.chain(append_images) - else: - chain = itertools.chain([im], append_images) - - im_frames: list[_Frame] = [] - frame_count = 0 - for im_seq in chain: - for im_frame in ImageSequence.Iterator(im_seq): - if im_frame.mode == mode: - im_frame = im_frame.copy() - else: - im_frame = im_frame.convert(mode) - encoderinfo = im.encoderinfo.copy() - if isinstance(duration, (list, tuple)): - encoderinfo["duration"] = duration[frame_count] - elif duration is None and "duration" in im_frame.info: - encoderinfo["duration"] = im_frame.info["duration"] - if isinstance(disposal, (list, tuple)): - encoderinfo["disposal"] = disposal[frame_count] - if isinstance(blend, (list, tuple)): - encoderinfo["blend"] = blend[frame_count] - frame_count += 1 - - if im_frames: - previous = im_frames[-1] - prev_disposal = previous.encoderinfo.get("disposal") - prev_blend = previous.encoderinfo.get("blend") - if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2: - prev_disposal = Disposal.OP_BACKGROUND - - if prev_disposal == Disposal.OP_BACKGROUND: - base_im = previous.im.copy() - dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) - bbox = previous.bbox - if bbox: - dispose = dispose.crop(bbox) - else: - bbox = (0, 0) + im.size - base_im.paste(dispose, bbox) - elif prev_disposal == Disposal.OP_PREVIOUS: - base_im = im_frames[-2].im - else: - base_im = previous.im - delta = ImageChops.subtract_modulo( - im_frame.convert("RGBA"), base_im.convert("RGBA") - ) - bbox = delta.getbbox(alpha_only=False) - if ( - not bbox - and prev_disposal == encoderinfo.get("disposal") - and prev_blend == encoderinfo.get("blend") - and "duration" in encoderinfo - ): - previous.encoderinfo["duration"] += encoderinfo["duration"] - continue - else: - bbox = None - im_frames.append(_Frame(im_frame, bbox, encoderinfo)) - - if len(im_frames) == 1 and not default_image: - return im_frames[0].im - - # animation control - chunk( - fp, - b"acTL", - o32(len(im_frames)), # 0: num_frames - o32(loop), # 4: num_plays - ) - - # default image IDAT (if it exists) - if default_image: - if im.mode != mode: - im = im.convert(mode) - ImageFile._save( - im, - cast(IO[bytes], _idat(fp, chunk)), - [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)], - ) - - seq_num = 0 - for frame, frame_data in enumerate(im_frames): - im_frame = frame_data.im - if not frame_data.bbox: - bbox = (0, 0) + im_frame.size - else: - bbox = frame_data.bbox - im_frame = im_frame.crop(bbox) - size = im_frame.size - encoderinfo = frame_data.encoderinfo - frame_duration = int(round(encoderinfo.get("duration", 0))) - frame_disposal = encoderinfo.get("disposal", disposal) - frame_blend = encoderinfo.get("blend", blend) - # frame control - chunk( - fp, - b"fcTL", - o32(seq_num), # sequence_number - o32(size[0]), # width - o32(size[1]), # height - o32(bbox[0]), # x_offset - o32(bbox[1]), # y_offset - o16(frame_duration), # delay_numerator - o16(1000), # delay_denominator - o8(frame_disposal), # dispose_op - o8(frame_blend), # blend_op - ) - seq_num += 1 - # frame data - if frame == 0 and not default_image: - # first frame must be in IDAT chunks for backwards compatibility - ImageFile._save( - im_frame, - cast(IO[bytes], _idat(fp, chunk)), - [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], - ) - else: - fdat_chunks = _fdat(fp, chunk, seq_num) - ImageFile._save( - im_frame, - cast(IO[bytes], fdat_chunks), - [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], - ) - seq_num = fdat_chunks.seq_num - return None - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, save_all=True) - - -def _save( - im: Image.Image, - fp: IO[bytes], - filename: str | bytes, - chunk: Callable[..., None] = putchunk, - save_all: bool = False, -) -> None: - # save an image to disk (called by the save method) - - if save_all: - default_image = im.encoderinfo.get( - "default_image", im.info.get("default_image") - ) - modes = set() - sizes = set() - append_images = im.encoderinfo.get("append_images", []) - for im_seq in itertools.chain([im], append_images): - for im_frame in ImageSequence.Iterator(im_seq): - modes.add(im_frame.mode) - sizes.add(im_frame.size) - for mode in ("RGBA", "RGB", "P"): - if mode in modes: - break - else: - mode = modes.pop() - size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2)) - else: - size = im.size - mode = im.mode - - outmode = mode - if mode == "P": - # - # attempt to minimize storage requirements for palette images - if "bits" in im.encoderinfo: - # number of bits specified by user - colors = min(1 << im.encoderinfo["bits"], 256) - else: - # check palette contents - if im.palette: - colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1) - else: - colors = 256 - - if colors <= 16: - if colors <= 2: - bits = 1 - elif colors <= 4: - bits = 2 - else: - bits = 4 - outmode += f";{bits}" - - # encoder options - im.encoderconfig = ( - im.encoderinfo.get("optimize", False), - im.encoderinfo.get("compress_level", -1), - im.encoderinfo.get("compress_type", -1), - im.encoderinfo.get("dictionary", b""), - ) - - # get the corresponding PNG mode - try: - rawmode, bit_depth, color_type = _OUTMODES[outmode] - except KeyError as e: - msg = f"cannot write mode {mode} as PNG" - raise OSError(msg) from e - if outmode == "I": - deprecate("Saving I mode images as PNG", 13, stacklevel=4) - - # - # write minimal PNG file - - fp.write(_MAGIC) - - chunk( - fp, - b"IHDR", - o32(size[0]), # 0: size - o32(size[1]), - bit_depth, - color_type, - b"\0", # 10: compression - b"\0", # 11: filter category - b"\0", # 12: interlace flag - ) - - chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"] - - icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) - if icc: - # ICC profile - # according to PNG spec, the iCCP chunk contains: - # Profile name 1-79 bytes (character string) - # Null separator 1 byte (null character) - # Compression method 1 byte (0) - # Compressed profile n bytes (zlib with deflate compression) - name = b"ICC Profile" - data = name + b"\0\0" + zlib.compress(icc) - chunk(fp, b"iCCP", data) - - # You must either have sRGB or iCCP. - # Disallow sRGB chunks when an iCCP-chunk has been emitted. - chunks.remove(b"sRGB") - - info = im.encoderinfo.get("pnginfo") - if info: - chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] - for info_chunk in info.chunks: - cid, data = info_chunk[:2] - if cid in chunks: - chunks.remove(cid) - chunk(fp, cid, data) - elif cid in chunks_multiple_allowed: - chunk(fp, cid, data) - elif cid[1:2].islower(): - # Private chunk - after_idat = len(info_chunk) == 3 and info_chunk[2] - if not after_idat: - chunk(fp, cid, data) - - if im.mode == "P": - palette_byte_number = colors * 3 - palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] - while len(palette_bytes) < palette_byte_number: - palette_bytes += b"\0" - chunk(fp, b"PLTE", palette_bytes) - - transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None)) - - if transparency or transparency == 0: - if im.mode == "P": - # limit to actual palette size - alpha_bytes = colors - if isinstance(transparency, bytes): - chunk(fp, b"tRNS", transparency[:alpha_bytes]) - else: - transparency = max(0, min(255, transparency)) - alpha = b"\xff" * transparency + b"\0" - chunk(fp, b"tRNS", alpha[:alpha_bytes]) - elif im.mode in ("1", "L", "I", "I;16"): - transparency = max(0, min(65535, transparency)) - chunk(fp, b"tRNS", o16(transparency)) - elif im.mode == "RGB": - red, green, blue = transparency - chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) - else: - if "transparency" in im.encoderinfo: - # don't bother with transparency if it's an RGBA - # and it's in the info dict. It's probably just stale. - msg = "cannot use transparency for this mode" - raise OSError(msg) - else: - if im.mode == "P" and im.im.getpalettemode() == "RGBA": - alpha = im.im.getpalette("RGBA", "A") - alpha_bytes = colors - chunk(fp, b"tRNS", alpha[:alpha_bytes]) - - dpi = im.encoderinfo.get("dpi") - if dpi: - chunk( - fp, - b"pHYs", - o32(int(dpi[0] / 0.0254 + 0.5)), - o32(int(dpi[1] / 0.0254 + 0.5)), - b"\x01", - ) - - if info: - chunks = [b"bKGD", b"hIST"] - for info_chunk in info.chunks: - cid, data = info_chunk[:2] - if cid in chunks: - chunks.remove(cid) - chunk(fp, cid, data) - - exif = im.encoderinfo.get("exif") - if exif: - if isinstance(exif, Image.Exif): - exif = exif.tobytes(8) - if exif.startswith(b"Exif\x00\x00"): - exif = exif[6:] - chunk(fp, b"eXIf", exif) - - single_im: Image.Image | None = im - if save_all: - single_im = _write_multiple_frames( - im, fp, chunk, mode, rawmode, default_image, append_images - ) - if single_im: - ImageFile._save( - single_im, - cast(IO[bytes], _idat(fp, chunk)), - [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)], - ) - - if info: - for info_chunk in info.chunks: - cid, data = info_chunk[:2] - if cid[1:2].islower(): - # Private chunk - after_idat = len(info_chunk) == 3 and info_chunk[2] - if after_idat: - chunk(fp, cid, data) - - chunk(fp, b"IEND", b"") - - if hasattr(fp, "flush"): - fp.flush() - - -# -------------------------------------------------------------------- -# PNG chunk converter - - -def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]: - """Return a list of PNG chunks representing this image.""" - from io import BytesIO - - chunks = [] - - def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None: - byte_data = b"".join(data) - crc = o32(_crc32(byte_data, _crc32(cid))) - chunks.append((cid, byte_data, crc)) - - fp = BytesIO() - - try: - im.encoderinfo = params - _save(im, fp, "", append) - finally: - del im.encoderinfo - - return chunks - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(PngImageFile.format, PngImageFile, _accept) -Image.register_save(PngImageFile.format, _save) -Image.register_save_all(PngImageFile.format, _save_all) - -Image.register_extensions(PngImageFile.format, [".png", ".apng"]) - -Image.register_mime(PngImageFile.format, "image/png") diff --git a/python_modules/PIL/PpmImagePlugin.py b/python_modules/PIL/PpmImagePlugin.py deleted file mode 100644 index db34d107a..000000000 --- a/python_modules/PIL/PpmImagePlugin.py +++ /dev/null @@ -1,375 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PPM support for PIL -# -# History: -# 96-03-24 fl Created -# 98-03-06 fl Write RGBA images (as RGB, that is) -# -# Copyright (c) Secret Labs AB 1997-98. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import math -from typing import IO - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import o8 -from ._binary import o32le as o32 - -# -# -------------------------------------------------------------------- - -b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" - -MODES = { - # standard - b"P1": "1", - b"P2": "L", - b"P3": "RGB", - b"P4": "1", - b"P5": "L", - b"P6": "RGB", - # extensions - b"P0CMYK": "CMYK", - b"Pf": "F", - # PIL extensions (for test purposes only) - b"PyP": "P", - b"PyRGBA": "RGBA", - b"PyCMYK": "CMYK", -} - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"P") and prefix[1] in b"0123456fy" - - -## -# Image plugin for PBM, PGM, and PPM images. - - -class PpmImageFile(ImageFile.ImageFile): - format = "PPM" - format_description = "Pbmplus image" - - def _read_magic(self) -> bytes: - assert self.fp is not None - - magic = b"" - # read until whitespace or longest available magic number - for _ in range(6): - c = self.fp.read(1) - if not c or c in b_whitespace: - break - magic += c - return magic - - def _read_token(self) -> bytes: - assert self.fp is not None - - token = b"" - while len(token) <= 10: # read until next whitespace or limit of 10 characters - c = self.fp.read(1) - if not c: - break - elif c in b_whitespace: # token ended - if not token: - # skip whitespace at start - continue - break - elif c == b"#": - # ignores rest of the line; stops at CR, LF or EOF - while self.fp.read(1) not in b"\r\n": - pass - continue - token += c - if not token: - # Token was not even 1 byte - msg = "Reached EOF while reading header" - raise ValueError(msg) - elif len(token) > 10: - msg_too_long = b"Token too long in file header: %s" % token - raise ValueError(msg_too_long) - return token - - def _open(self) -> None: - assert self.fp is not None - - magic_number = self._read_magic() - try: - mode = MODES[magic_number] - except KeyError: - msg = "not a PPM file" - raise SyntaxError(msg) - self._mode = mode - - if magic_number in (b"P1", b"P4"): - self.custom_mimetype = "image/x-portable-bitmap" - elif magic_number in (b"P2", b"P5"): - self.custom_mimetype = "image/x-portable-graymap" - elif magic_number in (b"P3", b"P6"): - self.custom_mimetype = "image/x-portable-pixmap" - - self._size = int(self._read_token()), int(self._read_token()) - - decoder_name = "raw" - if magic_number in (b"P1", b"P2", b"P3"): - decoder_name = "ppm_plain" - - args: str | tuple[str | int, ...] - if mode == "1": - args = "1;I" - elif mode == "F": - scale = float(self._read_token()) - if scale == 0.0 or not math.isfinite(scale): - msg = "scale must be finite and non-zero" - raise ValueError(msg) - self.info["scale"] = abs(scale) - - rawmode = "F;32F" if scale < 0 else "F;32BF" - args = (rawmode, 0, -1) - else: - maxval = int(self._read_token()) - if not 0 < maxval < 65536: - msg = "maxval must be greater than 0 and less than 65536" - raise ValueError(msg) - if maxval > 255 and mode == "L": - self._mode = "I" - - rawmode = mode - if decoder_name != "ppm_plain": - # If maxval matches a bit depth, use the raw decoder directly - if maxval == 65535 and mode == "L": - rawmode = "I;16B" - elif maxval != 255: - decoder_name = "ppm" - - args = rawmode if decoder_name == "raw" else (rawmode, maxval) - self.tile = [ - ImageFile._Tile(decoder_name, (0, 0) + self.size, self.fp.tell(), args) - ] - - -# -# -------------------------------------------------------------------- - - -class PpmPlainDecoder(ImageFile.PyDecoder): - _pulls_fd = True - _comment_spans: bool - - def _read_block(self) -> bytes: - assert self.fd is not None - - return self.fd.read(ImageFile.SAFEBLOCK) - - def _find_comment_end(self, block: bytes, start: int = 0) -> int: - a = block.find(b"\n", start) - b = block.find(b"\r", start) - return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) - - def _ignore_comments(self, block: bytes) -> bytes: - if self._comment_spans: - # Finish current comment - while block: - comment_end = self._find_comment_end(block) - if comment_end != -1: - # Comment ends in this block - # Delete tail of comment - block = block[comment_end + 1 :] - break - else: - # Comment spans whole block - # So read the next block, looking for the end - block = self._read_block() - - # Search for any further comments - self._comment_spans = False - while True: - comment_start = block.find(b"#") - if comment_start == -1: - # No comment found - break - comment_end = self._find_comment_end(block, comment_start) - if comment_end != -1: - # Comment ends in this block - # Delete comment - block = block[:comment_start] + block[comment_end + 1 :] - else: - # Comment continues to next block(s) - block = block[:comment_start] - self._comment_spans = True - break - return block - - def _decode_bitonal(self) -> bytearray: - """ - This is a separate method because in the plain PBM format, all data tokens are - exactly one byte, so the inter-token whitespace is optional. - """ - data = bytearray() - total_bytes = self.state.xsize * self.state.ysize - - while len(data) != total_bytes: - block = self._read_block() # read next block - if not block: - # eof - break - - block = self._ignore_comments(block) - - tokens = b"".join(block.split()) - for token in tokens: - if token not in (48, 49): - msg = b"Invalid token for this mode: %s" % bytes([token]) - raise ValueError(msg) - data = (data + tokens)[:total_bytes] - invert = bytes.maketrans(b"01", b"\xff\x00") - return data.translate(invert) - - def _decode_blocks(self, maxval: int) -> bytearray: - data = bytearray() - max_len = 10 - out_byte_count = 4 if self.mode == "I" else 1 - out_max = 65535 if self.mode == "I" else 255 - bands = Image.getmodebands(self.mode) - total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count - - half_token = b"" - while len(data) != total_bytes: - block = self._read_block() # read next block - if not block: - if half_token: - block = bytearray(b" ") # flush half_token - else: - # eof - break - - block = self._ignore_comments(block) - - if half_token: - block = half_token + block # stitch half_token to new block - half_token = b"" - - tokens = block.split() - - if block and not block[-1:].isspace(): # block might split token - half_token = tokens.pop() # save half token for later - if len(half_token) > max_len: # prevent buildup of half_token - msg = ( - b"Token too long found in data: %s" % half_token[: max_len + 1] - ) - raise ValueError(msg) - - for token in tokens: - if len(token) > max_len: - msg = b"Token too long found in data: %s" % token[: max_len + 1] - raise ValueError(msg) - value = int(token) - if value < 0: - msg_str = f"Channel value is negative: {value}" - raise ValueError(msg_str) - if value > maxval: - msg_str = f"Channel value too large for this mode: {value}" - raise ValueError(msg_str) - value = round(value / maxval * out_max) - data += o32(value) if self.mode == "I" else o8(value) - if len(data) == total_bytes: # finished! - break - return data - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - self._comment_spans = False - if self.mode == "1": - data = self._decode_bitonal() - rawmode = "1;8" - else: - maxval = self.args[-1] - data = self._decode_blocks(maxval) - rawmode = "I;32" if self.mode == "I" else self.mode - self.set_as_raw(bytes(data), rawmode) - return -1, 0 - - -class PpmDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - - data = bytearray() - maxval = self.args[-1] - in_byte_count = 1 if maxval < 256 else 2 - out_byte_count = 4 if self.mode == "I" else 1 - out_max = 65535 if self.mode == "I" else 255 - bands = Image.getmodebands(self.mode) - dest_length = self.state.xsize * self.state.ysize * bands * out_byte_count - while len(data) < dest_length: - pixels = self.fd.read(in_byte_count * bands) - if len(pixels) < in_byte_count * bands: - # eof - break - for b in range(bands): - value = ( - pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count) - ) - value = min(out_max, round(value / maxval * out_max)) - data += o32(value) if self.mode == "I" else o8(value) - rawmode = "I;32" if self.mode == "I" else self.mode - self.set_as_raw(bytes(data), rawmode) - return -1, 0 - - -# -# -------------------------------------------------------------------- - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode == "1": - rawmode, head = "1;I", b"P4" - elif im.mode == "L": - rawmode, head = "L", b"P5" - elif im.mode in ("I", "I;16"): - rawmode, head = "I;16B", b"P5" - elif im.mode in ("RGB", "RGBA"): - rawmode, head = "RGB", b"P6" - elif im.mode == "F": - rawmode, head = "F;32F", b"Pf" - else: - msg = f"cannot write mode {im.mode} as PPM" - raise OSError(msg) - fp.write(head + b"\n%d %d\n" % im.size) - if head == b"P6": - fp.write(b"255\n") - elif head == b"P5": - if rawmode == "L": - fp.write(b"255\n") - else: - fp.write(b"65535\n") - elif head == b"Pf": - fp.write(b"-1.0\n") - row_order = -1 if im.mode == "F" else 1 - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))] - ) - - -# -# -------------------------------------------------------------------- - - -Image.register_open(PpmImageFile.format, PpmImageFile, _accept) -Image.register_save(PpmImageFile.format, _save) - -Image.register_decoder("ppm", PpmDecoder) -Image.register_decoder("ppm_plain", PpmPlainDecoder) - -Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"]) - -Image.register_mime(PpmImageFile.format, "image/x-portable-anymap") diff --git a/python_modules/PIL/PsdImagePlugin.py b/python_modules/PIL/PsdImagePlugin.py deleted file mode 100644 index f49aaeeb1..000000000 --- a/python_modules/PIL/PsdImagePlugin.py +++ /dev/null @@ -1,333 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# Adobe PSD 2.5/3.0 file handling -# -# History: -# 1995-09-01 fl Created -# 1997-01-03 fl Read most PSD images -# 1997-01-18 fl Fixed P and CMYK support -# 2001-10-21 fl Added seek/tell support (for layers) -# -# Copyright (c) 1997-2001 by Secret Labs AB. -# Copyright (c) 1995-2001 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -from functools import cached_property -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i8 -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import si16be as si16 -from ._binary import si32be as si32 -from ._util import DeferredError - -MODES = { - # (photoshop mode, bits) -> (pil mode, required channels) - (0, 1): ("1", 1), - (0, 8): ("L", 1), - (1, 8): ("L", 1), - (2, 8): ("P", 1), - (3, 8): ("RGB", 3), - (4, 8): ("CMYK", 4), - (7, 8): ("L", 1), # FIXME: multilayer - (8, 8): ("L", 1), # duotone - (9, 8): ("LAB", 3), -} - - -# --------------------------------------------------------------------. -# read PSD images - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"8BPS") - - -## -# Image plugin for Photoshop images. - - -class PsdImageFile(ImageFile.ImageFile): - format = "PSD" - format_description = "Adobe Photoshop" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - read = self.fp.read - - # - # header - - s = read(26) - if not _accept(s) or i16(s, 4) != 1: - msg = "not a PSD file" - raise SyntaxError(msg) - - psd_bits = i16(s, 22) - psd_channels = i16(s, 12) - psd_mode = i16(s, 24) - - mode, channels = MODES[(psd_mode, psd_bits)] - - if channels > psd_channels: - msg = "not enough channels" - raise OSError(msg) - if mode == "RGB" and psd_channels == 4: - mode = "RGBA" - channels = 4 - - self._mode = mode - self._size = i32(s, 18), i32(s, 14) - - # - # color mode data - - size = i32(read(4)) - if size: - data = read(size) - if mode == "P" and size == 768: - self.palette = ImagePalette.raw("RGB;L", data) - - # - # image resources - - self.resources = [] - - size = i32(read(4)) - if size: - # load resources - end = self.fp.tell() + size - while self.fp.tell() < end: - read(4) # signature - id = i16(read(2)) - name = read(i8(read(1))) - if not (len(name) & 1): - read(1) # padding - data = read(i32(read(4))) - if len(data) & 1: - read(1) # padding - self.resources.append((id, name, data)) - if id == 1039: # ICC profile - self.info["icc_profile"] = data - - # - # layer and mask information - - self._layers_position = None - - size = i32(read(4)) - if size: - end = self.fp.tell() + size - size = i32(read(4)) - if size: - self._layers_position = self.fp.tell() - self._layers_size = size - self.fp.seek(end) - self._n_frames: int | None = None - - # - # image descriptor - - self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels) - - # keep the file open - self._fp = self.fp - self.frame = 1 - self._min_frame = 1 - - @cached_property - def layers( - self, - ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: - layers = [] - if self._layers_position is not None: - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self._fp.seek(self._layers_position) - _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size)) - layers = _layerinfo(_layer_data, self._layers_size) - self._n_frames = len(layers) - return layers - - @property - def n_frames(self) -> int: - if self._n_frames is None: - self._n_frames = len(self.layers) - return self._n_frames - - @property - def is_animated(self) -> bool: - return len(self.layers) > 1 - - def seek(self, layer: int) -> None: - if not self._seek_check(layer): - return - if isinstance(self._fp, DeferredError): - raise self._fp.ex - - # seek to given layer (1..max) - _, mode, _, tile = self.layers[layer - 1] - self._mode = mode - self.tile = tile - self.frame = layer - self.fp = self._fp - - def tell(self) -> int: - # return layer number (0=image, 1..max=layers) - return self.frame - - -def _layerinfo( - fp: IO[bytes], ct_bytes: int -) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: - # read layerinfo block - layers = [] - - def read(size: int) -> bytes: - return ImageFile._safe_read(fp, size) - - ct = si16(read(2)) - - # sanity check - if ct_bytes < (abs(ct) * 20): - msg = "Layer block too short for number of layers requested" - raise SyntaxError(msg) - - for _ in range(abs(ct)): - # bounding box - y0 = si32(read(4)) - x0 = si32(read(4)) - y1 = si32(read(4)) - x1 = si32(read(4)) - - # image info - bands = [] - ct_types = i16(read(2)) - if ct_types > 4: - fp.seek(ct_types * 6 + 12, io.SEEK_CUR) - size = i32(read(4)) - fp.seek(size, io.SEEK_CUR) - continue - - for _ in range(ct_types): - type = i16(read(2)) - - if type == 65535: - b = "A" - else: - b = "RGBA"[type] - - bands.append(b) - read(4) # size - - # figure out the image mode - bands.sort() - if bands == ["R"]: - mode = "L" - elif bands == ["B", "G", "R"]: - mode = "RGB" - elif bands == ["A", "B", "G", "R"]: - mode = "RGBA" - else: - mode = "" # unknown - - # skip over blend flags and extra information - read(12) # filler - name = "" - size = i32(read(4)) # length of the extra data field - if size: - data_end = fp.tell() + size - - length = i32(read(4)) - if length: - fp.seek(length - 16, io.SEEK_CUR) - - length = i32(read(4)) - if length: - fp.seek(length, io.SEEK_CUR) - - length = i8(read(1)) - if length: - # Don't know the proper encoding, - # Latin-1 should be a good guess - name = read(length).decode("latin-1", "replace") - - fp.seek(data_end) - layers.append((name, mode, (x0, y0, x1, y1))) - - # get tiles - layerinfo = [] - for i, (name, mode, bbox) in enumerate(layers): - tile = [] - for m in mode: - t = _maketile(fp, m, bbox, 1) - if t: - tile.extend(t) - layerinfo.append((name, mode, bbox, tile)) - - return layerinfo - - -def _maketile( - file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int -) -> list[ImageFile._Tile]: - tiles = [] - read = file.read - - compression = i16(read(2)) - - xsize = bbox[2] - bbox[0] - ysize = bbox[3] - bbox[1] - - offset = file.tell() - - if compression == 0: - # - # raw compression - for channel in range(channels): - layer = mode[channel] - if mode == "CMYK": - layer += ";I" - tiles.append(ImageFile._Tile("raw", bbox, offset, layer)) - offset = offset + xsize * ysize - - elif compression == 1: - # - # packbits compression - i = 0 - bytecount = read(channels * ysize * 2) - offset = file.tell() - for channel in range(channels): - layer = mode[channel] - if mode == "CMYK": - layer += ";I" - tiles.append(ImageFile._Tile("packbits", bbox, offset, layer)) - for y in range(ysize): - offset = offset + i16(bytecount, i) - i += 2 - - file.seek(offset) - - if offset & 1: - read(1) # padding - - return tiles - - -# -------------------------------------------------------------------- -# registry - - -Image.register_open(PsdImageFile.format, PsdImageFile, _accept) - -Image.register_extension(PsdImageFile.format, ".psd") - -Image.register_mime(PsdImageFile.format, "image/vnd.adobe.photoshop") diff --git a/python_modules/PIL/QoiImagePlugin.py b/python_modules/PIL/QoiImagePlugin.py deleted file mode 100644 index dba5d809f..000000000 --- a/python_modules/PIL/QoiImagePlugin.py +++ /dev/null @@ -1,234 +0,0 @@ -# -# The Python Imaging Library. -# -# QOI support for PIL -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import IO - -from . import Image, ImageFile -from ._binary import i32be as i32 -from ._binary import o8 -from ._binary import o32be as o32 - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"qoif") - - -class QoiImageFile(ImageFile.ImageFile): - format = "QOI" - format_description = "Quite OK Image" - - def _open(self) -> None: - if not _accept(self.fp.read(4)): - msg = "not a QOI file" - raise SyntaxError(msg) - - self._size = i32(self.fp.read(4)), i32(self.fp.read(4)) - - channels = self.fp.read(1)[0] - self._mode = "RGB" if channels == 3 else "RGBA" - - self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())] - - -class QoiDecoder(ImageFile.PyDecoder): - _pulls_fd = True - _previous_pixel: bytes | bytearray | None = None - _previously_seen_pixels: dict[int, bytes | bytearray] = {} - - def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: - self._previous_pixel = value - - r, g, b, a = value - hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 - self._previously_seen_pixels[hash_value] = value - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - - self._previously_seen_pixels = {} - self._previous_pixel = bytearray((0, 0, 0, 255)) - - data = bytearray() - bands = Image.getmodebands(self.mode) - dest_length = self.state.xsize * self.state.ysize * bands - while len(data) < dest_length: - byte = self.fd.read(1)[0] - value: bytes | bytearray - if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB - value = bytearray(self.fd.read(3)) + self._previous_pixel[3:] - elif byte == 0b11111111: # QOI_OP_RGBA - value = self.fd.read(4) - else: - op = byte >> 6 - if op == 0: # QOI_OP_INDEX - op_index = byte & 0b00111111 - value = self._previously_seen_pixels.get( - op_index, bytearray((0, 0, 0, 0)) - ) - elif op == 1 and self._previous_pixel: # QOI_OP_DIFF - value = bytearray( - ( - (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) - % 256, - (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2) - % 256, - (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256, - self._previous_pixel[3], - ) - ) - elif op == 2 and self._previous_pixel: # QOI_OP_LUMA - second_byte = self.fd.read(1)[0] - diff_green = (byte & 0b00111111) - 32 - diff_red = ((second_byte & 0b11110000) >> 4) - 8 - diff_blue = (second_byte & 0b00001111) - 8 - - value = bytearray( - tuple( - (self._previous_pixel[i] + diff_green + diff) % 256 - for i, diff in enumerate((diff_red, 0, diff_blue)) - ) - ) - value += self._previous_pixel[3:] - elif op == 3 and self._previous_pixel: # QOI_OP_RUN - run_length = (byte & 0b00111111) + 1 - value = self._previous_pixel - if bands == 3: - value = value[:3] - data += value * run_length - continue - self._add_to_previous_pixels(value) - - if bands == 3: - value = value[:3] - data += value - self.set_as_raw(data) - return -1, 0 - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode == "RGB": - channels = 3 - elif im.mode == "RGBA": - channels = 4 - else: - msg = "Unsupported QOI image mode" - raise ValueError(msg) - - colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1 - - fp.write(b"qoif") - fp.write(o32(im.size[0])) - fp.write(o32(im.size[1])) - fp.write(o8(channels)) - fp.write(o8(colorspace)) - - ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)]) - - -class QoiEncoder(ImageFile.PyEncoder): - _pushes_fd = True - _previous_pixel: tuple[int, int, int, int] | None = None - _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {} - _run = 0 - - def _write_run(self) -> bytes: - data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN - self._run = 0 - return data - - def _delta(self, left: int, right: int) -> int: - result = (left - right) & 255 - if result >= 128: - result -= 256 - return result - - def encode(self, bufsize: int) -> tuple[int, int, bytes]: - assert self.im is not None - - self._previously_seen_pixels = {0: (0, 0, 0, 0)} - self._previous_pixel = (0, 0, 0, 255) - - data = bytearray() - w, h = self.im.size - bands = Image.getmodebands(self.mode) - - for y in range(h): - for x in range(w): - pixel = self.im.getpixel((x, y)) - if bands == 3: - pixel = (*pixel, 255) - - if pixel == self._previous_pixel: - self._run += 1 - if self._run == 62: - data += self._write_run() - else: - if self._run: - data += self._write_run() - - r, g, b, a = pixel - hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 - if self._previously_seen_pixels.get(hash_value) == pixel: - data += o8(hash_value) # QOI_OP_INDEX - elif self._previous_pixel: - self._previously_seen_pixels[hash_value] = pixel - - prev_r, prev_g, prev_b, prev_a = self._previous_pixel - if prev_a == a: - delta_r = self._delta(r, prev_r) - delta_g = self._delta(g, prev_g) - delta_b = self._delta(b, prev_b) - - if ( - -2 <= delta_r < 2 - and -2 <= delta_g < 2 - and -2 <= delta_b < 2 - ): - data += o8( - 0b01000000 - | (delta_r + 2) << 4 - | (delta_g + 2) << 2 - | (delta_b + 2) - ) # QOI_OP_DIFF - else: - delta_gr = self._delta(delta_r, delta_g) - delta_gb = self._delta(delta_b, delta_g) - if ( - -8 <= delta_gr < 8 - and -32 <= delta_g < 32 - and -8 <= delta_gb < 8 - ): - data += o8( - 0b10000000 | (delta_g + 32) - ) # QOI_OP_LUMA - data += o8((delta_gr + 8) << 4 | (delta_gb + 8)) - else: - data += o8(0b11111110) # QOI_OP_RGB - data += bytes(pixel[:3]) - else: - data += o8(0b11111111) # QOI_OP_RGBA - data += bytes(pixel) - - self._previous_pixel = pixel - - if self._run: - data += self._write_run() - data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding - - return len(data), 0, data - - -Image.register_open(QoiImageFile.format, QoiImageFile, _accept) -Image.register_decoder("qoi", QoiDecoder) -Image.register_extension(QoiImageFile.format, ".qoi") - -Image.register_save(QoiImageFile.format, _save) -Image.register_encoder("qoi", QoiEncoder) diff --git a/python_modules/PIL/SgiImagePlugin.py b/python_modules/PIL/SgiImagePlugin.py deleted file mode 100644 index 853022150..000000000 --- a/python_modules/PIL/SgiImagePlugin.py +++ /dev/null @@ -1,231 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# SGI image file handling -# -# See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli. -# -# -# -# History: -# 2017-22-07 mb Add RLE decompression -# 2016-16-10 mb Add save method without compression -# 1995-09-10 fl Created -# -# Copyright (c) 2016 by Mickael Bonfill. -# Copyright (c) 2008 by Karsten Hiddemann. -# Copyright (c) 1997 by Secret Labs AB. -# Copyright (c) 1995 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -import struct -from typing import IO - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import o8 - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 2 and i16(prefix) == 474 - - -MODES = { - (1, 1, 1): "L", - (1, 2, 1): "L", - (2, 1, 1): "L;16B", - (2, 2, 1): "L;16B", - (1, 3, 3): "RGB", - (2, 3, 3): "RGB;16B", - (1, 3, 4): "RGBA", - (2, 3, 4): "RGBA;16B", -} - - -## -# Image plugin for SGI images. -class SgiImageFile(ImageFile.ImageFile): - format = "SGI" - format_description = "SGI Image File Format" - - def _open(self) -> None: - # HEAD - assert self.fp is not None - - headlen = 512 - s = self.fp.read(headlen) - - if not _accept(s): - msg = "Not an SGI image file" - raise ValueError(msg) - - # compression : verbatim or RLE - compression = s[2] - - # bpc : 1 or 2 bytes (8bits or 16bits) - bpc = s[3] - - # dimension : 1, 2 or 3 (depending on xsize, ysize and zsize) - dimension = i16(s, 4) - - # xsize : width - xsize = i16(s, 6) - - # ysize : height - ysize = i16(s, 8) - - # zsize : channels count - zsize = i16(s, 10) - - # determine mode from bits/zsize - try: - rawmode = MODES[(bpc, dimension, zsize)] - except KeyError: - msg = "Unsupported SGI image mode" - raise ValueError(msg) - - self._size = xsize, ysize - self._mode = rawmode.split(";")[0] - if self.mode == "RGB": - self.custom_mimetype = "image/rgb" - - # orientation -1 : scanlines begins at the bottom-left corner - orientation = -1 - - # decoder info - if compression == 0: - pagesize = xsize * ysize * bpc - if bpc == 2: - self.tile = [ - ImageFile._Tile( - "SGI16", - (0, 0) + self.size, - headlen, - (self.mode, 0, orientation), - ) - ] - else: - self.tile = [] - offset = headlen - for layer in self.mode: - self.tile.append( - ImageFile._Tile( - "raw", (0, 0) + self.size, offset, (layer, 0, orientation) - ) - ) - offset += pagesize - elif compression == 1: - self.tile = [ - ImageFile._Tile( - "sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc) - ) - ] - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode not in {"RGB", "RGBA", "L"}: - msg = "Unsupported SGI image mode" - raise ValueError(msg) - - # Get the keyword arguments - info = im.encoderinfo - - # Byte-per-pixel precision, 1 = 8bits per pixel - bpc = info.get("bpc", 1) - - if bpc not in (1, 2): - msg = "Unsupported number of bytes per pixel" - raise ValueError(msg) - - # Flip the image, since the origin of SGI file is the bottom-left corner - orientation = -1 - # Define the file as SGI File Format - magic_number = 474 - # Run-Length Encoding Compression - Unsupported at this time - rle = 0 - - # X Dimension = width / Y Dimension = height - x, y = im.size - # Z Dimension: Number of channels - z = len(im.mode) - # Number of dimensions (x,y,z) - if im.mode == "L": - dimension = 1 if y == 1 else 2 - else: - dimension = 3 - - # Minimum Byte value - pinmin = 0 - # Maximum Byte value (255 = 8bits per pixel) - pinmax = 255 - # Image name (79 characters max, truncated below in write) - img_name = os.path.splitext(os.path.basename(filename))[0] - if isinstance(img_name, str): - img_name = img_name.encode("ascii", "ignore") - # Standard representation of pixel in the file - colormap = 0 - fp.write(struct.pack(">h", magic_number)) - fp.write(o8(rle)) - fp.write(o8(bpc)) - fp.write(struct.pack(">H", dimension)) - fp.write(struct.pack(">H", x)) - fp.write(struct.pack(">H", y)) - fp.write(struct.pack(">H", z)) - fp.write(struct.pack(">l", pinmin)) - fp.write(struct.pack(">l", pinmax)) - fp.write(struct.pack("4s", b"")) # dummy - fp.write(struct.pack("79s", img_name)) # truncates to 79 chars - fp.write(struct.pack("s", b"")) # force null byte after img_name - fp.write(struct.pack(">l", colormap)) - fp.write(struct.pack("404s", b"")) # dummy - - rawmode = "L" - if bpc == 2: - rawmode = "L;16B" - - for channel in im.split(): - fp.write(channel.tobytes("raw", rawmode, 0, orientation)) - - if hasattr(fp, "flush"): - fp.flush() - - -class SGI16Decoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - assert self.im is not None - - rawmode, stride, orientation = self.args - pagesize = self.state.xsize * self.state.ysize - zsize = len(self.mode) - self.fd.seek(512) - - for band in range(zsize): - channel = Image.new("L", (self.state.xsize, self.state.ysize)) - channel.frombytes( - self.fd.read(2 * pagesize), "raw", "L;16B", stride, orientation - ) - self.im.putband(channel.im, band) - - return -1, 0 - - -# -# registry - - -Image.register_decoder("SGI16", SGI16Decoder) -Image.register_open(SgiImageFile.format, SgiImageFile, _accept) -Image.register_save(SgiImageFile.format, _save) -Image.register_mime(SgiImageFile.format, "image/sgi") - -Image.register_extensions(SgiImageFile.format, [".bw", ".rgb", ".rgba", ".sgi"]) - -# End of file diff --git a/python_modules/PIL/SpiderImagePlugin.py b/python_modules/PIL/SpiderImagePlugin.py deleted file mode 100644 index 868019e80..000000000 --- a/python_modules/PIL/SpiderImagePlugin.py +++ /dev/null @@ -1,331 +0,0 @@ -# -# The Python Imaging Library. -# -# SPIDER image file handling -# -# History: -# 2004-08-02 Created BB -# 2006-03-02 added save method -# 2006-03-13 added support for stack images -# -# Copyright (c) 2004 by Health Research Inc. (HRI) RENSSELAER, NY 12144. -# Copyright (c) 2004 by William Baxter. -# Copyright (c) 2004 by Secret Labs AB. -# Copyright (c) 2004 by Fredrik Lundh. -# - -## -# Image plugin for the Spider image format. This format is used -# by the SPIDER software, in processing image data from electron -# microscopy and tomography. -## - -# -# SpiderImagePlugin.py -# -# The Spider image format is used by SPIDER software, in processing -# image data from electron microscopy and tomography. -# -# Spider home page: -# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html -# -# Details about the Spider image format: -# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html -# -from __future__ import annotations - -import os -import struct -import sys -from typing import IO, Any, cast - -from . import Image, ImageFile -from ._util import DeferredError - -TYPE_CHECKING = False - - -def isInt(f: Any) -> int: - try: - i = int(f) - if f - i == 0: - return 1 - else: - return 0 - except (ValueError, OverflowError): - return 0 - - -iforms = [1, 3, -11, -12, -21, -22] - - -# There is no magic number to identify Spider files, so just check a -# series of header locations to see if they have reasonable values. -# Returns no. of bytes in the header, if it is a valid Spider header, -# otherwise returns 0 - - -def isSpiderHeader(t: tuple[float, ...]) -> int: - h = (99,) + t # add 1 value so can use spider header index start=1 - # header values 1,2,5,12,13,22,23 should be integers - for i in [1, 2, 5, 12, 13, 22, 23]: - if not isInt(h[i]): - return 0 - # check iform - iform = int(h[5]) - if iform not in iforms: - return 0 - # check other header values - labrec = int(h[13]) # no. records in file header - labbyt = int(h[22]) # total no. of bytes in header - lenbyt = int(h[23]) # record length in bytes - if labbyt != (labrec * lenbyt): - return 0 - # looks like a valid header - return labbyt - - -def isSpiderImage(filename: str) -> int: - with open(filename, "rb") as fp: - f = fp.read(92) # read 23 * 4 bytes - t = struct.unpack(">23f", f) # try big-endian first - hdrlen = isSpiderHeader(t) - if hdrlen == 0: - t = struct.unpack("<23f", f) # little-endian - hdrlen = isSpiderHeader(t) - return hdrlen - - -class SpiderImageFile(ImageFile.ImageFile): - format = "SPIDER" - format_description = "Spider 2D image" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # check header - n = 27 * 4 # read 27 float values - f = self.fp.read(n) - - try: - self.bigendian = 1 - t = struct.unpack(">27f", f) # try big-endian first - hdrlen = isSpiderHeader(t) - if hdrlen == 0: - self.bigendian = 0 - t = struct.unpack("<27f", f) # little-endian - hdrlen = isSpiderHeader(t) - if hdrlen == 0: - msg = "not a valid Spider file" - raise SyntaxError(msg) - except struct.error as e: - msg = "not a valid Spider file" - raise SyntaxError(msg) from e - - h = (99,) + t # add 1 value : spider header index starts at 1 - iform = int(h[5]) - if iform != 1: - msg = "not a Spider 2D image" - raise SyntaxError(msg) - - self._size = int(h[12]), int(h[2]) # size in pixels (width, height) - self.istack = int(h[24]) - self.imgnumber = int(h[27]) - - if self.istack == 0 and self.imgnumber == 0: - # stk=0, img=0: a regular 2D image - offset = hdrlen - self._nimages = 1 - elif self.istack > 0 and self.imgnumber == 0: - # stk>0, img=0: Opening the stack for the first time - self.imgbytes = int(h[12]) * int(h[2]) * 4 - self.hdrlen = hdrlen - self._nimages = int(h[26]) - # Point to the first image in the stack - offset = hdrlen * 2 - self.imgnumber = 1 - elif self.istack == 0 and self.imgnumber > 0: - # stk=0, img>0: an image within the stack - offset = hdrlen + self.stkoffset - self.istack = 2 # So Image knows it's still a stack - else: - msg = "inconsistent stack header values" - raise SyntaxError(msg) - - if self.bigendian: - self.rawmode = "F;32BF" - else: - self.rawmode = "F;32F" - self._mode = "F" - - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)] - self._fp = self.fp # FIXME: hack - - @property - def n_frames(self) -> int: - return self._nimages - - @property - def is_animated(self) -> bool: - return self._nimages > 1 - - # 1st image index is zero (although SPIDER imgnumber starts at 1) - def tell(self) -> int: - if self.imgnumber < 1: - return 0 - else: - return self.imgnumber - 1 - - def seek(self, frame: int) -> None: - if self.istack == 0: - msg = "attempt to seek in a non-stack file" - raise EOFError(msg) - if not self._seek_check(frame): - return - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) - self.fp = self._fp - self.fp.seek(self.stkoffset) - self._open() - - # returns a byte image after rescaling to 0..255 - def convert2byte(self, depth: int = 255) -> Image.Image: - extrema = self.getextrema() - assert isinstance(extrema[0], float) - minimum, maximum = cast(tuple[float, float], extrema) - m: float = 1 - if maximum != minimum: - m = depth / (maximum - minimum) - b = -m * minimum - return self.point(lambda i: i * m + b).convert("L") - - if TYPE_CHECKING: - from . import ImageTk - - # returns a ImageTk.PhotoImage object, after rescaling to 0..255 - def tkPhotoImage(self) -> ImageTk.PhotoImage: - from . import ImageTk - - return ImageTk.PhotoImage(self.convert2byte(), palette=256) - - -# -------------------------------------------------------------------- -# Image series - - -# given a list of filenames, return a list of images -def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None: - """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" - if filelist is None or len(filelist) < 1: - return None - - byte_imgs = [] - for img in filelist: - if not os.path.exists(img): - print(f"unable to find {img}") - continue - try: - with Image.open(img) as im: - assert isinstance(im, SpiderImageFile) - byte_im = im.convert2byte() - except Exception: - if not isSpiderImage(img): - print(f"{img} is not a Spider image file") - continue - byte_im.info["filename"] = img - byte_imgs.append(byte_im) - return byte_imgs - - -# -------------------------------------------------------------------- -# For saving images in Spider format - - -def makeSpiderHeader(im: Image.Image) -> list[bytes]: - nsam, nrow = im.size - lenbyt = nsam * 4 # There are labrec records in the header - labrec = int(1024 / lenbyt) - if 1024 % lenbyt != 0: - labrec += 1 - labbyt = labrec * lenbyt - nvalues = int(labbyt / 4) - if nvalues < 23: - return [] - - hdr = [0.0] * nvalues - - # NB these are Fortran indices - hdr[1] = 1.0 # nslice (=1 for an image) - hdr[2] = float(nrow) # number of rows per slice - hdr[3] = float(nrow) # number of records in the image - hdr[5] = 1.0 # iform for 2D image - hdr[12] = float(nsam) # number of pixels per line - hdr[13] = float(labrec) # number of records in file header - hdr[22] = float(labbyt) # total number of bytes in header - hdr[23] = float(lenbyt) # record length in bytes - - # adjust for Fortran indexing - hdr = hdr[1:] - hdr.append(0.0) - # pack binary data into a string - return [struct.pack("f", v) for v in hdr] - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode != "F": - im = im.convert("F") - - hdr = makeSpiderHeader(im) - if len(hdr) < 256: - msg = "Error creating Spider header" - raise OSError(msg) - - # write the SPIDER header - fp.writelines(hdr) - - rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) - - -def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # get the filename extension and register it with Image - filename_ext = os.path.splitext(filename)[1] - ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext - Image.register_extension(SpiderImageFile.format, ext) - _save(im, fp, filename) - - -# -------------------------------------------------------------------- - - -Image.register_open(SpiderImageFile.format, SpiderImageFile) -Image.register_save(SpiderImageFile.format, _save_spider) - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") - sys.exit() - - filename = sys.argv[1] - if not isSpiderImage(filename): - print("input image must be in Spider format") - sys.exit() - - with Image.open(filename) as im: - print(f"image: {im}") - print(f"format: {im.format}") - print(f"size: {im.size}") - print(f"mode: {im.mode}") - print("max, min: ", end=" ") - print(im.getextrema()) - - if len(sys.argv) > 2: - outfile = sys.argv[2] - - # perform some image operation - im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - print( - f"saving a flipped version of {os.path.basename(filename)} " - f"as {outfile} " - ) - im.save(outfile, SpiderImageFile.format) diff --git a/python_modules/PIL/SunImagePlugin.py b/python_modules/PIL/SunImagePlugin.py deleted file mode 100644 index 8912379ea..000000000 --- a/python_modules/PIL/SunImagePlugin.py +++ /dev/null @@ -1,145 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Sun image file handling -# -# History: -# 1995-09-10 fl Created -# 1996-05-28 fl Fixed 32-bit alignment -# 1998-12-29 fl Import ImagePalette module -# 2001-12-18 fl Fixed palette loading (from Jean-Claude Rimbault) -# -# Copyright (c) 1997-2001 by Secret Labs AB -# Copyright (c) 1995-1996 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile, ImagePalette -from ._binary import i32be as i32 - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 - - -## -# Image plugin for Sun raster files. - - -class SunImageFile(ImageFile.ImageFile): - format = "SUN" - format_description = "Sun Raster File" - - def _open(self) -> None: - # The Sun Raster file header is 32 bytes in length - # and has the following format: - - # typedef struct _SunRaster - # { - # DWORD MagicNumber; /* Magic (identification) number */ - # DWORD Width; /* Width of image in pixels */ - # DWORD Height; /* Height of image in pixels */ - # DWORD Depth; /* Number of bits per pixel */ - # DWORD Length; /* Size of image data in bytes */ - # DWORD Type; /* Type of raster file */ - # DWORD ColorMapType; /* Type of color map */ - # DWORD ColorMapLength; /* Size of the color map in bytes */ - # } SUNRASTER; - - assert self.fp is not None - - # HEAD - s = self.fp.read(32) - if not _accept(s): - msg = "not an SUN raster file" - raise SyntaxError(msg) - - offset = 32 - - self._size = i32(s, 4), i32(s, 8) - - depth = i32(s, 12) - # data_length = i32(s, 16) # unreliable, ignore. - file_type = i32(s, 20) - palette_type = i32(s, 24) # 0: None, 1: RGB, 2: Raw/arbitrary - palette_length = i32(s, 28) - - if depth == 1: - self._mode, rawmode = "1", "1;I" - elif depth == 4: - self._mode, rawmode = "L", "L;4" - elif depth == 8: - self._mode = rawmode = "L" - elif depth == 24: - if file_type == 3: - self._mode, rawmode = "RGB", "RGB" - else: - self._mode, rawmode = "RGB", "BGR" - elif depth == 32: - if file_type == 3: - self._mode, rawmode = "RGB", "RGBX" - else: - self._mode, rawmode = "RGB", "BGRX" - else: - msg = "Unsupported Mode/Bit Depth" - raise SyntaxError(msg) - - if palette_length: - if palette_length > 1024: - msg = "Unsupported Color Palette Length" - raise SyntaxError(msg) - - if palette_type != 1: - msg = "Unsupported Palette Type" - raise SyntaxError(msg) - - offset = offset + palette_length - self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) - if self.mode == "L": - self._mode = "P" - rawmode = rawmode.replace("L", "P") - - # 16 bit boundaries on stride - stride = ((self.size[0] * depth + 15) // 16) * 2 - - # file type: Type is the version (or flavor) of the bitmap - # file. The following values are typically found in the Type - # field: - # 0000h Old - # 0001h Standard - # 0002h Byte-encoded - # 0003h RGB format - # 0004h TIFF format - # 0005h IFF format - # FFFFh Experimental - - # Old and standard are the same, except for the length tag. - # byte-encoded is run-length-encoded - # RGB looks similar to standard, but RGB byte order - # TIFF and IFF mean that they were converted from T/IFF - # Experimental means that it's something else. - # (https://www.fileformat.info/format/sunraster/egff.htm) - - if file_type in (0, 1, 3, 4, 5): - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride)) - ] - elif file_type == 2: - self.tile = [ - ImageFile._Tile("sun_rle", (0, 0) + self.size, offset, rawmode) - ] - else: - msg = "Unsupported Sun Raster file type" - raise SyntaxError(msg) - - -# -# registry - - -Image.register_open(SunImageFile.format, SunImageFile, _accept) - -Image.register_extension(SunImageFile.format, ".ras") diff --git a/python_modules/PIL/TarIO.py b/python_modules/PIL/TarIO.py deleted file mode 100644 index 86490a496..000000000 --- a/python_modules/PIL/TarIO.py +++ /dev/null @@ -1,61 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# read files from within a tar file -# -# History: -# 95-06-18 fl Created -# 96-05-28 fl Open files in binary mode -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1995-96. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io - -from . import ContainerIO - - -class TarIO(ContainerIO.ContainerIO[bytes]): - """A file object that provides read access to a given member of a TAR file.""" - - def __init__(self, tarfile: str, file: str) -> None: - """ - Create file object. - - :param tarfile: Name of TAR file. - :param file: Name of member file. - """ - self.fh = open(tarfile, "rb") - - while True: - s = self.fh.read(512) - if len(s) != 512: - self.fh.close() - - msg = "unexpected end of tar file" - raise OSError(msg) - - name = s[:100].decode("utf-8") - i = name.find("\0") - if i == 0: - self.fh.close() - - msg = "cannot find subfile" - raise OSError(msg) - if i > 0: - name = name[:i] - - size = int(s[124:135], 8) - - if file == name: - break - - self.fh.seek((size + 511) & (~511), io.SEEK_CUR) - - # Open region - super().__init__(self.fh, self.fh.tell(), size) diff --git a/python_modules/PIL/TgaImagePlugin.py b/python_modules/PIL/TgaImagePlugin.py deleted file mode 100644 index 90d5b5cf4..000000000 --- a/python_modules/PIL/TgaImagePlugin.py +++ /dev/null @@ -1,264 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# TGA file handling -# -# History: -# 95-09-01 fl created (reads 24-bit files only) -# 97-01-04 fl support more TGA versions, including compressed images -# 98-07-04 fl fixed orientation and alpha layer bugs -# 98-09-11 fl fixed orientation for runlength decoder -# -# Copyright (c) Secret Labs AB 1997-98. -# Copyright (c) Fredrik Lundh 1995-97. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import warnings -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i16le as i16 -from ._binary import o8 -from ._binary import o16le as o16 - -# -# -------------------------------------------------------------------- -# Read RGA file - - -MODES = { - # map imagetype/depth to rawmode - (1, 8): "P", - (3, 1): "1", - (3, 8): "L", - (3, 16): "LA", - (2, 16): "BGRA;15Z", - (2, 24): "BGR", - (2, 32): "BGRA", -} - - -## -# Image plugin for Targa files. - - -class TgaImageFile(ImageFile.ImageFile): - format = "TGA" - format_description = "Targa" - - def _open(self) -> None: - # process header - assert self.fp is not None - - s = self.fp.read(18) - - id_len = s[0] - - colormaptype = s[1] - imagetype = s[2] - - depth = s[16] - - flags = s[17] - - self._size = i16(s, 12), i16(s, 14) - - # validate header fields - if ( - colormaptype not in (0, 1) - or self.size[0] <= 0 - or self.size[1] <= 0 - or depth not in (1, 8, 16, 24, 32) - ): - msg = "not a TGA file" - raise SyntaxError(msg) - - # image mode - if imagetype in (3, 11): - self._mode = "L" - if depth == 1: - self._mode = "1" # ??? - elif depth == 16: - self._mode = "LA" - elif imagetype in (1, 9): - self._mode = "P" if colormaptype else "L" - elif imagetype in (2, 10): - self._mode = "RGB" if depth == 24 else "RGBA" - else: - msg = "unknown TGA mode" - raise SyntaxError(msg) - - # orientation - orientation = flags & 0x30 - self._flip_horizontally = orientation in [0x10, 0x30] - if orientation in [0x20, 0x30]: - orientation = 1 - elif orientation in [0, 0x10]: - orientation = -1 - else: - msg = "unknown TGA orientation" - raise SyntaxError(msg) - - self.info["orientation"] = orientation - - if imagetype & 8: - self.info["compression"] = "tga_rle" - - if id_len: - self.info["id_section"] = self.fp.read(id_len) - - if colormaptype: - # read palette - start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] - if mapdepth == 16: - self.palette = ImagePalette.raw( - "BGRA;15Z", bytes(2 * start) + self.fp.read(2 * size) - ) - self.palette.mode = "RGBA" - elif mapdepth == 24: - self.palette = ImagePalette.raw( - "BGR", bytes(3 * start) + self.fp.read(3 * size) - ) - elif mapdepth == 32: - self.palette = ImagePalette.raw( - "BGRA", bytes(4 * start) + self.fp.read(4 * size) - ) - else: - msg = "unknown TGA map depth" - raise SyntaxError(msg) - - # setup tile descriptor - try: - rawmode = MODES[(imagetype & 7, depth)] - if imagetype & 8: - # compressed - self.tile = [ - ImageFile._Tile( - "tga_rle", - (0, 0) + self.size, - self.fp.tell(), - (rawmode, orientation, depth), - ) - ] - else: - self.tile = [ - ImageFile._Tile( - "raw", - (0, 0) + self.size, - self.fp.tell(), - (rawmode, 0, orientation), - ) - ] - except KeyError: - pass # cannot decode - - def load_end(self) -> None: - if self._flip_horizontally: - self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - - -# -# -------------------------------------------------------------------- -# Write TGA file - - -SAVE = { - "1": ("1", 1, 0, 3), - "L": ("L", 8, 0, 3), - "LA": ("LA", 16, 0, 3), - "P": ("P", 8, 1, 1), - "RGB": ("BGR", 24, 0, 2), - "RGBA": ("BGRA", 32, 0, 2), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - rawmode, bits, colormaptype, imagetype = SAVE[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as TGA" - raise OSError(msg) from e - - if "rle" in im.encoderinfo: - rle = im.encoderinfo["rle"] - else: - compression = im.encoderinfo.get("compression", im.info.get("compression")) - rle = compression == "tga_rle" - if rle: - imagetype += 8 - - id_section = im.encoderinfo.get("id_section", im.info.get("id_section", "")) - id_len = len(id_section) - if id_len > 255: - id_len = 255 - id_section = id_section[:255] - warnings.warn("id_section has been trimmed to 255 characters") - - if colormaptype: - palette = im.im.getpalette("RGB", "BGR") - colormaplength, colormapentry = len(palette) // 3, 24 - else: - colormaplength, colormapentry = 0, 0 - - if im.mode in ("LA", "RGBA"): - flags = 8 - else: - flags = 0 - - orientation = im.encoderinfo.get("orientation", im.info.get("orientation", -1)) - if orientation > 0: - flags = flags | 0x20 - - fp.write( - o8(id_len) - + o8(colormaptype) - + o8(imagetype) - + o16(0) # colormapfirst - + o16(colormaplength) - + o8(colormapentry) - + o16(0) - + o16(0) - + o16(im.size[0]) - + o16(im.size[1]) - + o8(bits) - + o8(flags) - ) - - if id_section: - fp.write(id_section) - - if colormaptype: - fp.write(palette) - - if rle: - ImageFile._save( - im, - fp, - [ImageFile._Tile("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))], - ) - else: - ImageFile._save( - im, - fp, - [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))], - ) - - # write targa version 2 footer - fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000") - - -# -# -------------------------------------------------------------------- -# Registry - - -Image.register_open(TgaImageFile.format, TgaImageFile) -Image.register_save(TgaImageFile.format, _save) - -Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"]) - -Image.register_mime(TgaImageFile.format, "image/x-tga") diff --git a/python_modules/PIL/TiffImagePlugin.py b/python_modules/PIL/TiffImagePlugin.py deleted file mode 100644 index daf20f2e8..000000000 --- a/python_modules/PIL/TiffImagePlugin.py +++ /dev/null @@ -1,2339 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# TIFF file handling -# -# TIFF is a flexible, if somewhat aged, image file format originally -# defined by Aldus. Although TIFF supports a wide variety of pixel -# layouts and compression methods, the name doesn't really stand for -# "thousands of incompatible file formats," it just feels that way. -# -# To read TIFF data from a stream, the stream must be seekable. For -# progressive decoding, make sure to use TIFF files where the tag -# directory is placed first in the file. -# -# History: -# 1995-09-01 fl Created -# 1996-05-04 fl Handle JPEGTABLES tag -# 1996-05-18 fl Fixed COLORMAP support -# 1997-01-05 fl Fixed PREDICTOR support -# 1997-08-27 fl Added support for rational tags (from Perry Stoll) -# 1998-01-10 fl Fixed seek/tell (from Jan Blom) -# 1998-07-15 fl Use private names for internal variables -# 1999-06-13 fl Rewritten for PIL 1.0 (1.0) -# 2000-10-11 fl Additional fixes for Python 2.0 (1.1) -# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2) -# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3) -# 2001-12-18 fl Added workaround for broken Matrox library -# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart) -# 2003-05-19 fl Check FILLORDER tag -# 2003-09-26 fl Added RGBa support -# 2004-02-24 fl Added DPI support; fixed rational write support -# 2005-02-07 fl Added workaround for broken Corel Draw 10 files -# 2006-01-09 fl Added support for float/double tags (from Russell Nelson) -# -# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import itertools -import logging -import math -import os -import struct -import warnings -from collections.abc import Iterator, MutableMapping -from fractions import Fraction -from numbers import Number, Rational -from typing import IO, Any, Callable, NoReturn, cast - -from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import o8 -from ._deprecate import deprecate -from ._typing import StrOrBytesPath -from ._util import DeferredError, is_path -from .TiffTags import TYPES - -TYPE_CHECKING = False -if TYPE_CHECKING: - from ._typing import Buffer, IntegralLike - -logger = logging.getLogger(__name__) - -# Set these to true to force use of libtiff for reading or writing. -READ_LIBTIFF = False -WRITE_LIBTIFF = False -STRIP_SIZE = 65536 - -II = b"II" # little-endian (Intel style) -MM = b"MM" # big-endian (Motorola style) - -# -# -------------------------------------------------------------------- -# Read TIFF files - -# a few tag names, just to make the code below a bit more readable -OSUBFILETYPE = 255 -IMAGEWIDTH = 256 -IMAGELENGTH = 257 -BITSPERSAMPLE = 258 -COMPRESSION = 259 -PHOTOMETRIC_INTERPRETATION = 262 -FILLORDER = 266 -IMAGEDESCRIPTION = 270 -STRIPOFFSETS = 273 -SAMPLESPERPIXEL = 277 -ROWSPERSTRIP = 278 -STRIPBYTECOUNTS = 279 -X_RESOLUTION = 282 -Y_RESOLUTION = 283 -PLANAR_CONFIGURATION = 284 -RESOLUTION_UNIT = 296 -TRANSFERFUNCTION = 301 -SOFTWARE = 305 -DATE_TIME = 306 -ARTIST = 315 -PREDICTOR = 317 -COLORMAP = 320 -TILEWIDTH = 322 -TILELENGTH = 323 -TILEOFFSETS = 324 -TILEBYTECOUNTS = 325 -SUBIFD = 330 -EXTRASAMPLES = 338 -SAMPLEFORMAT = 339 -JPEGTABLES = 347 -YCBCRSUBSAMPLING = 530 -REFERENCEBLACKWHITE = 532 -COPYRIGHT = 33432 -IPTC_NAA_CHUNK = 33723 # newsphoto properties -PHOTOSHOP_CHUNK = 34377 # photoshop properties -ICCPROFILE = 34675 -EXIFIFD = 34665 -XMP = 700 -JPEGQUALITY = 65537 # pseudo-tag by libtiff - -# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java -IMAGEJ_META_DATA_BYTE_COUNTS = 50838 -IMAGEJ_META_DATA = 50839 - -COMPRESSION_INFO = { - # Compression => pil compression name - 1: "raw", - 2: "tiff_ccitt", - 3: "group3", - 4: "group4", - 5: "tiff_lzw", - 6: "tiff_jpeg", # obsolete - 7: "jpeg", - 8: "tiff_adobe_deflate", - 32771: "tiff_raw_16", # 16-bit padding - 32773: "packbits", - 32809: "tiff_thunderscan", - 32946: "tiff_deflate", - 34676: "tiff_sgilog", - 34677: "tiff_sgilog24", - 34925: "lzma", - 50000: "zstd", - 50001: "webp", -} - -COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} - -OPEN_INFO = { - # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, - # ExtraSamples) => mode, rawmode - (II, 0, (1,), 1, (1,), ()): ("1", "1;I"), - (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), - (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), - (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), - (II, 1, (1,), 1, (1,), ()): ("1", "1"), - (MM, 1, (1,), 1, (1,), ()): ("1", "1"), - (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), - (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), - (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), - (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), - (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), - (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), - (II, 1, (1,), 1, (2,), ()): ("L", "L;2"), - (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), - (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), - (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), - (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), - (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), - (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), - (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), - (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), - (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), - (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), - (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), - (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), - (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), - (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), - (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), - (II, 1, (1,), 1, (8,), ()): ("L", "L"), - (MM, 1, (1,), 1, (8,), ()): ("L", "L"), - (II, 1, (2,), 1, (8,), ()): ("L", "L"), - (MM, 1, (2,), 1, (8,), ()): ("L", "L"), - (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), - (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), - (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), - (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), - (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), - (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), - (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), - (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), - (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), - (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), - (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), - (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), - (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), - (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), - (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), - (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), - (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), - (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), - (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), - (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), - (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), - (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), - (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), - (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), - (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), - (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), - (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), - (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), - (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), - (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), - (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), - (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), - (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), - (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), - (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), - (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), - (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), - (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), - (II, 3, (1,), 1, (4,), ()): ("P", "P;4"), - (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), - (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), - (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), - (II, 3, (1,), 1, (8,), ()): ("P", "P"), - (MM, 3, (1,), 1, (8,), ()): ("P", "P"), - (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), - (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), - (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), - (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), - (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), - (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), - (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), - (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), - (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), - (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), - (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), - (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), - (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"), - (II, 6, (1,), 1, (8,), ()): ("L", "L"), - (MM, 6, (1,), 1, (8,), ()): ("L", "L"), - # JPEG compressed images handled by LibTiff and auto-converted to RGBX - # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel - (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), - (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), - (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), - (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), -} - -MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) - -PREFIXES = [ - b"MM\x00\x2a", # Valid TIFF header with big-endian byte order - b"II\x2a\x00", # Valid TIFF header with little-endian byte order - b"MM\x2a\x00", # Invalid TIFF header, assume big-endian - b"II\x00\x2a", # Invalid TIFF header, assume little-endian - b"MM\x00\x2b", # BigTIFF with big-endian byte order - b"II\x2b\x00", # BigTIFF with little-endian byte order -] - -if not getattr(Image.core, "libtiff_support_custom_tags", True): - deprecate("Support for LibTIFF earlier than version 4", 12) - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(tuple(PREFIXES)) - - -def _limit_rational( - val: float | Fraction | IFDRational, max_val: int -) -> tuple[IntegralLike, IntegralLike]: - inv = abs(val) > 1 - n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) - return n_d[::-1] if inv else n_d - - -def _limit_signed_rational( - val: IFDRational, max_val: int, min_val: int -) -> tuple[IntegralLike, IntegralLike]: - frac = Fraction(val) - n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator - - if min(float(i) for i in n_d) < min_val: - n_d = _limit_rational(val, abs(min_val)) - - n_d_float = tuple(float(i) for i in n_d) - if max(n_d_float) > max_val: - n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val) - - return n_d - - -## -# Wrapper for TIFF IFDs. - -_load_dispatch = {} -_write_dispatch = {} - - -def _delegate(op: str) -> Any: - def delegate( - self: IFDRational, *args: tuple[float, ...] - ) -> bool | float | Fraction: - return getattr(self._val, op)(*args) - - return delegate - - -class IFDRational(Rational): - """Implements a rational class where 0/0 is a legal value to match - the in the wild use of exif rationals. - - e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used - """ - - """ If the denominator is 0, store this as a float('nan'), otherwise store - as a fractions.Fraction(). Delegate as appropriate - - """ - - __slots__ = ("_numerator", "_denominator", "_val") - - def __init__( - self, value: float | Fraction | IFDRational, denominator: int = 1 - ) -> None: - """ - :param value: either an integer numerator, a - float/rational/other number, or an IFDRational - :param denominator: Optional integer denominator - """ - self._val: Fraction | float - if isinstance(value, IFDRational): - self._numerator = value.numerator - self._denominator = value.denominator - self._val = value._val - return - - if isinstance(value, Fraction): - self._numerator = value.numerator - self._denominator = value.denominator - else: - if TYPE_CHECKING: - self._numerator = cast(IntegralLike, value) - else: - self._numerator = value - self._denominator = denominator - - if denominator == 0: - self._val = float("nan") - elif denominator == 1: - self._val = Fraction(value) - elif int(value) == value: - self._val = Fraction(int(value), denominator) - else: - self._val = Fraction(value / denominator) - - @property - def numerator(self) -> IntegralLike: - return self._numerator - - @property - def denominator(self) -> int: - return self._denominator - - def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]: - """ - - :param max_denominator: Integer, the maximum denominator value - :returns: Tuple of (numerator, denominator) - """ - - if self.denominator == 0: - return self.numerator, self.denominator - - assert isinstance(self._val, Fraction) - f = self._val.limit_denominator(max_denominator) - return f.numerator, f.denominator - - def __repr__(self) -> str: - return str(float(self._val)) - - def __hash__(self) -> int: # type: ignore[override] - return self._val.__hash__() - - def __eq__(self, other: object) -> bool: - val = self._val - if isinstance(other, IFDRational): - other = other._val - if isinstance(other, float): - val = float(val) - return val == other - - def __getstate__(self) -> list[float | Fraction | IntegralLike]: - return [self._val, self._numerator, self._denominator] - - def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None: - IFDRational.__init__(self, 0) - _val, _numerator, _denominator = state - assert isinstance(_val, (float, Fraction)) - self._val = _val - if TYPE_CHECKING: - self._numerator = cast(IntegralLike, _numerator) - else: - self._numerator = _numerator - assert isinstance(_denominator, int) - self._denominator = _denominator - - """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', - 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', - 'mod','rmod', 'pow','rpow', 'pos', 'neg', - 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', - 'ceil', 'floor', 'round'] - print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) - """ - - __add__ = _delegate("__add__") - __radd__ = _delegate("__radd__") - __sub__ = _delegate("__sub__") - __rsub__ = _delegate("__rsub__") - __mul__ = _delegate("__mul__") - __rmul__ = _delegate("__rmul__") - __truediv__ = _delegate("__truediv__") - __rtruediv__ = _delegate("__rtruediv__") - __floordiv__ = _delegate("__floordiv__") - __rfloordiv__ = _delegate("__rfloordiv__") - __mod__ = _delegate("__mod__") - __rmod__ = _delegate("__rmod__") - __pow__ = _delegate("__pow__") - __rpow__ = _delegate("__rpow__") - __pos__ = _delegate("__pos__") - __neg__ = _delegate("__neg__") - __abs__ = _delegate("__abs__") - __trunc__ = _delegate("__trunc__") - __lt__ = _delegate("__lt__") - __gt__ = _delegate("__gt__") - __le__ = _delegate("__le__") - __ge__ = _delegate("__ge__") - __bool__ = _delegate("__bool__") - __ceil__ = _delegate("__ceil__") - __floor__ = _delegate("__floor__") - __round__ = _delegate("__round__") - # Python >= 3.11 - if hasattr(Fraction, "__int__"): - __int__ = _delegate("__int__") - - -_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] - - -def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]: - def decorator(func: _LoaderFunc) -> _LoaderFunc: - from .TiffTags import TYPES - - if func.__name__.startswith("load_"): - TYPES[idx] = func.__name__[5:].replace("_", " ") - _load_dispatch[idx] = size, func # noqa: F821 - return func - - return decorator - - -def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]: - def decorator(func: Callable[..., Any]) -> Callable[..., Any]: - _write_dispatch[idx] = func # noqa: F821 - return func - - return decorator - - -def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None: - from .TiffTags import TYPES - - idx, fmt, name = idx_fmt_name - TYPES[idx] = name - size = struct.calcsize(f"={fmt}") - - def basic_handler( - self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True - ) -> tuple[Any, ...]: - return self._unpack(f"{len(data) // size}{fmt}", data) - - _load_dispatch[idx] = size, basic_handler # noqa: F821 - _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 - b"".join(self._pack(fmt, value) for value in values) - ) - - -if TYPE_CHECKING: - _IFDv2Base = MutableMapping[int, Any] -else: - _IFDv2Base = MutableMapping - - -class ImageFileDirectory_v2(_IFDv2Base): - """This class represents a TIFF tag directory. To speed things up, we - don't decode tags unless they're asked for. - - Exposes a dictionary interface of the tags in the directory:: - - ifd = ImageFileDirectory_v2() - ifd[key] = 'Some Data' - ifd.tagtype[key] = TiffTags.ASCII - print(ifd[key]) - 'Some Data' - - Individual values are returned as the strings or numbers, sequences are - returned as tuples of the values. - - The tiff metadata type of each item is stored in a dictionary of - tag types in - :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types - are read from a tiff file, guessed from the type added, or added - manually. - - Data Structures: - - * ``self.tagtype = {}`` - - * Key: numerical TIFF tag number - * Value: integer corresponding to the data type from - :py:data:`.TiffTags.TYPES` - - .. versionadded:: 3.0.0 - - 'Internal' data structures: - - * ``self._tags_v2 = {}`` - - * Key: numerical TIFF tag number - * Value: decoded data, as tuple for multiple values - - * ``self._tagdata = {}`` - - * Key: numerical TIFF tag number - * Value: undecoded byte string from file - - * ``self._tags_v1 = {}`` - - * Key: numerical TIFF tag number - * Value: decoded data in the v1 format - - Tags will be found in the private attributes ``self._tagdata``, and in - ``self._tags_v2`` once decoded. - - ``self.legacy_api`` is a value for internal use, and shouldn't be changed - from outside code. In cooperation with - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api`` - is true, then decoded tags will be populated into both ``_tags_v1`` and - ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF - save routine. Tags should be read from ``_tags_v1`` if - ``legacy_api == true``. - - """ - - _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {} - _write_dispatch: dict[int, Callable[..., Any]] = {} - - def __init__( - self, - ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00", - prefix: bytes | None = None, - group: int | None = None, - ) -> None: - """Initialize an ImageFileDirectory. - - To construct an ImageFileDirectory from a real file, pass the 8-byte - magic header to the constructor. To only set the endianness, pass it - as the 'prefix' keyword argument. - - :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets - endianness. - :param prefix: Override the endianness of the file. - """ - if not _accept(ifh): - msg = f"not a TIFF file (header {repr(ifh)} not valid)" - raise SyntaxError(msg) - self._prefix = prefix if prefix is not None else ifh[:2] - if self._prefix == MM: - self._endian = ">" - elif self._prefix == II: - self._endian = "<" - else: - msg = "not a TIFF IFD" - raise SyntaxError(msg) - self._bigtiff = ifh[2] == 43 - self.group = group - self.tagtype: dict[int, int] = {} - """ Dictionary of tag types """ - self.reset() - self.next = ( - self._unpack("Q", ifh[8:])[0] - if self._bigtiff - else self._unpack("L", ifh[4:])[0] - ) - self._legacy_api = False - - prefix = property(lambda self: self._prefix) - offset = property(lambda self: self._offset) - - @property - def legacy_api(self) -> bool: - return self._legacy_api - - @legacy_api.setter - def legacy_api(self, value: bool) -> NoReturn: - msg = "Not allowing setting of legacy api" - raise Exception(msg) - - def reset(self) -> None: - self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false - self._tags_v2: dict[int, Any] = {} # main tag storage - self._tagdata: dict[int, bytes] = {} - self.tagtype = {} # added 2008-06-05 by Florian Hoech - self._next = None - self._offset: int | None = None - - def __str__(self) -> str: - return str(dict(self)) - - def named(self) -> dict[str, Any]: - """ - :returns: dict of name|key: value - - Returns the complete tag dictionary, with named tags where possible. - """ - return { - TiffTags.lookup(code, self.group).name: value - for code, value in self.items() - } - - def __len__(self) -> int: - return len(set(self._tagdata) | set(self._tags_v2)) - - def __getitem__(self, tag: int) -> Any: - if tag not in self._tags_v2: # unpack on the fly - data = self._tagdata[tag] - typ = self.tagtype[tag] - size, handler = self._load_dispatch[typ] - self[tag] = handler(self, data, self.legacy_api) # check type - val = self._tags_v2[tag] - if self.legacy_api and not isinstance(val, (tuple, bytes)): - val = (val,) - return val - - def __contains__(self, tag: object) -> bool: - return tag in self._tags_v2 or tag in self._tagdata - - def __setitem__(self, tag: int, value: Any) -> None: - self._setitem(tag, value, self.legacy_api) - - def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: - basetypes = (Number, bytes, str) - - info = TiffTags.lookup(tag, self.group) - values = [value] if isinstance(value, basetypes) else value - - if tag not in self.tagtype: - if info.type: - self.tagtype[tag] = info.type - else: - self.tagtype[tag] = TiffTags.UNDEFINED - if all(isinstance(v, IFDRational) for v in values): - for v in values: - assert isinstance(v, IFDRational) - if v < 0: - self.tagtype[tag] = TiffTags.SIGNED_RATIONAL - break - else: - self.tagtype[tag] = TiffTags.RATIONAL - elif all(isinstance(v, int) for v in values): - short = True - signed_short = True - long = True - for v in values: - assert isinstance(v, int) - if short and not (0 <= v < 2**16): - short = False - if signed_short and not (-(2**15) < v < 2**15): - signed_short = False - if long and v < 0: - long = False - if short: - self.tagtype[tag] = TiffTags.SHORT - elif signed_short: - self.tagtype[tag] = TiffTags.SIGNED_SHORT - elif long: - self.tagtype[tag] = TiffTags.LONG - else: - self.tagtype[tag] = TiffTags.SIGNED_LONG - elif all(isinstance(v, float) for v in values): - self.tagtype[tag] = TiffTags.DOUBLE - elif all(isinstance(v, str) for v in values): - self.tagtype[tag] = TiffTags.ASCII - elif all(isinstance(v, bytes) for v in values): - self.tagtype[tag] = TiffTags.BYTE - - if self.tagtype[tag] == TiffTags.UNDEFINED: - values = [ - v.encode("ascii", "replace") if isinstance(v, str) else v - for v in values - ] - elif self.tagtype[tag] == TiffTags.RATIONAL: - values = [float(v) if isinstance(v, int) else v for v in values] - - is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) - if not is_ifd: - values = tuple( - info.cvt_enum(value) if isinstance(value, str) else value - for value in values - ) - - dest = self._tags_v1 if legacy_api else self._tags_v2 - - # Three branches: - # Spec'd length == 1, Actual length 1, store as element - # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. - # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. - # Don't mess with the legacy api, since it's frozen. - if not is_ifd and ( - (info.length == 1) - or self.tagtype[tag] == TiffTags.BYTE - or (info.length is None and len(values) == 1 and not legacy_api) - ): - # Don't mess with the legacy api, since it's frozen. - if legacy_api and self.tagtype[tag] in [ - TiffTags.RATIONAL, - TiffTags.SIGNED_RATIONAL, - ]: # rationals - values = (values,) - try: - (dest[tag],) = values - except ValueError: - # We've got a builtin tag with 1 expected entry - warnings.warn( - f"Metadata Warning, tag {tag} had too many entries: " - f"{len(values)}, expected 1" - ) - dest[tag] = values[0] - - else: - # Spec'd length > 1 or undefined - # Unspec'd, and length > 1 - dest[tag] = values - - def __delitem__(self, tag: int) -> None: - self._tags_v2.pop(tag, None) - self._tags_v1.pop(tag, None) - self._tagdata.pop(tag, None) - - def __iter__(self) -> Iterator[int]: - return iter(set(self._tagdata) | set(self._tags_v2)) - - def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]: - return struct.unpack(self._endian + fmt, data) - - def _pack(self, fmt: str, *values: Any) -> bytes: - return struct.pack(self._endian + fmt, *values) - - list( - map( - _register_basic, - [ - (TiffTags.SHORT, "H", "short"), - (TiffTags.LONG, "L", "long"), - (TiffTags.SIGNED_BYTE, "b", "signed byte"), - (TiffTags.SIGNED_SHORT, "h", "signed short"), - (TiffTags.SIGNED_LONG, "l", "signed long"), - (TiffTags.FLOAT, "f", "float"), - (TiffTags.DOUBLE, "d", "double"), - (TiffTags.IFD, "L", "long"), - (TiffTags.LONG8, "Q", "long8"), - ], - ) - ) - - @_register_loader(1, 1) # Basic type, except for the legacy API. - def load_byte(self, data: bytes, legacy_api: bool = True) -> bytes: - return data - - @_register_writer(1) # Basic type, except for the legacy API. - def write_byte(self, data: bytes | int | IFDRational) -> bytes: - if isinstance(data, IFDRational): - data = int(data) - if isinstance(data, int): - data = bytes((data,)) - return data - - @_register_loader(2, 1) - def load_string(self, data: bytes, legacy_api: bool = True) -> str: - if data.endswith(b"\0"): - data = data[:-1] - return data.decode("latin-1", "replace") - - @_register_writer(2) - def write_string(self, value: str | bytes | int) -> bytes: - # remerge of https://github.com/python-pillow/Pillow/pull/1416 - if isinstance(value, int): - value = str(value) - if not isinstance(value, bytes): - value = value.encode("ascii", "replace") - return value + b"\0" - - @_register_loader(5, 8) - def load_rational( - self, data: bytes, legacy_api: bool = True - ) -> tuple[tuple[int, int] | IFDRational, ...]: - vals = self._unpack(f"{len(data) // 4}L", data) - - def combine(a: int, b: int) -> tuple[int, int] | IFDRational: - return (a, b) if legacy_api else IFDRational(a, b) - - return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) - - @_register_writer(5) - def write_rational(self, *values: IFDRational) -> bytes: - return b"".join( - self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values - ) - - @_register_loader(7, 1) - def load_undefined(self, data: bytes, legacy_api: bool = True) -> bytes: - return data - - @_register_writer(7) - def write_undefined(self, value: bytes | int | IFDRational) -> bytes: - if isinstance(value, IFDRational): - value = int(value) - if isinstance(value, int): - value = str(value).encode("ascii", "replace") - return value - - @_register_loader(10, 8) - def load_signed_rational( - self, data: bytes, legacy_api: bool = True - ) -> tuple[tuple[int, int] | IFDRational, ...]: - vals = self._unpack(f"{len(data) // 4}l", data) - - def combine(a: int, b: int) -> tuple[int, int] | IFDRational: - return (a, b) if legacy_api else IFDRational(a, b) - - return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) - - @_register_writer(10) - def write_signed_rational(self, *values: IFDRational) -> bytes: - return b"".join( - self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) - for frac in values - ) - - def _ensure_read(self, fp: IO[bytes], size: int) -> bytes: - ret = fp.read(size) - if len(ret) != size: - msg = ( - "Corrupt EXIF data. " - f"Expecting to read {size} bytes but only got {len(ret)}. " - ) - raise OSError(msg) - return ret - - def load(self, fp: IO[bytes]) -> None: - self.reset() - self._offset = fp.tell() - - try: - tag_count = ( - self._unpack("Q", self._ensure_read(fp, 8)) - if self._bigtiff - else self._unpack("H", self._ensure_read(fp, 2)) - )[0] - for i in range(tag_count): - tag, typ, count, data = ( - self._unpack("HHQ8s", self._ensure_read(fp, 20)) - if self._bigtiff - else self._unpack("HHL4s", self._ensure_read(fp, 12)) - ) - - tagname = TiffTags.lookup(tag, self.group).name - typname = TYPES.get(typ, "unknown") - msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" - - try: - unit_size, handler = self._load_dispatch[typ] - except KeyError: - logger.debug("%s - unsupported type %s", msg, typ) - continue # ignore unsupported type - size = count * unit_size - if size > (8 if self._bigtiff else 4): - here = fp.tell() - (offset,) = self._unpack("Q" if self._bigtiff else "L", data) - msg += f" Tag Location: {here} - Data Location: {offset}" - fp.seek(offset) - data = ImageFile._safe_read(fp, size) - fp.seek(here) - else: - data = data[:size] - - if len(data) != size: - warnings.warn( - "Possibly corrupt EXIF data. " - f"Expecting to read {size} bytes but only got {len(data)}." - f" Skipping tag {tag}" - ) - logger.debug(msg) - continue - - if not data: - logger.debug(msg) - continue - - self._tagdata[tag] = data - self.tagtype[tag] = typ - - msg += " - value: " - msg += f"" if size > 32 else repr(data) - - logger.debug(msg) - - (self.next,) = ( - self._unpack("Q", self._ensure_read(fp, 8)) - if self._bigtiff - else self._unpack("L", self._ensure_read(fp, 4)) - ) - except OSError as msg: - warnings.warn(str(msg)) - return - - def _get_ifh(self) -> bytes: - ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42) - if self._bigtiff: - ifh += self._pack("HH", 8, 0) - ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8) - - return ifh - - def tobytes(self, offset: int = 0) -> bytes: - # FIXME What about tagdata? - result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2)) - - entries: list[tuple[int, int, int, bytes, bytes]] = [] - - fmt = "Q" if self._bigtiff else "L" - fmt_size = 8 if self._bigtiff else 4 - offset += ( - len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size - ) - stripoffsets = None - - # pass 1: convert tags to binary format - # always write tags in ascending order - for tag, value in sorted(self._tags_v2.items()): - if tag == STRIPOFFSETS: - stripoffsets = len(entries) - typ = self.tagtype[tag] - logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) - is_ifd = typ == TiffTags.LONG and isinstance(value, dict) - if is_ifd: - ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag) - values = self._tags_v2[tag] - for ifd_tag, ifd_value in values.items(): - ifd[ifd_tag] = ifd_value - data = ifd.tobytes(offset) - else: - values = value if isinstance(value, tuple) else (value,) - data = self._write_dispatch[typ](self, *values) - - tagname = TiffTags.lookup(tag, self.group).name - typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") - msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: " - msg += f"" if len(data) >= 16 else str(values) - logger.debug(msg) - - # count is sum of lengths for string and arbitrary data - if is_ifd: - count = 1 - elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: - count = len(data) - else: - count = len(values) - # figure out if data fits into the entry - if len(data) <= fmt_size: - entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b"")) - else: - entries.append((tag, typ, count, self._pack(fmt, offset), data)) - offset += (len(data) + 1) // 2 * 2 # pad to word - - # update strip offset data to point beyond auxiliary data - if stripoffsets is not None: - tag, typ, count, value, data = entries[stripoffsets] - if data: - size, handler = self._load_dispatch[typ] - values = [val + offset for val in handler(self, data, self.legacy_api)] - data = self._write_dispatch[typ](self, *values) - else: - value = self._pack(fmt, self._unpack(fmt, value)[0] + offset) - entries[stripoffsets] = tag, typ, count, value, data - - # pass 2: write entries to file - for tag, typ, count, value, data in entries: - logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) - result += self._pack( - "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value - ) - - # -- overwrite here for multi-page -- - result += self._pack(fmt, 0) # end of entries - - # pass 3: write auxiliary data to file - for tag, typ, count, value, data in entries: - result += data - if len(data) & 1: - result += b"\0" - - return result - - def save(self, fp: IO[bytes]) -> int: - if fp.tell() == 0: # skip TIFF header on subsequent pages - fp.write(self._get_ifh()) - - offset = fp.tell() - result = self.tobytes(offset) - fp.write(result) - return offset + len(result) - - -ImageFileDirectory_v2._load_dispatch = _load_dispatch -ImageFileDirectory_v2._write_dispatch = _write_dispatch -for idx, name in TYPES.items(): - name = name.replace(" ", "_") - setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1]) - setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx]) -del _load_dispatch, _write_dispatch, idx, name - - -# Legacy ImageFileDirectory support. -class ImageFileDirectory_v1(ImageFileDirectory_v2): - """This class represents the **legacy** interface to a TIFF tag directory. - - Exposes a dictionary interface of the tags in the directory:: - - ifd = ImageFileDirectory_v1() - ifd[key] = 'Some Data' - ifd.tagtype[key] = TiffTags.ASCII - print(ifd[key]) - ('Some Data',) - - Also contains a dictionary of tag types as read from the tiff image file, - :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. - - Values are returned as a tuple. - - .. deprecated:: 3.0.0 - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._legacy_api = True - - tags = property(lambda self: self._tags_v1) - tagdata = property(lambda self: self._tagdata) - - # defined in ImageFileDirectory_v2 - tagtype: dict[int, int] - """Dictionary of tag types""" - - @classmethod - def from_v2(cls, original: ImageFileDirectory_v2) -> ImageFileDirectory_v1: - """Returns an - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` - instance with the same data as is contained in the original - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` - instance. - - :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` - - """ - - ifd = cls(prefix=original.prefix) - ifd._tagdata = original._tagdata - ifd.tagtype = original.tagtype - ifd.next = original.next # an indicator for multipage tiffs - return ifd - - def to_v2(self) -> ImageFileDirectory_v2: - """Returns an - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` - instance with the same data as is contained in the original - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` - instance. - - :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` - - """ - - ifd = ImageFileDirectory_v2(prefix=self.prefix) - ifd._tagdata = dict(self._tagdata) - ifd.tagtype = dict(self.tagtype) - ifd._tags_v2 = dict(self._tags_v2) - return ifd - - def __contains__(self, tag: object) -> bool: - return tag in self._tags_v1 or tag in self._tagdata - - def __len__(self) -> int: - return len(set(self._tagdata) | set(self._tags_v1)) - - def __iter__(self) -> Iterator[int]: - return iter(set(self._tagdata) | set(self._tags_v1)) - - def __setitem__(self, tag: int, value: Any) -> None: - for legacy_api in (False, True): - self._setitem(tag, value, legacy_api) - - def __getitem__(self, tag: int) -> Any: - if tag not in self._tags_v1: # unpack on the fly - data = self._tagdata[tag] - typ = self.tagtype[tag] - size, handler = self._load_dispatch[typ] - for legacy in (False, True): - self._setitem(tag, handler(self, data, legacy), legacy) - val = self._tags_v1[tag] - if not isinstance(val, (tuple, bytes)): - val = (val,) - return val - - -# undone -- switch this pointer -ImageFileDirectory = ImageFileDirectory_v1 - - -## -# Image plugin for TIFF files. - - -class TiffImageFile(ImageFile.ImageFile): - format = "TIFF" - format_description = "Adobe TIFF" - _close_exclusive_fp_after_loading = False - - def __init__( - self, - fp: StrOrBytesPath | IO[bytes], - filename: str | bytes | None = None, - ) -> None: - self.tag_v2: ImageFileDirectory_v2 - """ Image file directory (tag dictionary) """ - - self.tag: ImageFileDirectory_v1 - """ Legacy tag entries """ - - super().__init__(fp, filename) - - def _open(self) -> None: - """Open the first image in a TIFF file""" - - # Header - ifh = self.fp.read(8) - if ifh[2] == 43: - ifh += self.fp.read(8) - - self.tag_v2 = ImageFileDirectory_v2(ifh) - - # setup frame pointers - self.__first = self.__next = self.tag_v2.next - self.__frame = -1 - self._fp = self.fp - self._frame_pos: list[int] = [] - self._n_frames: int | None = None - - logger.debug("*** TiffImageFile._open ***") - logger.debug("- __first: %s", self.__first) - logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes) - - # and load the first frame - self._seek(0) - - @property - def n_frames(self) -> int: - current_n_frames = self._n_frames - if current_n_frames is None: - current = self.tell() - self._seek(len(self._frame_pos)) - while self._n_frames is None: - self._seek(self.tell() + 1) - self.seek(current) - assert self._n_frames is not None - return self._n_frames - - def seek(self, frame: int) -> None: - """Select a given frame as current image""" - if not self._seek_check(frame): - return - self._seek(frame) - if self._im is not None and ( - self.im.size != self._tile_size - or self.im.mode != self.mode - or self.readonly - ): - self._im = None - - def _seek(self, frame: int) -> None: - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self.fp = self._fp - - while len(self._frame_pos) <= frame: - if not self.__next: - msg = "no more images in TIFF file" - raise EOFError(msg) - logger.debug( - "Seeking to frame %s, on frame %s, __next %s, location: %s", - frame, - self.__frame, - self.__next, - self.fp.tell(), - ) - if self.__next >= 2**63: - msg = "Unable to seek to frame" - raise ValueError(msg) - self.fp.seek(self.__next) - self._frame_pos.append(self.__next) - logger.debug("Loading tags, location: %s", self.fp.tell()) - self.tag_v2.load(self.fp) - if self.tag_v2.next in self._frame_pos: - # This IFD has already been processed - # Declare this to be the end of the image - self.__next = 0 - else: - self.__next = self.tag_v2.next - if self.__next == 0: - self._n_frames = frame + 1 - if len(self._frame_pos) == 1: - self.is_animated = self.__next != 0 - self.__frame += 1 - self.fp.seek(self._frame_pos[frame]) - self.tag_v2.load(self.fp) - if XMP in self.tag_v2: - xmp = self.tag_v2[XMP] - if isinstance(xmp, tuple) and len(xmp) == 1: - xmp = xmp[0] - self.info["xmp"] = xmp - elif "xmp" in self.info: - del self.info["xmp"] - self._reload_exif() - # fill the legacy tag/ifd entries - self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) - self.__frame = frame - self._setup() - - def tell(self) -> int: - """Return the current frame number""" - return self.__frame - - def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]: - """ - Returns a dictionary of Photoshop "Image Resource Blocks". - The keys are the image resource ID. For more information, see - https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727 - - :returns: Photoshop "Image Resource Blocks" in a dictionary. - """ - blocks = {} - val = self.tag_v2.get(ExifTags.Base.ImageResources) - if val: - while val.startswith(b"8BIM"): - id = i16(val[4:6]) - n = math.ceil((val[6] + 1) / 2) * 2 - size = i32(val[6 + n : 10 + n]) - data = val[10 + n : 10 + n + size] - blocks[id] = {"data": data} - - val = val[math.ceil((10 + n + size) / 2) * 2 :] - return blocks - - def load(self) -> Image.core.PixelAccess | None: - if self.tile and self.use_load_libtiff: - return self._load_libtiff() - return super().load() - - def load_prepare(self) -> None: - if self._im is None: - Image._decompression_bomb_check(self._tile_size) - self.im = Image.core.new(self.mode, self._tile_size) - ImageFile.ImageFile.load_prepare(self) - - def load_end(self) -> None: - # allow closing if we're on the first frame, there's no next - # This is the ImageFile.load path only, libtiff specific below. - if not self.is_animated: - self._close_exclusive_fp_after_loading = True - - # load IFD data from fp before it is closed - exif = self.getexif() - for key in TiffTags.TAGS_V2_GROUPS: - if key not in exif: - continue - exif.get_ifd(key) - - ImageOps.exif_transpose(self, in_place=True) - if ExifTags.Base.Orientation in self.tag_v2: - del self.tag_v2[ExifTags.Base.Orientation] - - def _load_libtiff(self) -> Image.core.PixelAccess | None: - """Overload method triggered when we detect a compressed tiff - Calls out to libtiff""" - - Image.Image.load(self) - - self.load_prepare() - - if not len(self.tile) == 1: - msg = "Not exactly one tile" - raise OSError(msg) - - # (self._compression, (extents tuple), - # 0, (rawmode, self._compression, fp)) - extents = self.tile[0][1] - args = self.tile[0][3] - - # To be nice on memory footprint, if there's a - # file descriptor, use that instead of reading - # into a string in python. - try: - fp = hasattr(self.fp, "fileno") and self.fp.fileno() - # flush the file descriptor, prevents error on pypy 2.4+ - # should also eliminate the need for fp.tell - # in _seek - if hasattr(self.fp, "flush"): - self.fp.flush() - except OSError: - # io.BytesIO have a fileno, but returns an OSError if - # it doesn't use a file descriptor. - fp = False - - if fp: - assert isinstance(args, tuple) - args_list = list(args) - args_list[2] = fp - args = tuple(args_list) - - decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig) - try: - decoder.setimage(self.im, extents) - except ValueError as e: - msg = "Couldn't set the image" - raise OSError(msg) from e - - close_self_fp = self._exclusive_fp and not self.is_animated - if hasattr(self.fp, "getvalue"): - # We've got a stringio like thing passed in. Yay for all in memory. - # The decoder needs the entire file in one shot, so there's not - # a lot we can do here other than give it the entire file. - # unless we could do something like get the address of the - # underlying string for stringio. - # - # Rearranging for supporting byteio items, since they have a fileno - # that returns an OSError if there's no underlying fp. Easier to - # deal with here by reordering. - logger.debug("have getvalue. just sending in a string from getvalue") - n, err = decoder.decode(self.fp.getvalue()) - elif fp: - # we've got a actual file on disk, pass in the fp. - logger.debug("have fileno, calling fileno version of the decoder.") - if not close_self_fp: - self.fp.seek(0) - # Save and restore the file position, because libtiff will move it - # outside of the Python runtime, and that will confuse - # io.BufferedReader and possible others. - # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), - # because the buffer read head already may not equal the actual - # file position, and fp.seek() may just adjust it's internal - # pointer and not actually seek the OS file handle. - pos = os.lseek(fp, 0, os.SEEK_CUR) - # 4 bytes, otherwise the trace might error out - n, err = decoder.decode(b"fpfp") - os.lseek(fp, pos, os.SEEK_SET) - else: - # we have something else. - logger.debug("don't have fileno or getvalue. just reading") - self.fp.seek(0) - # UNDONE -- so much for that buffer size thing. - n, err = decoder.decode(self.fp.read()) - - self.tile = [] - self.readonly = 0 - - self.load_end() - - if close_self_fp: - self.fp.close() - self.fp = None # might be shared - - if err < 0: - msg = f"decoder error {err}" - raise OSError(msg) - - return Image.Image.load(self) - - def _setup(self) -> None: - """Setup this image object based on current tags""" - - if 0xBC01 in self.tag_v2: - msg = "Windows Media Photo files not yet supported" - raise OSError(msg) - - # extract relevant tags - self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] - self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) - - # photometric is a required tag, but not everyone is reading - # the specification - photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) - - # old style jpeg compression images most certainly are YCbCr - if self._compression == "tiff_jpeg": - photo = 6 - - fillorder = self.tag_v2.get(FILLORDER, 1) - - logger.debug("*** Summary ***") - logger.debug("- compression: %s", self._compression) - logger.debug("- photometric_interpretation: %s", photo) - logger.debug("- planar_configuration: %s", self._planar_configuration) - logger.debug("- fill_order: %s", fillorder) - logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) - - # size - try: - xsize = self.tag_v2[IMAGEWIDTH] - ysize = self.tag_v2[IMAGELENGTH] - except KeyError as e: - msg = "Missing dimensions" - raise TypeError(msg) from e - if not isinstance(xsize, int) or not isinstance(ysize, int): - msg = "Invalid dimensions" - raise ValueError(msg) - self._tile_size = xsize, ysize - orientation = self.tag_v2.get(ExifTags.Base.Orientation) - if orientation in (5, 6, 7, 8): - self._size = ysize, xsize - else: - self._size = xsize, ysize - - logger.debug("- size: %s", self.size) - - sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) - if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: - # SAMPLEFORMAT is properly per band, so an RGB image will - # be (1,1,1). But, we don't support per band pixel types, - # and anything more than one band is a uint8. So, just - # take the first element. Revisit this if adding support - # for more exotic images. - sample_format = (1,) - - bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) - extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) - if photo in (2, 6, 8): # RGB, YCbCr, LAB - bps_count = 3 - elif photo == 5: # CMYK - bps_count = 4 - else: - bps_count = 1 - bps_count += len(extra_tuple) - bps_actual_count = len(bps_tuple) - samples_per_pixel = self.tag_v2.get( - SAMPLESPERPIXEL, - 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, - ) - - if samples_per_pixel > MAX_SAMPLESPERPIXEL: - # DOS check, samples_per_pixel can be a Long, and we extend the tuple below - logger.error( - "More samples per pixel than can be decoded: %s", samples_per_pixel - ) - msg = "Invalid value for samples per pixel" - raise SyntaxError(msg) - - if samples_per_pixel < bps_actual_count: - # If a file has more values in bps_tuple than expected, - # remove the excess. - bps_tuple = bps_tuple[:samples_per_pixel] - elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: - # If a file has only one value in bps_tuple, when it should have more, - # presume it is the same number of bits for all of the samples. - bps_tuple = bps_tuple * samples_per_pixel - - if len(bps_tuple) != samples_per_pixel: - msg = "unknown data organization" - raise SyntaxError(msg) - - # mode: check photometric interpretation and bits per pixel - key = ( - self.tag_v2.prefix, - photo, - sample_format, - fillorder, - bps_tuple, - extra_tuple, - ) - logger.debug("format key: %s", key) - try: - self._mode, rawmode = OPEN_INFO[key] - except KeyError as e: - logger.debug("- unsupported format") - msg = "unknown pixel mode" - raise SyntaxError(msg) from e - - logger.debug("- raw mode: %s", rawmode) - logger.debug("- pil mode: %s", self.mode) - - self.info["compression"] = self._compression - - xres = self.tag_v2.get(X_RESOLUTION, 1) - yres = self.tag_v2.get(Y_RESOLUTION, 1) - - if xres and yres: - resunit = self.tag_v2.get(RESOLUTION_UNIT) - if resunit == 2: # dots per inch - self.info["dpi"] = (xres, yres) - elif resunit == 3: # dots per centimeter. convert to dpi - self.info["dpi"] = (xres * 2.54, yres * 2.54) - elif resunit is None: # used to default to 1, but now 2) - self.info["dpi"] = (xres, yres) - # For backward compatibility, - # we also preserve the old behavior - self.info["resolution"] = xres, yres - else: # No absolute unit of measurement - self.info["resolution"] = xres, yres - - # build tile descriptors - x = y = layer = 0 - self.tile = [] - self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw" - if self.use_load_libtiff: - # Decoder expects entire file as one tile. - # There's a buffer size limit in load (64k) - # so large g4 images will fail if we use that - # function. - # - # Setup the one tile for the whole image, then - # use the _load_libtiff function. - - # libtiff handles the fillmode for us, so 1;IR should - # actually be 1;I. Including the R double reverses the - # bits, so stripes of the image are reversed. See - # https://github.com/python-pillow/Pillow/issues/279 - if fillorder == 2: - # Replace fillorder with fillorder=1 - key = key[:3] + (1,) + key[4:] - logger.debug("format key: %s", key) - # this should always work, since all the - # fillorder==2 modes have a corresponding - # fillorder=1 mode - self._mode, rawmode = OPEN_INFO[key] - # YCbCr images with new jpeg compression with pixels in one plane - # unpacked straight into RGB values - if ( - photo == 6 - and self._compression == "jpeg" - and self._planar_configuration == 1 - ): - rawmode = "RGB" - # libtiff always returns the bytes in native order. - # we're expecting image byte order. So, if the rawmode - # contains I;16, we need to convert from native to image - # byte order. - elif rawmode == "I;16": - rawmode = "I;16N" - elif rawmode.endswith((";16B", ";16L")): - rawmode = rawmode[:-1] + "N" - - # Offset in the tile tuple is 0, we go from 0,0 to - # w,h, and we only do this once -- eds - a = (rawmode, self._compression, False, self.tag_v2.offset) - self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a)) - - elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: - # striped image - if STRIPOFFSETS in self.tag_v2: - offsets = self.tag_v2[STRIPOFFSETS] - h = self.tag_v2.get(ROWSPERSTRIP, ysize) - w = xsize - else: - # tiled image - offsets = self.tag_v2[TILEOFFSETS] - tilewidth = self.tag_v2.get(TILEWIDTH) - h = self.tag_v2.get(TILELENGTH) - if not isinstance(tilewidth, int) or not isinstance(h, int): - msg = "Invalid tile dimensions" - raise ValueError(msg) - w = tilewidth - - if w == xsize and h == ysize and self._planar_configuration != 2: - # Every tile covers the image. Only use the last offset - offsets = offsets[-1:] - - for offset in offsets: - if x + w > xsize: - stride = w * sum(bps_tuple) / 8 # bytes per line - else: - stride = 0 - - tile_rawmode = rawmode - if self._planar_configuration == 2: - # each band on it's own layer - tile_rawmode = rawmode[layer] - # adjust stride width accordingly - stride /= bps_count - - args = (tile_rawmode, int(stride), 1) - self.tile.append( - ImageFile._Tile( - self._compression, - (x, y, min(x + w, xsize), min(y + h, ysize)), - offset, - args, - ) - ) - x += w - if x >= xsize: - x, y = 0, y + h - if y >= ysize: - y = 0 - layer += 1 - else: - logger.debug("- unsupported data organization") - msg = "unknown data organization" - raise SyntaxError(msg) - - # Fix up info. - if ICCPROFILE in self.tag_v2: - self.info["icc_profile"] = self.tag_v2[ICCPROFILE] - - # fixup palette descriptor - - if self.mode in ["P", "PA"]: - palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] - self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) - - -# -# -------------------------------------------------------------------- -# Write TIFF files - -# little endian is default except for image modes with -# explicit big endian byte-order - -SAVE_INFO = { - # mode => rawmode, byteorder, photometrics, - # sampleformat, bitspersample, extra - "1": ("1", II, 1, 1, (1,), None), - "L": ("L", II, 1, 1, (8,), None), - "LA": ("LA", II, 1, 1, (8, 8), 2), - "P": ("P", II, 3, 1, (8,), None), - "PA": ("PA", II, 3, 1, (8, 8), 2), - "I": ("I;32S", II, 1, 2, (32,), None), - "I;16": ("I;16", II, 1, 1, (16,), None), - "I;16L": ("I;16L", II, 1, 1, (16,), None), - "F": ("F;32F", II, 1, 3, (32,), None), - "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), - "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), - "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), - "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), - "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), - "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), - "I;16B": ("I;16B", MM, 1, 1, (16,), None), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as TIFF" - raise OSError(msg) from e - - encoderinfo = im.encoderinfo - encoderconfig = im.encoderconfig - - ifd = ImageFileDirectory_v2(prefix=prefix) - if encoderinfo.get("big_tiff"): - ifd._bigtiff = True - - try: - compression = encoderinfo["compression"] - except KeyError: - compression = im.info.get("compression") - if isinstance(compression, int): - # compression value may be from BMP. Ignore it - compression = None - if compression is None: - compression = "raw" - elif compression == "tiff_jpeg": - # OJPEG is obsolete, so use new-style JPEG compression instead - compression = "jpeg" - elif compression == "tiff_deflate": - compression = "tiff_adobe_deflate" - - libtiff = WRITE_LIBTIFF or compression != "raw" - - # required for color libtiff images - ifd[PLANAR_CONFIGURATION] = 1 - - ifd[IMAGEWIDTH] = im.size[0] - ifd[IMAGELENGTH] = im.size[1] - - # write any arbitrary tags passed in as an ImageFileDirectory - if "tiffinfo" in encoderinfo: - info = encoderinfo["tiffinfo"] - elif "exif" in encoderinfo: - info = encoderinfo["exif"] - if isinstance(info, bytes): - exif = Image.Exif() - exif.load(info) - info = exif - else: - info = {} - logger.debug("Tiffinfo Keys: %s", list(info)) - if isinstance(info, ImageFileDirectory_v1): - info = info.to_v2() - for key in info: - if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: - ifd[key] = info.get_ifd(key) - else: - ifd[key] = info.get(key) - try: - ifd.tagtype[key] = info.tagtype[key] - except Exception: - pass # might not be an IFD. Might not have populated type - - legacy_ifd = {} - if hasattr(im, "tag"): - legacy_ifd = im.tag.to_v2() - - supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})} - for tag in ( - # IFD offset that may not be correct in the saved image - EXIFIFD, - # Determined by the image format and should not be copied from legacy_ifd. - SAMPLEFORMAT, - ): - if tag in supplied_tags: - del supplied_tags[tag] - - # additions written by Greg Couch, gregc@cgl.ucsf.edu - # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com - if hasattr(im, "tag_v2"): - # preserve tags from original TIFF image file - for key in ( - RESOLUTION_UNIT, - X_RESOLUTION, - Y_RESOLUTION, - IPTC_NAA_CHUNK, - PHOTOSHOP_CHUNK, - XMP, - ): - if key in im.tag_v2: - if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in ( - TiffTags.BYTE, - TiffTags.UNDEFINED, - ): - del supplied_tags[key] - else: - ifd[key] = im.tag_v2[key] - ifd.tagtype[key] = im.tag_v2.tagtype[key] - - # preserve ICC profile (should also work when saving other formats - # which support profiles as TIFF) -- 2008-06-06 Florian Hoech - icc = encoderinfo.get("icc_profile", im.info.get("icc_profile")) - if icc: - ifd[ICCPROFILE] = icc - - for key, name in [ - (IMAGEDESCRIPTION, "description"), - (X_RESOLUTION, "resolution"), - (Y_RESOLUTION, "resolution"), - (X_RESOLUTION, "x_resolution"), - (Y_RESOLUTION, "y_resolution"), - (RESOLUTION_UNIT, "resolution_unit"), - (SOFTWARE, "software"), - (DATE_TIME, "date_time"), - (ARTIST, "artist"), - (COPYRIGHT, "copyright"), - ]: - if name in encoderinfo: - ifd[key] = encoderinfo[name] - - dpi = encoderinfo.get("dpi") - if dpi: - ifd[RESOLUTION_UNIT] = 2 - ifd[X_RESOLUTION] = dpi[0] - ifd[Y_RESOLUTION] = dpi[1] - - if bits != (1,): - ifd[BITSPERSAMPLE] = bits - if len(bits) != 1: - ifd[SAMPLESPERPIXEL] = len(bits) - if extra is not None: - ifd[EXTRASAMPLES] = extra - if format != 1: - ifd[SAMPLEFORMAT] = format - - if PHOTOMETRIC_INTERPRETATION not in ifd: - ifd[PHOTOMETRIC_INTERPRETATION] = photo - elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0: - if im.mode == "1": - inverted_im = im.copy() - px = inverted_im.load() - if px is not None: - for y in range(inverted_im.height): - for x in range(inverted_im.width): - px[x, y] = 0 if px[x, y] == 255 else 255 - im = inverted_im - else: - im = ImageOps.invert(im) - - if im.mode in ["P", "PA"]: - lut = im.im.getpalette("RGB", "RGB;L") - colormap = [] - colors = len(lut) // 3 - for i in range(3): - colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]] - colormap += [0] * (256 - colors) - ifd[COLORMAP] = colormap - # data orientation - w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] - stride = len(bits) * ((w * bits[0] + 7) // 8) - if ROWSPERSTRIP not in ifd: - # aim for given strip size (64 KB by default) when using libtiff writer - if libtiff: - im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) - rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) - # JPEG encoder expects multiple of 8 rows - if compression == "jpeg": - rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) - else: - rows_per_strip = h - if rows_per_strip == 0: - rows_per_strip = 1 - ifd[ROWSPERSTRIP] = rows_per_strip - strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP] - strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP] - if strip_byte_counts >= 2**16: - ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG - ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( - stride * h - strip_byte_counts * (strips_per_image - 1), - ) - ifd[STRIPOFFSETS] = tuple( - range(0, strip_byte_counts * strips_per_image, strip_byte_counts) - ) # this is adjusted by IFD writer - # no compression by default: - ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) - - if im.mode == "YCbCr": - for tag, default_value in { - YCBCRSUBSAMPLING: (1, 1), - REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), - }.items(): - ifd.setdefault(tag, default_value) - - blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] - if libtiff: - if "quality" in encoderinfo: - quality = encoderinfo["quality"] - if not isinstance(quality, int) or quality < 0 or quality > 100: - msg = "Invalid quality setting" - raise ValueError(msg) - if compression != "jpeg": - msg = "quality setting only supported for 'jpeg' compression" - raise ValueError(msg) - ifd[JPEGQUALITY] = quality - - logger.debug("Saving using libtiff encoder") - logger.debug("Items: %s", sorted(ifd.items())) - _fp = 0 - if hasattr(fp, "fileno"): - try: - fp.seek(0) - _fp = fp.fileno() - except io.UnsupportedOperation: - pass - - # optional types for non core tags - types = {} - # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library - # based on the data in the strip. - # OSUBFILETYPE is deprecated. - # The other tags expect arrays with a certain length (fixed or depending on - # BITSPERSAMPLE, etc), passing arrays with a different length will result in - # segfaults. Block these tags until we add extra validation. - # SUBIFD may also cause a segfault. - blocklist += [ - OSUBFILETYPE, - REFERENCEBLACKWHITE, - STRIPBYTECOUNTS, - STRIPOFFSETS, - TRANSFERFUNCTION, - SUBIFD, - ] - - # bits per sample is a single short in the tiff directory, not a list. - atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]} - # Merge the ones that we have with (optional) more bits from - # the original file, e.g x,y resolution so that we can - # save(load('')) == original file. - for tag, value in itertools.chain(ifd.items(), supplied_tags.items()): - # Libtiff can only process certain core items without adding - # them to the custom dictionary. - # Custom items are supported for int, float, unicode, string and byte - # values. Other types and tuples require a tagtype. - if tag not in TiffTags.LIBTIFF_CORE: - if not getattr(Image.core, "libtiff_support_custom_tags", False): - continue - - if tag in TiffTags.TAGS_V2_GROUPS: - types[tag] = TiffTags.LONG8 - elif tag in ifd.tagtype: - types[tag] = ifd.tagtype[tag] - elif not (isinstance(value, (int, float, str, bytes))): - continue - else: - type = TiffTags.lookup(tag).type - if type: - types[tag] = type - if tag not in atts and tag not in blocklist: - if isinstance(value, str): - atts[tag] = value.encode("ascii", "replace") + b"\0" - elif isinstance(value, IFDRational): - atts[tag] = float(value) - else: - atts[tag] = value - - if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1: - atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] - - logger.debug("Converted items: %s", sorted(atts.items())) - - # libtiff always expects the bytes in native order. - # we're storing image byte order. So, if the rawmode - # contains I;16, we need to convert from native to image - # byte order. - if im.mode in ("I;16", "I;16B", "I;16L"): - rawmode = "I;16N" - - # Pass tags as sorted list so that the tags are set in a fixed order. - # This is required by libtiff for some tags. For example, the JPEGQUALITY - # pseudo tag requires that the COMPRESS tag was already set. - tags = list(atts.items()) - tags.sort() - a = (rawmode, compression, _fp, filename, tags, types) - encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) - encoder.setimage(im.im, (0, 0) + im.size) - while True: - errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:] - if not _fp: - fp.write(data) - if errcode: - break - if errcode < 0: - msg = f"encoder error {errcode} when writing image file" - raise OSError(msg) - - else: - for tag in blocklist: - del ifd[tag] - offset = ifd.save(fp) - - ImageFile._save( - im, - fp, - [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))], - ) - - # -- helper for multi-page save -- - if "_debug_multipage" in encoderinfo: - # just to access o32 and o16 (using correct byte order) - setattr(im, "_debug_multipage", ifd) - - -class AppendingTiffWriter(io.BytesIO): - fieldSizes = [ - 0, # None - 1, # byte - 1, # ascii - 2, # short - 4, # long - 8, # rational - 1, # sbyte - 1, # undefined - 2, # sshort - 4, # slong - 8, # srational - 4, # float - 8, # double - 4, # ifd - 2, # unicode - 4, # complex - 8, # long8 - ] - - Tags = { - 273, # StripOffsets - 288, # FreeOffsets - 324, # TileOffsets - 519, # JPEGQTables - 520, # JPEGDCTables - 521, # JPEGACTables - } - - def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None: - self.f: IO[bytes] - if is_path(fn): - self.name = fn - self.close_fp = True - try: - self.f = open(fn, "w+b" if new else "r+b") - except OSError: - self.f = open(fn, "w+b") - else: - self.f = cast(IO[bytes], fn) - self.close_fp = False - self.beginning = self.f.tell() - self.setup() - - def setup(self) -> None: - # Reset everything. - self.f.seek(self.beginning, os.SEEK_SET) - - self.whereToWriteNewIFDOffset: int | None = None - self.offsetOfNewPage = 0 - - self.IIMM = iimm = self.f.read(4) - self._bigtiff = b"\x2b" in iimm - if not iimm: - # empty file - first page - self.isFirst = True - return - - self.isFirst = False - if iimm not in PREFIXES: - msg = "Invalid TIFF file header" - raise RuntimeError(msg) - - self.setEndian("<" if iimm.startswith(II) else ">") - - if self._bigtiff: - self.f.seek(4, os.SEEK_CUR) - self.skipIFDs() - self.goToEnd() - - def finalize(self) -> None: - if self.isFirst: - return - - # fix offsets - self.f.seek(self.offsetOfNewPage) - - iimm = self.f.read(4) - if not iimm: - # Make it easy to finish a frame without committing to a new one. - return - - if iimm != self.IIMM: - msg = "IIMM of new page doesn't match IIMM of first page" - raise RuntimeError(msg) - - if self._bigtiff: - self.f.seek(4, os.SEEK_CUR) - ifd_offset = self._read(8 if self._bigtiff else 4) - ifd_offset += self.offsetOfNewPage - assert self.whereToWriteNewIFDOffset is not None - self.f.seek(self.whereToWriteNewIFDOffset) - self._write(ifd_offset, 8 if self._bigtiff else 4) - self.f.seek(ifd_offset) - self.fixIFD() - - def newFrame(self) -> None: - # Call this to finish a frame. - self.finalize() - self.setup() - - def __enter__(self) -> AppendingTiffWriter: - return self - - def __exit__(self, *args: object) -> None: - if self.close_fp: - self.close() - - def tell(self) -> int: - return self.f.tell() - self.offsetOfNewPage - - def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: - """ - :param offset: Distance to seek. - :param whence: Whether the distance is relative to the start, - end or current position. - :returns: The resulting position, relative to the start. - """ - if whence == os.SEEK_SET: - offset += self.offsetOfNewPage - - self.f.seek(offset, whence) - return self.tell() - - def goToEnd(self) -> None: - self.f.seek(0, os.SEEK_END) - pos = self.f.tell() - - # pad to 16 byte boundary - pad_bytes = 16 - pos % 16 - if 0 < pad_bytes < 16: - self.f.write(bytes(pad_bytes)) - self.offsetOfNewPage = self.f.tell() - - def setEndian(self, endian: str) -> None: - self.endian = endian - self.longFmt = f"{self.endian}L" - self.shortFmt = f"{self.endian}H" - self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L") - - def skipIFDs(self) -> None: - while True: - ifd_offset = self._read(8 if self._bigtiff else 4) - if ifd_offset == 0: - self.whereToWriteNewIFDOffset = self.f.tell() - ( - 8 if self._bigtiff else 4 - ) - break - - self.f.seek(ifd_offset) - num_tags = self._read(8 if self._bigtiff else 2) - self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR) - - def write(self, data: Buffer, /) -> int: - return self.f.write(data) - - def _fmt(self, field_size: int) -> str: - try: - return {2: "H", 4: "L", 8: "Q"}[field_size] - except KeyError: - msg = "offset is not supported" - raise RuntimeError(msg) - - def _read(self, field_size: int) -> int: - (value,) = struct.unpack( - self.endian + self._fmt(field_size), self.f.read(field_size) - ) - return value - - def readShort(self) -> int: - return self._read(2) - - def readLong(self) -> int: - return self._read(4) - - @staticmethod - def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: - if bytes_written is not None and bytes_written != expected: - msg = f"wrote only {bytes_written} bytes but wanted {expected}" - raise RuntimeError(msg) - - def _rewriteLast( - self, value: int, field_size: int, new_field_size: int = 0 - ) -> None: - self.f.seek(-field_size, os.SEEK_CUR) - if not new_field_size: - new_field_size = field_size - bytes_written = self.f.write( - struct.pack(self.endian + self._fmt(new_field_size), value) - ) - self._verify_bytes_written(bytes_written, new_field_size) - - def rewriteLastShortToLong(self, value: int) -> None: - self._rewriteLast(value, 2, 4) - - def rewriteLastShort(self, value: int) -> None: - return self._rewriteLast(value, 2) - - def rewriteLastLong(self, value: int) -> None: - return self._rewriteLast(value, 4) - - def _write(self, value: int, field_size: int) -> None: - bytes_written = self.f.write( - struct.pack(self.endian + self._fmt(field_size), value) - ) - self._verify_bytes_written(bytes_written, field_size) - - def writeShort(self, value: int) -> None: - self._write(value, 2) - - def writeLong(self, value: int) -> None: - self._write(value, 4) - - def close(self) -> None: - self.finalize() - if self.close_fp: - self.f.close() - - def fixIFD(self) -> None: - num_tags = self._read(8 if self._bigtiff else 2) - - for i in range(num_tags): - tag, field_type, count = struct.unpack( - self.tagFormat, self.f.read(12 if self._bigtiff else 8) - ) - - field_size = self.fieldSizes[field_type] - total_size = field_size * count - fmt_size = 8 if self._bigtiff else 4 - is_local = total_size <= fmt_size - if not is_local: - offset = self._read(fmt_size) + self.offsetOfNewPage - self._rewriteLast(offset, fmt_size) - - if tag in self.Tags: - cur_pos = self.f.tell() - - logger.debug( - "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d", - TiffTags.lookup(tag).name, - tag, - TYPES.get(field_type, "unknown"), - field_type, - field_size, - count, - ) - - if is_local: - self._fixOffsets(count, field_size) - self.f.seek(cur_pos + fmt_size) - else: - self.f.seek(offset) - self._fixOffsets(count, field_size) - self.f.seek(cur_pos) - - elif is_local: - # skip the locally stored value that is not an offset - self.f.seek(fmt_size, os.SEEK_CUR) - - def _fixOffsets(self, count: int, field_size: int) -> None: - for i in range(count): - offset = self._read(field_size) - offset += self.offsetOfNewPage - - new_field_size = 0 - if self._bigtiff and field_size in (2, 4) and offset >= 2**32: - # offset is now too large - we must convert long to long8 - new_field_size = 8 - elif field_size == 2 and offset >= 2**16: - # offset is now too large - we must convert short to long - new_field_size = 4 - if new_field_size: - if count != 1: - msg = "not implemented" - raise RuntimeError(msg) # XXX TODO - - # simple case - the offset is just one and therefore it is - # local (not referenced with another offset) - self._rewriteLast(offset, field_size, new_field_size) - # Move back past the new offset, past 'count', and before 'field_type' - rewind = -new_field_size - 4 - 2 - self.f.seek(rewind, os.SEEK_CUR) - self.writeShort(new_field_size) # rewrite the type - self.f.seek(2 - rewind, os.SEEK_CUR) - else: - self._rewriteLast(offset, field_size) - - def fixOffsets( - self, count: int, isShort: bool = False, isLong: bool = False - ) -> None: - if isShort: - field_size = 2 - elif isLong: - field_size = 4 - else: - field_size = 0 - return self._fixOffsets(count, field_size) - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - append_images = list(im.encoderinfo.get("append_images", [])) - if not hasattr(im, "n_frames") and not append_images: - return _save(im, fp, filename) - - cur_idx = im.tell() - try: - with AppendingTiffWriter(fp) as tf: - for ims in [im] + append_images: - encoderinfo = ims._attach_default_encoderinfo(im) - if not hasattr(ims, "encoderconfig"): - ims.encoderconfig = () - nfr = getattr(ims, "n_frames", 1) - - for idx in range(nfr): - ims.seek(idx) - ims.load() - _save(ims, tf, filename) - tf.newFrame() - ims.encoderinfo = encoderinfo - finally: - im.seek(cur_idx) - - -# -# -------------------------------------------------------------------- -# Register - -Image.register_open(TiffImageFile.format, TiffImageFile, _accept) -Image.register_save(TiffImageFile.format, _save) -Image.register_save_all(TiffImageFile.format, _save_all) - -Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"]) - -Image.register_mime(TiffImageFile.format, "image/tiff") diff --git a/python_modules/PIL/TiffTags.py b/python_modules/PIL/TiffTags.py deleted file mode 100644 index 86adaa458..000000000 --- a/python_modules/PIL/TiffTags.py +++ /dev/null @@ -1,562 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# TIFF tags -# -# This module provides clear-text names for various well-known -# TIFF tags. the TIFF codec works just fine without it. -# -# Copyright (c) Secret Labs AB 1999. -# -# See the README file for information on usage and redistribution. -# - -## -# This module provides constants and clear-text names for various -# well-known TIFF tags. -## -from __future__ import annotations - -from typing import NamedTuple - - -class _TagInfo(NamedTuple): - value: int | None - name: str - type: int | None - length: int | None - enum: dict[str, int] - - -class TagInfo(_TagInfo): - __slots__: list[str] = [] - - def __new__( - cls, - value: int | None = None, - name: str = "unknown", - type: int | None = None, - length: int | None = None, - enum: dict[str, int] | None = None, - ) -> TagInfo: - return super().__new__(cls, value, name, type, length, enum or {}) - - def cvt_enum(self, value: str) -> int | str: - # Using get will call hash(value), which can be expensive - # for some types (e.g. Fraction). Since self.enum is rarely - # used, it's usually better to test it first. - return self.enum.get(value, value) if self.enum else value - - -def lookup(tag: int, group: int | None = None) -> TagInfo: - """ - :param tag: Integer tag number - :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in - - .. versionadded:: 8.3.0 - - :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible, - otherwise just populating the value and name from ``TAGS``. - If the tag is not recognized, "unknown" is returned for the name - - """ - - if group is not None: - info = TAGS_V2_GROUPS[group].get(tag) if group in TAGS_V2_GROUPS else None - else: - info = TAGS_V2.get(tag) - return info or TagInfo(tag, TAGS.get(tag, "unknown")) - - -## -# Map tag numbers to tag info. -# -# id: (Name, Type, Length[, enum_values]) -# -# The length here differs from the length in the tiff spec. For -# numbers, the tiff spec is for the number of fields returned. We -# agree here. For string-like types, the tiff spec uses the length of -# field in bytes. In Pillow, we are using the number of expected -# fields, in general 1 for string-like types. - - -BYTE = 1 -ASCII = 2 -SHORT = 3 -LONG = 4 -RATIONAL = 5 -SIGNED_BYTE = 6 -UNDEFINED = 7 -SIGNED_SHORT = 8 -SIGNED_LONG = 9 -SIGNED_RATIONAL = 10 -FLOAT = 11 -DOUBLE = 12 -IFD = 13 -LONG8 = 16 - -_tags_v2: dict[int, tuple[str, int, int] | tuple[str, int, int, dict[str, int]]] = { - 254: ("NewSubfileType", LONG, 1), - 255: ("SubfileType", SHORT, 1), - 256: ("ImageWidth", LONG, 1), - 257: ("ImageLength", LONG, 1), - 258: ("BitsPerSample", SHORT, 0), - 259: ( - "Compression", - SHORT, - 1, - { - "Uncompressed": 1, - "CCITT 1d": 2, - "Group 3 Fax": 3, - "Group 4 Fax": 4, - "LZW": 5, - "JPEG": 6, - "PackBits": 32773, - }, - ), - 262: ( - "PhotometricInterpretation", - SHORT, - 1, - { - "WhiteIsZero": 0, - "BlackIsZero": 1, - "RGB": 2, - "RGB Palette": 3, - "Transparency Mask": 4, - "CMYK": 5, - "YCbCr": 6, - "CieLAB": 8, - "CFA": 32803, # TIFF/EP, Adobe DNG - "LinearRaw": 32892, # Adobe DNG - }, - ), - 263: ("Threshholding", SHORT, 1), - 264: ("CellWidth", SHORT, 1), - 265: ("CellLength", SHORT, 1), - 266: ("FillOrder", SHORT, 1), - 269: ("DocumentName", ASCII, 1), - 270: ("ImageDescription", ASCII, 1), - 271: ("Make", ASCII, 1), - 272: ("Model", ASCII, 1), - 273: ("StripOffsets", LONG, 0), - 274: ("Orientation", SHORT, 1), - 277: ("SamplesPerPixel", SHORT, 1), - 278: ("RowsPerStrip", LONG, 1), - 279: ("StripByteCounts", LONG, 0), - 280: ("MinSampleValue", SHORT, 0), - 281: ("MaxSampleValue", SHORT, 0), - 282: ("XResolution", RATIONAL, 1), - 283: ("YResolution", RATIONAL, 1), - 284: ("PlanarConfiguration", SHORT, 1, {"Contiguous": 1, "Separate": 2}), - 285: ("PageName", ASCII, 1), - 286: ("XPosition", RATIONAL, 1), - 287: ("YPosition", RATIONAL, 1), - 288: ("FreeOffsets", LONG, 1), - 289: ("FreeByteCounts", LONG, 1), - 290: ("GrayResponseUnit", SHORT, 1), - 291: ("GrayResponseCurve", SHORT, 0), - 292: ("T4Options", LONG, 1), - 293: ("T6Options", LONG, 1), - 296: ("ResolutionUnit", SHORT, 1, {"none": 1, "inch": 2, "cm": 3}), - 297: ("PageNumber", SHORT, 2), - 301: ("TransferFunction", SHORT, 0), - 305: ("Software", ASCII, 1), - 306: ("DateTime", ASCII, 1), - 315: ("Artist", ASCII, 1), - 316: ("HostComputer", ASCII, 1), - 317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}), - 318: ("WhitePoint", RATIONAL, 2), - 319: ("PrimaryChromaticities", RATIONAL, 6), - 320: ("ColorMap", SHORT, 0), - 321: ("HalftoneHints", SHORT, 2), - 322: ("TileWidth", LONG, 1), - 323: ("TileLength", LONG, 1), - 324: ("TileOffsets", LONG, 0), - 325: ("TileByteCounts", LONG, 0), - 330: ("SubIFDs", LONG, 0), - 332: ("InkSet", SHORT, 1), - 333: ("InkNames", ASCII, 1), - 334: ("NumberOfInks", SHORT, 1), - 336: ("DotRange", SHORT, 0), - 337: ("TargetPrinter", ASCII, 1), - 338: ("ExtraSamples", SHORT, 0), - 339: ("SampleFormat", SHORT, 0), - 340: ("SMinSampleValue", DOUBLE, 0), - 341: ("SMaxSampleValue", DOUBLE, 0), - 342: ("TransferRange", SHORT, 6), - 347: ("JPEGTables", UNDEFINED, 1), - # obsolete JPEG tags - 512: ("JPEGProc", SHORT, 1), - 513: ("JPEGInterchangeFormat", LONG, 1), - 514: ("JPEGInterchangeFormatLength", LONG, 1), - 515: ("JPEGRestartInterval", SHORT, 1), - 517: ("JPEGLosslessPredictors", SHORT, 0), - 518: ("JPEGPointTransforms", SHORT, 0), - 519: ("JPEGQTables", LONG, 0), - 520: ("JPEGDCTables", LONG, 0), - 521: ("JPEGACTables", LONG, 0), - 529: ("YCbCrCoefficients", RATIONAL, 3), - 530: ("YCbCrSubSampling", SHORT, 2), - 531: ("YCbCrPositioning", SHORT, 1), - 532: ("ReferenceBlackWhite", RATIONAL, 6), - 700: ("XMP", BYTE, 0), - 33432: ("Copyright", ASCII, 1), - 33723: ("IptcNaaInfo", UNDEFINED, 1), - 34377: ("PhotoshopInfo", BYTE, 0), - # FIXME add more tags here - 34665: ("ExifIFD", LONG, 1), - 34675: ("ICCProfile", UNDEFINED, 1), - 34853: ("GPSInfoIFD", LONG, 1), - 36864: ("ExifVersion", UNDEFINED, 1), - 37724: ("ImageSourceData", UNDEFINED, 1), - 40965: ("InteroperabilityIFD", LONG, 1), - 41730: ("CFAPattern", UNDEFINED, 1), - # MPInfo - 45056: ("MPFVersion", UNDEFINED, 1), - 45057: ("NumberOfImages", LONG, 1), - 45058: ("MPEntry", UNDEFINED, 1), - 45059: ("ImageUIDList", UNDEFINED, 0), # UNDONE, check - 45060: ("TotalFrames", LONG, 1), - 45313: ("MPIndividualNum", LONG, 1), - 45569: ("PanOrientation", LONG, 1), - 45570: ("PanOverlap_H", RATIONAL, 1), - 45571: ("PanOverlap_V", RATIONAL, 1), - 45572: ("BaseViewpointNum", LONG, 1), - 45573: ("ConvergenceAngle", SIGNED_RATIONAL, 1), - 45574: ("BaselineLength", RATIONAL, 1), - 45575: ("VerticalDivergence", SIGNED_RATIONAL, 1), - 45576: ("AxisDistance_X", SIGNED_RATIONAL, 1), - 45577: ("AxisDistance_Y", SIGNED_RATIONAL, 1), - 45578: ("AxisDistance_Z", SIGNED_RATIONAL, 1), - 45579: ("YawAngle", SIGNED_RATIONAL, 1), - 45580: ("PitchAngle", SIGNED_RATIONAL, 1), - 45581: ("RollAngle", SIGNED_RATIONAL, 1), - 40960: ("FlashPixVersion", UNDEFINED, 1), - 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), - 50780: ("BestQualityScale", RATIONAL, 1), - 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one - 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 -} -_tags_v2_groups = { - # ExifIFD - 34665: { - 36864: ("ExifVersion", UNDEFINED, 1), - 40960: ("FlashPixVersion", UNDEFINED, 1), - 40965: ("InteroperabilityIFD", LONG, 1), - 41730: ("CFAPattern", UNDEFINED, 1), - }, - # GPSInfoIFD - 34853: { - 0: ("GPSVersionID", BYTE, 4), - 1: ("GPSLatitudeRef", ASCII, 2), - 2: ("GPSLatitude", RATIONAL, 3), - 3: ("GPSLongitudeRef", ASCII, 2), - 4: ("GPSLongitude", RATIONAL, 3), - 5: ("GPSAltitudeRef", BYTE, 1), - 6: ("GPSAltitude", RATIONAL, 1), - 7: ("GPSTimeStamp", RATIONAL, 3), - 8: ("GPSSatellites", ASCII, 0), - 9: ("GPSStatus", ASCII, 2), - 10: ("GPSMeasureMode", ASCII, 2), - 11: ("GPSDOP", RATIONAL, 1), - 12: ("GPSSpeedRef", ASCII, 2), - 13: ("GPSSpeed", RATIONAL, 1), - 14: ("GPSTrackRef", ASCII, 2), - 15: ("GPSTrack", RATIONAL, 1), - 16: ("GPSImgDirectionRef", ASCII, 2), - 17: ("GPSImgDirection", RATIONAL, 1), - 18: ("GPSMapDatum", ASCII, 0), - 19: ("GPSDestLatitudeRef", ASCII, 2), - 20: ("GPSDestLatitude", RATIONAL, 3), - 21: ("GPSDestLongitudeRef", ASCII, 2), - 22: ("GPSDestLongitude", RATIONAL, 3), - 23: ("GPSDestBearingRef", ASCII, 2), - 24: ("GPSDestBearing", RATIONAL, 1), - 25: ("GPSDestDistanceRef", ASCII, 2), - 26: ("GPSDestDistance", RATIONAL, 1), - 27: ("GPSProcessingMethod", UNDEFINED, 0), - 28: ("GPSAreaInformation", UNDEFINED, 0), - 29: ("GPSDateStamp", ASCII, 11), - 30: ("GPSDifferential", SHORT, 1), - }, - # InteroperabilityIFD - 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)}, -} - -# Legacy Tags structure -# these tags aren't included above, but were in the previous versions -TAGS: dict[int | tuple[int, int], str] = { - 347: "JPEGTables", - 700: "XMP", - # Additional Exif Info - 32932: "Wang Annotation", - 33434: "ExposureTime", - 33437: "FNumber", - 33445: "MD FileTag", - 33446: "MD ScalePixel", - 33447: "MD ColorTable", - 33448: "MD LabName", - 33449: "MD SampleInfo", - 33450: "MD PrepDate", - 33451: "MD PrepTime", - 33452: "MD FileUnits", - 33550: "ModelPixelScaleTag", - 33723: "IptcNaaInfo", - 33918: "INGR Packet Data Tag", - 33919: "INGR Flag Registers", - 33920: "IrasB Transformation Matrix", - 33922: "ModelTiepointTag", - 34264: "ModelTransformationTag", - 34377: "PhotoshopInfo", - 34735: "GeoKeyDirectoryTag", - 34736: "GeoDoubleParamsTag", - 34737: "GeoAsciiParamsTag", - 34850: "ExposureProgram", - 34852: "SpectralSensitivity", - 34855: "ISOSpeedRatings", - 34856: "OECF", - 34864: "SensitivityType", - 34865: "StandardOutputSensitivity", - 34866: "RecommendedExposureIndex", - 34867: "ISOSpeed", - 34868: "ISOSpeedLatitudeyyy", - 34869: "ISOSpeedLatitudezzz", - 34908: "HylaFAX FaxRecvParams", - 34909: "HylaFAX FaxSubAddress", - 34910: "HylaFAX FaxRecvTime", - 36864: "ExifVersion", - 36867: "DateTimeOriginal", - 36868: "DateTimeDigitized", - 37121: "ComponentsConfiguration", - 37122: "CompressedBitsPerPixel", - 37724: "ImageSourceData", - 37377: "ShutterSpeedValue", - 37378: "ApertureValue", - 37379: "BrightnessValue", - 37380: "ExposureBiasValue", - 37381: "MaxApertureValue", - 37382: "SubjectDistance", - 37383: "MeteringMode", - 37384: "LightSource", - 37385: "Flash", - 37386: "FocalLength", - 37396: "SubjectArea", - 37500: "MakerNote", - 37510: "UserComment", - 37520: "SubSec", - 37521: "SubSecTimeOriginal", - 37522: "SubsecTimeDigitized", - 40960: "FlashPixVersion", - 40961: "ColorSpace", - 40962: "PixelXDimension", - 40963: "PixelYDimension", - 40964: "RelatedSoundFile", - 40965: "InteroperabilityIFD", - 41483: "FlashEnergy", - 41484: "SpatialFrequencyResponse", - 41486: "FocalPlaneXResolution", - 41487: "FocalPlaneYResolution", - 41488: "FocalPlaneResolutionUnit", - 41492: "SubjectLocation", - 41493: "ExposureIndex", - 41495: "SensingMethod", - 41728: "FileSource", - 41729: "SceneType", - 41730: "CFAPattern", - 41985: "CustomRendered", - 41986: "ExposureMode", - 41987: "WhiteBalance", - 41988: "DigitalZoomRatio", - 41989: "FocalLengthIn35mmFilm", - 41990: "SceneCaptureType", - 41991: "GainControl", - 41992: "Contrast", - 41993: "Saturation", - 41994: "Sharpness", - 41995: "DeviceSettingDescription", - 41996: "SubjectDistanceRange", - 42016: "ImageUniqueID", - 42032: "CameraOwnerName", - 42033: "BodySerialNumber", - 42034: "LensSpecification", - 42035: "LensMake", - 42036: "LensModel", - 42037: "LensSerialNumber", - 42112: "GDAL_METADATA", - 42113: "GDAL_NODATA", - 42240: "Gamma", - 50215: "Oce Scanjob Description", - 50216: "Oce Application Selector", - 50217: "Oce Identification Number", - 50218: "Oce ImageLogic Characteristics", - # Adobe DNG - 50706: "DNGVersion", - 50707: "DNGBackwardVersion", - 50708: "UniqueCameraModel", - 50709: "LocalizedCameraModel", - 50710: "CFAPlaneColor", - 50711: "CFALayout", - 50712: "LinearizationTable", - 50713: "BlackLevelRepeatDim", - 50714: "BlackLevel", - 50715: "BlackLevelDeltaH", - 50716: "BlackLevelDeltaV", - 50717: "WhiteLevel", - 50718: "DefaultScale", - 50719: "DefaultCropOrigin", - 50720: "DefaultCropSize", - 50721: "ColorMatrix1", - 50722: "ColorMatrix2", - 50723: "CameraCalibration1", - 50724: "CameraCalibration2", - 50725: "ReductionMatrix1", - 50726: "ReductionMatrix2", - 50727: "AnalogBalance", - 50728: "AsShotNeutral", - 50729: "AsShotWhiteXY", - 50730: "BaselineExposure", - 50731: "BaselineNoise", - 50732: "BaselineSharpness", - 50733: "BayerGreenSplit", - 50734: "LinearResponseLimit", - 50735: "CameraSerialNumber", - 50736: "LensInfo", - 50737: "ChromaBlurRadius", - 50738: "AntiAliasStrength", - 50740: "DNGPrivateData", - 50778: "CalibrationIlluminant1", - 50779: "CalibrationIlluminant2", - 50784: "Alias Layer Metadata", -} - -TAGS_V2: dict[int, TagInfo] = {} -TAGS_V2_GROUPS: dict[int, dict[int, TagInfo]] = {} - - -def _populate() -> None: - for k, v in _tags_v2.items(): - # Populate legacy structure. - TAGS[k] = v[0] - if len(v) == 4: - for sk, sv in v[3].items(): - TAGS[(k, sv)] = sk - - TAGS_V2[k] = TagInfo(k, *v) - - for group, tags in _tags_v2_groups.items(): - TAGS_V2_GROUPS[group] = {k: TagInfo(k, *v) for k, v in tags.items()} - - -_populate() -## -# Map type numbers to type names -- defined in ImageFileDirectory. - -TYPES: dict[int, str] = {} - -# -# These tags are handled by default in libtiff, without -# adding to the custom dictionary. From tif_dir.c, searching for -# case TIFFTAG in the _TIFFVSetField function: -# Line: item. -# 148: case TIFFTAG_SUBFILETYPE: -# 151: case TIFFTAG_IMAGEWIDTH: -# 154: case TIFFTAG_IMAGELENGTH: -# 157: case TIFFTAG_BITSPERSAMPLE: -# 181: case TIFFTAG_COMPRESSION: -# 202: case TIFFTAG_PHOTOMETRIC: -# 205: case TIFFTAG_THRESHHOLDING: -# 208: case TIFFTAG_FILLORDER: -# 214: case TIFFTAG_ORIENTATION: -# 221: case TIFFTAG_SAMPLESPERPIXEL: -# 228: case TIFFTAG_ROWSPERSTRIP: -# 238: case TIFFTAG_MINSAMPLEVALUE: -# 241: case TIFFTAG_MAXSAMPLEVALUE: -# 244: case TIFFTAG_SMINSAMPLEVALUE: -# 247: case TIFFTAG_SMAXSAMPLEVALUE: -# 250: case TIFFTAG_XRESOLUTION: -# 256: case TIFFTAG_YRESOLUTION: -# 262: case TIFFTAG_PLANARCONFIG: -# 268: case TIFFTAG_XPOSITION: -# 271: case TIFFTAG_YPOSITION: -# 274: case TIFFTAG_RESOLUTIONUNIT: -# 280: case TIFFTAG_PAGENUMBER: -# 284: case TIFFTAG_HALFTONEHINTS: -# 288: case TIFFTAG_COLORMAP: -# 294: case TIFFTAG_EXTRASAMPLES: -# 298: case TIFFTAG_MATTEING: -# 305: case TIFFTAG_TILEWIDTH: -# 316: case TIFFTAG_TILELENGTH: -# 327: case TIFFTAG_TILEDEPTH: -# 333: case TIFFTAG_DATATYPE: -# 344: case TIFFTAG_SAMPLEFORMAT: -# 361: case TIFFTAG_IMAGEDEPTH: -# 364: case TIFFTAG_SUBIFD: -# 376: case TIFFTAG_YCBCRPOSITIONING: -# 379: case TIFFTAG_YCBCRSUBSAMPLING: -# 383: case TIFFTAG_TRANSFERFUNCTION: -# 389: case TIFFTAG_REFERENCEBLACKWHITE: -# 393: case TIFFTAG_INKNAMES: - -# Following pseudo-tags are also handled by default in libtiff: -# TIFFTAG_JPEGQUALITY 65537 - -# some of these are not in our TAGS_V2 dict and were included from tiff.h - -# This list also exists in encode.c -LIBTIFF_CORE = { - 255, - 256, - 257, - 258, - 259, - 262, - 263, - 266, - 274, - 277, - 278, - 280, - 281, - 340, - 341, - 282, - 283, - 284, - 286, - 287, - 296, - 297, - 321, - 320, - 338, - 32995, - 322, - 323, - 32998, - 32996, - 339, - 32997, - 330, - 531, - 530, - 301, - 532, - 333, - # as above - 269, # this has been in our tests forever, and works - 65537, -} - -LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes -LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff -LIBTIFF_CORE.remove(323) # Tiled images -LIBTIFF_CORE.remove(333) # Ink Names either - -# Note to advanced users: There may be combinations of these -# parameters and values that when added properly, will work and -# produce valid tiff images that may work in your application. -# It is safe to add and remove tags from this set from Pillow's point -# of view so long as you test against libtiff. diff --git a/python_modules/PIL/WalImageFile.py b/python_modules/PIL/WalImageFile.py deleted file mode 100644 index 87e32878b..000000000 --- a/python_modules/PIL/WalImageFile.py +++ /dev/null @@ -1,127 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# WAL file handling -# -# History: -# 2003-04-23 fl created -# -# Copyright (c) 2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -""" -This reader is based on the specification available from: -https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml -and has been tested with a few sample files found using google. - -.. note:: - This format cannot be automatically recognized, so the reader - is not registered for use with :py:func:`PIL.Image.open()`. - To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead. -""" -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile -from ._binary import i32le as i32 -from ._typing import StrOrBytesPath - - -class WalImageFile(ImageFile.ImageFile): - format = "WAL" - format_description = "Quake2 Texture" - - def _open(self) -> None: - self._mode = "P" - - # read header fields - header = self.fp.read(32 + 24 + 32 + 12) - self._size = i32(header, 32), i32(header, 36) - Image._decompression_bomb_check(self.size) - - # load pixel data - offset = i32(header, 40) - self.fp.seek(offset) - - # strings are null-terminated - self.info["name"] = header[:32].split(b"\0", 1)[0] - next_name = header[56 : 56 + 32].split(b"\0", 1)[0] - if next_name: - self.info["next_name"] = next_name - - def load(self) -> Image.core.PixelAccess | None: - if self._im is None: - self.im = Image.core.new(self.mode, self.size) - self.frombytes(self.fp.read(self.size[0] * self.size[1])) - self.putpalette(quake2palette) - return Image.Image.load(self) - - -def open(filename: StrOrBytesPath | IO[bytes]) -> WalImageFile: - """ - Load texture from a Quake2 WAL texture file. - - By default, a Quake2 standard palette is attached to the texture. - To override the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. - - :param filename: WAL file name, or an opened file handle. - :returns: An image instance. - """ - return WalImageFile(filename) - - -quake2palette = ( - # default palette taken from piffo 0.93 by Hans Häggström - b"\x01\x01\x01\x0b\x0b\x0b\x12\x12\x12\x17\x17\x17\x1b\x1b\x1b\x1e" - b"\x1e\x1e\x22\x22\x22\x26\x26\x26\x29\x29\x29\x2c\x2c\x2c\x2f\x2f" - b"\x2f\x32\x32\x32\x35\x35\x35\x37\x37\x37\x3a\x3a\x3a\x3c\x3c\x3c" - b"\x24\x1e\x13\x22\x1c\x12\x20\x1b\x12\x1f\x1a\x10\x1d\x19\x10\x1b" - b"\x17\x0f\x1a\x16\x0f\x18\x14\x0d\x17\x13\x0d\x16\x12\x0d\x14\x10" - b"\x0b\x13\x0f\x0b\x10\x0d\x0a\x0f\x0b\x0a\x0d\x0b\x07\x0b\x0a\x07" - b"\x23\x23\x26\x22\x22\x25\x22\x20\x23\x21\x1f\x22\x20\x1e\x20\x1f" - b"\x1d\x1e\x1d\x1b\x1c\x1b\x1a\x1a\x1a\x19\x19\x18\x17\x17\x17\x16" - b"\x16\x14\x14\x14\x13\x13\x13\x10\x10\x10\x0f\x0f\x0f\x0d\x0d\x0d" - b"\x2d\x28\x20\x29\x24\x1c\x27\x22\x1a\x25\x1f\x17\x38\x2e\x1e\x31" - b"\x29\x1a\x2c\x25\x17\x26\x20\x14\x3c\x30\x14\x37\x2c\x13\x33\x28" - b"\x12\x2d\x24\x10\x28\x1f\x0f\x22\x1a\x0b\x1b\x14\x0a\x13\x0f\x07" - b"\x31\x1a\x16\x30\x17\x13\x2e\x16\x10\x2c\x14\x0d\x2a\x12\x0b\x27" - b"\x0f\x0a\x25\x0f\x07\x21\x0d\x01\x1e\x0b\x01\x1c\x0b\x01\x1a\x0b" - b"\x01\x18\x0a\x01\x16\x0a\x01\x13\x0a\x01\x10\x07\x01\x0d\x07\x01" - b"\x29\x23\x1e\x27\x21\x1c\x26\x20\x1b\x25\x1f\x1a\x23\x1d\x19\x21" - b"\x1c\x18\x20\x1b\x17\x1e\x19\x16\x1c\x18\x14\x1b\x17\x13\x19\x14" - b"\x10\x17\x13\x0f\x14\x10\x0d\x12\x0f\x0b\x0f\x0b\x0a\x0b\x0a\x07" - b"\x26\x1a\x0f\x23\x19\x0f\x20\x17\x0f\x1c\x16\x0f\x19\x13\x0d\x14" - b"\x10\x0b\x10\x0d\x0a\x0b\x0a\x07\x33\x22\x1f\x35\x29\x26\x37\x2f" - b"\x2d\x39\x35\x34\x37\x39\x3a\x33\x37\x39\x30\x34\x36\x2b\x31\x34" - b"\x27\x2e\x31\x22\x2b\x2f\x1d\x28\x2c\x17\x25\x2a\x0f\x20\x26\x0d" - b"\x1e\x25\x0b\x1c\x22\x0a\x1b\x20\x07\x19\x1e\x07\x17\x1b\x07\x14" - b"\x18\x01\x12\x16\x01\x0f\x12\x01\x0b\x0d\x01\x07\x0a\x01\x01\x01" - b"\x2c\x21\x21\x2a\x1f\x1f\x29\x1d\x1d\x27\x1c\x1c\x26\x1a\x1a\x24" - b"\x18\x18\x22\x17\x17\x21\x16\x16\x1e\x13\x13\x1b\x12\x12\x18\x10" - b"\x10\x16\x0d\x0d\x12\x0b\x0b\x0d\x0a\x0a\x0a\x07\x07\x01\x01\x01" - b"\x2e\x30\x29\x2d\x2e\x27\x2b\x2c\x26\x2a\x2a\x24\x28\x29\x23\x27" - b"\x27\x21\x26\x26\x1f\x24\x24\x1d\x22\x22\x1c\x1f\x1f\x1a\x1c\x1c" - b"\x18\x19\x19\x16\x17\x17\x13\x13\x13\x10\x0f\x0f\x0d\x0b\x0b\x0a" - b"\x30\x1e\x1b\x2d\x1c\x19\x2c\x1a\x17\x2a\x19\x14\x28\x17\x13\x26" - b"\x16\x10\x24\x13\x0f\x21\x12\x0d\x1f\x10\x0b\x1c\x0f\x0a\x19\x0d" - b"\x0a\x16\x0b\x07\x12\x0a\x07\x0f\x07\x01\x0a\x01\x01\x01\x01\x01" - b"\x28\x29\x38\x26\x27\x36\x25\x26\x34\x24\x24\x31\x22\x22\x2f\x20" - b"\x21\x2d\x1e\x1f\x2a\x1d\x1d\x27\x1b\x1b\x25\x19\x19\x21\x17\x17" - b"\x1e\x14\x14\x1b\x13\x12\x17\x10\x0f\x13\x0d\x0b\x0f\x0a\x07\x07" - b"\x2f\x32\x29\x2d\x30\x26\x2b\x2e\x24\x29\x2c\x21\x27\x2a\x1e\x25" - b"\x28\x1c\x23\x26\x1a\x21\x25\x18\x1e\x22\x14\x1b\x1f\x10\x19\x1c" - b"\x0d\x17\x1a\x0a\x13\x17\x07\x10\x13\x01\x0d\x0f\x01\x0a\x0b\x01" - b"\x01\x3f\x01\x13\x3c\x0b\x1b\x39\x10\x20\x35\x14\x23\x31\x17\x23" - b"\x2d\x18\x23\x29\x18\x3f\x3f\x3f\x3f\x3f\x39\x3f\x3f\x31\x3f\x3f" - b"\x2a\x3f\x3f\x20\x3f\x3f\x14\x3f\x3c\x12\x3f\x39\x0f\x3f\x35\x0b" - b"\x3f\x32\x07\x3f\x2d\x01\x3d\x2a\x01\x3b\x26\x01\x39\x21\x01\x37" - b"\x1d\x01\x34\x1a\x01\x32\x16\x01\x2f\x12\x01\x2d\x0f\x01\x2a\x0b" - b"\x01\x27\x07\x01\x23\x01\x01\x1d\x01\x01\x17\x01\x01\x10\x01\x01" - b"\x3d\x01\x01\x19\x19\x3f\x3f\x01\x01\x01\x01\x3f\x16\x16\x13\x10" - b"\x10\x0f\x0d\x0d\x0b\x3c\x2e\x2a\x36\x27\x20\x30\x21\x18\x29\x1b" - b"\x10\x3c\x39\x37\x37\x32\x2f\x31\x2c\x28\x2b\x26\x21\x30\x22\x20" -) diff --git a/python_modules/PIL/WebPImagePlugin.py b/python_modules/PIL/WebPImagePlugin.py deleted file mode 100644 index 1716a18cc..000000000 --- a/python_modules/PIL/WebPImagePlugin.py +++ /dev/null @@ -1,320 +0,0 @@ -from __future__ import annotations - -from io import BytesIO -from typing import IO, Any - -from . import Image, ImageFile - -try: - from . import _webp - - SUPPORTED = True -except ImportError: - SUPPORTED = False - - -_VP8_MODES_BY_IDENTIFIER = { - b"VP8 ": "RGB", - b"VP8X": "RGBA", - b"VP8L": "RGBA", # lossless -} - - -def _accept(prefix: bytes) -> bool | str: - is_riff_file_format = prefix.startswith(b"RIFF") - is_webp_file = prefix[8:12] == b"WEBP" - is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER - - if is_riff_file_format and is_webp_file and is_valid_vp8_mode: - if not SUPPORTED: - return ( - "image file could not be identified because WEBP support not installed" - ) - return True - return False - - -class WebPImageFile(ImageFile.ImageFile): - format = "WEBP" - format_description = "WebP image" - __loaded = 0 - __logical_frame = 0 - - def _open(self) -> None: - # Use the newer AnimDecoder API to parse the (possibly) animated file, - # and access muxed chunks like ICC/EXIF/XMP. - self._decoder = _webp.WebPAnimDecoder(self.fp.read()) - - # Get info from decoder - self._size, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() - self.info["loop"] = loop_count - bg_a, bg_r, bg_g, bg_b = ( - (bgcolor >> 24) & 0xFF, - (bgcolor >> 16) & 0xFF, - (bgcolor >> 8) & 0xFF, - bgcolor & 0xFF, - ) - self.info["background"] = (bg_r, bg_g, bg_b, bg_a) - self.n_frames = frame_count - self.is_animated = self.n_frames > 1 - self._mode = "RGB" if mode == "RGBX" else mode - self.rawmode = mode - - # Attempt to read ICC / EXIF / XMP chunks from file - icc_profile = self._decoder.get_chunk("ICCP") - exif = self._decoder.get_chunk("EXIF") - xmp = self._decoder.get_chunk("XMP ") - if icc_profile: - self.info["icc_profile"] = icc_profile - if exif: - self.info["exif"] = exif - if xmp: - self.info["xmp"] = xmp - - # Initialize seek state - self._reset(reset=False) - - def _getexif(self) -> dict[int, Any] | None: - if "exif" not in self.info: - return None - return self.getexif()._get_merged_dict() - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - - # Set logical frame to requested position - self.__logical_frame = frame - - def _reset(self, reset: bool = True) -> None: - if reset: - self._decoder.reset() - self.__physical_frame = 0 - self.__loaded = -1 - self.__timestamp = 0 - - def _get_next(self) -> tuple[bytes, int, int]: - # Get next frame - ret = self._decoder.get_next() - self.__physical_frame += 1 - - # Check if an error occurred - if ret is None: - self._reset() # Reset just to be safe - self.seek(0) - msg = "failed to decode next frame in WebP file" - raise EOFError(msg) - - # Compute duration - data, timestamp = ret - duration = timestamp - self.__timestamp - self.__timestamp = timestamp - - # libwebp gives frame end, adjust to start of frame - timestamp -= duration - return data, timestamp, duration - - def _seek(self, frame: int) -> None: - if self.__physical_frame == frame: - return # Nothing to do - if frame < self.__physical_frame: - self._reset() # Rewind to beginning - while self.__physical_frame < frame: - self._get_next() # Advance to the requested frame - - def load(self) -> Image.core.PixelAccess | None: - if self.__loaded != self.__logical_frame: - self._seek(self.__logical_frame) - - # We need to load the image data for this frame - data, timestamp, duration = self._get_next() - self.info["timestamp"] = timestamp - self.info["duration"] = duration - self.__loaded = self.__logical_frame - - # Set tile - if self.fp and self._exclusive_fp: - self.fp.close() - self.fp = BytesIO(data) - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)] - - return super().load() - - def load_seek(self, pos: int) -> None: - pass - - def tell(self) -> int: - return self.__logical_frame - - -def _convert_frame(im: Image.Image) -> Image.Image: - # Make sure image mode is supported - if im.mode not in ("RGBX", "RGBA", "RGB"): - im = im.convert("RGBA" if im.has_transparency_data else "RGB") - return im - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - encoderinfo = im.encoderinfo.copy() - append_images = list(encoderinfo.get("append_images", [])) - - # If total frame count is 1, then save using the legacy API, which - # will preserve non-alpha modes - total = 0 - for ims in [im] + append_images: - total += getattr(ims, "n_frames", 1) - if total == 1: - _save(im, fp, filename) - return - - background: int | tuple[int, ...] = (0, 0, 0, 0) - if "background" in encoderinfo: - background = encoderinfo["background"] - elif "background" in im.info: - background = im.info["background"] - if isinstance(background, int): - # GifImagePlugin stores a global color table index in - # info["background"]. So it must be converted to an RGBA value - palette = im.getpalette() - if palette: - r, g, b = palette[background * 3 : (background + 1) * 3] - background = (r, g, b, 255) - else: - background = (background, background, background, 255) - - duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) - loop = im.encoderinfo.get("loop", 0) - minimize_size = im.encoderinfo.get("minimize_size", False) - kmin = im.encoderinfo.get("kmin", None) - kmax = im.encoderinfo.get("kmax", None) - allow_mixed = im.encoderinfo.get("allow_mixed", False) - verbose = False - lossless = im.encoderinfo.get("lossless", False) - quality = im.encoderinfo.get("quality", 80) - alpha_quality = im.encoderinfo.get("alpha_quality", 100) - method = im.encoderinfo.get("method", 0) - icc_profile = im.encoderinfo.get("icc_profile") or "" - exif = im.encoderinfo.get("exif", "") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - xmp = im.encoderinfo.get("xmp", "") - if allow_mixed: - lossless = False - - # Sensible keyframe defaults are from gif2webp.c script - if kmin is None: - kmin = 9 if lossless else 3 - if kmax is None: - kmax = 17 if lossless else 5 - - # Validate background color - if ( - not isinstance(background, (list, tuple)) - or len(background) != 4 - or not all(0 <= v < 256 for v in background) - ): - msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}" - raise OSError(msg) - - # Convert to packed uint - bg_r, bg_g, bg_b, bg_a = background - background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0) - - # Setup the WebP animation encoder - enc = _webp.WebPAnimEncoder( - im.size, - background, - loop, - minimize_size, - kmin, - kmax, - allow_mixed, - verbose, - ) - - # Add each frame - frame_idx = 0 - timestamp = 0 - cur_idx = im.tell() - try: - for ims in [im] + append_images: - # Get number of frames in this image - nfr = getattr(ims, "n_frames", 1) - - for idx in range(nfr): - ims.seek(idx) - - frame = _convert_frame(ims) - - # Append the frame to the animation encoder - enc.add( - frame.getim(), - round(timestamp), - lossless, - quality, - alpha_quality, - method, - ) - - # Update timestamp and frame index - if isinstance(duration, (list, tuple)): - timestamp += duration[frame_idx] - else: - timestamp += duration - frame_idx += 1 - - finally: - im.seek(cur_idx) - - # Force encoder to flush frames - enc.add(None, round(timestamp), lossless, quality, alpha_quality, 0) - - # Get the final output from the encoder - data = enc.assemble(icc_profile, exif, xmp) - if data is None: - msg = "cannot write file as WebP (encoder returned None)" - raise OSError(msg) - - fp.write(data) - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - lossless = im.encoderinfo.get("lossless", False) - quality = im.encoderinfo.get("quality", 80) - alpha_quality = im.encoderinfo.get("alpha_quality", 100) - icc_profile = im.encoderinfo.get("icc_profile") or "" - exif = im.encoderinfo.get("exif", b"") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - if exif.startswith(b"Exif\x00\x00"): - exif = exif[6:] - xmp = im.encoderinfo.get("xmp", "") - method = im.encoderinfo.get("method", 4) - exact = 1 if im.encoderinfo.get("exact") else 0 - - im = _convert_frame(im) - - data = _webp.WebPEncode( - im.getim(), - lossless, - float(quality), - float(alpha_quality), - icc_profile, - method, - exact, - exif, - xmp, - ) - if data is None: - msg = "cannot write file as WebP (encoder returned None)" - raise OSError(msg) - - fp.write(data) - - -Image.register_open(WebPImageFile.format, WebPImageFile, _accept) -if SUPPORTED: - Image.register_save(WebPImageFile.format, _save) - Image.register_save_all(WebPImageFile.format, _save_all) - Image.register_extension(WebPImageFile.format, ".webp") - Image.register_mime(WebPImageFile.format, "image/webp") diff --git a/python_modules/PIL/WmfImagePlugin.py b/python_modules/PIL/WmfImagePlugin.py deleted file mode 100644 index d569cb4b8..000000000 --- a/python_modules/PIL/WmfImagePlugin.py +++ /dev/null @@ -1,186 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# WMF stub codec -# -# history: -# 1996-12-14 fl Created -# 2004-02-22 fl Turned into a stub driver -# 2004-02-23 fl Added EMF support -# -# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -# WMF/EMF reference documentation: -# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-WMF/[MS-WMF].pdf -# http://wvware.sourceforge.net/caolan/index.html -# http://wvware.sourceforge.net/caolan/ora-wmf.html -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile -from ._binary import i16le as word -from ._binary import si16le as short -from ._binary import si32le as _long - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific WMF image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -if hasattr(Image.core, "drawwmf"): - # install default handler (windows only) - - class WmfHandler(ImageFile.StubHandler): - def open(self, im: ImageFile.StubImageFile) -> None: - im._mode = "RGB" - self.bbox = im.info["wmf_bbox"] - - def load(self, im: ImageFile.StubImageFile) -> Image.Image: - im.fp.seek(0) # rewind - return Image.frombytes( - "RGB", - im.size, - Image.core.drawwmf(im.fp.read(), im.size, self.bbox), - "raw", - "BGR", - (im.size[0] * 3 + 3) & -4, - -1, - ) - - register_handler(WmfHandler()) - -# -# -------------------------------------------------------------------- -# Read WMF file - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith((b"\xd7\xcd\xc6\x9a\x00\x00", b"\x01\x00\x00\x00")) - - -## -# Image plugin for Windows metafiles. - - -class WmfStubImageFile(ImageFile.StubImageFile): - format = "WMF" - format_description = "Windows Metafile" - - def _open(self) -> None: - # check placable header - s = self.fp.read(44) - - if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): - # placeable windows metafile - - # get units per inch - inch = word(s, 14) - if inch == 0: - msg = "Invalid inch" - raise ValueError(msg) - self._inch: tuple[float, float] = inch, inch - - # get bounding box - x0 = short(s, 6) - y0 = short(s, 8) - x1 = short(s, 10) - y1 = short(s, 12) - - # normalize size to 72 dots per inch - self.info["dpi"] = 72 - size = ( - (x1 - x0) * self.info["dpi"] // inch, - (y1 - y0) * self.info["dpi"] // inch, - ) - - self.info["wmf_bbox"] = x0, y0, x1, y1 - - # sanity check (standard metafile header) - if s[22:26] != b"\x01\x00\t\x00": - msg = "Unsupported WMF file format" - raise SyntaxError(msg) - - elif s.startswith(b"\x01\x00\x00\x00") and s[40:44] == b" EMF": - # enhanced metafile - - # get bounding box - x0 = _long(s, 8) - y0 = _long(s, 12) - x1 = _long(s, 16) - y1 = _long(s, 20) - - # get frame (in 0.01 millimeter units) - frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36) - - size = x1 - x0, y1 - y0 - - # calculate dots per inch from bbox and frame - xdpi = 2540.0 * (x1 - x0) / (frame[2] - frame[0]) - ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) - - self.info["wmf_bbox"] = x0, y0, x1, y1 - - if xdpi == ydpi: - self.info["dpi"] = xdpi - else: - self.info["dpi"] = xdpi, ydpi - self._inch = xdpi, ydpi - - else: - msg = "Unsupported file format" - raise SyntaxError(msg) - - self._mode = "RGB" - self._size = size - - loader = self._load() - if loader: - loader.open(self) - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - def load( - self, dpi: float | tuple[float, float] | None = None - ) -> Image.core.PixelAccess | None: - if dpi is not None: - self.info["dpi"] = dpi - x0, y0, x1, y1 = self.info["wmf_bbox"] - if not isinstance(dpi, tuple): - dpi = dpi, dpi - self._size = ( - int((x1 - x0) * dpi[0] / self._inch[0]), - int((y1 - y0) * dpi[1] / self._inch[1]), - ) - return super().load() - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "WMF save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -# -------------------------------------------------------------------- -# Registry stuff - - -Image.register_open(WmfStubImageFile.format, WmfStubImageFile, _accept) -Image.register_save(WmfStubImageFile.format, _save) - -Image.register_extensions(WmfStubImageFile.format, [".wmf", ".emf"]) diff --git a/python_modules/PIL/XVThumbImagePlugin.py b/python_modules/PIL/XVThumbImagePlugin.py deleted file mode 100644 index cde28388f..000000000 --- a/python_modules/PIL/XVThumbImagePlugin.py +++ /dev/null @@ -1,83 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# XV Thumbnail file handler by Charles E. "Gene" Cash -# (gcash@magicnet.net) -# -# see xvcolor.c and xvbrowse.c in the sources to John Bradley's XV, -# available from ftp://ftp.cis.upenn.edu/pub/xv/ -# -# history: -# 98-08-15 cec created (b/w only) -# 98-12-09 cec added color palette -# 98-12-28 fl added to PIL (with only a few very minor modifications) -# -# To do: -# FIXME: make save work (this requires quantization support) -# -from __future__ import annotations - -from . import Image, ImageFile, ImagePalette -from ._binary import o8 - -_MAGIC = b"P7 332" - -# standard color palette for thumbnails (RGB332) -PALETTE = b"" -for r in range(8): - for g in range(8): - for b in range(4): - PALETTE = PALETTE + ( - o8((r * 255) // 7) + o8((g * 255) // 7) + o8((b * 255) // 3) - ) - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(_MAGIC) - - -## -# Image plugin for XV thumbnail images. - - -class XVThumbImageFile(ImageFile.ImageFile): - format = "XVThumb" - format_description = "XV thumbnail image" - - def _open(self) -> None: - # check magic - assert self.fp is not None - - if not _accept(self.fp.read(6)): - msg = "not an XV thumbnail file" - raise SyntaxError(msg) - - # Skip to beginning of next line - self.fp.readline() - - # skip info comments - while True: - s = self.fp.readline() - if not s: - msg = "Unexpected EOF reading XV thumbnail file" - raise SyntaxError(msg) - if s[0] != 35: # ie. when not a comment: '#' - break - - # parse header line (already read) - s = s.strip().split() - - self._mode = "P" - self._size = int(s[0]), int(s[1]) - - self.palette = ImagePalette.raw("RGB", PALETTE) - - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode) - ] - - -# -------------------------------------------------------------------- - -Image.register_open(XVThumbImageFile.format, XVThumbImageFile, _accept) diff --git a/python_modules/PIL/XbmImagePlugin.py b/python_modules/PIL/XbmImagePlugin.py deleted file mode 100644 index 1e57aa162..000000000 --- a/python_modules/PIL/XbmImagePlugin.py +++ /dev/null @@ -1,98 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# XBM File handling -# -# History: -# 1995-09-08 fl Created -# 1996-11-01 fl Added save support -# 1997-07-07 fl Made header parser more tolerant -# 1997-07-22 fl Fixed yet another parser bug -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) -# 2001-05-13 fl Added hotspot handling (based on code from Bernhard Herzog) -# 2004-02-24 fl Allow some whitespace before first #define -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1996-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re -from typing import IO - -from . import Image, ImageFile - -# XBM header -xbm_head = re.compile( - rb"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" - b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+" - b"(?P" - b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" - b"#define[ \t]+[^_]*_y_hot[ \t]+(?P[0-9]+)[\r\n]+" - b")?" - rb"[\000-\377]*_bits\[]" -) - - -def _accept(prefix: bytes) -> bool: - return prefix.lstrip().startswith(b"#define") - - -## -# Image plugin for X11 bitmaps. - - -class XbmImageFile(ImageFile.ImageFile): - format = "XBM" - format_description = "X11 Bitmap" - - def _open(self) -> None: - assert self.fp is not None - - m = xbm_head.match(self.fp.read(512)) - - if not m: - msg = "not a XBM file" - raise SyntaxError(msg) - - xsize = int(m.group("width")) - ysize = int(m.group("height")) - - if m.group("hotspot"): - self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) - - self._mode = "1" - self._size = xsize, ysize - - self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end())] - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode != "1": - msg = f"cannot write mode {im.mode} as XBM" - raise OSError(msg) - - fp.write(f"#define im_width {im.size[0]}\n".encode("ascii")) - fp.write(f"#define im_height {im.size[1]}\n".encode("ascii")) - - hotspot = im.encoderinfo.get("hotspot") - if hotspot: - fp.write(f"#define im_x_hot {hotspot[0]}\n".encode("ascii")) - fp.write(f"#define im_y_hot {hotspot[1]}\n".encode("ascii")) - - fp.write(b"static char im_bits[] = {\n") - - ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size)]) - - fp.write(b"};\n") - - -Image.register_open(XbmImageFile.format, XbmImageFile, _accept) -Image.register_save(XbmImageFile.format, _save) - -Image.register_extension(XbmImageFile.format, ".xbm") - -Image.register_mime(XbmImageFile.format, "image/xbm") diff --git a/python_modules/PIL/XpmImagePlugin.py b/python_modules/PIL/XpmImagePlugin.py deleted file mode 100644 index 3be240fbc..000000000 --- a/python_modules/PIL/XpmImagePlugin.py +++ /dev/null @@ -1,157 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# XPM File handling -# -# History: -# 1996-12-29 fl Created -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) -# -# Copyright (c) Secret Labs AB 1997-2001. -# Copyright (c) Fredrik Lundh 1996-2001. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re - -from . import Image, ImageFile, ImagePalette -from ._binary import o8 - -# XPM header -xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"/* XPM */") - - -## -# Image plugin for X11 pixel maps. - - -class XpmImageFile(ImageFile.ImageFile): - format = "XPM" - format_description = "X11 Pixel Map" - - def _open(self) -> None: - assert self.fp is not None - if not _accept(self.fp.read(9)): - msg = "not an XPM file" - raise SyntaxError(msg) - - # skip forward to next string - while True: - line = self.fp.readline() - if not line: - msg = "broken XPM file" - raise SyntaxError(msg) - m = xpm_head.match(line) - if m: - break - - self._size = int(m.group(1)), int(m.group(2)) - - palette_length = int(m.group(3)) - bpp = int(m.group(4)) - - # - # load palette description - - palette = {} - - for _ in range(palette_length): - line = self.fp.readline().rstrip() - - c = line[1 : bpp + 1] - s = line[bpp + 1 : -2].split() - - for i in range(0, len(s), 2): - if s[i] == b"c": - # process colour key - rgb = s[i + 1] - if rgb == b"None": - self.info["transparency"] = c - elif rgb.startswith(b"#"): - rgb_int = int(rgb[1:], 16) - palette[c] = ( - o8((rgb_int >> 16) & 255) - + o8((rgb_int >> 8) & 255) - + o8(rgb_int & 255) - ) - else: - # unknown colour - msg = "cannot read this XPM file" - raise ValueError(msg) - break - - else: - # missing colour key - msg = "cannot read this XPM file" - raise ValueError(msg) - - args: tuple[int, dict[bytes, bytes] | tuple[bytes, ...]] - if palette_length > 256: - self._mode = "RGB" - args = (bpp, palette) - else: - self._mode = "P" - self.palette = ImagePalette.raw("RGB", b"".join(palette.values())) - args = (bpp, tuple(palette.keys())) - - self.tile = [ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), args)] - - def load_read(self, read_bytes: int) -> bytes: - # - # load all image data in one chunk - - xsize, ysize = self.size - - assert self.fp is not None - s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] - - return b"".join(s) - - -class XpmDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - - data = bytearray() - bpp, palette = self.args - dest_length = self.state.xsize * self.state.ysize - if self.mode == "RGB": - dest_length *= 3 - pixel_header = False - while len(data) < dest_length: - line = self.fd.readline() - if not line: - break - if line.rstrip() == b"/* pixels */" and not pixel_header: - pixel_header = True - continue - line = b'"'.join(line.split(b'"')[1:-1]) - for i in range(0, len(line), bpp): - key = line[i : i + bpp] - if self.mode == "RGB": - data += palette[key] - else: - data += o8(palette.index(key)) - self.set_as_raw(bytes(data)) - return -1, 0 - - -# -# Registry - - -Image.register_open(XpmImageFile.format, XpmImageFile, _accept) -Image.register_decoder("xpm", XpmDecoder) - -Image.register_extension(XpmImageFile.format, ".xpm") - -Image.register_mime(XpmImageFile.format, "image/xpm") diff --git a/python_modules/PIL/__init__.py b/python_modules/PIL/__init__.py deleted file mode 100644 index 6e4c23f89..000000000 --- a/python_modules/PIL/__init__.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Pillow (Fork of the Python Imaging Library) - -Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors. - https://github.com/python-pillow/Pillow/ - -Pillow is forked from PIL 1.1.7. - -PIL is the Python Imaging Library by Fredrik Lundh and contributors. -Copyright (c) 1999 by Secret Labs AB. - -Use PIL.__version__ for this Pillow version. - -;-) -""" - -from __future__ import annotations - -from . import _version - -# VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION was removed in Pillow 9.0.0. -# Use __version__ instead. -__version__ = _version.__version__ -del _version - - -_plugins = [ - "AvifImagePlugin", - "BlpImagePlugin", - "BmpImagePlugin", - "BufrStubImagePlugin", - "CurImagePlugin", - "DcxImagePlugin", - "DdsImagePlugin", - "EpsImagePlugin", - "FitsImagePlugin", - "FliImagePlugin", - "FpxImagePlugin", - "FtexImagePlugin", - "GbrImagePlugin", - "GifImagePlugin", - "GribStubImagePlugin", - "Hdf5StubImagePlugin", - "IcnsImagePlugin", - "IcoImagePlugin", - "ImImagePlugin", - "ImtImagePlugin", - "IptcImagePlugin", - "JpegImagePlugin", - "Jpeg2KImagePlugin", - "McIdasImagePlugin", - "MicImagePlugin", - "MpegImagePlugin", - "MpoImagePlugin", - "MspImagePlugin", - "PalmImagePlugin", - "PcdImagePlugin", - "PcxImagePlugin", - "PdfImagePlugin", - "PixarImagePlugin", - "PngImagePlugin", - "PpmImagePlugin", - "PsdImagePlugin", - "QoiImagePlugin", - "SgiImagePlugin", - "SpiderImagePlugin", - "SunImagePlugin", - "TgaImagePlugin", - "TiffImagePlugin", - "WebPImagePlugin", - "WmfImagePlugin", - "XbmImagePlugin", - "XpmImagePlugin", - "XVThumbImagePlugin", -] - - -class UnidentifiedImageError(OSError): - """ - Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. - - If a PNG image raises this error, setting :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` - to true may allow the image to be opened after all. The setting will ignore missing - data and checksum failures. - """ - - pass diff --git a/python_modules/PIL/__main__.py b/python_modules/PIL/__main__.py deleted file mode 100644 index 043156e89..000000000 --- a/python_modules/PIL/__main__.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import annotations - -import sys - -from .features import pilinfo - -pilinfo(supported_formats="--report" not in sys.argv) diff --git a/python_modules/PIL/_avif.pyi b/python_modules/PIL/_avif.pyi deleted file mode 100644 index e27843e53..000000000 --- a/python_modules/PIL/_avif.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_binary.py b/python_modules/PIL/_binary.py deleted file mode 100644 index 4594ccce3..000000000 --- a/python_modules/PIL/_binary.py +++ /dev/null @@ -1,112 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Binary input/output support routines. -# -# Copyright (c) 1997-2003 by Secret Labs AB -# Copyright (c) 1995-2003 by Fredrik Lundh -# Copyright (c) 2012 by Brian Crowell -# -# See the README file for information on usage and redistribution. -# - - -"""Binary input/output support routines.""" -from __future__ import annotations - -from struct import pack, unpack_from - - -def i8(c: bytes) -> int: - return c[0] - - -def o8(i: int) -> bytes: - return bytes((i & 255,)) - - -# Input, le = little endian, be = big endian -def i16le(c: bytes, o: int = 0) -> int: - """ - Converts a 2-bytes (16 bits) string to an unsigned integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(" int: - """ - Converts a 2-bytes (16 bits) string to a signed integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(" int: - """ - Converts a 2-bytes (16 bits) string to a signed integer, big endian. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(">h", c, o)[0] - - -def i32le(c: bytes, o: int = 0) -> int: - """ - Converts a 4-bytes (32 bits) string to an unsigned integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(" int: - """ - Converts a 4-bytes (32 bits) string to a signed integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(" int: - """ - Converts a 4-bytes (32 bits) string to a signed integer, big endian. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(">i", c, o)[0] - - -def i16be(c: bytes, o: int = 0) -> int: - return unpack_from(">H", c, o)[0] - - -def i32be(c: bytes, o: int = 0) -> int: - return unpack_from(">I", c, o)[0] - - -# Output, le = little endian, be = big endian -def o16le(i: int) -> bytes: - return pack(" bytes: - return pack(" bytes: - return pack(">H", i) - - -def o32be(i: int) -> bytes: - return pack(">I", i) diff --git a/python_modules/PIL/_deprecate.py b/python_modules/PIL/_deprecate.py deleted file mode 100644 index 170d44490..000000000 --- a/python_modules/PIL/_deprecate.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import annotations - -import warnings - -from . import __version__ - - -def deprecate( - deprecated: str, - when: int | None, - replacement: str | None = None, - *, - action: str | None = None, - plural: bool = False, - stacklevel: int = 3, -) -> None: - """ - Deprecations helper. - - :param deprecated: Name of thing to be deprecated. - :param when: Pillow major version to be removed in. - :param replacement: Name of replacement. - :param action: Instead of "replacement", give a custom call to action - e.g. "Upgrade to new thing". - :param plural: if the deprecated thing is plural, needing "are" instead of "is". - - Usually of the form: - - "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). - Use [replacement] instead." - - You can leave out the replacement sentence: - - "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd)" - - Or with another call to action: - - "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). - [action]." - """ - - is_ = "are" if plural else "is" - - if when is None: - removed = "a future version" - elif when <= int(__version__.split(".")[0]): - msg = f"{deprecated} {is_} deprecated and should be removed." - raise RuntimeError(msg) - elif when == 12: - removed = "Pillow 12 (2025-10-15)" - elif when == 13: - removed = "Pillow 13 (2026-10-15)" - else: - msg = f"Unknown removal version: {when}. Update {__name__}?" - raise ValueError(msg) - - if replacement and action: - msg = "Use only one of 'replacement' and 'action'" - raise ValueError(msg) - - if replacement: - action = f". Use {replacement} instead." - elif action: - action = f". {action.rstrip('.')}." - else: - action = "" - - warnings.warn( - f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", - DeprecationWarning, - stacklevel=stacklevel, - ) diff --git a/python_modules/PIL/_imaging.pyi b/python_modules/PIL/_imaging.pyi deleted file mode 100644 index 998bc52eb..000000000 --- a/python_modules/PIL/_imaging.pyi +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Any - -class ImagingCore: - def __getitem__(self, index: int) -> float: ... - def __getattr__(self, name: str) -> Any: ... - -class ImagingFont: - def __getattr__(self, name: str) -> Any: ... - -class ImagingDraw: - def __getattr__(self, name: str) -> Any: ... - -class PixelAccess: - def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: ... - def __setitem__( - self, xy: tuple[int, int], color: float | tuple[int, ...] - ) -> None: ... - -class ImagingDecoder: - def __getattr__(self, name: str) -> Any: ... - -class ImagingEncoder: - def __getattr__(self, name: str) -> Any: ... - -class _Outline: - def close(self) -> None: ... - def __getattr__(self, name: str) -> Any: ... - -def font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ... -def outline() -> _Outline: ... -def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_imagingcms.pyi b/python_modules/PIL/_imagingcms.pyi deleted file mode 100644 index ddcf93ab1..000000000 --- a/python_modules/PIL/_imagingcms.pyi +++ /dev/null @@ -1,143 +0,0 @@ -import datetime -import sys -from typing import Literal, SupportsFloat, TypedDict - -from ._typing import CapsuleType - -littlecms_version: str | None - -_Tuple3f = tuple[float, float, float] -_Tuple2x3f = tuple[_Tuple3f, _Tuple3f] -_Tuple3x3f = tuple[_Tuple3f, _Tuple3f, _Tuple3f] - -class _IccMeasurementCondition(TypedDict): - observer: int - backing: _Tuple3f - geo: str - flare: float - illuminant_type: str - -class _IccViewingCondition(TypedDict): - illuminant: _Tuple3f - surround: _Tuple3f - illuminant_type: str - -class CmsProfile: - @property - def rendering_intent(self) -> int: ... - @property - def creation_date(self) -> datetime.datetime | None: ... - @property - def copyright(self) -> str | None: ... - @property - def target(self) -> str | None: ... - @property - def manufacturer(self) -> str | None: ... - @property - def model(self) -> str | None: ... - @property - def profile_description(self) -> str | None: ... - @property - def screening_description(self) -> str | None: ... - @property - def viewing_condition(self) -> str | None: ... - @property - def version(self) -> float: ... - @property - def icc_version(self) -> int: ... - @property - def attributes(self) -> int: ... - @property - def header_flags(self) -> int: ... - @property - def header_manufacturer(self) -> str: ... - @property - def header_model(self) -> str: ... - @property - def device_class(self) -> str: ... - @property - def connection_space(self) -> str: ... - @property - def xcolor_space(self) -> str: ... - @property - def profile_id(self) -> bytes: ... - @property - def is_matrix_shaper(self) -> bool: ... - @property - def technology(self) -> str | None: ... - @property - def colorimetric_intent(self) -> str | None: ... - @property - def perceptual_rendering_intent_gamut(self) -> str | None: ... - @property - def saturation_rendering_intent_gamut(self) -> str | None: ... - @property - def red_colorant(self) -> _Tuple2x3f | None: ... - @property - def green_colorant(self) -> _Tuple2x3f | None: ... - @property - def blue_colorant(self) -> _Tuple2x3f | None: ... - @property - def red_primary(self) -> _Tuple2x3f | None: ... - @property - def green_primary(self) -> _Tuple2x3f | None: ... - @property - def blue_primary(self) -> _Tuple2x3f | None: ... - @property - def media_white_point_temperature(self) -> float | None: ... - @property - def media_white_point(self) -> _Tuple2x3f | None: ... - @property - def media_black_point(self) -> _Tuple2x3f | None: ... - @property - def luminance(self) -> _Tuple2x3f | None: ... - @property - def chromatic_adaptation(self) -> tuple[_Tuple3x3f, _Tuple3x3f] | None: ... - @property - def chromaticity(self) -> _Tuple3x3f | None: ... - @property - def colorant_table(self) -> list[str] | None: ... - @property - def colorant_table_out(self) -> list[str] | None: ... - @property - def intent_supported(self) -> dict[int, tuple[bool, bool, bool]] | None: ... - @property - def clut(self) -> dict[int, tuple[bool, bool, bool]] | None: ... - @property - def icc_measurement_condition(self) -> _IccMeasurementCondition | None: ... - @property - def icc_viewing_condition(self) -> _IccViewingCondition | None: ... - def is_intent_supported(self, intent: int, direction: int, /) -> int: ... - -class CmsTransform: - def apply(self, id_in: CapsuleType, id_out: CapsuleType) -> int: ... - -def profile_open(profile: str, /) -> CmsProfile: ... -def profile_frombytes(profile: bytes, /) -> CmsProfile: ... -def profile_tobytes(profile: CmsProfile, /) -> bytes: ... -def buildTransform( - input_profile: CmsProfile, - output_profile: CmsProfile, - in_mode: str, - out_mode: str, - rendering_intent: int = 0, - cms_flags: int = 0, - /, -) -> CmsTransform: ... -def buildProofTransform( - input_profile: CmsProfile, - output_profile: CmsProfile, - proof_profile: CmsProfile, - in_mode: str, - out_mode: str, - rendering_intent: int = 0, - proof_intent: int = 0, - cms_flags: int = 0, - /, -) -> CmsTransform: ... -def createProfile( - color_space: Literal["LAB", "XYZ", "sRGB"], color_temp: SupportsFloat = 0.0, / -) -> CmsProfile: ... - -if sys.platform == "win32": - def get_display_profile_win32(handle: int = 0, is_dc: int = 0, /) -> str | None: ... diff --git a/python_modules/PIL/_imagingft.pyi b/python_modules/PIL/_imagingft.pyi deleted file mode 100644 index 1cb1429d6..000000000 --- a/python_modules/PIL/_imagingft.pyi +++ /dev/null @@ -1,69 +0,0 @@ -from typing import Any, Callable - -from . import ImageFont, _imaging - -class Font: - @property - def family(self) -> str | None: ... - @property - def style(self) -> str | None: ... - @property - def ascent(self) -> int: ... - @property - def descent(self) -> int: ... - @property - def height(self) -> int: ... - @property - def x_ppem(self) -> int: ... - @property - def y_ppem(self) -> int: ... - @property - def glyphs(self) -> int: ... - def render( - self, - string: str | bytes, - fill: Callable[[int, int], _imaging.ImagingCore], - mode: str, - dir: str | None, - features: list[str] | None, - lang: str | None, - stroke_width: float, - stroke_filled: bool, - anchor: str | None, - foreground_ink_long: int, - start: tuple[float, float], - /, - ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... - def getsize( - self, - string: str | bytes | bytearray, - mode: str, - dir: str | None, - features: list[str] | None, - lang: str | None, - anchor: str | None, - /, - ) -> tuple[tuple[int, int], tuple[int, int]]: ... - def getlength( - self, - string: str | bytes, - mode: str, - dir: str | None, - features: list[str] | None, - lang: str | None, - /, - ) -> float: ... - def getvarnames(self) -> list[bytes]: ... - def getvaraxes(self) -> list[ImageFont.Axis]: ... - def setvarname(self, instance_index: int, /) -> None: ... - def setvaraxes(self, axes: list[float], /) -> None: ... - -def getfont( - filename: str | bytes, - size: float, - index: int, - encoding: str, - font_bytes: bytes, - layout_engine: int, -) -> Font: ... -def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_imagingmath.pyi b/python_modules/PIL/_imagingmath.pyi deleted file mode 100644 index e27843e53..000000000 --- a/python_modules/PIL/_imagingmath.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_imagingmorph.pyi b/python_modules/PIL/_imagingmorph.pyi deleted file mode 100644 index e27843e53..000000000 --- a/python_modules/PIL/_imagingmorph.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_imagingtk.pyi b/python_modules/PIL/_imagingtk.pyi deleted file mode 100644 index e27843e53..000000000 --- a/python_modules/PIL/_imagingtk.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/_tkinter_finder.py b/python_modules/PIL/_tkinter_finder.py deleted file mode 100644 index 9c0143003..000000000 --- a/python_modules/PIL/_tkinter_finder.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Find compiled module linking to Tcl / Tk libraries""" - -from __future__ import annotations - -import sys -import tkinter - -tk = getattr(tkinter, "_tkinter") - -try: - if hasattr(sys, "pypy_find_executable"): - TKINTER_LIB = tk.tklib_cffi.__file__ - else: - TKINTER_LIB = tk.__file__ -except AttributeError: - # _tkinter may be compiled directly into Python, in which case __file__ is - # not available. load_tkinter_funcs will check the binary first in any case. - TKINTER_LIB = None - -tk_version = str(tkinter.TkVersion) diff --git a/python_modules/PIL/_typing.py b/python_modules/PIL/_typing.py deleted file mode 100644 index 373938e71..000000000 --- a/python_modules/PIL/_typing.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import annotations - -import os -import sys -from collections.abc import Sequence -from typing import Any, Protocol, TypeVar, Union - -TYPE_CHECKING = False -if TYPE_CHECKING: - from numbers import _IntegralLike as IntegralLike - - try: - import numpy.typing as npt - - NumpyArray = npt.NDArray[Any] # requires numpy>=1.21 - except (ImportError, AttributeError): - pass - -if sys.version_info >= (3, 13): - from types import CapsuleType -else: - CapsuleType = object - -if sys.version_info >= (3, 12): - from collections.abc import Buffer -else: - Buffer = Any - -if sys.version_info >= (3, 10): - from typing import TypeGuard -else: - try: - from typing_extensions import TypeGuard - except ImportError: - - class TypeGuard: # type: ignore[no-redef] - def __class_getitem__(cls, item: Any) -> type[bool]: - return bool - - -Coords = Union[Sequence[float], Sequence[Sequence[float]]] - - -_T_co = TypeVar("_T_co", covariant=True) - - -class SupportsRead(Protocol[_T_co]): - def read(self, length: int = ..., /) -> _T_co: ... - - -StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] - - -__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] diff --git a/python_modules/PIL/_util.py b/python_modules/PIL/_util.py deleted file mode 100644 index 8ef0d36f7..000000000 --- a/python_modules/PIL/_util.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -import os -from typing import Any, NoReturn - -from ._typing import StrOrBytesPath, TypeGuard - - -def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: - return isinstance(f, (bytes, str, os.PathLike)) - - -class DeferredError: - def __init__(self, ex: BaseException): - self.ex = ex - - def __getattr__(self, elt: str) -> NoReturn: - raise self.ex - - @staticmethod - def new(ex: BaseException) -> Any: - """ - Creates an object that raises the wrapped exception ``ex`` when used, - and casts it to :py:obj:`~typing.Any` type. - """ - return DeferredError(ex) diff --git a/python_modules/PIL/_version.py b/python_modules/PIL/_version.py deleted file mode 100644 index 74e63356c..000000000 --- a/python_modules/PIL/_version.py +++ /dev/null @@ -1,4 +0,0 @@ -# Master version for Pillow -from __future__ import annotations - -__version__ = "11.3.0" diff --git a/python_modules/PIL/_webp.pyi b/python_modules/PIL/_webp.pyi deleted file mode 100644 index e27843e53..000000000 --- a/python_modules/PIL/_webp.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/python_modules/PIL/features.py b/python_modules/PIL/features.py deleted file mode 100644 index 573f1d412..000000000 --- a/python_modules/PIL/features.py +++ /dev/null @@ -1,361 +0,0 @@ -from __future__ import annotations - -import collections -import os -import sys -import warnings -from typing import IO - -import PIL - -from . import Image -from ._deprecate import deprecate - -modules = { - "pil": ("PIL._imaging", "PILLOW_VERSION"), - "tkinter": ("PIL._tkinter_finder", "tk_version"), - "freetype2": ("PIL._imagingft", "freetype2_version"), - "littlecms2": ("PIL._imagingcms", "littlecms_version"), - "webp": ("PIL._webp", "webpdecoder_version"), - "avif": ("PIL._avif", "libavif_version"), -} - - -def check_module(feature: str) -> bool: - """ - Checks if a module is available. - - :param feature: The module to check for. - :returns: ``True`` if available, ``False`` otherwise. - :raises ValueError: If the module is not defined in this version of Pillow. - """ - if feature not in modules: - msg = f"Unknown module {feature}" - raise ValueError(msg) - - module, ver = modules[feature] - - try: - __import__(module) - return True - except ModuleNotFoundError: - return False - except ImportError as ex: - warnings.warn(str(ex)) - return False - - -def version_module(feature: str) -> str | None: - """ - :param feature: The module to check for. - :returns: - The loaded version number as a string, or ``None`` if unknown or not available. - :raises ValueError: If the module is not defined in this version of Pillow. - """ - if not check_module(feature): - return None - - module, ver = modules[feature] - - return getattr(__import__(module, fromlist=[ver]), ver) - - -def get_supported_modules() -> list[str]: - """ - :returns: A list of all supported modules. - """ - return [f for f in modules if check_module(f)] - - -codecs = { - "jpg": ("jpeg", "jpeglib"), - "jpg_2000": ("jpeg2k", "jp2klib"), - "zlib": ("zip", "zlib"), - "libtiff": ("libtiff", "libtiff"), -} - - -def check_codec(feature: str) -> bool: - """ - Checks if a codec is available. - - :param feature: The codec to check for. - :returns: ``True`` if available, ``False`` otherwise. - :raises ValueError: If the codec is not defined in this version of Pillow. - """ - if feature not in codecs: - msg = f"Unknown codec {feature}" - raise ValueError(msg) - - codec, lib = codecs[feature] - - return f"{codec}_encoder" in dir(Image.core) - - -def version_codec(feature: str) -> str | None: - """ - :param feature: The codec to check for. - :returns: - The version number as a string, or ``None`` if not available. - Checked at compile time for ``jpg``, run-time otherwise. - :raises ValueError: If the codec is not defined in this version of Pillow. - """ - if not check_codec(feature): - return None - - codec, lib = codecs[feature] - - version = getattr(Image.core, f"{lib}_version") - - if feature == "libtiff": - return version.split("\n")[0].split("Version ")[1] - - return version - - -def get_supported_codecs() -> list[str]: - """ - :returns: A list of all supported codecs. - """ - return [f for f in codecs if check_codec(f)] - - -features: dict[str, tuple[str, str | bool, str | None]] = { - "webp_anim": ("PIL._webp", True, None), - "webp_mux": ("PIL._webp", True, None), - "transp_webp": ("PIL._webp", True, None), - "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), - "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), - "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), - "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), - "mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"), - "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), - "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), - "xcb": ("PIL._imaging", "HAVE_XCB", None), -} - - -def check_feature(feature: str) -> bool | None: - """ - Checks if a feature is available. - - :param feature: The feature to check for. - :returns: ``True`` if available, ``False`` if unavailable, ``None`` if unknown. - :raises ValueError: If the feature is not defined in this version of Pillow. - """ - if feature not in features: - msg = f"Unknown feature {feature}" - raise ValueError(msg) - - module, flag, ver = features[feature] - - if isinstance(flag, bool): - deprecate(f'check_feature("{feature}")', 12) - try: - imported_module = __import__(module, fromlist=["PIL"]) - if isinstance(flag, bool): - return flag - return getattr(imported_module, flag) - except ModuleNotFoundError: - return None - except ImportError as ex: - warnings.warn(str(ex)) - return None - - -def version_feature(feature: str) -> str | None: - """ - :param feature: The feature to check for. - :returns: The version number as a string, or ``None`` if not available. - :raises ValueError: If the feature is not defined in this version of Pillow. - """ - if not check_feature(feature): - return None - - module, flag, ver = features[feature] - - if ver is None: - return None - - return getattr(__import__(module, fromlist=[ver]), ver) - - -def get_supported_features() -> list[str]: - """ - :returns: A list of all supported features. - """ - supported_features = [] - for f, (module, flag, _) in features.items(): - if flag is True: - for feature, (feature_module, _) in modules.items(): - if feature_module == module: - if check_module(feature): - supported_features.append(f) - break - elif check_feature(f): - supported_features.append(f) - return supported_features - - -def check(feature: str) -> bool | None: - """ - :param feature: A module, codec, or feature name. - :returns: - ``True`` if the module, codec, or feature is available, - ``False`` or ``None`` otherwise. - """ - - if feature in modules: - return check_module(feature) - if feature in codecs: - return check_codec(feature) - if feature in features: - return check_feature(feature) - warnings.warn(f"Unknown feature '{feature}'.", stacklevel=2) - return False - - -def version(feature: str) -> str | None: - """ - :param feature: - The module, codec, or feature to check for. - :returns: - The version number as a string, or ``None`` if unknown or not available. - """ - if feature in modules: - return version_module(feature) - if feature in codecs: - return version_codec(feature) - if feature in features: - return version_feature(feature) - return None - - -def get_supported() -> list[str]: - """ - :returns: A list of all supported modules, features, and codecs. - """ - - ret = get_supported_modules() - ret.extend(get_supported_features()) - ret.extend(get_supported_codecs()) - return ret - - -def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: - """ - Prints information about this installation of Pillow. - This function can be called with ``python3 -m PIL``. - It can also be called with ``python3 -m PIL.report`` or ``python3 -m PIL --report`` - to have "supported_formats" set to ``False``, omitting the list of all supported - image file formats. - - :param out: - The output stream to print to. Defaults to ``sys.stdout`` if ``None``. - :param supported_formats: - If ``True``, a list of all supported image file formats will be printed. - """ - - if out is None: - out = sys.stdout - - Image.init() - - print("-" * 68, file=out) - print(f"Pillow {PIL.__version__}", file=out) - py_version_lines = sys.version.splitlines() - print(f"Python {py_version_lines[0].strip()}", file=out) - for py_version in py_version_lines[1:]: - print(f" {py_version.strip()}", file=out) - print("-" * 68, file=out) - print(f"Python executable is {sys.executable or 'unknown'}", file=out) - if sys.prefix != sys.base_prefix: - print(f"Environment Python files loaded from {sys.prefix}", file=out) - print(f"System Python files loaded from {sys.base_prefix}", file=out) - print("-" * 68, file=out) - print( - f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}", - file=out, - ) - print( - f"Binary Pillow modules loaded from {os.path.dirname(Image.core.__file__)}", - file=out, - ) - print("-" * 68, file=out) - - for name, feature in [ - ("pil", "PIL CORE"), - ("tkinter", "TKINTER"), - ("freetype2", "FREETYPE2"), - ("littlecms2", "LITTLECMS2"), - ("webp", "WEBP"), - ("avif", "AVIF"), - ("jpg", "JPEG"), - ("jpg_2000", "OPENJPEG (JPEG2000)"), - ("zlib", "ZLIB (PNG/ZIP)"), - ("libtiff", "LIBTIFF"), - ("raqm", "RAQM (Bidirectional Text)"), - ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), - ("xcb", "XCB (X protocol)"), - ]: - if check(name): - v: str | None = None - if name == "jpg": - libjpeg_turbo_version = version_feature("libjpeg_turbo") - if libjpeg_turbo_version is not None: - v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo" - v += " " + libjpeg_turbo_version - if v is None: - v = version(name) - if v is not None: - version_static = name in ("pil", "jpg") - if name == "littlecms2": - # this check is also in src/_imagingcms.c:setup_module() - version_static = tuple(int(x) for x in v.split(".")) < (2, 7) - t = "compiled for" if version_static else "loaded" - if name == "zlib": - zlib_ng_version = version_feature("zlib_ng") - if zlib_ng_version is not None: - v += ", compiled for zlib-ng " + zlib_ng_version - elif name == "raqm": - for f in ("fribidi", "harfbuzz"): - v2 = version_feature(f) - if v2 is not None: - v += f", {f} {v2}" - print("---", feature, "support ok,", t, v, file=out) - else: - print("---", feature, "support ok", file=out) - else: - print("***", feature, "support not installed", file=out) - print("-" * 68, file=out) - - if supported_formats: - extensions = collections.defaultdict(list) - for ext, i in Image.EXTENSION.items(): - extensions[i].append(ext) - - for i in sorted(Image.ID): - line = f"{i}" - if i in Image.MIME: - line = f"{line} {Image.MIME[i]}" - print(line, file=out) - - if i in extensions: - print( - "Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out - ) - - features = [] - if i in Image.OPEN: - features.append("open") - if i in Image.SAVE: - features.append("save") - if i in Image.SAVE_ALL: - features.append("save_all") - if i in Image.DECODERS: - features.append("decode") - if i in Image.ENCODERS: - features.append("encode") - - print("Features: {}".format(", ".join(features)), file=out) - print("-" * 68, file=out) diff --git a/python_modules/PIL/py.typed b/python_modules/PIL/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_modules/PIL/report.py b/python_modules/PIL/report.py deleted file mode 100644 index d2815e845..000000000 --- a/python_modules/PIL/report.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import annotations - -from .features import pilinfo - -pilinfo(supported_formats=False) diff --git a/python_modules/_cloudflare_compat_flags.pyi b/python_modules/_cloudflare_compat_flags.pyi deleted file mode 100644 index 147262959..000000000 --- a/python_modules/_cloudflare_compat_flags.pyi +++ /dev/null @@ -1,4 +0,0 @@ -python_workflows_implicit_dependencies: bool -python_request_headers_preserve_commas: bool - -def __getattr__(name: str) -> bool: ... diff --git a/python_modules/_pyodide_entrypoint_helper.pyi b/python_modules/_pyodide_entrypoint_helper.pyi deleted file mode 100644 index 3407c1d14..000000000 --- a/python_modules/_pyodide_entrypoint_helper.pyi +++ /dev/null @@ -1,9 +0,0 @@ -from collections.abc import Generator -from typing import Any - -cloudflareWorkersModule: Any -cloudflareSocketsModule: Any - -def doAnImport(module_name: str) -> Any: ... -def patch_env_helper(kwds: Any) -> Generator[None]: ... -def patchWaitUntil(ctx: Any) -> None: ... diff --git a/python_modules/_workers_sdk_entropy_import_context.pth b/python_modules/_workers_sdk_entropy_import_context.pth deleted file mode 100644 index e0f6c4673..000000000 --- a/python_modules/_workers_sdk_entropy_import_context.pth +++ /dev/null @@ -1 +0,0 @@ -import _workers_sdk_entropy_import_context_loader diff --git a/python_modules/_workers_sdk_entropy_import_context.py b/python_modules/_workers_sdk_entropy_import_context.py deleted file mode 100644 index 630b26a82..000000000 --- a/python_modules/_workers_sdk_entropy_import_context.py +++ /dev/null @@ -1,183 +0,0 @@ -""" -Top level entropy patches for packages -""" - -import sys -from contextlib import contextmanager - -from _cloudflare.allow_entropy import ( - allow_bad_entropy_calls, -) -from _cloudflare.import_patch_manager import ( - block_calls, - register_after_snapshot, - register_before_first_request, - register_create_patch, - register_exec_patch, -) - - -class STATE: - imported_rust_package = False - numpy_random = None - - -@register_create_patch("tiktoken._tiktoken") -@register_exec_patch("cryptography.exceptions") -@register_exec_patch("jiter") -@contextmanager -def rust_package_context(module): - """Rust packages need one entropy call if they create a rust hash map at - init time. - - For reasons I don't entirely understand, in Pyodide 0.28 only the first Rust package to be - imported makes the get_entropy call. See gen_rust_import_tests() which tests that importing - four rust packages in different permutations works correctly. - """ - if STATE.imported_rust_package: - yield - return - STATE.imported_rust_package = True - with allow_bad_entropy_calls(1): - yield - - -@register_exec_patch("numpy.random") -@contextmanager -def numpy_random_context(numpy_random): - """numpy.random doesn't call getentropy() itself, but we want to block calls - that might use the bad seed. - - TODO: Maybe there are more calls we can whitelist? - TODO: Is it not enough to just block numpy.random.mtrand calls? - """ - yield - # Calling default_rng() with a given seed is fine, calling it without a seed - # will call getentropy() and fail. - block_calls(numpy_random, allowlist=("default_rng", "RandomState")) - - -@register_after_snapshot("numpy.random") -def numpy_random_after_snapshot(numpy_random): - r1 = numpy_random.random() - numpy_random.set_state(STATE.numpy_random) - r2 = numpy_random.random() - if r1 != r2: - raise RuntimeError("random seed in bad state") - - -@register_before_first_request("numpy.random") -def numpy_random_before_first_request(numpy_random): - numpy_random.seed() - - -@register_exec_patch("numpy.random.mtrand") -@contextmanager -def numpy_random_mtrand_context(module): - # numpy.random.mtrand calls secrets.randbits at top level to seed itself. - # This will fail if we don't let it through. - with allow_bad_entropy_calls(1): - yield - # Block calls until we get a chance to replace the bad random seed. - STATE.numpy_random = module.get_state() - block_calls(module, allowlist=("RandomState",)) - - -@register_exec_patch("pydantic_core") -@contextmanager -def pydantic_core_context(module): - try: - # Initial import needs one entropy call to initialize - # std::collections::HashMap hash seed - with allow_bad_entropy_calls(1): - yield - finally: - try: - with allow_bad_entropy_calls(1): - # validate_core_schema makes an ahash::AHashMap which makes - # another entropy call for its hash seed. It will throw an error - # but only after making the needed entropy call. - module.validate_core_schema(None) - except module.SchemaError: - pass - - -@register_exec_patch("aiohttp.http_websocket") -@contextmanager -def aiohttp_http_websocket_context(module): - import random - - Random = random.Random - - def patched_Random(): - return random - - random.Random = patched_Random - try: - yield - finally: - random.Random = Random - - -class NoSslFinder: - def find_spec(self, fullname, path, target): - if fullname == "ssl": - raise ModuleNotFoundError( - f"No module named {fullname!r}", name=fullname - ) from None - - -@contextmanager -def no_ssl(): - """ - Various packages will call ssl.create_default_context() at top level which uses entropy if they - can import ssl. By temporarily making importing ssl raise an import error, we exercise the - workaround code and so avoid the entropy calls. After, we put the ssl module back to the normal - value. - """ - try: - f = NoSslFinder() - ssl = sys.modules.pop("ssl", None) - sys.meta_path.insert(0, f) - yield - finally: - sys.meta_path.remove(f) - if ssl: - sys.modules["ssl"] = ssl - - -@register_exec_patch("aiohttp.connector") -@contextmanager -def aiohttp_connector_context(module): - with no_ssl(): - yield - - -@register_exec_patch("requests.adapters") -@contextmanager -def requests_adapters_context(module): - with no_ssl(): - yield - - -@register_exec_patch("urllib3.util.ssl_") -@contextmanager -def urllib3_util_ssl__context(module): - with no_ssl(): - yield - - -@register_exec_patch("langsmith._internal._constants") -@contextmanager -def langsmith__internal__constants_context(module): - # Langsmith uses a UUID to communicate with a background thread. This obviously won't work so we - # might as well allow it to make a UUID. - with allow_bad_entropy_calls(1): - yield - - -@register_exec_patch("langchain_openai.chat_models.base") -@contextmanager -def langchain_openai_chat_models_base_context(module): - with allow_bad_entropy_calls(1): - yield diff --git a/python_modules/_workers_sdk_entropy_import_context_loader.py b/python_modules/_workers_sdk_entropy_import_context_loader.py deleted file mode 100644 index 2e09aeb3d..000000000 --- a/python_modules/_workers_sdk_entropy_import_context_loader.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Loader shim for _workers_sdk_entropy_import_context. - -This module is imported by the .pth file on every Python startup. The entropy -context patches depend on the `_cloudflare` package, which only exists inside -the workers runtime. When running outside of the workers runtime, `_cloudflare` -is not available, so we silently skip loading the patches. -""" - -import importlib.util - -if importlib.util.find_spec("_cloudflare") is not None: - import _workers_sdk_entropy_import_context # noqa: F401 diff --git a/python_modules/pillow-11.3.0.dist-info/INSTALLER b/python_modules/pillow-11.3.0.dist-info/INSTALLER deleted file mode 100644 index 5c69047b2..000000000 --- a/python_modules/pillow-11.3.0.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -uv \ No newline at end of file diff --git a/python_modules/pillow-11.3.0.dist-info/METADATA b/python_modules/pillow-11.3.0.dist-info/METADATA deleted file mode 100644 index ea8c3fd50..000000000 --- a/python_modules/pillow-11.3.0.dist-info/METADATA +++ /dev/null @@ -1,177 +0,0 @@ -Metadata-Version: 2.4 -Name: pillow -Version: 11.3.0 -Summary: Python Imaging Library (Fork) -Author-email: "Jeffrey A. Clark" -License-Expression: MIT-CMU -Project-URL: Changelog, https://github.com/python-pillow/Pillow/releases -Project-URL: Documentation, https://pillow.readthedocs.io -Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi -Project-URL: Homepage, https://python-pillow.github.io -Project-URL: Mastodon, https://fosstodon.org/@pillow -Project-URL: Release notes, https://pillow.readthedocs.io/en/stable/releasenotes/index.html -Project-URL: Source, https://github.com/python-pillow/Pillow -Keywords: Imaging -Classifier: Development Status :: 6 - Mature -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Classifier: Programming Language :: Python :: Implementation :: CPython -Classifier: Programming Language :: Python :: Implementation :: PyPy -Classifier: Topic :: Multimedia :: Graphics -Classifier: Topic :: Multimedia :: Graphics :: Capture :: Digital Camera -Classifier: Topic :: Multimedia :: Graphics :: Capture :: Screen Capture -Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion -Classifier: Topic :: Multimedia :: Graphics :: Viewers -Classifier: Typing :: Typed -Requires-Python: >=3.9 -Description-Content-Type: text/markdown -License-File: LICENSE -Provides-Extra: docs -Requires-Dist: furo; extra == "docs" -Requires-Dist: olefile; extra == "docs" -Requires-Dist: sphinx>=8.2; extra == "docs" -Requires-Dist: sphinx-autobuild; extra == "docs" -Requires-Dist: sphinx-copybutton; extra == "docs" -Requires-Dist: sphinx-inline-tabs; extra == "docs" -Requires-Dist: sphinxext-opengraph; extra == "docs" -Provides-Extra: fpx -Requires-Dist: olefile; extra == "fpx" -Provides-Extra: mic -Requires-Dist: olefile; extra == "mic" -Provides-Extra: test-arrow -Requires-Dist: pyarrow; extra == "test-arrow" -Provides-Extra: tests -Requires-Dist: check-manifest; extra == "tests" -Requires-Dist: coverage>=7.4.2; extra == "tests" -Requires-Dist: defusedxml; extra == "tests" -Requires-Dist: markdown2; extra == "tests" -Requires-Dist: olefile; extra == "tests" -Requires-Dist: packaging; extra == "tests" -Requires-Dist: pyroma; extra == "tests" -Requires-Dist: pytest; extra == "tests" -Requires-Dist: pytest-cov; extra == "tests" -Requires-Dist: pytest-timeout; extra == "tests" -Requires-Dist: pytest-xdist; extra == "tests" -Requires-Dist: trove-classifiers>=2024.10.12; extra == "tests" -Provides-Extra: typing -Requires-Dist: typing-extensions; python_version < "3.10" and extra == "typing" -Provides-Extra: xmp -Requires-Dist: defusedxml; extra == "xmp" -Dynamic: license-file - -

    - Pillow logo -

    - -# Pillow - -## Python Imaging Library (Fork) - -Pillow is the friendly PIL fork by [Jeffrey A. Clark and -contributors](https://github.com/python-pillow/Pillow/graphs/contributors). -PIL is the Python Imaging Library by Fredrik Lundh and contributors. -As of 2019, Pillow development is -[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). - - - - - - - - - - - - - - - - - - -
    docs - Documentation Status -
    tests - GitHub Actions build status (Lint) - GitHub Actions build status (Test Linux and macOS) - GitHub Actions build status (Test Windows) - GitHub Actions build status (Test MinGW) - GitHub Actions build status (Test Cygwin) - GitHub Actions build status (Test Docker) - GitHub Actions build status (Wheels) - Code coverage - Fuzzing Status -
    package - Zenodo - Tidelift - Newest PyPI version - Number of PyPI downloads - OpenSSF Best Practices -
    social - Join the chat at https://gitter.im/python-pillow/Pillow - Follow on https://fosstodon.org/@pillow -
    - -## Overview - -The Python Imaging Library adds image processing capabilities to your Python interpreter. - -This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities. - -The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. - -## More information - -- [Documentation](https://pillow.readthedocs.io/) - - [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html) - - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html) -- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md) - - [Issues](https://github.com/python-pillow/Pillow/issues) - - [Pull requests](https://github.com/python-pillow/Pillow/pulls) -- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) -- [Changelog](https://github.com/python-pillow/Pillow/releases) - - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) - -## Report a vulnerability - -To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security). diff --git a/python_modules/pillow-11.3.0.dist-info/RECORD b/python_modules/pillow-11.3.0.dist-info/RECORD deleted file mode 100644 index 8a569d552..000000000 --- a/python_modules/pillow-11.3.0.dist-info/RECORD +++ /dev/null @@ -1,119 +0,0 @@ -PIL/AvifImagePlugin.py,sha256=5IiDMvMZQXLnS3t25XJjlwgNWmeVSNaGfReWAp-V5lo,8994 -PIL/BdfFontFile.py,sha256=PhlZfIRmEfmorbhZZeSM5eebGo1Ei7fL-lR9XlfTZZA,3285 -PIL/BlpImagePlugin.py,sha256=Ub4vVKBEniiNBEgNizxScEpO1VKbC1w6iecWUU7T-Vs,16533 -PIL/BmpImagePlugin.py,sha256=-SNdj2godmaKYAc08dEng6z3mRPbYYHezjveIR5e-tU,19855 -PIL/BufrStubImagePlugin.py,sha256=JSqDhkPNPnFw0Qcz-gQJl-D_iSCFdtcLvPynshKJ4WM,1730 -PIL/ContainerIO.py,sha256=wkBqL2GDAb5fh3wrtfTGUfqioJipCl-lg2GxbjQrTZw,4604 -PIL/CurImagePlugin.py,sha256=bICiwXZrzSONWBu4bKtshxZSNFj8su0lbDojYntEUYs,1797 -PIL/DcxImagePlugin.py,sha256=DhqsmW7MjmnUSTGZ-Skv9hz1XeX3XoQQoAl9GWLAEEY,2145 -PIL/DdsImagePlugin.py,sha256=fjdfZK_eQtUp_-bjoRmt-5wgOT5GTmvg6aI-itch4mo,18906 -PIL/EpsImagePlugin.py,sha256=ROWwCv08bC_B41eMf2AFe8UW6ZH4_XQ18x12KB_aQLM,16389 -PIL/ExifTags.py,sha256=zW6kVikCosiyoCo7J7R62evD3hoxjKPchnVh8po7CZc,9931 -PIL/FitsImagePlugin.py,sha256=-oDJnAH113CK5qPvwz9lL81fkV1gla_tNfqLcq8zKgo,4644 -PIL/FliImagePlugin.py,sha256=DaWuH8f-9GihS0VVZqF1bT3uDv1Vb0VBl0chnNd82Ow,4786 -PIL/FontFile.py,sha256=St7MxO5Q-oakCLWn3ZrgrtaT3wSsmAarxm8AU-G8Moc,3577 -PIL/FpxImagePlugin.py,sha256=aXfg0YdvNeJhxqh-f-f22D1NobQ8tSVCj-tpLE2PKfE,7293 -PIL/FtexImagePlugin.py,sha256=v2I5YkdfNA3iW35JzKnWry9v6Rgvr0oezGVOuArREac,3535 -PIL/GbrImagePlugin.py,sha256=5t0UfLubTPQcuDDbafwC78OLR7IsD5hjpvhUZ5g8z4A,3006 -PIL/GdImageFile.py,sha256=LP4Uxv3Y2ivGZIyOVuGJarDDVS7zK6F1Q6SNl4wyGuQ,2788 -PIL/GifImagePlugin.py,sha256=SkXboZwxTolq0uteXYX0ncrZiUxyASywqAurOcVAi3U,42201 -PIL/GimpGradientFile.py,sha256=Z_4TUYMdPyUsiP40KSIpMJ5yLGMnBaIKOAkHyiQGEWE,3906 -PIL/GimpPaletteFile.py,sha256=YHEhKThsEVlXVjFQUnGvhDgNsJcfFqUAN0O0ucG9G-Q,1815 -PIL/GribStubImagePlugin.py,sha256=degHg344X3JXL8u-x8NWn08BsmM9wRh-Jg08HHrvfOc,1738 -PIL/Hdf5StubImagePlugin.py,sha256=OuEQijGqVwTTSG4dB2vAyQzmN-NYT22tiuZHFH0Q0Sw,1741 -PIL/IcnsImagePlugin.py,sha256=qvi-OP0g8CRlNlJE--5_rPlfyxLFLlSOil66Fw4TMwU,12949 -PIL/IcoImagePlugin.py,sha256=QCo29Toh08UX8vEcdCAaIeuidSolbPiZlCnQ4rUu2SQ,12491 -PIL/ImImagePlugin.py,sha256=wo5OL2PAcQW2MwRkJnS-N16toZzXWL95jx9FBM7l9ok,11567 -PIL/Image.py,sha256=95Jefi2QFIfZYOyfHNBRTwBtwrnNZsn5oCsLQsBLdK8,148332 -PIL/ImageChops.py,sha256=GEjlymcoDtA5OOeIxQVIX96BD-s6AXhb7TmSLYn2tUg,7946 -PIL/ImageCms.py,sha256=A5ZVaTjjxR6AeDNNvK-hmu0QqKOMTscou6BUBTLob0g,41934 -PIL/ImageColor.py,sha256=IGA9C2umeED_EzS2Cvj6KsU0VutC9RstWIYPe8uDsVk,9441 -PIL/ImageDraw.py,sha256=Enr0ctBHKBnSHVBDlqcIbIAyHgVj5ZbLL-swVb8s8Vo,42845 -PIL/ImageDraw2.py,sha256=pdVMW7bVw3KwhXvRZh28Md4y-2xFfuo5fHcDnaYqVK4,7227 -PIL/ImageEnhance.py,sha256=4Elhz_lyyxLmx0GkSHrwOAmNJ2TkqVQPHejzGihZUMI,3627 -PIL/ImageFile.py,sha256=HLgKqn6K9J4HlnyiPFZUTAfcqxXYjE06fZeKO6V-haw,29334 -PIL/ImageFilter.py,sha256=MiTowY9micg1dSfwZkExXSBNPr2b_11kDCGreP6W8x4,18671 -PIL/ImageFont.py,sha256=rVQm3zwnTFZ1HSp4OeA5THKjTezhE8HMrnOhHzmqfEM,64292 -PIL/ImageGrab.py,sha256=I9PHpsQf2VyNX4T8QL-8awFNotyAzB1mGxTt_I5FbTE,6471 -PIL/ImageMath.py,sha256=XasMsgjaD9p2OZa7naOdpEACq3yJl-Q2RGTf4xo7CgM,11919 -PIL/ImageMode.py,sha256=5yOxODAZ7jG03DsUFrt7eQayTtIpWPgvfyhlXDWwcv8,2681 -PIL/ImageMorph.py,sha256=TowXnk1Q2wX9AXVBDWRRQhCfAbFOUWGMo00vq4yn-fU,8563 -PIL/ImageOps.py,sha256=A69qjt-mxDI99387z_4cHI-wtH85SLL_ENTI9EeOQGI,25525 -PIL/ImagePalette.py,sha256=M5tYUgadWR7mxUEByyVl7IV9QFFzAGiKKmAhCZtdG0w,9009 -PIL/ImagePath.py,sha256=5yUG5XCUil1KKTTA_8PgGhcmg-mnue-GK0FwTBlhjw4,371 -PIL/ImageQt.py,sha256=dQbadF2Lg59OJVjiNVcbz3wvymqEpL-uEZG32b8E-bg,6841 -PIL/ImageSequence.py,sha256=gx2EvywPBEjxNJujCqdpbfAm2BpyNV2_f1IaO3niubw,2200 -PIL/ImageShow.py,sha256=Ju0_Db2B4_n3yKJV9sDsF7_HAgciEdXlq6I1Eiw1YTo,10106 -PIL/ImageStat.py,sha256=S43FZ89r_u4hKCj59lVuWpyVJfhbUy3igXkp9DwaMgM,5325 -PIL/ImageTk.py,sha256=b5SntckGXs0ECsI2MmdJg3CSX6AtELsWh0Ohxu41u_k,8132 -PIL/ImageTransform.py,sha256=-qek7P3lzLddcXt9cWt5w_L11JGp2yY3AJtOfmJAkDc,3916 -PIL/ImageWin.py,sha256=LT05w8_vTfRrC3n9S9pM0TNbXrzZLEJHlCJil7Xv80k,8085 -PIL/ImtImagePlugin.py,sha256=SL5IrsHcblltxtX4v_HVFhYnR6haJ0AOd2NHhZKMImY,2665 -PIL/IptcImagePlugin.py,sha256=3BVI_oEbFEJC-yn6zmp5Joqf8edCJLKH9N5FQanyaV8,6719 -PIL/Jpeg2KImagePlugin.py,sha256=k9UoU7-Hq8vAWi9ZoosA4bfufNJsctBd4ttM1RFxwnk,13865 -PIL/JpegImagePlugin.py,sha256=WaCZTpdmzuCM5mi44bNyN4p1EXOsnKz63qv4XEbm8Ns,31786 -PIL/JpegPresets.py,sha256=lnqWHo4DLIHIulcdHp0NJ7CWexHt8T3w51kIKlLfkIA,12379 -PIL/McIdasImagePlugin.py,sha256=baOIkD-CIIeCgBFTf8kos928PKBuCUqYYa38u3WES_8,1877 -PIL/MicImagePlugin.py,sha256=aoIwkWVyr_X-dPvB6ldZOJF3a9kd_OeuEW3say5Y0QM,2564 -PIL/MpegImagePlugin.py,sha256=g7BZd93kWpFi41SG_wKFoi0yEPsioI4kj45b2F-3Vrw,2010 -PIL/MpoImagePlugin.py,sha256=S45qt7OcY7rBjYlwEk0nUmEj5IOu5z8KVLo066V1RBE,6722 -PIL/MspImagePlugin.py,sha256=oxk_MLUDvzJ4JDuOZCHkmqOPXniG42PHOyNGwe60slY,5892 -PIL/PSDraw.py,sha256=KMBGj3vXaFpblaIcA9KjFFTpdal41AQggY-UgzqoMkQ,6918 -PIL/PaletteFile.py,sha256=suDdAL6VMljXw4oEn1vhTt4DQ4vbpIHGd3A4oxOgE6s,1216 -PIL/PalmImagePlugin.py,sha256=WJ1b8I1xTSAXYDJhIpkVFCLu2LlpbiBD5d1Hr-m2l08,8748 -PIL/PcdImagePlugin.py,sha256=VweZ108HBHeNEfsoE26EOR4ktxqNGSOWOnd58DhS8Fo,1601 -PIL/PcfFontFile.py,sha256=NPZQ0XkbGB8uTlGqgmIPGkwuLMYBdykDeVuvFgIC7JU,7147 -PIL/PcxImagePlugin.py,sha256=2dqnjRjSLbjm8Opub4sZRhOIdYLdn3y7Q_ETV8EmiOQ,6224 -PIL/PdfImagePlugin.py,sha256=AbJA2f4qzH8G1olfmk18SzQlcx3WsipUYDc5bcR8Wvk,9349 -PIL/PdfParser.py,sha256=LnmX0Cm7ZQwGkB1uYP4rvXZUkERmURzmYo78zjeq6VI,37987 -PIL/PixarImagePlugin.py,sha256=l_4GwBd0mATnIXYJbwmmODU2vP7wewLu6BRviHCB2EI,1758 -PIL/PngImagePlugin.py,sha256=jPBNqZ50txFHWIsDikcdkeeBfLNY1PxT5wzcPMcmcmQ,51117 -PIL/PpmImagePlugin.py,sha256=QJM-V-odV7w-prA7B5bLRQcykdC4d7OJ5BBbCvPPIzY,12370 -PIL/PsdImagePlugin.py,sha256=ImnNRG4VANs2GATXVEB5Q-yy1Jskc6XRVRtZYi2fALg,8685 -PIL/QoiImagePlugin.py,sha256=RPO63QsgHAsyPpcxh7ymeMYlnjVu5gT5ELolkvJt0vc,8572 -PIL/SgiImagePlugin.py,sha256=3Ql89s8vycNWjcxJwMw28iksV9Yj2xWoKBQ6c5DHXBg,6389 -PIL/SpiderImagePlugin.py,sha256=Bsg6pfZMctas1xYx__oL-ZZseUReZdnLy5a-aKEJhpE,10249 -PIL/SunImagePlugin.py,sha256=Hdxkhk0pxpBGxYhPJfCDLwsYcO1KjxjtplNMFYibIvk,4589 -PIL/TarIO.py,sha256=BqYUChCBb9F7Sh-uZ86iz1Dtoy2D0obNwGm65z1rdc0,1442 -PIL/TgaImagePlugin.py,sha256=2vDsFTcBUBHw1V80wpVv4tgpLDbPr6yVHi6Fvaqf0HY,6980 -PIL/TiffImagePlugin.py,sha256=IK7Ur131NNyJET-wk50tzLkSyd7TI1lwSES4N_txy5w,85029 -PIL/TiffTags.py,sha256=-gbXLZ5rlHD6crwtY6TkafDm2tamlc5v8e7FjS8PcIg,17082 -PIL/WalImageFile.py,sha256=Lfuq_WZ_V_onwucfUc6GWfvY7z_K4s-5EdaQGu_2DD4,5704 -PIL/WebPImagePlugin.py,sha256=YFWo6_FYBSrzAf6XMbmrF4YRtR4x7tYecCWF7EA13WQ,10010 -PIL/WmfImagePlugin.py,sha256=Z1hzGuHGt08tBLsxgBV7ZVOLdQPykDMYd4RGkw1J8rw,5243 -PIL/XVThumbImagePlugin.py,sha256=cJSapkBasFt11O6XYXxqcyA-njxA5BD3wHhNj6VC7Fk,2115 -PIL/XbmImagePlugin.py,sha256=Fd6GVDEo73nyFICA3Z3w4LjkwoZWvhHB6rKCm5yVrho,2669 -PIL/XpmImagePlugin.py,sha256=jtUKavJCYwIAsJaJwSx8vJsx1oTbCywfDxePENmA93w,4400 -PIL/__init__.py,sha256=Q4KOEpR7S_Xsj30fvOsvR94xEpX4KUsVeUwaVP1fU80,2031 -PIL/__main__.py,sha256=Lpj4vef8mI7jA1sRCUAoVYaeePD_Uc898xF5c7XLx1A,133 -PIL/_avif.pyi,sha256=3fBxcSppJr6EOEcUojvflG3Eegg7lv2Qp0dNQQILrP4,63 -PIL/_binary.py,sha256=pcM6AL04GxgmGeLfcH1V1BZHENwIrQH0uxhJ7r0HIL0,2550 -PIL/_deprecate.py,sha256=JYJfJgemvedcdHMH6_RFTDBLNp4vSJqd-o32e3WzNlM,2034 -PIL/_imaging.cpython-313-wasm32-emscripten.so,sha256=VVq0eE0RznSOkOHVSggKOP0XUIXEs2kafWJGcejOGE4,1059007 -PIL/_imaging.pyi,sha256=StMbXUZS32AegATP1sUHfs5P05A3TD_BiQKsDHQBW40,868 -PIL/_imagingcms.pyi,sha256=brpjxRoiY_2ItyfTrjhKeGEsExe4GPG-25q9AQP8Jp8,4389 -PIL/_imagingft.cpython-313-wasm32-emscripten.so,sha256=4xLnJ-WNrEeMB4a370bV48E0_6q45NTZeInmBm3FVYc,605360 -PIL/_imagingft.pyi,sha256=IYdFGfApwsqYiJVoD5AVOvgMvnO1eP1J3cMA6L0YZJ0,1806 -PIL/_imagingmath.cpython-313-wasm32-emscripten.so,sha256=09jVv99pbgGakxOyZFTROVDiurcBdabOPJ1W2ZG4ibg,15459 -PIL/_imagingmath.pyi,sha256=3fBxcSppJr6EOEcUojvflG3Eegg7lv2Qp0dNQQILrP4,63 -PIL/_imagingmorph.cpython-313-wasm32-emscripten.so,sha256=9te_JRXP9hyQ2jVjg_QmOG57CTyTSKYrWhx1QDIEYEM,2584 -PIL/_imagingmorph.pyi,sha256=3fBxcSppJr6EOEcUojvflG3Eegg7lv2Qp0dNQQILrP4,63 -PIL/_imagingtk.cpython-313-wasm32-emscripten.so,sha256=zBJhe_d_CeEbGenqDoCiJz2S0k4cJk3kvMVkXmoOahw,3299 -PIL/_imagingtk.pyi,sha256=3fBxcSppJr6EOEcUojvflG3Eegg7lv2Qp0dNQQILrP4,63 -PIL/_tkinter_finder.py,sha256=GIZ4stmFhUosmHKSrdxcjStiocDNfyJn7RBie2SWxU0,538 -PIL/_typing.py,sha256=1NAWJ7Z59TP98cFv9qGpBMgSHbyR4CAByLjMRRbSZxY,1251 -PIL/_util.py,sha256=E76J1WLAe6Xg5yNWYztQwYzxUT_sR_VQxFJu7IZ3S3k,635 -PIL/_version.py,sha256=Zwv2LKWt6v32STL5K9uN7PdcJmZhDlokKTLkDA7Ky1w,87 -PIL/_webp.cpython-313-wasm32-emscripten.so,sha256=rDmrwirTAWmvM-aJ2tCxo_JHPd6TNC07ud7KecJTpHo,370564 -PIL/_webp.pyi,sha256=3fBxcSppJr6EOEcUojvflG3Eegg7lv2Qp0dNQQILrP4,63 -PIL/features.py,sha256=FfyYObVJbzYQUXf8KuRuqY6kvA8md2LorE81k3EuQrw,11479 -PIL/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -PIL/report.py,sha256=4JY6-IU7sH1RKuRbOvy1fUt0dAoi79FX4tYJN3p1DT0,100 -pillow-11.3.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -pillow-11.3.0.dist-info/METADATA,sha256=1T1NePio7-GCOWcR73aEA4bSukzNrUIfWMwAw7NAH3M,9023 -pillow-11.3.0.dist-info/RECORD,, -pillow-11.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -pillow-11.3.0.dist-info/WHEEL,sha256=BpQa-T1rm3obVjGx9TDh4sk-6ZZL2DSSkDmWtR-U0xk,113 -pillow-11.3.0.dist-info/licenses/LICENSE,sha256=F_JArhARQ3B-XnMDpdgARQ2czHR1tGPO21Vc79s8bs4,1453 -pillow-11.3.0.dist-info/top_level.txt,sha256=riZqrk-hyZqh5f1Z0Zwii3dKfxEsByhu9cU9IODF-NY,4 -pillow-11.3.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 diff --git a/python_modules/pillow-11.3.0.dist-info/REQUESTED b/python_modules/pillow-11.3.0.dist-info/REQUESTED deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_modules/pillow-11.3.0.dist-info/WHEEL b/python_modules/pillow-11.3.0.dist-info/WHEEL deleted file mode 100644 index 4c684866c..000000000 --- a/python_modules/pillow-11.3.0.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (80.9.0) -Root-Is-Purelib: false -Tag: cp313-cp313-pyodide_2025_0_wasm32 - diff --git a/python_modules/pillow-11.3.0.dist-info/licenses/LICENSE b/python_modules/pillow-11.3.0.dist-info/licenses/LICENSE deleted file mode 100644 index 10dd42d9e..000000000 --- a/python_modules/pillow-11.3.0.dist-info/licenses/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ -The Python Imaging Library (PIL) is - - Copyright © 1997-2011 by Secret Labs AB - Copyright © 1995-2011 by Fredrik Lundh and contributors - -Pillow is the friendly PIL fork. It is - - Copyright © 2010 by Jeffrey A. Clark and contributors - -Like PIL, Pillow is licensed under the open source MIT-CMU License: - -By obtaining, using, and/or copying this software and/or its associated -documentation, you agree that you have read, understood, and will comply -with the following terms and conditions: - -Permission to use, copy, modify and distribute this software and its -documentation for any purpose and without fee is hereby granted, -provided that the above copyright notice appears in all copies, and that -both that copyright notice and this permission notice appear in supporting -documentation, and that the name of Secret Labs AB or the author not be -used in advertising or publicity pertaining to distribution of the software -without specific, written prior permission. - -SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS -SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. -IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, -INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. diff --git a/python_modules/pillow-11.3.0.dist-info/top_level.txt b/python_modules/pillow-11.3.0.dist-info/top_level.txt deleted file mode 100644 index b338169ce..000000000 --- a/python_modules/pillow-11.3.0.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -PIL diff --git a/python_modules/pillow-11.3.0.dist-info/zip-safe b/python_modules/pillow-11.3.0.dist-info/zip-safe deleted file mode 100644 index 8b1378917..000000000 --- a/python_modules/pillow-11.3.0.dist-info/zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/python_modules/pyvenv.cfg b/python_modules/pyvenv.cfg deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_modules/qrcode-8.2.dist-info/INSTALLER b/python_modules/qrcode-8.2.dist-info/INSTALLER deleted file mode 100644 index 5c69047b2..000000000 --- a/python_modules/qrcode-8.2.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -uv \ No newline at end of file diff --git a/python_modules/qrcode-8.2.dist-info/LICENSE b/python_modules/qrcode-8.2.dist-info/LICENSE deleted file mode 100644 index bb4b0c708..000000000 --- a/python_modules/qrcode-8.2.dist-info/LICENSE +++ /dev/null @@ -1,48 +0,0 @@ -Copyright (c) 2011, Lincoln Loop -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the package name nor the names of its contributors may be - used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -------------------------------------------------------------------------------- - - -Original text and license from the pyqrnative package where this was forked -from (http://code.google.com/p/pyqrnative): - -#Ported from the Javascript library by Sam Curren -# -#QRCode for Javascript -#http://d-project.googlecode.com/svn/trunk/misc/qrcode/js/qrcode.js -# -#Copyright (c) 2009 Kazuhiko Arase -# -#URL: http://www.d-project.com/ -# -#Licensed under the MIT license: -# http://www.opensource.org/licenses/mit-license.php -# -# The word "QR Code" is registered trademark of -# DENSO WAVE INCORPORATED -# http://www.denso-wave.com/qrcode/faqpatent-e.html diff --git a/python_modules/qrcode-8.2.dist-info/METADATA b/python_modules/qrcode-8.2.dist-info/METADATA deleted file mode 100644 index 3e84181b8..000000000 --- a/python_modules/qrcode-8.2.dist-info/METADATA +++ /dev/null @@ -1,668 +0,0 @@ -Metadata-Version: 2.1 -Name: qrcode -Version: 8.2 -Summary: QR Code image generator -Home-page: https://github.com/lincolnloop/python-qrcode -License: BSD -Keywords: qr,denso-wave,IEC18004 -Author: Lincoln Loop -Author-email: info@lincolnloop.com -Requires-Python: >=3.9,<4.0 -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License -Classifier: License :: Other/Proprietary License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.13 -Classifier: Topic :: Multimedia :: Graphics -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Provides-Extra: all -Provides-Extra: pil -Provides-Extra: png -Requires-Dist: colorama ; sys_platform == "win32" -Requires-Dist: pillow (>=9.1.0) ; extra == "pil" or extra == "all" -Requires-Dist: pypng ; extra == "png" or extra == "all" -Description-Content-Type: text/x-rst - -============================= -Pure python QR Code generator -============================= - -Generate QR codes. - -A standard install uses pypng_ to generate PNG files and can also render QR -codes directly to the console. A standard install is just:: - - pip install qrcode - -For more image functionality, install qrcode with the ``pil`` dependency so -that pillow_ is installed and can be used for generating images:: - - pip install "qrcode[pil]" - -.. _pypng: https://pypi.python.org/pypi/pypng -.. _pillow: https://pypi.python.org/pypi/Pillow - - -What is a QR Code? -================== - -A Quick Response code is a two-dimensional pictographic code used for its fast -readability and comparatively large storage capacity. The code consists of -black modules arranged in a square pattern on a white background. The -information encoded can be made up of any kind of data (e.g., binary, -alphanumeric, or Kanji symbols) - -Usage -===== - -From the command line, use the installed ``qr`` script:: - - qr "Some text" > test.png - -Or in Python, use the ``make`` shortcut function: - -.. code:: python - - import qrcode - img = qrcode.make('Some data here') - type(img) # qrcode.image.pil.PilImage - img.save("some_file.png") - -Advanced Usage --------------- - -For more control, use the ``QRCode`` class. For example: - -.. code:: python - - import qrcode - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=4, - ) - qr.add_data('Some data') - qr.make(fit=True) - - img = qr.make_image(fill_color="black", back_color="white") - -The ``version`` parameter is an integer from 1 to 40 that controls the size of -the QR Code (the smallest, version 1, is a 21x21 matrix). -Set to ``None`` and use the ``fit`` parameter when making the code to determine -this automatically. - -``fill_color`` and ``back_color`` can change the background and the painting -color of the QR, when using the default image factory. Both parameters accept -RGB color tuples. - -.. code:: python - - - img = qr.make_image(back_color=(255, 195, 235), fill_color=(55, 95, 35)) - -The ``error_correction`` parameter controls the error correction used for the -QR Code. The following four constants are made available on the ``qrcode`` -package: - -``ERROR_CORRECT_L`` - About 7% or less errors can be corrected. -``ERROR_CORRECT_M`` (default) - About 15% or less errors can be corrected. -``ERROR_CORRECT_Q`` - About 25% or less errors can be corrected. -``ERROR_CORRECT_H``. - About 30% or less errors can be corrected. - -The ``box_size`` parameter controls how many pixels each "box" of the QR code -is. - -The ``border`` parameter controls how many boxes thick the border should be -(the default is 4, which is the minimum according to the specs). - -Other image factories -===================== - -You can encode as SVG, or use a new pure Python image processor to encode to -PNG images. - -The Python examples below use the ``make`` shortcut. The same ``image_factory`` -keyword argument is a valid option for the ``QRCode`` class for more advanced -usage. - -SVG ---- - -You can create the entire SVG or an SVG fragment. When building an entire SVG -image, you can use the factory that combines as a path (recommended, and -default for the script) or a factory that creates a simple set of rectangles. - -From your command line:: - - qr --factory=svg-path "Some text" > test.svg - qr --factory=svg "Some text" > test.svg - qr --factory=svg-fragment "Some text" > test.svg - -Or in Python: - -.. code:: python - - import qrcode - import qrcode.image.svg - - if method == 'basic': - # Simple factory, just a set of rects. - factory = qrcode.image.svg.SvgImage - elif method == 'fragment': - # Fragment factory (also just a set of rects) - factory = qrcode.image.svg.SvgFragmentImage - else: - # Combined path factory, fixes white space that may occur when zooming - factory = qrcode.image.svg.SvgPathImage - - img = qrcode.make('Some data here', image_factory=factory) - -Two other related factories are available that work the same, but also fill the -background of the SVG with white:: - - qrcode.image.svg.SvgFillImage - qrcode.image.svg.SvgPathFillImage - -The ``QRCode.make_image()`` method forwards additional keyword arguments to the -underlying ElementTree XML library. This helps to fine tune the root element of -the resulting SVG: - -.. code:: python - - import qrcode - qr = qrcode.QRCode(image_factory=qrcode.image.svg.SvgPathImage) - qr.add_data('Some data') - qr.make(fit=True) - - img = qr.make_image(attrib={'class': 'some-css-class'}) - -You can convert the SVG image into strings using the ``to_string()`` method. -Additional keyword arguments are forwarded to ElementTrees ``tostring()``: - -.. code:: python - - img.to_string(encoding='unicode') - - -Pure Python PNG ---------------- - -If Pillow is not installed, the default image factory will be a pure Python PNG -encoder that uses `pypng`. - -You can use the factory explicitly from your command line:: - - qr --factory=png "Some text" > test.png - -Or in Python: - -.. code:: python - - import qrcode - from qrcode.image.pure import PyPNGImage - img = qrcode.make('Some data here', image_factory=PyPNGImage) - - -Styled Image ------------- - -Works only with versions_ >=7.2 (SVG styled images require 7.4). - -.. _versions: https://github.com/lincolnloop/python-qrcode/blob/master/CHANGES.rst#72-19-july-2021 - -To apply styles to the QRCode, use the ``StyledPilImage`` or one of the -standard SVG_ image factories. These accept an optional ``module_drawer`` -parameter to control the shape of the QR Code. - -These QR Codes are not guaranteed to work with all readers, so do some -experimentation and set the error correction to high (especially if embedding -an image). - -Other PIL module drawers: - - .. image:: doc/module_drawers.png - -For SVGs, use ``SvgSquareDrawer``, ``SvgCircleDrawer``, -``SvgPathSquareDrawer``, or ``SvgPathCircleDrawer``. - -These all accept a ``size_ratio`` argument which allows for "gapped" squares or -circles by reducing this less than the default of ``Decimal(1)``. - - -The ``StyledPilImage`` additionally accepts an optional ``color_mask`` -parameter to change the colors of the QR Code, and an optional -``embedded_image_path`` to embed an image in the center of the code. - -Other color masks: - - .. image:: doc/color_masks.png - -Here is a code example to draw a QR code with rounded corners, radial gradient -and an embedded image: - -.. code:: python - - import qrcode - from qrcode.image.styledpil import StyledPilImage - from qrcode.image.styles.moduledrawers.pil import RoundedModuleDrawer - from qrcode.image.styles.colormasks import RadialGradiantColorMask - - qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H) - qr.add_data('Some data') - - img_1 = qr.make_image(image_factory=StyledPilImage, module_drawer=RoundedModuleDrawer()) - img_2 = qr.make_image(image_factory=StyledPilImage, color_mask=RadialGradiantColorMask()) - img_3 = qr.make_image(image_factory=StyledPilImage, embedded_image_path="/path/to/image.png") - -Examples -======== - -Get the text content from `print_ascii`: - -.. code:: python - - import io - import qrcode - qr = qrcode.QRCode() - qr.add_data("Some text") - f = io.StringIO() - qr.print_ascii(out=f) - f.seek(0) - print(f.read()) - -The `add_data` method will append data to the current QR object. To add new data by replacing previous content in the same object, first use clear method: - -.. code:: python - - import qrcode - qr = qrcode.QRCode() - qr.add_data('Some data') - img = qr.make_image() - qr.clear() - qr.add_data('New data') - other_img = qr.make_image() - -Pipe ascii output to text file in command line:: - - qr --ascii "Some data" > "test.txt" - cat test.txt - -Alternative to piping output to file to avoid PowerShell issues:: - - # qr "Some data" > test.png - qr --output=test.png "Some data" - -========== -Change log -========== - -8.2 (01 May 2025) -================= - -- Optimize QRColorMask apply_mask method for enhanced performance -- Fix typos on StyledPilImage embeded_* parameters. - The old parameters with the typos are still accepted - for backward compatibility. - - -8.1 (02 April 2025) -==================== - -- Added support for Python 3.13. - -8.0 (27 September 2024) -======================== - -- Added support for Python 3.11 and 3.12. - -- Drop support for Python <=3.8. - -- Change local development setup to use Poetry_. - -- Testsuite and code quality checks are done through Github Actions. - -- Code quality and formatting utilises ruff_. - -- Removed ``typing_extensions`` as a dependency, as it's no longer required - with having Python 3.9+ as a requirement. - having Python 3.9+ as a requirement. - -- Only allow high error correction rate (`qrcode.ERROR_CORRECT_H`) - when generating - QR codes with embedded images to ensure content is readable - -.. _Poetry: https://python-poetry.org -.. _ruff: https://astral.sh/ruff - - -7.4.2 (6 February 2023) -======================= - -- Allow ``pypng`` factory to allow for saving to a string (like - ``qr.save("some_file.png")``) in addition to file-like objects. - - -7.4.1 (3 February 2023) -======================= - -- Fix bad over-optimization in v7.4 that broke large QR codes. Thanks to - mattiasj-axis! - - -7.4 (1 February 2023) -===================== - -- Restructure the factory drawers, allowing different shapes in SVG image - factories as well. - -- Add a ``--factory-drawer`` option to the ``qr`` console script. - -- Optimize the output for the ``SVGPathImage`` factory (more than 30% reduction - in file sizes). - -- Add a ``pypng`` image factory as a pure Python PNG solution. If ``pillow`` is - *not* installed, then this becomes the default factory. - -- The ``pymaging`` image factory has been removed, but its factory shortcut and - the actual PymagingImage factory class now just link to the PyPNGImage - factory. - - -7.3.1 (1 October 2021) -====================== - -- Improvements for embedded image. - - -7.3 (19 August 2021) -==================== - -- Skip color mask if QR is black and white - - -7.2 (19 July 2021) -================== - -- Add Styled PIL image factory, allowing different color masks and shapes in QR codes - -- Small performance inprovement - -- Add check for border size parameter - - -7.1 (1 July 2021) -================= - -- Add --ascii parameter to command line interface allowing to output ascii when stdout is piped - -- Add --output parameter to command line interface to specify output file - -- Accept RGB tuples in fill_color and back_color - -- Add to_string method to SVG images - -- Replace inline styles with SVG attributes to avoid CSP issues - -- Add Python3.10 to supported versions - - -7.0 (29 June 2021) -================== - -- Drop Python < 3.6 support. - - -6.1 (14 January 2019) -===================== - -- Fix short chunks of data not being optimized to the correct mode. - -- Tests fixed for Python 3 - - -6.0 (23 March 2018) -=================== - -- Fix optimize length being ignored in ``QRCode.add_data``. - -- Better calculation of the best mask pattern and related optimizations. Big - thanks to cryptogun! - - -5.3 (18 May 2016) -================= - -* Fix incomplete block table for QR version 15. Thanks Rodrigo Queiro for the - report and Jacob Welsh for the investigation and fix. - -* Avoid unnecessary dependency for non MS platforms, thanks to Noah Vesely. - -* Make ``BaseImage.get_image()`` actually work. - - -5.2 (25 Jan 2016) -================= - -* Add ``--error-correction`` option to qr script. - -* Fix script piping to stdout in Python 3 and reading non-UTF-8 characters in - Python 3. - -* Fix script piping in Windows. - -* Add some useful behind-the-curtain methods for tinkerers. - -* Fix terminal output when using Python 2.6 - -* Fix terminal output to display correctly on MS command line. - -5.2.1 ------ - -* Small fix to terminal output in Python 3 (and fix tests) - -5.2.2 ------ - -* Revert some terminal changes from 5.2 that broke Python 3's real life tty - code generation and introduce a better way from Jacob Welsh. - - -5.1 (22 Oct 2014) -================= - -* Make ``qr`` script work in Windows. Thanks Ionel Cristian Mărieș - -* Fixed print_ascii function in Python 3. - -* Out-of-bounds code version numbers are handled more consistently with a - ValueError. - -* Much better test coverage (now only officially supporting Python 2.6+) - - -5.0 (17 Jun 2014) -================= - -* Speed optimizations. - -* Change the output when using the ``qr`` script to use ASCII rather than - just colors, better using the terminal real estate. - -* Fix a bug in passing bytecode data directly when in Python 3. - -* Substation speed optimizations to best-fit algorithm (thanks Jacob Welsh!). - -* Introduce a ``print_ascii`` method and use it as the default for the ``qr`` - script rather than ``print_tty``. - -5.0.1 ------ - -* Update version numbers correctly. - - -4.0 (4 Sep 2013) -================ - -* Made qrcode work on Python 2.4 - Thanks tcely. - Note: officially, qrcode only supports 2.5+. - -* Support pure-python PNG generation (via pymaging) for Python 2.6+ -- thanks - Adam Wisniewski! - -* SVG image generation now supports alternate sizing (the default box size of - 10 == 1mm per rectangle). - -* SVG path image generation allows cleaner SVG output by combining all QR rects - into a single path. Thank you, Viktor Stískala. - -* Added some extra simple SVG factories that fill the background white. - -4.0.1 ------ - -* Fix the pymaging backend not able to save the image to a buffer. Thanks ilj! - -4.0.2 ------ - -* Fix incorrect regex causing a comma to be considered part of the alphanumeric - set. - -* Switch to using setuptools for setup.py. - -4.0.3 ------ - -* Fix bad QR code generation due to the regex comma fix in version 4.0.2. - -4.0.4 ------ - -* Bad version number for previous hotfix release. - - -3.1 (12 Aug 2013) -================= - -* Important fixes for incorrect matches of the alphanumeric encoding mode. - Previously, the pattern would match if a single line was alphanumeric only - (even if others wern't). Also, the two characters ``{`` and ``}`` had snuck - in as valid characters. Thanks to Eran Tromer for the report and fix. - -* Optimized chunking -- if the parts of the data stream can be encoded more - efficiently, the data will be split into chunks of the most efficient modes. - -3.1.1 ------ - -* Update change log to contain version 3.1 changes. :P - -* Give the ``qr`` script an ``--optimize`` argument to control the chunk - optimization setting. - - -3.0 (25 Jun 2013) -================= - -* Python 3 support. - -* Add QRCode.get_matrix, an easy way to get the matrix array of a QR code - including the border. Thanks Hugh Rawlinson. - -* Add in a workaround so that Python 2.6 users can use SVG generation (they - must install ``lxml``). - -* Some initial tests! And tox support (``pip install tox``) for testing across - Python platforms. - - -2.7 (5 Mar 2013) -================ - -* Fix incorrect termination padding. - - -2.6 (2 Apr 2013) -================ - -* Fix the first four columns incorrectly shifted by one. Thanks to Josep - Gómez-Suay for the report and fix. - -* Fix strings within 4 bits of the QR version limit being incorrectly - terminated. Thanks to zhjie231 for the report. - - -2.5 (12 Mar 2013) -================= - -* The PilImage wrapper is more transparent - you can use any methods or - attributes available to the underlying PIL Image instance. - -* Fixed the first column of the QR Code coming up empty! Thanks to BecoKo. - -2.5.1 ------ - -* Fix installation error on Windows. - - -2.4 (23 Apr 2012) -================= - -* Use a pluggable backend system for generating images, thanks to Branko Čibej! - Comes with PIL and SVG backends built in. - -2.4.1 ------ - -* Fix a packaging issue - -2.4.2 ------ - -* Added a ``show`` method to the PIL image wrapper so the ``run_example`` - function actually works. - - -2.3 (29 Jan 2012) -================= - -* When adding data, auto-select the more efficient encoding methods for numbers - and alphanumeric data (KANJI still not supported). - -2.3.1 ------ - -* Encode unicode to utf-8 bytestrings when adding data to a QRCode. - - -2.2 (18 Jan 2012) -================= - -* Fixed tty output to work on both white and black backgrounds. - -* Added `border` parameter to allow customizing of the number of boxes used to - create the border of the QR code - - -2.1 (17 Jan 2012) -================= - -* Added a ``qr`` script which can be used to output a qr code to the tty using - background colors, or to a file via a pipe. - diff --git a/python_modules/qrcode-8.2.dist-info/RECORD b/python_modules/qrcode-8.2.dist-info/RECORD deleted file mode 100644 index e235a1e4a..000000000 --- a/python_modules/qrcode-8.2.dist-info/RECORD +++ /dev/null @@ -1,42 +0,0 @@ -../../Scripts/qr.exe,sha256=2LA_6m7Th5Ne3B9vQbJ2uaf0VYWHGqwFdCULvY0lJkQ,46080 -qrcode-8.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -qrcode-8.2.dist-info/LICENSE,sha256=QN-5A8lO4_eJUAExMRGGVI7Lpc79NVdiPXcA4lIquZQ,2143 -qrcode-8.2.dist-info/METADATA,sha256=Oo8b5tqUKLl4BiktBeMUgmS5BTwi55iUkYtnDpMK_DY,17686 -qrcode-8.2.dist-info/RECORD,, -qrcode-8.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -qrcode-8.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88 -qrcode-8.2.dist-info/entry_points.txt,sha256=jokYBrUZ_Sf1bO7FcE53iIhHYn1CJ9_a5SohTIayOP8,50 -qrcode/LUT.py,sha256=NjXKPfHSTFYoLlGkXhFjf2OUq_EGD6mrdyYHIG3dNck,3599 -qrcode/__init__.py,sha256=0C8jx3gDHSJ4yydlHN01ytyipNh2pMO3VYS9Dk-m4oU,645 -qrcode/base.py,sha256=9J_1LynF5dXJK14Azs8XyHJY66FfTluYJ66F8ZjeStY,7288 -qrcode/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -qrcode/compat/etree.py,sha256=rEyWRA9QMsVFva_9rOdth3RAkRpFOmkF59c2EQM44gE,152 -qrcode/compat/png.py,sha256=OCe5WsuiTI_UTqmyVqLbcJloSbZvgCYJdMCznKvMCCM,171 -qrcode/console_scripts.py,sha256=n-bQ5vpKtcjG30l1jkQ_q22HTV4X-JEMwkLRdJno8vc,5558 -qrcode/constants.py,sha256=0Csa8YYdeQ8NaFrRmt43maVg12O89d-oKgiKAVIO2s4,106 -qrcode/exceptions.py,sha256=L2fZuYOKscvdn72ra-wF8Gwsr2ZB9eRZWrp1f0IDx4E,45 -qrcode/image/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -qrcode/image/base.py,sha256=jCrbt4UD1ZfOC8jMFjK3elZfgUJ7M_FsHKRMVvje-BE,4965 -qrcode/image/pil.py,sha256=y5a3t6VB4gnvsTK4eSR04YEVP_Qk82iT1NXL6jFP1jQ,1589 -qrcode/image/pure.py,sha256=B8PJANvAHPyd1DaBtAC83Csb2xKbZ-fP4SSWuw1NNvU,1525 -qrcode/image/styledpil.py,sha256=RC7JoDS-Uzez2nN-I2xwePsGX3qYDeHg8YepT2FbO_M,4951 -qrcode/image/styles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -qrcode/image/styles/colormasks.py,sha256=h8asIvQKMTRuq6bFzvaiZgFyoa-2tqvIb_hB5H5XwM0,7936 -qrcode/image/styles/moduledrawers/__init__.py,sha256=Mklw5SjYiGbs2Aym38jwwrKt0plJGzwIVgZ--jiOVBc,430 -qrcode/image/styles/moduledrawers/base.py,sha256=gLFq20p07tBEmsFfirEM19XZshYsaNP2Wr1UNAkJq90,1019 -qrcode/image/styles/moduledrawers/pil.py,sha256=lkT8I8q8PUB_TdYBrP5DlzKN8UtQW-XQEYqoXBWkD7Y,9773 -qrcode/image/styles/moduledrawers/svg.py,sha256=-WngEvZF8LwtTpWmSdt0tDZ6dKtYzLctYDNY-Mi7crc,3936 -qrcode/image/svg.py,sha256=G2dmuybVP3fwkgyrFF5RfQT3dpWDCYl80OKe2Xal8gU,5188 -qrcode/main.py,sha256=OF7uHDAz2Tihpe6Fftef6fiVH2tpoVJ5ekLCIG4lyJA,16869 -qrcode/release.py,sha256=wJjVEklWnATUh8CU88HEKyhUgZU9hzpl__SZYyyNUZo,1080 -qrcode/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -qrcode/tests/consts.py,sha256=Tn2AbI9zTEi_KiAish6f74AbBWrZg8A-Js8jTrN_vF0,96 -qrcode/tests/test_example.py,sha256=z5p5Tnumnj0EWsKo6YJ4vuUQM7KjisvgLwbJt8wTAG0,244 -qrcode/tests/test_qrcode.py,sha256=FPjfdmLAXa0lrbspQXbtD1wPKMggCOPN_O4tT-jLJug,6461 -qrcode/tests/test_qrcode_pil.py,sha256=m12SfImnfgqzvKmdR4mMQCSVb9GyNnNJ4Edxwjm7tus,5148 -qrcode/tests/test_qrcode_pypng.py,sha256=fgTr78vX1T_cH03mS_ikHJgoITKLU11bJMXEd0Um5yA,944 -qrcode/tests/test_qrcode_svg.py,sha256=21enlZjXNUm0M_ZoK_AMp0UE4mELoCeIWE6UveQsJwk,1296 -qrcode/tests/test_release.py,sha256=SVCXUx4BeNz_d3osbBNdW2N3rstbErGvY5N2V_kHrhc,1341 -qrcode/tests/test_script.py,sha256=1gchpke_DKZLEhtN5cZJBZTT28fhbjvXfAPttvvM5tA,2908 -qrcode/tests/test_util.py,sha256=Pgnp1DRFe44YRH9deR9_9Nb9zH-ska61CIYkVWwuy9A,207 -qrcode/util.py,sha256=VOG4RrJ6QkPs0fLaZkfeMwxwxdIpKsRHm6dXvjo9Yl4,17103 diff --git a/python_modules/qrcode-8.2.dist-info/REQUESTED b/python_modules/qrcode-8.2.dist-info/REQUESTED deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_modules/qrcode-8.2.dist-info/WHEEL b/python_modules/qrcode-8.2.dist-info/WHEEL deleted file mode 100644 index d73ccaae8..000000000 --- a/python_modules/qrcode-8.2.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: poetry-core 1.9.0 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/python_modules/qrcode-8.2.dist-info/entry_points.txt b/python_modules/qrcode-8.2.dist-info/entry_points.txt deleted file mode 100644 index 66dc3c52f..000000000 --- a/python_modules/qrcode-8.2.dist-info/entry_points.txt +++ /dev/null @@ -1,3 +0,0 @@ -[console_scripts] -qr=qrcode.console_scripts:main - diff --git a/python_modules/qrcode/LUT.py b/python_modules/qrcode/LUT.py deleted file mode 100644 index 115892f13..000000000 --- a/python_modules/qrcode/LUT.py +++ /dev/null @@ -1,223 +0,0 @@ -# Store all kinds of lookup table. - - -# # generate rsPoly lookup table. - -# from qrcode import base - -# def create_bytes(rs_blocks): -# for r in range(len(rs_blocks)): -# dcCount = rs_blocks[r].data_count -# ecCount = rs_blocks[r].total_count - dcCount -# rsPoly = base.Polynomial([1], 0) -# for i in range(ecCount): -# rsPoly = rsPoly * base.Polynomial([1, base.gexp(i)], 0) -# return ecCount, rsPoly - -# rsPoly_LUT = {} -# for version in range(1,41): -# for error_correction in range(4): -# rs_blocks_list = base.rs_blocks(version, error_correction) -# ecCount, rsPoly = create_bytes(rs_blocks_list) -# rsPoly_LUT[ecCount]=rsPoly.num -# print(rsPoly_LUT) - -# Result. Usage: input: ecCount, output: Polynomial.num -# e.g. rsPoly = base.Polynomial(LUT.rsPoly_LUT[ecCount], 0) -rsPoly_LUT = { - 7: [1, 127, 122, 154, 164, 11, 68, 117], - 10: [1, 216, 194, 159, 111, 199, 94, 95, 113, 157, 193], - 13: [1, 137, 73, 227, 17, 177, 17, 52, 13, 46, 43, 83, 132, 120], - 15: [1, 29, 196, 111, 163, 112, 74, 10, 105, 105, 139, 132, 151, 32, 134, 26], - 16: [1, 59, 13, 104, 189, 68, 209, 30, 8, 163, 65, 41, 229, 98, 50, 36, 59], - 17: [1, 119, 66, 83, 120, 119, 22, 197, 83, 249, 41, 143, 134, 85, 53, 125, 99, 79], - 18: [ - 1, - 239, - 251, - 183, - 113, - 149, - 175, - 199, - 215, - 240, - 220, - 73, - 82, - 173, - 75, - 32, - 67, - 217, - 146, - ], - 20: [ - 1, - 152, - 185, - 240, - 5, - 111, - 99, - 6, - 220, - 112, - 150, - 69, - 36, - 187, - 22, - 228, - 198, - 121, - 121, - 165, - 174, - ], - 22: [ - 1, - 89, - 179, - 131, - 176, - 182, - 244, - 19, - 189, - 69, - 40, - 28, - 137, - 29, - 123, - 67, - 253, - 86, - 218, - 230, - 26, - 145, - 245, - ], - 24: [ - 1, - 122, - 118, - 169, - 70, - 178, - 237, - 216, - 102, - 115, - 150, - 229, - 73, - 130, - 72, - 61, - 43, - 206, - 1, - 237, - 247, - 127, - 217, - 144, - 117, - ], - 26: [ - 1, - 246, - 51, - 183, - 4, - 136, - 98, - 199, - 152, - 77, - 56, - 206, - 24, - 145, - 40, - 209, - 117, - 233, - 42, - 135, - 68, - 70, - 144, - 146, - 77, - 43, - 94, - ], - 28: [ - 1, - 252, - 9, - 28, - 13, - 18, - 251, - 208, - 150, - 103, - 174, - 100, - 41, - 167, - 12, - 247, - 56, - 117, - 119, - 233, - 127, - 181, - 100, - 121, - 147, - 176, - 74, - 58, - 197, - ], - 30: [ - 1, - 212, - 246, - 77, - 73, - 195, - 192, - 75, - 98, - 5, - 70, - 103, - 177, - 22, - 217, - 138, - 51, - 181, - 246, - 72, - 25, - 18, - 46, - 228, - 74, - 216, - 195, - 11, - 106, - 130, - 150, - ], -} diff --git a/python_modules/qrcode/__init__.py b/python_modules/qrcode/__init__.py deleted file mode 100644 index 6b238d33e..000000000 --- a/python_modules/qrcode/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -from qrcode.main import QRCode -from qrcode.main import make # noqa -from qrcode.constants import ( # noqa - ERROR_CORRECT_L, - ERROR_CORRECT_M, - ERROR_CORRECT_Q, - ERROR_CORRECT_H, -) - -from qrcode import image # noqa - - -def run_example(data="http://www.lincolnloop.com", *args, **kwargs): - """ - Build an example QR Code and display it. - - There's an even easier way than the code here though: just use the ``make`` - shortcut. - """ - qr = QRCode(*args, **kwargs) - qr.add_data(data) - - im = qr.make_image() - im.show() - - -if __name__ == "__main__": # pragma: no cover - import sys - - run_example(*sys.argv[1:]) diff --git a/python_modules/qrcode/base.py b/python_modules/qrcode/base.py deleted file mode 100644 index 20f81f6ff..000000000 --- a/python_modules/qrcode/base.py +++ /dev/null @@ -1,313 +0,0 @@ -from typing import NamedTuple -from qrcode import constants - -EXP_TABLE = list(range(256)) - -LOG_TABLE = list(range(256)) - -for i in range(8): - EXP_TABLE[i] = 1 << i - -for i in range(8, 256): - EXP_TABLE[i] = ( - EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^ EXP_TABLE[i - 8] - ) - -for i in range(255): - LOG_TABLE[EXP_TABLE[i]] = i - -RS_BLOCK_OFFSET = { - constants.ERROR_CORRECT_L: 0, - constants.ERROR_CORRECT_M: 1, - constants.ERROR_CORRECT_Q: 2, - constants.ERROR_CORRECT_H: 3, -} - -RS_BLOCK_TABLE = ( - # L - # M - # Q - # H - # 1 - (1, 26, 19), - (1, 26, 16), - (1, 26, 13), - (1, 26, 9), - # 2 - (1, 44, 34), - (1, 44, 28), - (1, 44, 22), - (1, 44, 16), - # 3 - (1, 70, 55), - (1, 70, 44), - (2, 35, 17), - (2, 35, 13), - # 4 - (1, 100, 80), - (2, 50, 32), - (2, 50, 24), - (4, 25, 9), - # 5 - (1, 134, 108), - (2, 67, 43), - (2, 33, 15, 2, 34, 16), - (2, 33, 11, 2, 34, 12), - # 6 - (2, 86, 68), - (4, 43, 27), - (4, 43, 19), - (4, 43, 15), - # 7 - (2, 98, 78), - (4, 49, 31), - (2, 32, 14, 4, 33, 15), - (4, 39, 13, 1, 40, 14), - # 8 - (2, 121, 97), - (2, 60, 38, 2, 61, 39), - (4, 40, 18, 2, 41, 19), - (4, 40, 14, 2, 41, 15), - # 9 - (2, 146, 116), - (3, 58, 36, 2, 59, 37), - (4, 36, 16, 4, 37, 17), - (4, 36, 12, 4, 37, 13), - # 10 - (2, 86, 68, 2, 87, 69), - (4, 69, 43, 1, 70, 44), - (6, 43, 19, 2, 44, 20), - (6, 43, 15, 2, 44, 16), - # 11 - (4, 101, 81), - (1, 80, 50, 4, 81, 51), - (4, 50, 22, 4, 51, 23), - (3, 36, 12, 8, 37, 13), - # 12 - (2, 116, 92, 2, 117, 93), - (6, 58, 36, 2, 59, 37), - (4, 46, 20, 6, 47, 21), - (7, 42, 14, 4, 43, 15), - # 13 - (4, 133, 107), - (8, 59, 37, 1, 60, 38), - (8, 44, 20, 4, 45, 21), - (12, 33, 11, 4, 34, 12), - # 14 - (3, 145, 115, 1, 146, 116), - (4, 64, 40, 5, 65, 41), - (11, 36, 16, 5, 37, 17), - (11, 36, 12, 5, 37, 13), - # 15 - (5, 109, 87, 1, 110, 88), - (5, 65, 41, 5, 66, 42), - (5, 54, 24, 7, 55, 25), - (11, 36, 12, 7, 37, 13), - # 16 - (5, 122, 98, 1, 123, 99), - (7, 73, 45, 3, 74, 46), - (15, 43, 19, 2, 44, 20), - (3, 45, 15, 13, 46, 16), - # 17 - (1, 135, 107, 5, 136, 108), - (10, 74, 46, 1, 75, 47), - (1, 50, 22, 15, 51, 23), - (2, 42, 14, 17, 43, 15), - # 18 - (5, 150, 120, 1, 151, 121), - (9, 69, 43, 4, 70, 44), - (17, 50, 22, 1, 51, 23), - (2, 42, 14, 19, 43, 15), - # 19 - (3, 141, 113, 4, 142, 114), - (3, 70, 44, 11, 71, 45), - (17, 47, 21, 4, 48, 22), - (9, 39, 13, 16, 40, 14), - # 20 - (3, 135, 107, 5, 136, 108), - (3, 67, 41, 13, 68, 42), - (15, 54, 24, 5, 55, 25), - (15, 43, 15, 10, 44, 16), - # 21 - (4, 144, 116, 4, 145, 117), - (17, 68, 42), - (17, 50, 22, 6, 51, 23), - (19, 46, 16, 6, 47, 17), - # 22 - (2, 139, 111, 7, 140, 112), - (17, 74, 46), - (7, 54, 24, 16, 55, 25), - (34, 37, 13), - # 23 - (4, 151, 121, 5, 152, 122), - (4, 75, 47, 14, 76, 48), - (11, 54, 24, 14, 55, 25), - (16, 45, 15, 14, 46, 16), - # 24 - (6, 147, 117, 4, 148, 118), - (6, 73, 45, 14, 74, 46), - (11, 54, 24, 16, 55, 25), - (30, 46, 16, 2, 47, 17), - # 25 - (8, 132, 106, 4, 133, 107), - (8, 75, 47, 13, 76, 48), - (7, 54, 24, 22, 55, 25), - (22, 45, 15, 13, 46, 16), - # 26 - (10, 142, 114, 2, 143, 115), - (19, 74, 46, 4, 75, 47), - (28, 50, 22, 6, 51, 23), - (33, 46, 16, 4, 47, 17), - # 27 - (8, 152, 122, 4, 153, 123), - (22, 73, 45, 3, 74, 46), - (8, 53, 23, 26, 54, 24), - (12, 45, 15, 28, 46, 16), - # 28 - (3, 147, 117, 10, 148, 118), - (3, 73, 45, 23, 74, 46), - (4, 54, 24, 31, 55, 25), - (11, 45, 15, 31, 46, 16), - # 29 - (7, 146, 116, 7, 147, 117), - (21, 73, 45, 7, 74, 46), - (1, 53, 23, 37, 54, 24), - (19, 45, 15, 26, 46, 16), - # 30 - (5, 145, 115, 10, 146, 116), - (19, 75, 47, 10, 76, 48), - (15, 54, 24, 25, 55, 25), - (23, 45, 15, 25, 46, 16), - # 31 - (13, 145, 115, 3, 146, 116), - (2, 74, 46, 29, 75, 47), - (42, 54, 24, 1, 55, 25), - (23, 45, 15, 28, 46, 16), - # 32 - (17, 145, 115), - (10, 74, 46, 23, 75, 47), - (10, 54, 24, 35, 55, 25), - (19, 45, 15, 35, 46, 16), - # 33 - (17, 145, 115, 1, 146, 116), - (14, 74, 46, 21, 75, 47), - (29, 54, 24, 19, 55, 25), - (11, 45, 15, 46, 46, 16), - # 34 - (13, 145, 115, 6, 146, 116), - (14, 74, 46, 23, 75, 47), - (44, 54, 24, 7, 55, 25), - (59, 46, 16, 1, 47, 17), - # 35 - (12, 151, 121, 7, 152, 122), - (12, 75, 47, 26, 76, 48), - (39, 54, 24, 14, 55, 25), - (22, 45, 15, 41, 46, 16), - # 36 - (6, 151, 121, 14, 152, 122), - (6, 75, 47, 34, 76, 48), - (46, 54, 24, 10, 55, 25), - (2, 45, 15, 64, 46, 16), - # 37 - (17, 152, 122, 4, 153, 123), - (29, 74, 46, 14, 75, 47), - (49, 54, 24, 10, 55, 25), - (24, 45, 15, 46, 46, 16), - # 38 - (4, 152, 122, 18, 153, 123), - (13, 74, 46, 32, 75, 47), - (48, 54, 24, 14, 55, 25), - (42, 45, 15, 32, 46, 16), - # 39 - (20, 147, 117, 4, 148, 118), - (40, 75, 47, 7, 76, 48), - (43, 54, 24, 22, 55, 25), - (10, 45, 15, 67, 46, 16), - # 40 - (19, 148, 118, 6, 149, 119), - (18, 75, 47, 31, 76, 48), - (34, 54, 24, 34, 55, 25), - (20, 45, 15, 61, 46, 16), -) - - -def glog(n): - if n < 1: # pragma: no cover - raise ValueError(f"glog({n})") - return LOG_TABLE[n] - - -def gexp(n): - return EXP_TABLE[n % 255] - - -class Polynomial: - def __init__(self, num, shift): - if not num: # pragma: no cover - raise Exception(f"{len(num)}/{shift}") - - offset = 0 - for offset in range(len(num)): - if num[offset] != 0: - break - - self.num = num[offset:] + [0] * shift - - def __getitem__(self, index): - return self.num[index] - - def __iter__(self): - return iter(self.num) - - def __len__(self): - return len(self.num) - - def __mul__(self, other): - num = [0] * (len(self) + len(other) - 1) - - for i, item in enumerate(self): - for j, other_item in enumerate(other): - num[i + j] ^= gexp(glog(item) + glog(other_item)) - - return Polynomial(num, 0) - - def __mod__(self, other): - difference = len(self) - len(other) - if difference < 0: - return self - - ratio = glog(self[0]) - glog(other[0]) - - num = [ - item ^ gexp(glog(other_item) + ratio) - for item, other_item in zip(self, other) - ] - if difference: - num.extend(self[-difference:]) - - # recursive call - return Polynomial(num, 0) % other - - -class RSBlock(NamedTuple): - total_count: int - data_count: int - - -def rs_blocks(version, error_correction): - if error_correction not in RS_BLOCK_OFFSET: # pragma: no cover - raise Exception( - "bad rs block @ version: %s / error_correction: %s" - % (version, error_correction) - ) - offset = RS_BLOCK_OFFSET[error_correction] - rs_block = RS_BLOCK_TABLE[(version - 1) * 4 + offset] - - blocks = [] - - for i in range(0, len(rs_block), 3): - count, total_count, data_count = rs_block[i : i + 3] - for _ in range(count): - blocks.append(RSBlock(total_count, data_count)) - - return blocks diff --git a/python_modules/qrcode/compat/__init__.py b/python_modules/qrcode/compat/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_modules/qrcode/compat/etree.py b/python_modules/qrcode/compat/etree.py deleted file mode 100644 index 6739d2271..000000000 --- a/python_modules/qrcode/compat/etree.py +++ /dev/null @@ -1,4 +0,0 @@ -try: - import lxml.etree as ET # type: ignore # noqa: F401 -except ImportError: - import xml.etree.ElementTree as ET # type: ignore # noqa: F401 diff --git a/python_modules/qrcode/compat/png.py b/python_modules/qrcode/compat/png.py deleted file mode 100644 index 8d7b90566..000000000 --- a/python_modules/qrcode/compat/png.py +++ /dev/null @@ -1,7 +0,0 @@ -# Try to import png library. -PngWriter = None - -try: - from png import Writer as PngWriter # type: ignore # noqa: F401 -except ImportError: # pragma: no cover - pass diff --git a/python_modules/qrcode/console_scripts.py b/python_modules/qrcode/console_scripts.py deleted file mode 100644 index ebe8810f8..000000000 --- a/python_modules/qrcode/console_scripts.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python -""" -qr - Convert stdin (or the first argument) to a QR Code. - -When stdout is a tty the QR Code is printed to the terminal and when stdout is -a pipe to a file an image is written. The default image format is PNG. -""" - -import optparse -import os -import sys -from typing import NoReturn, Optional -from collections.abc import Iterable -from importlib import metadata - -import qrcode -from qrcode.image.base import BaseImage, DrawerAliases - -# The next block is added to get the terminal to display properly on MS platforms -if sys.platform.startswith(("win", "cygwin")): # pragma: no cover - import colorama # type: ignore - - colorama.init() - -default_factories = { - "pil": "qrcode.image.pil.PilImage", - "png": "qrcode.image.pure.PyPNGImage", - "svg": "qrcode.image.svg.SvgImage", - "svg-fragment": "qrcode.image.svg.SvgFragmentImage", - "svg-path": "qrcode.image.svg.SvgPathImage", - # Keeping for backwards compatibility: - "pymaging": "qrcode.image.pure.PymagingImage", -} - -error_correction = { - "L": qrcode.ERROR_CORRECT_L, - "M": qrcode.ERROR_CORRECT_M, - "Q": qrcode.ERROR_CORRECT_Q, - "H": qrcode.ERROR_CORRECT_H, -} - - -def main(args=None): - if args is None: - args = sys.argv[1:] - - version = metadata.version("qrcode") - parser = optparse.OptionParser(usage=(__doc__ or "").strip(), version=version) - - # Wrap parser.error in a typed NoReturn method for better typing. - def raise_error(msg: str) -> NoReturn: - parser.error(msg) - raise # pragma: no cover - - parser.add_option( - "--factory", - help="Full python path to the image factory class to " - "create the image with. You can use the following shortcuts to the " - f"built-in image factory classes: {commas(default_factories)}.", - ) - parser.add_option( - "--factory-drawer", - help=f"Use an alternate drawer. {get_drawer_help()}.", - ) - parser.add_option( - "--optimize", - type=int, - help="Optimize the data by looking for chunks " - "of at least this many characters that could use a more efficient " - "encoding method. Use 0 to turn off chunk optimization.", - ) - parser.add_option( - "--error-correction", - type="choice", - choices=sorted(error_correction.keys()), - default="M", - help="The error correction level to use. Choices are L (7%), " - "M (15%, default), Q (25%), and H (30%).", - ) - parser.add_option( - "--ascii", help="Print as ascii even if stdout is piped.", action="store_true" - ) - parser.add_option( - "--output", - help="The output file. If not specified, the image is sent to " - "the standard output.", - ) - - opts, args = parser.parse_args(args) - - if opts.factory: - module = default_factories.get(opts.factory, opts.factory) - try: - image_factory = get_factory(module) - except ValueError as e: - raise_error(str(e)) - else: - image_factory = None - - qr = qrcode.QRCode( - error_correction=error_correction[opts.error_correction], - image_factory=image_factory, - ) - - if args: - data = args[0] - data = data.encode(errors="surrogateescape") - else: - data = sys.stdin.buffer.read() - if opts.optimize is None: - qr.add_data(data) - else: - qr.add_data(data, optimize=opts.optimize) - - if opts.output: - img = qr.make_image() - with open(opts.output, "wb") as out: - img.save(out) - else: - if image_factory is None and (os.isatty(sys.stdout.fileno()) or opts.ascii): - qr.print_ascii(tty=not opts.ascii) - return - - kwargs = {} - aliases: Optional[DrawerAliases] = getattr( - qr.image_factory, "drawer_aliases", None - ) - if opts.factory_drawer: - if not aliases: - raise_error("The selected factory has no drawer aliases.") - if opts.factory_drawer not in aliases: - raise_error( - f"{opts.factory_drawer} factory drawer not found." - f" Expected {commas(aliases)}" - ) - drawer_cls, drawer_kwargs = aliases[opts.factory_drawer] - kwargs["module_drawer"] = drawer_cls(**drawer_kwargs) - img = qr.make_image(**kwargs) - - sys.stdout.flush() - img.save(sys.stdout.buffer) - - -def get_factory(module: str) -> type[BaseImage]: - if "." not in module: - raise ValueError("The image factory is not a full python path") - module, name = module.rsplit(".", 1) - imp = __import__(module, {}, {}, [name]) - return getattr(imp, name) - - -def get_drawer_help() -> str: - help: dict[str, set] = {} - for alias, module in default_factories.items(): - try: - image = get_factory(module) - except ImportError: # pragma: no cover - continue - aliases: Optional[DrawerAliases] = getattr(image, "drawer_aliases", None) - if not aliases: - continue - factories = help.setdefault(commas(aliases), set()) - factories.add(alias) - - return ". ".join( - f"For {commas(factories, 'and')}, use: {aliases}" - for aliases, factories in help.items() - ) - - -def commas(items: Iterable[str], joiner="or") -> str: - items = tuple(items) - if not items: - return "" - if len(items) == 1: - return items[0] - return f"{', '.join(items[:-1])} {joiner} {items[-1]}" - - -if __name__ == "__main__": # pragma: no cover - main() diff --git a/python_modules/qrcode/constants.py b/python_modules/qrcode/constants.py deleted file mode 100644 index 385dda088..000000000 --- a/python_modules/qrcode/constants.py +++ /dev/null @@ -1,5 +0,0 @@ -# QR error correct levels -ERROR_CORRECT_L = 1 -ERROR_CORRECT_M = 0 -ERROR_CORRECT_Q = 3 -ERROR_CORRECT_H = 2 diff --git a/python_modules/qrcode/exceptions.py b/python_modules/qrcode/exceptions.py deleted file mode 100644 index b37bd30c3..000000000 --- a/python_modules/qrcode/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class DataOverflowError(Exception): - pass diff --git a/python_modules/qrcode/image/__init__.py b/python_modules/qrcode/image/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_modules/qrcode/image/base.py b/python_modules/qrcode/image/base.py deleted file mode 100644 index 119c30a67..000000000 --- a/python_modules/qrcode/image/base.py +++ /dev/null @@ -1,164 +0,0 @@ -import abc -from typing import TYPE_CHECKING, Any, Optional, Union - -from qrcode.image.styles.moduledrawers.base import QRModuleDrawer - -if TYPE_CHECKING: - from qrcode.main import ActiveWithNeighbors, QRCode - - -DrawerAliases = dict[str, tuple[type[QRModuleDrawer], dict[str, Any]]] - - -class BaseImage: - """ - Base QRCode image output class. - """ - - kind: Optional[str] = None - allowed_kinds: Optional[tuple[str]] = None - needs_context = False - needs_processing = False - needs_drawrect = True - - def __init__(self, border, width, box_size, *args, **kwargs): - self.border = border - self.width = width - self.box_size = box_size - self.pixel_size = (self.width + self.border * 2) * self.box_size - self.modules = kwargs.pop("qrcode_modules") - self._img = self.new_image(**kwargs) - self.init_new_image() - - @abc.abstractmethod - def drawrect(self, row, col): - """ - Draw a single rectangle of the QR code. - """ - - def drawrect_context(self, row: int, col: int, qr: "QRCode"): - """ - Draw a single rectangle of the QR code given the surrounding context - """ - raise NotImplementedError("BaseImage.drawrect_context") # pragma: no cover - - def process(self): - """ - Processes QR code after completion - """ - raise NotImplementedError("BaseImage.drawimage") # pragma: no cover - - @abc.abstractmethod - def save(self, stream, kind=None): - """ - Save the image file. - """ - - def pixel_box(self, row, col): - """ - A helper method for pixel-based image generators that specifies the - four pixel coordinates for a single rect. - """ - x = (col + self.border) * self.box_size - y = (row + self.border) * self.box_size - return ( - (x, y), - (x + self.box_size - 1, y + self.box_size - 1), - ) - - @abc.abstractmethod - def new_image(self, **kwargs) -> Any: - """ - Build the image class. Subclasses should return the class created. - """ - - def init_new_image(self): - pass - - def get_image(self, **kwargs): - """ - Return the image class for further processing. - """ - return self._img - - def check_kind(self, kind, transform=None): - """ - Get the image type. - """ - if kind is None: - kind = self.kind - allowed = not self.allowed_kinds or kind in self.allowed_kinds - if transform: - kind = transform(kind) - if not allowed: - allowed = kind in self.allowed_kinds - if not allowed: - raise ValueError(f"Cannot set {type(self).__name__} type to {kind}") - return kind - - def is_eye(self, row: int, col: int): - """ - Find whether the referenced module is in an eye. - """ - return ( - (row < 7 and col < 7) - or (row < 7 and self.width - col < 8) - or (self.width - row < 8 and col < 7) - ) - - -class BaseImageWithDrawer(BaseImage): - default_drawer_class: type[QRModuleDrawer] - drawer_aliases: DrawerAliases = {} - - def get_default_module_drawer(self) -> QRModuleDrawer: - return self.default_drawer_class() - - def get_default_eye_drawer(self) -> QRModuleDrawer: - return self.default_drawer_class() - - needs_context = True - - module_drawer: "QRModuleDrawer" - eye_drawer: "QRModuleDrawer" - - def __init__( - self, - *args, - module_drawer: Union[QRModuleDrawer, str, None] = None, - eye_drawer: Union[QRModuleDrawer, str, None] = None, - **kwargs, - ): - self.module_drawer = ( - self.get_drawer(module_drawer) or self.get_default_module_drawer() - ) - # The eye drawer can be overridden by another module drawer as well, - # but you have to be more careful with these in order to make the QR - # code still parseable - self.eye_drawer = self.get_drawer(eye_drawer) or self.get_default_eye_drawer() - super().__init__(*args, **kwargs) - - def get_drawer( - self, drawer: Union[QRModuleDrawer, str, None] - ) -> Optional[QRModuleDrawer]: - if not isinstance(drawer, str): - return drawer - drawer_cls, kwargs = self.drawer_aliases[drawer] - return drawer_cls(**kwargs) - - def init_new_image(self): - self.module_drawer.initialize(img=self) - self.eye_drawer.initialize(img=self) - - return super().init_new_image() - - def drawrect_context(self, row: int, col: int, qr: "QRCode"): - box = self.pixel_box(row, col) - drawer = self.eye_drawer if self.is_eye(row, col) else self.module_drawer - is_active: Union[bool, ActiveWithNeighbors] = ( - qr.active_with_neighbors(row, col) - if drawer.needs_neighbors - else bool(qr.modules[row][col]) - ) - - drawer.drawrect(box, is_active) diff --git a/python_modules/qrcode/image/pil.py b/python_modules/qrcode/image/pil.py deleted file mode 100644 index 57ee13a8d..000000000 --- a/python_modules/qrcode/image/pil.py +++ /dev/null @@ -1,57 +0,0 @@ -import qrcode.image.base -from PIL import Image, ImageDraw - - -class PilImage(qrcode.image.base.BaseImage): - """ - PIL image builder, default format is PNG. - """ - - kind = "PNG" - - def new_image(self, **kwargs): - if not Image: - raise ImportError("PIL library not found.") - - back_color = kwargs.get("back_color", "white") - fill_color = kwargs.get("fill_color", "black") - - try: - fill_color = fill_color.lower() - except AttributeError: - pass - - try: - back_color = back_color.lower() - except AttributeError: - pass - - # L mode (1 mode) color = (r*299 + g*587 + b*114)//1000 - if fill_color == "black" and back_color == "white": - mode = "1" - fill_color = 0 - if back_color == "white": - back_color = 255 - elif back_color == "transparent": - mode = "RGBA" - back_color = None - else: - mode = "RGB" - - img = Image.new(mode, (self.pixel_size, self.pixel_size), back_color) - self.fill_color = fill_color - self._idr = ImageDraw.Draw(img) - return img - - def drawrect(self, row, col): - box = self.pixel_box(row, col) - self._idr.rectangle(box, fill=self.fill_color) - - def save(self, stream, format=None, **kwargs): - kind = kwargs.pop("kind", self.kind) - if format is None: - format = kind - self._img.save(stream, format=format, **kwargs) - - def __getattr__(self, name): - return getattr(self._img, name) diff --git a/python_modules/qrcode/image/pure.py b/python_modules/qrcode/image/pure.py deleted file mode 100644 index 5a8b2c5e2..000000000 --- a/python_modules/qrcode/image/pure.py +++ /dev/null @@ -1,56 +0,0 @@ -from itertools import chain - -from qrcode.compat.png import PngWriter -from qrcode.image.base import BaseImage - - -class PyPNGImage(BaseImage): - """ - pyPNG image builder. - """ - - kind = "PNG" - allowed_kinds = ("PNG",) - needs_drawrect = False - - def new_image(self, **kwargs): - if not PngWriter: - raise ImportError("PyPNG library not installed.") - - return PngWriter(self.pixel_size, self.pixel_size, greyscale=True, bitdepth=1) - - def drawrect(self, row, col): - """ - Not used. - """ - - def save(self, stream, kind=None): - if isinstance(stream, str): - stream = open(stream, "wb") - self._img.write(stream, self.rows_iter()) - - def rows_iter(self): - yield from self.border_rows_iter() - border_col = [1] * (self.box_size * self.border) - for module_row in self.modules: - row = ( - border_col - + list( - chain.from_iterable( - ([not point] * self.box_size) for point in module_row - ) - ) - + border_col - ) - for _ in range(self.box_size): - yield row - yield from self.border_rows_iter() - - def border_rows_iter(self): - border_row = [1] * (self.box_size * (self.width + self.border * 2)) - for _ in range(self.border * self.box_size): - yield border_row - - -# Keeping this for backwards compatibility. -PymagingImage = PyPNGImage diff --git a/python_modules/qrcode/image/styledpil.py b/python_modules/qrcode/image/styledpil.py deleted file mode 100644 index cd63a6e0c..000000000 --- a/python_modules/qrcode/image/styledpil.py +++ /dev/null @@ -1,120 +0,0 @@ -import qrcode.image.base -from PIL import Image -from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask -from qrcode.image.styles.moduledrawers import SquareModuleDrawer - - -class StyledPilImage(qrcode.image.base.BaseImageWithDrawer): - """ - Styled PIL image builder, default format is PNG. - - This differs from the PilImage in that there is a module_drawer, a - color_mask, and an optional image - - The module_drawer should extend the QRModuleDrawer class and implement the - drawrect_context(self, box, active, context), and probably also the - initialize function. This will draw an individual "module" or square on - the QR code. - - The color_mask will extend the QRColorMask class and will at very least - implement the get_fg_pixel(image, x, y) function, calculating a color to - put on the image at the pixel location (x,y) (more advanced functionality - can be gotten by instead overriding other functions defined in the - QRColorMask class) - - The Image can be specified either by path or with a Pillow Image, and if it - is there will be placed in the middle of the QR code. No effort is done to - ensure that the QR code is still legible after the image has been placed - there; Q or H level error correction levels are recommended to maintain - data integrity A resampling filter can be specified (defaulting to - PIL.Image.Resampling.LANCZOS) for resizing; see PIL.Image.resize() for possible - options for this parameter. - The image size can be controlled by `embedded_image_ratio` which is a ratio - between 0 and 1 that's set in relation to the overall width of the QR code. - """ - - kind = "PNG" - - needs_processing = True - color_mask: QRColorMask - default_drawer_class = SquareModuleDrawer - - def __init__(self, *args, **kwargs): - self.color_mask = kwargs.get("color_mask", SolidFillColorMask()) - # allow embeded_ parameters with typos for backwards compatibility - embedded_image_path = kwargs.get( - "embedded_image_path", kwargs.get("embeded_image_path", None) - ) - self.embedded_image = kwargs.get( - "embedded_image", kwargs.get("embeded_image", None) - ) - self.embedded_image_ratio = kwargs.get( - "embedded_image_ratio", kwargs.get("embeded_image_ratio", 0.25) - ) - self.embedded_image_resample = kwargs.get( - "embedded_image_resample", - kwargs.get("embeded_image_resample", Image.Resampling.LANCZOS), - ) - if not self.embedded_image and embedded_image_path: - self.embedded_image = Image.open(embedded_image_path) - - # the paint_color is the color the module drawer will use to draw upon - # a canvas During the color mask process, pixels that are paint_color - # are replaced by a newly-calculated color - self.paint_color = tuple(0 for i in self.color_mask.back_color) - if self.color_mask.has_transparency: - self.paint_color = tuple([*self.color_mask.back_color[:3], 255]) - - super().__init__(*args, **kwargs) - - def new_image(self, **kwargs): - mode = ( - "RGBA" - if ( - self.color_mask.has_transparency - or (self.embedded_image and "A" in self.embedded_image.getbands()) - ) - else "RGB" - ) - # This is the background color. Should be white or whiteish - back_color = self.color_mask.back_color - - return Image.new(mode, (self.pixel_size, self.pixel_size), back_color) - - def init_new_image(self): - self.color_mask.initialize(self, self._img) - super().init_new_image() - - def process(self): - self.color_mask.apply_mask(self._img) - if self.embedded_image: - self.draw_embedded_image() - - def draw_embedded_image(self): - if not self.embedded_image: - return - total_width, _ = self._img.size - total_width = int(total_width) - logo_width_ish = int(total_width * self.embedded_image_ratio) - logo_offset = ( - int((int(total_width / 2) - int(logo_width_ish / 2)) / self.box_size) - * self.box_size - ) # round the offset to the nearest module - logo_position = (logo_offset, logo_offset) - logo_width = total_width - logo_offset * 2 - region = self.embedded_image - region = region.resize((logo_width, logo_width), self.embedded_image_resample) - if "A" in region.getbands(): - self._img.alpha_composite(region, logo_position) - else: - self._img.paste(region, logo_position) - - def save(self, stream, format=None, **kwargs): - if format is None: - format = kwargs.get("kind", self.kind) - if "kind" in kwargs: - del kwargs["kind"] - self._img.save(stream, format=format, **kwargs) - - def __getattr__(self, name): - return getattr(self._img, name) diff --git a/python_modules/qrcode/image/styles/__init__.py b/python_modules/qrcode/image/styles/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_modules/qrcode/image/styles/colormasks.py b/python_modules/qrcode/image/styles/colormasks.py deleted file mode 100644 index 9599f7fb2..000000000 --- a/python_modules/qrcode/image/styles/colormasks.py +++ /dev/null @@ -1,226 +0,0 @@ -import math - -from PIL import Image - - -class QRColorMask: - """ - QRColorMask is used to color in the QRCode. - - By the time apply_mask is called, the QRModuleDrawer of the StyledPilImage - will have drawn all of the modules on the canvas (the color of these - modules will be mostly black, although antialiasing may result in - gradients) In the base class, apply_mask is implemented such that the - background color will remain, but the foreground pixels will be replaced by - a color determined by a call to get_fg_pixel. There is additional - calculation done to preserve the gradient artifacts of antialiasing. - - All QRColorMask objects should be careful about RGB vs RGBA color spaces. - - For examples of what these look like, see doc/color_masks.png - """ - - back_color = (255, 255, 255) - has_transparency = False - paint_color = back_color - - def initialize(self, styledPilImage, image): - self.paint_color = styledPilImage.paint_color - - def apply_mask(self, image, use_cache=False): - width, height = image.size - pixels = image.load() - fg_color_cache = {} if use_cache else None - for x in range(width): - for y in range(height): - current_color = pixels[x, y] - if current_color == self.back_color: - continue - if use_cache and current_color in fg_color_cache: - pixels[x, y] = fg_color_cache[current_color] - continue - norm = self.extrap_color( - self.back_color, self.paint_color, current_color - ) - if norm is not None: - new_color = self.interp_color( - self.get_bg_pixel(image, x, y), - self.get_fg_pixel(image, x, y), - norm, - ) - pixels[x, y] = new_color - - if use_cache: - fg_color_cache[current_color] = new_color - else: - pixels[x, y] = self.get_bg_pixel(image, x, y) - - def get_fg_pixel(self, image, x, y): - raise NotImplementedError("QRModuleDrawer.paint_fg_pixel") - - def get_bg_pixel(self, image, x, y): - return self.back_color - - # The following functions are helpful for color calculation: - - # interpolate a number between two numbers - def interp_num(self, n1, n2, norm): - return int(n2 * norm + n1 * (1 - norm)) - - # interpolate a color between two colorrs - def interp_color(self, col1, col2, norm): - return tuple(self.interp_num(col1[i], col2[i], norm) for i in range(len(col1))) - - # find the interpolation coefficient between two numbers - def extrap_num(self, n1, n2, interped_num): - if n2 == n1: - return None - else: - return (interped_num - n1) / (n2 - n1) - - # find the interpolation coefficient between two numbers - def extrap_color(self, col1, col2, interped_color): - normed = [] - for c1, c2, ci in zip(col1, col2, interped_color): - extrap = self.extrap_num(c1, c2, ci) - if extrap is not None: - normed.append(extrap) - if not normed: - return None - return sum(normed) / len(normed) - - -class SolidFillColorMask(QRColorMask): - """ - Just fills in the background with one color and the foreground with another - """ - - def __init__(self, back_color=(255, 255, 255), front_color=(0, 0, 0)): - self.back_color = back_color - self.front_color = front_color - self.has_transparency = len(self.back_color) == 4 - - def apply_mask(self, image): - if self.back_color == (255, 255, 255) and self.front_color == (0, 0, 0): - # Optimization: the image is already drawn by QRModuleDrawer in - # black and white, so if these are also our mask colors we don't - # need to do anything. This is much faster than actually applying a - # mask. - pass - else: - # TODO there's probably a way to use PIL.ImageMath instead of doing - # the individual pixel comparisons that the base class uses, which - # would be a lot faster. (In fact doing this would probably remove - # the need for the B&W optimization above.) - QRColorMask.apply_mask(self, image, use_cache=True) - - def get_fg_pixel(self, image, x, y): - return self.front_color - - -class RadialGradiantColorMask(QRColorMask): - """ - Fills in the foreground with a radial gradient from the center to the edge - """ - - def __init__( - self, back_color=(255, 255, 255), center_color=(0, 0, 0), edge_color=(0, 0, 255) - ): - self.back_color = back_color - self.center_color = center_color - self.edge_color = edge_color - self.has_transparency = len(self.back_color) == 4 - - def get_fg_pixel(self, image, x, y): - width, _ = image.size - normedDistanceToCenter = math.sqrt( - (x - width / 2) ** 2 + (y - width / 2) ** 2 - ) / (math.sqrt(2) * width / 2) - return self.interp_color( - self.center_color, self.edge_color, normedDistanceToCenter - ) - - -class SquareGradiantColorMask(QRColorMask): - """ - Fills in the foreground with a square gradient from the center to the edge - """ - - def __init__( - self, back_color=(255, 255, 255), center_color=(0, 0, 0), edge_color=(0, 0, 255) - ): - self.back_color = back_color - self.center_color = center_color - self.edge_color = edge_color - self.has_transparency = len(self.back_color) == 4 - - def get_fg_pixel(self, image, x, y): - width, _ = image.size - normedDistanceToCenter = max(abs(x - width / 2), abs(y - width / 2)) / ( - width / 2 - ) - return self.interp_color( - self.center_color, self.edge_color, normedDistanceToCenter - ) - - -class HorizontalGradiantColorMask(QRColorMask): - """ - Fills in the foreground with a gradient sweeping from the left to the right - """ - - def __init__( - self, back_color=(255, 255, 255), left_color=(0, 0, 0), right_color=(0, 0, 255) - ): - self.back_color = back_color - self.left_color = left_color - self.right_color = right_color - self.has_transparency = len(self.back_color) == 4 - - def get_fg_pixel(self, image, x, y): - width, _ = image.size - return self.interp_color(self.left_color, self.right_color, x / width) - - -class VerticalGradiantColorMask(QRColorMask): - """ - Fills in the forefround with a gradient sweeping from the top to the bottom - """ - - def __init__( - self, back_color=(255, 255, 255), top_color=(0, 0, 0), bottom_color=(0, 0, 255) - ): - self.back_color = back_color - self.top_color = top_color - self.bottom_color = bottom_color - self.has_transparency = len(self.back_color) == 4 - - def get_fg_pixel(self, image, x, y): - width, _ = image.size - return self.interp_color(self.top_color, self.bottom_color, y / width) - - -class ImageColorMask(QRColorMask): - """ - Fills in the foreground with pixels from another image, either passed by - path or passed by image object. - """ - - def __init__( - self, back_color=(255, 255, 255), color_mask_path=None, color_mask_image=None - ): - self.back_color = back_color - if color_mask_image: - self.color_img = color_mask_image - else: - self.color_img = Image.open(color_mask_path) - - self.has_transparency = len(self.back_color) == 4 - - def initialize(self, styledPilImage, image): - self.paint_color = styledPilImage.paint_color - self.color_img = self.color_img.resize(image.size) - - def get_fg_pixel(self, image, x, y): - width, _ = image.size - return self.color_img.getpixel((x, y)) diff --git a/python_modules/qrcode/image/styles/moduledrawers/__init__.py b/python_modules/qrcode/image/styles/moduledrawers/__init__.py deleted file mode 100644 index 99217d496..000000000 --- a/python_modules/qrcode/image/styles/moduledrawers/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# For backwards compatibility, importing the PIL drawers here. -try: - from .pil import CircleModuleDrawer # noqa: F401 - from .pil import GappedSquareModuleDrawer # noqa: F401 - from .pil import HorizontalBarsDrawer # noqa: F401 - from .pil import RoundedModuleDrawer # noqa: F401 - from .pil import SquareModuleDrawer # noqa: F401 - from .pil import VerticalBarsDrawer # noqa: F401 -except ImportError: - pass diff --git a/python_modules/qrcode/image/styles/moduledrawers/base.py b/python_modules/qrcode/image/styles/moduledrawers/base.py deleted file mode 100644 index 154d2cfa8..000000000 --- a/python_modules/qrcode/image/styles/moduledrawers/base.py +++ /dev/null @@ -1,33 +0,0 @@ -import abc -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from qrcode.image.base import BaseImage - - -class QRModuleDrawer(abc.ABC): - """ - QRModuleDrawer exists to draw the modules of the QR Code onto images. - - For this, technically all that is necessary is a ``drawrect(self, box, - is_active)`` function which takes in the box in which it is to draw, - whether or not the box is "active" (a module exists there). If - ``needs_neighbors`` is set to True, then the method should also accept a - ``neighbors`` kwarg (the neighboring pixels). - - It is frequently necessary to also implement an "initialize" function to - set up values that only the containing Image class knows about. - - For examples of what these look like, see doc/module_drawers.png - """ - - needs_neighbors = False - - def __init__(self, **kwargs): - pass - - def initialize(self, img: "BaseImage") -> None: - self.img = img - - @abc.abstractmethod - def drawrect(self, box, is_active) -> None: ... diff --git a/python_modules/qrcode/image/styles/moduledrawers/pil.py b/python_modules/qrcode/image/styles/moduledrawers/pil.py deleted file mode 100644 index 4aa424962..000000000 --- a/python_modules/qrcode/image/styles/moduledrawers/pil.py +++ /dev/null @@ -1,265 +0,0 @@ -from typing import TYPE_CHECKING - -from PIL import Image, ImageDraw -from qrcode.image.styles.moduledrawers.base import QRModuleDrawer - -if TYPE_CHECKING: - from qrcode.image.styledpil import StyledPilImage - from qrcode.main import ActiveWithNeighbors - -# When drawing antialiased things, make them bigger and then shrink them down -# to size after the geometry has been drawn. -ANTIALIASING_FACTOR = 4 - - -class StyledPilQRModuleDrawer(QRModuleDrawer): - """ - A base class for StyledPilImage module drawers. - - NOTE: the color that this draws in should be whatever is equivalent to - black in the color space, and the specified QRColorMask will handle adding - colors as necessary to the image - """ - - img: "StyledPilImage" - - -class SquareModuleDrawer(StyledPilQRModuleDrawer): - """ - Draws the modules as simple squares - """ - - def initialize(self, *args, **kwargs): - super().initialize(*args, **kwargs) - self.imgDraw = ImageDraw.Draw(self.img._img) - - def drawrect(self, box, is_active: bool): - if is_active: - self.imgDraw.rectangle(box, fill=self.img.paint_color) - - -class GappedSquareModuleDrawer(StyledPilQRModuleDrawer): - """ - Draws the modules as simple squares that are not contiguous. - - The size_ratio determines how wide the squares are relative to the width of - the space they are printed in - """ - - def __init__(self, size_ratio=0.8): - self.size_ratio = size_ratio - - def initialize(self, *args, **kwargs): - super().initialize(*args, **kwargs) - self.imgDraw = ImageDraw.Draw(self.img._img) - self.delta = (1 - self.size_ratio) * self.img.box_size / 2 - - def drawrect(self, box, is_active: bool): - if is_active: - smaller_box = ( - box[0][0] + self.delta, - box[0][1] + self.delta, - box[1][0] - self.delta, - box[1][1] - self.delta, - ) - self.imgDraw.rectangle(smaller_box, fill=self.img.paint_color) - - -class CircleModuleDrawer(StyledPilQRModuleDrawer): - """ - Draws the modules as circles - """ - - circle = None - - def initialize(self, *args, **kwargs): - super().initialize(*args, **kwargs) - box_size = self.img.box_size - fake_size = box_size * ANTIALIASING_FACTOR - self.circle = Image.new( - self.img.mode, - (fake_size, fake_size), - self.img.color_mask.back_color, - ) - ImageDraw.Draw(self.circle).ellipse( - (0, 0, fake_size, fake_size), fill=self.img.paint_color - ) - self.circle = self.circle.resize((box_size, box_size), Image.Resampling.LANCZOS) - - def drawrect(self, box, is_active: bool): - if is_active: - self.img._img.paste(self.circle, (box[0][0], box[0][1])) - - -class RoundedModuleDrawer(StyledPilQRModuleDrawer): - """ - Draws the modules with all 90 degree corners replaced with rounded edges. - - radius_ratio determines the radius of the rounded edges - a value of 1 - means that an isolated module will be drawn as a circle, while a value of 0 - means that the radius of the rounded edge will be 0 (and thus back to 90 - degrees again). - """ - - needs_neighbors = True - - def __init__(self, radius_ratio=1): - self.radius_ratio = radius_ratio - - def initialize(self, *args, **kwargs): - super().initialize(*args, **kwargs) - self.corner_width = int(self.img.box_size / 2) - self.setup_corners() - - def setup_corners(self): - mode = self.img.mode - back_color = self.img.color_mask.back_color - front_color = self.img.paint_color - self.SQUARE = Image.new( - mode, (self.corner_width, self.corner_width), front_color - ) - - fake_width = self.corner_width * ANTIALIASING_FACTOR - radius = self.radius_ratio * fake_width - diameter = radius * 2 - base = Image.new( - mode, (fake_width, fake_width), back_color - ) # make something 4x bigger for antialiasing - base_draw = ImageDraw.Draw(base) - base_draw.ellipse((0, 0, diameter, diameter), fill=front_color) - base_draw.rectangle((radius, 0, fake_width, fake_width), fill=front_color) - base_draw.rectangle((0, radius, fake_width, fake_width), fill=front_color) - self.NW_ROUND = base.resize( - (self.corner_width, self.corner_width), Image.Resampling.LANCZOS - ) - self.SW_ROUND = self.NW_ROUND.transpose(Image.Transpose.FLIP_TOP_BOTTOM) - self.SE_ROUND = self.NW_ROUND.transpose(Image.Transpose.ROTATE_180) - self.NE_ROUND = self.NW_ROUND.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - - def drawrect(self, box: list[list[int]], is_active: "ActiveWithNeighbors"): - if not is_active: - return - # find rounded edges - nw_rounded = not is_active.W and not is_active.N - ne_rounded = not is_active.N and not is_active.E - se_rounded = not is_active.E and not is_active.S - sw_rounded = not is_active.S and not is_active.W - - nw = self.NW_ROUND if nw_rounded else self.SQUARE - ne = self.NE_ROUND if ne_rounded else self.SQUARE - se = self.SE_ROUND if se_rounded else self.SQUARE - sw = self.SW_ROUND if sw_rounded else self.SQUARE - self.img._img.paste(nw, (box[0][0], box[0][1])) - self.img._img.paste(ne, (box[0][0] + self.corner_width, box[0][1])) - self.img._img.paste( - se, (box[0][0] + self.corner_width, box[0][1] + self.corner_width) - ) - self.img._img.paste(sw, (box[0][0], box[0][1] + self.corner_width)) - - -class VerticalBarsDrawer(StyledPilQRModuleDrawer): - """ - Draws vertically contiguous groups of modules as long rounded rectangles, - with gaps between neighboring bands (the size of these gaps is inversely - proportional to the horizontal_shrink). - """ - - needs_neighbors = True - - def __init__(self, horizontal_shrink=0.8): - self.horizontal_shrink = horizontal_shrink - - def initialize(self, *args, **kwargs): - super().initialize(*args, **kwargs) - self.half_height = int(self.img.box_size / 2) - self.delta = int((1 - self.horizontal_shrink) * self.half_height) - self.setup_edges() - - def setup_edges(self): - mode = self.img.mode - back_color = self.img.color_mask.back_color - front_color = self.img.paint_color - - height = self.half_height - width = height * 2 - shrunken_width = int(width * self.horizontal_shrink) - self.SQUARE = Image.new(mode, (shrunken_width, height), front_color) - - fake_width = width * ANTIALIASING_FACTOR - fake_height = height * ANTIALIASING_FACTOR - base = Image.new( - mode, (fake_width, fake_height), back_color - ) # make something 4x bigger for antialiasing - base_draw = ImageDraw.Draw(base) - base_draw.ellipse((0, 0, fake_width, fake_height * 2), fill=front_color) - - self.ROUND_TOP = base.resize((shrunken_width, height), Image.Resampling.LANCZOS) - self.ROUND_BOTTOM = self.ROUND_TOP.transpose(Image.Transpose.FLIP_TOP_BOTTOM) - - def drawrect(self, box, is_active: "ActiveWithNeighbors"): - if is_active: - # find rounded edges - top_rounded = not is_active.N - bottom_rounded = not is_active.S - - top = self.ROUND_TOP if top_rounded else self.SQUARE - bottom = self.ROUND_BOTTOM if bottom_rounded else self.SQUARE - self.img._img.paste(top, (box[0][0] + self.delta, box[0][1])) - self.img._img.paste( - bottom, (box[0][0] + self.delta, box[0][1] + self.half_height) - ) - - -class HorizontalBarsDrawer(StyledPilQRModuleDrawer): - """ - Draws horizontally contiguous groups of modules as long rounded rectangles, - with gaps between neighboring bands (the size of these gaps is inversely - proportional to the vertical_shrink). - """ - - needs_neighbors = True - - def __init__(self, vertical_shrink=0.8): - self.vertical_shrink = vertical_shrink - - def initialize(self, *args, **kwargs): - super().initialize(*args, **kwargs) - self.half_width = int(self.img.box_size / 2) - self.delta = int((1 - self.vertical_shrink) * self.half_width) - self.setup_edges() - - def setup_edges(self): - mode = self.img.mode - back_color = self.img.color_mask.back_color - front_color = self.img.paint_color - - width = self.half_width - height = width * 2 - shrunken_height = int(height * self.vertical_shrink) - self.SQUARE = Image.new(mode, (width, shrunken_height), front_color) - - fake_width = width * ANTIALIASING_FACTOR - fake_height = height * ANTIALIASING_FACTOR - base = Image.new( - mode, (fake_width, fake_height), back_color - ) # make something 4x bigger for antialiasing - base_draw = ImageDraw.Draw(base) - base_draw.ellipse((0, 0, fake_width * 2, fake_height), fill=front_color) - - self.ROUND_LEFT = base.resize( - (width, shrunken_height), Image.Resampling.LANCZOS - ) - self.ROUND_RIGHT = self.ROUND_LEFT.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - - def drawrect(self, box, is_active: "ActiveWithNeighbors"): - if is_active: - # find rounded edges - left_rounded = not is_active.W - right_rounded = not is_active.E - - left = self.ROUND_LEFT if left_rounded else self.SQUARE - right = self.ROUND_RIGHT if right_rounded else self.SQUARE - self.img._img.paste(left, (box[0][0], box[0][1] + self.delta)) - self.img._img.paste( - right, (box[0][0] + self.half_width, box[0][1] + self.delta) - ) diff --git a/python_modules/qrcode/image/styles/moduledrawers/svg.py b/python_modules/qrcode/image/styles/moduledrawers/svg.py deleted file mode 100644 index cf5b9e7d9..000000000 --- a/python_modules/qrcode/image/styles/moduledrawers/svg.py +++ /dev/null @@ -1,139 +0,0 @@ -import abc -from decimal import Decimal -from typing import TYPE_CHECKING, NamedTuple - -from qrcode.image.styles.moduledrawers.base import QRModuleDrawer -from qrcode.compat.etree import ET - -if TYPE_CHECKING: - from qrcode.image.svg import SvgFragmentImage, SvgPathImage - -ANTIALIASING_FACTOR = 4 - - -class Coords(NamedTuple): - x0: Decimal - y0: Decimal - x1: Decimal - y1: Decimal - xh: Decimal - yh: Decimal - - -class BaseSvgQRModuleDrawer(QRModuleDrawer): - img: "SvgFragmentImage" - - def __init__(self, *, size_ratio: Decimal = Decimal(1), **kwargs): - self.size_ratio = size_ratio - - def initialize(self, *args, **kwargs) -> None: - super().initialize(*args, **kwargs) - self.box_delta = (1 - self.size_ratio) * self.img.box_size / 2 - self.box_size = Decimal(self.img.box_size) * self.size_ratio - self.box_half = self.box_size / 2 - - def coords(self, box) -> Coords: - row, col = box[0] - x = row + self.box_delta - y = col + self.box_delta - - return Coords( - x, - y, - x + self.box_size, - y + self.box_size, - x + self.box_half, - y + self.box_half, - ) - - -class SvgQRModuleDrawer(BaseSvgQRModuleDrawer): - tag = "rect" - - def initialize(self, *args, **kwargs) -> None: - super().initialize(*args, **kwargs) - self.tag_qname = ET.QName(self.img._SVG_namespace, self.tag) - - def drawrect(self, box, is_active: bool): - if not is_active: - return - self.img._img.append(self.el(box)) - - @abc.abstractmethod - def el(self, box): ... - - -class SvgSquareDrawer(SvgQRModuleDrawer): - def initialize(self, *args, **kwargs) -> None: - super().initialize(*args, **kwargs) - self.unit_size = self.img.units(self.box_size) - - def el(self, box): - coords = self.coords(box) - return ET.Element( - self.tag_qname, # type: ignore - x=self.img.units(coords.x0), - y=self.img.units(coords.y0), - width=self.unit_size, - height=self.unit_size, - ) - - -class SvgCircleDrawer(SvgQRModuleDrawer): - tag = "circle" - - def initialize(self, *args, **kwargs) -> None: - super().initialize(*args, **kwargs) - self.radius = self.img.units(self.box_half) - - def el(self, box): - coords = self.coords(box) - return ET.Element( - self.tag_qname, # type: ignore - cx=self.img.units(coords.xh), - cy=self.img.units(coords.yh), - r=self.radius, - ) - - -class SvgPathQRModuleDrawer(BaseSvgQRModuleDrawer): - img: "SvgPathImage" - - def drawrect(self, box, is_active: bool): - if not is_active: - return - self.img._subpaths.append(self.subpath(box)) - - @abc.abstractmethod - def subpath(self, box) -> str: ... - - -class SvgPathSquareDrawer(SvgPathQRModuleDrawer): - def subpath(self, box) -> str: - coords = self.coords(box) - x0 = self.img.units(coords.x0, text=False) - y0 = self.img.units(coords.y0, text=False) - x1 = self.img.units(coords.x1, text=False) - y1 = self.img.units(coords.y1, text=False) - - return f"M{x0},{y0}H{x1}V{y1}H{x0}z" - - -class SvgPathCircleDrawer(SvgPathQRModuleDrawer): - def initialize(self, *args, **kwargs) -> None: - super().initialize(*args, **kwargs) - - def subpath(self, box) -> str: - coords = self.coords(box) - x0 = self.img.units(coords.x0, text=False) - yh = self.img.units(coords.yh, text=False) - h = self.img.units(self.box_half - self.box_delta, text=False) - x1 = self.img.units(coords.x1, text=False) - - # rx,ry is the centerpoint of the arc - # 1? is the x-axis-rotation - # 2? is the large-arc-flag - # 3? is the sweep flag - # x,y is the point the arc is drawn to - - return f"M{x0},{yh}A{h},{h} 0 0 0 {x1},{yh}A{h},{h} 0 0 0 {x0},{yh}z" diff --git a/python_modules/qrcode/image/svg.py b/python_modules/qrcode/image/svg.py deleted file mode 100644 index 4117559ab..000000000 --- a/python_modules/qrcode/image/svg.py +++ /dev/null @@ -1,175 +0,0 @@ -import decimal -from decimal import Decimal -from typing import Optional, Union, overload, Literal - -import qrcode.image.base -from qrcode.compat.etree import ET -from qrcode.image.styles.moduledrawers import svg as svg_drawers -from qrcode.image.styles.moduledrawers.base import QRModuleDrawer - - -class SvgFragmentImage(qrcode.image.base.BaseImageWithDrawer): - """ - SVG image builder - - Creates a QR-code image as a SVG document fragment. - """ - - _SVG_namespace = "http://www.w3.org/2000/svg" - kind = "SVG" - allowed_kinds = ("SVG",) - default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgSquareDrawer - - def __init__(self, *args, **kwargs): - ET.register_namespace("svg", self._SVG_namespace) - super().__init__(*args, **kwargs) - # Save the unit size, for example the default box_size of 10 is '1mm'. - self.unit_size = self.units(self.box_size) - - @overload - def units(self, pixels: Union[int, Decimal], text: Literal[False]) -> Decimal: ... - - @overload - def units(self, pixels: Union[int, Decimal], text: Literal[True] = True) -> str: ... - - def units(self, pixels, text=True): - """ - A box_size of 10 (default) equals 1mm. - """ - units = Decimal(pixels) / 10 - if not text: - return units - units = units.quantize(Decimal("0.001")) - context = decimal.Context(traps=[decimal.Inexact]) - try: - for d in (Decimal("0.01"), Decimal("0.1"), Decimal("0")): - units = units.quantize(d, context=context) - except decimal.Inexact: - pass - return f"{units}mm" - - def save(self, stream, kind=None): - self.check_kind(kind=kind) - self._write(stream) - - def to_string(self, **kwargs): - return ET.tostring(self._img, **kwargs) - - def new_image(self, **kwargs): - return self._svg(**kwargs) - - def _svg(self, tag=None, version="1.1", **kwargs): - if tag is None: - tag = ET.QName(self._SVG_namespace, "svg") - dimension = self.units(self.pixel_size) - return ET.Element( - tag, # type: ignore - width=dimension, - height=dimension, - version=version, - **kwargs, - ) - - def _write(self, stream): - ET.ElementTree(self._img).write(stream, xml_declaration=False) - - -class SvgImage(SvgFragmentImage): - """ - Standalone SVG image builder - - Creates a QR-code image as a standalone SVG document. - """ - - background: Optional[str] = None - drawer_aliases: qrcode.image.base.DrawerAliases = { - "circle": (svg_drawers.SvgCircleDrawer, {}), - "gapped-circle": (svg_drawers.SvgCircleDrawer, {"size_ratio": Decimal(0.8)}), - "gapped-square": (svg_drawers.SvgSquareDrawer, {"size_ratio": Decimal(0.8)}), - } - - def _svg(self, tag="svg", **kwargs): - svg = super()._svg(tag=tag, **kwargs) - svg.set("xmlns", self._SVG_namespace) - if self.background: - svg.append( - ET.Element( - "rect", - fill=self.background, - x="0", - y="0", - width="100%", - height="100%", - ) - ) - return svg - - def _write(self, stream): - ET.ElementTree(self._img).write(stream, encoding="UTF-8", xml_declaration=True) - - -class SvgPathImage(SvgImage): - """ - SVG image builder with one single element (removes white spaces - between individual QR points). - """ - - QR_PATH_STYLE = { - "fill": "#000000", - "fill-opacity": "1", - "fill-rule": "nonzero", - "stroke": "none", - } - - needs_processing = True - path: Optional[ET.Element] = None - default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgPathSquareDrawer - drawer_aliases = { - "circle": (svg_drawers.SvgPathCircleDrawer, {}), - "gapped-circle": ( - svg_drawers.SvgPathCircleDrawer, - {"size_ratio": Decimal(0.8)}, - ), - "gapped-square": ( - svg_drawers.SvgPathSquareDrawer, - {"size_ratio": Decimal(0.8)}, - ), - } - - def __init__(self, *args, **kwargs): - self._subpaths: list[str] = [] - super().__init__(*args, **kwargs) - - def _svg(self, viewBox=None, **kwargs): - if viewBox is None: - dimension = self.units(self.pixel_size, text=False) - viewBox = "0 0 {d} {d}".format(d=dimension) - return super()._svg(viewBox=viewBox, **kwargs) - - def process(self): - # Store the path just in case someone wants to use it again or in some - # unique way. - self.path = ET.Element( - ET.QName("path"), # type: ignore - d="".join(self._subpaths), - id="qr-path", - **self.QR_PATH_STYLE, - ) - self._subpaths = [] - self._img.append(self.path) - - -class SvgFillImage(SvgImage): - """ - An SvgImage that fills the background to white. - """ - - background = "white" - - -class SvgPathFillImage(SvgPathImage): - """ - An SvgPathImage that fills the background to white. - """ - - background = "white" diff --git a/python_modules/qrcode/main.py b/python_modules/qrcode/main.py deleted file mode 100644 index 152c97b0d..000000000 --- a/python_modules/qrcode/main.py +++ /dev/null @@ -1,541 +0,0 @@ -import sys -from bisect import bisect_left -from typing import ( - Generic, - NamedTuple, - Optional, - TypeVar, - cast, - overload, - Literal, -) - -from qrcode import constants, exceptions, util -from qrcode.image.base import BaseImage -from qrcode.image.pure import PyPNGImage - -ModulesType = list[list[Optional[bool]]] -# Cache modules generated just based on the QR Code version -precomputed_qr_blanks: dict[int, ModulesType] = {} - - -def make(data=None, **kwargs): - qr = QRCode(**kwargs) - qr.add_data(data) - return qr.make_image() - - -def _check_box_size(size): - if int(size) <= 0: - raise ValueError(f"Invalid box size (was {size}, expected larger than 0)") - - -def _check_border(size): - if int(size) < 0: - raise ValueError( - "Invalid border value (was %s, expected 0 or larger than that)" % size - ) - - -def _check_mask_pattern(mask_pattern): - if mask_pattern is None: - return - if not isinstance(mask_pattern, int): - raise TypeError( - f"Invalid mask pattern (was {type(mask_pattern)}, expected int)" - ) - if mask_pattern < 0 or mask_pattern > 7: - raise ValueError(f"Mask pattern should be in range(8) (got {mask_pattern})") - - -def copy_2d_array(x): - return [row[:] for row in x] - - -class ActiveWithNeighbors(NamedTuple): - NW: bool - N: bool - NE: bool - W: bool - me: bool - E: bool - SW: bool - S: bool - SE: bool - - def __bool__(self) -> bool: - return self.me - - -GenericImage = TypeVar("GenericImage", bound=BaseImage) -GenericImageLocal = TypeVar("GenericImageLocal", bound=BaseImage) - - -class QRCode(Generic[GenericImage]): - modules: ModulesType - _version: Optional[int] = None - - def __init__( - self, - version=None, - error_correction=constants.ERROR_CORRECT_M, - box_size=10, - border=4, - image_factory: Optional[type[GenericImage]] = None, - mask_pattern=None, - ): - _check_box_size(box_size) - _check_border(border) - self.version = version - self.error_correction = int(error_correction) - self.box_size = int(box_size) - # Spec says border should be at least four boxes wide, but allow for - # any (e.g. for producing printable QR codes). - self.border = int(border) - self.mask_pattern = mask_pattern - self.image_factory = image_factory - if image_factory is not None: - assert issubclass(image_factory, BaseImage) - self.clear() - - @property - def version(self) -> int: - if self._version is None: - self.best_fit() - return cast(int, self._version) - - @version.setter - def version(self, value) -> None: - if value is not None: - value = int(value) - util.check_version(value) - self._version = value - - @property - def mask_pattern(self): - return self._mask_pattern - - @mask_pattern.setter - def mask_pattern(self, pattern): - _check_mask_pattern(pattern) - self._mask_pattern = pattern - - def clear(self): - """ - Reset the internal data. - """ - self.modules = [[]] - self.modules_count = 0 - self.data_cache = None - self.data_list = [] - - def add_data(self, data, optimize=20): - """ - Add data to this QR Code. - - :param optimize: Data will be split into multiple chunks to optimize - the QR size by finding to more compressed modes of at least this - length. Set to ``0`` to avoid optimizing at all. - """ - if isinstance(data, util.QRData): - self.data_list.append(data) - elif optimize: - self.data_list.extend(util.optimal_data_chunks(data, minimum=optimize)) - else: - self.data_list.append(util.QRData(data)) - self.data_cache = None - - def make(self, fit=True): - """ - Compile the data into a QR Code array. - - :param fit: If ``True`` (or if a size has not been provided), find the - best fit for the data to avoid data overflow errors. - """ - if fit or (self.version is None): - self.best_fit(start=self.version) - if self.mask_pattern is None: - self.makeImpl(False, self.best_mask_pattern()) - else: - self.makeImpl(False, self.mask_pattern) - - def makeImpl(self, test, mask_pattern): - self.modules_count = self.version * 4 + 17 - - if self.version in precomputed_qr_blanks: - self.modules = copy_2d_array(precomputed_qr_blanks[self.version]) - else: - self.modules = [ - [None] * self.modules_count for i in range(self.modules_count) - ] - self.setup_position_probe_pattern(0, 0) - self.setup_position_probe_pattern(self.modules_count - 7, 0) - self.setup_position_probe_pattern(0, self.modules_count - 7) - self.setup_position_adjust_pattern() - self.setup_timing_pattern() - - precomputed_qr_blanks[self.version] = copy_2d_array(self.modules) - - self.setup_type_info(test, mask_pattern) - - if self.version >= 7: - self.setup_type_number(test) - - if self.data_cache is None: - self.data_cache = util.create_data( - self.version, self.error_correction, self.data_list - ) - self.map_data(self.data_cache, mask_pattern) - - def setup_position_probe_pattern(self, row, col): - for r in range(-1, 8): - if row + r <= -1 or self.modules_count <= row + r: - continue - - for c in range(-1, 8): - if col + c <= -1 or self.modules_count <= col + c: - continue - - if ( - (0 <= r <= 6 and c in {0, 6}) - or (0 <= c <= 6 and r in {0, 6}) - or (2 <= r <= 4 and 2 <= c <= 4) - ): - self.modules[row + r][col + c] = True - else: - self.modules[row + r][col + c] = False - - def best_fit(self, start=None): - """ - Find the minimum size required to fit in the data. - """ - if start is None: - start = 1 - util.check_version(start) - - # Corresponds to the code in util.create_data, except we don't yet know - # version, so optimistically assume start and check later - mode_sizes = util.mode_sizes_for_version(start) - buffer = util.BitBuffer() - for data in self.data_list: - buffer.put(data.mode, 4) - buffer.put(len(data), mode_sizes[data.mode]) - data.write(buffer) - - needed_bits = len(buffer) - self.version = bisect_left( - util.BIT_LIMIT_TABLE[self.error_correction], needed_bits, start - ) - if self.version == 41: - raise exceptions.DataOverflowError() - - # Now check whether we need more bits for the mode sizes, recursing if - # our guess was too low - if mode_sizes is not util.mode_sizes_for_version(self.version): - self.best_fit(start=self.version) - return self.version - - def best_mask_pattern(self): - """ - Find the most efficient mask pattern. - """ - min_lost_point = 0 - pattern = 0 - - for i in range(8): - self.makeImpl(True, i) - - lost_point = util.lost_point(self.modules) - - if i == 0 or min_lost_point > lost_point: - min_lost_point = lost_point - pattern = i - - return pattern - - def print_tty(self, out=None): - """ - Output the QR Code only using TTY colors. - - If the data has not been compiled yet, make it first. - """ - if out is None: - import sys - - out = sys.stdout - - if not out.isatty(): - raise OSError("Not a tty") - - if self.data_cache is None: - self.make() - - modcount = self.modules_count - out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n") - for r in range(modcount): - out.write("\x1b[1;47m \x1b[40m") - for c in range(modcount): - if self.modules[r][c]: - out.write(" ") - else: - out.write("\x1b[1;47m \x1b[40m") - out.write("\x1b[1;47m \x1b[0m\n") - out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n") - out.flush() - - def print_ascii(self, out=None, tty=False, invert=False): - """ - Output the QR Code using ASCII characters. - - :param tty: use fixed TTY color codes (forces invert=True) - :param invert: invert the ASCII characters (solid <-> transparent) - """ - if out is None: - out = sys.stdout - - if tty and not out.isatty(): - raise OSError("Not a tty") - - if self.data_cache is None: - self.make() - - modcount = self.modules_count - codes = [bytes((code,)).decode("cp437") for code in (255, 223, 220, 219)] - if tty: - invert = True - if invert: - codes.reverse() - - def get_module(x, y) -> int: - if invert and self.border and max(x, y) >= modcount + self.border: - return 1 - if min(x, y) < 0 or max(x, y) >= modcount: - return 0 - return cast(int, self.modules[x][y]) - - for r in range(-self.border, modcount + self.border, 2): - if tty: - if not invert or r < modcount + self.border - 1: - out.write("\x1b[48;5;232m") # Background black - out.write("\x1b[38;5;255m") # Foreground white - for c in range(-self.border, modcount + self.border): - pos = get_module(r, c) + (get_module(r + 1, c) << 1) - out.write(codes[pos]) - if tty: - out.write("\x1b[0m") - out.write("\n") - out.flush() - - @overload - def make_image( - self, image_factory: Literal[None] = None, **kwargs - ) -> GenericImage: ... - - @overload - def make_image( - self, image_factory: type[GenericImageLocal] = None, **kwargs - ) -> GenericImageLocal: ... - - def make_image(self, image_factory=None, **kwargs): - """ - Make an image from the QR Code data. - - If the data has not been compiled yet, make it first. - """ - # allow embeded_ parameters with typos for backwards compatibility - if ( - kwargs.get("embedded_image_path") - or kwargs.get("embedded_image") - or kwargs.get("embeded_image_path") - or kwargs.get("embeded_image") - ) and self.error_correction != constants.ERROR_CORRECT_H: - raise ValueError( - "Error correction level must be ERROR_CORRECT_H if an embedded image is provided" - ) - _check_box_size(self.box_size) - if self.data_cache is None: - self.make() - - if image_factory is not None: - assert issubclass(image_factory, BaseImage) - else: - image_factory = self.image_factory - if image_factory is None: - from qrcode.image.pil import Image, PilImage - - # Use PIL by default if available, otherwise use PyPNG. - image_factory = PilImage if Image else PyPNGImage - - im = image_factory( - self.border, - self.modules_count, - self.box_size, - qrcode_modules=self.modules, - **kwargs, - ) - - if im.needs_drawrect: - for r in range(self.modules_count): - for c in range(self.modules_count): - if im.needs_context: - im.drawrect_context(r, c, qr=self) - elif self.modules[r][c]: - im.drawrect(r, c) - if im.needs_processing: - im.process() - - return im - - # return true if and only if (row, col) is in the module - def is_constrained(self, row: int, col: int) -> bool: - return ( - row >= 0 - and row < len(self.modules) - and col >= 0 - and col < len(self.modules[row]) - ) - - def setup_timing_pattern(self): - for r in range(8, self.modules_count - 8): - if self.modules[r][6] is not None: - continue - self.modules[r][6] = r % 2 == 0 - - for c in range(8, self.modules_count - 8): - if self.modules[6][c] is not None: - continue - self.modules[6][c] = c % 2 == 0 - - def setup_position_adjust_pattern(self): - pos = util.pattern_position(self.version) - - for i in range(len(pos)): - row = pos[i] - - for j in range(len(pos)): - col = pos[j] - - if self.modules[row][col] is not None: - continue - - for r in range(-2, 3): - for c in range(-2, 3): - if ( - r == -2 - or r == 2 - or c == -2 - or c == 2 - or (r == 0 and c == 0) - ): - self.modules[row + r][col + c] = True - else: - self.modules[row + r][col + c] = False - - def setup_type_number(self, test): - bits = util.BCH_type_number(self.version) - - for i in range(18): - mod = not test and ((bits >> i) & 1) == 1 - self.modules[i // 3][i % 3 + self.modules_count - 8 - 3] = mod - - for i in range(18): - mod = not test and ((bits >> i) & 1) == 1 - self.modules[i % 3 + self.modules_count - 8 - 3][i // 3] = mod - - def setup_type_info(self, test, mask_pattern): - data = (self.error_correction << 3) | mask_pattern - bits = util.BCH_type_info(data) - - # vertical - for i in range(15): - mod = not test and ((bits >> i) & 1) == 1 - - if i < 6: - self.modules[i][8] = mod - elif i < 8: - self.modules[i + 1][8] = mod - else: - self.modules[self.modules_count - 15 + i][8] = mod - - # horizontal - for i in range(15): - mod = not test and ((bits >> i) & 1) == 1 - - if i < 8: - self.modules[8][self.modules_count - i - 1] = mod - elif i < 9: - self.modules[8][15 - i - 1 + 1] = mod - else: - self.modules[8][15 - i - 1] = mod - - # fixed module - self.modules[self.modules_count - 8][8] = not test - - def map_data(self, data, mask_pattern): - inc = -1 - row = self.modules_count - 1 - bitIndex = 7 - byteIndex = 0 - - mask_func = util.mask_func(mask_pattern) - - data_len = len(data) - - for col in range(self.modules_count - 1, 0, -2): - if col <= 6: - col -= 1 - - col_range = (col, col - 1) - - while True: - for c in col_range: - if self.modules[row][c] is None: - dark = False - - if byteIndex < data_len: - dark = ((data[byteIndex] >> bitIndex) & 1) == 1 - - if mask_func(row, c): - dark = not dark - - self.modules[row][c] = dark - bitIndex -= 1 - - if bitIndex == -1: - byteIndex += 1 - bitIndex = 7 - - row += inc - - if row < 0 or self.modules_count <= row: - row -= inc - inc = -inc - break - - def get_matrix(self): - """ - Return the QR Code as a multidimensional array, including the border. - - To return the array without a border, set ``self.border`` to 0 first. - """ - if self.data_cache is None: - self.make() - - if not self.border: - return self.modules - - width = len(self.modules) + self.border * 2 - code = [[False] * width] * self.border - x_border = [False] * self.border - for module in self.modules: - code.append(x_border + cast(list[bool], module) + x_border) - code += [[False] * width] * self.border - - return code - - def active_with_neighbors(self, row: int, col: int) -> ActiveWithNeighbors: - context: list[bool] = [] - for r in range(row - 1, row + 2): - for c in range(col - 1, col + 2): - context.append(self.is_constrained(r, c) and bool(self.modules[r][c])) - return ActiveWithNeighbors(*context) diff --git a/python_modules/qrcode/release.py b/python_modules/qrcode/release.py deleted file mode 100644 index 208ac1ee1..000000000 --- a/python_modules/qrcode/release.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -This file provides zest.releaser entrypoints using when releasing new -qrcode versions. -""" - -import os -import re -import datetime - - -def update_manpage(data): - """ - Update the version in the manpage document. - """ - if data["name"] != "qrcode": - return - - base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - filename = os.path.join(base_dir, "doc", "qr.1") - with open(filename) as f: - lines = f.readlines() - - changed = False - for i, line in enumerate(lines): - if not line.startswith(".TH "): - continue - parts = re.split(r'"([^"]*)"', line) - if len(parts) < 5: - continue - changed = parts[3] != data["new_version"] - if changed: - # Update version - parts[3] = data["new_version"] - # Update date - parts[1] = datetime.datetime.now().strftime("%-d %b %Y") - lines[i] = '"'.join(parts) - break - - if changed: - with open(filename, "w") as f: - for line in lines: - f.write(line) diff --git a/python_modules/qrcode/tests/__init__.py b/python_modules/qrcode/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_modules/qrcode/tests/consts.py b/python_modules/qrcode/tests/consts.py deleted file mode 100644 index b1240139b..000000000 --- a/python_modules/qrcode/tests/consts.py +++ /dev/null @@ -1,4 +0,0 @@ -UNICODE_TEXT = "\u03b1\u03b2\u03b3" -WHITE = (255, 255, 255) -BLACK = (0, 0, 0) -RED = (255, 0, 0) diff --git a/python_modules/qrcode/tests/test_example.py b/python_modules/qrcode/tests/test_example.py deleted file mode 100644 index 7190fe3e7..000000000 --- a/python_modules/qrcode/tests/test_example.py +++ /dev/null @@ -1,13 +0,0 @@ -from unittest import mock - -import pytest - -from qrcode import run_example - -pytest.importorskip("PIL", reason="Requires PIL") - - -@mock.patch("PIL.Image.Image.show") -def test_example(mock_show): - run_example() - mock_show.assert_called_with() diff --git a/python_modules/qrcode/tests/test_qrcode.py b/python_modules/qrcode/tests/test_qrcode.py deleted file mode 100644 index 652428480..000000000 --- a/python_modules/qrcode/tests/test_qrcode.py +++ /dev/null @@ -1,271 +0,0 @@ -import io -from unittest import mock - -import pytest - -import qrcode -import qrcode.util -from qrcode.exceptions import DataOverflowError -from qrcode.image.base import BaseImage -from qrcode.tests.consts import UNICODE_TEXT -from qrcode.util import MODE_8BIT_BYTE, MODE_ALPHA_NUM, MODE_NUMBER, QRData - - -def test_basic(): - qr = qrcode.QRCode(version=1) - qr.add_data("a") - qr.make(fit=False) - - -def test_large(): - qr = qrcode.QRCode(version=27) - qr.add_data("a") - qr.make(fit=False) - - -def test_invalid_version(): - with pytest.raises(ValueError): - qrcode.QRCode(version=42) - - -def test_invalid_border(): - with pytest.raises(ValueError): - qrcode.QRCode(border=-1) - - -def test_overflow(): - qr = qrcode.QRCode(version=1) - qr.add_data("abcdefghijklmno") - with pytest.raises(DataOverflowError): - qr.make(fit=False) - - -def test_add_qrdata(): - qr = qrcode.QRCode(version=1) - data = QRData("a") - qr.add_data(data) - qr.make(fit=False) - - -def test_fit(): - qr = qrcode.QRCode() - qr.add_data("a") - qr.make() - assert qr.version == 1 - qr.add_data("bcdefghijklmno") - qr.make() - assert qr.version == 2 - - -def test_mode_number(): - qr = qrcode.QRCode() - qr.add_data("1234567890123456789012345678901234", optimize=0) - qr.make() - assert qr.version == 1 - assert qr.data_list[0].mode == MODE_NUMBER - - -def test_mode_alpha(): - qr = qrcode.QRCode() - qr.add_data("ABCDEFGHIJ1234567890", optimize=0) - qr.make() - assert qr.version == 1 - assert qr.data_list[0].mode == MODE_ALPHA_NUM - - -def test_regression_mode_comma(): - qr = qrcode.QRCode() - qr.add_data(",", optimize=0) - qr.make() - assert qr.data_list[0].mode == MODE_8BIT_BYTE - - -def test_mode_8bit(): - qr = qrcode.QRCode() - qr.add_data("abcABC" + UNICODE_TEXT, optimize=0) - qr.make() - assert qr.version == 1 - assert qr.data_list[0].mode == MODE_8BIT_BYTE - - -def test_mode_8bit_newline(): - qr = qrcode.QRCode() - qr.add_data("ABCDEFGHIJ1234567890\n", optimize=0) - qr.make() - assert qr.data_list[0].mode == MODE_8BIT_BYTE - - -def test_make_image_with_wrong_pattern(): - with pytest.raises(TypeError): - qrcode.QRCode(mask_pattern="string pattern") - - with pytest.raises(ValueError): - qrcode.QRCode(mask_pattern=-1) - - with pytest.raises(ValueError): - qrcode.QRCode(mask_pattern=42) - - -def test_mask_pattern_setter(): - qr = qrcode.QRCode() - - with pytest.raises(TypeError): - qr.mask_pattern = "string pattern" - - with pytest.raises(ValueError): - qr.mask_pattern = -1 - - with pytest.raises(ValueError): - qr.mask_pattern = 8 - - -def test_qrcode_bad_factory(): - with pytest.raises(TypeError): - qrcode.QRCode(image_factory="not_BaseImage") # type: ignore - - with pytest.raises(AssertionError): - qrcode.QRCode(image_factory=dict) # type: ignore - - -def test_qrcode_factory(): - class MockFactory(BaseImage): - drawrect = mock.Mock() - new_image = mock.Mock() - - qr = qrcode.QRCode(image_factory=MockFactory) - qr.add_data(UNICODE_TEXT) - qr.make_image() - assert MockFactory.new_image.called - assert MockFactory.drawrect.called - - -def test_optimize(): - qr = qrcode.QRCode() - text = "A1abc12345def1HELLOa" - qr.add_data(text, optimize=4) - qr.make() - assert [d.mode for d in qr.data_list] == [ - MODE_8BIT_BYTE, - MODE_NUMBER, - MODE_8BIT_BYTE, - MODE_ALPHA_NUM, - MODE_8BIT_BYTE, - ] - assert qr.version == 2 - - -def test_optimize_short(): - qr = qrcode.QRCode() - text = "A1abc1234567def1HELLOa" - qr.add_data(text, optimize=7) - qr.make() - assert len(qr.data_list) == 3 - assert [d.mode for d in qr.data_list] == [ - MODE_8BIT_BYTE, - MODE_NUMBER, - MODE_8BIT_BYTE, - ] - assert qr.version == 2 - - -def test_optimize_longer_than_data(): - qr = qrcode.QRCode() - text = "ABCDEFGHIJK" - qr.add_data(text, optimize=12) - assert len(qr.data_list) == 1 - assert qr.data_list[0].mode == MODE_ALPHA_NUM - - -def test_optimize_size(): - text = "A1abc12345123451234512345def1HELLOHELLOHELLOHELLOa" * 5 - - qr = qrcode.QRCode() - qr.add_data(text) - qr.make() - assert qr.version == 10 - - qr = qrcode.QRCode() - qr.add_data(text, optimize=0) - qr.make() - assert qr.version == 11 - - -def test_qrdata_repr(): - data = b"hello" - data_obj = qrcode.util.QRData(data) - assert repr(data_obj) == repr(data) - - -def test_print_ascii_stdout(): - qr = qrcode.QRCode() - with mock.patch("sys.stdout") as fake_stdout: - fake_stdout.isatty.return_value = None - with pytest.raises(OSError): - qr.print_ascii(tty=True) - assert fake_stdout.isatty.called - - -def test_print_ascii(): - qr = qrcode.QRCode(border=0) - f = io.StringIO() - qr.print_ascii(out=f) - printed = f.getvalue() - f.close() - expected = "\u2588\u2580\u2580\u2580\u2580\u2580\u2588" - assert printed[: len(expected)] == expected - - f = io.StringIO() - f.isatty = lambda: True - qr.print_ascii(out=f, tty=True) - printed = f.getvalue() - f.close() - expected = "\x1b[48;5;232m\x1b[38;5;255m" + "\xa0\u2584\u2584\u2584\u2584\u2584\xa0" - assert printed[: len(expected)] == expected - - -def test_print_tty_stdout(): - qr = qrcode.QRCode() - with mock.patch("sys.stdout") as fake_stdout: - fake_stdout.isatty.return_value = None - pytest.raises(OSError, qr.print_tty) - assert fake_stdout.isatty.called - - -def test_print_tty(): - qr = qrcode.QRCode() - f = io.StringIO() - f.isatty = lambda: True - qr.print_tty(out=f) - printed = f.getvalue() - f.close() - BOLD_WHITE_BG = "\x1b[1;47m" - BLACK_BG = "\x1b[40m" - WHITE_BLOCK = BOLD_WHITE_BG + " " + BLACK_BG - EOL = "\x1b[0m\n" - expected = BOLD_WHITE_BG + " " * 23 + EOL + WHITE_BLOCK + " " * 7 + WHITE_BLOCK - assert printed[: len(expected)] == expected - - -def test_get_matrix(): - qr = qrcode.QRCode(border=0) - qr.add_data("1") - assert qr.get_matrix() == qr.modules - - -def test_get_matrix_border(): - qr = qrcode.QRCode(border=1) - qr.add_data("1") - matrix = [row[1:-1] for row in qr.get_matrix()[1:-1]] - assert matrix == qr.modules - - -def test_negative_size_at_construction(): - with pytest.raises(ValueError): - qrcode.QRCode(box_size=-1) - - -def test_negative_size_at_usage(): - qr = qrcode.QRCode() - qr.box_size = -1 - with pytest.raises(ValueError): - qr.make_image() diff --git a/python_modules/qrcode/tests/test_qrcode_pil.py b/python_modules/qrcode/tests/test_qrcode_pil.py deleted file mode 100644 index 95ef5af7b..000000000 --- a/python_modules/qrcode/tests/test_qrcode_pil.py +++ /dev/null @@ -1,157 +0,0 @@ -import io - -import pytest - -import qrcode -import qrcode.util -from qrcode.tests.consts import BLACK, RED, UNICODE_TEXT, WHITE - -Image = pytest.importorskip("PIL.Image", reason="PIL is not installed") - -if Image: - from qrcode.image.styledpil import StyledPilImage - from qrcode.image.styles import colormasks, moduledrawers - - -def test_render_pil(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image() - img.save(io.BytesIO()) - assert isinstance(img.get_image(), Image.Image) - - -@pytest.mark.parametrize("back_color", ["TransParent", "red", (255, 195, 235)]) -def test_render_pil_background(back_color): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(back_color="TransParent") - img.save(io.BytesIO()) - - -def test_render_pil_with_rgb_color_tuples(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(back_color=(255, 195, 235), fill_color=(55, 95, 35)) - img.save(io.BytesIO()) - - -def test_render_with_pattern(): - qr = qrcode.QRCode(mask_pattern=3) - qr.add_data(UNICODE_TEXT) - img = qr.make_image() - img.save(io.BytesIO()) - - -def test_render_styled_Image(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage) - img.save(io.BytesIO()) - - -def test_render_styled_with_embedded_image(): - embedded_img = Image.new("RGB", (10, 10), color="red") - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage, embedded_image=embedded_img) - img.save(io.BytesIO()) - - -def test_render_styled_with_embedded_image_path(tmp_path): - tmpfile = str(tmp_path / "test.png") - embedded_img = Image.new("RGB", (10, 10), color="red") - embedded_img.save(tmpfile) - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage, embedded_image_path=tmpfile) - img.save(io.BytesIO()) - - -@pytest.mark.parametrize( - "drawer", - [ - moduledrawers.CircleModuleDrawer, - moduledrawers.GappedSquareModuleDrawer, - moduledrawers.HorizontalBarsDrawer, - moduledrawers.RoundedModuleDrawer, - moduledrawers.SquareModuleDrawer, - moduledrawers.VerticalBarsDrawer, - ], -) -def test_render_styled_with_drawer(drawer): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=drawer(), - ) - img.save(io.BytesIO()) - - -@pytest.mark.parametrize( - "mask", - [ - colormasks.SolidFillColorMask(), - colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED), - colormasks.SolidFillColorMask(back_color=(255, 0, 255, 255), front_color=RED), - colormasks.RadialGradiantColorMask( - back_color=WHITE, center_color=BLACK, edge_color=RED - ), - colormasks.SquareGradiantColorMask( - back_color=WHITE, center_color=BLACK, edge_color=RED - ), - colormasks.HorizontalGradiantColorMask( - back_color=WHITE, left_color=RED, right_color=BLACK - ), - colormasks.VerticalGradiantColorMask( - back_color=WHITE, top_color=RED, bottom_color=BLACK - ), - colormasks.ImageColorMask( - back_color=WHITE, color_mask_image=Image.new("RGB", (10, 10), color="red") - ), - ], -) -def test_render_styled_with_mask(mask): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - -def test_embedded_image_and_error_correction(tmp_path): - "If an embedded image is specified, error correction must be the highest so the QR code is readable" - tmpfile = str(tmp_path / "test.png") - embedded_img = Image.new("RGB", (10, 10), color="red") - embedded_img.save(tmpfile) - - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - with pytest.raises(ValueError): - qr.make_image(embedded_image_path=tmpfile) - with pytest.raises(ValueError): - qr.make_image(embedded_image=embedded_img) - - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_M) - qr.add_data(UNICODE_TEXT) - with pytest.raises(ValueError): - qr.make_image(embedded_image_path=tmpfile) - with pytest.raises(ValueError): - qr.make_image(embedded_image=embedded_img) - - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_Q) - qr.add_data(UNICODE_TEXT) - with pytest.raises(ValueError): - qr.make_image(embedded_image_path=tmpfile) - with pytest.raises(ValueError): - qr.make_image(embedded_image=embedded_img) - - # The only accepted correction level when an embedded image is provided - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) - qr.add_data(UNICODE_TEXT) - qr.make_image(embedded_image_path=tmpfile) - qr.make_image(embedded_image=embedded_img) - - -def test_shortcut(): - qrcode.make("image") diff --git a/python_modules/qrcode/tests/test_qrcode_pypng.py b/python_modules/qrcode/tests/test_qrcode_pypng.py deleted file mode 100644 index c502a3b3c..000000000 --- a/python_modules/qrcode/tests/test_qrcode_pypng.py +++ /dev/null @@ -1,35 +0,0 @@ -import io -from unittest import mock - -import pytest - - -import qrcode -import qrcode.util -from qrcode.image.pure import PyPNGImage -from qrcode.tests.consts import UNICODE_TEXT - -png = pytest.importorskip("png", reason="png is not installed") - - -def test_render_pypng(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=PyPNGImage) - assert isinstance(img.get_image(), png.Writer) - - print(img.width, img.box_size, img.border) - img.save(io.BytesIO()) - - -def test_render_pypng_to_str(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=PyPNGImage) - assert isinstance(img.get_image(), png.Writer) - - mock_open = mock.mock_open() - with mock.patch("qrcode.image.pure.open", mock_open, create=True): - img.save("test_file.png") - mock_open.assert_called_once_with("test_file.png", "wb") - mock_open("test_file.png", "wb").write.assert_called() diff --git a/python_modules/qrcode/tests/test_qrcode_svg.py b/python_modules/qrcode/tests/test_qrcode_svg.py deleted file mode 100644 index 4774b2451..000000000 --- a/python_modules/qrcode/tests/test_qrcode_svg.py +++ /dev/null @@ -1,54 +0,0 @@ -import io - -import qrcode -from qrcode.image import svg -from qrcode.tests.consts import UNICODE_TEXT - - -class SvgImageWhite(svg.SvgImage): - background = "white" - - -def test_render_svg(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=svg.SvgImage) - img.save(io.BytesIO()) - - -def test_render_svg_path(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=svg.SvgPathImage) - img.save(io.BytesIO()) - - -def test_render_svg_fragment(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=svg.SvgFragmentImage) - img.save(io.BytesIO()) - - -def test_svg_string(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=svg.SvgFragmentImage) - file_like = io.BytesIO() - img.save(file_like) - file_like.seek(0) - assert file_like.read() in img.to_string() - - -def test_render_svg_with_background(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=SvgImageWhite) - img.save(io.BytesIO()) - - -def test_svg_circle_drawer(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer="circle") - img.save(io.BytesIO()) diff --git a/python_modules/qrcode/tests/test_release.py b/python_modules/qrcode/tests/test_release.py deleted file mode 100644 index d61454b6e..000000000 --- a/python_modules/qrcode/tests/test_release.py +++ /dev/null @@ -1,43 +0,0 @@ -import builtins -import datetime -import re -from unittest import mock - -from qrcode.release import update_manpage - -OPEN = f"{builtins.__name__}.open" -DATA = 'test\n.TH "date" "version" "description"\nthis' - - -@mock.patch(OPEN, new_callable=mock.mock_open, read_data=".TH invalid") -def test_invalid_data(mock_file): - update_manpage({"name": "qrcode", "new_version": "1.23"}) - mock_file.assert_called() - mock_file().write.assert_not_called() - - -@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) -def test_not_qrcode(mock_file): - update_manpage({"name": "not-qrcode"}) - mock_file.assert_not_called() - - -@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) -def test_no_change(mock_file): - update_manpage({"name": "qrcode", "new_version": "version"}) - mock_file.assert_called() - mock_file().write.assert_not_called() - - -@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) -def test_change(mock_file): - update_manpage({"name": "qrcode", "new_version": "3.11"}) - expected = re.split(r"([^\n]*(?:\n|$))", DATA)[1::2] - expected[1] = ( - expected[1] - .replace("version", "3.11") - .replace("date", datetime.datetime.now().strftime("%-d %b %Y")) - ) - mock_file().write.assert_has_calls( - [mock.call(line) for line in expected if line != ""], any_order=True - ) diff --git a/python_modules/qrcode/tests/test_script.py b/python_modules/qrcode/tests/test_script.py deleted file mode 100644 index d6338ded4..000000000 --- a/python_modules/qrcode/tests/test_script.py +++ /dev/null @@ -1,97 +0,0 @@ -import sys -from unittest import mock - -import pytest - -from qrcode.console_scripts import commas, main - - -def bad_read(): - raise UnicodeDecodeError("utf-8", b"0x80", 0, 1, "invalid start byte") - - -@mock.patch("os.isatty", lambda *args: True) -@mock.patch("qrcode.main.QRCode.print_ascii") -def test_isatty(mock_print_ascii): - main(["testtext"]) - mock_print_ascii.assert_called_with(tty=True) - - -@mock.patch("os.isatty", lambda *args: False) -def test_piped(): - pytest.importorskip("PIL", reason="Requires PIL") - main(["testtext"]) - - -@mock.patch("os.isatty", lambda *args: True) -def test_stdin(): - with mock.patch("qrcode.main.QRCode.print_ascii") as mock_print_ascii: - with mock.patch("sys.stdin") as mock_stdin: - mock_stdin.buffer.read.return_value = "testtext" - main([]) - assert mock_stdin.buffer.read.called - mock_print_ascii.assert_called_with(tty=True) - - -@mock.patch("os.isatty", lambda *args: True) -def test_stdin_py3_unicodedecodeerror(): - with mock.patch("qrcode.main.QRCode.print_ascii") as mock_print_ascii: - with mock.patch("sys.stdin") as mock_stdin: - mock_stdin.buffer.read.return_value = "testtext" - mock_stdin.read.side_effect = bad_read - # sys.stdin.read() will raise an error... - with pytest.raises(UnicodeDecodeError): - sys.stdin.read() - # ... but it won't be used now. - main([]) - mock_print_ascii.assert_called_with(tty=True) - - -def test_optimize(): - pytest.importorskip("PIL", reason="Requires PIL") - main("testtext --optimize 0".split()) - - -def test_factory(): - main(["testtext", "--factory", "svg"]) - - -def test_bad_factory(): - with pytest.raises(SystemExit): - main(["testtext", "--factory", "nope"]) - - -@mock.patch.object(sys, "argv", "qr testtext output".split()) -def test_sys_argv(): - pytest.importorskip("PIL", reason="Requires PIL") - main() - - -def test_output(tmp_path): - pytest.importorskip("PIL", reason="Requires PIL") - main(["testtext", "--output", str(tmp_path / "test.png")]) - - -def test_factory_drawer_none(capsys): - pytest.importorskip("PIL", reason="Requires PIL") - with pytest.raises(SystemExit): - main("testtext --factory pil --factory-drawer nope".split()) - assert "The selected factory has no drawer aliases" in capsys.readouterr()[1] - - -def test_factory_drawer_bad(capsys): - with pytest.raises(SystemExit): - main("testtext --factory svg --factory-drawer sobad".split()) - assert "sobad factory drawer not found" in capsys.readouterr()[1] - - -def test_factory_drawer(capsys): - main("testtext --factory svg --factory-drawer circle".split()) - - -def test_commas(): - assert commas([]) == "" - assert commas(["A"]) == "A" - assert commas("AB") == "A or B" - assert commas("ABC") == "A, B or C" - assert commas("ABC", joiner="and") == "A, B and C" diff --git a/python_modules/qrcode/tests/test_util.py b/python_modules/qrcode/tests/test_util.py deleted file mode 100644 index e57badbcc..000000000 --- a/python_modules/qrcode/tests/test_util.py +++ /dev/null @@ -1,11 +0,0 @@ -import pytest - -from qrcode import util - - -def test_check_wrong_version(): - with pytest.raises(ValueError): - util.check_version(0) - - with pytest.raises(ValueError): - util.check_version(41) diff --git a/python_modules/qrcode/util.py b/python_modules/qrcode/util.py deleted file mode 100644 index fe25548f9..000000000 --- a/python_modules/qrcode/util.py +++ /dev/null @@ -1,584 +0,0 @@ -import math -import re - -from qrcode import LUT, base, exceptions -from qrcode.base import RSBlock - -# QR encoding modes. -MODE_NUMBER = 1 << 0 -MODE_ALPHA_NUM = 1 << 1 -MODE_8BIT_BYTE = 1 << 2 -MODE_KANJI = 1 << 3 - -# Encoding mode sizes. -MODE_SIZE_SMALL = { - MODE_NUMBER: 10, - MODE_ALPHA_NUM: 9, - MODE_8BIT_BYTE: 8, - MODE_KANJI: 8, -} -MODE_SIZE_MEDIUM = { - MODE_NUMBER: 12, - MODE_ALPHA_NUM: 11, - MODE_8BIT_BYTE: 16, - MODE_KANJI: 10, -} -MODE_SIZE_LARGE = { - MODE_NUMBER: 14, - MODE_ALPHA_NUM: 13, - MODE_8BIT_BYTE: 16, - MODE_KANJI: 12, -} - -ALPHA_NUM = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" -RE_ALPHA_NUM = re.compile(b"^[" + re.escape(ALPHA_NUM) + rb"]*\Z") - -# The number of bits for numeric delimited data lengths. -NUMBER_LENGTH = {3: 10, 2: 7, 1: 4} - -PATTERN_POSITION_TABLE = [ - [], - [6, 18], - [6, 22], - [6, 26], - [6, 30], - [6, 34], - [6, 22, 38], - [6, 24, 42], - [6, 26, 46], - [6, 28, 50], - [6, 30, 54], - [6, 32, 58], - [6, 34, 62], - [6, 26, 46, 66], - [6, 26, 48, 70], - [6, 26, 50, 74], - [6, 30, 54, 78], - [6, 30, 56, 82], - [6, 30, 58, 86], - [6, 34, 62, 90], - [6, 28, 50, 72, 94], - [6, 26, 50, 74, 98], - [6, 30, 54, 78, 102], - [6, 28, 54, 80, 106], - [6, 32, 58, 84, 110], - [6, 30, 58, 86, 114], - [6, 34, 62, 90, 118], - [6, 26, 50, 74, 98, 122], - [6, 30, 54, 78, 102, 126], - [6, 26, 52, 78, 104, 130], - [6, 30, 56, 82, 108, 134], - [6, 34, 60, 86, 112, 138], - [6, 30, 58, 86, 114, 142], - [6, 34, 62, 90, 118, 146], - [6, 30, 54, 78, 102, 126, 150], - [6, 24, 50, 76, 102, 128, 154], - [6, 28, 54, 80, 106, 132, 158], - [6, 32, 58, 84, 110, 136, 162], - [6, 26, 54, 82, 110, 138, 166], - [6, 30, 58, 86, 114, 142, 170], -] - -G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0) -G18 = ( - (1 << 12) - | (1 << 11) - | (1 << 10) - | (1 << 9) - | (1 << 8) - | (1 << 5) - | (1 << 2) - | (1 << 0) -) -G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1) - -PAD0 = 0xEC -PAD1 = 0x11 - - -# Precompute bit count limits, indexed by error correction level and code size -def _data_count(block): - return block.data_count - - -BIT_LIMIT_TABLE = [ - [0] - + [ - 8 * sum(map(_data_count, base.rs_blocks(version, error_correction))) - for version in range(1, 41) - ] - for error_correction in range(4) -] - - -def BCH_type_info(data): - d = data << 10 - while BCH_digit(d) - BCH_digit(G15) >= 0: - d ^= G15 << (BCH_digit(d) - BCH_digit(G15)) - - return ((data << 10) | d) ^ G15_MASK - - -def BCH_type_number(data): - d = data << 12 - while BCH_digit(d) - BCH_digit(G18) >= 0: - d ^= G18 << (BCH_digit(d) - BCH_digit(G18)) - return (data << 12) | d - - -def BCH_digit(data): - digit = 0 - while data != 0: - digit += 1 - data >>= 1 - return digit - - -def pattern_position(version): - return PATTERN_POSITION_TABLE[version - 1] - - -def mask_func(pattern): - """ - Return the mask function for the given mask pattern. - """ - if pattern == 0: # 000 - return lambda i, j: (i + j) % 2 == 0 - if pattern == 1: # 001 - return lambda i, j: i % 2 == 0 - if pattern == 2: # 010 - return lambda i, j: j % 3 == 0 - if pattern == 3: # 011 - return lambda i, j: (i + j) % 3 == 0 - if pattern == 4: # 100 - return lambda i, j: (math.floor(i / 2) + math.floor(j / 3)) % 2 == 0 - if pattern == 5: # 101 - return lambda i, j: (i * j) % 2 + (i * j) % 3 == 0 - if pattern == 6: # 110 - return lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0 - if pattern == 7: # 111 - return lambda i, j: ((i * j) % 3 + (i + j) % 2) % 2 == 0 - raise TypeError("Bad mask pattern: " + pattern) # pragma: no cover - - -def mode_sizes_for_version(version): - if version < 10: - return MODE_SIZE_SMALL - elif version < 27: - return MODE_SIZE_MEDIUM - else: - return MODE_SIZE_LARGE - - -def length_in_bits(mode, version): - if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE, MODE_KANJI): - raise TypeError(f"Invalid mode ({mode})") # pragma: no cover - - check_version(version) - - return mode_sizes_for_version(version)[mode] - - -def check_version(version): - if version < 1 or version > 40: - raise ValueError(f"Invalid version (was {version}, expected 1 to 40)") - - -def lost_point(modules): - modules_count = len(modules) - - lost_point = 0 - - lost_point = _lost_point_level1(modules, modules_count) - lost_point += _lost_point_level2(modules, modules_count) - lost_point += _lost_point_level3(modules, modules_count) - lost_point += _lost_point_level4(modules, modules_count) - - return lost_point - - -def _lost_point_level1(modules, modules_count): - lost_point = 0 - - modules_range = range(modules_count) - container = [0] * (modules_count + 1) - - for row in modules_range: - this_row = modules[row] - previous_color = this_row[0] - length = 0 - for col in modules_range: - if this_row[col] == previous_color: - length += 1 - else: - if length >= 5: - container[length] += 1 - length = 1 - previous_color = this_row[col] - if length >= 5: - container[length] += 1 - - for col in modules_range: - previous_color = modules[0][col] - length = 0 - for row in modules_range: - if modules[row][col] == previous_color: - length += 1 - else: - if length >= 5: - container[length] += 1 - length = 1 - previous_color = modules[row][col] - if length >= 5: - container[length] += 1 - - lost_point += sum( - container[each_length] * (each_length - 2) - for each_length in range(5, modules_count + 1) - ) - - return lost_point - - -def _lost_point_level2(modules, modules_count): - lost_point = 0 - - modules_range = range(modules_count - 1) - for row in modules_range: - this_row = modules[row] - next_row = modules[row + 1] - # use iter() and next() to skip next four-block. e.g. - # d a f if top-right a != b bottom-right, - # c b e then both abcd and abef won't lost any point. - modules_range_iter = iter(modules_range) - for col in modules_range_iter: - top_right = this_row[col + 1] - if top_right != next_row[col + 1]: - # reduce 33.3% of runtime via next(). - # None: raise nothing if there is no next item. - next(modules_range_iter, None) - elif top_right != this_row[col]: - continue - elif top_right != next_row[col]: - continue - else: - lost_point += 3 - - return lost_point - - -def _lost_point_level3(modules, modules_count): - # 1 : 1 : 3 : 1 : 1 ratio (dark:light:dark:light:dark) pattern in - # row/column, preceded or followed by light area 4 modules wide. From ISOIEC. - # pattern1: 10111010000 - # pattern2: 00001011101 - modules_range = range(modules_count) - modules_range_short = range(modules_count - 10) - lost_point = 0 - - for row in modules_range: - this_row = modules[row] - modules_range_short_iter = iter(modules_range_short) - col = 0 - for col in modules_range_short_iter: - if ( - not this_row[col + 1] - and this_row[col + 4] - and not this_row[col + 5] - and this_row[col + 6] - and not this_row[col + 9] - and ( - this_row[col + 0] - and this_row[col + 2] - and this_row[col + 3] - and not this_row[col + 7] - and not this_row[col + 8] - and not this_row[col + 10] - or not this_row[col + 0] - and not this_row[col + 2] - and not this_row[col + 3] - and this_row[col + 7] - and this_row[col + 8] - and this_row[col + 10] - ) - ): - lost_point += 40 - # horspool algorithm. - # if this_row[col + 10]: - # pattern1 shift 4, pattern2 shift 2. So min=2. - # else: - # pattern1 shift 1, pattern2 shift 1. So min=1. - if this_row[col + 10]: - next(modules_range_short_iter, None) - - for col in modules_range: - modules_range_short_iter = iter(modules_range_short) - row = 0 - for row in modules_range_short_iter: - if ( - not modules[row + 1][col] - and modules[row + 4][col] - and not modules[row + 5][col] - and modules[row + 6][col] - and not modules[row + 9][col] - and ( - modules[row + 0][col] - and modules[row + 2][col] - and modules[row + 3][col] - and not modules[row + 7][col] - and not modules[row + 8][col] - and not modules[row + 10][col] - or not modules[row + 0][col] - and not modules[row + 2][col] - and not modules[row + 3][col] - and modules[row + 7][col] - and modules[row + 8][col] - and modules[row + 10][col] - ) - ): - lost_point += 40 - if modules[row + 10][col]: - next(modules_range_short_iter, None) - - return lost_point - - -def _lost_point_level4(modules, modules_count): - dark_count = sum(map(sum, modules)) - percent = float(dark_count) / (modules_count**2) - # Every 5% departure from 50%, rating++ - rating = int(abs(percent * 100 - 50) / 5) - return rating * 10 - - -def optimal_data_chunks(data, minimum=4): - """ - An iterator returning QRData chunks optimized to the data content. - - :param minimum: The minimum number of bytes in a row to split as a chunk. - """ - data = to_bytestring(data) - num_pattern = rb"\d" - alpha_pattern = b"[" + re.escape(ALPHA_NUM) + b"]" - if len(data) <= minimum: - num_pattern = re.compile(b"^" + num_pattern + b"+$") - alpha_pattern = re.compile(b"^" + alpha_pattern + b"+$") - else: - re_repeat = b"{" + str(minimum).encode("ascii") + b",}" - num_pattern = re.compile(num_pattern + re_repeat) - alpha_pattern = re.compile(alpha_pattern + re_repeat) - num_bits = _optimal_split(data, num_pattern) - for is_num, chunk in num_bits: - if is_num: - yield QRData(chunk, mode=MODE_NUMBER, check_data=False) - else: - for is_alpha, sub_chunk in _optimal_split(chunk, alpha_pattern): - mode = MODE_ALPHA_NUM if is_alpha else MODE_8BIT_BYTE - yield QRData(sub_chunk, mode=mode, check_data=False) - - -def _optimal_split(data, pattern): - while data: - match = re.search(pattern, data) - if not match: - break - start, end = match.start(), match.end() - if start: - yield False, data[:start] - yield True, data[start:end] - data = data[end:] - if data: - yield False, data - - -def to_bytestring(data): - """ - Convert data to a (utf-8 encoded) byte-string if it isn't a byte-string - already. - """ - if not isinstance(data, bytes): - data = str(data).encode("utf-8") - return data - - -def optimal_mode(data): - """ - Calculate the optimal mode for this chunk of data. - """ - if data.isdigit(): - return MODE_NUMBER - if RE_ALPHA_NUM.match(data): - return MODE_ALPHA_NUM - return MODE_8BIT_BYTE - - -class QRData: - """ - Data held in a QR compatible format. - - Doesn't currently handle KANJI. - """ - - def __init__(self, data, mode=None, check_data=True): - """ - If ``mode`` isn't provided, the most compact QR data type possible is - chosen. - """ - if check_data: - data = to_bytestring(data) - - if mode is None: - self.mode = optimal_mode(data) - else: - self.mode = mode - if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE): - raise TypeError(f"Invalid mode ({mode})") # pragma: no cover - if check_data and mode < optimal_mode(data): # pragma: no cover - raise ValueError(f"Provided data can not be represented in mode {mode}") - - self.data = data - - def __len__(self): - return len(self.data) - - def write(self, buffer): - if self.mode == MODE_NUMBER: - for i in range(0, len(self.data), 3): - chars = self.data[i : i + 3] - bit_length = NUMBER_LENGTH[len(chars)] - buffer.put(int(chars), bit_length) - elif self.mode == MODE_ALPHA_NUM: - for i in range(0, len(self.data), 2): - chars = self.data[i : i + 2] - if len(chars) > 1: - buffer.put( - ALPHA_NUM.find(chars[0]) * 45 + ALPHA_NUM.find(chars[1]), 11 - ) - else: - buffer.put(ALPHA_NUM.find(chars), 6) - else: - # Iterating a bytestring in Python 3 returns an integer, - # no need to ord(). - data = self.data - for c in data: - buffer.put(c, 8) - - def __repr__(self): - return repr(self.data) - - -class BitBuffer: - def __init__(self): - self.buffer: list[int] = [] - self.length = 0 - - def __repr__(self): - return ".".join([str(n) for n in self.buffer]) - - def get(self, index): - buf_index = math.floor(index / 8) - return ((self.buffer[buf_index] >> (7 - index % 8)) & 1) == 1 - - def put(self, num, length): - for i in range(length): - self.put_bit(((num >> (length - i - 1)) & 1) == 1) - - def __len__(self): - return self.length - - def put_bit(self, bit): - buf_index = self.length // 8 - if len(self.buffer) <= buf_index: - self.buffer.append(0) - if bit: - self.buffer[buf_index] |= 0x80 >> (self.length % 8) - self.length += 1 - - -def create_bytes(buffer: BitBuffer, rs_blocks: list[RSBlock]): - offset = 0 - - maxDcCount = 0 - maxEcCount = 0 - - dcdata: list[list[int]] = [] - ecdata: list[list[int]] = [] - - for rs_block in rs_blocks: - dcCount = rs_block.data_count - ecCount = rs_block.total_count - dcCount - - maxDcCount = max(maxDcCount, dcCount) - maxEcCount = max(maxEcCount, ecCount) - - current_dc = [0xFF & buffer.buffer[i + offset] for i in range(dcCount)] - offset += dcCount - - # Get error correction polynomial. - if ecCount in LUT.rsPoly_LUT: - rsPoly = base.Polynomial(LUT.rsPoly_LUT[ecCount], 0) - else: - rsPoly = base.Polynomial([1], 0) - for i in range(ecCount): - rsPoly = rsPoly * base.Polynomial([1, base.gexp(i)], 0) - - rawPoly = base.Polynomial(current_dc, len(rsPoly) - 1) - - modPoly = rawPoly % rsPoly - current_ec = [] - mod_offset = len(modPoly) - ecCount - for i in range(ecCount): - modIndex = i + mod_offset - current_ec.append(modPoly[modIndex] if (modIndex >= 0) else 0) - - dcdata.append(current_dc) - ecdata.append(current_ec) - - data = [] - for i in range(maxDcCount): - for dc in dcdata: - if i < len(dc): - data.append(dc[i]) - for i in range(maxEcCount): - for ec in ecdata: - if i < len(ec): - data.append(ec[i]) - - return data - - -def create_data(version, error_correction, data_list): - buffer = BitBuffer() - for data in data_list: - buffer.put(data.mode, 4) - buffer.put(len(data), length_in_bits(data.mode, version)) - data.write(buffer) - - # Calculate the maximum number of bits for the given version. - rs_blocks = base.rs_blocks(version, error_correction) - bit_limit = sum(block.data_count * 8 for block in rs_blocks) - if len(buffer) > bit_limit: - raise exceptions.DataOverflowError( - "Code length overflow. Data size (%s) > size available (%s)" - % (len(buffer), bit_limit) - ) - - # Terminate the bits (add up to four 0s). - for _ in range(min(bit_limit - len(buffer), 4)): - buffer.put_bit(False) - - # Delimit the string into 8-bit words, padding with 0s if necessary. - delimit = len(buffer) % 8 - if delimit: - for _ in range(8 - delimit): - buffer.put_bit(False) - - # Add special alternating padding bitstrings until buffer is full. - bytes_to_fill = (bit_limit - len(buffer)) // 8 - for i in range(bytes_to_fill): - if i % 2 == 0: - buffer.put(PAD0, 8) - else: - buffer.put(PAD1, 8) - - return create_bytes(buffer, rs_blocks) diff --git a/python_modules/workers/__init__.py b/python_modules/workers/__init__.py deleted file mode 100644 index 765cf2bba..000000000 --- a/python_modules/workers/__init__.py +++ /dev/null @@ -1,66 +0,0 @@ -from ._workers import ( - Blob, - BlobEnding, - BlobValue, - Body, - Context, - DurableObject, - FetchKwargs, - FetchResponse, - File, - FormData, - FormDataValue, - Headers, - JSBody, - Request, - RequestInitCfProperties, - Response, - WorkerEntrypoint, - WorkflowEntrypoint, - fetch, - handler, - import_from_javascript, - patch_env, - python_from_rpc, - python_to_rpc, -) - -__all__ = [ - "Blob", - "BlobEnding", - "BlobValue", - "Body", - "Context", - "DurableObject", - "FetchKwargs", - "FetchResponse", - "File", - "FormData", - "FormDataValue", - "Headers", - "JSBody", - "Request", - "RequestInitCfProperties", - "Response", - "WorkerEntrypoint", - "WorkflowEntrypoint", - "env", - "fetch", - "handler", - "import_from_javascript", - "patch_env", - "python_from_rpc", - "python_to_rpc", - "waitUntil", - "wait_until", -] - - -def __getattr__(key): - if key == "env": - cloudflare_workers = import_from_javascript("cloudflare:workers") - return cloudflare_workers.env - if key in ("wait_until", "waitUntil"): - cloudflare_workers = import_from_javascript("cloudflare:workers") - return cloudflare_workers.waitUntil - raise AttributeError(f"module {__name__!r} has no attribute {key!r}") diff --git a/python_modules/workers/_workers.py b/python_modules/workers/_workers.py deleted file mode 100644 index 95f47781b..000000000 --- a/python_modules/workers/_workers.py +++ /dev/null @@ -1,1454 +0,0 @@ -# This module defines a Workers API for Python. It is similar to the API provided by -# JS Workers, but with changes and additions to be more idiomatic to the Python -# programming language. -import datetime -import functools -import inspect -import json -from asyncio import create_task, gather -from collections.abc import ( - Awaitable, - Generator, - Iterable, - Iterator, - MutableMapping, - Sequence, -) -from contextlib import ExitStack, contextmanager -from enum import StrEnum -from http import HTTPMethod, HTTPStatus -from types import LambdaType -from typing import TYPE_CHECKING, Any, Never, Protocol, TypedDict, Unpack - -import _cloudflare_compat_flags - -# Get globals modules and import function from the entrypoint-helper -import _pyodide_entrypoint_helper -import js -import pyodide.http -from js import Object -from pyodide import __version__ as pyodide_version -from pyodide.ffi import ( - JsBuffer, - JsException, - JsProxy, - create_once_callable, - create_proxy, - destroy_proxies, - to_js, -) -from pyodide.http import pyfetch - -from workers.workflows import NonRetryableError - -if TYPE_CHECKING: - from js import DurableObjectState, Env, ExecutionContext - - -class Context(Protocol): - def waitUntil(self, other: Awaitable[Any]) -> None: ... - - -try: - from pyodide.ffi import jsnull -except ImportError: - jsnull = None - - -def _jsnull_to_none(x): - if x is jsnull: - return None - return x - - -def import_from_javascript(module_name: str) -> Any: - """ - Import a JavaScript ES module from Python. - - Args: - module_name: The name of the module to import. This can be a module name or a path. - - Returns: - The imported module object. - - Example: - cloudflare_workers = import_from_javascript("cloudflare:workers") - env = cloudflare_workers.env - - Note: - Behind the scenes import_from_javascript uses JSPI to do imports but that means we need an - async context. To enable importing cloudflare:workers and cloudflare:sockets in the global - scope we specifically imported them in the global scope and exposed them here. - """ - # Special case for global scope available modules - # JSPI won't work in the global scope in 0.26.0a2 so we need modules importable in the global - # scope to be imported beforehand. - if module_name == "cloudflare:workers": - return _pyodide_entrypoint_helper.cloudflareWorkersModule - elif module_name == "cloudflare:sockets": - return _pyodide_entrypoint_helper.cloudflareSocketsModule - - try: - from pyodide.ffi import run_sync - - # Call the JavaScript import function - return run_sync(_pyodide_entrypoint_helper.doAnImport(module_name)) - except JsException as e: - raise ImportError(f"Failed to import '{module_name}': {e}") from e - except RuntimeError as e: - if e.args[0] == "No suspender": - raise ImportError( - f"Failed to import '{module_name}': Only 'cloudflare:workers' and 'cloudflare:sockets' are available in the global scope." - ) from e - raise - except ImportError as e: - if e.args[0].startswith("cannot import name 'run_sync' from 'pyodide.ffi'"): - raise ImportError( - f"Failed to import '{module_name}': Only 'cloudflare:workers' and 'cloudflare:sockets' are available until the next python runtime version." - ) from e - raise - - -@contextmanager -def patch_env( - d: dict[str, Any] | Sequence[tuple[str, Any]] | None = None, **kwds: dict[str, Any] -) -> Iterator[None]: - if d: - kwds = dict(d) | kwds - yield from _pyodide_entrypoint_helper.patch_env_helper(to_js(kwds)) - - -type JSBody = ( - "js.Blob | JsBuffer | js.FormData | js.ReadableStream | js.URLSearchParams" -) -type Body = "str | FormData | JSBody" -type Headers = "dict[str, str] | list[tuple[str, str]] | js.Headers" - - -# https://developers.cloudflare.com/workers/runtime-apis/request/#the-cf-property-requestinitcfproperties -class RequestInitCfProperties(TypedDict, total=False): - apps: bool | None - cacheEverything: bool | None - cacheKey: str | None - cacheTags: list[str] | None - cacheTtl: int - cacheTtlByStatus: dict[str, int] - image: ( - Any | None - ) # TODO: https://developers.cloudflare.com/images/transform-images/transform-via-workers/ - mirage: bool | None - polish: str | None - resolveOverride: str | None - scrapeShield: bool | None - webp: bool | None - - -# This matches the Request options: -# https://developers.cloudflare.com/workers/runtime-apis/request/#options -class FetchKwargs(TypedDict, total=False): - headers: "Headers | None" - body: "Body | None" - method: HTTPMethod | None - redirect: str | None - cf: RequestInitCfProperties | None - fetcher: type[pyfetch] | None - - -# TODO: Pyodide's FetchResponse.headers returns a dict[str, str] which means -# duplicates are lost, we should fix that so it returns a http.client.HTTPMessage -class FetchResponse(pyodide.http.FetchResponse): - # TODO: Consider upstreaming the `body` attribute - # TODO: Behind a compat flag make this return a native stream (StreamReader?), or perhaps - # behind a different name, maybe `stream`? - @property - def body(self) -> "js.ReadableStream": - """ - Returns the body as a JavaScript ReadableStream from the JavaScript Response instance. - """ - return _jsnull_to_none(self.js_response.body) - - @property - def js_object(self) -> "js.Response": - return self.js_response - - """ - Instance methods defined below. - - Some methods are implemented by `FetchResponse`, these include `buffer` - (replacing JavaScript's `arrayBuffer`), `bytes`, `json`, and `text`. - - There are also some additional methods implemented by `FetchResponse`. - See https://pyodide.org/en/stable/usage/api/python-api/http.html#pyodide.http.FetchResponse - for details. - """ - - async def formData(self) -> "FormData": # TODO: Remove after certain compat date. - return await self.form_data() - - async def form_data(self) -> "FormData": - self._raise_if_failed() - try: - return FormData(await self.js_response.formData()) - except JsException as exc: - raise _to_python_exception(exc) from exc - - def replace_body(self, body: Body) -> "Response": - """ - Returns a new Response object with the same options (status, headers, etc) as - the original but with an updated body. - """ - b = body.js_object if isinstance(body, FormData) else body - js_resp = js.Response.new(b, self.js_response) - return Response(js_resp) - - async def blob(self) -> "Blob": - self._raise_if_failed() - return Blob(await self.js_object.blob()) - - """ - Static methods defined below. The `error` static method is not implemented as - it is not useful for the Workers use case. - """ - - @staticmethod - def redirect(url: str, status: HTTPStatus | int = HTTPStatus.FOUND): - code = status.value if isinstance(status, HTTPStatus) else status - try: - return js.Response.redirect(url, code) - except JsException as exc: - raise _to_python_exception(exc) from exc - - @staticmethod - def from_json( - data: str | dict[str, Any] | list[Any] | JsProxy, - status: HTTPStatus | int = HTTPStatus.OK, - status_text="", - headers: Headers = None, - ) -> "Response": - options = Response._create_options(status, status_text, headers) - js_resp = None - try: - if isinstance(data, JsProxy): - js_resp = js.Response.json(data, **options) - else: - if "headers" not in options: - options["headers"] = _to_js_headers( - {"content-type": "application/json"} - ) - elif not options["headers"].has("content-type"): - options["headers"].set("content-type", "application/json") - js_resp = js.Response.new(json.dumps(data), **options) - except JsException as exc: - raise _to_python_exception(exc) from exc - - return Response(js_resp) - - def json(self, *args: Never, **kwargs: Never): - if isinstance(self, Response): - return super().json() - # For compatibility, allow static use of Response.json() to mean Response.from_json(). - data = self - return Response.from_json(data, *args, **kwargs) - - -if pyodide_version == "0.26.0a2": - - async def _pyfetch_patched( - request: "str | js.Request", **kwargs: Any - ) -> "Response": - # This is copied from https://github.com/pyodide/pyodide/blob/d3f99e1d/src/py/pyodide/http.py - custom_fetch = kwargs["fetcher"] if "fetcher" in kwargs else js.fetch - kwargs["fetcher"] = None - try: - return Response( - await custom_fetch( - request, to_js(kwargs, dict_converter=Object.fromEntries) - ), - ) - except JsException as e: - raise OSError(e.message) from None -else: - _pyfetch_patched = pyfetch - - -async def fetch( - resource: "str | Request | js.Request", - **other_options: Unpack[FetchKwargs], -) -> "Response": - if isinstance(resource, Request): - resource = resource.js_object - if "method" in other_options and isinstance(other_options["method"], HTTPMethod): - other_options["method"] = other_options["method"].value - - resp = await _pyfetch_patched(resource, **other_options) - return Response(resp.js_response) - - -def _to_python_exception(exc: JsException) -> Exception: - if exc.name == "RangeError": - return ValueError(exc.message) - elif exc.name == "TypeError": - return TypeError(exc.message) - else: - return exc - - -def _from_js_error(exc: JsException) -> Exception: - # convert into Python exception after a full round trip - # Python - JS - Python - if not exc.message or not exc.message.startswith("PythonError"): - return _to_python_exception(exc) - - # extract the Python exception type from the traceback - error_message_last_line = exc.message.split("\n")[-2] - if error_message_last_line.startswith("TypeError"): - return TypeError(error_message_last_line) - elif error_message_last_line.startswith("ValueError"): - return ValueError(error_message_last_line) - elif error_message_last_line.startswith("workers.workflows.NonRetryableError"): - return NonRetryableError(error_message_last_line) - else: - return _to_python_exception(exc) - - -@contextmanager -def _manage_pyproxies(): - proxies = js.Array.new() - try: - yield proxies - finally: - destroy_proxies(proxies) - - -def _is_js_instance(val, js_cls_name): - return hasattr(val, "constructor") and val.constructor.name == js_cls_name - - -try: - import _cloudflare_compat_flags -except ImportError: - _cloudflare_compat_flags = object() - - -def get_compat_flag(flag: str) -> bool: - return getattr(_cloudflare_compat_flags, flag, False) - - -def _to_js_headers(headers: Headers): - if isinstance(headers, list): - # We should have a list[tuple[str, str]] - return js.Headers.new(headers) - elif isinstance(headers, dict): - return js.Headers.new(headers.items()) - elif _is_js_instance(headers, "Headers"): - return headers - else: - raise TypeError("Received unexpected type for headers argument") - - -@contextmanager -def _get_js_body(body): - if isinstance(body, bytes): - proxy_bytes = create_proxy(body) - proxy_buffer = proxy_bytes.getBuffer() - try: - yield proxy_buffer.data - return - finally: - proxy_buffer.release() - proxy_bytes.destroy() - if isinstance(body, FormData): - yield body.js_object - return - yield body - - -RESPONSE_ACCEPTED_TYPES = { - # BufferSource types - "Blob", - "ArrayBuffer", - "TypedArray", - "DataView", - "Uint8Array", - "Uint8ClampedArray", - "Int8Array", - "Uint16Array", - "Int16Array", - "Uint32Array", - "Int32Array", - "Float16Array", - "Float32Array", - "Float64Array", - "BigInt64Array", - "BigUint64Array", - # Other types - "FormData", - "ReadableStream", - "URLSearchParams", - "Response", -} - - -class Response(FetchResponse): - """ - This class represents the response to an HTTP request, with a similar API to that of the web - `Response` API: https://developer.mozilla.org/en-US/docs/Web/API/Response. - """ - - def __init__( - self, - body: Body = None, - status: HTTPStatus | int | None = None, - status_text="", - headers: Headers = None, - web_socket: "js.WebSocket | None" = None, - ): - """ - Represents the response to a request. - - Based on the JS API of the same name: - https://developer.mozilla.org/en-US/docs/Web/API/Response/Response. - """ - # Verify passed in types. - if hasattr(body, "constructor"): - if body.constructor.name not in RESPONSE_ACCEPTED_TYPES: - raise TypeError( - f"Unsupported type in Response: {body.constructor.name}" - ) - elif not isinstance(body, str | FormData | bytes) and body is not None: - raise TypeError(f"Unsupported type in Response: {type(body).__name__}") - - # Handle constructing a Response from a JS Response. - if _is_js_instance(body, "Response"): - if status is not None or len(status_text) > 0 or headers is not None: - raise ValueError( - "Expected no options when constructing Response from a js.Response" - ) - super().__init__(body.url, body) - return - - options = self._create_options(status, status_text, headers, web_socket) - - # To avoid unnecessary copies we use this context manager. - with _get_js_body(body) as js_body: - # Initialize via the FetchResponse super-class which gives us access to - # methods that we would ordinarily have to redeclare. - js_resp = js.Response.new(js_body, **options) - super().__init__(js_resp.url, js_resp) - - def __repr__(self): - body = [f"status={self.status}"] - if self.js_object.statusText: - body.append(f"status_text={self.status_text!r}") - if "content-type" in self.headers: - body.append(f"content_type={self.headers['content-type']!r}") - if self.js_object.url: - body.append(f"url={self.js_object.url!r}") - if self.js_object.type != "default": - body.append(f"type={self.js_object.type!r}") - return f"Response({', '.join(body)})" - - @staticmethod - def _create_options( - status: HTTPStatus | int | None = HTTPStatus.OK, - status_text="", - headers: Headers = None, - web_socket: "js.WebSocket | None" = None, - ): - options = {} - if status: - options["status"] = ( - status.value if isinstance(status, HTTPStatus) else status - ) - if status_text: - options["statusText"] = status_text - if headers: - options["headers"] = _to_js_headers(headers) - if web_socket: - options["webSocket"] = web_socket - return options - - -FormDataValue = "str | js.Blob | Blob" - - -def _py_value_to_js(item: FormDataValue) -> "str | js.Blob": - if isinstance(item, Blob): - return item.js_object - else: - return item - - -def _js_value_to_py(item: FormDataValue) -> "str | Blob | File": - if hasattr(item, "constructor") and (item.constructor.name in ("Blob", "File")): - if item.constructor.name == "File": - return File(item, item.name) - else: - return Blob(item) - else: - return item - - -class FormData(MutableMapping[str, FormDataValue]): - """ - This class represents a set of key/value pairs for forms. - - The API of this class follows that of https://pypi.org/project/multidict/ and - https://developer.mozilla.org/en-US/docs/Web/API/FormData. - """ - - def __init__( - self, form_data: "js.FormData | None | dict[str, FormDataValue]" = None - ): - if not form_data: - self._js_form_data = js.FormData.new() - return - - if isinstance(form_data, dict): - self._js_form_data = js.FormData.new() - for k, v in form_data.items(): - self._js_form_data.append(k, _py_value_to_js(v)) - return - - if _is_js_instance(form_data, "FormData"): - self._js_form_data = form_data - return - - raise TypeError("Expected form_data to be a dict or an instance of FormData") - - def __getitem__(self, key: str) -> FormDataValue: - return _js_value_to_py(self._js_form_data.get(key)) - - def __setitem__(self, key: str, value: FormDataValue): - if isinstance(value, list): - raise TypeError("Expected single item in arguments to FormData.__setitem__") - self._js_form_data.set(key, _py_value_to_js(value)) - - def append(self, key: str, value: FormDataValue, filename: str | None = None): - self._js_form_data.append(key, _py_value_to_js(value), filename) - - def delete(self, key: str): - self._js_form_data.delete(key) - - def __contains__(self, key: str) -> bool: - return self._js_form_data.has(key) - - def values(self) -> Generator[FormDataValue, None, None]: - for val in self._js_form_data.values(): - yield _js_value_to_py(val) - - def keys(self) -> Generator[str, None, None]: - yield from self._js_form_data.keys() - - def __iter__(self): - yield from self.keys() - - def items(self) -> Generator[tuple[str, FormDataValue], None, None]: - for k, v in self._js_form_data.entries(): - yield (k, _js_value_to_py(v)) - - def __delitem__(self, key: str): - self.delete(key) - - def __len__(self): - return len(self.keys()) - - def get_all(self, key: str) -> list[FormDataValue]: - return [_js_value_to_py(x) for x in self._js_form_data.getAll(key)] - - @property - def js_object(self) -> "js.FormData": - return self._js_form_data - - -def _supports_buffer_protocol(o): - try: - # memoryview used only for testing type; 'with' releases the view instantly - with memoryview(o): - return True - except TypeError: - return False - - -@contextmanager -def _make_blob_entry(e): - if isinstance(e, str): - yield e - return - if isinstance(e, Blob): - yield e._js_blob - return - if hasattr(e, "constructor") and (e.constructor.name in ("Blob", "File")): - yield e - return - if _supports_buffer_protocol(e): - px = create_proxy(e) - buf = px.getBuffer() - try: - yield buf.data - return - finally: - buf.release() - px.destroy() - raise TypeError(f"Don't know how to handle {type(e)} for Blob()") - - -def _is_iterable(obj): - if isinstance(obj, (str, bytes)): - return False - try: - iter(obj) - except TypeError: - return False - else: - return True - - -BlobValue = ( - "str | bytes | js.ArrayBuffer | js.TypedArray | js.DataView | js.Blob | Blob | File" -) - - -class BlobEnding(StrEnum): - TRANSPARENT = "transparent" - NATIVE = "native" - - -class Blob: - def __init__( - self, - blob_parts: "Iterable[BlobValue] | BlobValue", - content_type: str | None = None, - endings: BlobEnding | str | None = None, - ): - if endings: - endings = str(endings) - - is_single_item = not _is_iterable(blob_parts) - if is_single_item: - # Inherit the content_type if we have a single item. If a File is passed - # in then its metadata is lost. - if not content_type and isinstance(blob_parts, Blob): - content_type = blob_parts.content_type - if hasattr(blob_parts, "constructor") and ( - blob_parts.constructor.name in ("Blob", "File") - ): - if not content_type: - content_type = blob_parts.type - - # Otherwise create a new Blob below. - blob_parts = [blob_parts] - - with ExitStack() as stack: - args = [stack.enter_context(_make_blob_entry(e)) for e in blob_parts] - with _manage_pyproxies() as pyproxies: - self._js_blob = js.Blob.new( - to_js(args, pyproxies=pyproxies), - type=content_type, - endings=endings, - ) - - @property - def size(self) -> int: - return self._js_blob.size - - @property - def content_type(self) -> str: - return self._js_blob.type - - @property - def js_object(self) -> "js.Blob": - return self._js_blob - - async def text(self) -> str: - return await self.js_object.text() - - async def bytes(self) -> bytes: - return (await self.js_object.arrayBuffer()).to_bytes() - - def slice( - self, - start: int | None = None, - end: int | None = None, - content_type: str | None = None, - ): - js_sliced_blob = self.js_object.slice(start, end, content_type) - return Blob([js_sliced_blob]) - - -class File(Blob): - def __init__( - self, - blob_parts: "Iterable[BlobValue] | BlobValue", - filename: str, - content_type: str | None = None, - endings: BlobEnding | str | None = None, - last_modified: int | None = None, - ): - if endings: - endings = str(endings) - - is_single_item = not _is_iterable(blob_parts) - if is_single_item: - # Inherit the content_type and lastModified if we have a - # single item. - if not content_type and isinstance(blob_parts, Blob): - content_type = blob_parts.content_type - if not last_modified and isinstance(blob_parts, File): - last_modified = blob_parts.last_modified - if hasattr(blob_parts, "constructor") and ( - blob_parts.constructor.name in ("Blob", "File") - ): - if not content_type: - content_type = blob_parts.type - if blob_parts.constructor.name == "File": - if not last_modified: - last_modified = blob_parts.lastModified - - # Otherwise create a new File below. - blob_parts = [blob_parts] - - with ExitStack() as stack: - args = [stack.enter_context(_make_blob_entry(e)) for e in blob_parts] - with _manage_pyproxies() as pyproxies: - self._js_blob = js.File.new( - to_js(args, pyproxies=pyproxies), - filename, - type=content_type, - endings=endings, - lastModified=last_modified, - ) - - @property - def name(self) -> str: - return self._js_blob.name - - @property - def last_modified(self) -> int: - return self._js_blob.lastModified - - -class Request: - def __init__( - self, input: "Request | str | js.Request", **other_options: Unpack[FetchKwargs] - ): - if _is_js_instance(input, "Request"): - if len(other_options) > 0: - raise ValueError( - "Expected no options when constructing Request from a js.Request" - ) - self._js_request = input - return - - if "method" in other_options and isinstance( - other_options["method"], HTTPMethod - ): - other_options["method"] = other_options["method"].value - - if "headers" in other_options: - other_options["headers"] = _to_js_headers(other_options["headers"]) - self._js_request = js.Request.new( - input._js_request if isinstance(input, Request) else input, **other_options - ) - - def __repr__(self): - return ( - f"Request(method={self._js_request.method!r}, url={self._js_request.url!r})" - ) - - @property - def js_object(self) -> "js.Request": - return self._js_request - - # TODO: expose `body` as a native Python stream in the future, follow how we define `Response` - @property - def body(self) -> "js.ReadableStream": - return self.js_object.body - - @property - def body_used(self) -> bool: - return self.js_object.bodyUsed - - @property - def cache(self) -> str: - return self.js_object.cache - - @property - def credentials(self) -> str: - return self.js_object.credentials - - @property - def destination(self) -> str: - return self.js_object.destination - - @property - def headers(self): - # This is imported here because it costs a lot of CPU time when imported at the top-level. - # At least it does when we do so in our validator tests, doesn't seem to cause trouble in - # production. So as a workaround we do the import here. - # - # TODO(later): when dedicated snapshots are default we can move this import to the top-level. - import http.client - - result = http.client.HTTPMessage() - if not get_compat_flag("python_request_headers_preserve_commas"): - for key, val in self.js_object.headers: - result[key] = val.strip() - - return result - - # With the exception of Set-Cookie, duplicate headers can and are combined with a comma - # in the JS Headers API. We do the same when returning the headers to Python. - # - # See https://httpwg.org/specs/rfc9110.html#rfc.section.5.3. - js_headers = self.js_object.headers - set_cookie_headers = js_headers.getSetCookie() - if set_cookie_headers: - for value in set_cookie_headers: - result.add_header("Set-Cookie", value.strip()) - - for key, val in js_headers: - if key.lower() == "set-cookie": - continue - result.add_header(key, val.strip()) - - return result - - @property - def integrity(self) -> str: - return self.js_object.integrity - - @property - def is_history_navigation(self) -> bool: - return self.js_object.isHistoryNavigation - - @property - def keepalive(self) -> bool: - return self.js_object.keepalive - - @property - def method(self) -> HTTPMethod: - return HTTPMethod[self.js_object.method] - - @property - def mode(self) -> str: - return self.js_object.mode - - @property - def redirect(self) -> str: - return self.js_object.redirect - - @property - def referrer(self) -> str: - return self.js_object.referrer - - @property - def referrer_policy(self) -> str: - return self.js_object.referrerPolicy - - @property - def url(self) -> str: - return self.js_object.url - - def _raise_if_failed(self) -> None: - # TODO: https://github.com/pyodide/pyodide/blob/a53c17fd8/src/py/pyodide/http.py#L252 - if self.body_used: - # TODO: Use BodyUsedError in newer Pyodide versions. - raise OSError("Body already used") - - """ - Instance methods defined below. - - The naming of these methods should match Request's methods when possible. - - TODO: AbortController support. - """ - - async def buffer(self) -> "js.ArrayBuffer": - # The naming of this method matches that of Response. - self._raise_if_failed() - return await self.js_object.arrayBuffer() - - async def form_data(self) -> "FormData": - self._raise_if_failed() - try: - return FormData(await self.js_object.formData()) - except JsException as exc: - raise _to_python_exception(exc) from exc - - async def blob(self) -> Blob: - self._raise_if_failed() - return Blob(await self.js_object.blob()) - - async def bytes(self) -> bytes: - self._raise_if_failed() - return (await self.buffer()).to_bytes() - - def clone(self) -> "Request": - if self.body_used: - # TODO: Use BodyUsedError in newer Pyodide versions. - raise OSError("Body already used") - return Request( - self.js_object.clone(), - ) - - async def json(self, **kwargs: Any) -> Any: - self._raise_if_failed() - return json.loads(await self.text(), **kwargs) - - async def text(self) -> str: - self._raise_if_failed() - return await self.js_object.text() - - -def _python_from_rpc_default_converter(value, convert, cache): - if not hasattr(value, "constructor"): - # Assume that the object doesn't need conversion as it's not a JS object. - return value - - if value.constructor.name == "Response": - return Response(value) - elif value.constructor.name == "FormData": - return FormData(value) - elif value.constructor.name == "Blob": - return Blob(value) - elif value.constructor.name == "File": - return File(value) - elif value.constructor.name == "Request": - return Request(value) - elif value.constructor.name == "Date": - # TODO: Pyodide should gain support for this, we should upstream this. - return datetime.datetime.fromtimestamp(value.getTime() / 1000) - elif value.constructor.name == "Error": - return Exception(value.toString()) - elif value.constructor.name == "Number": - return value.valueOf() - - # We used to throw an error here, but since these conversions are now automatic when the default - # entrypoint is being used, it makes sense to be less loud about it and just pass through the - # JS value un-modified. - # - # This does mean that in the future we need to be careful when adding type wrappers for new - # types here, so if you're doing this make sure to do so behind a compat flag. - return value - - -def python_from_rpc(obj: "JsProxy"): - """ - Converts JS objects like Response, Request, Blob, etc. to equivalent Python objects defined in - this module and also other JS objects like Map, Set, etc. to equivalent Python stdlib objects. - - This method is used for Workers RPC in Python to convert JavaScript objects to Python. As such - it does not support serializing all JS object types. - """ - - if not hasattr(obj, "constructor"): - return obj - - if obj.constructor.name == "TestController": - # This object currently has no methods defined on it. If this changes we should - # implement a Python wrapper for it, but for now we'll just pass in None. - return None - - result = obj.to_py(default_converter=_python_from_rpc_default_converter) - - return result - - -def _raise_on_disabled_type(value): - if _is_js_instance(value, "RegExp"): - raise TypeError(f"{value.constructor.name} cannot be sent over RPC.") - - if isinstance(value, (tuple, bytearray, LambdaType)): - raise TypeError(f"{type(value)} cannot be sent over RPC.") - - if inspect.isawaitable(value): - # The caller is expected to await the value prior to conversion. - raise TypeError(f"Awaitable {type(value)} cannot be sent over RPC.") - - if _is_iterable(value): - if isinstance(value, dict): - for v in value.values(): - _raise_on_disabled_type(v) - else: - for v in value: - _raise_on_disabled_type(v) - - -def _python_to_rpc_default_converter(obj, convert, cache): - if obj is None: - return obj - - if hasattr(obj, "js_object"): - return obj.js_object - - if isinstance(obj, datetime.datetime): - # TODO: Pyodide should gain support for this, we should upstream this. - return js.Date.new(obj.timestamp() * 1000) - - if isinstance(obj, Exception): - return js.Error.new(str(obj)) - - _raise_on_disabled_type(obj) - - return obj - - -def python_to_rpc(value) -> JsProxy: - """ - Converts Python objects defined in this module (Response, Request, etc) and native Python types - like Map, Set, datetime to equivalent JavaScript types. - - This method is used for Workers RPC in Python to convert Python objects to JavaScript. As such - it does not support serializing all Python object types. - """ - - # `to_js` won't always call the default_converter, for example when a list of tuples is passed - _raise_on_disabled_type(value) - - result = to_js( - value, - default_converter=_python_to_rpc_default_converter, - dict_converter=js.Map.new, - ) - - return result - - -class _FetcherWrapper: - def __init__(self, binding): - self._binding = binding - - def _getattr_helper(self, name): - attr = getattr(self._binding, name) - - if not callable(attr): - return attr - - # Not using `@functools.wraps(attr)` here because `attr` is a JS proxy. - async def wrapper(*args, **kwargs): - js_args = [python_to_rpc(arg) for arg in args] - js_kwargs = {k: python_to_rpc(v) for k, v in kwargs.items()} - result = attr(*js_args, **js_kwargs) - if hasattr(result, "then") and callable(result.then): - return python_from_rpc(await result) - else: - return python_from_rpc(result) - - return wrapper - - def __getattr__(self, name): - result = self._getattr_helper(name) - setattr(self, name, result) - return result - - def fetch(self, *args, **kwargs): - return fetch(*args, fetcher=self._binding.fetch, **kwargs) - - -class _DurableObjectNamespaceWrapper: - def __init__(self, binding): - self._binding = binding - - def __getattr__(self, name): - return getattr(self._binding, name) - - def get(self, *args, **kwargs): - return _FetcherWrapper(self._binding.get(*args, **kwargs)) - - def getByName(self, *args, **kwargs): - return _FetcherWrapper(self._binding.getByName(*args, **kwargs)) - - def jurisdiction(self, *args, **kwargs): - return _DurableObjectNamespaceWrapper( - self._binding.jurisdiction(*args, **kwargs) - ) - - -class DurableObjectAbort(BaseException): - pass - - -class DurableObjectContext: - def __init__(self, ctx: "DurableObjectState"): - self._ctx = ctx - - def __getattr__(self, name: str): - result = getattr(self._ctx, name) - setattr(self, name, result) - return result - - def abort(self, reason: str | None = None): - # DurableObjectState.abort() terminates JS execution immediately. If Python - # calls it synchronously while asyncio is still running the task in the event loop, - # V8 unwinds the stack before asyncio can run its task-exit cleanup, leaving - # stale task state behind for the next request. - # - # Therefore, we queue the real abort into a microtask so Python can unwind first, - # then raise BaseException to stop user code without being swallowed by - # `except Exception` handlers. - ctx = self._ctx - - if reason is None: - callback = create_once_callable(lambda: ctx.abort()) - else: - callback = create_once_callable(lambda: ctx.abort(reason)) - - js.queueMicrotask(callback) - raise DurableObjectAbort(reason or "Durable Object abort requested") - - -class _WorkflowInstanceWrapper: - def __init__(self, binding): - self._binding = binding - - def __getattr__(self, name): - return getattr(self._binding, name) - - async def send_event(self, *args, **kwargs): - return self._binding.sendEvent(*args, **kwargs) - - async def pause(self, *args, **kwargs): - return self._binding.pause(*args, **kwargs) - - async def resume(self, *args, **kwargs): - return self._binding.resume(*args, **kwargs) - - async def terminate(self, *args, **kwargs): - return self._binding.terminate(*args, **kwargs) - - async def restart(self, *args, **kwargs): - return self._binding.restart(*args, **kwargs) - - async def status(self, *args, **kwargs): - return self._binding.status(*args, **kwargs) - - -class _WorkflowBindingWrapper: - def __init__(self, binding): - self._binding = binding - - def __getattr__(self, name): - return getattr(self._binding, name) - - async def get(self, *args, **kwargs): - return _WorkflowInstanceWrapper(await self._binding.get(*args, **kwargs)) - - async def create(self, *args, **kwargs): - return _WorkflowInstanceWrapper(await self._binding.create(*args, **kwargs)) - - async def create_batch(self, *args, **kwargs): - return [ - _WorkflowInstanceWrapper(w) - for w in await self._binding.createBatch(*args, **kwargs) - ] - - -class _EnvWrapper: - def __init__(self, env: Any): - self._env = env - - def _getattr_helper(self, name): - binding = getattr(self._env, name) - if _is_js_instance(binding, "Fetcher"): - return _FetcherWrapper(binding) - - if _is_js_instance(binding, "DurableObjectNamespace"): - return _DurableObjectNamespaceWrapper(binding) - - if _is_js_instance(binding, "WorkflowImpl"): - return _WorkflowBindingWrapper(binding) - - # TODO: Implement APIs for bindings. - return binding - - def __getattr__(self, name): - result = self._getattr_helper(name) - setattr(self, name, result) - return result - - -def handler(func): - """ - When applied to handlers such as `on_fetch` it will rewrite arguments passed in to native Python - types defined in this module. For example, the `request` argument to `on_fetch` gets converted - to an instance of the Request class defined in this module. - """ - - @functools.wraps(func) - def wrapper(*args, **kwargs): - # TODO: support transforming kwargs - if len(args) > 0 and _is_js_instance(args[0], "Request"): - args = (Request(args[0]), *args[1:]) - - # Wrap `env` so that bindings can be used without to_js. - if len(args) > 1: - args = (args[0], _EnvWrapper(args[1]), *args[2:]) - - return func(*args, **kwargs) - - return wrapper - - -class _WorkflowStepWrapper: - def __init__(self, js_step): - self._js_step = js_step - self._memoized_dependencies = {} - self._in_flight = {} - self.step_closures = {} - - # Assign the appropriate method based on compat flag - if _cloudflare_compat_flags.python_workflows_implicit_dependencies: - self.do = self._do_implicit - else: - self.do = self._do_legacy - - def _do_legacy(self, name, depends=None, concurrent=False, config=None): - """Original signature - positional args allowed, explicit depends parameter.""" - return self._create_step_decorator( - name=name, - depends=depends, - concurrent=concurrent, - config=config, - implicit=False, - ) - - def _do_implicit(self, name=None, *, concurrent=False, config=None): - """New signature - keyword-only args, dependencies resolved from param names.""" - return self._create_step_decorator( - name=name, - depends=None, - concurrent=concurrent, - config=config, - implicit=True, - ) - - def _create_step_decorator(self, name, depends, concurrent, config, implicit): - """Shared decorator factory for both legacy and implicit modes.""" - - def decorator(func): - step_name = func.__name__ if name is None else name - - async def wrapper(): - results_future_list = self._build_dependency_list( - func, depends, implicit - ) - results = await self._gather_results(results_future_list, concurrent) - return await _do_call(self, step_name, config, func, *results) - - wrapper._step_name = step_name - self.step_closures[step_name] = wrapper - return wrapper - - return decorator - - def _build_dependency_list(self, func, depends, implicit): - """Build the dependency list based on mode (implicit vs legacy).""" - sig = inspect.signature(func) - results_future_list = [] - - if implicit: - # Implicit mode: resolve dependencies from parameter names - for p in sig.parameters.values(): - if p.name in self.step_closures: - results_future_list.append(self.step_closures[p.name]) - elif p.name == "ctx": - results_future_list.append(p) - else: - raise TypeError(f"Received unexpected parameter {p.name}") - else: - # Legacy mode: use explicit depends list, support ctx parameter - non_ctx_params = [p for p in sig.parameters.values() if p.name != "ctx"] - - if depends is None and len(non_ctx_params) > 0: - raise TypeError( - f"Step has {len(non_ctx_params)} non-ctx parameter(s) but no 'depends' list provided" - ) - - elif depends is not None and len(depends) != len(non_ctx_params): - raise TypeError( - f"Step declares {len(non_ctx_params)} non-ctx parameter(s) but 'depends' has {len(depends)} item(s)" - ) - - curr = 0 - for p in sig.parameters.values(): - if p.name == "ctx": - results_future_list.append(p) - else: - results_future_list.append(depends[curr]) - curr += 1 - - return results_future_list - - async def _gather_results(self, results_future_list, concurrent): - """Resolve dependencies concurrently or sequentially.""" - if concurrent: - return await gather( - *[self._resolve_dependency(dep) for dep in results_future_list or []] - ) - else: - return [ - await self._resolve_dependency(dep) for dep in results_future_list or [] - ] - - def sleep(self, *args, **kwargs): - return self._js_step.sleep(*args, **kwargs) - - def sleep_until(self, name, timestamp): - if not isinstance(timestamp, str): - timestamp = python_to_rpc(timestamp) - - return self._js_step.sleepUntil(name, timestamp) - - def wait_for_event(self, name, event_type, /, timeout="24 hours"): - return self._js_step.waitForEvent( - name, - to_js( - {"type": event_type, "timeout": timeout}, - dict_converter=Object.fromEntries, - ), - ) - - async def _resolve_dependency(self, dep): - if hasattr(dep, "name") and dep.name == "ctx": - return dep - elif dep._step_name in self._memoized_dependencies: - return self._memoized_dependencies[dep._step_name] - elif dep._step_name in self._in_flight: - return await self._in_flight[dep._step_name] - - return await dep() - - -async def _do_call(entrypoint, name, config, callback, *results): - async def _callback(ctx=None): - # deconstruct the actual ctx object - resolved_results = tuple( - python_from_rpc(ctx) - if isinstance(r, inspect.Parameter) and r.name == "ctx" - else r - for r in results - ) - result = callback(*resolved_results) - - if inspect.iscoroutine(result): - result = await result - return to_js(result, dict_converter=Object.fromEntries) - - async def _closure(): - try: - if config is None: - coroutine = await entrypoint._js_step.do(name, _callback) - else: - coroutine = await entrypoint._js_step.do( - name, to_js(config, dict_converter=Object.fromEntries), _callback - ) - - return python_from_rpc(coroutine) - except Exception as exc: - raise _from_js_error(exc) from exc - - task = create_task(_closure()) - entrypoint._in_flight[name] = task - - try: - result = await task - entrypoint._memoized_dependencies[name] = result - finally: - del entrypoint._in_flight[name] - - return result - - -def _wrap_subclass(cls): - # Override the class __init__ so that we can wrap the `env` in the constructor. - original_init = cls.__init__ - - def wrapped_init(self, *args, **kwargs): - args = list(args) - if len(args) > 0: - _pyodide_entrypoint_helper.patchWaitUntil(args[0]) - if issubclass(cls, DurableObject): - args[0] = DurableObjectContext(args[0]) - if len(args) > 1: - args[1] = _EnvWrapper(args[1]) - - original_init(self, *args, **kwargs) - - cls.__init__ = wrapped_init - - -def _wrap_workflow_step(cls): - run_fn = getattr(cls, "run", None) - if run_fn is None: - return - - # Only patch `on_run` for subclasses of WorkflowEntrypoint. - if not issubclass(cls, WorkflowEntrypoint): - # Not a workflow subclass, so don't wrap `on_run`. - return - - @functools.wraps(run_fn) - async def wrapped_run(self, event=None, step=None, /, *args, **kwargs): - if event is not None: - event = python_from_rpc(event) - if step is not None: - step = _WorkflowStepWrapper(step) - - result = run_fn(self, event, step, *args, **kwargs) - - if inspect.iscoroutine(result): - result = await result - - return result - - cls.run = wrapped_run - - -class DurableObject: - """ - Base class used to define a Durable Object. - """ - - ctx: "DurableObjectContext" - env: "Env" - - def __init__(self, ctx: "DurableObjectState", env: "Env"): - self.ctx = ctx - self.env = env - - def __init_subclass__(cls, **_kwargs): - _wrap_subclass(cls) - - -class WorkerEntrypoint: - """ - Base class used to define a Worker Entrypoint. - """ - - ctx: "ExecutionContext" - env: "Env" - - def __init__(self, ctx: "ExecutionContext", env: "Env"): - self.ctx = ctx - self.env = env - - def __init_subclass__(cls, **_kwargs: Any): - _wrap_subclass(cls) - - -class WorkflowEntrypoint: - """ - Base class used to define a Workflow Entrypoint. - """ - - ctx: "ExecutionContext" - env: "Env" - - def __init__(self, ctx: "ExecutionContext", env: "Env"): - self.ctx = ctx - self.env = env - - def __init_subclass__(cls, **_kwargs: Any): - _wrap_subclass(cls) - _wrap_workflow_step(cls) diff --git a/python_modules/workers/py.typed b/python_modules/workers/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_modules/workers/workflows.py b/python_modules/workers/workflows.py deleted file mode 100644 index ee34fcf47..000000000 --- a/python_modules/workers/workflows.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Workflow-specific classes and exceptions for the workers module. -""" - - -class NonRetryableError(Exception): - """ - A marker exception used to signal that a workflow step should not be retried. - This is a special exception used by workflows. - """ - - pass diff --git a/python_modules/workers_runtime_sdk-1.1.5.dist-info/INSTALLER b/python_modules/workers_runtime_sdk-1.1.5.dist-info/INSTALLER deleted file mode 100644 index 5c69047b2..000000000 --- a/python_modules/workers_runtime_sdk-1.1.5.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -uv \ No newline at end of file diff --git a/python_modules/workers_runtime_sdk-1.1.5.dist-info/METADATA b/python_modules/workers_runtime_sdk-1.1.5.dist-info/METADATA deleted file mode 100644 index 1405d489c..000000000 --- a/python_modules/workers_runtime_sdk-1.1.5.dist-info/METADATA +++ /dev/null @@ -1,15 +0,0 @@ -Metadata-Version: 2.4 -Name: workers-runtime-sdk -Version: 1.1.5 -Summary: Python SDK for Cloudflare Workers -Project-URL: Homepage, https://github.com/cloudflare/workers-py -Project-URL: Bug Tracker, https://github.com/cloudflare/workers-py/issues -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python :: 3 -Requires-Python: >=3.12 -Description-Content-Type: text/markdown - -# workers-runtime-sdk - -Runtime SDK for Python Cloudflare Workers. diff --git a/python_modules/workers_runtime_sdk-1.1.5.dist-info/RECORD b/python_modules/workers_runtime_sdk-1.1.5.dist-info/RECORD deleted file mode 100644 index c64fa233b..000000000 --- a/python_modules/workers_runtime_sdk-1.1.5.dist-info/RECORD +++ /dev/null @@ -1,14 +0,0 @@ -_cloudflare_compat_flags.pyi,sha256=xvA4GFtXkDbK6rIv4GMZ_4WYo--45cOvP7kuNcaehFk,131 -_pyodide_entrypoint_helper.pyi,sha256=WbDYGJQbNnFIFDlrVIeHeEukU-HuAn33e9wShldLkyU,264 -_workers_sdk_entropy_import_context.pth,sha256=Xj7M7o1SlvQfb21987pWiUqRgsf7whD93fW8j3CJ4F4,50 -_workers_sdk_entropy_import_context.py,sha256=dKZW2VpskaXBfBtc74qTLksIqiCBXnejvFA7atokRbg,5175 -_workers_sdk_entropy_import_context_loader.py,sha256=INu69ZEiIhOjA-l2ewnig1vKcqcQNPZ_-G65TvjyxuM,498 -workers/__init__.py,sha256=reEDBHb3EK8cKT9L7YMGIVw70C-kd3o_-9lZElu3O-4,1319 -workers/_workers.py,sha256=HshDzyWvthb3VETIzVICgaoBgrFFqIiUOQtXzNY2Lkw,46578 -workers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -workers/workflows.py,sha256=k3n99BbaVtJAA2kWWlG3Lag_ckrqiTrHhShWMdCYA78,270 -workers_runtime_sdk-1.1.5.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -workers_runtime_sdk-1.1.5.dist-info/METADATA,sha256=MEwOQlavR-QF3vd4tT9Wh2YtIhhaecRCDOPapfo_Buw,521 -workers_runtime_sdk-1.1.5.dist-info/RECORD,, -workers_runtime_sdk-1.1.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -workers_runtime_sdk-1.1.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 diff --git a/python_modules/workers_runtime_sdk-1.1.5.dist-info/REQUESTED b/python_modules/workers_runtime_sdk-1.1.5.dist-info/REQUESTED deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_modules/workers_runtime_sdk-1.1.5.dist-info/WHEEL b/python_modules/workers_runtime_sdk-1.1.5.dist-info/WHEEL deleted file mode 100644 index b1b94fd58..000000000 --- a/python_modules/workers_runtime_sdk-1.1.5.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.29.0 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/src/handlers/__init__.py b/src/handlers/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/handlers/_shared.py b/src/handlers/_shared.py deleted file mode 100644 index 95d60e418..000000000 --- a/src/handlers/_shared.py +++ /dev/null @@ -1,38 +0,0 @@ -import json - -from js import Object, Response -from pyodide.ffi import to_js - - -CORS_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type", -} - - -def _opts(d): - return to_js(d, dict_converter=Object.fromEntries) - - -def json_response(payload, status=200): - body = json.dumps(payload) - return Response.new(body, _opts({ - "status": status, - "headers": {"Content-Type": "application/json; charset=utf-8", **CORS_HEADERS}, - })) - - -def error_response(message, status=400): - return json_response({"error": message}, status=status) - - -def cors_preflight(): - return Response.new("", _opts({"status": 204, "headers": CORS_HEADERS})) - - -async def read_json(request): - try: - return (await request.json()).to_py() - except Exception: - return None diff --git a/src/handlers/bmi.py b/src/handlers/bmi.py deleted file mode 100644 index 02f9a61a7..000000000 --- a/src/handlers/bmi.py +++ /dev/null @@ -1,35 +0,0 @@ -from ._shared import error_response, json_response, read_json - - -def calculate_bmi(height_m, weight_kg): - return round(weight_kg / (height_m ** 2), 2) - - -def category_for(bmi): - if bmi < 18.5: - return "Underweight" - if bmi < 24.9: - return "Normal weight" - if bmi < 29.9: - return "Overweight" - if bmi < 34.9: - return "Obese (Class I)" - if bmi < 39.9: - return "Obese (Class II)" - return "Obese (Class III)" - - -async def handle(request, env): - body = await read_json(request) - if body is None: - return error_response("invalid JSON") - try: - h = float(body["height_m"]) - w = float(body["weight_kg"]) - except (KeyError, TypeError, ValueError): - return error_response("height_m and weight_kg required (numbers)") - if h <= 0 or w <= 0: - return error_response("height_m and weight_kg must be > 0") - - bmi = calculate_bmi(h, w) - return json_response({"bmi": bmi, "category": category_for(bmi)}) diff --git a/src/handlers/hangman.py b/src/handlers/hangman.py deleted file mode 100644 index a5fdd0b33..000000000 --- a/src/handlers/hangman.py +++ /dev/null @@ -1,78 +0,0 @@ -import random - -from ._shared import error_response, json_response, read_json - - -WORDS = { - "easy": [ - "egg", "toy", "water", "day", "shower", "tiger", "home", "coat", - "garden", "throw", "crown", "baby", "office", "beach", "phone", - "computer", "flower", "bank", "train", "brush", "game", - ], - "medium": [ - "soup", "tree", "purple", "orange", "rocket", "pillow", "guitar", - "kitchen", "window", "yellow", "planet", "doctor", "rabbit", - "engine", "ticket", "candle", "puzzle", "mountain", "ocean", - ], - "hard": [ - "syzygy", "labyrinth", "quixotic", "ephemeral", "petrichor", - "serendipity", "sonder", "limerence", "halcyon", - ], -} - -INITIAL_TRIES = {"easy": 8, "medium": 6, "hard": 4} - - -def pick_word(difficulty, seed): - pool = WORDS.get(difficulty, WORDS["medium"]) - rng = random.Random(seed) - return rng.choice(pool).upper() - - -def mask_word(word, guessed_letters): - return "".join((c if c in guessed_letters else "_") for c in word) - - -async def handle(request, env): - body = await read_json(request) - if body is None: - return error_response("invalid JSON") - - difficulty = (body.get("difficulty") or "medium").lower() - if difficulty not in WORDS: - return error_response("difficulty must be easy|medium|hard") - - seed = body.get("word_seed") - if not isinstance(seed, int): - return error_response("word_seed (int) required") - - guessed = body.get("guessed") or [] - if not isinstance(guessed, list) or not all(isinstance(g, str) and len(g) == 1 for g in guessed): - return error_response("guessed must be a list of single letters") - guessed_set = {g.upper() for g in guessed if g.isalpha()} - - word = pick_word(difficulty, seed) - wrong = sorted(g for g in guessed_set if g not in word) - max_tries = INITIAL_TRIES[difficulty] - tries_left = max_tries - len(wrong) - mask = mask_word(word, guessed_set) - - if "_" not in mask: - status = "win" - elif tries_left <= 0: - status = "lose" - else: - status = "ongoing" - - out = { - "mask": mask, - "wrong": wrong, - "tries_left": max(0, tries_left), - "max_tries": max_tries, - "status": status, - "word_length": len(word), - } - if status == "lose": - out["word"] = word - - return json_response(out) diff --git a/src/handlers/madlibs.py b/src/handlers/madlibs.py deleted file mode 100644 index 07c29c877..000000000 --- a/src/handlers/madlibs.py +++ /dev/null @@ -1,60 +0,0 @@ -from ._shared import error_response, json_response, read_json - - -def generate_madlib(choice, adjective, noun, verb, adverb): - stories = { - 1: ( - f"In a mystical and distant land, there was a brave and {adjective} explorer named {noun}. " - f"{noun.capitalize()} had always dreamed of {verb} {adverb} to discover hidden treasures. " - f"One day, while {verb} {adverb} deep in the dense {adjective} jungle, {noun} stumbled upon an ancient {noun}. " - f"The {noun} was covered in {adjective} vines and moss, but it {verb} {adverb} with the promise of untold riches. " - f"With {adjective} excitement, {noun} began to {verb} {adverb}, clearing away every obstacle. " - f"At the heart of the {noun}, a {adjective} light {verb} {adverb}, revealing a chest of {adjective} {noun}. " - f"{noun} decided to {verb} {adverb} home and share the {adjective} riches with the village — " - f"and lived {adverb} ever after." - ), - 2: ( - f"In the enchanting world of {noun}, there existed a {adjective} school known as the {adjective} Academy of {noun}. " - f"Our protagonist, {noun}, had always dreamed of {verb} {adverb} and becoming a {adjective} wizard. " - f"At the academy, {noun} learned to {verb} {adverb}, brew {adjective} potions, and cast {adjective} spells. " - f"With a {adjective} friend by their side, {noun} set out to {verb} {adverb} and uncover the secrets of the {adjective} forest, " - f"making the world a {adjective} place along the way." - ), - 3: ( - f"In the bustling city of {noun}, Dr. {noun} was renowned for {verb} {adverb} and pushing the boundaries of {adjective} science. " - f"One day, Dr. {noun} had a {adjective} idea: a {adjective} time machine. " - f"After many {adjective} experiments, Dr. {noun} stepped inside and {verb} {adverb} into the unknown. " - f"In a {adjective} era, Dr. {noun} encountered {adjective} figures and had the chance to {verb} {adverb} with legends — " - f"only to return with a {adjective} new appreciation for the {adjective} present." - ), - } - return stories.get(choice) - - -async def handle(request, env): - body = await read_json(request) - if body is None: - return error_response("invalid JSON") - - try: - choice = int(body.get("story", 1)) - except (TypeError, ValueError): - return error_response("story must be 1, 2, or 3") - if choice not in (1, 2, 3): - return error_response("story must be 1, 2, or 3") - - def field(key): - v = body.get(key) - if not isinstance(v, str) or not v.strip(): - return None - return v.strip()[:40] - - adjective = field("adjective") - noun = field("noun") - verb = field("verb") - adverb = field("adverb") - if not all([adjective, noun, verb, adverb]): - return error_response("adjective, noun, verb, adverb required") - - story = generate_madlib(choice, adjective, noun, verb, adverb) - return json_response({"story": story}) diff --git a/src/handlers/qr.py b/src/handlers/qr.py deleted file mode 100644 index f2a5315e4..000000000 --- a/src/handlers/qr.py +++ /dev/null @@ -1,28 +0,0 @@ -import base64 -import io - -import qrcode - -from ._shared import error_response, json_response, read_json - - -async def handle(request, env): - body = await read_json(request) - if body is None: - return error_response("invalid JSON") - text = (body.get("text") or "").strip() - if not text: - return error_response("text required") - if len(text) > 1024: - return error_response("text too long (max 1024 chars)") - - q = qrcode.QRCode(version=1, box_size=10, border=4) - q.add_data(text) - q.make(fit=True) - img = q.make_image(fill_color="black", back_color="white") - - buf = io.BytesIO() - img.save(buf, format="PNG") - png_b64 = base64.b64encode(buf.getvalue()).decode("ascii") - - return json_response({"png_b64": png_b64}) diff --git a/src/handlers/rps.py b/src/handlers/rps.py deleted file mode 100644 index f83c6a108..000000000 --- a/src/handlers/rps.py +++ /dev/null @@ -1,32 +0,0 @@ -from random import randint - -from ._shared import error_response, json_response, read_json - - -CHOICES = {1: "rock", 2: "paper", 3: "scissors"} -WIN_PAIRS = {"13", "21", "32"} # player vs cpu where player wins - - -async def handle(request, env): - body = await read_json(request) - if body is None: - return error_response("invalid JSON") - - choice = str(body.get("choice", "")).lower() - player_num = next((k for k, v in CHOICES.items() if v == choice), None) - if player_num is None: - return error_response("choice must be rock|paper|scissors") - - cpu_num = randint(1, 3) - if player_num == cpu_num: - result = "draw" - elif f"{player_num}{cpu_num}" in WIN_PAIRS: - result = "win" - else: - result = "lose" - - return json_response({ - "player": CHOICES[player_num], - "cpu": CHOICES[cpu_num], - "result": result, - }) diff --git a/src/handlers/tictactoe.py b/src/handlers/tictactoe.py deleted file mode 100644 index 27824976d..000000000 --- a/src/handlers/tictactoe.py +++ /dev/null @@ -1,95 +0,0 @@ -import random - -from ._shared import error_response, json_response, read_json - - -WIN_LINES = [ - (0, 1, 2), (3, 4, 5), (6, 7, 8), # rows - (0, 3, 6), (1, 4, 7), (2, 5, 8), # columns - (0, 4, 8), (2, 4, 6), # diagonals -] - - -def winner_on(board): - for a, b, c in WIN_LINES: - if board[a] and board[a] == board[b] == board[c]: - return board[a] - return None - - -def is_full(board): - return all(cell for cell in board) - - -def free_squares(board, options): - return [i for i in options if not board[i]] - - -def computer_move(board, cpu_letter): - player_letter = "O" if cpu_letter == "X" else "X" - - # 1. win if possible - for i in range(9): - if not board[i]: - board[i] = cpu_letter - if winner_on(board) == cpu_letter: - board[i] = "" - return i - board[i] = "" - - # 2. block opponent - for i in range(9): - if not board[i]: - board[i] = player_letter - if winner_on(board) == player_letter: - board[i] = "" - return i - board[i] = "" - - # 3. corner, center, side - for group in ([0, 2, 6, 8], [4], [1, 3, 5, 7]): - free = free_squares(board, group) - if free: - return random.choice(free) - return None - - -async def handle(request, env): - body = await read_json(request) - if body is None: - return error_response("invalid JSON") - - board = body.get("board") - player = body.get("player") - move = body.get("move") - - if (not isinstance(board, list) or len(board) != 9 - or not all(c in ("", "X", "O") for c in board)): - return error_response("board must be 9-array of '', 'X', or 'O'") - if player not in ("X", "O"): - return error_response("player must be 'X' or 'O'") - if move is not None: - if not isinstance(move, int) or not (0 <= move < 9): - return error_response("move must be int 0..8") - if board[move]: - return error_response("square already taken") - board[move] = player - - win = winner_on(board) - if win: - return json_response({"board": board, "status": "win", "winner": win}) - if is_full(board): - return json_response({"board": board, "status": "draw"}) - - cpu_letter = "O" if player == "X" else "X" - cpu_idx = computer_move(board, cpu_letter) - if cpu_idx is not None: - board[cpu_idx] = cpu_letter - - win = winner_on(board) - if win: - return json_response({"board": board, "status": "win", "winner": win, "cpu_move": cpu_idx}) - if is_full(board): - return json_response({"board": board, "status": "draw", "cpu_move": cpu_idx}) - - return json_response({"board": board, "status": "ongoing", "cpu_move": cpu_idx}) diff --git a/src/worker.py b/src/worker.py deleted file mode 100644 index 813b36061..000000000 --- a/src/worker.py +++ /dev/null @@ -1,27 +0,0 @@ -from urllib.parse import urlparse - -from handlers import bmi, hangman, madlibs, qr, rps, tictactoe -from handlers._shared import cors_preflight, error_response - - -ROUTES = { - "/api/bmi": bmi.handle, - "/api/rps": rps.handle, - "/api/qr": qr.handle, - "/api/madlibs": madlibs.handle, - "/api/tictactoe": tictactoe.handle, - "/api/hangman": hangman.handle, -} - - -async def on_fetch(request, env): - path = urlparse(request.url).path - - if path in ROUTES: - if request.method == "OPTIONS": - return cors_preflight() - if request.method != "POST": - return error_response("POST only", 405) - return await ROUTES[path](request, env) - - return error_response("not found", 404) diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 748dea75e..000000000 --- a/uv.lock +++ /dev/null @@ -1,108 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.12" - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[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/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" }, -] - -[[package]] -name = "pybegin-worker" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "pillow" }, - { name = "qrcode" }, -] - -[package.metadata] -requires-dist = [ - { name = "pillow" }, - { name = "qrcode", specifier = ">=7.4" }, -] - -[[package]] -name = "qrcode" -version = "8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" }, -] diff --git a/web/.gitignore b/web/.gitignore index d36cbddfb..d08ad59ea 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -32,14 +32,10 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* -# NEXT_PUBLIC_API_BASE is a public URL, not a secret — keep it in version control. -!.env.production # vercel .vercel # typescript *.tsbuildinfo -next-env.d.ts - -.env.production \ No newline at end of file +next-env.d.ts \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json deleted file mode 100644 index c5aac0592..000000000 --- a/web/package-lock.json +++ /dev/null @@ -1,1259 +0,0 @@ -{ - "name": "web", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "web", - "version": "0.1.0", - "dependencies": { - "@codemirror/lang-python": "^6.2.1", - "@codemirror/lint": "^6.9.6", - "@lezer/highlight": "^1.2.3", - "@uiw/codemirror-themes": "^4.25.9", - "@uiw/react-codemirror": "^4.25.9", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "next": "16.2.6", - "react": "19.2.4", - "react-dom": "19.2.4" - }, - "devDependencies": { - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "typescript": "^5" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@codemirror/autocomplete": { - "version": "6.20.2", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz", - "integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", - "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.6.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/lang-python": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", - "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.3.2", - "@codemirror/language": "^6.8.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.2.1", - "@lezer/python": "^1.1.4" - } - }, - "node_modules/@codemirror/language": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", - "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.5.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.9.6", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz", - "integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.42.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", - "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.37.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", - "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/theme-one-dark": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", - "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/highlight": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", - "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.6.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@lezer/common": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", - "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", - "license": "MIT" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", - "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.3.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", - "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/python": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", - "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "license": "MIT" - }, - "node_modules/@next/env": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", - "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", - "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", - "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", - "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", - "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", - "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", - "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", - "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", - "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@types/node": { - "version": "20.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", - "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.25.9", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.9.tgz", - "integrity": "sha512-QFAqr+pu6lDmNpAlecODcF49TlsrZ0bj15zPzfhiqSDl+Um3EsDLFLppixC7kFLn+rdDM2LTvVjn5CPvefpRgw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@codemirror/autocomplete": ">=6.0.0", - "@codemirror/commands": ">=6.0.0", - "@codemirror/language": ">=6.0.0", - "@codemirror/lint": ">=6.0.0", - "@codemirror/search": ">=6.0.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/view": ">=6.0.0" - } - }, - "node_modules/@uiw/codemirror-themes": { - "version": "4.25.9", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.9.tgz", - "integrity": "sha512-DAHKb/L9ELwjY4nCf/MP/mIllHOn4GQe7RR4x8AMJuNeh9nGRRoo1uPxrxMmUL/bKqe6kDmDbIZ2AlhlqyIJuw==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@codemirror/language": ">=6.0.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/view": ">=6.0.0" - } - }, - "node_modules/@uiw/react-codemirror": { - "version": "4.25.9", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.9.tgz", - "integrity": "sha512-HftqCBUYShAOH0pGi1CHP8vfm5L8fQ3+0j0VI6lQD6QpK+UBu3J7nxfEN5O/BXMilMNf9ZyFJRvRcuMMOLHMng==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.6", - "@codemirror/commands": "^6.1.0", - "@codemirror/state": "^6.1.1", - "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.25.9", - "codemirror": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.11.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/theme-one-dark": ">=6.0.0", - "@codemirror/view": ">=6.0.0", - "codemirror": ">=6.0.0", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@xterm/addon-fit": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", - "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", - "license": "MIT" - }, - "node_modules/@xterm/xterm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", - "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", - "license": "MIT", - "workspaces": [ - "addons/*" - ] - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.31", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", - "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/codemirror": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", - "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/next": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", - "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", - "license": "MIT", - "dependencies": { - "@next/env": "16.2.6", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.9.19", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.6", - "@next/swc-darwin-x64": "16.2.6", - "@next/swc-linux-arm64-gnu": "16.2.6", - "@next/swc-linux-arm64-musl": "16.2.6", - "@next/swc-linux-x64-gnu": "16.2.6", - "@next/swc-linux-x64-musl": "16.2.6", - "@next/swc-win32-arm64-msvc": "16.2.6", - "@next/swc-win32-x64-msvc": "16.2.6", - "sharp": "^0.34.5" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/style-mod": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", - "license": "MIT" - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "license": "MIT" - } - } -} diff --git a/web/package.json b/web/package.json index 68ccaec99..cbcd2bb81 100644 --- a/web/package.json +++ b/web/package.json @@ -2,10 +2,16 @@ "name": "web", "version": "0.1.0", "private": true, + "packageManager": "pnpm@10.33.4", + "engines": { + "node": ">=20" + }, "scripts": { "dev": "next dev", "build": "next build", - "start": "next start" + "start": "next start", + "catalog": "node scripts/gen-catalog.mjs", + "deploy": "next build && pnpm dlx wrangler pages deploy out --project-name=pybegin --branch=main --commit-dirty=true" }, "dependencies": { "@codemirror/lang-python": "^6.2.1", @@ -15,6 +21,8 @@ "@uiw/react-codemirror": "^4.25.9", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", + "lucide-react": "^1.16.0", + "marked": "^16.4.1", "next": "16.2.6", "react": "19.2.4", "react-dom": "19.2.4" diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 000000000..752277e7a --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,880 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + postcss@<8.5.10: '>=8.5.10' + +importers: + + .: + dependencies: + '@codemirror/lang-python': + specifier: ^6.2.1 + version: 6.2.1 + '@codemirror/lint': + specifier: ^6.9.6 + version: 6.9.6 + '@lezer/highlight': + specifier: ^1.2.3 + version: 1.2.3 + '@uiw/codemirror-themes': + specifier: ^4.25.9 + version: 4.25.9(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) + '@uiw/react-codemirror': + specifier: ^4.25.9 + version: 4.25.9(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.0)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@xterm/addon-fit': + specifier: ^0.11.0 + version: 0.11.0 + '@xterm/xterm': + specifier: ^6.0.0 + version: 6.0.0 + lucide-react: + specifier: ^1.16.0 + version: 1.16.0(react@19.2.4) + marked: + specifier: ^16.4.1 + version: 16.4.2 + next: + specifier: 16.2.6 + version: 16.2.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 19.2.4 + version: 19.2.4 + react-dom: + specifier: 19.2.4 + version: 19.2.4(react@19.2.4) + devDependencies: + '@types/node': + specifier: ^20 + version: 20.19.41 + '@types/react': + specifier: ^19 + version: 19.2.15 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.15) + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@codemirror/autocomplete@6.20.2': + resolution: {integrity: sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==} + + '@codemirror/commands@6.10.3': + resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} + + '@codemirror/lang-python@6.2.1': + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} + + '@codemirror/language@6.12.3': + resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} + + '@codemirror/lint@6.9.6': + resolution: {integrity: sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==} + + '@codemirror/search@6.7.0': + resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==} + + '@codemirror/state@6.6.0': + resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.43.0': + resolution: {integrity: sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@lezer/python@1.1.18': + resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + + '@next/env@16.2.6': + resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} + + '@next/swc-darwin-arm64@16.2.6': + resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.2.6': + resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.2.6': + resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.2.6': + resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.2.6': + resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.2.6': + resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.2.6': + resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.2.6': + resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@types/node@20.19.41': + resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + + '@uiw/codemirror-extensions-basic-setup@4.25.9': + resolution: {integrity: sha512-QFAqr+pu6lDmNpAlecODcF49TlsrZ0bj15zPzfhiqSDl+Um3EsDLFLppixC7kFLn+rdDM2LTvVjn5CPvefpRgw==} + peerDependencies: + '@codemirror/autocomplete': '>=6.0.0' + '@codemirror/commands': '>=6.0.0' + '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' + '@codemirror/search': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + + '@uiw/codemirror-themes@4.25.9': + resolution: {integrity: sha512-DAHKb/L9ELwjY4nCf/MP/mIllHOn4GQe7RR4x8AMJuNeh9nGRRoo1uPxrxMmUL/bKqe6kDmDbIZ2AlhlqyIJuw==} + peerDependencies: + '@codemirror/language': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + + '@uiw/react-codemirror@4.25.9': + resolution: {integrity: sha512-HftqCBUYShAOH0pGi1CHP8vfm5L8fQ3+0j0VI6lQD6QpK+UBu3J7nxfEN5O/BXMilMNf9ZyFJRvRcuMMOLHMng==} + peerDependencies: + '@babel/runtime': '>=7.11.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' + react: '>=17.0.0' + react-dom: '>=17.0.0' + + '@xterm/addon-fit@0.11.0': + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} + + '@xterm/xterm@6.0.0': + resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} + + baseline-browser-mapping@2.10.31: + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + lucide-react@1.16.0: + resolution: {integrity: sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@16.2.6: + resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + +snapshots: + + '@babel/runtime@7.29.2': {} + + '@codemirror/autocomplete@6.20.2': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.10.3': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + + '@codemirror/lang-python@6.2.1': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.2 + '@lezer/python': 1.1.18 + + '@codemirror/language@6.12.3': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.6': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + crelt: 1.0.6 + + '@codemirror/search@6.7.0': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + crelt: 1.0.6 + + '@codemirror/state@6.6.0': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.43.0': + dependencies: + '@codemirror/state': 6.6.0 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/python@1.1.18': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@marijn/find-cluster-break@1.0.2': {} + + '@next/env@16.2.6': {} + + '@next/swc-darwin-arm64@16.2.6': + optional: true + + '@next/swc-darwin-x64@16.2.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.2.6': + optional: true + + '@next/swc-linux-arm64-musl@16.2.6': + optional: true + + '@next/swc-linux-x64-gnu@16.2.6': + optional: true + + '@next/swc-linux-x64-musl@16.2.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.2.6': + optional: true + + '@next/swc-win32-x64-msvc@16.2.6': + optional: true + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@types/node@20.19.41': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.2.3(@types/react@19.2.15)': + dependencies: + '@types/react': 19.2.15 + + '@types/react@19.2.15': + dependencies: + csstype: 3.2.3 + + '@uiw/codemirror-extensions-basic-setup@4.25.9(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/commands': 6.10.3 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/search': 6.7.0 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + + '@uiw/codemirror-themes@4.25.9(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + + '@uiw/react-codemirror@4.25.9(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.0)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.29.2 + '@codemirror/commands': 6.10.3 + '@codemirror/state': 6.6.0 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.43.0 + '@uiw/codemirror-extensions-basic-setup': 4.25.9(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) + codemirror: 6.0.2 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + transitivePeerDependencies: + - '@codemirror/autocomplete' + - '@codemirror/language' + - '@codemirror/lint' + - '@codemirror/search' + + '@xterm/addon-fit@0.11.0': {} + + '@xterm/xterm@6.0.0': {} + + baseline-browser-mapping@2.10.31: {} + + caniuse-lite@1.0.30001793: {} + + client-only@0.0.1: {} + + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/commands': 6.10.3 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/search': 6.7.0 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + + crelt@1.0.6: {} + + csstype@3.2.3: {} + + detect-libc@2.1.2: + optional: true + + lucide-react@1.16.0(react@19.2.4): + dependencies: + react: 19.2.4 + + marked@16.4.2: {} + + nanoid@3.3.12: {} + + next@16.2.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@next/env': 16.2.6 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.31 + caniuse-lite: 1.0.30001793 + postcss: 8.5.14 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + styled-jsx: 5.1.6(react@19.2.4) + optionalDependencies: + '@next/swc-darwin-arm64': 16.2.6 + '@next/swc-darwin-x64': 16.2.6 + '@next/swc-linux-arm64-gnu': 16.2.6 + '@next/swc-linux-arm64-musl': 16.2.6 + '@next/swc-linux-x64-gnu': 16.2.6 + '@next/swc-linux-x64-musl': 16.2.6 + '@next/swc-win32-arm64-msvc': 16.2.6 + '@next/swc-win32-x64-msvc': 16.2.6 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + picocolors@1.1.1: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react@19.2.4: {} + + scheduler@0.27.0: {} + + semver@7.8.0: + optional: true + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.0 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + source-map-js@1.2.1: {} + + style-mod@4.1.3: {} + + styled-jsx@5.1.6(react@19.2.4): + dependencies: + client-only: 0.0.1 + react: 19.2.4 + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + w3c-keyname@2.2.8: {} diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml new file mode 100644 index 000000000..bebb01256 --- /dev/null +++ b/web/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +minimumReleaseAge: 10080 + +onlyBuiltDependencies: [] + +overrides: + postcss@<8.5.10: '>=8.5.10' diff --git a/web/public/playground/aes256.py b/web/public/playground/aes256.py new file mode 100644 index 000000000..f75ddad4b --- /dev/null +++ b/web/public/playground/aes256.py @@ -0,0 +1,93 @@ +# === AES 256 Encryption and Decryption · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @oyeanmol. + +# Import hashing and encryption libraries +import hashlib +from base64 import b64encode, b64decode +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes + + +# Encrypt plain text using a password +def encrypt(plain_text, password): + if not password: + raise ValueError("Password cannot be empty.") + + # Generate a random salt and derive a 256-bit key + salt = get_random_bytes(AES.block_size) + private_key = hashlib.scrypt( + password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32 + ) + # Create AES cipher and encrypt the message + cipher_config = AES.new(private_key, AES.MODE_GCM) + cipher_text, tag = cipher_config.encrypt_and_digest(bytes(plain_text, "utf-8")) + # Return all parts needed for decryption as base64 strings + return { + "cipher_text": b64encode(cipher_text).decode("utf-8"), + "salt": b64encode(salt).decode("utf-8"), + "nonce": b64encode(cipher_config.nonce).decode("utf-8"), + "tag": b64encode(tag).decode("utf-8"), + } + + +# Decrypt an encrypted dictionary back to plain text +def decrypt(enc_dict, password): + if not password: + raise ValueError("Password cannot be empty.") + + try: + # Decode all base64 parts from the encrypted dict + salt = b64decode(enc_dict["salt"]) + cipher_text = b64decode(enc_dict["cipher_text"]) + nonce = b64decode(enc_dict["nonce"]) + tag = b64decode(enc_dict["tag"]) + # Rebuild the key and decrypt the message + private_key = hashlib.scrypt( + password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32 + ) + cipher = AES.new(private_key, AES.MODE_GCM, nonce=nonce) + decrypted = cipher.decrypt_and_verify(cipher_text, tag) + return decrypted.decode("utf-8") + except (ValueError, KeyError) as e: + raise ValueError("Invalid encrypted message format.") from e + + +def main(): + # Show the program title + print("\t\tAES 256 Encryption and Decryption Algorithm") + print("\t\t-------------------------------------------\n\n") + # Ask the user to choose encrypt or decrypt + x = input("Enter 1 to encrypt and 2 to decrypt: ") + if x == "1": + # Gather password and message, then encrypt + password = input("Enter the Password: ") + secret_mssg = input("\nEnter the Secret Message: ") + + encrypted = encrypt(secret_mssg, password) + # Print each part of the encrypted result + print("\n\nEncrypted:") + print("---------------\n") + for k, v in encrypted.items(): + print(f"{k}: {v}") + + elif x == "2": + try: + # Collect all encrypted parts from the user + encrypted = {} + encrypted["cipher_text"] = input("Enter the cipher text: ") + encrypted["salt"] = input("Enter the salt: ") + encrypted["nonce"] = input("Enter the nonce: ") + encrypted["tag"] = input("Enter the tag: ") + password = input("Enter the password: ") + + # Decrypt and display the original message + decrypted = decrypt(encrypted, password) + print("\n\nDecrypted:") + print("-----------------\n") + print(decrypted) + except ValueError as e: + print(f"Error: {e}") + + +if __name__ == "__main__": + main() diff --git a/web/public/playground/battleship.py b/web/public/playground/battleship.py new file mode 100644 index 000000000..2073e42ff --- /dev/null +++ b/web/public/playground/battleship.py @@ -0,0 +1,140 @@ +# === Py-Battleship · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @robertlent. + +# Import random number generator +from random import randint + +# Set up the board list and default size +board = [] +size = 5 + + +# Ask the player to start a new game +def new_game(): + global size + answer = input("Do you want to start a new game of Battleship? ").lower() + + # Validate the yes/no answer + if answer not in ["yes", "y", "no", "n"]: + new_game() + else: + if answer == "yes" or answer == "y": + # Ask for board size between 5 and 15 + while True: + try: + size = int( + input( + "Enter a number between 5 and 15.\n\nThis will determine how big the playing board is and how many turns you have to find the Battleship. (5 rows, 5 columns, 5 turns, etc.): " + ) + ) + if size not in range(5, 16): + raise ValueError() + except ValueError: + print("You did not enter a number!") + continue + else: + break + + # Build the board grid with "O" cells + for x in range(0, size): + board.append(["O"] * size) + game() + elif answer == "no" or answer == "n": + print("Thank you for playing!\n") + input("Press the 'Enter' key to exit the game.") + quit() + + +# Print the current board to the screen +def print_board(board): + for row in board: + print(" ".join(row)) + + +# Pick a random row index +def random_row(board): + return randint(0, len(board) - 1) + + +# Pick a random column index +def random_col(board): + return randint(0, len(board[0]) - 1) + + +# Run the main game loop +def game(): + global board + print( + "Welcome to Battleship.\n\nA ship, one cell long, has been randomly placed on the below %dx%d grid.\nYou have %d turns to find it.\n" + % (size, size, size) + ) + + # Place the battleship at a random position + ship_row = random_row(board) + ship_col = random_col(board) + + # The next two lines are for debugging purposes. They display the position of the battleship. + # print(ship_row + 1) + # print(ship_col + 1) + + print_board(board) + + # Give the player one turn per size unit + for turn in range(size): + print("\nTurn", turn + 1, "of", size, "\n") + + # Ask for a valid row guess + while True: + try: + guess_row = int(input("Guess Row: ")) - 1 + if guess_row not in range(0, size): + raise ValueError() + except ValueError: + print("Enter a valid selection!") + continue + else: + break + + # Ask for a valid column guess + while True: + try: + guess_col = int(input("Guess Column: ")) - 1 + if guess_col not in range(0, size): + raise ValueError() + except ValueError: + print("Enter a valid selection!") + continue + else: + break + + # Check if the player found the ship + if guess_row == ship_row and guess_col == ship_col: + input( + "Congratulations! You sank my battleship!\n\nPress enter to continue." + ) + board = [] + new_game() + break + else: + # Handle out-of-bounds guess + if (guess_row > size or guess_row < 0) or ( + guess_col > size or guess_col < 0 + ): + print("Oops, that's not even in the ocean.") + # Handle repeated guess + elif board[guess_row][guess_col] == "X": + print("You guessed that one already. Good job, you wasted a turn.\n") + print_board(board) + else: + # Mark the miss and show the board + print("Miss!\n") + board[guess_row][guess_col] = "X" + print_board(board) + else: + # All turns used — game over + input("\nGame Over\n\nPress enter to continue.") + board = [] + new_game() + + +new_game() diff --git a/web/public/playground/bigram-autocomplete.py b/web/public/playground/bigram-autocomplete.py new file mode 100644 index 000000000..a5b68711f --- /dev/null +++ b/web/public/playground/bigram-autocomplete.py @@ -0,0 +1,56 @@ +# === Bigram Autocomplete · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @HridayAg0102. + +# Import regular expressions library +import re + +# Read the training text from the user +training_corpus = input("""Insert the training corpus here (can be multiline)""") + + +# Clean text and split it into tokens +def text_clean_and_tokenize(training_corpus): + # Lowercase and split into lines + text = training_corpus.lower().split("\n") + # Remove punctuation from each line + text = [re.sub("[^a-zA-Z0-9 -]", "", i) for i in text] + # Wrap each line with start/end markers + text = [" " + i + " " for i in text] + # Join everything into one flat token list + tokenized_text = " ".join(text).split() + return tokenized_text + + +# Find the most likely word to follow the starting word +def autocomplete(starting_word, training_corpus): + tokenized = text_clean_and_tokenize(training_corpus) + max_freq = 0 + freq_dict = {} + # Count how often each word follows the starting word + for i in range(len(tokenized)): + if tokenized[i] == starting_word: + if tokenized[i + 1] in freq_dict: + freq_dict[tokenized[i + 1]] += 1 + else: + freq_dict[tokenized[i + 1]] = 1 + + # Pick the word with the highest count + max_probab_word = "" + for i in freq_dict: + if max_freq <= freq_dict[i]: + max_freq = freq_dict[i] + max_probab_word = i + + return max_probab_word + + +if __name__ == "__main__": + # Start with a single word from the user + word = [input("insert a word: ")] + + # Predict the next 6 words using bigrams + for i in range(6): + word.append(autocomplete(word[i], training_corpus)) + + # Print the completed sentence + print(" ".join(word)) diff --git a/web/public/playground/bitcoin-mining.py b/web/public/playground/bitcoin-mining.py new file mode 100644 index 000000000..dffee851e --- /dev/null +++ b/web/public/playground/bitcoin-mining.py @@ -0,0 +1,52 @@ +# === Bitcoin Mining · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @MonitSharma. + +# Import hashing library and timer +from hashlib import sha256 +import time + +# Set the maximum nonce attempts allowed +MAX_NONCE = 100000000000 + + +# Hash a text string using SHA-256 +def SHA256(text): + return sha256(text.encode("ascii")).hexdigest() + + +# Try nonces until the hash starts with enough zeros +def mine(block_number, transactions, previous_hash, prefix_zeros): + prefix_str = "0" * prefix_zeros + for nonce in range(MAX_NONCE): + # Build the block data string with current nonce + text = str(block_number) + transactions + previous_hash + str(nonce) + new_hash = SHA256(text) + # Return the hash if it meets the difficulty target + if new_hash.startswith(prefix_str): + print(f"Yay! Successfully mined bitcoins with nonce value:{nonce}") + return new_hash + + raise BaseException(f"Couldn't find correct has after trying {MAX_NONCE} times") + + +if __name__ == "__main__": + # Define sample transactions for the block + transactions = """ + Player1->Player2->200, + Player3->Player4->450 + """ + # Set mining difficulty (more zeros = harder) + difficulty = 6 + + # Time how long mining takes + start = time.time() + print("start mining") + new_hash = mine( + 5, + transactions, + "0000000xa036944e29568d0cff17edbe038f81208fecf9a66be9a2b8321c6ec7", + difficulty, + ) + total_time = str((time.time() - start)) + print(f"end mining. Mining took: {total_time} seconds") + print(new_hash) diff --git a/web/public/playground/blackjack.py b/web/public/playground/blackjack.py new file mode 100644 index 000000000..350343f7c --- /dev/null +++ b/web/public/playground/blackjack.py @@ -0,0 +1,328 @@ +# === BLACKJACK · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ChiragAgg5k. + +# Import random for shuffling +import random + +# Define card suits, ranks, and point values +suits = ("Hearts", "Diamonds", "Spades", "Clubs") +ranks = ( + "Two", + "Three", + "Four", + "Five", + "Six", + "Seven", + "Eight", + "Nine", + "Ten", + "Jack", + "Queen", + "King", + "Ace", +) +values = { + "Two": 2, + "Three": 3, + "Four": 4, + "Five": 5, + "Six": 6, + "Seven": 7, + "Eight": 8, + "Nine": 9, + "Ten": 10, + "Jack": 10, + "Queen": 10, + "King": 10, + "Ace": 11, +} +# Track player chips and game state +chips = 100 +game_num = 0 +game_on = True + + +# Represent a single playing card +class Card: + def __init__(self, suit, rank): + self.suit = suit + self.rank = rank + self.value = values[rank] + + def __str__(self): + return self.rank + " of " + self.suit + + +# Represent a full 52-card deck +class Deck: + def __init__(self): + self.all_cards = [] + # Build every suit-rank combination + for suit in suits: + for rank in ranks: + self.all_cards.append(Card(suit, rank)) + + def shuffle(self): + random.shuffle(self.all_cards) + + def deal_one(self): + # Remove and return the top card + return self.all_cards.pop() + + +# Let the player choose Ace value (1 or 11) +def check_ace(card): + if card.rank == "Ace": + while True: + ace_val = int( + input("\nWhat value do you want to consider for Ace (1/11)? :") + ) + + if ace_val == 1: + values["Ace"] = 1 + + break + elif ace_val == 11: + values["Ace"] = 11 + + break + else: + print("choose valid value.") + continue + + +# Clear the terminal and show the title banner +print("\n" * 100) + +print( + """ +██████╗░██╗░░░░░░█████╗░░█████╗░██╗░░██╗░░░░░██╗░█████╗░░█████╗░██╗░░██╗ +██╔══██╗██║░░░░░██╔══██╗██╔══██╗██║░██╔╝░░░░░██║██╔══██╗██╔══██╗██║░██╔╝ +██████╦╝██║░░░░░███████║██║░░╚═╝█████═╝░░░░░░██║███████║██║░░╚═╝█████═╝░ +██╔══██╗██║░░░░░██╔══██║██║░░██╗██╔═██╗░██╗░░██║██╔══██║██║░░██╗██╔═██╗░ +██████╦╝███████╗██║░░██║╚█████╔╝██║░╚██╗╚█████╔╝██║░░██║╚█████╔╝██║░╚██╗ +╚═════╝░╚══════╝╚═╝░░╚═╝░╚════╝░╚═╝░░╚═╝░╚════╝░╚═╝░░╚═╝░╚════╝░╚═╝░░╚═╝""" +) + +# Print game rules for the player +print( + """ +BlackJack is very popular card game mainly played in casinos around the world. +Let's imagine this program as a virtual casino with computer as the Dealer. +The purpose of this game is to beat the Dealer, which can be done in various ways. + +--------------------------------------------------------------------------------------------------------------- +Both the player and the dealer are given 2 cards at the beginning , but one of the dealer's card is kept hidden. + +Each card holds a certain value. +Numbered cards contain value identical to their number. +All face cards hold a value of 10 +Ace can either hold a value of 1 or 11 depending on the situation. + +BlackJack means 21. Whoever gets a total value of 21 with their cards immediately wins! +(winning through blackjack results in 3x the money) +If the value of cards goes over 21, it's called a BUST, which results in an immediate loss... +If both the players get the same value of cards , it's a TIE and the bet money is returned. + +If none of the above cases are met ,the person with closer value to 21 wins. +(winning like this returns 2x the bet money) +--------------------------------------------------------------------------------------------------------------- + +Let the game begin!""" +) + +# Main game loop — runs each round +while game_on: + try: + # Create and shuffle a fresh deck each round + new_deck = Deck() + new_deck.shuffle() + + game_num += 1 + print(f"\nGame Round number : {game_num}") + print(f"Chips remaining = {chips}") + + # Ask player how many chips to bet + while True: + bet = int(input("\nEnter the amount of chips you want to bet:")) + if bet > chips: + print("You dont have enough chips.") + print("Enter a valid amount. \n") + elif bet <= 0: + print("Invalid Bet") + else: + chips -= bet + break + + # Deal two cards each to player and dealer + player_table_cards = [] + dealer_table_cards = [] + + [player_table_cards.append(new_deck.deal_one()) for i in range(2)] + [dealer_table_cards.append(new_deck.deal_one()) for i in range(2)] + + # Show player cards; hide one dealer card + print(f"\nPlayer cards are {player_table_cards[0]} and {player_table_cards[1]}") + print(f"Dealer cards are {dealer_table_cards[0]} and Hidden.") + + # Check player's starting cards for Ace + check_ace(player_table_cards[0]) + check_ace(player_table_cards[1]) + + # Let the player hit or stand + while True: + hit_or_stand = input("Do you want to hit or stand? :").lower() + + if hit_or_stand == "hit": + # Deal one more card to the player + player_table_cards.append(new_deck.deal_one()) + check_ace(player_table_cards[-1]) + + print(f"\nThe player hits card : {player_table_cards[-1]}") + + print("\nPlayer's hand :") + [print(i) for i in player_table_cards] + print() + + player_cards_val = 0 + + for i in player_table_cards: + player_cards_val += i.value + + # Stop if player hits 21 or busts + if player_cards_val == 21: + print("You got a blackjack!") + break + + elif player_cards_val < 21: + continue + + else: + print("YOU BUSTED!") + break + + elif hit_or_stand == "stand": + # Calculate player total on stand + player_cards_val = 0 + for i in player_table_cards: + player_cards_val += i.value + + print("\nPlayer has decided to stand.") + + print("\nPlayer's hand:") + [print(i) for i in player_table_cards] + print() + + if player_cards_val == 21: + print("Player got a blackjack!") + break + + break + + else: + print("Enter a valid option. \n") + continue + + # Dealer draws until reaching at least 17 + no_of_hits = 0 + + while True: + dealer_cards_val = 0 + + # Recalculate dealer total each loop + for i in dealer_table_cards: + dealer_cards_val += i.value + + if dealer_cards_val < 17: + no_of_hits += 1 + dealer_table_cards.append(new_deck.deal_one()) + continue + + elif 17 <= dealer_cards_val < 21: + print(f"The Dealer has hit {no_of_hits} times.") + print("\nDealer's hand :") + [print(i) for i in dealer_table_cards] + break + + elif dealer_cards_val == 21: + print(f"The Dealer has hit {no_of_hits} times.") + print("The Dealer got a blackjack!") + print("\nDealer's hand :") + [print(i) for i in dealer_table_cards] + break + + elif dealer_cards_val > 21: + # Dealer bust only counts if player didn't bust + if not (player_cards_val > 21): + print(f"The Dealer has hit {no_of_hits} times.") + print("The Dealer busted!") + print("\nDealer's hand :") + [print(i) for i in dealer_table_cards] + break + + else: + break + + # Decide winner and update chips + if player_cards_val > 21: + print( + "\nSince the Player busted , the round is lost.\nPlayer lost the bet money" + ) + + elif dealer_cards_val > 21: + print( + "\n Since the Dealer busted , Player won the round! \nPlayer got twice the money bet." + ) + chips += bet * 2 + + elif player_cards_val == 21: + print("\nPlayer won with a blackjack! \nPlayer got thrice the money bet.") + chips += bet * 3 + + elif 21 - player_cards_val > 21 - dealer_cards_val: + print("\nDealer won the round. \nPlayer lost the bet money") + + elif 21 - dealer_cards_val > 21 - player_cards_val: + print("\nPlayer won the round. \nPlayer got twice the money bet.") + chips += bet * 2 + + else: + # Equal distance to 21 means a tie + print("\nIt's a tie. \nBet money was returned.") + chips += bet + + # End game if player has no chips left + if chips == 0: + print("\nYou are out of chips , Game over.") + break + + else: + # Ask if the player wants another round + cont = input("Do you want to continue? (y/n) :") + check = cont.upper() + + if check == "Y": + print("\n" * 100) + + print( + """ +██████╗░██╗░░░░░░█████╗░░█████╗░██╗░░██╗░░░░░██╗░█████╗░░█████╗░██╗░░██╗ +██╔══██╗██║░░░░░██╔══██╗██╔══██╗██║░██╔╝░░░░░██║██╔══██╗██╔══██╗██║░██╔╝ +██████╦╝██║░░░░░███████║██║░░╚═╝█████═╝░░░░░░██║███████║██║░░╚═╝█████═╝░ +██╔══██╗██║░░░░░██╔══██║██║░░██╗██╔═██╗░██╗░░██║██╔══██║██║░░██╗██╔═██╗░ +██████╦╝███████╗██║░░██║╚█████╔╝██║░╚██╗╚█████╔╝██║░░██║╚█████╔╝██║░╚██╗ +╚═════╝░╚══════╝╚═╝░░╚═╝░╚════╝░╚═╝░░╚═╝░╚════╝░╚═╝░░╚═╝░╚════╝░╚═╝░░╚═╝""" + ) + + continue + + else: + print(f"\nTotal amount of chips left with the player = {chips}") + print(input("Press Enter to exit the terminal...")) + break + + except Exception as error: + # Catch errors and retry the round + print(f"Following error occurred : {error} \nPlease try again.") + game_num -= 1 + continue diff --git a/web/public/playground/blind-auction-2.py b/web/public/playground/blind-auction-2.py new file mode 100644 index 000000000..832a6f782 --- /dev/null +++ b/web/public/playground/blind-auction-2.py @@ -0,0 +1,58 @@ +# === Blind Auction · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @yogesh78026. +# Note: the project's art.py logo is inlined below so this runs as a single +# self-contained file in the browser playground. + +# ASCII-art logo (originally in art.py) +logo = ''' + ___________ + \ / + )_______( + |"""""""|_.-._,.---------.,_.-._ + | | | | | | ''-. + | |_| |_ _| |_..-' + |_______| '-' `'---------'` '-' + )"""""""( + /_________\\ + .-------------. + /_______________\\ +''' + +# Display the logo and welcome message +print(logo) +print("Welcome to Blind Auction") + +# Store all bids and a flag to end bidding +bids = {} +is_game_finished = False + + +# Find and print the highest bidder +def compare(bidding_record): + highest_bidder = 0 + winner = " " + # Loop through all bids to find the max + for bidder in bidding_record: + bid_amount = bidding_record[bidder] + if bid_amount > highest_bidder: + highest_bidder = bid_amount + winner = bidder + print(f"The winner is {winner} with the highest bid of {highest_bidder}.") + + +# Keep collecting bids until no bidders remain +while not is_game_finished: + # Get this bidder's name and amount + name = input("Enter your name: ") + bidding_amount = int(input("Enter your bidding amount: ")) + bids[name] = bidding_amount + + # Check if more bidders need to go + should_continue = input("Are there any other bidders? Type 'yes' or 'no':") + if should_continue == "no": + is_game_finished = True + # Reveal the winner when bidding ends + compare(bids) + elif should_continue == "yes": + print("Continue bidding") + # clear() diff --git a/web/public/playground/blind-auction.py b/web/public/playground/blind-auction.py new file mode 100644 index 000000000..082ff24d0 --- /dev/null +++ b/web/public/playground/blind-auction.py @@ -0,0 +1,55 @@ +# === Blind Auction · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Polqt. +# Note: the project's art.py logo is inlined below so this runs as a single +# self-contained file in the browser playground. + +# ASCII-art logo (originally in art.py) +logo = ''' + ___________ + \ / + )_______( + |"""""""|_.-._,.---------.,_.-._ + | | | | | | ''-. + | |_| |_ _| |_..-' + |_______| '-' `'---------'` '-' + )"""""""( + /_________\\ + .-------------. + /_______________\\ +''' + +# Display the logo +print(logo) + +# Store bids and a flag to end the auction +bids = {} +bidding_finished = False + + +# Find and announce the highest bidder +def find_highest_bidder(bidding_record): + highest_bid = 0 + winner = "" + # Compare each bid to find the maximum + for bidder in bidding_record: + bid_amount = bidding_record[bidder] + if bid_amount > highest_bid: + highest_bid = bid_amount + winner = bidder + print(f"The winner is {winner} with a bid of ${highest_bid}.") + + +# Collect bids until no more bidders remain +while not bidding_finished: + # Ask for the current bidder's name and amount + name = input("What is your name? ") + bid = int(input("What is your bid? $")) + bids[name] = bid + + # Decide whether to continue or reveal the winner + should_continue = input("Are there any other bidders? Type 'yes' or 'no': ").lower() + if should_continue == "no": + find_highest_bidder(bids) + bidding_finished = True + elif should_continue == "yes": + print() diff --git a/web/public/playground/bmi.py b/web/public/playground/bmi.py index 89c8f2e23..660406c16 100644 --- a/web/public/playground/bmi.py +++ b/web/public/playground/bmi.py @@ -1,11 +1,14 @@ -# BMI Calculator -# Asks for your height and weight, then tells you your Body Mass Index. +# === BMI Calculator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @sudipg4112001. +# Get the user's height and weight height = float(input("Your height in metres (e.g. 1.75): ")) weight = float(input("Your weight in kilograms (e.g. 70): ")) +# Calculate BMI using the standard formula bmi = weight / (height ** 2) +# Assign a category based on the BMI value if bmi < 18.5: category = "Underweight" elif bmi < 25: @@ -15,6 +18,7 @@ else: category = "Obese" +# Display the result and category print() print(f"Your BMI is {bmi:.1f}") print(f"Category: {category}") diff --git a/web/public/playground/caesar-cipher.py b/web/public/playground/caesar-cipher.py new file mode 100644 index 000000000..0c8e18725 --- /dev/null +++ b/web/public/playground/caesar-cipher.py @@ -0,0 +1,91 @@ +# === Caesar Cipher · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Harry830. + +# Define the alphabet used for shifting +alphabet = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", +] +# Get direction, message, and shift from user +direction = input("Type 'encode' to encrypt, type 'decode' to decrypt:\n") +text = input("Type your message:\n").lower() +shift = int(input("Type the shift number:\n")) + + +# Shift each letter forward to encrypt the message +def encrypt(text, shift): + new_word = [] + new_sentence = [] + text = text.split() + for words in text: + for chosen_letter in words: + i = 0 + # Find the letter and shift its position forward + for letter in alphabet: + if chosen_letter == letter: + new_shift = i + shift + new_shift = new_shift % 26 + position = alphabet[new_shift] + new_word.append(position) + i = i + 1 + new_word = "".join(new_word) + new_sentence.append(new_word) + new_word = [] + new_sentence = " ".join(new_sentence) + print(f"the encoded message is {new_sentence}") + + +# Shift each letter backward to decrypt the message +def decrypt(text, shift): + alphabet.reverse() + new_word = [] + new_sentence = [] + text = text.split() + for words in text: + for chosen_letter in words: + i = 0 + # Find the letter in reversed alphabet and shift + for letter in alphabet: + if chosen_letter == letter: + new_shift = i + shift + new_shift = new_shift % 26 + position = alphabet[new_shift] + new_word.append(position) + i = i + 1 + new_word = "".join(new_word) + new_sentence.append(new_word) + new_word = [] + new_sentence = " ".join(new_sentence) + print(f"the decoded message is {new_sentence}") + alphabet.reverse() + + +# Run encrypt or decrypt based on user's choice +if direction == "encode": + encrypt(text, shift) +elif direction == "decode": + decrypt(text, shift) diff --git a/web/public/playground/calculate-age.py b/web/public/playground/calculate-age.py new file mode 100644 index 000000000..7c894eaee --- /dev/null +++ b/web/public/playground/calculate-age.py @@ -0,0 +1,58 @@ +# === Calculate Your Age! · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @xlo-u. + +# Import time utilities +import time +from calendar import isleap + + +# Return True if the given year is a leap year +def judge_leap_year(year): + if isleap(year): + return True + else: + return False + + +# Return the number of days in a given month +def month_days(month, leap_year): + if month in [1, 3, 5, 7, 8, 10, 12]: + return 31 + elif month in [4, 6, 9, 11]: + return 30 + elif month == 2 and leap_year: + return 29 + elif month == 2 and (not leap_year): + return 28 + + +# Get the user's name and age, then get today's date +name = input("input your name: ") +age = input("input your age: ") +localtime = time.localtime(time.time()) + +# Calculate total months lived +year = int(age) +month = year * 12 + localtime.tm_mon +day = 0 + +# Find the birth year and the current year +begin_year = int(localtime.tm_year) - year +end_year = begin_year + year + +# Count total days across each full year lived +for y in range(begin_year, end_year): + if judge_leap_year(y): + day = day + 366 + else: + day = day + 365 + +# Add days for each completed month this year +leap_year = judge_leap_year(localtime.tm_year) +for m in range(1, localtime.tm_mon): + day = day + month_days(m, leap_year) + +# Add the remaining days of the current month +day = day + localtime.tm_mday +print("%s's age is %d years or " % (name, year), end="") +print("%d months or %d days" % (month, day)) diff --git a/web/public/playground/calendar.py b/web/public/playground/calendar.py new file mode 100644 index 000000000..ec58de2da --- /dev/null +++ b/web/public/playground/calendar.py @@ -0,0 +1,43 @@ +# === Calendar · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @snehafarkya. + +import calendar + + +# Print the calendar for a given year and month +def display_cal(year_input, month_input): + import calendar + + print(calendar.month(year_input, month_input)) + + +# Ask user for a valid year +def fetch_year(): + while True: + try: + year_input = int(input("Enter year: ")) + if year_input < 0: + raise ValueError("Year must be a positive integer") + return year_input + except ValueError: + print("Invalid input. Please enter a valid year.") + + +# Ask user for a valid month (1–12) +def fetch_month(): + while True: + try: + month_input = int(input("Enter month: ")) + if month_input < 1 or month_input > 12: + raise ValueError("Month must be between 1 and 12") + return month_input + except ValueError: + print("Invalid input. Please enter a valid month.") + + +# Get year and month from the user +year_input = fetch_year() +month_input = fetch_month() + +# Display the calendar +display_cal(year_input, month_input) diff --git a/web/public/playground/card-game.py b/web/public/playground/card-game.py new file mode 100644 index 000000000..4fbe2cad6 --- /dev/null +++ b/web/public/playground/card-game.py @@ -0,0 +1,151 @@ +# === Card Game - War · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Sayakb03. + +from random import shuffle +import re + + +# Define a single playing card with value and suit +class Card: + suits = ["spades", "hearts", "diamonds", "clubs"] + + values = [ + None, + None, + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "Jack", + "Queen", + "King", + "Ace", + ] + + # Store the card's value and suit as integers + def __init__(self, v, s): + self.value = v + self.suit = s + + # Compare this card as less than another + def __lt__(self, c2): + if self.value < c2.value: + return True + if self.value == c2.value: + if self.suit < c2.suit: + return True + else: + return False + return False + + # Compare this card as greater than another + def __gt__(self, c2): + if self.value > c2.value: + return True + if self.value == c2.value: + if self.suit > c2.suit: + return True + else: + return False + return False + + # Return a human-readable card name + def __repr__(self): + v = self.values[self.value] + " of " + self.suits[self.suit] + return v + + +# Build and shuffle a full 52-card deck +class Deck: + def __init__(self): + self.cards = [] + for i in range(2, 15): + for j in range(4): + self.cards.append(Card(i, j)) + shuffle(self.cards) + + # Remove and return the top card + def rm_card(self): + if len(self.cards) == 0: + return + return self.cards.pop() + + +# Represent a player with a name and win count +class Player: + def __init__(self, name): + self.wins = 0 + self.card = None + self.name = name + + +# Manage the overall War game logic +class Game: + # Ask for player names and set up the deck + def __init__(self): + while True: + pattern = r"\W" + name1 = input("Player 1 name: ") + name2 = input("Player 2 name: ") + str = name1 + name2 + if re.search(pattern, str) == None: + break + else: + print("Please, don't use special characters") + self.deck = Deck() + self.p1 = Player(name1) + self.p2 = Player(name2) + + # Print which player won this round + def display_winner(self, winner): + w = "{} wins this round" + w = w.format(winner) + print(w) + + # Print what each player drew + def draw(self, p1n, p1c, p2n, p2c): + d = "{} drew {} {} drew {}" + d = d.format(p1n, p1c, p2n, p2c) + print(d) + + # Run the main game loop round by round + def play_game(self): + cards = self.deck.cards + print("Beginning War!") + while len(cards) >= 2: + m = "q to quit. Any " + "key to play:" + response = input(m) + if response == "q": + break + p1c = self.deck.rm_card() + p2c = self.deck.rm_card() + p1n = self.p1.name + p2n = self.p2.name + self.draw(p1n, p1c, p2n, p2c) + if p1c > p2c: + self.p1.wins += 1 + self.display_winner(self.p1.name) + else: + self.p2.wins += 1 + self.display_winner(self.p2.name) + + win = self.winner(self.p1, self.p2) + print("War is over. {} wins".format(win)) + + # Determine the overall game winner + def winner(self, p1, p2): + if p1.wins > p2.wins: + return p1.name + if p1.wins < p2.wins: + return p2.name + return "It was a tie!" + + +# Create and start the game +game = Game() +game.play_game() diff --git a/web/public/playground/character-picture-grid.py b/web/public/playground/character-picture-grid.py new file mode 100644 index 000000000..4a2418be1 --- /dev/null +++ b/web/public/playground/character-picture-grid.py @@ -0,0 +1,35 @@ +# === Character Picture Grid · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ibra-kdbra. + +# Rotate a 2D grid 90 degrees clockwise +def rotate90(grid): + return list(zip(*grid[::-1])) + + +# Print every row of a 2D grid +def print2DGrid(grid): + for row in range(len(grid)): + for col in range(len(grid[row])): + print(grid[row][col], end="") + + print() + + +if __name__ == "__main__": + + # Define the character picture as a 2D list + grid = [ + [".", ".", ".", ".", ".", "."], + [".", "O", "O", ".", ".", "."], + ["O", "O", "O", "O", ".", "."], + ["O", "O", "O", "O", "O", "."], + [".", "O", "O", "O", "O", "O"], + ["O", "O", "O", "O", "O", "."], + ["O", "O", "O", "O", ".", "."], + [".", "O", "O", ".", ".", "."], + [".", ".", ".", ".", ".", "."], + ] + + # Rotate the grid and print the result + gridRotated = rotate90(grid) + print2DGrid(gridRotated) diff --git a/web/public/playground/coin-flip.py b/web/public/playground/coin-flip.py new file mode 100644 index 000000000..76a8131f0 --- /dev/null +++ b/web/public/playground/coin-flip.py @@ -0,0 +1,52 @@ +# === Coin Flip · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @pranavdasan. + +import random + + +# Randomly return "heads" or "tails" +def toss_coin(): + list1 = ["heads", "tails"] + return random.choice(list1) + + +# Run the coin flip game loop +def main(): + while True: + flag = False + + # Ask player to pick heads or tails + answer = input("Pick a side for the coin toss (heads/tails): ") + + # Restart loop if input is invalid + if answer.lower() not in ["heads", "tails"]: + continue + + # Flip the coin and show the result + result = toss_coin() + + print(f"You got... {result}") + + if answer.lower() == result: + print("Nice, you won the coin toss!!") + else: + print("OOF. Better luck next time.") + + # Ask if the player wants to play again + while True: + answer_y = input("Wanna play again? (yes/no): ") + if answer_y.lower() == "no" or answer_y.lower() == "n": + flag = True + break + elif answer_y.lower() == "yes" or answer_y.lower() == "y": + break + else: + continue + + # Exit the outer loop if done + if flag: + break + + +if __name__ == "__main__": + main() diff --git a/web/public/playground/collatz-conjecture.py b/web/public/playground/collatz-conjecture.py new file mode 100644 index 000000000..e8f1e8328 --- /dev/null +++ b/web/public/playground/collatz-conjecture.py @@ -0,0 +1,37 @@ +# === Collatz Conjecture Iterator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @lonelyH3b. + +# Define a class to generate the Collatz sequence +class Collatz: + # Store the starting number + def __init__(self, n): + self.n = n + + # Make the object usable in a for loop + def __iter__(self): + return self + + # Compute the next number in the sequence + def __next__(self): + if self.n == 1: + # Stop when we reach 1 + raise StopIteration + + if self.n % 2 == 0: + self.n = self.n // 2 + else: + self.n = 3 * self.n + 1 + return self.n + + +if __name__ == "__main__": + print("To generate a Collatz Sequence,") + # Ask user for a starting number and print the sequence + try: + input_number = int(input("Please enter a positive integer number: ")) + for i in Collatz(input_number): + print(i, end=" ") + except StopIteration: + print("Collatz sequence ended!") + except ValueError: + print("Input Should be an Integer!") diff --git a/web/public/playground/comma-code.py b/web/public/playground/comma-code.py new file mode 100644 index 000000000..7299e1e00 --- /dev/null +++ b/web/public/playground/comma-code.py @@ -0,0 +1,21 @@ +# === Comma Code · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ibra-kdbra. + +# Join a list into a comma-separated English string +def comma_code(items): + item_len = len(items) + + # Return empty string for empty list + if item_len == 0: + return "" + elif item_len == 1: + return items[0] + + # Join all but last, then append "and " + return ", ".join(items[:-1]) + ", and " + items[-1] + + +if __name__ == "__main__": + # Test with a sample list + spam = ["apples", "bananas", "tofu", "cats"] + print(comma_code(spam)) diff --git a/web/public/playground/countdown.py b/web/public/playground/countdown.py new file mode 100644 index 000000000..6e3b58748 --- /dev/null +++ b/web/public/playground/countdown.py @@ -0,0 +1,25 @@ +# === Countdown Timer · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @vk0812. + +import time + + +# Count down from time_sec to zero, printing each second +def countdown(time_sec): + while time_sec: + mins, secs = divmod(time_sec, 60) + timeformat = "{:02d}:{:02d}".format(mins, secs) + print(timeformat, end="\r") + time.sleep(1) + time_sec -= 1 + + print("Time Up") + + +if __name__ == "__main__": + # Ask user for seconds and start countdown + try: + t = input("Enter the time in seconds: ") + countdown(int(t)) + except: + print("Invalid input please try again...") diff --git a/web/public/playground/dice-roll-simulator.py b/web/public/playground/dice-roll-simulator.py new file mode 100644 index 000000000..c5bc9b73c --- /dev/null +++ b/web/public/playground/dice-roll-simulator.py @@ -0,0 +1,30 @@ +# === Dice Rolling Simulator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ZackeryRSmith. + +import random + + +# Roll a 6-sided die and print the result +def roll_dice(): + number = random.randint(1, 6) + print("You rolled:", number) + + +# Show menu and handle user choice +def main(): + while True: + print("1. Roll the dice") + print("2. Exit") + user_choice = input("What do you want to do? ") + + # Roll if player chose 1, exit if 2 + if user_choice == "1": + roll_dice() + elif user_choice == "2": + break + else: + print("Invalid choice. Please try again.") + + +if __name__ == "__main__": + main() diff --git a/web/public/playground/dice-simulator.py b/web/public/playground/dice-simulator.py new file mode 100644 index 000000000..f64033a9a --- /dev/null +++ b/web/public/playground/dice-simulator.py @@ -0,0 +1,75 @@ +# === Dice Simulator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Subha-5. + +import random + +# Map each face value to its ASCII art drawing +dice_drawing = { + 1: ( + " __________", + "| |", + "| 1 |", + "| ● |", + "| |", + "|__________|", + ), + 2: ( + " __________", + "| |", + "| ● |", + "| 2 |", + "| ● |", + "|__________|", + ), + 3: ( + " __________", + "| |", + "| 3 ● |", + "| ● |", + "| ● |", + "|__________|", + ), + 4: ( + " __________", + "| |", + "| ● ● |", + "| 4 |", + "| ● ● |", + "|__________|", + ), + 5: ( + " __________", + "| |", + "| ● 5 ● |", + "| ● |", + "| ● ● |", + "|__________|", + ), + 6: ( + " __________", + "| |", + "| ● ● |", + "| ● 6 ● |", + "| ● ● |", + "|__________|", + ), +} + + +# Ask user to roll and keep rolling until they quit +def roll_dice(): + roll = input("Roll the dice? (y/n) : ") + + while roll.lower() == "y".lower(): + # Roll two dice and show their drawings + dice1 = random.randint(1, 6) + dice2 = random.randint(1, 6) + + print("dice rolled {} and {}".format(dice1, dice2)) + print("\n".join(dice_drawing[dice1])) + print("\n".join(dice_drawing[dice2])) + + roll = input("\nRoll again? (y/n): ") + + +roll_dice() diff --git a/web/public/playground/dnd-dice.py b/web/public/playground/dnd-dice.py new file mode 100644 index 000000000..a43034280 --- /dev/null +++ b/web/public/playground/dnd-dice.py @@ -0,0 +1,34 @@ +# === DnD Dice · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Ramisky. + +import random + +# Ask user which type of dice to roll +dice_type = int(input("Pick a Dice type: \n1. D4\n2. D6\n3. D8\n4. D10\n5. D12\n6. D20\n7. D100\n\n")) + +# Roll the chosen dice type and print the result +match dice_type: + case 1: + number = random.randint(1, 4) + print(number) + case 2: + number = random.randint(1, 6) + print(number) + case 3: + number = random.randint(1, 8) + print(number) + case 4: + number = random.randint(1, 10) + print(number) + case 5: + number = random.randint(1, 12) + print(number) + case 6: + number = random.randint(1, 20) + print(number) + case 7: + number = random.randint(1, 100) + print(number) + case _: + print("Not a valid option, Adios <3") + exit() diff --git a/web/public/playground/fantasy-game-inventory.py b/web/public/playground/fantasy-game-inventory.py new file mode 100644 index 000000000..bfbe02ed5 --- /dev/null +++ b/web/public/playground/fantasy-game-inventory.py @@ -0,0 +1,38 @@ +# === Fantasy Game Inventory · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ibra-kdbra. + +# Print each item and count in inventory +def displayInventory(inventory): + print("Inventory:") + item_total = 0 + + # Loop over every item and add to total + for k, v in inventory.items(): + print(v, " ", k) + item_total += v + + print("Total number of items: " + str(item_total)) + + +# Add a list of new items to the inventory dict +def addToInventory(inventory, addedItems): + updatedInventory = dict(inventory) + # Count each item from the loot list + for item in addedItems: + updatedInventory.setdefault(item, 0) + updatedInventory[item] += 1 + + return updatedInventory + + +if __name__ == "__main__": + + # Show an existing inventory + stuff = {"rope": 1, "torch": 6, "gold coin": 42, "dagger": 1, "arrow": 12} + displayInventory(stuff) + + # Add dragon loot to a smaller inventory and display + inv = {"gold coin": 42, "rope": 1} + dragonLoot = ["gold coin", "dagger", "gold coin", "gold coin", "ruby"] + inv = addToInventory(inv, dragonLoot) + displayInventory(inv) diff --git a/web/public/playground/game-of-cricket.py b/web/public/playground/game-of-cricket.py new file mode 100644 index 000000000..fec5c9335 --- /dev/null +++ b/web/public/playground/game-of-cricket.py @@ -0,0 +1,126 @@ +# === Game of Cricket · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @whoisjayd. + +import random + + +# Ask the user to pick a number 1–6 +def get_user_choice(): + while True: + try: + user_choice = int(input("Choose any number from 1 to 6: ")) + if 1 <= user_choice <= 6: + return user_choice + else: + print("Please choose a number from 1 to 6.") + except ValueError: + print("Invalid input. Please enter a number between 1 and 6.") + + +# Simulate one full innings for a player +def play_innings(player, overs, max_wickets): + runs = 0 + wickets = 0 + balls = 0 + + print(f"\n{player}'s Innings Begins") + + # Keep playing until wickets or overs are used up + while wickets < max_wickets and balls < overs * 6: + try: + user_choice = get_user_choice() + computer_choice = random.randint(1, 6) + + print(f"Your choice: {user_choice}\nComputer's choice: {computer_choice}") + + # Same number means a wicket falls + if user_choice == computer_choice: + wickets += 1 + else: + runs += user_choice + + balls += 1 + + # Print over summary every 6 balls + if balls % 6 == 0: + over_number = balls // 6 + print(f"End of Over {over_number}") + print( + f"Over {over_number} Summary: {player} scored {runs} runs with {wickets} wickets." + ) + + print(f"Total Score: {runs}/{wickets}") + print(f"Balls remaining: {overs * 6 - balls}") + + except KeyboardInterrupt: + print("\nGame interrupted. Exiting.") + exit() + except Exception as e: + print(f"An error occurred: {e}") + print("Please try again.") + + # Show final innings result + print("\nEnd of Innings") + print(f"Final Score for {player}:\nRuns = {runs}\nWickets = {wickets}") + + return runs, wickets + + +# Welcome message and rules +print("~ Welcome to the Game of Cricket ~") + +print("\nInstructions:") +print("1. You have to select any number from 1 to 6.") +print("2. The computer will also select a number.") +print( + "3. While batting, if your number and computer's number are different, you'll add to your runs." +) +print(" If they are the same, you'll lose a wicket.") +print( + "4. While bowling, if your number and computer's number are different, the computer adds to its runs." +) +print(" If they are the same, the computer loses a wicket.") +print( + "5. Each player will get 2 wickets and 2 overs (12 balls) for batting and bowling." +) +print("6. The innings will end after either three wickets fall or the overs end.") +print("7. The player with the maximum runs wins.") + +print("\n---------- Start Game ----------") + +# Flip a coin for the toss +toss_result = random.choice(["Heads", "Tails"]) +user_toss_choice = input("Choose heads or tails: ").capitalize() +toss_winner = "User" if user_toss_choice == toss_result else "Computer" +print(f"\nToss Result: {toss_result}") +print(f"{toss_winner} won the toss") + +# Toss winner picks bat or bowl +if toss_winner == "User": + user_choice = input("Choose to bat or bowl: ").lower() + computer_choice = "bowl" if user_choice == "bat" else "bat" +else: + computer_choice = random.choice(["bat", "bowl"]) + user_choice = "bowl" if computer_choice == "bat" else "bat" + +print( + f"{toss_winner} chose to {user_choice} and the computer chose to {computer_choice}" +) + +# Play both innings +user_runs, user_wickets = play_innings("You", 2, 2) +computer_runs, computer_wickets = play_innings("Computer", 2, 2) + +# Display the final match result +print("\n~~~~~~~~~~ Result ~~~~~~~~~~") +print(f"Your total runs: {user_runs}") +print(f"Computer's total runs: {computer_runs}") + +if user_runs > computer_runs: + print(f"Congratulations! You won the Match by {user_runs - computer_runs} runs.") +elif user_runs < computer_runs: + print( + f"Better luck next time! The Computer won the Match by {computer_runs - user_runs} runs." + ) +else: + print("The Match is a Tie. No one Wins.") diff --git a/web/public/playground/game-snake-water-gun.py b/web/public/playground/game-snake-water-gun.py new file mode 100644 index 000000000..49e104b14 --- /dev/null +++ b/web/public/playground/game-snake-water-gun.py @@ -0,0 +1,123 @@ +# === Snake Water and Gun · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Yashparwal1. + +import random + +# Show game title and rules +print("GAME NAME - Snake, Water and Gun") +print( + "GAME RULES :\n[ + ]. Choose s for Snake, w for Water and g for Gun\n[ + ].If any game draws, it'll be counted\n[ + ]. If you pressed other than s, w and g key, it'll be counted." +) +print( + "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-" +) +ready = input( + "So, Be ready the game is starting\n=========Press Enter to start=========" +) + +# Set up scores and game counter +totalchances = 3 +user_score = 0 +computer_score = 0 +gameno = 0 + +lst = ["s", "w", "g"] +print("[ s ] - Snake\n[ w ] - Water\n[ g ] - Gun\n") + +# Repeat for each round of the game +while gameno < totalchances: + gameno = gameno + 1 + print(f"=========Game {gameno} is starting=========") + + # Get player and computer choices + user = input("CHOOSE WISELY:= ") + computer = random.choice(lst) + + # Check all win/draw/lose combinations + if user == "s" and computer == "s": + print(f"Your Choice = {user} and Computer Choice = {computer}") + print("!!! Match Draw !!!") + print(f"===> Youe Score: {user_score}") + print(f"===> Computer Score: {computer_score}") + + elif user == "s" and computer == "w": + user_score = user_score + 1 + print(f"Your Choice = {user} and Computer choice = {computer}") + print("!!! You WON !!!") + print(f"===> Youe Score: {user_score}") + print(f"===> Computer Score: {computer_score}") + + elif user == "s" and computer == "g": + computer_score = computer_score + 1 + print(f"Your Choice = {user} and Computer choice = {computer}") + print("!!! Computer WON !!!") + print(f"===> Youe Score: {user_score}") + print(f"===> Computer Score: {computer_score}") + + elif user == "w" and computer == "w": + print(f"Your Choice = {user} and Computer Choice = {computer}") + print("!!! Match Draw !!!") + print(f"===> Youe Score: {user_score}") + print(f"===> Computer Score: {computer_score}") + + elif user == "w" and computer == "s": + computer_score = computer_score + 1 + print(f"Your Choice = {user} and Computer Choice = {computer}") + print("!!! Computer WON !!!") + print(f"===> Youe Score: {user_score}") + print(f"===> Computer Score: {computer_score}") + + elif user == "w" and computer == "g": + user_score = user_score + 1 + print(f"Your Choice = {user} and Computer Choice = {computer}") + print("!!! You WON !!!") + print(f"===> Youe Score: {user_score}") + print(f"===> Computer Score: {computer_score}") + + elif user == "g" and computer == "g": + print(f"Your Choice = {user} and Computer Choice = {computer}") + print("!!! Match Draw !!!") + print(f"===> Youe Score: {user_score}") + print(f"===> Computer Score: {computer_score}") + + elif user == "g" and computer == "s": + user_score = user_score + 1 + print(f"Your Choice = {user} and Computer Choice = {computer}") + print("!!! You WON !!!") + print(f"===> Youe Score: {user_score}") + print(f"===> Computer Score: {computer_score}") + + elif user == "g" and computer == "w": + computer_score = computer_score + 1 + print(f"Your Choice = {user} and Computer Choice = {computer}") + print("!!! Computer WON !!!") + print(f"===> Youe Score: {user_score}") + print(f"===> Computer Score: {computer_score}") + + else: + print("(*)---Please choose correct option---(*)") + +# Show final scores and series winner +print("|*|*|*|*|*|*|*| GAME OVER |*|*|*|*|*|*|*|") +print( + "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-" +) +print( + f"Your Total Score is : {user_score} and Computer's Total Score is : {computer_score}" +) +if user_score > computer_score: + print( + "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-" + ) + print( + "Hurrey!!! You Won the Series\nHere is a gift for you.... ❤❤❤🎂🎂🎂\nSee you next time.Bye......" + ) +else: + print( + "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-" + ) + print( + "Sorry!!! You lose the series\nDon't worry you'll still get a gift.... 😝LOL😝 Come next time " + ) + +out = input("<=== Press any key to exit ===>") diff --git a/web/public/playground/gpa-calculator.py b/web/public/playground/gpa-calculator.py new file mode 100644 index 000000000..9f3766aa4 --- /dev/null +++ b/web/public/playground/gpa-calculator.py @@ -0,0 +1,68 @@ +# === GPA Calculator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @nilay-banerjee. + +import math + + +# Compute GPA from course marks and credits +def calculate_gpa(num_courses, marks): + # Start credit accumulators at zero + credits = 0 + total_credits = 0 + + # Collect details for each course + for i in range(num_courses): + # Ask for course name and its credit value + course_name = input("Enter Course Name: ") + course_credits = int(input(f"Enter Total Credits Of {course_name}: ")) + + # Either enter total marks or split marks + if marks == 0: + course_marks = int(input(f"Enter Total Marks Acquired for {course_name}: ")) + else: + mid_sem_marks = int(input(f"Enter Your Mid Sem Marks for {course_name}: ")) + internal_marks = int( + input(f"Enter Your Internal Marks for {course_name}: ") + ) + end_sem_marks = int(input(f"Enter Your End Sem Marks for {course_name}: ")) + course_marks = mid_sem_marks + internal_marks + end_sem_marks + + print() + + # Validate marks are within 0–100 range + if course_marks > 100: + print("Enter Valid Marks") + credits = 0 + total_credits = 1 + break + elif course_marks < 100: + course_marks += 1 + + # Convert marks to grade points using ceiling + course_point = math.ceil(course_marks / 10) + + # Add weighted points to running totals + credits += course_point * course_credits + total_credits += course_credits + + # Divide total weighted points by total credits + gpa = credits / total_credits + return gpa + + +if __name__ == "__main__": + print("\n------------------------------------------") + print("GPA Calculator: ") + print("------------------------------------------") + + # Get number of courses and mark-entry preference + num_courses = int(input("Enter Number Of Courses: ")) + marks = int( + input( + "Enter 1 to individually enter Mid Sem, Internals & End Sem Marks\nElse Enter 0 to enter Total Marks: " + ) + ) + + # Calculate and print the semester GPA + gpa = calculate_gpa(num_courses, marks) + print("Your Credits for this Semester is:", gpa) diff --git a/web/public/playground/guess-the-word.py b/web/public/playground/guess-the-word.py new file mode 100644 index 000000000..cb5d27194 --- /dev/null +++ b/web/public/playground/guess-the-word.py @@ -0,0 +1,85 @@ +# === Guess The Word · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @smw-1211. + +import random + +# All possible words the game can pick +word_list = [ + "python", + "java", + "javascript", + "ruby", + "php", + "html", + "css", + "csharp", + "angular", + "golang", + "c", + "dotnet", + "perl", + "rust", + "scala", + "dart", + "fortran", + "cobol", + "haskell", +] + + +# Pick one random word from the list +def choose_random_word(word_list): + return random.choice(word_list) + + +# Run the full word-guessing game loop +def word_guessing_game(): + word_to_guess = choose_random_word(word_list) + guessed_letters = [] + attempts = 6 + + # Greet the player and show blank spaces + print("Welcome to the Word Guessing Game!") + print("You have 6 attempts to guess the word.") + print("_ " * len(word_to_guess)) + + # Keep looping while attempts remain + while attempts > 0: + guess = input("Guess a letter: ").lower() + + # Reject non-single-letter inputs + if len(guess) != 1 or not guess.isalpha(): + print("Please enter a single letter.") + continue + + # Reject already-guessed letters + if guess in guessed_letters: + print("You've already guessed that letter.") + continue + + guessed_letters.append(guess) + + # Check if the guessed letter is in the word + if guess in word_to_guess: + print("Correct guess!") + remaining_letters = [ + letter if letter in guessed_letters else "_" for letter in word_to_guess + ] + print(" ".join(remaining_letters)) + # Win if no blanks left + if "_" not in remaining_letters: + print("Congratulations! You've guessed the word:", word_to_guess) + break + else: + attempts -= 1 + if attempts < 3: + print(f"It is a name of a programming language") + print(f"Wrong guess. You have {attempts} attempts remaining.") + + # Out of attempts — reveal the word + if attempts == 0: + print("You've run out of attempts. The word was:", word_to_guess) + + +# Start the game +word_guessing_game() diff --git a/web/public/playground/handcricket.py b/web/public/playground/handcricket.py new file mode 100644 index 000000000..0113eed46 --- /dev/null +++ b/web/public/playground/handcricket.py @@ -0,0 +1,170 @@ +# === Hand Cricket Game · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Mrinank-Bhowmick. + +import random +import time + + +# Entry point — sets up game and calls helpers +def main(): + print("Welcome Hand Cricket") + print("You will be playing against another player") + try: + overs = int(input("Enter the number of overs (1-10): ")) + + # Run the coin toss to decide who bats first + toss_winner = toss() + if toss_winner == 1: + print("Player 1 won the toss!") + player1_choice = input("Player 1, choose 1 to bat first, 2 to bowl first: ") + player2_choice = "1" if player1_choice == "2" else "2" + else: + print("Player 2 won the toss!") + player2_choice = input("Player 2, choose 1 to bat first, 2 to bowl first: ") + player1_choice = "1" if player2_choice == "2" else "2" + + difficulty = int(input("Select difficulty level (1-Easy, 2-Medium, 3-Hard): ")) + + # Play the match and collect final scores + player1_score, player2_score = play_game( + overs, player1_choice, player2_choice, difficulty + ) + + # Announce the winner + who_won(player1_score, player2_score) + except ValueError: + print("Invalid input, exiting game") + + +# Simulate a coin toss for both players +def toss(): + print("Toss time!") + user_choice = input("Choose heads (1) or tails (2): ") + toss_result = random.randint(1, 2) + if int(user_choice) == toss_result: + print("It's", "Heads!" if toss_result == 1 else "Tails!") + return 1 + else: + print("It's", "Heads!" if toss_result == 1 else "Tails!") + return 2 + + +# Run all overs for both players +def play_game(overs, player1_choice, player2_choice, difficulty=1): + player1_score = 0 + player2_score = 0 + player1_wickets = 10 + player2_wickets = 10 + + print("\nMatch Summary") + print("=============") + print(f"Overs: {overs}") + + # Loop through each over + for over in range(overs): + print( + f"\nOver {over + 1}, Player 1: {player1_wickets} wickets left, Player 2: {player2_wickets} wickets left" + ) + + if player1_choice == "1": + # Player 1 bats first + player1_score, player1_wickets = user_turn( + player1_score, player1_wickets, "1", over + ) + + # Player 2 bowls + player2_score, player2_wickets = user_turn( + player2_score, player2_wickets, "2", over + ) + else: + # Player 2 bowls first + player2_score, player2_wickets = user_turn( + player2_score, player2_wickets, "2", over + ) + + # Player 1 bats + player1_score, player1_wickets = user_turn( + player1_score, player1_wickets, "1", over + ) + + # Show scores at end of each over + display_scoreboard(player1_score, player2_score, over) + + return player1_score, player2_score + + +# Handle one player's turn ball by ball +def user_turn(player_score, player_wickets, player_choice, over): + print(f"Player's turn - {'Batting' if player_choice == '1' else 'Bowling'}") + balls = 0 + while balls < 6 and player_wickets > 0: + if player_choice == "1": + # Batting: player enters shot, computer bowls randomly + player_runs = int( + input(f"Over {over + 1}, Ball {balls + 1}: Enter your {'shot'} (1-6): ") + ) + opponent_runs = random.randint(1, 6) + else: + # Bowling: player chooses delivery, computer bats randomly + opponent_choice = input( + f"Over {over + 1}, Ball {balls + 1}: Player 2, choose 1 to bat, 2 to bowl: " + ) + player_runs = random.randint(1, 6) + opponent_runs = int( + input( + f"Over {over + 1}, Ball {balls + 1}: Enter your {'delivery'} (1-6): " + ) + ) + + print(f"You chose {player_runs}, Opponent chose {opponent_runs}") + + # Matching numbers means a wicket or an out + if player_choice == "1" and player_runs == opponent_runs: + print("Player is out!") + player_wickets -= 1 + if player_wickets > 0: + print(f"Player has {player_wickets} wickets left.") + elif ( + player_choice == "2" + and opponent_choice == "2" + and player_runs == opponent_runs + ): + print("Opponent is out!") + player_wickets -= 1 + if player_wickets > 0: + print(f"Opponent has {player_wickets} wickets left.") + else: + player_score += player_runs + print(f"Player's score is {player_score}") + balls += 1 + + return player_score, player_wickets + + +# Print the current scoreboard after an over +def display_scoreboard(player1_score, player2_score, over): + print("\nScoreboard") + print("==========") + print(f"Over {over + 1}:") + print(f"Player 1: {player1_score} runs") + print(f"Player 2: {player2_score} runs") + + +# Compare scores and declare the winner +def who_won(player1_score, player2_score): + print("\nMatch Result") + print("============") + print("Player 1's score =", player1_score) + print("Player 2's score =", player2_score) + if player1_score > player2_score: + print("Player 1 won") + elif player2_score > player1_score: + print("Player 2 won") + else: + print("The match ended in a draw") + print("Thank you for playing and have a good day :) ") + + +# Run the main function if the script is executed +if __name__ == "__main__": + main() diff --git a/web/public/playground/hangman.py b/web/public/playground/hangman.py index 3d27e76d6..6b6f7ce56 100644 --- a/web/public/playground/hangman.py +++ b/web/public/playground/hangman.py @@ -1,39 +1,47 @@ -# Hangman -# Guess the hidden word one letter at a time before you run out of tries. +# === Hangman · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Mrinank-Bhowmick. import random +# Pick a random secret word from the list words = ["python", "browser", "rocket", "puzzle", "guitar", "planet", "coffee"] word = random.choice(words) +# Track guessed letters and remaining tries guessed = set() tries = 6 +# Welcome the player and show word length print("Welcome to Hangman! Guess the word, one letter at a time.") print(f"The word has {len(word)} letters.") print() +# Keep looping while tries remain while tries > 0: shown = " ".join(letter if letter in guessed else "_" for letter in word) print("Word: ", shown) print("Tries left:", tries) + # Check if the player has won if all(letter in guessed for letter in word): print("\nYou win! 🎉 The word was:", word) break guess = input("Guess a letter: ").strip().lower() + # Reject input that is not a single letter if len(guess) != 1 or not guess.isalpha(): print("Please type a single letter.\n") continue + # Skip already-tried letters if guess in guessed: print("You already tried that one.\n") continue guessed.add(guess) + # Give feedback and deduct a try if wrong if guess in word: print("Good guess!\n") else: diff --git a/web/public/playground/higher-lower.py b/web/public/playground/higher-lower.py new file mode 100644 index 000000000..17c1baaa0 --- /dev/null +++ b/web/public/playground/higher-lower.py @@ -0,0 +1,30 @@ +# === Higher-Lower Game · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ZackeryRSmith. + +from random import randint + +# Welcome message and pick the secret number +print("Welcome to the Higher-Lower Game!") +rnum = randint(0, 100) +noguesses = 0 + +# Keep looping until the player wins +while True: + + # Keep asking until a valid integer is entered + while True: + guess = input("Guess the number: ") + if guess.isdigit(): + integer_number = int(guess) + print("You entered:", integer_number) + + # Tell the player to go higher, lower, or celebrate a win + if integer_number > rnum: + print("Lower") + elif integer_number < rnum: + print("Higher") + else: + (print("You win! the number is " + guess + "!"), quit()) + noguesses += 1 + else: + print("Invalid input. Please enter an integer number.") diff --git a/web/public/playground/inverse-matrix-calculator.py b/web/public/playground/inverse-matrix-calculator.py new file mode 100644 index 000000000..c7c4318bd --- /dev/null +++ b/web/public/playground/inverse-matrix-calculator.py @@ -0,0 +1,131 @@ +# === Inverse Matrix Calculator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @farisfaikar. + +# Ask the user for matrix size and elements +def input_matrix(): + matrix = [] + + print(">>> Input order of matrix") + row = int(input("Choose matrix rows and columns: ")) + column = row + print(f"Matrix A: {row}x{column}") + print("-" * 20) + + # Fill the matrix row by row + print(">>> Input elements of matrix") + for i in range(row): + matrix.append([]) + for j in range(column): + matrix[i].append(int(input(f"A {i + 1}, {j + 1}: "))) + print("-" * 20) + + return matrix + + +# Compute the inverse of a square matrix +def inverse_matrix(matrix): + # Stop if the matrix has no inverse + determinant = find_determinant(matrix) + if determinant == 0: + return print("Matrix is Singular! (Determinant = 0)") + + # Step 1: Build the matrix of minors + column = len(matrix) + matrix_2 = [] + for i in range(column): + matrix_2.append([]) + for j in range(column): + matrix_3 = [] + iter = 0 + for k in [x for x in range(column) if x != i]: + matrix_3.append([]) + for l_ in [y for y in range(column) if y != j]: + matrix_3[iter].append(matrix[k][l_]) + iter += 1 + matrix_2[i].append(find_determinant(matrix_3)) + + # Step 2: Apply checkerboard sign pattern for cofactors + for i in range(column): + for j in range(column): + if ((i + j) % 2) == 1: + matrix_2[i][j] *= -1 + + # Step 3: Transpose to get the adjugate matrix + matrix_3 = [] + for i in range(column): + matrix_3.append([]) + for j in range(column): + if i == j: + matrix_3[i].append(matrix_2[i][j]) + else: + matrix_3[i].append(matrix_2[j][i]) + + # Step 4: Divide every element by the determinant + for i in range(column): + for j in range(column): + matrix_3[i][j] /= determinant + + return matrix_3 + + +# Recursively compute the determinant of a matrix +def find_determinant(matrix): + column = len(matrix) + if column > 2: + final_determinant = 0 + for i in range(column): + matrix_2 = [] + iter = 0 + for j in [x for x in range(column) if x != 0]: + matrix_2.append([]) + for k in [y for y in range(column) if y != i]: + matrix_2[iter].append(matrix[j][k]) + iter += 1 + determinant = find_determinant(matrix_2) + if (i % 2) == 0: + final_determinant += matrix[0][i] * determinant + else: + final_determinant -= matrix[0][i] * determinant + + return final_determinant + elif column == 1: + return matrix[0][0] + else: + # Base case: 2x2 determinant formula + a = matrix[0][0] + b = matrix[0][1] + c = matrix[1][0] + d = matrix[1][1] + determinant = (a * d) - (b * c) + return determinant + + +# Print a matrix with a label +def print_matrix(matrix, text=""): + column = len(matrix) + print(f"{text}") + for i in range(column): + print("\t[", end=" ") + for j in range(column): + print(f"{round(matrix[i][j], 3)}", end=" ") + print("]\n", end="") + + +# Main loop — keep computing until user exits +def main(): + print("===== Inverse Matrix Calculator =====") + while True: + matrix = input_matrix() + inv_matrix = inverse_matrix(matrix) + print(">>> Calculation Results") + print_matrix(matrix, "A:") + print_matrix(inv_matrix, "A^-1:") + print("-" * 20) + pilihan = input(">>> Would you like to make another calculation? (y): ") + if pilihan.lower() != "y": + print("Thank you for using this program :)") + break + + +if __name__ == "__main__": + main() diff --git a/web/public/playground/ipv4-calculator-main.py b/web/public/playground/ipv4-calculator-main.py new file mode 100644 index 000000000..635d1a868 --- /dev/null +++ b/web/public/playground/ipv4-calculator-main.py @@ -0,0 +1,207 @@ +# === IPv4 Calculator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @bim22614. + +# Build an IP string with its prefix notation +def pr(name, mask): + return name + mask + + +# Validate that all four octets are in 0–255 +def ip_correct_check(octet): + check = octet.split(".") + counter = 0 + for i in check: + if 0 <= int(i) <= 255: + counter += 1 + else: + return False + if counter == 4: + return True + + +# Ask the user for a valid IP address +ip_addr = input("Enter IP address of host: ") + +# Keep asking until the address is valid +while True: + if ip_correct_check(ip_addr): + break + else: + ip_addr = input("Enter correct IP address of host: ") + +# Ask for the subnet prefix length +ip_prefix = int(input("Enter IP prefix: ")) + +# Validate that prefix is within 0–31 +while True: + if 0 <= ip_prefix <= 31: + break + else: + ip_prefix = input("Prefix can be in range [0, 31]: ") + + +# Determine the IP address class from first octet +ip_list_octet = ip_addr.split(".") +ip_list_octet = [int(i) for i in ip_list_octet] +class_of_ip = None + + +if int(ip_list_octet[0]) in range(0, 128): + class_of_ip = "A" +elif int(ip_list_octet[0]) in range(128, 191): + class_of_ip = "B" +elif int(ip_list_octet[0]) in range(191, 224): + class_of_ip = "C" +elif int(ip_list_octet[0]) in range(224, 240): + class_of_ip = "D" +elif int(ip_list_octet[0]) in range(240, 255): + class_of_ip = "E" + + +# Decide if the address is public or private +type_of_ip = None +if ( + (int(ip_list_octet[0]) == 10) + or (int(ip_list_octet[0]) == 172 and int(ip_list_octet[1]) in range(16, 32)) + or (int(ip_list_octet[0]) == 192 and int(ip_list_octet[1]) == 168) +): + type_of_ip = "Private" +else: + type_of_ip = "Public" + + +# Build the subnet mask in binary then decimal +mask_bin = int(ip_prefix) * "1" + (32 - int(ip_prefix)) * "0" + +mask_bin_list = [] +for i in range(0, len(mask_bin), 8): + mask_bin_list.append(mask_bin[i : i + 8]) + + +mask_dec_list = [int(("0b" + i), 2) for i in mask_bin_list] + + +# Convert a single decimal octet to 8-bit binary string +def ip_in_bin(octet): + m = str(bin(octet)) + m1 = [i for i in m] + m1 = m1[2::] + if len(m1) != 8: + for i in range(0, 8 - len(m1)): + m1.insert(0, "0") + m2 = ["".join(m1)] + return m2 + + +# Convert each IP octet to binary +bin_ip_list = [] +for i in range(len(ip_list_octet)): + bin_ip_list.insert(i, ip_in_bin(int(ip_list_octet[i]))) + bin_ip_list[i] = bin_ip_list[i][0] + + +# AND the IP and mask to get the network (subnet) address +subnet_decimal_list = [] +for i in range(4): + subnet_decimal_list.append( + int(("0b" + mask_bin_list[i]), 2) & int(("0b" + bin_ip_list[i]), 2) + ) + + +# Convert the subnet address to binary +subnet_bin_list = [] +for i in range(len(ip_list_octet)): + subnet_bin_list.insert(i, ip_in_bin(int(subnet_decimal_list[i]))) + subnet_bin_list[i] = subnet_bin_list[i][0] + +# Calculate the broadcast address in decimal +broadcast_dec = [] +for i in range(4): + if mask_dec_list[i] == 255: + broadcast_dec.append(int(ip_list_octet[i])) + elif mask_dec_list[i] == 0: + broadcast_dec.append(255) + else: + prom_broadcast_num = 256 - mask_dec_list[i] + prom_broadcast_final = 0 + while prom_broadcast_final < int(ip_list_octet[i]): + prom_broadcast_final += prom_broadcast_num + prom_broadcast_final -= 1 + broadcast_dec.append(prom_broadcast_final) + +# Convert broadcast address to binary +broadcast_bin = [] +for i in range(4): + broadcast_bin.insert(i, ip_in_bin(int(broadcast_dec[i]))) + broadcast_bin[i] = broadcast_bin[i][0] + +# Compute first and last usable host addresses +if 1 <= int(ip_prefix) <= 30: + first_av_host = subnet_decimal_list[:3] + [subnet_decimal_list[3] + 1] +else: + first_av_host = subnet_decimal_list[:3] + [subnet_decimal_list[3]] + + +last_av_host = [] +if 1 <= int(ip_prefix) <= 8: + last_av_host = [subnet_decimal_list[0] + 2 ** (8 - int(ip_prefix)) - 1] + [ + 255, + 255, + 254, + ] +elif 9 <= int(ip_prefix) <= 16: + last_av_host = ( + subnet_decimal_list[:1] + + [subnet_decimal_list[1] + 2 ** (16 - int(ip_prefix)) - 1] + + [255, 254] + ) +elif 17 <= int(ip_prefix) <= 24: + last_av_host = ( + subnet_decimal_list[:2] + + [subnet_decimal_list[2] + 2 ** (24 - int(ip_prefix)) - 1] + + [254] + ) +elif 25 <= int(ip_prefix) <= 30: + last_av_host = subnet_decimal_list[:3] + [ + subnet_decimal_list[3] + 2 ** (32 - int(ip_prefix)) - 2 + ] +elif int(ip_prefix) == 31: + last_av_host = subnet_decimal_list[:3] + [ + subnet_decimal_list[3] + 2 ** (32 - int(ip_prefix)) - 1 + ] + +# Convert first and last host addresses to binary +first_bin_list = [] +for i in range(len(ip_list_octet)): + first_bin_list.insert(i, ip_in_bin(int(first_av_host[i]))) + first_bin_list[i] = first_bin_list[i][0] + +last_bin_list = [] +for i in range(len(ip_list_octet)): + last_bin_list.insert(i, ip_in_bin(int(last_av_host[i]))) + last_bin_list[i] = last_bin_list[i][0] + +# Count total and usable host addresses +available_number_prom = 32 - ip_prefix +available_number = 2**available_number_prom + + +# Print all calculated network information +print("IP address: {}/{}".format(ip_addr, ip_prefix)) +print("Class of IP address: {}".format(class_of_ip)) +print("Address category: {}".format(type_of_ip)) +print("Host address (decimal): {}".format(ip_list_octet)) +print("Mask (decimal): {}".format(mask_dec_list)) +print("Network address (decimal): {}".format(subnet_decimal_list)) +print("Broadcast address (decimal): {}".format(broadcast_dec)) +print("First available host (decimal): {}".format(first_av_host)) +print("Last available host (decimal): {}".format(last_av_host)) + +print("Host address(binary): {}".format(bin_ip_list)) +print("Mask(binary): {}".format(mask_bin_list)) +print("Network address(binary): {}".format(subnet_bin_list)) +print("Broadcast address(binary): {}".format(broadcast_bin)) +print("First available host (binary): {}".format(first_bin_list)) +print("Last available host (binary): {}".format(last_bin_list)) +print("Total Number of Hosts: {}".format(available_number)) +print("Number of Usable Hosts: {}".format(available_number - 2)) diff --git a/web/public/playground/jokenpo.py b/web/public/playground/jokenpo.py new file mode 100644 index 000000000..b90851f48 --- /dev/null +++ b/web/public/playground/jokenpo.py @@ -0,0 +1,193 @@ +# === Jokenpo (Rock, Paper, Scissors) · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @jrbublitz. +# Note: the project's Jogadores and Partida classes are inlined below so this +# runs as a single self-contained file in the browser playground. + +from random import randint + + +# A player: holds a name and a hand of cards (1=stone, 2=paper, 3=scissors) +class Jogadores: + nome: str + cartas = [] + + def __init__(self, nome): + self.nome = nome + self.cartas = [] + + # Deal three random cards to this player + def distribuir_cartas(self) -> list: + for i in range(3): + carta = randint(1, 3) + self.cartas.append(carta) + return self.cartas + + # Turn card numbers into readable words + def traduzir_cartas(self, cartas) -> list: + cartasTraduzidas = "" + for i in cartas: + if i == 1 or i == "1": + cartasTraduzidas += "Stone " + elif i == 2 or i == "2": + cartasTraduzidas += "Paper " + else: + cartasTraduzidas += "Scissors " + + return cartasTraduzidas + + # Empty this player's hand + def limpar_cartas(self): + return self.cartas.clear() + + +# A match between two players +class Partida(Jogadores): + def __init__(self, jogador_um, jogador_dois): + self.jogador_um = jogador_um + self.jogador_dois = jogador_dois + + # Play an interactive match where you pick your cards + def jogar(self): + rodadas = [] + count_jogador1 = 0 + count_jogador2 = 0 + + # Play two rounds + for i in range(2): + print("Suas Cartas: ", self.traduzir_cartas(self.jogador_um.cartas)) + carta_jogador1 = int(input("Escolha sua carta: ")) + + # Computer picks a random card + carta_jogador2 = randint(0, len(self.jogador_dois.cartas) - 1) + + # Record this round + rodadas.append( + self.traduzir_cartas(str(self.jogador_um.cartas[carta_jogador1])) + + " x " + + self.traduzir_cartas(str(self.jogador_dois.cartas[carta_jogador2])) + ) + for rodada in rodadas: + print(rodada) + + # Decide the round winner + if self.jogador_um.cartas[carta_jogador1] == 1: + if self.jogador_dois.cartas[carta_jogador2] == 2: + count_jogador2 += 1 + else: + count_jogador1 += 1 + if self.jogador_um.cartas[carta_jogador1] == 2: + if self.jogador_dois.cartas[carta_jogador2] == 3: + count_jogador2 += 1 + else: + count_jogador1 += 1 + + if self.jogador_um.cartas[carta_jogador1] == 3: + if self.jogador_dois.cartas[carta_jogador2] == 1: + count_jogador2 += 1 + else: + count_jogador1 += 1 + + # Discard the cards that were played + self.jogador_um.cartas.pop(carta_jogador1) + self.jogador_dois.cartas.pop(carta_jogador2) + + # Announce the overall winner + if count_jogador1 == count_jogador2: + return print("Empate ") + elif count_jogador1 > count_jogador2: + return print("Jogador 1 - Venceu") + return print("Jogador 2 - Venceu") + + # Play an automatic match using the dealt cards + def simular(self): + count_jogador1 = 0 + count_jogador2 = 0 + count_Empate = 0 + + # Compare both hands card by card + for round in range(3): + if self.jogador_um.cartas[round] == self.jogador_dois.cartas[round]: + print("Round(" + str(round + 1) + ") - Empate") + count_Empate += 1 + if count_Empate == 3: + return print("Todos os rounds Empataram !") + else: + if self.jogador_um.cartas[round] == 1: + if self.jogador_dois.cartas[round] == 2: + count_jogador2 += 1 + else: + count_jogador1 += 1 + + if self.jogador_um.cartas[round] == 2: + if self.jogador_dois.cartas[round] == 3: + count_jogador2 += 1 + else: + count_jogador1 += 1 + + if self.jogador_um.cartas[round] == 3: + if self.jogador_dois.cartas[round] == 1: + count_jogador2 += 1 + else: + count_jogador1 += 1 + + # Announce the overall winner + if count_jogador1 == count_jogador2: + return print("Empate ") + + if count_jogador1 > count_jogador2: + return print("Jogador 1 - Venceu") + return print("Jogador 2 - Venceu") + + +# Define a function to play interactively +def jogar(): + print("====== JOGAR ======") + try: + # Set up the human player + jogador1 = Jogadores + jogador1.nome = input("Digite seu nome: ") + jogador1.distribuir_cartas(jogador1) + + # Set up the computer player + jogador2 = Jogadores("Computador") + jogador2.distribuir_cartas() + + # Start and run the match + partida = Partida(jogador1, jogador2) + partida.jogar() + except Exception as e: + print("Erro: " + str(e)) + + +# Define a function to simulate a game automatically +def simularJogo(): + print("====== SIMULAÇÃO ======") + try: + # Create first simulated player and deal cards + jogador1 = Jogadores("João") + jogador1.distribuir_cartas() + print(jogador1.nome, " - Cartas: ", jogador1.traduzir_cartas(jogador1.cartas)) + + # Create second simulated player and deal cards + jogador2 = Jogadores("Maria") + jogador2.distribuir_cartas() + print(jogador2.nome, " - Cartas: ", jogador2.traduzir_cartas(jogador2.cartas)) + + # Run the simulation + partida = Partida(jogador1, jogador2) + partida.simular() + except Exception as e: + print("Erro: " + str(e)) + + +# Show the menu and get the user's choice +print("Escolha um opção:\n" "1 - Jogar\n" "2 - Simular um jogo\n" "0 - Sair\n") +menu = int(input("Digite: ")) + +# Route to the right function based on choice +if menu == 1: + jogar() +elif menu == 2: + simularJogo() +else: + print("Finalizando jogo...") diff --git a/web/public/playground/loan-calculator.py b/web/public/playground/loan-calculator.py new file mode 100644 index 000000000..e92181dfb --- /dev/null +++ b/web/public/playground/loan-calculator.py @@ -0,0 +1,62 @@ +# === Loan Calculator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @rudy3333. + +# Calculate the fixed monthly payment for a loan +def calculate_loan_payment(principal, annual_interest_rate, months): + # Convert annual interest rate to monthly rate + monthly_interest_rate = annual_interest_rate / 12 / 100 + + # Calculate monthly payment + if monthly_interest_rate == 0: + monthly_payment = principal / months + else: + monthly_payment = ( + principal + * (monthly_interest_rate * (1 + monthly_interest_rate) ** months) + / ((1 + monthly_interest_rate) ** months - 1) + ) + + return monthly_payment + + +# Greet the user and gather loan details +def main(): + print("Welcome to the Loan Calculator!") + + # Get user input + principal = floatValidation("Enter the loan amount: $") + annual_interest_rate = floatValidation( + "Enter the annual interest rate (as a percentage): " + ) + months = intValidtaion("Enter the loan term (in months): ") + + # Calculate monthly payment + monthly_payment = calculate_loan_payment(principal, annual_interest_rate, months) + + # Display the result + print(f"Your monthly payment will be: ${monthly_payment:.2f}") + + +# Keep asking until the user enters a valid decimal number +def floatValidation(question): + while True: + try: + value = float(input(question)) + return value + except ValueError: + print("Input should be a valid number") + continue + + +# Keep asking until the user enters a valid integer +def intValidtaion(question): + while True: + try: + value = int(input(question)) + return value + except ValueError: + print("Input should be a valid number") + + +if __name__ == "__main__": + main() diff --git a/web/public/playground/love-calculator.py b/web/public/playground/love-calculator.py new file mode 100644 index 000000000..2450527b5 --- /dev/null +++ b/web/public/playground/love-calculator.py @@ -0,0 +1,103 @@ +# === Love Calculator · annotated for the pyBegin playground === + +import random +import time + +CONSONANTS = "bcdfghjklmnprstvwxyz" +VOWELS = "aeiou" + + +# Count how many vowels are in a name +def count_vowels(name): + # Initialize vowel_counts to 0 + vowels_count = 0 + + # Iterate over each letter in the name + for letter in name.lower(): + # Check if the letter is a vowel + if letter in VOWELS: + vowels_count += 1 + + return vowels_count + + +# Count how many consonants are in a name +def count_consonants(name): + # Initialize an empty list to store consonants + consonants_list = [] + + # Iterate over each letter in the name + for letter in name: + # Check if the letter is a consonant + if len(letter.lower()) == 1 and letter.lower() in CONSONANTS: + consonants_list.append(letter) + + return len(consonants_list) + + +# Compute a love score from two names +def calculate_love_score(name1, name2): + # Initialize love score to 0 + love = 0 + + # Compare total vowels in both names + total_vowel1 = count_vowels(name1) + total_vowel2 = count_vowels(name2) + + if total_vowel1 == total_vowel2: + love += random.randint(10, 30) + + # Compare total consonants in both names + consonants1 = count_consonants(name1) + consonants2 = count_consonants(name2) + + if consonants1 == consonants2: + love += random.randint( + 20, 40 + ) + + # Compare first letters of both names + if name1.split()[0][0] == name2.split()[0][0]: + love += random.randint( + 10, 30 + ) + + # Compare the lengths of both names + if len(name1) == len(name2): + love += random.randint(1, 10) + + # Add a random score to love score + love += random.randint(10, 50) + + return min(love, 100) + + +# Print the score and describe the relationship +def display_relationship(name1, name2, love): + print("Calculating...") + time.sleep(random.randint(1, 3)) + print(f"{name1} and {name2} have a {love}% relationship.") + + # Show a message based on the score range + if love >= 90: + print("They have an unbreakable relationship that will last forever.") + elif 70 <= love < 90: + print( + "They have a strong relationship that will most likely lead to a marriage." + ) + elif 50 <= love < 70: + print("They have a good relationship that can lead to a honeymoon to Paris.") + else: + print( + "They have a weak relationship that could have been a 'match made in heaven'." + ) + + +if __name__ == "__main__": + # Ask the user for two names + name1 = input("Please type Name 1.\n") + name2 = input("Please type Name 2.\n") + + # Calculate and display the love score + love_score = calculate_love_score(name1, name2) + display_relationship(name1, name2, love_score) diff --git a/web/public/playground/madlibs.py b/web/public/playground/madlibs.py index 03676c3e6..f1d9c35b2 100644 --- a/web/public/playground/madlibs.py +++ b/web/public/playground/madlibs.py @@ -1,12 +1,14 @@ -# Madlibs Generator -# Fill in a few words and get back a silly story. +# === Madlibs Generator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ZackeryRSmith. +# Ask the user for the five story ingredients adjective = input("Give me an adjective: ") animal = input("Give me an animal: ") verb = input("Give me a verb (ending in -ing): ") place = input("Give me a place: ") adverb = input("Give me an adverb: ") +# Print the completed silly story print() print("=== Your story ===") print() diff --git a/web/public/playground/mastermind.py b/web/public/playground/mastermind.py new file mode 100644 index 000000000..af8b4f72b --- /dev/null +++ b/web/public/playground/mastermind.py @@ -0,0 +1,59 @@ +# === Mastermind · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @mr-desilva. + +import random + +# Pick a secret 4-digit number +num = random.randrange(1000, 10000) + +# Get the player's first guess +n = int(input("Guess the 4 digit number:")) + +# Check if the very first guess is correct +if n == num: + print("Great! You guessed the number in just 1 try! You're a Mastermind!") +else: + # Track how many attempts the player makes + ctr = 0 + + # Keep looping until the player guesses correctly + while n != num: + # Increment the attempt counter + ctr += 1 + + count = 0 + + # Convert both numbers to strings for digit comparison + n = str(n) + num = str(num) + + # Prepare a list to show which digits were correct + correct = ["X"] * 4 + + # Compare each digit position one by one + for i in range(0, 4): + if n[i] == num[i]: + count += 1 + correct[i] = n[i] + else: + continue + + # Tell the player how many digits they got right + if (count < 4) and (count != 0): + print("Not quite the number. But you did get ", count, " digit(s) correct!") + print("Also these numbers in your input were correct.") + for k in correct: + print(k, end=" ") + print("\n") + print("\n") + n = int(input("Enter your next choice of numbers: ")) + + # Tell the player if no digits matched + elif count == 0: + print("None of the numbers in your input match.") + n = int(input("Enter your next choice of numbers: ")) + + # Announce victory when the guess matches + if n == num: + print("You've become a Mastermind!") + print("It took you only", ctr, "tries.") diff --git a/web/public/playground/maths.py b/web/public/playground/maths.py new file mode 100644 index 000000000..d7067560c --- /dev/null +++ b/web/public/playground/maths.py @@ -0,0 +1,135 @@ +# === 3D Shape Volume Calculator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ChefYeshpal. + +from cmath import sqrt + + +# Print a welcome banner +print("----------------") +print("created by: ") +print("https://github.com/ChefYeshpal") +print("for: hacktober") +print("----------------") + + +# Basic arithmetic helper functions +def add(x, y): + return x + y + + +def subtract(x, y): + return x - y + + +def multiply(x, y): + return x * y + + +def divide(x, y): + return x / y + + +# Show the list of available shapes +print("-----------------------") +print("Select operation.") +print("1.Cube/cuboid") +print("2.Pyramid(RightRectangular)") +print("3.Cylinder") +print("4.Sphere") +print("5.Cone") +print("6.Ellipsoid") +print("7.Torus(Donut)") +print("8.Hexagonal Prism") +print("-----------------------") +# Keep asking for a shape choice and compute its volume +while True: + choice = input("Enter choice(1/2/3/4/5/6/7/8): ") + print("-----------------------") + + # Shapes that need length, breadth, and height + if choice in ("1", "2"): + num1 = float(input("Enter Length: ")) + num2 = float(input("Enter Breadth/Width: ")) + num3 = float(input("Enter Height: ")) + print("-----------------------") + + if choice == "1": + print("The volume of the Cube/cuboid is: ", num1 * num2 * num3, "unit's") + + elif choice == "2": + print("The volume of the Pyramid is: ", (num1 * num2 * num3) / 3, "unit's") + + # Shapes that need a radius and height + if choice in ("3", "4", "5"): + R = float(input("Enter the raduis: ")) + H = float(input("Enter the height: ")) + print("-----------------------") + + if choice == "3": + print("Finding the volume for Cylinder") + print("-----------------------") + + print("The volume of the Cylinder is: ", 22 / 7 * ((R) ** 2) * H, "unit's") + + elif choice == "4": + print("Finding the volume for Sphere") + print("-----------------------") + + print( + "The volume of the Sphere is: ", 4 / 3 * 22 / 7 * ((R) ** 3), "unit's" + ) + + elif choice == "5": + print("Finding the volume of Cone") + print("-----------------------") + + print("The volume of the cone is: ", 22 / 7 * ((R) ** 2) * H / 3) + + # Ellipsoid needs three axis lengths + if choice in ("6"): + Axs = float(input("a axis: ")) + Bxs = float(input("b axis: ")) + Cxs = float(input("c axis: ")) + + print("-----------------------") + + if choice == "6": + print( + "The volume of the Ellipsoid is: ", + 4 / 3 * 22 / 7 * Axs * Bxs * Cxs, + "unit's", + ) + + # Torus needs a major and minor radius + if choice in ("7"): + R = float(input("Enter the Major Radius: ")) + print("Make sure that what Major Radius > Minor Radius") + r = float(input("Enter the Minor Radius: ")) + + print("-----------------------") + + print( + "The volume of the Torus is: ", + ((22 / 7 * ((r) ** 2)) * (2 * 22 / 7 * R)), + "unit's", + ) + + print("-----------------------") + + # Hexagonal prism needs base edge and height + if choice in ("8"): + a = float(input("Enter the Base edge: ")) + h = float(input("Enter the Height: ")) + + print("-----------------------") + + print( + "The volume of the Right Regular Hexagonal Prism is:", + (3 * sqrt(3) / 2 * a * a * h), + "unit's", + ) + + else: + print("Invalid Input") + +print("thank you for using my program!!") diff --git a/web/public/playground/minesweeper.py b/web/public/playground/minesweeper.py new file mode 100644 index 000000000..70d41ba72 --- /dev/null +++ b/web/public/playground/minesweeper.py @@ -0,0 +1,165 @@ +# === Minesweeper · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @myudak. + +import random +import re + + +# Board class holds the grid, bombs, and dig logic +class Board: + def __init__(self, dim_size, num_bombs): + # Store grid dimensions and bomb count + self.dim_size = dim_size + self.num_bombs = num_bombs + + # Build the grid and assign neighbor counts + self.board = self.make_new_board() + self.assign_values_to_board() + + # Track which cells the player has uncovered + self.dug = set() + + # Create an empty grid and randomly place bombs + def make_new_board(self): + board = [[None for _ in range(self.dim_size)] for _ in range(self.dim_size)] + + # Plant bombs at random positions + bombs_planted = 0 + while bombs_planted < self.num_bombs: + loc = random.randint( + 0, self.dim_size**2 - 1 + ) + row = ( + loc // self.dim_size + ) + col = ( + loc % self.dim_size + ) + + if board[row][col] == "*": + continue + + board[row][col] = "*" + bombs_planted += 1 + + return board + + # Fill non-bomb cells with neighbor bomb counts + def assign_values_to_board(self): + for r in range(self.dim_size): + for c in range(self.dim_size): + if self.board[r][c] == "*": + continue + self.board[r][c] = self.get_num_neighboring_bombs(r, c) + + # Count bombs in the 8 cells surrounding a position + def get_num_neighboring_bombs(self, row, col): + num_neighboring_bombs = 0 + for r in range(max(0, row - 1), min(self.dim_size - 1, row + 1) + 1): + for c in range(max(0, col - 1), min(self.dim_size - 1, col + 1) + 1): + if r == row and c == col: + continue + if self.board[r][c] == "*": + num_neighboring_bombs += 1 + + return num_neighboring_bombs + + # Dig a cell; recurse into empty neighbors automatically + def dig(self, row, col): + self.dug.add((row, col)) + + # Return False immediately if a bomb is hit + if self.board[row][col] == "*": + return False + elif self.board[row][col] > 0: + return True + + # Recursively dig all untouched neighbors + for r in range(max(0, row - 1), min(self.dim_size - 1, row + 1) + 1): + for c in range(max(0, col - 1), min(self.dim_size - 1, col + 1) + 1): + if (r, c) in self.dug: + continue + self.dig(r, c) + + return True + + # Build a printable string of the visible board + def __str__(self): + visible_board = [ + [None for _ in range(self.dim_size)] for _ in range(self.dim_size) + ] + for row in range(self.dim_size): + for col in range(self.dim_size): + if (row, col) in self.dug: + visible_board[row][col] = str(self.board[row][col]) + else: + visible_board[row][col] = " " + + # Assemble a formatted string with column headers + string_rep = "" + widths = [] + for idx in range(self.dim_size): + columns = map(lambda x: x[idx], visible_board) + widths.append(len(max(columns, key=len))) + + indices = [i for i in range(self.dim_size)] + indices_row = " " + cells = [] + for idx, col in enumerate(indices): + format = "%-" + str(widths[idx]) + "s" + cells.append(format % (col)) + indices_row += " ".join(cells) + indices_row += " \n" + + for i in range(len(visible_board)): + row = visible_board[i] + string_rep += f"{i} |" + cells = [] + for idx, col in enumerate(row): + format = "%-" + str(widths[idx]) + "s" + cells.append(format % (col)) + string_rep += " |".join(cells) + string_rep += " |\n" + + str_len = int(len(string_rep) / self.dim_size) + string_rep = indices_row + "-" * str_len + "\n" + string_rep + "-" * str_len + + return string_rep + + +# Main game loop: show board, get input, dig, repeat +def play(dim_size=10, num_bombs=10): + # Set up a fresh board + board = Board(dim_size, num_bombs) + + safe = True + + # Keep playing until all safe cells are dug + while len(board.dug) < board.dim_size**2 - num_bombs: + print(board) + user_input = re.split( + ",(\\s)*", input("Where would you like to dig? Input as row,col: ") + ) + row, col = int(user_input[0]), int(user_input[-1]) + if row < 0 or row >= board.dim_size or col < 0 or col >= dim_size: + print("Invalid location. Try again.") + continue + + # Dig the chosen cell and check if it was a bomb + safe = board.dig(row, col) + if not safe: + break + + # Announce win or reveal the board on loss + if safe: + print("CONGRATULATIONS!!!! YOU ARE VICTORIOUS!") + else: + print("SORRY GAME OVER :(") + board.dug = [ + (r, c) for r in range(board.dim_size) for c in range(board.dim_size) + ] + print(board) + + +if __name__ == "__main__": + play() diff --git a/web/public/playground/morse-code-translator.py b/web/public/playground/morse-code-translator.py new file mode 100644 index 000000000..8889aeb41 --- /dev/null +++ b/web/public/playground/morse-code-translator.py @@ -0,0 +1,118 @@ +# === Morse Code Translator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ahmedalhamad7. + +# Lookup table mapping letters/digits to Morse symbols +morse_code = { + "A": ".-", + "B": "-...", + "C": "-.-.", + "D": "-..", + "E": ".", + "F": "..-.", + "G": "--.", + "H": "....", + "I": "..", + "J": ".---", + "K": "-.-", + "L": ".-..", + "M": "--", + "N": "-.", + "O": "---", + "P": ".--.", + "Q": "--.-", + "R": ".-.", + "S": "...", + "T": "-", + "U": "..-", + "V": "...-", + "W": ".--", + "X": "-..-", + "Y": "-.--", + "Z": "--..", + "1": ".----", + "2": "..---", + "3": "...--", + "4": "....-", + "5": ".....", + "6": "-....", + "7": "--...", + "8": "---..", + "9": "----.", + "0": "-----", + ", ": "--..--", + ".": ".-.-.-", + "?": "..--..", + "/": "-..-.", + "-": "-....-", + "(": "-.--.", + ")": "-.--.-", +} + + +# Convert plain text to Morse code symbols +def encrypt(text): + text = text.upper() + cipher = "" + for letter in text: + if letter != " ": + if letter in morse_code: + cipher += morse_code[letter] + " " + else: + cipher += " " + else: + cipher += " " + + return cipher + + +# Convert Morse code back to plain text +def decrypt(text): + # Append a trailing space so the last code is processed + text += " " + + decipher = "" + citext = "" + for letter in text: + if letter != " ": + i = 0 + + # Accumulate dots and dashes for one character + citext += letter + + else: + i += 1 + + # Two spaces mean a word boundary + if i == 2: + decipher += " " + else: + # Reverse-lookup the Morse sequence + for char, code in morse_code.items(): + if code == citext: + decipher += char + break + citext = "" + + return decipher + + +# Ask user whether to encrypt or decrypt +choice = input( + "Enter 'E' for encryption (text to Morse code) or 'D' for decryption (Morse code to text): " +) + +# Run the chosen operation and print the result +if choice == "E": + user_input = input("Enter the text you want to encrypt: ") + encrypted_text = encrypt(user_input) + print("Encrypted Morse code:", encrypted_text) + +elif choice == "D": + user_input = input( + "Enter the Morse code you want to decrypt (separate symbols with spaces): " + ) + decrypted_text = decrypt(user_input) + print("Decrypted text:", decrypted_text) + +else: + print("Invalid choice. Please enter 'E' for encryption or 'D' for decryption.") diff --git a/web/public/playground/morsecode-translator.py b/web/public/playground/morsecode-translator.py new file mode 100644 index 000000000..450fa8d0f --- /dev/null +++ b/web/public/playground/morsecode-translator.py @@ -0,0 +1,106 @@ +# === Morse Code Translator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ZackeryRSmith. + +# Lookup table mapping letters/digits to Morse symbols +MORSE_CODE_DICT = { + "A": ".-", + "B": "-...", + "C": "-.-.", + "D": "-..", + "E": ".", + "F": "..-.", + "G": "--.", + "H": "....", + "I": "..", + "J": ".---", + "K": "-.-", + "L": ".-..", + "M": "--", + "N": "-.", + "O": "---", + "P": ".--.", + "Q": "--.-", + "R": ".-.", + "S": "...", + "T": "-", + "U": "..-", + "V": "...-", + "W": ".--", + "X": "-..-", + "Y": "-.--", + "Z": "--..", + "1": ".----", + "2": "..---", + "3": "...--", + "4": "....-", + "5": ".....", + "6": "-....", + "7": "--...", + "8": "---..", + "9": "----.", + "0": "-----", + ", ": "--..--", + ".": ".-.-.-", + "?": "..--..", + "/": "-..-.", + "-": "-....-", + "(": "-.--.", + ")": "-.--.-", +} + + +# Convert a plain-text message to Morse code +def encrypt(message): + cipher = "" + for letter in message: + if letter != " ": + cipher += MORSE_CODE_DICT[letter] + " " + else: + cipher += " " + + return cipher + + +# Convert a Morse code message back to plain text +def decrypt(message): + # Append trailing space so last code gets processed + message += " " + + decipher = "" + citext = "" + for letter in message: + if letter != " ": + i = 0 + + # Accumulate dots and dashes for one character + citext += letter + + else: + i += 1 + + # Two spaces mark a word boundary + if i == 2: + decipher += " " + else: + # Reverse-lookup the Morse sequence + decipher += list(MORSE_CODE_DICT.keys())[ + list(MORSE_CODE_DICT.values()).index(citext) + ] + citext = "" + + return decipher + + +# Run a hard-coded demo: encode then decode a name +def main(): + message = "Mrinank-Bhowmick" + result = encrypt(message.upper()) + print(result) + + message = "-- .-. .. -. .- -. -.- -....- -... .... --- .-- -- .. -.-. -.-" + result = decrypt(message) + print(result) + + +if __name__ == "__main__": + main() diff --git a/web/public/playground/neurons.py b/web/public/playground/neurons.py new file mode 100644 index 000000000..6a14648d8 --- /dev/null +++ b/web/public/playground/neurons.py @@ -0,0 +1,106 @@ +# === Neurons · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Zedeldi. + +import random +import time +from enum import Enum +from shutil import get_terminal_size + + +TICKS_PER_SECOND = 60 + + +# Enum listing the four movement directions +class Direction(Enum): + UP = 0 + RIGHT = 1 + DOWN = 2 + LEFT = 3 + + +# Represents an empty cell on the grid +class Empty(str): + CHAR = " " + + def __new__(cls) -> str: + return str.__new__(cls, cls.CHAR) + + +# Represents a living neuron cell on the grid +class Neuron(str): + CHAR = "██" + CHANCE_OF_DEATH = 35 + + def __new__(cls) -> str: + return str.__new__(cls, cls.CHAR) + + +# Stores the 2-D grid of cells +class Grid: + # Build the grid filled with empty cells + def __init__(self, width, height) -> None: + self._grid = [[Empty() for _ in range(width)] for _ in range(height)] + + # Return the grid as a printable string + def __str__(self) -> str: + return "\n".join(["".join(row) for row in self._grid]) + + # Collect all neuron positions from the grid + @property + def neurons(self) -> tuple[tuple[Neuron, tuple[int, int]]]: + return tuple( + (element, (x, y)) + for y, row in enumerate(self._grid) + for x, element in enumerate(row) + if isinstance(element, Neuron) + ) + + # Place a neuron at the given coordinates + def set_neuron(self, x, y) -> None: + self._grid[y][x] = Neuron() + + # Advance one tick: kill or move each neuron randomly + def tick(self) -> None: + for neuron_data in self.neurons: + neuron = neuron_data[0] + x, y = neuron_data[1] + + # Randomly kill the neuron based on its death chance + if random.randint(1, 100) <= Neuron.CHANCE_OF_DEATH: + self._grid[y][x] = Empty() + continue + + # Move the neuron in a random direction + direction = random.choice(list(Direction)) + if direction == Direction.UP: + y -= 1 + elif direction == Direction.RIGHT: + x += 1 + elif direction == Direction.DOWN: + y += 1 + elif direction == Direction.LEFT: + x -= 1 + try: + self._grid[y][x] = Neuron() + except IndexError: + pass + + +# Set up two starter neurons and run the simulation +def main() -> None: + # Size the grid to fit the terminal window + width, height = get_terminal_size() + grid = Grid(width // len(Neuron.CHAR), height) + grid.set_neuron(15, 15) + grid.set_neuron(35, 35) + # Print and update the grid until all neurons die + while grid.neurons: + print(grid) + grid.tick() + time.sleep(1 / TICKS_PER_SECOND) + + +if __name__ == "__main__": + # Restart automatically when all neurons are gone + while True: + main() diff --git a/web/public/playground/number-guessing-app.py b/web/public/playground/number-guessing-app.py new file mode 100644 index 000000000..9ccded327 --- /dev/null +++ b/web/public/playground/number-guessing-app.py @@ -0,0 +1,83 @@ +# === Number Guessing App · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @cypherab01. + +# Import random to generate secret numbers +import random + + +# Define the player-guesses mode +def guess(): + # Pick a secret number between 1 and 20 + n = random.randrange(1, 20) + + # Ask the player for their first guess + var_guess = int(input("Enter any number: ")) + + # Keep looping until the guess matches + while n != var_guess: + if var_guess < n: + print("OOPS! Too low") + # Ask for another guess + var_guess = int(input("Guess again: ")) + elif var_guess > n: + print("OOPS! Too high!") + # Ask for another guess + var_guess = int(input("Guess again: ")) + else: + break + + # Congratulate the player on a correct guess + print(f"Congratulations!! You guessed the number {n} correctly") + + +# Define the computer-guesses mode +def computer_guess(): + global comp_guess + + # Ask the player to pick a secret number + x = int(input("Enter your number: ")) + + # Set initial search range for the computer + low = 1 + high = x + + # Track player feedback each round + comp_ans = "" + + # Loop until the computer gets it right + while comp_ans != "c": + if low != high: + # Pick a random number within the current range + comp_guess = random.randint(low, high) + else: + comp_guess = ( + low + ) + + # Show the guess and get player feedback + comp_ans = input( + f"Is {comp_guess} too high (h), too low (l), or correct (c)? \n=>" + ).lower() + + # Narrow the range based on feedback + if comp_ans == "h": + high = comp_guess - 1 + elif comp_ans == "l": + low = comp_guess + 1 + + # Announce the computer found the number + print(f"Yay! The computer guessed your number, {comp_guess}, correctly!") + + +if __name__ == "__main__": + # Show the mode selection menu + print( + "Select gaming mode\n Press 1 to guess the number\nPress 2 to choose the number" + ) + g_mode = int(input()) + + # Run the chosen game mode + if g_mode == 1: + guess() + if g_mode == 2: + computer_guess() diff --git a/web/public/playground/otp-generator.py b/web/public/playground/otp-generator.py new file mode 100644 index 000000000..dae95e335 --- /dev/null +++ b/web/public/playground/otp-generator.py @@ -0,0 +1,75 @@ +# === OTP Generator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @iamdestinychild. + +import random + +# Define character pools for building OTPs +s_char = "abcdefghijklmnopqrstuvwxyz" +b_char = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +d_char = "123456789" + + +class Otp: + # Store the desired OTP length + def __init__(self, len): + self.len = len + + # Generate a digits-only OTP + @property + def digits(self): + num = 0 + result = [] + while num < self.len: + rand_choice = "".join(random.choices(d_char, k=self.len)[0:1]) + result.append(rand_choice) + num += 1 + value = "".join(result) + return value + + # Generate an OTP mixing uppercase letters and digits + @property + def bd_digits(self): + num = 0 + result = [] + while num < self.len: + b_choice = "".join(random.choices(b_char, k=self.len)[0:1]) + d_choice = "".join(random.choices(d_char, k=self.len)[0:1]) + result.append(b_choice) + result.append(d_choice) + num += 1 + value = "".join(result[0 : self.len]) + return value + + # Generate an OTP mixing lowercase letters and digits + @property + def sd_digits(self): + num = 0 + result = [] + while num < self.len: + s_choice = "".join(random.choices(s_char, k=self.len)[0:1]) + d_choice = "".join(random.choices(d_char, k=self.len)[0:1]) + result.append(s_choice) + result.append(d_choice) + num += 1 + value = "".join(result[0 : self.len]) + return value + + # Generate an OTP using all three character types + @property + def sbd_digits(self): + num = 0 + result = [] + while num < self.len: + s_choice = "".join(random.choices(s_char, k=self.len)[0:1]) + b_choice = "".join(random.choices(b_char, k=self.len)[0:1]) + d_choice = "".join(random.choices(d_char, k=self.len)[0:1]) + result.append(s_choice) + result.append(b_choice) + result.append(d_choice) + num += 1 + value = "".join(result[0 : self.len]) + return value + + +# Create a 10-digit OTP and print it +print("OTP:" + Otp(10).digits) diff --git a/web/public/playground/personal-finance-tracker.py b/web/public/playground/personal-finance-tracker.py new file mode 100644 index 000000000..caeb65e10 --- /dev/null +++ b/web/public/playground/personal-finance-tracker.py @@ -0,0 +1,63 @@ +# === Personal Finance Tracker · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Manishak798. + +# Represent a single expense with name, amount, category +class Expense: + def __init__(self, name, amount, category): + self.name = name + self.amount = amount + self.category = category + + +# Manage a list of expenses and calculations +class FinanceManager: + # Start with an empty expense list + def __init__(self): + self.expenses = [] + + # Create and store a new Expense object + def add_expense(self, name, amount, category): + expense = Expense(name, amount, category) + self.expenses.append(expense) + + # Sum all stored expense amounts + def calculate_total_expenses(self): + total_expenses = sum(expense.amount for expense in self.expenses) + return total_expenses + + # Print every expense's details + def list_expenses(self): + for expense in self.expenses: + print( + f"Name: {expense.name}, Amount: {expense.amount}, Category: {expense.category}" + ) + + +if __name__ == "__main__": + # Create the finance manager instance + finance_manager = FinanceManager() + + # Show menu and handle user choices in a loop + while True: + print("Personal Finance Manager") + print("1. Add Expense") + print("2. List Expenses") + print("3. Calculate Total Expenses") + print("4. Exit") + choice = input("Enter your choice: ") + + if choice == "1": + name = input("Expense Name: ") + amount = float(input("Expense Amount: ")) + category = input("Expense Category: ") + finance_manager.add_expense(name, amount, category) + + elif choice == "2": + finance_manager.list_expenses() + + elif choice == "3": + total_expenses = finance_manager.calculate_total_expenses() + print(f"Total Expenses: {total_expenses}") + + elif choice == "4": + break diff --git a/web/public/playground/pig-latin.py b/web/public/playground/pig-latin.py new file mode 100644 index 000000000..e5e904091 --- /dev/null +++ b/web/public/playground/pig-latin.py @@ -0,0 +1,51 @@ +# === Pig Latin Converter · annotated for the pyBegin playground === +# Note: the project's function.py helper is inlined below so this runs as a +# single self-contained file in the browser playground. + + +# Convert an English sentence into Pig Latin +def to_pig(eng_string): + eng_words = eng_string.split() + pig_words = [] + vowels = ["a", "e", "i", "o", "u"] + cap_vowels = ["A", "E", "I", "O", "U"] + + # Transform each word in turn + for word in eng_words: + for i in range(len(word)): + # Words starting with a vowel just get "yay" + if word[0] in vowels or word[0] in cap_vowels: + pig_words.append(word + "yay") + break + + # Capitalised words: move letters and keep the title case + elif word[0].isupper(): + if word[i] in vowels or word[0] in cap_vowels: + remain = word[i:].title() + pig_words.append(remain + word[:i].lower() + "ay") + break + + # Other words: move the leading consonants and add "ay" + else: + if word[i] in vowels or word[0] in cap_vowels: + pig_words.append(word[i:] + word[:i] + "ay") + break + + # Join the converted words back into a sentence + pig_sentence = " ".join(pig_words) + return pig_sentence + + +# Greet the user and show a separator +print("Hello! Welcome to a Pig Latin converter") +print("=" * 39) + +# Ask user for the string to convert +original_string = input( + "Please type the string you would like to convert to pig latin: " +) + +# Print the converted Pig Latin result +print() +print("Your converted string is:") +print(to_pig(original_string)) diff --git a/web/public/playground/pokemon-battle.py b/web/public/playground/pokemon-battle.py new file mode 100644 index 000000000..0614067f5 --- /dev/null +++ b/web/public/playground/pokemon-battle.py @@ -0,0 +1,195 @@ +# === Pokemon Battle · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @EpicNesh26. + +import time +import numpy as np +import sys + + +# Print text one character at a time for drama +def delay_print(s): + for c in s: + sys.stdout.write(c) + sys.stdout.flush() + time.sleep(0.05) + + +# Define the Pokemon class with stats and battle logic +class Pokemon: + def __init__(self, name, types, moves, EVs, health="==================="): + # Save all attributes on the instance + self.name = name + self.types = types + self.moves = moves + self.attack = EVs["ATTACK"] + self.defense = EVs["DEFENSE"] + self.health = health + self.bars = 20 + + def fight(self, Pokemon2): + # Print both Pokemon's stats before battle + print("-----POKEMONE BATTLE-----") + print(f"\n{self.name}") + print("TYPE/", self.types) + print("ATTACK/", self.attack) + print("DEFENSE/", self.defense) + print("LVL/", 3 * (1 + np.mean([self.attack, self.defense]))) + print("\nVS") + print(f"\n{Pokemon2.name}") + print("TYPE/", Pokemon2.types) + print("ATTACK/", Pokemon2.attack) + print("DEFENSE/", Pokemon2.defense) + print("LVL/", 3 * (1 + np.mean([Pokemon2.attack, Pokemon2.defense]))) + + time.sleep(2) + + # Adjust stats based on type advantages + version = ["Fire", "Water", "Grass"] + for i, k in enumerate(version): + if self.types == k: + if Pokemon2.types == k: + string_1_attack = "\nIts not very effective..." + string_2_attack = "\nIts not very effective..." + + if Pokemon2.types == version[(i + 1) % 3]: + Pokemon2.attack *= 2 + Pokemon2.defense *= 2 + self.attack /= 2 + self.defense /= 2 + string_1_attack = "\nIts not very effective..." + string_2_attack = "\nIts super effective!" + + if Pokemon2.types == version[(i + 2) % 3]: + self.attack *= 2 + self.defense *= 2 + Pokemon2.attack /= 2 + Pokemon2.defense /= 2 + string_1_attack = "\nIts super effective!" + string_2_attack = "\nIts not very effective..." + + # Battle loop — continues while both have health + while (self.bars > 0) and (Pokemon2.bars > 0): + # Show current health bars for both Pokemon + print(f"\n{self.name}\t\tHLTH\t{self.health}") + print(f"{Pokemon2.name}\t\tHLTH\t{Pokemon2.health}\n") + + # Let player 1 pick a move + print(f"Go {self.name}!") + for i, x in enumerate(self.moves): + print(f"{i+1}.", x) + index = int(input("Pick a move: ")) + delay_print(f"\n{self.name} used {self.moves[index-1]}!") + time.sleep(1) + delay_print(string_1_attack) + + # Apply damage to Pokemon2 + Pokemon2.bars -= self.attack + Pokemon2.health = "" + + # Rebuild health bar string with defense bonus + for j in range(int(Pokemon2.bars + 0.1 * Pokemon2.defense)): + Pokemon2.health += "=" + + time.sleep(1) + print(f"\n{self.name}\t\tHLTH\t{self.health}") + print(f"{Pokemon2.name}\t\tHLTH\t{Pokemon2.health}\n") + time.sleep(0.5) + + # Check if Pokemon2 has fainted + if Pokemon2.bars <= 0: + delay_print("\n..." + Pokemon2.name + " fainted.") + break + + # Let player 2 pick a move + print(f"Go {Pokemon2.name}!") + for i, x in enumerate(Pokemon2.moves): + print(f"{i+1}.", x) + index = int(input("Pick a move: ")) + delay_print(f"\n{Pokemon2.name} used {Pokemon2.moves[index-1]}!") + time.sleep(1) + delay_print(string_2_attack) + + # Apply damage to self + self.bars -= Pokemon2.attack + self.health = "" + + # Rebuild health bar string with defense bonus + for j in range(int(self.bars + 0.1 * self.defense)): + self.health += "=" + + time.sleep(1) + print(f"{self.name}\t\tHLTH\t{self.health}") + print(f"{Pokemon2.name}\t\tHLTH\t{Pokemon2.health}\n") + time.sleep(0.5) + + # Check if self has fainted + if self.bars <= 0: + delay_print("\n..." + self.name + " fainted.") + break + + # Award random prize money at the end + money = np.random.choice(5000) + delay_print(f"\nOpponent paid you ${money}.\n") + + +if __name__ == "__main__": + # Create all available Pokemon with stats + Charizard = Pokemon( + "Charizard", + "Fire", + ["Flamethrower", "Fly", "Blast Burn", "Fire Punch"], + {"ATTACK": 12, "DEFENSE": 8}, + ) + Blastoise = Pokemon( + "Blastoise", + "Water", + ["Water Gun", "Bubblebeam", "Hydro Pump", "Surf"], + {"ATTACK": 10, "DEFENSE": 10}, + ) + Venusaur = Pokemon( + "Venusaur", + "Grass", + ["Vine Wip", "Razor Leaf", "Earthquake", "Frenzy Plant"], + {"ATTACK": 8, "DEFENSE": 12}, + ) + + Charmeleon = Pokemon( + "Charmeleon", + "Fire", + ["Ember", "Scratch", "Flamethrower", "Fire Punch"], + {"ATTACK": 6, "DEFENSE": 5}, + ) + Wartortle = Pokemon( + "Wartortle", + "Water", + ["Bubblebeam", "Water Gun", "Headbutt", "Surf"], + {"ATTACK": 5, "DEFENSE": 5}, + ) + Ivysaur = Pokemon( + "Ivysaur\t", + "Grass", + ["Vine Wip", "Razor Leaf", "Bullet Seed", "Leech Seed"], + {"ATTACK": 4, "DEFENSE": 6}, + ) + + Charmander = Pokemon( + "Charmander", + "Fire", + ["Ember", "Scratch", "Tackle", "Fire Punch"], + {"ATTACK": 4, "DEFENSE": 2}, + ) + Squirtle = Pokemon( + "Squirtle", + "Water", + ["Bubblebeam", "Tackle", "Headbutt", "Surf"], + {"ATTACK": 3, "DEFENSE": 3}, + ) + Bulbasaur = Pokemon( + "Bulbasaur", + "Grass", + ["Vine Wip", "Razor Leaf", "Tackle", "Leech Seed"], + {"ATTACK": 2, "DEFENSE": 4}, + ) + + # Start a battle between two Pokemon + Blastoise.fight(Squirtle) diff --git a/web/public/playground/projecteuler.py b/web/public/playground/projecteuler.py new file mode 100644 index 000000000..b12161a12 --- /dev/null +++ b/web/public/playground/projecteuler.py @@ -0,0 +1,12 @@ +# === Project Euler · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ZackeryRSmith. + +# Sum multiples of 3 or 5 below 1000 +def compute(): + ans = sum(x for x in range(1000) if (x % 3 == 0 or x % 5 == 0)) + return str(ans) + + +if __name__ == "__main__": + # Print the computed answer + print(compute()) diff --git a/web/public/playground/pwd.py b/web/public/playground/pwd.py index 508656389..76c878445 100644 --- a/web/public/playground/pwd.py +++ b/web/public/playground/pwd.py @@ -1,15 +1,20 @@ -# Password Generator -# Builds a strong random password using the `secrets` module. +# === Password Generator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @u749929. import secrets import string +# Ask user for desired password length length = int(input("How many characters should the password have? ")) +# Clamp length between 4 and 64 length = max(4, min(length, 64)) +# Build the pool of allowed characters alphabet = string.ascii_letters + string.digits + "!@#$%^&*-_" +# Pick random characters to form the password password = "".join(secrets.choice(alphabet) for _ in range(length)) +# Display the generated password print() print("Your new password:") print(password) diff --git a/web/public/playground/python-banking-system.py b/web/public/playground/python-banking-system.py new file mode 100644 index 000000000..78aa8c9be --- /dev/null +++ b/web/public/playground/python-banking-system.py @@ -0,0 +1,132 @@ +# === Python Banking System · annotated for the pyBegin playground === + +import random + + +# Manage all accounts and their transaction histories +class Bank: + def __init__(self): + # Store accounts and transaction logs in dicts + self.accounts = {} + self.transactions = {} + + # Create a new account and return it + def create_account(self, account_holder, initial_balance): + account_number = self.generate_account_number() + account = BankAccount(account_number, account_holder, initial_balance) + self.accounts[account_number] = account + self.transactions[account_number] = [] + return account + + # Build a random 8-digit account number string + def generate_account_number(self): + return "".join(random.choice("0123456789") for _ in range(8)) + + # Look up and return an account by number + def get_account(self, account_number): + return self.accounts.get(account_number) + + # Deposit or withdraw and record the transaction + def perform_transaction(self, account_number, transaction_type, amount): + account = self.get_account(account_number) + if not account: + return "Account not found." + + if transaction_type == "deposit": + account.deposit(amount) + elif transaction_type == "withdraw": + account.withdraw(amount) + else: + return "Invalid transaction type." + + self.transactions[account_number].append((transaction_type, amount)) + return "Transaction completed." + + # Return the full transaction history list + def get_transaction_history(self, account_number): + return self.transactions.get(account_number, []) + + +# Represent a single bank account with balance +class BankAccount: + def __init__(self, account_number, account_holder, initial_balance): + # Save account details as attributes + self.account_number = account_number + self.account_holder = account_holder + self.balance = initial_balance + + # Add amount to balance if positive + def deposit(self, amount): + if amount > 0: + self.balance += amount + + # Subtract amount if valid and sufficient funds + def withdraw(self, amount): + if 0 < amount <= self.balance: + self.balance -= amount + + # Return the current balance + def get_balance(self): + return self.balance + + +def main(): + # Create a new bank instance + bank = Bank() + + # Show menu and handle choices in a loop + while True: + print("\nPython Banking System") + print("1. Create Account") + print("2. Perform Transaction") + print("3. Check Balance") + print("4. Transaction History") + print("5. Exit") + + choice = input("Enter your choice: ") + + if choice == "1": + account_holder = input("Enter your name: ") + initial_balance = float(input("Enter initial balance: ")) + account = bank.create_account(account_holder, initial_balance) + print( + f"Account created successfully. Account Number: {account.account_number}" + ) + + elif choice == "2": + account_number = input("Enter account number: ") + transaction_type = input( + "Enter transaction type (deposit/withdraw): " + ).lower() + amount = float(input("Enter transaction amount: ")) + result = bank.perform_transaction(account_number, transaction_type, amount) + print(result) + + elif choice == "3": + account_number = input("Enter account number: ") + account = bank.get_account(account_number) + if account: + print(f"Account Balance: ${account.get_balance()}") + else: + print("Account not found.") + + elif choice == "4": + account_number = input("Enter account number: ") + transactions = bank.get_transaction_history(account_number) + if transactions: + print("Transaction History:") + for trans_type, amount in transactions: + print(f"{trans_type.capitalize()}: ${amount}") + else: + print("Account not found or no transaction history.") + + elif choice == "5": + print("Exiting the Python Banking System. Goodbye!") + break + + else: + print("Invalid choice. Please try again.") + + +if __name__ == "__main__": + main() diff --git a/web/public/playground/qr.py b/web/public/playground/qr.py index d40c2727c..9da80e7b9 100644 --- a/web/public/playground/qr.py +++ b/web/public/playground/qr.py @@ -1,14 +1,17 @@ -# QR Code Generator -# Turns any text or URL into a QR code drawn right in the terminal. +# === QRCode Generator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @SubramanyaKS. import qrcode +# Ask user for text or URL to encode text = input("Enter text or a URL to encode: ") +# Build the QR code object qr = qrcode.QRCode(border=2) qr.add_data(text) qr.make(fit=True) +# Print the QR code as ASCII art print() print(f"QR code for: {text}") print() diff --git a/web/public/playground/quiz-game.py b/web/public/playground/quiz-game.py new file mode 100644 index 000000000..e4f56fa69 --- /dev/null +++ b/web/public/playground/quiz-game.py @@ -0,0 +1,108 @@ +# === Quiz Game · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ZackeryRSmith. + +import sqlite3 + + +# Open (or create) the SQLite database file +def create_connection(db_file): + conn = None + try: + conn = sqlite3.connect(db_file) + except sqlite3.Error as e: + print(e) + return conn + + +# Insert a player's name and score into the database +def save_score(conn, name, score): + sql = """ INSERT INTO scores(name, score) + VALUES(?,?) """ + cur = conn.cursor() + cur.execute(sql, (name, score)) + conn.commit() + return cur.lastrowid + + +# Fetch all scores ordered best first +def get_all_scores(conn): + cur = conn.cursor() + cur.execute("SELECT * FROM scores ORDER BY score DESC") + + rows = cur.fetchall() + return rows + + +# Welcome the player to the quiz +print("Welcome to AskPython Quiz") + +# Ask if the player is ready to start +answer = input("Are you ready to play the Quiz? (yes/no) :") + +# Track score and total question count +score = 0 +total_questions = 3 + +# Only proceed if the player says yes +if answer.lower() == "yes": + # Ask question 1 and check the answer + answer = input("Question 1: What is your Favourite programming language?") + if answer.lower() == "python": + score += 1 + print("correct") + else: + print("Wrong Answer :(") + + # Ask question 2 and check the answer + answer = input("Question 2: Do you follow any author on AskPython? ") + if answer.lower() == "yes": + score += 1 + print("correct") + else: + print("Wrong Answer :(") + + # Ask question 3 and check the answer + answer = input( + "Question 3: What is the name of your favourite website for learning Python?" + ) + if answer.lower() == "askpython": + score += 1 + print("correct") + else: + print("Wrong Answer :(") + + # Show final score and percentage + print( + "Thank you for Playing this small quiz game, you attempted", + score, + "questions correctly!", + ) + mark = int((score / total_questions) * 100) + print(f"Marks obtained: {mark}%") + + # Get player name and prepare to save score + player_name = input("Enter your name: ") + player_score = score + + database = "quiz_game.db" + + # Connect to the database + conn = create_connection(database) + + if conn is not None: + # Save score and display all previous scores + save_score(conn, player_name, player_score) + + print("Previous scores:") + scores = get_all_scores(conn) + for row in scores: + print(f"Name: {row[1]}, Score: {row[2]}, Date: {row[3]}") + + conn.close() + else: + print("Error! Cannot create the database connection.") +else: + print(" Please, when you're ready, enter the game again.") + +# Print farewell message +print("BYE!") diff --git a/web/public/playground/regex-strip.py b/web/public/playground/regex-strip.py new file mode 100644 index 000000000..ce4139770 --- /dev/null +++ b/web/public/playground/regex-strip.py @@ -0,0 +1,22 @@ +# === Regex Strip · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ibra-kdbra. + +import re + + +# Strip leading/trailing whitespace using regex +def strip(text): + stripStartRegex = re.compile(r"(^\s*)") + stripEndRegex = re.compile(r"(\s*$)") + + # Remove leading spaces, then trailing spaces + textStartStripped = stripStartRegex.sub("", text) + textStripped = stripEndRegex.sub("", textStartStripped) + + return textStripped + + +# Run the strip function on a sample string +if __name__ == "__main__": + text = " test ffs " + print(strip(text)) diff --git a/web/public/playground/rps.py b/web/public/playground/rps.py index 6525aea57..07d08ef35 100644 --- a/web/public/playground/rps.py +++ b/web/public/playground/rps.py @@ -1,18 +1,22 @@ -# Rock, Paper, Scissors -# Play a round against the computer's random choice. +# === Rock Paper Scissors · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @ZackeryRSmith. import random +# Define valid choices and what each choice beats choices = ["rock", "paper", "scissors"] beats = {"rock": "scissors", "paper": "rock", "scissors": "paper"} +# Get the player's move and validate it you = input("Pick rock, paper or scissors: ").strip().lower() if you not in choices: print("That's not a valid choice!") else: + # Computer picks a random move cpu = random.choice(choices) print(f"You chose {you}, the computer chose {cpu}.") + # Compare moves and announce the result if you == cpu: print("It's a draw!") elif beats[you] == cpu: diff --git a/web/public/playground/split-tip.py b/web/public/playground/split-tip.py new file mode 100644 index 000000000..84c44ebf0 --- /dev/null +++ b/web/public/playground/split-tip.py @@ -0,0 +1,21 @@ +# === Split Tip Calculator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @DevTomilola-OS. + +# Greet the user +print("Welcome to the Tip calculator") +# Collect bill amount, tip percentage, and number of people +amount = float(input("Enter your bill amount ($): ")) +tip = float(input("what percentage of tip you want to give? 5, 10, 12 or 15?\n")) +split = int(input("How many people to split the bill amount: ")) +# Calculate each person's tip share and total share +w = ((0.01 * tip) * amount) // split +x = (amount + (0.01 * tip) * amount) // split +# Collect inputs a second time (as in original program) +amount = float(input("Enter your bill amount ($): ")) +tip = float(input("what percentage of tip you want to give? 5, 10, 12 or 15?\n")) +split = int(input("How many people to split the bill amount: ")) +# Recalculate with the new inputs +w = ((0.01 * tip) * amount) // split +x = (amount + (0.01 * tip) * amount) // split +# Print each person's tip and total +print(f"Each person has to pay ${w} in tip and ${x} in total") diff --git a/web/public/playground/subnetting-flsm.py b/web/public/playground/subnetting-flsm.py new file mode 100644 index 000000000..88efdb0b1 --- /dev/null +++ b/web/public/playground/subnetting-flsm.py @@ -0,0 +1,198 @@ +# === Subnetting FLSM · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @bim22614. + +# Collect network settings from the user +def start_v(): + network_addr = input("Network address: ") + prefix = int(input("Prefix of network: ")) + num_of_sub = int(input("Num of subnets: ")) + max_value_dev = int(input("Max value of hosts for one sub: ")) + return [network_addr, prefix, num_of_sub, max_value_dev] + + +# Unpack the returned list into named variables +start_list = start_v() +network_addr = start_list[0] +prefix = start_list[1] +num_of_sub = start_list[2] +max_value_dev = start_list[3] + + +# Check if the network has enough IP addresses +def enough_ip(): + needed_ip = num_of_sub * max_value_dev + haved_ip = 2 ** (32 - int(prefix)) + if haved_ip >= needed_ip: + return haved_ip + else: + start_v() + + +enough_ip() + + +# Build the subnet mask in binary for each octet +def rules_ip(): + ip_by_octat = [] + + mask_bin = int(prefix) * "1" + (32 - int(prefix)) * "0" + + mask_bin_list = [] + for i in range(0, len(mask_bin), 8): + mask_bin_list.append(mask_bin[i : i + 8]) + + return mask_bin_list + + +ip_by_octat = rules_ip() + +# Build a lookup table mapping prefix to host count +table_of_prefix_value = {} +for i in range(0, 32): + table_of_prefix_value.update({str(32 - i): 2**i}) + + +# Find the smallest prefix that fits all hosts +for i in table_of_prefix_value.keys(): + if int(int(table_of_prefix_value[i])) >= max_value_dev: + prefix_need = [i, int(table_of_prefix_value[i])] + break + + +# Convert one IP octet integer to an 8-bit binary string +def ip_in_bin(octet): + m = str(bin(octet)) + m1 = [i for i in m] + m1 = m1[2::] + if len(m1) != 8: + for i in range(0, 8 - len(m1)): + m1.insert(0, "0") + m2 = ["".join(m1)] + return m2 + + +# Split the network address into octets and convert to binary +ip_list_octet = network_addr.split(".") +ip_list_octet = [int(i) for i in ip_list_octet] +bin_ip_list = [] +for i in range(len(ip_list_octet)): + bin_ip_list.insert(i, ip_in_bin(int(ip_list_octet[i]))) + bin_ip_list[i] = bin_ip_list[i][0] + + +ip_list_sub = [] + + +# Calculate network and broadcast addresses for each subnet +if int(prefix_need[0]) in range(25, 33): + prom_ip_1 = ip_list_octet[3] + prom_ip_2 = ip_list_octet[3] + prefix_need[1] - 1 + prom_ip_3 = ip_list_octet[2] + prom_ip_4 = ip_list_octet[1] + prom_ip_5 = ip_list_octet[0] + + ip_list_sub.append( + [ + [prom_ip_5, prom_ip_4, prom_ip_3, prom_ip_1], + [prom_ip_5, prom_ip_4, prom_ip_3, prom_ip_2], + ] + ) + for i in range(num_of_sub - 1): + prom_ip_1 = prom_ip_2 + 1 + prom_ip_2 = ip_list_sub[i][1][3] + prefix_need[1] + if prom_ip_1 > 255 or prom_ip_2 > 255: + prom_ip_1 = 0 + prom_ip_2 = prom_ip_1 + prefix_need[1] - 1 + prom_ip_3 = prom_ip_3 + 1 + if prom_ip_3 > 255: + prom_ip_3 = 0 + prom_ip_4 = prom_ip_4 + 1 + if prom_ip_4 > 255: + prom_ip_4 = 0 + prom_ip_5 = prom_ip_5 + 1 + + ip_list_sub.append( + [ + [prom_ip_5, prom_ip_4, prom_ip_3, prom_ip_1], + [prom_ip_5, prom_ip_4, prom_ip_3, prom_ip_2], + ] + ) + continue + + ip_list_sub.append( + [ + [prom_ip_5, prom_ip_4, prom_ip_3, prom_ip_1], + [prom_ip_5, prom_ip_4, prom_ip_3, prom_ip_2], + ] + ) + + +# Print the summary header +print(25 * " " + "Subnetting - FLSM") +print( + "Network address/prefix" + + 5 * " " + + "Number of subnets" + + 5 * " " + + "Max num of hosts for one sub\n" + + 4 * " " + + network_addr + + "/" + + str(prefix) + + 17 * " " + + str(num_of_sub) + + 25 * " " + + str(max_value_dev) +) + +print() + +# Print the column headers +print( + "№Net" + + 5 * " " + + "Value of hosts" + + 6 * " " + + "Network address" + + 5 * " " + + "Broadcast address" + + 8 * " " + + "Usable hosts" +) + +# Print each subnet's details +for i in range(num_of_sub): + print( + str(i + 1) + + "\t" + + 10 * " " + + str(max_value_dev) + + "\t" + + 10 * " " + + "{}.{}.{}.{}".format( + ip_list_sub[i][0][0], + ip_list_sub[i][0][1], + ip_list_sub[i][0][2], + ip_list_sub[i][0][3], + ) + + "\t" + + 7 * " " + + "{}.{}.{}.{}".format( + ip_list_sub[i][1][0], + ip_list_sub[i][1][1], + ip_list_sub[i][1][2], + ip_list_sub[i][1][3], + ) + + "\t" + + 7 * " " + + "{}.{}.{}.{} - {}.{}.{}.{}".format( + ip_list_sub[i][0][0], + ip_list_sub[i][0][1], + ip_list_sub[i][0][2], + ip_list_sub[i][0][3] + 1, + ip_list_sub[i][1][0], + ip_list_sub[i][1][1], + ip_list_sub[i][1][2], + ip_list_sub[i][1][3] - 1, + ) + ) diff --git a/web/public/playground/sudoku-solver.py b/web/public/playground/sudoku-solver.py new file mode 100644 index 000000000..b57af8a66 --- /dev/null +++ b/web/public/playground/sudoku-solver.py @@ -0,0 +1,138 @@ +# === Sudoku Solver · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @saltX5. + +from random import sample + + +# Generate a randomized valid Sudoku board +def generate_board(num): + base = 3 + side = base * base + + def pattern(r, c): + return (base * (r % base) + r // base + c) % side + + def shuffle(s): + return sample(s, len(s)) + + # Shuffle row groups, rows within groups, and columns + rBase = range(base) + rows = [g * base + r for g in shuffle(rBase) for r in shuffle(rBase)] + cols = [g * base + c for g in shuffle(rBase) for c in shuffle(rBase)] + nums = shuffle(range(1, base * base + 1)) + + # Build the full randomized board + board_tmp = [[nums[pattern(r, c)] for c in cols] for r in rows] + + # Show the complete solution first + print("=======full board========") + print_board(board_tmp) + + # Remove cells to create the puzzle + squares = side * side + if num == 0: + empties = squares * 3 // 4 + else: + empties = 81 - num + # Set chosen positions to 0 (empty) + for p in sample(range(squares), empties): + board_tmp[p // side][p % side] = 0 + + return board_tmp + + +# Print the board with grid dividers +def print_board(bo): + for i in range(len(bo)): + # Print horizontal divider every 3 rows + if i % 3 == 0 and i != 0: + print("- - - - - - - - - - - - - ") + for j in range(len(bo[0])): + # Print vertical divider every 3 columns + if j % 3 == 0 and j != 0: + print(" | ", end="") + if j == 8: + print(bo[i][j]) + else: + print(str(bo[i][j]) + " ", end="") + print("") + + +# Check if placing num at pos is valid +def possible(bo, pos, num): + # Check the row for duplicates + for i in range(len(bo[0])): + if bo[pos[0]][i] == num and pos[1] != i: + return False + + # Check the column for duplicates + for i in range(len(bo)): + if bo[i][pos[1]] == num and pos[0] != i: + return False + + # Check the 3x3 box for duplicates + box_x = pos[1] // 3 + box_y = pos[0] // 3 + + for i in range(box_y * 3, box_y * 3 + 3): + for j in range(box_x * 3, box_x * 3 + 3): + if bo[i][j] == num and (i, j) != pos: + return False + return True + + +# Find the next empty (zero) cell +def next_empty(bo): + for i in range(len(bo)): + for j in range(len(bo[0])): + if bo[i][j] == 0: + return i, j + + +# Solve the board using backtracking recursion +def solve(bo): + slot = next_empty(bo) + if not slot: + return True + else: + row, col = slot + # Try each number 1–9 in the empty cell + for i in range(1, 10): + if possible(bo, (row, col), i): + bo[row][col] = i + + # Recurse; if solved, bubble True up + if solve(bo): + return True + + # Undo the placement and try next number + bo[row][col] = 0 + return False + + +# Define the board to solve (all zeros = fully generated) +board = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], +] + +# Generate a random puzzle board +board = generate_board(0) + +# Display the unsolved puzzle +print("======solvable board=====") +print_board(board) + +# Solve the puzzle in-place +solve(board) + +# Display the solved board +print("======solved board=======") +print_board(board) diff --git a/web/public/playground/tennistournamentsim.py b/web/public/playground/tennistournamentsim.py new file mode 100644 index 000000000..8afff45a2 --- /dev/null +++ b/web/public/playground/tennistournamentsim.py @@ -0,0 +1,440 @@ +# === Tennis Tournament Simulator · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @jpyces. + +import random +import time + +# Record the program start time +start = time.time() + +if __name__ == "__main__": + print("Program Starts from here") + +# Reusable label strings for match output +BRWINNERNAMESTR = "Bracket Winner Name: " +WINNINGSCHOOLSTR = ", Winning School Name: " +WINNERSKILLSTR = ", Winner Skill: " +WINNUM = ", Wins: " + +BRLOSERNAMESTR = "Bracket Loser Name: " +LOSINGSCHOOLSTR = ", Losing School Name: " +LOSERSKILLSTR = ", Loser Skill: " +LOSSNUMSTR = ", Losses: " + +CHAMPNAMESTR = "Champion Name: " +CHAMPSCHOOLSTR = ", Champion School: " +CHAMPSKILLSTR = ", Champion Skill: " + +RUNUPNAMESTR = "Runner Up Name: " +RUNUPSCHOOLSTR = ", Running Up School: " +RUNUPSKILLSTR = ", Runner Up Skill: " + +# Name/school pools — each pick is removed to avoid repeats +random_first_names = ["Max", "Corey", "Evander", "Timothy"] +random_last_names = ["Kenshin", "Holyfield", "Roosevelt", "Wong"] +random_school = ["Newport", "Sammamish", "Inglemore", "Eastlake"] + + +# Pick and remove a random first name +def first_name_picker(): + global random_first_names + first_name = random.choice(random_first_names) + random_first_names.remove(first_name) + return first_name + + +# Pick and remove a random last name +def last_name_picker(): + global random_last_names + last_name = random.choice(random_last_names) + random_last_names.remove(last_name) + return last_name + + +# Pick and remove a random school name +def school_name_picker(): + global random_school + school_name = random.choice(random_school) + random_school.remove(school_name) + return school_name + + +# Player class storing identity and match record +class Player: + def __init__(self, first, last, skill, school, win, loss): + self.first = first + self.last = last + self.skill = skill + self.school = school + self.win = win + self.loss = loss + + +# Create four players with random attributes +p1 = Player( + first_name_picker(), + last_name_picker(), + random.randint(1, 6), + school_name_picker(), + 0, + 0, +) +p2 = Player( + first_name_picker(), + last_name_picker(), + random.randint(1, 6), + school_name_picker(), + 0, + 0, +) +p3 = Player( + first_name_picker(), + last_name_picker(), + random.randint(1, 6), + school_name_picker(), + 0, + 0, +) +p4 = Player( + first_name_picker(), + last_name_picker(), + random.randint(1, 6), + school_name_picker(), + 0, + 0, +) + + +# Simulate a match with random skill variance +def bracket_results(player1, player2): + rngskillinc1 = random.random() + rngskilldec1 = random.random() + rngskill1 = rngskillinc1 - rngskilldec1 + + rngskillinc2 = random.random() + rngskilldec2 = random.random() + rngskill2 = rngskillinc2 - rngskilldec2 + + # Determine winner by adjusted skill score + if (player1.skill * rngskill1) + player1.skill > ( + player2.skill * rngskill2 + ) + player2.skill: + player1.skill = player1.skill + 0.5 + player1.win += 1 + player2.loss += 1 + elif (player1.skill * rngskill1) + player1.skill < ( + player2.skill * rngskill2 + ) + player2.skill: + player2.skill = player2.skill + 0.5 + player2.win += 1 + player1.loss += 1 + elif (player1.skill * rngskill1) + player1.skill == ( + player2.skill * rngskill2 + ) + player2.skill: + # Break exact ties randomly + choice = random.randint(0, 2) + if choice == 1: + player1.skill = player1.skill + 0.5 + player1.win += 1 + player2.loss += 1 + + else: + player2.skill = player2.skill + 0.5 + player2.win += 1 + player1.loss += 1 + + return player1.win, player1.loss, player2.win, player2.loss + + +# Run Bracket 1, Round 1: p1 vs p2 +bracket_results(p1, p2) +print( + f"""Player's info - Bracket 1, Round 1 +Player 1 +Name: {p1.first} {p1.last}, School: {p1.school} + +Player 2 +Name: {p2.first} {p2.last}, School: {p2.school} +""" +) +# Print the winner and loser of bracket 1 round 1 +if p1.win > p2.win: + winnerbr1r1 = p1 + loserbr1r1 = p2 + print( + BRWINNERNAMESTR + winnerbr1r1.first, + winnerbr1r1.last + + WINNINGSCHOOLSTR + + winnerbr1r1.school + + WINNERSKILLSTR + + str(winnerbr1r1.skill) + + WINNUM + + str(winnerbr1r1.win), + ) + print( + BRLOSERNAMESTR + loserbr1r1.first, + loserbr1r1.last + + LOSINGSCHOOLSTR + + loserbr1r1.school + + LOSERSKILLSTR + + str(loserbr1r1.skill) + + LOSSNUMSTR + + str(loserbr1r1.loss), + ) +elif p1.win < p2.win: + winnerbr1r1 = p2 + loserbr1r1 = p1 + print( + BRWINNERNAMESTR + winnerbr1r1.first, + winnerbr1r1.last + + WINNINGSCHOOLSTR + + winnerbr1r1.school + + WINNERSKILLSTR + + str(winnerbr1r1.skill) + + WINNUM + + str(winnerbr1r1.win), + ) + print( + BRLOSERNAMESTR + loserbr1r1.first, + loserbr1r1.last + + LOSINGSCHOOLSTR + + loserbr1r1.school + + LOSERSKILLSTR + + str(loserbr1r1.skill) + + LOSSNUMSTR + + str(loserbr1r1.loss), + ) + +# Run Bracket 2, Round 1: p3 vs p4 +bracket_results(p3, p4) +print( + f""" +------------------------------------- + +Player's info - Bracket 2, Round 1 +Player 3 +Name: {p3.first} {p3.last}, School: {p3.school} + +Player 4 +Name: {p4.first} {p4.last}, School: {p4.school} +""" +) +# Print the winner and loser of bracket 2 round 1 +if p3.win > p4.win: + winnerbr2r1 = p3 + loserbr2r1 = p4 + print( + BRWINNERNAMESTR + winnerbr2r1.first, + winnerbr2r1.last + + WINNINGSCHOOLSTR + + winnerbr2r1.school + + WINNERSKILLSTR + + str(winnerbr2r1.skill) + + WINNUM + + str(winnerbr2r1.win), + ) + print( + BRLOSERNAMESTR + loserbr2r1.first, + loserbr2r1.last + + LOSINGSCHOOLSTR + + loserbr2r1.school + + LOSERSKILLSTR + + str(loserbr2r1.skill) + + LOSSNUMSTR + + str(loserbr2r1.loss), + ) +elif p3.win < p4.win: + winnerbr2r1 = p4 + loserbr2r1 = p3 + print( + BRWINNERNAMESTR + winnerbr2r1.first, + winnerbr2r1.last + + WINNINGSCHOOLSTR + + winnerbr2r1.school + + WINNERSKILLSTR + + str(winnerbr2r1.skill) + + WINNUM + + str(winnerbr2r1.win), + ) + print( + BRLOSERNAMESTR + loserbr2r1.first, + loserbr2r1.last + + LOSINGSCHOOLSTR + + loserbr2r1.school + + LOSERSKILLSTR + + str(loserbr2r1.skill) + + LOSSNUMSTR + + str(loserbr2r1.loss), + ) + +# Finals: match the two bracket winners +if p1.win == p3.win: + print( + f""" +##################################### + +Player's info - Finals +Player 1 +Name: {p1.first} {p1.last}, School: {p1.school} + +Player 3 +Name: {p3.first} {p3.last}, School: {p3.school} + """ + ) + bracket_results(p1, p3) + # Announce champion and runner-up + if p1.win > p3.win: + champ = p1 + runup = p3 + print( + CHAMPNAMESTR + champ.first, + champ.last + CHAMPSCHOOLSTR + champ.school, + CHAMPSKILLSTR + str(champ.skill), + ) + print( + RUNUPNAMESTR + runup.first, + runup.last + RUNUPSCHOOLSTR + runup.school, + RUNUPSKILLSTR + str(runup.skill), + ) + elif p3.win > p1.win: + champ = p3 + runup = p1 + print( + CHAMPNAMESTR + champ.first, + champ.last + CHAMPSCHOOLSTR + champ.school, + CHAMPSKILLSTR + str(champ.skill), + ) + print( + RUNUPNAMESTR + runup.first, + runup.last + RUNUPSCHOOLSTR + runup.school, + RUNUPSKILLSTR + str(runup.skill), + ) +elif p1.win == p4.win: + print( + f""" +##################################### + +Player's info - Finals +Player 1 +Name: {p1.first} {p1.last}, School: {p1.school} + +Player 4 +Name: {p4.first} {p4.last}, School: {p4.school} + """ + ) + bracket_results(p1, p4) + # Announce champion and runner-up + if p1.win > p4.win: + champ = p1 + runup = p4 + print( + CHAMPNAMESTR + champ.first, + champ.last + CHAMPSCHOOLSTR + champ.school, + CHAMPSKILLSTR + str(champ.skill), + ) + print( + RUNUPNAMESTR + runup.first, + runup.last + RUNUPSCHOOLSTR + runup.school, + RUNUPSKILLSTR + str(runup.skill), + ) + elif p4.win > p1.win: + champ = p4 + runup = p1 + print( + CHAMPNAMESTR + champ.first, + champ.last + CHAMPSCHOOLSTR + champ.school, + CHAMPSKILLSTR + str(champ.skill), + ) + print( + RUNUPNAMESTR + runup.first, + runup.last + RUNUPSCHOOLSTR + runup.school, + RUNUPSKILLSTR + str(runup.skill), + ) +elif p2.win == p3.win: + print( + f""" +##################################### + +Player's info - Finals +Player 2 +Name: {p2.first} {p2.last}, School: {p2.school} + +Player 3 +Name: {p3.first} {p3.last}, School: {p3.school} + """ + ) + bracket_results(p2, p3) + # Announce champion and runner-up + if p2.win > p3.win: + champ = p2 + runup = p3 + print( + CHAMPNAMESTR + champ.first, + champ.last + CHAMPSCHOOLSTR + champ.school, + CHAMPSKILLSTR + str(champ.skill), + ) + print( + RUNUPNAMESTR + runup.first, + runup.last + RUNUPSCHOOLSTR + runup.school, + RUNUPSKILLSTR + str(runup.skill), + ) + elif p3.win > p2.win: + champ = p3 + runup = p2 + print( + CHAMPNAMESTR + champ.first, + champ.last + CHAMPSCHOOLSTR + champ.school, + CHAMPSKILLSTR + str(champ.skill), + ) + print( + RUNUPNAMESTR + runup.first, + runup.last + RUNUPSCHOOLSTR + runup.school, + RUNUPSKILLSTR + str(runup.skill), + ) +elif p2.win == p4.win: + print( + f""" +##################################### + +Player's info - Finals +Player 2 +Name: {p2.first} {p2.last}, School: {p2.school} + +Player 4 +Name: {p4.first} {p4.last}, School: {p4.school} + """ + ) + bracket_results(p2, p4) + # Announce champion and runner-up + if p2.win > p4.win: + champ = p2 + runup = p4 + print( + CHAMPNAMESTR + champ.first, + champ.last + CHAMPSCHOOLSTR + champ.school, + CHAMPSKILLSTR + str(champ.skill), + ) + print( + RUNUPNAMESTR + runup.first, + runup.last + RUNUPSCHOOLSTR + runup.school, + RUNUPSKILLSTR + str(runup.skill), + ) + elif p4.win > p2.win: + champ = p4 + runup = p2 + print( + CHAMPNAMESTR + champ.first, + champ.last + CHAMPSCHOOLSTR + champ.school, + CHAMPSKILLSTR + str(champ.skill), + ) + print( + RUNUPNAMESTR + runup.first, + runup.last + RUNUPSCHOOLSTR + runup.school, + RUNUPSKILLSTR + str(runup.skill), + ) + +# Record end time and print total execution time +end = time.time() + +print("\n") +print("The time of execution of above program is:", (end - start) * 10**3, "ms") diff --git a/web/public/playground/tictactoe-tylerpear.py b/web/public/playground/tictactoe-tylerpear.py new file mode 100644 index 000000000..9ed10c028 --- /dev/null +++ b/web/public/playground/tictactoe-tylerpear.py @@ -0,0 +1,192 @@ +# === Tic-Tac-Toe (TylerPear) · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @tylerapear. + +# Set up board cell templates and the 5-row board list +spot = " " +leftSpot = f"{spot} |" +middleSpot = f" {spot} | " +rightSpot = f"{spot}" +board = [ + [leftSpot, middleSpot, rightSpot], + ["___", "____", "__"], + [leftSpot, middleSpot, rightSpot], + ["___", "____", "__"], + [leftSpot, middleSpot, rightSpot], +] + +i = 0 +player = "" + + +# Print the current board to the screen +def print_board(): + print(board[0][0], end="") + print(board[0][1], end="") + print(board[0][2], end="\n") + print(board[1][0], end="") + print(board[1][1], end="") + print(board[1][2], end="\n") + print(board[2][0], end="") + print(board[2][1], end="") + print(board[2][2], end="\n") + print(board[3][0], end="") + print(board[3][1], end="") + print(board[3][2], end="\n") + print(board[4][0], end="") + print(board[4][1], end="") + print(board[4][2], end="\n\n") + + +# Ask the current player for a valid move code +def collect_input(player): + print("KEY: TL = Top Left, TM = Top Middle, TR = Top Right") + print(" ML = Middle Left, MM = Middle Middle, MR = Middle Right") + print(" BL = Bottom Left, BM = Bottom Middle, BR = Bottom Right") + move = input(f"Player {player}, Type Your Move: ") + valid = False + if move in ["TL", "TM", "TR", "ML", "MM", "MR", "BL", "BM", "BR"]: + valid = True + return move + else: + # Keep asking until a valid code is entered + while valid == False: + move = input("Type a valid move:") + if move in ["TL", "TM", "TR", "ML", "MM", "MR", "BL", "BM", "BR"]: + valid = True + return move + + +# Check if a cell is taken; place marker if free +def examine_move(board, x, y, msg): + taken = False + + if "X" in board[x][y] or "O" in board[x][y]: + taken = True + else: + board[x][y] = msg + + return taken + + +# Map the move code to a board cell and update it +def change_board(move, board, player): + taken = False + + # Top row moves + if move == "TL": + taken = examine_move(board, 0, 0, f" {player}|") + elif move == "TM": + taken = examine_move(board, 0, 1, f" {player} | ") + elif move == "TR": + taken = examine_move(board, 0, 2, f"{player}") + + # Middle row moves + if move == "ML": + taken = examine_move(board, 2, 0, f" {player}|") + elif move == "MM": + taken = examine_move(board, 2, 1, f" {player} | ") + elif move == "MR": + taken = examine_move(board, 2, 2, f"{player}") + + # Bottom row moves + if move == "BL": + taken = examine_move(board, 4, 0, f" {player}|") + elif move == "BM": + taken = examine_move(board, 4, 1, f" {player} | ") + elif move == "BR": + taken = examine_move(board, 4, 2, f"{player}") + + # If cell was taken, ask for a new move + if taken: + move = input("That spot is taken! Pick another spot: ") + change_board(move, board, player) + + return board + + +# Alternate between "O" (even turns) and "X" (odd turns) +def change_player(i): + if i % 2 == 0: + return "O" + else: + return "X" + + +# Check all win conditions for a given player +def check_for_win(board, player): + # Check rows + if board[0] == [f" {player}|", f" {player} | ", f"{player}"]: + return True + elif board[2] == [f" {player}|", f" {player} | ", f"{player}"]: + return True + elif board[4] == [f" {player}|", f" {player} | ", f"{player}"]: + return True + + # Check columns + elif ( + board[0][0] == f" {player}|" + and board[2][0] == f" {player}|" + and board[4][0] == f" {player}|" + ): + return True + elif ( + board[0][1] == f" {player} | " + and board[2][1] == f" {player} | " + and board[4][1] == f" {player} | " + ): + return True + elif ( + board[0][2] == f"{player}" + and board[2][2] == f"{player}" + and board[4][2] == f"{player}" + ): + return True + + # Check diagonals + elif ( + board[0][0] == f" {player}|" + and board[2][1] == f" {player} | " + and board[4][2] == f"{player}" + ): + return True + elif ( + board[0][2] == f"{player}" + and board[2][1] == f" {player} | " + and board[4][0] == f" {player}|" + ): + return True + + else: + return False + + +# Return True if either player has won +def player_won(board): + xwon = check_for_win(board, "X") + owon = check_for_win(board, "O") + + if xwon or owon: + return True + else: + return False + + +# Main game loop — play until someone wins or 9 turns pass +if __name__ == "__main__": + catsgame = False + while player_won(board) == False and catsgame == False: + i += 1 + if i == 10: + print("Cat's Game!") + catsgame = True + else: + player = change_player(i) + board = change_board(collect_input(player), board, player) + print_board() + + # Announce the winner if the game wasn't a draw + if catsgame == False: + if player == "X": + print("Congrats! X Wins!") + elif player == "O": + print("Congrats! O Wins!") diff --git a/web/public/playground/tictactoe.py b/web/public/playground/tictactoe.py index 52fa03d00..f4a9c41d9 100644 --- a/web/public/playground/tictactoe.py +++ b/web/public/playground/tictactoe.py @@ -1,8 +1,9 @@ -# Tic-Tac-Toe -# You are X, the computer is O. Enter a cell number 1-9 to play. +# === Tic-Tac-Toe · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @Mrinank-Bhowmick. import random +# Create a blank 9-cell board and define all win combos board = [" "] * 9 wins = [ (0, 1, 2), (3, 4, 5), (6, 7, 8), @@ -11,6 +12,7 @@ ] +# Print the board in a 3x3 grid with separators def show(): print() for row in range(3): @@ -24,6 +26,7 @@ def show(): print() +# Return "X" or "O" if a winning line exists def winner(): for a, b, c in wins: if board[a] != " " and board[a] == board[b] == board[c]: @@ -31,10 +34,13 @@ def winner(): return None +# Show opening message and initial board print("Tic-Tac-Toe — you are X.") show() +# Main game loop — alternate player and computer turns while True: + # Get a valid move from the player move = input("Your move (1-9): ").strip() if not move.isdigit() or not (1 <= int(move) <= 9): print("Please enter a number from 1 to 9.") @@ -44,9 +50,11 @@ def winner(): print("That cell is taken — try another.") continue + # Place player's mark and show updated board board[spot] = "X" show() + # Check if player won or board is full if winner() == "X": print("You win! 🎉") break @@ -54,11 +62,13 @@ def winner(): print("It's a draw! 🤝") break + # Computer picks a random empty cell cpu = random.choice([i for i, v in enumerate(board) if v == " "]) board[cpu] = "O" print(f"Computer played {cpu + 1}.") show() + # Check if computer won or board is full if winner() == "O": print("Computer wins! 🤖") break diff --git a/web/public/playground/timer.py b/web/public/playground/timer.py new file mode 100644 index 000000000..ce73f9751 --- /dev/null +++ b/web/public/playground/timer.py @@ -0,0 +1,20 @@ +# === Timer · annotated for the pyBegin playground === +# A beginner-friendly walkthrough — original project by @cj-praveen. + +# Import sleep to pause execution for one second +from time import sleep + +# Ask the user how many seconds to count +duration = int(input("Enter the duration in seconds: ")) + +# Start count at zero +count = 0 + +# Loop, printing each second until duration is reached +while count < duration: + print(count, end="\r") + sleep(1) + count += 1 + +# Notify the user that the timer has finished +print("Your time is up!") diff --git a/web/scripts/gen-authors.mjs b/web/scripts/gen-authors.mjs new file mode 100644 index 000000000..9130d7ead --- /dev/null +++ b/web/scripts/gen-authors.mjs @@ -0,0 +1,74 @@ +// One-off: build src/lib/authors.json — a { "": "" } +// map crediting the contributor who first added each project. +// +// It finds the commit that first added a file under projects//, then +// asks the GitHub API for that commit's author login (the git author name in +// the local history is not always a GitHub handle). +// +// Run from web/ with gh authenticated: node scripts/gen-authors.mjs +// authors.json is committed; re-run only when new projects are added. + +import { execSync } from "child_process"; +import fs from "fs"; +import path from "path"; + +const WEB = process.cwd(); +const REPO = path.join(WEB, ".."); +const OUT = path.join(WEB, "src", "lib", "authors.json"); + +// folder -> first-add commit SHA +const log = execSync( + 'git log --reverse --diff-filter=A --format="C|%H" --name-only -- projects/', + { cwd: REPO, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }, +); +const folderSha = {}; +let sha = null; +for (const line of log.split(/\r?\n/)) { + if (line.startsWith("C|")) { + sha = line.slice(2); + continue; + } + const m = line.match(/^projects\/([^/]+)\//); + if (m && !folderSha[m[1]]) folderSha[m[1]] = sha; +} + +const shas = [...new Set(Object.values(folderSha))]; +console.log(`${Object.keys(folderSha).length} folders · ${shas.length} commits`); + +// commit SHA -> github login (cached to survive interrupted runs) +const cacheFile = path.join(WEB, "scripts", "_sha-login.json"); +const shaLogin = fs.existsSync(cacheFile) + ? JSON.parse(fs.readFileSync(cacheFile, "utf8")) + : {}; + +let done = 0; +for (const s of shas) { + done++; + if (s in shaLogin) continue; + try { + const login = execSync( + `gh api repos/Mrinank-Bhowmick/python-beginner-projects/commits/${s} --jq .author.login`, + { cwd: REPO, encoding: "utf8" }, + ).trim(); + shaLogin[s] = login || null; + } catch { + shaLogin[s] = null; + } + if (done % 20 === 0) { + fs.writeFileSync(cacheFile, JSON.stringify(shaLogin)); + console.log(` ${done}/${shas.length}`); + } +} +fs.writeFileSync(cacheFile, JSON.stringify(shaLogin)); + +const authors = {}; +for (const [folder, s] of Object.entries(folderSha)) { + const login = shaLogin[s]; + if (login) authors[folder] = login; +} + +const sorted = Object.fromEntries( + Object.entries(authors).sort(([a], [b]) => a.localeCompare(b)), +); +fs.writeFileSync(OUT, JSON.stringify(sorted, null, 2) + "\n"); +console.log(`authors.json: ${Object.keys(sorted).length} credited`); diff --git a/web/scripts/gen-catalog.mjs b/web/scripts/gen-catalog.mjs new file mode 100644 index 000000000..602f3b4da --- /dev/null +++ b/web/scripts/gen-catalog.mjs @@ -0,0 +1,314 @@ +// Catalog generator for the pyBegin site. +// +// Scans the repo's projects/ folder and emits src/lib/catalog.json — one entry +// per project (id, folder, name, blurb, runnable flag, line count). +// +// The in-browser playground for a project loads public/playground/.py — +// a hand-written, annotated "tutorial" version of the project (the originals +// in projects/ are never modified). This script does NOT create those files; +// it only marks hasPlayground=true when one already exists. +// +// Run from the web/ directory: node scripts/gen-catalog.mjs +// +// catalog.json is committed; re-run this whenever projects/ changes. + +import fs from "fs"; +import path from "path"; + +const WEB = process.cwd(); // expected: .../web +const PROJECTS_DIR = path.join(WEB, "..", "projects"); +const PLAYGROUND_DIR = path.join(WEB, "public", "playground"); +const OUT = path.join(WEB, "src", "lib", "catalog.json"); + +// The curated projects featured on the home page keep their original short +// ids / emoji / playground status so existing routes and the hand-tuned +// playground source files (public/playground/*.py) stay valid. +const CURATED = { + "Snake Game": { id: "snake", emoji: "🐍", playground: false }, + "Tic-Tac-Toe": { id: "tictactoe", emoji: "⭕", playground: true }, + Hangman: { id: "hangman", emoji: "🪢", playground: true }, + Flappybird_game: { id: "flappy", emoji: "🐦", playground: false }, + Rock_Paper_Scissors: { id: "rps", emoji: "✊", playground: true }, + Calculator: { id: "calc", emoji: "🧮", playground: false }, + BMI_calculator: { id: "bmi", emoji: "⚖️", playground: true }, + "API Based Weather Report": { id: "weather", emoji: "☁️", playground: false }, + "QRCode-Generator": { id: "qr", emoji: "▦", playground: true }, + "YouTube Video Downloader": { id: "yt", emoji: "⬇", playground: false }, + "Madlibs Generator": { id: "madlibs", emoji: "✏️", playground: true }, +}; + +// Contributor credit: folder -> github login. Built by gen-authors.mjs. +const AUTHORS_FILE = path.join(WEB, "src", "lib", "authors.json"); +const AUTHORS = fs.existsSync(AUTHORS_FILE) + ? JSON.parse(fs.readFileSync(AUTHORS_FILE, "utf8")) + : {}; + +// Pick a context-appropriate emoji from the project's name + blurb. Ordered: +// the first matching rule wins, so more specific keywords come first. +const EMOJI_RULES = [ + [/tic.?tac.?toe/, "⭕"], [/hangman/, "🪢"], [/flappy/, "🐦"], + [/rock.?paper|paper.?scissor/, "✊"], [/madlib/, "✏️"], + [/snake/, "🐍"], [/sudoku|2048/, "🔢"], [/minesweeper/, "💣"], + [/battleship/, "🚢"], [/\bpong\b/, "🏓"], [/tetris/, "🟦"], + [/\bchess\b/, "♟️"], [/\bcard\b|blackjack|poker/, "🃏"], + [/\bdice\b|\broll\b/, "🎲"], [/coin.?(flip|toss)|flip.?coin/, "🪙"], + [/\bquiz\b|trivia/, "❓"], [/\bmaze\b|puzzle/, "🧩"], + [/guess|\bgame\b/, "🎮"], + [/calculator|\bcalc\b/, "🧮"], [/\bbmi\b/, "⚖️"], + [/cipher|encrypt|decrypt|cryptograph|caesar|\baes\b|\brsa\b|\bhash\b/, "🔐"], + [/password|\blogin\b|\bauth\b/, "🔑"], + [/steganograph/, "🕵️"], + [/qr.?code|barcode/, "▦"], + [/weather/, "⛅"], [/\bnews\b/, "📰"], + [/stock|market|trading/, "📈"], + [/currency|exchange rate/, "💱"], + [/\bbank\b|\batm\b|expense|budget|finance|\bmoney\b|salary/, "💰"], + [/\btip\b/, "💵"], + [/email|\bmail\b|gmail/, "📧"], [/telegram/, "✈️"], + [/discord|whatsapp|\bchat\b|\bsms\b/, "💬"], [/\bbot\b/, "🤖"], + [/scrap|crawl|spider/, "🕷️"], + [/translat/, "🌍"], [/morse/, "📡"], + [/wiki/, "📚"], [/dictionary|\bword\b/, "📖"], + [/youtube|\bvideo\b/, "🎬"], + [/download/, "⬇️"], [/upload/, "⬆️"], + [/\bapi\b/, "🔌"], [/url.?shorten|shorten.?url/, "🔗"], + [/website|\bweb\b|\burl\b|browser/, "🌐"], + [/\bsearch\b/, "🔍"], + [/music|\bsong\b|spotify|\bmp3\b/, "🎵"], + [/audio|\bsound\b|\bvoice\b|speech|text.?to.?speech|\btts\b/, "🔊"], + [/camera|webcam|\bopencv\b|face.?(detect|recogn)/, "📷"], + [/screenshot|screen.?record/, "🖥️"], + [/neural|neuron|machine.?learning|deep.?learning|\bai\b/, "🧠"], + [/data|analysis|\bplot\b|\bchart\b|visuali[sz]/, "📊"], + [/image|photo|picture|\bjpg\b|\bpng\b/, "🖼️"], + [/\bpdf\b/, "📄"], [/excel|spreadsheet/, "📊"], + [/\bcsv\b|\bjson\b/, "🗂️"], + [/draw|paint|sketch|turtle|\bart\b/, "🎨"], + [/colou?r/, "🎨"], + [/clock|stopwatch/, "🕐"], [/timer|countdown/, "⏳"], + [/alarm|reminder/, "⏰"], [/calendar/, "📅"], + [/pomodoro/, "🍅"], + [/todo|to.?do|\btask\b/, "✅"], [/\bnote\b|diary|journal/, "🗒️"], + [/typing|keyboard|\bkey\b/, "⌨️"], + [/automat|pyautogui|\bmouse\b/, "🖱️"], + [/\bcpu\b|\bram\b|battery|\bsystem\b|monitor/, "🖥️"], + [/network|\bping\b|\bport\b|\bip\b|wifi/, "🌐"], + [/\bgui\b|tkinter|\bwindow\b/, "🪟"], + [/math|equation|fibonacci|\bprime\b|factorial|euler|matrix|\balgebra\b/, "➗"], + [/notification|\bnotify\b/, "🔔"], + [/recommend/, "👍"], [/summar/, "📝"], [/\breview\b/, "🔎"], + [/jarvis|assistant/, "🤖"], + [/internet speed|speed test/, "📶"], + [/predict|classif|\bquality\b|recogni[sz]/, "🔮"], + [/drowsi|sleep/, "😴"], [/othello|reversi|\bgo\b game/, "⚫"], + [/regex|regular expression/, "🔣"], + [/number|counter|\bcount\b/, "🔢"], + [/random/, "🎰"], + [/\bname\b|username/, "🏷️"], + [/\bjoke\b/, "😂"], [/\bmeme\b/, "🤣"], [/emoji/, "😀"], + [/story|\bpoem\b|poetry/, "📜"], + [/recipe|\bfood\b/, "🍽️"], + [/\bage\b|birthday/, "🎂"], + [/shopping|\bcart\b|\bstore\b|ecommerce/, "🛒"], + [/contact/, "📇"], + [/\bmap\b|location|\bgps\b/, "🗺️"], + [/planet|\bspace\b|nasa|astronom|\bsolar\b/, "🪐"], + [/rocket|launch/, "🚀"], + [/\bcar\b|vehicle|tesla/, "🚗"], + [/convert|converter/, "🔄"], + [/\bascii\b/, "🔡"], [/generator|generate/, "⚙️"], +]; + +function emojiFor(name, blurb) { + const hay = `${name} ${blurb}`.toLowerCase(); + for (const [re, emoji] of EMOJI_RULES) { + if (re.test(hay)) return emoji; + } + return "🐍"; +} + +const slugify = (s) => + s + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") || "project"; + +function findReadme(dir) { + for (const f of fs.readdirSync(dir)) { + if (/^readme\.md$/i.test(f)) return path.join(dir, f); + } + return null; +} + +function findMainPy(dir, folder) { + const top = fs.readdirSync(dir, { withFileTypes: true }); + const pys = top + .filter((d) => d.isFile() && d.name.toLowerCase().endsWith(".py")) + .map((d) => d.name); + const pick = (names) => { + const byMain = names.find((n) => n.toLowerCase() === "main.py"); + if (byMain) return byMain; + const slug = slugify(folder); + const byName = names.find( + (n) => slugify(n.replace(/\.py$/i, "")) === slug, + ); + return byName || names[0]; + }; + if (pys.length) return path.join(dir, pick(pys)); + // look one level deep for projects that nest their code in a subfolder + for (const d of top) { + if (!d.isDirectory()) continue; + const sub = path.join(dir, d.name); + const subpys = fs + .readdirSync(sub) + .filter((n) => n.toLowerCase().endsWith(".py")); + if (subpys.length) { + const byMain = subpys.find((n) => n.toLowerCase() === "main.py"); + return path.join(sub, byMain || subpys[0]); + } + } + return null; +} + +function parseReadme(file) { + const lines = fs.readFileSync(file, "utf8").split(/\r?\n/); + + let name = null; + for (const l of lines) { + const m = l.match(/^#\s+(.+)/); + if (m) { + name = m[1].trim(); + break; + } + } + + // blurb: first plain paragraph (skip headings, images, quotes, code fences), + // joining the wrapped lines of that paragraph back into one sentence. + let blurb = ""; + let inCode = false; + const para = []; + for (const raw of lines) { + const l = raw.trim(); + if (l.startsWith("```")) { + inCode = !inCode; + continue; + } + if (inCode) continue; + if (!l) { + if (para.length) break; + continue; + } + if (l.startsWith("#") || l.startsWith("![") || l.startsWith(">")) { + if (para.length) break; + continue; + } + if (l.startsWith("- ") || l.startsWith("* ") || /^\d+\.\s/.test(l)) { + if (para.length) break; + continue; + } + para.push(l); + } + blurb = para.join(" "); + blurb = blurb + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/[*`_]/g, "") + .trim(); + if (blurb.length > 150) blurb = blurb.slice(0, 147).trimEnd() + "…"; + + // Pyodide-runnable verdict + let runnable = false; + const idx = lines.findIndex((l) => + /^#+\s*pyodide-runnable/i.test(l.trim()), + ); + if (idx >= 0) { + for (let i = idx + 1; i < lines.length; i++) { + const l = lines[i].trim(); + if (!l) continue; + runnable = /^(yes|partly)/i.test(l); + break; + } + } + return { name, blurb, runnable }; +} + +const folders = fs + .readdirSync(PROJECTS_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort((a, b) => a.localeCompare(b)); + +const usedIds = new Set(); +const entries = []; + +for (const folder of folders) { + const dir = path.join(PROJECTS_DIR, folder); + const curated = CURATED[folder]; + + let id; + if (curated) { + id = curated.id; + } else { + const base = slugify(folder); + id = base; + let n = 2; + while (usedIds.has(id)) id = `${base}-${n++}`; + } + usedIds.add(id); + + let name = folder; + let blurb = ""; + let verdictRunnable = false; + const readmeFile = findReadme(dir); + if (readmeFile) { + const parsed = parseReadme(readmeFile); + name = parsed.name || folder; + blurb = parsed.blurb; + verdictRunnable = parsed.runnable; + } + + const mainPy = findMainPy(dir, folder); + const lines = mainPy + ? fs.readFileSync(mainPy, "utf8").split(/\r?\n/).length + : 0; + + const runnable = curated ? curated.playground : verdictRunnable; + const emoji = curated ? curated.emoji : emojiFor(name, blurb); + const author = AUTHORS[folder]; + + // A project is playable iff a hand-written annotated tutorial file exists + // at public/playground/.py. The originals in projects/ are never copied. + const pgFile = path.join(PLAYGROUND_DIR, `${id}.py`); + const hasPlayground = fs.existsSync(pgFile); + + entries.push({ + id, folder, name, blurb, emoji, runnable, hasPlayground, lines, + ...(author ? { author } : {}), + }); +} + +// The "Password Generator" sub-project is featured on the home page under its +// own short id; surface it as a standalone catalog entry too. +if (!usedIds.has("pwd")) { + entries.push({ + id: "pwd", + folder: "Password Projects/Password Generator", + name: "Password Generator", + blurb: "Generate random, strong passwords from the command line.", + emoji: "🔐", + runnable: true, + hasPlayground: fs.existsSync(path.join(PLAYGROUND_DIR, "pwd.py")), + lines: 30, + ...(AUTHORS["Password Projects"] + ? { author: AUTHORS["Password Projects"] } + : {}), + }); +} + +entries.sort((a, b) => a.name.localeCompare(b.name)); +fs.writeFileSync(OUT, JSON.stringify(entries, null, 2) + "\n"); + +const runnableCount = entries.filter((e) => e.runnable).length; +const pgCount = entries.filter((e) => e.hasPlayground).length; +console.log( + `catalog.json: ${entries.length} projects · ${runnableCount} runnable · ${pgCount} with a playground file`, +); diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 84eb4fbfa..cff8c2680 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -491,165 +491,11 @@ a { color: inherit; } .s-contrib.lead { grid-column: span 2; } } -/* ====== Try-it panel (modal demo) ====== */ -.s-meta-pill.run { - background: var(--s-accent); - color: var(--s-ink); - font-weight: 700; - border-color: var(--s-ink); -} .s-meta-pill.play { background: var(--s-accent-3); color: var(--s-ink); font-weight: 700; } -.s-try { - margin-top: 4px; - padding: 20px; - border: var(--s-border-thin); - border-radius: 16px; - background: var(--s-bg); - box-shadow: var(--s-shadow-sm); - margin-bottom: 22px; - font-family: var(--s-body); -} -.s-try-h { - font-family: var(--s-display); - font-size: 18px; - font-weight: 800; - margin-bottom: 14px; - color: var(--s-ink); -} -.s-try-form { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); - gap: 10px 14px; - margin-bottom: 14px; -} -.s-try-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--s-ink); } -.s-try-field > span { font-weight: 700; } -.s-try-field input, .s-try-field select { - padding: 9px 12px; - border-radius: 10px; - border: var(--s-border-thin); - background: #fff; - color: var(--s-ink); - font: inherit; - font-size: 14px; -} -.s-try-field input:focus, .s-try-field select:focus { - outline: none; - box-shadow: 0 0 0 3px var(--s-accent); -} -.s-try-run { margin-bottom: 12px; } -.s-try-run:disabled { opacity: 0.55; cursor: wait; } -.s-try-error { - margin: 0 0 12px; - padding: 10px 14px; - border-radius: 10px; - background: #ffe1e1; - border: var(--s-border-thin); - border-color: #b34d4d; - color: #6e1414; - font-size: 13px; -} -.s-try-out { - margin-top: 6px; - padding: 14px 16px; - border-radius: 12px; - background: var(--s-surface-warm); - border: var(--s-border-thin); - color: var(--s-ink); -} -.s-try-out pre, .s-try-story pre { - margin: 0; - white-space: pre-wrap; - font: 14px/1.55 var(--s-body); - color: var(--s-ink); -} - -/* Tic-tac-toe board */ -.s-ttt-grid { - display: grid; - grid-template-columns: repeat(3, 78px); - grid-template-rows: repeat(3, 78px); - gap: 8px; - justify-content: center; - margin: 6px auto 4px; -} -.s-ttt-cell { - background: #fff; - border: var(--s-border-thin); - border-radius: 14px; - font: 800 38px/1 var(--s-display); - color: var(--s-ink); - cursor: pointer; - transition: transform 80ms ease; - box-shadow: var(--s-shadow-sm); -} -.s-ttt-cell:not(:disabled):hover { transform: translate(-2px, -2px); box-shadow: 5px 5px 0 var(--s-ink); } -.s-ttt-cell:disabled { cursor: default; } -.s-ttt-cell [data-mark="X"] { color: var(--s-accent); } -.s-ttt-cell [data-mark="O"] { color: #2f7a4f; } - -/* Hangman */ -.s-hg-row { - display: flex; - gap: 14px; - align-items: flex-end; - margin-bottom: 14px; -} -.s-hg-word { - display: flex; - gap: 8px; - flex-wrap: wrap; - justify-content: center; - margin: 6px 0 10px; -} -.s-hg-slot { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 28px; - padding: 6px 4px; - border-bottom: 3px solid var(--s-ink); - font: 800 26px/1 var(--s-display); - color: var(--s-ink); -} -.s-hg-meta { - display: flex; - justify-content: space-between; - font-size: 13px; - margin-bottom: 10px; - color: var(--s-ink); -} -.s-hg-wrong { color: #b34d4d; letter-spacing: 2px; font-weight: 700; } -.s-hg-keys { - display: grid; - grid-template-columns: repeat(13, 1fr); - gap: 5px; -} -.s-hg-key { - padding: 8px 0; - border-radius: 8px; - border: var(--s-border-thin); - background: #fff; - color: var(--s-ink); - font: 700 13px/1 var(--s-body); - cursor: pointer; - transition: transform 80ms ease; -} -.s-hg-key:not(:disabled):hover { transform: translateY(-1px); } -.s-hg-key.used { cursor: default; opacity: 0.55; } -.s-hg-key.hit { background: #c9f3d4; opacity: 1; } -.s-hg-key.miss { background: #ffd4d4; } - -@media (max-width: 640px) { - .s-hg-keys { grid-template-columns: repeat(9, 1fr); } - .s-ttt-grid { grid-template-columns: repeat(3, 64px); grid-template-rows: repeat(3, 64px); } - .s-ttt-cell { font-size: 30px; } -} - /* ====== Project detail page ====== */ .s-proj { padding: 40px 0 20px; @@ -755,6 +601,7 @@ a { color: inherit; } .pg-nav-mid a.active { background: var(--pg-ink); color: var(--pg-bg); } .pg-nav-mid a:hover:not(.active) { background: rgba(29,24,48,0.08); } .pg-star-btn { + display: inline-flex; align-items: center; gap: 6px; padding: 9px 18px; border-radius: var(--pg-radius-pill); background: var(--pg-ink); color: var(--pg-bg); border: var(--pg-border-thin); box-shadow: 3px 3px 0 var(--pg-accent); @@ -766,7 +613,7 @@ a { color: inherit; } /* sidebar */ .pg-side { - width: 280px; flex-shrink: 0; + width: var(--pg-side-w, 280px); flex-shrink: 0; position: relative; display: flex; flex-direction: column; background: rgba(255, 255, 255, 0.45); } @@ -781,6 +628,13 @@ a { color: inherit; } } .pg-side-search input { flex: 1; background: transparent; border: 0; outline: 0; font-size: 13px; color: var(--pg-ink); font-family: inherit; } .pg-side-search input::placeholder { color: rgba(29,24,48,0.4); } +.pg-side-search > svg { color: var(--pg-muted); flex-shrink: 0; } +.pg-side-search-clear { + display: inline-flex; align-items: center; justify-content: center; + padding: 2px; border: 0; background: transparent; cursor: pointer; + color: var(--pg-muted); border-radius: 6px; flex-shrink: 0; +} +.pg-side-search-clear:hover { color: var(--pg-ink); background: rgba(29,24,48,0.07); } .pg-side-list { flex: 1; overflow-y: auto; padding: 4px 14px 24px; } .pg-group { margin-top: 14px; } .pg-group-h { @@ -885,7 +739,7 @@ a { color: inherit; } .pg-btn .ic { font-size: 13px; } /* editor + console split */ -.pg-split { flex: 1; display: flex; gap: 14px; min-height: 0; } +.pg-split { flex: 1; display: flex; gap: 0; min-height: 0; } .pg-split.row { flex-direction: row; } .pg-window { display: flex; flex-direction: column; min-height: 0; @@ -919,14 +773,14 @@ a { color: inherit; } .pg-winbar .iconbtn:hover { background: rgba(29,24,48,0.1); border-color: var(--pg-ink); } /* editor pane */ -.pg-editor { flex: 1.55 1 0; min-width: 0; } +.pg-editor { flex: var(--pg-ed-grow, 1.55) 1 0; min-width: 0; } .pg-editor-body { flex: 1; min-height: 0; min-width: 0; display: flex; } .pg-editor-body > * { flex: 1; min-width: 0; height: 100%; } .pg-editor-body .cm-editor { height: 100%; } .pg-editor-body .cm-scroller { font-size: 13.5px; padding: 6px 0; } /* console pane */ -.pg-console { flex: 1 1 0; min-width: 280px; } +.pg-console { flex: var(--pg-co-grow, 1) 1 0; min-width: 280px; } .pg-console-body { flex: 1; min-height: 0; display: flex; background: var(--pg-dark); @@ -939,15 +793,17 @@ a { color: inherit; } /* responsive */ @media (max-width: 1200px) { - .pg-split.row { flex-direction: column; } + .pg-split.row { flex-direction: column; gap: 14px; } .pg-split.row .pg-editor { flex: 1.4 1 0; } .pg-split.row .pg-console { flex: 1 1 0; min-height: 200px; min-width: 0; } + .pg-resize-split { display: none; } } @media (max-width: 1100px) { .pg-side { width: 240px; } } @media (max-width: 880px) { .pg-app { height: auto; min-height: 100vh; } .pg-main { flex-direction: column; } .pg-side { width: 100%; } + .pg-side-resize { display: none; } .pg-nav-mid { display: none; } .pg-split.row { min-height: 70vh; } } @@ -956,3 +812,228 @@ a { color: inherit; } .pg-toolbar-right { justify-content: flex-end; } } + +/* ---- Runnable badge (live green dot in a rounded green chip) ---- */ +.s-runnable { + display: inline-flex; align-items: center; gap: 7px; + padding: 6px 13px 6px 11px; border-radius: var(--s-radius-pill); + background: linear-gradient(180deg, #e8fff0, #d2f9df); + border: 1.5px solid var(--s-ink); + box-shadow: 2px 2px 0 var(--s-ink); + color: #0a5c34; + font-family: var(--s-display, var(--s-mono)); + font-size: 12px; font-weight: 800; + letter-spacing: -0.005em; + white-space: nowrap; line-height: 1; +} +.s-runnable.sm { + padding: 4px 9px 4px 8px; font-size: 10px; gap: 5px; + box-shadow: 1.5px 1.5px 0 var(--s-ink); +} +.s-runnable-dot { + width: 9px; height: 9px; border-radius: 50%; + background: #16b86c; border: 1px solid #0a5c34; + flex-shrink: 0; +} +.s-runnable.sm .s-runnable-dot { width: 7px; height: 7px; } +.s-meta-pill.desk { + background: rgba(29, 24, 48, 0.06); color: rgba(29, 24, 48, 0.7); + border: 1.5px solid rgba(29, 24, 48, 0.35); +} + +/* ---- Playground sidebar: per-item runnable dot ---- */ +.pg-run-dot { + width: 7px; height: 7px; border-radius: 50%; + background: var(--s-accent-3); + box-shadow: 0 0 0 2.5px rgba(28, 201, 124, 0.25); + flex-shrink: 0; +} + +/* ---- Project README (rendered markdown) ---- */ +.s-readme { + background: var(--s-surface); + border: var(--s-border); + border-radius: var(--s-radius-lg); + box-shadow: var(--s-shadow); + padding: 36px 40px; + margin: 8px 0 48px; + line-height: 1.7; + overflow-wrap: anywhere; +} +.s-readme > :first-child { margin-top: 0; } +.s-readme > :last-child { margin-bottom: 0; } +.s-readme h1, .s-readme h2, .s-readme h3, .s-readme h4 { + font-family: var(--s-display); font-weight: 900; + letter-spacing: -0.02em; line-height: 1.15; + margin: 1.6em 0 0.5em; +} +.s-readme h1 { font-size: 30px; } +.s-readme h2 { + font-size: 23px; + padding-bottom: 6px; border-bottom: 2px solid rgba(29, 24, 48, 0.12); +} +.s-readme h3 { font-size: 18px; } +.s-readme h4 { font-size: 15px; } +.s-readme p { margin: 0.8em 0; } +.s-readme a { color: var(--s-accent); font-weight: 600; } +.s-readme a:hover { text-decoration: underline; } +.s-readme ul, .s-readme ol { margin: 0.8em 0; padding-left: 1.5em; } +.s-readme li { margin: 0.3em 0; } +.s-readme img { max-width: 100%; height: auto; border-radius: var(--s-radius-md); } +.s-readme code { + font-family: var(--s-mono); font-size: 0.86em; + background: rgba(29, 24, 48, 0.07); + padding: 2px 6px; border-radius: 6px; +} +.s-readme pre { + background: var(--s-ink); color: var(--s-bg); + border-radius: var(--s-radius-md); + padding: 16px 18px; margin: 1em 0; overflow-x: auto; +} +.s-readme pre code { + background: none; padding: 0; font-size: 13px; color: inherit; +} +.s-readme blockquote { + margin: 1em 0; padding: 6px 16px; + border-left: 4px solid var(--s-accent); + background: rgba(255, 122, 89, 0.07); + color: rgba(29, 24, 48, 0.8); +} +.s-readme hr { + border: 0; border-top: 2px dashed rgba(29, 24, 48, 0.18); margin: 1.6em 0; +} +.s-readme table { + border-collapse: collapse; margin: 1em 0; width: 100%; font-size: 14px; +} +.s-readme th, .s-readme td { + border: 1.5px solid rgba(29, 24, 48, 0.2); padding: 7px 12px; text-align: left; +} +.s-readme th { background: rgba(29, 24, 48, 0.05); font-weight: 700; } +@media (max-width: 640px) { + .s-readme { padding: 24px 20px; } +} + +/* ---- Contributor credit (project detail page) ---- */ +.s-proj-credit { + display: inline-flex; align-items: center; gap: 8px; + padding: 6px 14px 6px 8px; border-radius: var(--s-radius-pill); + border: 1.5px solid var(--s-ink); background: var(--s-surface); + box-shadow: 2px 2px 0 var(--s-ink); + font-size: 13px; font-weight: 800; color: var(--s-ink); + text-decoration: none; transition: transform .1s, box-shadow .1s; +} +.s-proj-credit:hover { + transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--s-ink); +} +.s-proj-credit img { + width: 22px; height: 22px; border-radius: 50%; + border: 1.5px solid var(--s-ink); background: var(--s-bg); +} +.s-proj-credit-by { + font-size: 10px; font-weight: 700; letter-spacing: 0.04em; + text-transform: uppercase; color: rgba(29, 24, 48, 0.55); +} + +/* ---- Playground: collapsible README strip (above the editor) ---- */ +.pg-readme-bar { + flex-shrink: 0; + border: var(--pg-border); border-radius: var(--pg-radius); + background: var(--pg-paper); box-shadow: var(--pg-shadow); + overflow: hidden; +} +.pg-readme-toggle { + display: flex; align-items: center; gap: 8px; width: 100%; + padding: 10px 18px; cursor: pointer; + background: var(--pg-paper-warm); border: 0; + font-family: var(--s-display, var(--pg-mono)); + font-size: 13px; font-weight: 800; color: var(--pg-ink); + text-align: left; +} +.pg-readme-bar.open .pg-readme-toggle { + border-bottom: var(--pg-border-thin); +} +.pg-readme-emoji { color: var(--pg-accent); } +.pg-readme-label { letter-spacing: -0.01em; } +.pg-readme-hint { + font-family: var(--pg-mono); font-size: 11px; font-weight: 600; + color: var(--pg-muted); +} +/* The clearly-visible collapse / expand button at the end of the header */ +.pg-readme-btn { + margin-left: auto; + display: inline-flex; align-items: center; gap: 5px; + padding: 5px 11px 5px 8px; border-radius: var(--pg-radius-pill); + border: var(--pg-border-thin); background: var(--pg-paper); + box-shadow: 2px 2px 0 var(--pg-ink); + font-family: var(--pg-mono); font-size: 11px; font-weight: 800; + letter-spacing: 0.02em; text-transform: uppercase; + color: var(--pg-ink); + transition: transform .1s, box-shadow .1s; +} +.pg-readme-toggle:hover .pg-readme-btn { + transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--pg-ink); +} +.pg-readme-toggle:active .pg-readme-btn { + transform: translate(2px, 2px); box-shadow: 0 0 0 var(--pg-ink); +} +.pg-readme-bar.open .pg-readme-btn { background: var(--pg-accent); } +.pg-readme-content { + margin: 0; border: 0; border-radius: 0; box-shadow: none; + background: var(--pg-paper); + max-height: 23vh; overflow: auto; + padding: 18px 26px; +} +.pg-readme-content > :first-child { margin-top: 0; } +.pg-readme-content h1 { font-size: 22px; } +.pg-readme-content h2 { font-size: 18px; } +.pg-readme-content h3 { font-size: 15px; } + +/* ---- Playground: draggable resize handles ---- */ +.pg-resize { + flex-shrink: 0; position: relative; z-index: 5; + touch-action: none; -webkit-user-select: none; user-select: none; +} +.pg-resize-x { cursor: col-resize; } +.pg-resize-y { cursor: row-resize; } +.pg-resize:focus-visible { + outline: 2.5px solid var(--pg-accent); outline-offset: -2px; +} +.pg-resize-grip { + display: block; border-radius: 99px; + background: var(--pg-ink); opacity: 0.22; + transition: opacity .12s ease, background .12s ease, + width .12s ease, height .12s ease; +} +.pg-resize:hover .pg-resize-grip, +.pg-resize.dragging .pg-resize-grip { + opacity: 1; background: var(--pg-accent); +} + +/* between editor and console */ +.pg-resize-split { + width: 16px; align-self: stretch; + display: flex; align-items: center; justify-content: center; +} +.pg-resize-split .pg-resize-grip { width: 4px; height: 40px; } +.pg-resize-split:hover .pg-resize-grip, +.pg-resize-split.dragging .pg-resize-grip { height: 60px; } + +/* sidebar right edge */ +.pg-side-resize { + position: absolute; top: 0; right: -7px; width: 14px; height: 100%; + display: flex; align-items: center; justify-content: center; +} +.pg-side-resize .pg-resize-grip { width: 4px; height: 46px; opacity: 0.16; } +.pg-side-resize:hover .pg-resize-grip, +.pg-side-resize.dragging .pg-resize-grip { height: 72px; } + +/* under the README panel */ +.pg-resize-readme { + height: 13px; width: 100%; + display: flex; align-items: center; justify-content: center; + background: var(--pg-paper-warm); + border-top: var(--pg-border-thin); +} +.pg-resize-readme .pg-resize-grip { width: 46px; height: 4px; opacity: 0.32; } +.pg-resize-readme:hover .pg-resize-grip, +.pg-resize-readme.dragging .pg-resize-grip { width: 70px; } diff --git a/web/src/app/playground/[id]/page.tsx b/web/src/app/playground/[id]/page.tsx index 4e932243d..beba91f73 100644 --- a/web/src/app/playground/[id]/page.tsx +++ b/web/src/app/playground/[id]/page.tsx @@ -2,10 +2,11 @@ import fs from "fs"; import path from "path"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; +import { marked } from "marked"; import PlaygroundNav from "@/components/PlaygroundNav"; import PlaygroundSidebar from "@/components/PlaygroundSidebar"; import Playground, { type PlaygroundProject } from "@/components/Playground"; -import { PROJECTS, getProject, projectFolderUrl } from "@/lib/data"; +import { CATALOG, getCatalogProject, catalogFolderUrl } from "@/lib/catalog"; interface Params { params: Promise<{ id: string }>; @@ -14,9 +15,11 @@ interface Params { // micropip packages each playground project needs (most need none). const PACKAGES: Record = { qr: ["qrcode"], + aes256: ["pycryptodome"], + "pokemon-battle": ["numpy"], }; -// Read the curated source for a project from public/playground at build time. +// Read a project's playground source from public/playground at build time. function readSource(id: string): string | null { try { return fs.readFileSync( @@ -28,13 +31,27 @@ function readSource(id: string): string | null { } } +// Read and render the project's README (one level up, in projects//). +function readReadmeHtml(folder: string): string | null { + const dir = path.join(process.cwd(), "..", "projects", folder); + for (const name of ["README.md", "readme.md", "Readme.md"]) { + const file = path.join(dir, name); + if (fs.existsSync(file)) { + return marked.parse(fs.readFileSync(file, "utf8"), { + async: false, + }) as string; + } + } + return null; +} + export function generateStaticParams() { - return PROJECTS.filter((p) => p.playground).map((p) => ({ id: p.id })); + return CATALOG.filter((p) => p.hasPlayground).map((p) => ({ id: p.id })); } export async function generateMetadata({ params }: Params): Promise { const { id } = await params; - const p = getProject(id); + const p = getCatalogProject(id); if (!p) return { title: "Playground" }; const description = `Edit and run the ${p.name} Python project live in your browser. ${p.blurb}`; return { @@ -51,8 +68,8 @@ export async function generateMetadata({ params }: Params): Promise { export default async function ProjectPlaygroundPage({ params }: Params) { const { id } = await params; - const p = getProject(id); - if (!p || !p.playground) notFound(); + const p = getCatalogProject(id); + if (!p || !p.hasPlayground) notFound(); const source = readSource(id); if (source == null) notFound(); @@ -61,13 +78,15 @@ export default async function ProjectPlaygroundPage({ params }: Params) { id: p.id, name: p.name, emoji: p.emoji, - deps: p.deps, + deps: "", lines: p.lines, blurb: p.blurb, author: p.author, - folderUrl: projectFolderUrl(p), + folderUrl: catalogFolderUrl(p), }; + const readmeHtml = readReadmeHtml(p.folder); + return (
    @@ -77,6 +96,7 @@ export default async function ProjectPlaygroundPage({ params }: Params) { project={project} initialCode={source} packages={PACKAGES[id] || []} + readmeHtml={readmeHtml} />
diff --git a/web/src/app/projects/[id]/page.tsx b/web/src/app/projects/[id]/page.tsx index 37100e792..77af6cd40 100644 --- a/web/src/app/projects/[id]/page.tsx +++ b/web/src/app/projects/[id]/page.tsx @@ -1,28 +1,47 @@ +import fs from "fs"; +import path from "path"; import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; +import { marked } from "marked"; import SiteNav from "@/components/SiteNav"; import SiteFooter from "@/components/SiteFooter"; -import TryItPanel from "@/components/TryItPanel"; -import ProjectCredit from "@/components/ProjectCredit"; -import { PROJECTS, REPO_URL, getProject } from "@/lib/data"; +import RunnableBadge from "@/components/RunnableBadge"; +import { REPO_URL } from "@/lib/data"; +import { CATALOG, getCatalogProject, catalogFolderUrl } from "@/lib/catalog"; interface Params { params: Promise<{ id: string }>; } -// Pre-render one static page per project. +// Read a project's README from projects// at build time and render it +// to HTML. The catalog lives in web/, the projects/ folder is one level up. +function readReadmeHtml(folder: string): string | null { + const dir = path.join(process.cwd(), "..", "projects", folder); + for (const name of ["README.md", "readme.md", "Readme.md"]) { + const file = path.join(dir, name); + if (fs.existsSync(file)) { + const md = fs.readFileSync(file, "utf8"); + return marked.parse(md, { async: false }) as string; + } + } + return null; +} + +// Pre-render one static page per catalog project. export function generateStaticParams() { - return PROJECTS.map((p) => ({ id: p.id })); + return CATALOG.map((p) => ({ id: p.id })); } export async function generateMetadata({ params }: Params): Promise { const { id } = await params; - const p = getProject(id); + const p = getCatalogProject(id); if (!p) return { title: "Project not found" }; const title = `${p.name} — Python beginner project`; - const description = `${p.blurb} A ${p.lines}-line Python project (${p.deps}). ${ - p.playground ? "Edit and run it live in your browser." : "Read the code on GitHub." + const description = `${p.blurb} ${ + p.hasPlayground + ? "Edit and run it live in your browser." + : "Read the code and README on GitHub." }`; return { title, @@ -40,9 +59,12 @@ export async function generateMetadata({ params }: Params): Promise { export default async function ProjectPage({ params }: Params) { const { id } = await params; - const p = getProject(id); + const p = getCatalogProject(id); if (!p) notFound(); + const readmeHtml = readReadmeHtml(p.folder); + const folderUrl = catalogFolderUrl(p); + const jsonLd = { "@context": "https://schema.org", "@type": "SoftwareSourceCode", @@ -51,11 +73,6 @@ export default async function ProjectPage({ params }: Params) { programmingLanguage: "Python", codeRepository: REPO_URL, url: `https://pybegin.pages.dev/projects/${p.id}/`, - author: { - "@type": "Person", - name: p.author, - url: `https://github.com/${p.author}`, - }, }; return ( @@ -79,48 +96,80 @@ export default async function ProjectPage({ params }: Params) {

{p.blurb}

{p.lines} lines - {p.deps} - {p.cat} - {p.playground && ▶ Playground} -
-
- + {p.runnable ? ( + + ) : ( + 🖥 Desktop only + )}
- {p.playground && ( + {p.hasPlayground && ( ▶ Open in playground )} - View on GitHub → + View source folder on GitHub → + {p.author && ( + + contributed by + + @{p.author} + + )}
- {p.runnable && } + {readmeHtml ? ( +
+ ) : ( +
+

This project does not have a README yet.

+
+ )} + diff --git a/web/src/app/sitemap.ts b/web/src/app/sitemap.ts index 67adab671..0a21ee915 100644 --- a/web/src/app/sitemap.ts +++ b/web/src/app/sitemap.ts @@ -1,5 +1,5 @@ import type { MetadataRoute } from "next"; -import { PROJECTS } from "@/lib/data"; +import { CATALOG } from "@/lib/catalog"; const BASE = "https://pybegin.pages.dev"; @@ -11,13 +11,13 @@ export default function sitemap(): MetadataRoute.Sitemap { { url: `${BASE}/`, lastModified: now, priority: 1 }, { url: `${BASE}/playground/`, lastModified: now, priority: 0.8 }, ]; - for (const p of PROJECTS) { + for (const p of CATALOG) { entries.push({ url: `${BASE}/projects/${p.id}/`, lastModified: now, priority: 0.7, }); - if (p.playground) { + if (p.hasPlayground) { entries.push({ url: `${BASE}/playground/${p.id}/`, lastModified: now, diff --git a/web/src/components/Playground.tsx b/web/src/components/Playground.tsx index 8f738f9bc..c3d1770f9 100644 --- a/web/src/components/Playground.tsx +++ b/web/src/components/Playground.tsx @@ -6,14 +6,35 @@ // (public/pyodide-worker.js); input() is bridged via a SharedArrayBuffer. // The editor lints against Pyodide's compile() — real syntax-error squiggles. -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type CSSProperties, +} from "react"; import CodeMirror from "@uiw/react-codemirror"; import { python } from "@codemirror/lang-python"; import { linter, lintGutter, type Diagnostic } from "@codemirror/lint"; import { Terminal } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; import "@xterm/xterm/css/xterm.css"; +import { + Play, + Square, + RotateCcw, + BookOpen, + ChevronDown, + ChevronRight, + Check, + Copy, + Eraser, + Code2, + ArrowUpRight, +} from "lucide-react"; import { pybeginPaper } from "@/lib/cmTheme"; +import ResizeHandle from "./ResizeHandle"; type Status = "idle" | "running" | "waiting" | "done" | "error"; @@ -74,18 +95,33 @@ function WinDots() { const nl = (s: string) => s.replace(/\r?\n/g, "\r\n"); +const clamp = (v: number, lo: number, hi: number) => + Math.max(lo, Math.min(hi, v)); + +// Stable empty-array default so the prop reference doesn't change each render. +const NO_PACKAGES: string[] = []; + export default function Playground({ project, initialCode, - packages = [], + packages = NO_PACKAGES, + readmeHtml, }: { project: PlaygroundProject; initialCode: string; packages?: string[]; + readmeHtml?: string | null; }) { const [code, setCode] = useState(initialCode); const [status, setStatus] = useState("idle"); const [copied, setCopied] = useState(false); + const [showReadme, setShowReadme] = useState(true); + + // Resizable layout — editor/console split ratio and README panel height. + const [editorPct, setEditorPct] = useState(58); + const [readmeHeight, setReadmeHeight] = useState(null); + const splitRef = useRef(null); + const readmeBodyRef = useRef(null); const workerRef = useRef(null); const sabRef = useRef(null); @@ -210,7 +246,9 @@ export default function Playground({ term.loadAddon(fit); term.open(termHostRef.current); fit.fit(); - term.writeln("\x1b[2;3m# Output appears here. Hit ▶ Run to start.\x1b[0m"); + term.writeln( + "\x1b[2;3m# Output appears here. Hit Run code to start.\x1b[0m", + ); term.onData((d) => { if (!runActiveRef.current) return; @@ -265,7 +303,13 @@ export default function Playground({ // can lint and the first Run is instant. useEffect(() => { ensureWorker(); - return () => workerRef.current?.terminate(); + return () => { + // Null the ref too — otherwise React StrictMode's mount/unmount/mount + // cycle leaves a terminated worker in the ref and ensureWorker reuses it, + // so the first Run posts to a dead worker and hangs on "Running…". + workerRef.current?.terminate(); + workerRef.current = null; + }; }, [ensureWorker]); // --- editor lint: round-trip the source to the worker's compile() check --- @@ -361,6 +405,21 @@ export default function Playground({ }); }; + // Drag the bar between editor and console — convert px delta to a percentage + // of the split container so the two panes keep filling the available width. + const resizeSplit = useCallback((dx: number) => { + const w = splitRef.current?.clientWidth ?? 1; + setEditorPct((p) => clamp(p + (dx / w) * 100, 24, 80)); + }, []); + + // Drag the bar under the README to set its panel height. + const resizeReadme = useCallback((dy: number) => { + setReadmeHeight((h) => { + const cur = h ?? readmeBodyRef.current?.offsetHeight ?? 240; + return clamp(cur + dy, 90, 560); + }); + }, []); + const lineCount = code.split("\n").length; const isRunning = status === "running" || status === "waiting"; @@ -374,20 +433,27 @@ export default function Playground({

{project.name}

Python - {project.deps} + {project.deps && {project.deps}} {project.lines} lines {project.blurb && ( · {project.blurb} )}
- {project.author && project.folderUrl && ( + {project.folderUrl && ( - 🧑‍💻 Original project by @{project.author} — view source on GitHub ↗ + )} @@ -399,21 +465,83 @@ export default function Playground({ {!isRunning ? ( ) : ( )} + {/* README — collapsible, resizable strip above the editor */} + {readmeHtml && ( +
+ + {showReadme && ( + <> +
+ + + )} +
+ )} + {/* editor + console */} -
+
{/* editor */}
@@ -424,7 +552,11 @@ export default function Playground({
{lineCount} lines
@@ -439,6 +571,14 @@ export default function Playground({
+ {/* drag bar between editor and console */} + + {/* console — real terminal */}
@@ -452,7 +592,7 @@ export default function Playground({ onClick={() => termRef.current?.clear()} title="Clear console" > - ⌫ +
diff --git a/web/src/components/PlaygroundNav.tsx b/web/src/components/PlaygroundNav.tsx index ff5db3e32..cfb099414 100644 --- a/web/src/components/PlaygroundNav.tsx +++ b/web/src/components/PlaygroundNav.tsx @@ -1,6 +1,7 @@ // Slim top nav for the full-bleed playground workspace. import Link from "next/link"; +import { Star } from "lucide-react"; import StickerLogo from "./StickerLogo"; import { REPO_URL } from "@/lib/data"; @@ -26,7 +27,8 @@ export default function PlaygroundNav() {
diff --git a/web/src/components/PlaygroundSidebar.tsx b/web/src/components/PlaygroundSidebar.tsx index af3e68985..d0ee02f87 100644 --- a/web/src/components/PlaygroundSidebar.tsx +++ b/web/src/components/PlaygroundSidebar.tsx @@ -1,50 +1,65 @@ "use client"; -// Playground sidebar — searchable, with collapsible category groups -// (open by default, collapse state persisted to localStorage; an active -// search force-opens every group so matches stay visible). +// Playground sidebar — lists every project in the repo, searchable. +// Projects are split into two collapsible groups: +// • Runnable → open live in the in-browser playground +// • Desktop → can't run in-browser; link to the project's detail page +// Collapse state is persisted to localStorage; an active search force-opens +// every group so matches stay visible. -import { useEffect, useState } from "react"; +import { useEffect, useState, type CSSProperties } from "react"; import Link from "next/link"; -import { CATEGORIES, PROJECTS } from "@/lib/data"; +import { Search, X } from "lucide-react"; +import { CATALOG } from "@/lib/catalog"; +import ResizeHandle from "./ResizeHandle"; -const PLAYABLE = PROJECTS.filter((p) => p.playground); +const SIDE_MIN = 210; +const SIDE_MAX = 520; interface SideItem { id: string; name: string; emoji: string; - lines: number; href: string; + runnable: boolean; } -interface SideGroup { - id: string; - label: string; - items: SideItem[]; -} +const RUNNABLE_ITEMS: SideItem[] = CATALOG.filter((p) => p.hasPlayground).map( + (p) => ({ + id: p.id, + name: p.name, + emoji: p.emoji, + href: `/playground/${p.id}`, + runnable: true, + }), +); -const GROUPS: SideGroup[] = [ +const DESKTOP_ITEMS: SideItem[] = CATALOG.filter((p) => !p.hasPlayground).map( + (p) => ({ + id: p.id, + name: p.name, + emoji: p.emoji, + href: `/projects/${p.id}`, + runnable: false, + }), +); + +const GROUPS: { id: string; label: string; items: SideItem[] }[] = [ { id: "scratch", label: "Scratchpad", items: [ - { id: "scratch", name: "Blank scratchpad", emoji: "✏️", lines: 9, href: "/playground" }, + { + id: "scratch", + name: "Blank scratchpad", + emoji: "✏️", + href: "/playground", + runnable: true, + }, ], }, - ...CATEGORIES.filter((c) => c.id !== "all") - .map((c) => ({ - id: c.id, - label: c.name, - items: PLAYABLE.filter((p) => p.cat === c.id).map((p) => ({ - id: p.id, - name: p.name, - emoji: p.emoji, - lines: p.lines, - href: `/playground/${p.id}`, - })), - })) - .filter((g) => g.items.length > 0), + { id: "runnable", label: "Runnable in browser", items: RUNNABLE_ITEMS }, + { id: "desktop", label: "Desktop projects", items: DESKTOP_ITEMS }, ]; function Chevron() { @@ -67,12 +82,16 @@ export default function PlaygroundSidebar({ activeId?: string; }) { const [q, setQ] = useState(""); - const [collapsed, setCollapsed] = useState>({}); + // Desktop group is huge — start it collapsed unless the user changed it. + const [collapsed, setCollapsed] = useState>({ + desktop: true, + }); + // User-draggable sidebar width — resets to the default on every refresh. + const [width, setWidth] = useState(280); - // Load persisted collapse state once on mount. useEffect(() => { try { - const saved = localStorage.getItem("pg_collapsed_v1"); + const saved = localStorage.getItem("pg_collapsed_v2"); if (saved) setCollapsed(JSON.parse(saved)); } catch { /* ignore */ @@ -83,7 +102,7 @@ export default function PlaygroundSidebar({ setCollapsed((prev) => { const next = { ...prev, [gid]: !prev[gid] }; try { - localStorage.setItem("pg_collapsed_v1", JSON.stringify(next)); + localStorage.setItem("pg_collapsed_v2", JSON.stringify(next)); } catch { /* ignore */ } @@ -94,28 +113,33 @@ export default function PlaygroundSidebar({ !q || it.name.toLowerCase().includes(q.toLowerCase()); return ( -
); diff --git a/web/src/components/ProjectModal.tsx b/web/src/components/ProjectModal.tsx index f36bd404f..a2d7601c0 100644 --- a/web/src/components/ProjectModal.tsx +++ b/web/src/components/ProjectModal.tsx @@ -2,8 +2,8 @@ import { useEffect } from "react"; import Link from "next/link"; -import TryItPanel from "./TryItPanel"; import ProjectCredit from "./ProjectCredit"; +import RunnableBadge from "./RunnableBadge"; import { REPO_URL } from "@/lib/data"; import type { Project } from "@/types"; @@ -43,6 +43,7 @@ export default function ProjectModal({ {p.lines} lines {p.deps} {p.cat} + {p.playground && }
@@ -54,7 +55,6 @@ export default function ProjectModal({
$ python {p.id}.py
- {p.runnable && }
{p.playground && ( diff --git a/web/src/components/ResizeHandle.tsx b/web/src/components/ResizeHandle.tsx new file mode 100644 index 000000000..34dd13576 --- /dev/null +++ b/web/src/components/ResizeHandle.tsx @@ -0,0 +1,72 @@ +"use client"; + +// A draggable splitter bar. Reports the pointer delta (in px) on every move so +// the parent can grow/shrink whatever panel sits next to it. Pointer capture +// keeps the drag alive even when the cursor outruns the thin bar. Arrow keys +// nudge it too, so the splitter is keyboard-accessible. + +import { useState } from "react"; + +export default function ResizeHandle({ + orientation, + onResize, + className = "", + ariaLabel, +}: { + /** "x" = drag left/right (col-resize), "y" = drag up/down (row-resize). */ + orientation: "x" | "y"; + /** Called with the pixel delta since the last move. */ + onResize: (delta: number) => void; + className?: string; + ariaLabel?: string; +}) { + const [dragging, setDragging] = useState(false); + + return ( +
{ + e.preventDefault(); + const el = e.currentTarget; + el.setPointerCapture(e.pointerId); + el.dataset.last = String(orientation === "x" ? e.clientX : e.clientY); + setDragging(true); + }} + onPointerMove={(e) => { + const el = e.currentTarget; + if (!el.hasPointerCapture(e.pointerId)) return; + const pos = orientation === "x" ? e.clientX : e.clientY; + const delta = pos - Number(el.dataset.last); + if (delta !== 0) { + el.dataset.last = String(pos); + onResize(delta); + } + }} + onPointerUp={(e) => { + e.currentTarget.releasePointerCapture(e.pointerId); + setDragging(false); + }} + onKeyDown={(e) => { + const step = e.shiftKey ? 48 : 16; + const back = orientation === "x" ? "ArrowLeft" : "ArrowUp"; + const fwd = orientation === "x" ? "ArrowRight" : "ArrowDown"; + if (e.key === back) { + e.preventDefault(); + onResize(-step); + } else if (e.key === fwd) { + e.preventDefault(); + onResize(step); + } + }} + > +
+ ); +} diff --git a/web/src/components/RunnableBadge.tsx b/web/src/components/RunnableBadge.tsx new file mode 100644 index 000000000..81fd67dd5 --- /dev/null +++ b/web/src/components/RunnableBadge.tsx @@ -0,0 +1,18 @@ +// Small "Runnable" pill — a green dot inside a rounded green chip. Marks +// projects that run live in the in-browser Pyodide playground. + +export default function RunnableBadge({ + size = "md", +}: { + size?: "sm" | "md"; +}) { + return ( + + + ); +} diff --git a/web/src/components/TryItPanel.tsx b/web/src/components/TryItPanel.tsx deleted file mode 100644 index dd1795778..000000000 --- a/web/src/components/TryItPanel.tsx +++ /dev/null @@ -1,406 +0,0 @@ -"use client"; - -// Try It — live demo panel for the runnable projects. Renders a simple input -// form for stateless projects (rps, bmi, qr, madlibs) and dedicated widgets -// for the turn-based ones (tictactoe, hangman). All POSTs go to the -// Cloudflare Worker backend. - -import { useEffect, useState } from "react"; -import { - call, - defaults, - inputsFor, - isInteractive, - type InputField, - type SelectOption, -} from "@/lib/api"; -import type { Project } from "@/types"; - -export default function TryItPanel({ project }: { project: Project }) { - if (isInteractive(project.id)) { - if (project.id === "tictactoe") return ; - if (project.id === "hangman") return ; - } - return ; -} - -function castOption(field: InputField, raw: string): string | number { - const opts = field.options; - if (opts && opts.length && typeof opts[0] === "object") { - const match = (opts as SelectOption[]).find((o) => String(o.value) === raw); - return match ? match.value : raw; - } - return raw; -} - -type FormState = Record; - -function SimpleRunner({ project }: { project: Project }) { - const fields = inputsFor(project.id); - const [form, setForm] = useState(() => defaults(project.id)); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [out, setOut] = useState | null>(null); - - const run = async () => { - setError(null); - setLoading(true); - try { - const res = await call>(project.id, form); - setOut(res); - } catch (e) { - setError(e instanceof Error ? e.message : "Request failed"); - setOut(null); - } finally { - setLoading(false); - } - }; - - return ( -
-
▶ Try it live
-
- {fields.map((f) => ( - - ))} -
- - {error &&
⚠ {error}
} - {out && } -
- ); -} - -function SimpleOutput({ - projectId, - out, -}: { - projectId: string; - out: Record; -}) { - if (projectId === "qr" && out.png_b64) { - return ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - Generated QR code -
- ); - } - if (projectId === "bmi") { - return ( -
-
{String(out.bmi)}
-
{String(out.category)}
-
- ); - } - if (projectId === "rps") { - const verdict = - ({ win: "🎉 You win", lose: "😅 You lose", draw: "🤝 Draw" } as Record< - string, - string - >)[String(out.result)] || String(out.result); - return ( -
-
- You: {String(out.player)} · CPU: {String(out.cpu)} -
-
- {verdict} -
-
- ); - } - if (projectId === "madlibs") { - return ( -
-
{String(out.story)}
-
- ); - } - return
{JSON.stringify(out, null, 2)}
; -} - -// ---- Tic-Tac-Toe ---- - -const EMPTY_BOARD = ["", "", "", "", "", "", "", "", ""]; - -interface TttResponse { - board: string[]; - status: "ongoing" | "win" | "draw"; - winner?: string; -} - -function TicTacToePlay() { - const [board, setBoard] = useState(EMPTY_BOARD); - const [status, setStatus] = useState("ongoing"); - const [winner, setWinner] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const reset = () => { - setBoard(EMPTY_BOARD); - setStatus("ongoing"); - setWinner(null); - setError(null); - }; - - const play = async (idx: number) => { - if (board[idx] || status !== "ongoing" || loading) return; - setError(null); - setLoading(true); - try { - const res = await call("tictactoe", { - board, - player: "X", - move: idx, - }); - setBoard(res.board); - setStatus(res.status); - setWinner(res.winner || null); - } catch (e) { - setError(e instanceof Error ? e.message : "Request failed"); - } finally { - setLoading(false); - } - }; - - const headline = - status === "win" - ? winner === "X" - ? "🎉 You win" - : "🤖 CPU wins" - : status === "draw" - ? "🤝 Draw" - : "Your move (X)"; - - return ( -
-
▶ Try it live · vs the bot
-
-
{headline}
- -
-
- {board.map((cell, i) => ( - - ))} -
- {error &&
⚠ {error}
} -
- ); -} - -// ---- Hangman ---- - -const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); - -interface HangmanState { - mask: string; - wrong: string[]; - tries_left: number; - max_tries: number; - status: "ongoing" | "win" | "lose"; - word_length: number; - word?: string; -} - -function HangmanPlay() { - const [seed, setSeed] = useState(() => Math.floor(Math.random() * 1e9)); - const [difficulty, setDifficulty] = useState("medium"); - const [guessed, setGuessed] = useState([]); - const [state, setState] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const refresh = async ( - nextGuessed: string[], - nextDifficulty = difficulty, - nextSeed = seed, - ) => { - setError(null); - setLoading(true); - try { - const res = await call("hangman", { - word_seed: nextSeed, - guessed: nextGuessed, - difficulty: nextDifficulty, - }); - setState(res); - } catch (e) { - setError(e instanceof Error ? e.message : "Request failed"); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - refresh([], difficulty, seed); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const guess = (letter: string) => { - if (loading || !state || state.status !== "ongoing" || guessed.includes(letter)) - return; - const next = [...guessed, letter]; - setGuessed(next); - refresh(next); - }; - - const newWord = (nextDifficulty = difficulty) => { - const s = Math.floor(Math.random() * 1e9); - setSeed(s); - setGuessed([]); - setDifficulty(nextDifficulty); - refresh([], nextDifficulty, s); - }; - - return ( -
-
▶ Try it live · pick letters
-
- - -
- {state && ( - <> -
- {state.mask.split("").map((c, i) => ( - - {c === "_" ? "·" : c} - - ))} -
-
- - {state.tries_left} / {state.max_tries} tries left - - - {state.wrong.length ? "wrong: " + state.wrong.join(" ") : ""} - -
-
- {ALPHABET.map((l) => { - const used = guessed.includes(l); - const hit = used && state.mask.includes(l); - return ( - - ); - })} -
- {state.status === "win" && ( -
🎉 You got it!
- )} - {state.status === "lose" && ( -
- 😅 Out of tries — the word was {state.word} -
- )} - - )} - {error &&
⚠ {error}
} -
- ); -} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts deleted file mode 100644 index 27b138477..000000000 --- a/web/src/lib/api.ts +++ /dev/null @@ -1,108 +0,0 @@ -// Client wrapper around the Cloudflare Workers Python backend ("Try it"). -// -// Base URL comes from NEXT_PUBLIC_API_BASE (set in .env.production). Leave it -// empty for same-origin local dev. - -const BASE = process.env.NEXT_PUBLIC_API_BASE || ""; - -export interface SelectOption { - value: string | number; - label: string; -} - -export interface InputField { - key: string; - label: string; - type: "select" | "number" | "text"; - options?: Array; - step?: number; - min?: number; - max?: number; - default: string | number; -} - -interface RunnableConfig { - endpoint: string; - inputs?: InputField[]; - interactive?: "tictactoe" | "hangman"; -} - -const RUNNABLE: Record = { - rps: { - endpoint: "/api/rps", - inputs: [ - { key: "choice", label: "Your throw", type: "select", options: ["rock", "paper", "scissors"], default: "rock" }, - ], - }, - bmi: { - endpoint: "/api/bmi", - inputs: [ - { key: "height_m", label: "Height (m)", type: "number", step: 0.01, min: 0.5, max: 2.5, default: 1.75 }, - { key: "weight_kg", label: "Weight (kg)", type: "number", step: 0.1, min: 10, max: 400, default: 70 }, - ], - }, - qr: { - endpoint: "/api/qr", - inputs: [ - { key: "text", label: "Text or URL", type: "text", default: "https://github.com/Mrinank-Bhowmick/python-beginner-projects" }, - ], - }, - madlibs: { - endpoint: "/api/madlibs", - inputs: [ - { - key: "story", - label: "Story", - type: "select", - options: [ - { value: 1, label: "1 · Mystical land" }, - { value: 2, label: "2 · Wizard academy" }, - { value: 3, label: "3 · Time machine" }, - ], - default: 1, - }, - { key: "adjective", label: "Adjective", type: "text", default: "sparkly" }, - { key: "noun", label: "Noun", type: "text", default: "penguin" }, - { key: "verb", label: "Verb", type: "text", default: "dancing" }, - { key: "adverb", label: "Adverb", type: "text", default: "wildly" }, - ], - }, - tictactoe: { endpoint: "/api/tictactoe", interactive: "tictactoe" }, - hangman: { endpoint: "/api/hangman", interactive: "hangman" }, -}; - -export async function call>( - id: string, - payload?: Record, -): Promise { - const cfg = RUNNABLE[id]; - if (!cfg) throw new Error("Not runnable: " + id); - const res = await fetch(BASE + cfg.endpoint, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload || {}), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(data.error || "HTTP " + res.status); - return data as T; -} - -export function defaults(id: string): Record { - const cfg = RUNNABLE[id]; - if (!cfg || !cfg.inputs) return {}; - const out: Record = {}; - for (const f of cfg.inputs) out[f.key] = f.default; - return out; -} - -export function isRunnable(id: string): boolean { - return !!RUNNABLE[id]; -} - -export function isInteractive(id: string): boolean { - return !!RUNNABLE[id]?.interactive; -} - -export function inputsFor(id: string): InputField[] { - return RUNNABLE[id]?.inputs || []; -} diff --git a/web/src/lib/authors.json b/web/src/lib/authors.json new file mode 100644 index 000000000..71ef8ecca --- /dev/null +++ b/web/src/lib/authors.json @@ -0,0 +1,276 @@ +{ + "Adjactive_compartive_superlative": "ibra-kdbra", + "Advisor": "ibra-kdbra", + "AES256": "oyeanmol", + "Alarm Clock": "vk0812", + "Amazon Product Availbility Checker": "ayushi-ras", + "Analog Clock": "Adrija-G", + "AnalogClock": "Adrija-G", + "API Based Weather Report": "srujan-landeri", + "Audio Converter": "guiszk", + "AudioAPI": "akghosh111", + "Audiobook": "ZackeryRSmith", + "AudioRecorder": "Mrinank-Bhowmick", + "AutoGui": "jadkinsgr", + "AWS_s3_upload": "itsyashvardhan", + "Battleship": "robertlent", + "BFS visualizer": "wre9-tesh", + "Bigram_Autocomplete": "HridayAg0102", + "Bitcoin Mining": "MonitSharma", + "bittorrent-downloader": "ibra-kdbra", + "BlackJack": "ChiragAgg5k", + "Blender_tools": "ibra-kdbra", + "Blind Auction": "Polqt", + "Blind_Auction": "yogesh78026", + "BMI WebApp": "Mrinank-Bhowmick", + "BMI_calculator": "sudipg4112001", + "Browser": "Mrinank-Bhowmick", + "Budget-manager": "abc-is-here", + "caesar_cipher": "Harry830", + "Calculate Age": "xlo-u", + "Calculator": "shubham7668", + "Calculator-GUI-With-Python": "YashTariyal", + "Calendar": "snehafarkya", + "Captcha_Genrator": "yashd26", + "car game": "pratyusha0710", + "Card Game": "Sayakb03", + "career-guide-bot": "ShivaNagachander", + "character-picture-grid": "ibra-kdbra", + "Chat Application": "Nini010", + "Chat-GPT-Discord-Bot": "Alby084", + "Chess": "ImaginedTime", + "chore-assignment-emailer": "ibra-kdbra", + "Code_Reviewer": "ibra-kdbra", + "Coin Flip": "pranavdasan", + "coin-toss": "ibra-kdbra", + "Collatz_Conjecture": "lonelyH3b", + "Comics_Scraper": "ibra-kdbra", + "comma-code": "ibra-kdbra", + "computer-algebra": "ca20110820", + "Connect Four": "iamkunalpitale", + "Countdown": "vk0812", + "CRUD-with-postgresql": "newtoallofthis123", + "currency converter": "Mrinank-Bhowmick", + "custom-invitations": "ibra-kdbra", + "custom-seating-cards": "ibra-kdbra", + "Data Entry Automation": "khushhalgarg112", + "Data_Abstractor": "Nachiket94", + "Desktop Weather Notifier": "TERNION-1121", + "Desktop-Notification": "ZackeryRSmith", + "Diabetes Monitoring Dashboard": "Akarsh3053", + "Dice Simulator": "Subha-5", + "Dice-Roll-Simulator": "ZackeryRSmith", + "dictionary.com-scraper": "ibra-kdbra", + "digital_clock": "Krishna13515", + "Discord Bot": "Mrinank-Bhowmick", + "DnD Dice": "Ramisky", + "Drowsiness Detector": "MonitSharma", + "duplicate_search": "Zedeldi", + "Encryptor and Decryptor": "amunjuluri", + "English Thesaurus": "Surya-29", + "excel-to-csv-converter": "ibra-kdbra", + "Expense-Tracker": "Mrinank-Bhowmick", + "Eye Blink Detection": "MonitSharma", + "Face detect": "bim22614", + "facebook_video_downloader": "Prabinshrestha737", + "facerecoginition": "AbhinandanSingla", + "fantasy-game-inventory": "ibra-kdbra", + "File_Organizer": "JohnRTitor", + "fill-gaps": "ibra-kdbra", + "Find_imbd_rating": "Mrinank-Bhowmick", + "find-unneeded-files": "ibra-kdbra", + "Flappybird_game": "Nikita0509", + "Full_Calendar": "DevTomilola-OS", + "Game of Cricket": "whoisjayd", + "game-snake_water_gun": "Yashparwal1", + "goodreads-quotes-scraper": "ibra-kdbra", + "GPA-Calculator": "nilay-banerjee", + "Gradient-Image": "smw-1211", + "Guess Number": "maglionaire", + "Guess The Word": "smw-1211", + "HandCricket": "Mrinank-Bhowmick", + "HandTrack": "bibaswanroy", + "Hangman": "Mrinank-Bhowmick", + "hashing-passwords": "itsyashvardhan", + "healthmanagementsystem": "anishaxtha", + "Higher-Lower": "ZackeryRSmith", + "hill_cipher": "payallenka", + "Historical Data Breaches": "u749929", + "Image compressor": "Preeray", + "Image Manipulation": "manav0702", + "Image Sketcher": "sudipg4112001", + "Image to PDF Converter": "ibra-kdbra", + "Image to text generation project": "anish2105", + "image-site-downloader": "ibra-kdbra", + "Image-to-art": "DevTomilola-OS", + "Image-Upscale": "Mrinank-Bhowmick", + "ImagegenChatbot": "Santhoshnov", + "indeed-scraper": "ibra-kdbra", + "Infix to Postfix": "bibaswanroy", + "Instagram Post Creation": "vagxrth", + "instant-messenger-bot": "ibra-kdbra", + "Internet Speed Tester": "niirmaaltwaatii", + "Internet-speed-test": "spyke7", + "inventory management": "kanchanraiii", + "Inverse Matrix Calculator": "farisfaikar", + "IP Blacklist Checker": "dimonalik", + "IPv4_Calculator-main": "bim22614", + "JARVIS.PY": "Morbius00", + "Jokenpo": "jrbublitz", + "Jokey": "DevTomilola-OS", + "KbdXylo": "Zedeldi", + "Language_learning_assistant": "ibra-kdbra", + "Language-Translate": "SirRomey", + "Lawyer AI Assistant": "paartheee", + "link-verification": "ibra-kdbra", + "linkedin-scrape": "ibra-kdbra", + "Live AQI": "kanchanraiii", + "Loan Calculator": "rudy3333", + "Location Search App (GUI)": "nebulaanish", + "looking-busy": "ibra-kdbra", + "Lyrics-Extractor": "sirKiraUzumaki", + "mad-libs": "ibra-kdbra", + "Madlibs Generator": "ZackeryRSmith", + "Make-API": "ZackeryRSmith", + "Market Financial Sentiment Predictor": "rik-chatterjee", + "Mastermind": "mr-desilva", + "maths": "ChefYeshpal", + "Medium Article Reader": "ZackeryRSmith", + "Merge PDFs": "SuhasBk", + "Message-Spam": "Toheed07", + "Minecraft-in-Python-main": "benedictprajwal", + "Mineral Processing Technology-Image Analytics": "manishkumar00208", + "minesweeper": "myudak", + "ML_examples": "ibra-kdbra", + "ML-Notebooks_Beginners": "ibra-kdbra", + "Mobile Document Scanner": "MonitSharma", + "Mongo CRUD": "jrbublitz", + "Morse-Code-Translator": "ahmedalhamad7", + "MorseCode Translator": "ZackeryRSmith", + "MotivateBot": "EFFLUX110", + "Movie recommendation": "rajdeepdas2000", + "movie-rater": "Mrinank-Bhowmick", + "MQTT Client": "jonascarvalh", + "multiplayer_socket": "QubitMatrix", + "Music Player": "GargiMittal", + "Music-Playlist-Generator": "imshivamrai282", + "NASA-APOD": "dapoadedire", + "Neurons": "Zedeldi", + "Number Guessing App": "cypherab01", + "Online Trivia": "whoisjayd", + "OpenCV_color_detect_in_live_feed": "GigabyteZX1", + "Organize_Directory": "ibra-kdbra", + "Othello": "TERNION-1121", + "Otp_Generator": "iamdestinychild", + "OTP-Verfication-System": "mzaryabrafique", + "Password Generator": "UffaModey", + "Password Meter": "jrbublitz", + "Password Projects": "u749929", + "Password_manager": "NischayGoyal1", + "PDF_Reader": "aryangulati", + "pdf_to_text": "Mrinank-Bhowmick", + "pdf-paranoia": "ibra-kdbra", + "Personal-Finance-tracker": "Manishak798", + "ping_pong": "AtharvaDeshmukh0909", + "Pokemon Battle": "EpicNesh26", + "PONG": "ambushneupane", + "Port_Scaner": "bim22614", + "prettified-stopwatch": "ibra-kdbra", + "Print_Colored_Text": "Nowshin1077", + "ProjectEuler": "ZackeryRSmith", + "proxy-scrapper": "jakbin", + "Python story generator": "Ylavish64", + "QR code generator": "amunjuluri", + "qr-code-generator-with-window-and-simple-ui": "VedSadh", + "QRCode-Generator": "SubramanyaKS", + "Qt5_YouTube": "Zedeldi", + "QuickWordCloud": "IamSoo", + "Quiz Game": "ZackeryRSmith", + "Random-Quote-Generator": "saiuttejr", + "reciept generator": "Subha-5", + "reddit-scraper": "ibra-kdbra", + "regex-search": "ibra-kdbra", + "regex-strip": "ibra-kdbra", + "Rename_Images": "ibra-kdbra", + "Resize_Image": "ibra-kdbra", + "RestrauntAPI": "KartikRN", + "reuters-scraper": "ibra-kdbra", + "Rock_Paper_Scissors": "ZackeryRSmith", + "Roll_A_dice": "snehafarkya", + "Rubik-tracking": "this-mkhy", + "Sales Optimizer": "rik-chatterjee", + "scheduledShutdown": "Prajwol-Shrestha", + "Scientific-Calculator": "tamojeetK", + "scrap-ycombinator": "dapoadedire", + "ScreenRecorder": "sb-decoder", + "Seek_with_hand_track": "QuantumNovice", + "Send-Email": "ZackeryRSmith", + "servo motor classifier": "rik-chatterjee", + "Servo motor classifier": "rik-chatterjee", + "Simple-Plagiarism-Checker-Project": "sudhanshu-77", + "SketchifyMe": "emanalytic", + "Skycast": "Devparihar5", + "Slice-Audio": "Dishant10", + "SMS_ChatBot": "Vivek-GuptaXCode", + "Snake Game": "EbrG786", + "snake_game": "saanvibk", + "snake_water_gun_game": "d-coder111", + "Social_media_content_creation": "Mrinank-Bhowmick", + "Socket": "Alex108-lab", + "SongsMashup": "asingh4451", + "Space Shooter": "DevTomilola-OS", + "space_battle": "invalid-email-address", + "Speed-Type-test": "AdityaSahai123", + "Split_Tip": "DevTomilola-OS", + "sponge-bob": "JenilGajjar20", + "Spotify Player": "omkarxpatel", + "Stock-Market-Dashboard": "Crack-er-jack", + "student-management-system": "siddharth9300", + "Subnetting Flsm": "bim22614", + "Subtitle_synchronizer.py": "shriyansnaik", + "Sudoku_solver": "saltX5", + "takeImage": "anju-chhetri", + "TennisTournamentSim": "jpyces", + "Tesla": "ZackeryRSmith", + "Tetris Game": "Jishnu2608", + "Text Editor": "samayita1606", + "Text Summarizer": "VykSI", + "Text to Speech": "sudipg4112001", + "Text_to_SpreadSheet": "ibra-kdbra", + "Text-Editor-master": "HimanshuSinghNegi", + "Text-to-Image Generation Project": "anish2105", + "text-translate": "Mrinank-Bhowmick", + "TextDetection": "Mrinank-Bhowmick", + "Tic-Tac-Toe": "Mrinank-Bhowmick", + "TicTacToe-GUI": "saanvibk", + "TicTacToe-TylerPear": "tylerapear", + "Tile Matching": "kanchanraiii", + "Timer": "cj-praveen", + "Tkinter": "sudipg4112001", + "ToDoList": "otahina", + "Turtle Pattern": "Adhiraj-11", + "Turtle_Graphics": "Nowshin1077", + "Twitter-Bot": "harshhes", + "Type Racer Game": "kanchanraiii", + "url_shortener": "justinjohnson-dev", + "Video Reversal": "blindaks", + "video_transcoder_project": "shashaaankkkkk", + "Video-subtitle-generator": "Mrinank-Bhowmick", + "Voice-to-Text": "Mrinank-Bhowmick", + "Watermarker": "highb33kay", + "Weather": "ZackeryRSmith", + "Web Scraping Jujustu Kaisen Manga": "Vishvam10", + "web-crawler(movie extract)": "sudipg4112001", + "WebButtonSimpelGUI": "Sameer-choudhary-git", + "Website Blocker": "Dhruvil-Lakhtaria", + "Weights_in_different_planets_GUI": "Astrasv", + "what-for-dinner": "ta-brook", + "WiFi Password Generator": "vrup0408", + "Windows Logo": "its-100rabh", + "Wine_quality_predictor": "rik-chatterjee", + "Word_Predictor": "nik-6174", + "Worksheet_to_text": "ibra-kdbra", + "World-Cup-Player-Comparison": "MattBlodgettProjects", + "xls_to_xlsx": "odhyp", + "YouTube Video Downloader": "vk0812" +} diff --git a/web/src/lib/catalog.json b/web/src/lib/catalog.json new file mode 100644 index 000000000..abb425aeb --- /dev/null +++ b/web/src/lib/catalog.json @@ -0,0 +1,2958 @@ +[ + { + "id": "maths", + "folder": "maths", + "name": "3D Shape Volume Calculator", + "blurb": "A console program that calculates the volume of various 3D shapes (cube, pyramid, cylinder, sphere, cone, ellipsoid, torus, hexagonal prism) from u…", + "emoji": "🧮", + "runnable": true, + "hasPlayground": true, + "lines": 137, + "author": "ChefYeshpal" + }, + { + "id": "adjactive-compartive-superlative", + "folder": "Adjactive_compartive_superlative", + "name": "Adjective Comparative & Superlative", + "blurb": "A console tool that takes a comma-separated list of adjectives and prints the comparative and superlative form of each, using WordNet (via NLTK) wi…", + "emoji": "🐍", + "runnable": false, + "hasPlayground": false, + "lines": 68, + "author": "ibra-kdbra" + }, + { + "id": "aes256", + "folder": "AES256", + "name": "AES 256 Encryption and Decryption", + "blurb": "A beginner-friendly console project demonstrating AES-256 encryption and decryption in Python. It uses AES in GCM mode (authenticated encryption) w…", + "emoji": "🔐", + "runnable": true, + "hasPlayground": true, + "lines": 81, + "author": "oyeanmol" + }, + { + "id": "alarm-clock", + "folder": "Alarm Clock", + "name": "Alarm Clock", + "blurb": "A Tkinter desktop alarm clock. Pick an hour, minute and second from the drop-downs, press Set Alarm, and when the system clock reaches that time it…", + "emoji": "🕐", + "runnable": false, + "hasPlayground": false, + "lines": 225, + "author": "vk0812" + }, + { + "id": "amazon-product-availbility-checker", + "folder": "Amazon Product Availbility Checker", + "name": "Amazon Product Availability Checker", + "blurb": "A console script that fetches an Amazon product page, parses it with BeautifulSoup, and reports whether the product is in stock.", + "emoji": "📈", + "runnable": false, + "hasPlayground": false, + "lines": 35, + "author": "ayushi-ras" + }, + { + "id": "analogclock", + "folder": "AnalogClock", + "name": "Analog Clock", + "blurb": "A Tkinter program that draws a working analog clock on a canvas — face, hour numbers, and moving hour/minute/second hands. The clock always starts…", + "emoji": "🎨", + "runnable": false, + "hasPlayground": false, + "lines": 53, + "author": "Adrija-G" + }, + { + "id": "audiorecorder", + "folder": "AudioRecorder", + "name": "Audio Recorder", + "blurb": "Records audio from the system microphone and saves it to RAW, WAV, AIFF, and FLAC files.", + "emoji": "🔊", + "runnable": false, + "hasPlayground": false, + "lines": 24, + "author": "Mrinank-Bhowmick" + }, + { + "id": "audio-converter", + "folder": "Audio Converter", + "name": "audioconverter", + "blurb": "Convert audio files with pydub.", + "emoji": "🔊", + "runnable": false, + "hasPlayground": false, + "lines": 27, + "author": "guiszk" + }, + { + "id": "aws-s3-upload", + "folder": "AWS_s3_upload", + "name": "AWS_s3_upload", + "blurb": "boto3 (pip install boto3)
", + "emoji": "⬆️", + "runnable": false, + "hasPlayground": false, + "lines": 29, + "author": "itsyashvardhan" + }, + { + "id": "bfs-visualizer", + "folder": "BFS visualizer", + "name": "BFS algorithm visulizer", + "blurb": "This project helps us to visualize BFS algorithm by searching end point from start in a maze.", + "emoji": "🧩", + "runnable": false, + "hasPlayground": false, + "lines": 117, + "author": "wre9-tesh" + }, + { + "id": "bigram-autocomplete", + "folder": "Bigram_Autocomplete", + "name": "Bigram Autocomplete", + "blurb": "A simple word-autocomplete tool. It builds bigram (word-pair) frequencies from a training corpus you type in, then predicts the next words followin…", + "emoji": "📖", + "runnable": true, + "hasPlayground": true, + "lines": 73, + "author": "HridayAg0102" + }, + { + "id": "bitcoin-mining", + "folder": "Bitcoin Mining", + "name": "Bitcoin Mining", + "blurb": "Bitcoin is the latest trend in the cryptocurrency world which aims to disrupt the centralized banking system of the world by making it decentralize…", + "emoji": "💱", + "runnable": true, + "hasPlayground": true, + "lines": 50, + "author": "MonitSharma" + }, + { + "id": "bittorrent-downloader", + "folder": "bittorrent-downloader", + "name": "BitTorrent Downloader", + "blurb": "Checks an email inbox for torrent links sent from a verified account, launches a torrent client to download them, and sends an SMS notification whe…", + "emoji": "📧", + "runnable": false, + "hasPlayground": false, + "lines": 84, + "author": "ibra-kdbra" + }, + { + "id": "blackjack", + "folder": "BlackJack", + "name": "BLACKJACK", + "blurb": "Blackjack is a very popular card game commonlu played in casinos worldwide. This program will simulate a virtual casino, with computer as the deale…", + "emoji": "🃏", + "runnable": true, + "hasPlayground": true, + "lines": 314, + "author": "ChiragAgg5k" + }, + { + "id": "blender-tools", + "folder": "Blender_tools", + "name": "Blender Tools with Unreal/Unity", + "blurb": "A series of tools used to store vertex data in various ways. The data can then used in a game engine to animate meshes via a vertex shader.", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 151, + "author": "ibra-kdbra" + }, + { + "id": "blind-auction-2", + "folder": "Blind_Auction", + "name": "Blind Auction", + "blurb": "A console blind-auction game. Each bidder enters a name and a secret bid; once bidding ends, the program announces the highest bidder.", + "emoji": "🎮", + "runnable": true, + "hasPlayground": true, + "lines": 42, + "author": "yogesh78026" + }, + { + "id": "bmi", + "folder": "BMI_calculator", + "name": "BMI Calculator", + "blurb": "This Python script calculates Body Mass Index (BMI) based on a person's height and weight. It then interprets the BMI to provide a classification r…", + "emoji": "⚖️", + "runnable": true, + "hasPlayground": true, + "lines": 81, + "author": "sudipg4112001" + }, + { + "id": "bmi-webapp", + "folder": "BMI WebApp", + "name": "BMI WebApp", + "blurb": "A small BMI calculator built with PyWebIO. It asks for height and weight in a browser form and reports the BMI along with a weight classification.", + "emoji": "🧮", + "runnable": false, + "hasPlayground": false, + "lines": 40, + "author": "Mrinank-Bhowmick" + }, + { + "id": "book-data-extractor", + "folder": "Book Data Extractor", + "name": "Book Data Extractor", + "blurb": "This project is designed to extract large datasets of book information from Goodreads based on user ratings. It includes a Python script, main.py,…", + "emoji": "📊", + "runnable": false, + "hasPlayground": false, + "lines": 83 + }, + { + "id": "budget-manager", + "folder": "Budget-manager", + "name": "Budget Manager", + "blurb": "A personal budget manager desktop app. It lets you add and delete transactions, categorise them, and view a running balance, storing everything in…", + "emoji": "💰", + "runnable": false, + "hasPlayground": false, + "lines": 156, + "author": "abc-is-here" + }, + { + "id": "caesar-cipher", + "folder": "caesar_cipher", + "name": "Caesar Cipher", + "blurb": "A console implementation of the classic Caesar cipher. It encodes or decodes a message by shifting each letter a chosen number of positions through…", + "emoji": "🔐", + "runnable": true, + "hasPlayground": true, + "lines": 82, + "author": "Harry830" + }, + { + "id": "calculate-age", + "folder": "Calculate Age", + "name": "Calculate Your Age!", + "blurb": "This script prints your age in three different ways :", + "emoji": "🎂", + "runnable": true, + "hasPlayground": true, + "lines": 50, + "author": "xlo-u" + }, + { + "id": "calculator-gui-with-python", + "folder": "Calculator-GUI-With-Python", + "name": "Calculator GUI With Python", + "blurb": "A graphical calculator built with the Kivy framework, featuring a button grid, an evaluation display, and a clear button.", + "emoji": "🧮", + "runnable": false, + "hasPlayground": false, + "lines": 67, + "author": "YashTariyal" + }, + { + "id": "calendar", + "folder": "Calendar", + "name": "Calendar", + "blurb": "A console program that prints the calendar for a year and month entered by the user, with input validation.", + "emoji": "📅", + "runnable": true, + "hasPlayground": true, + "lines": 47, + "author": "snehafarkya" + }, + { + "id": "captcha-genrator", + "folder": "Captcha_Genrator", + "name": "Captcha Genrator", + "blurb": "A simple image captcha genrator", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 62, + "author": "yashd26" + }, + { + "id": "car-game", + "folder": "car game", + "name": "Car Game", + "blurb": "A 2D car-dodging arcade game built with Pygame. Steer your car to avoid oncoming traffic, with sound effects, background music, and a high-score tr…", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 458, + "author": "pratyusha0710" + }, + { + "id": "card-game", + "folder": "Card Game", + "name": "Card Game - War", + "blurb": "This is a simple command-line card game known as \"War.\" In the game, two players compete by drawing cards from a shuffled deck, and the player with…", + "emoji": "🃏", + "runnable": true, + "hasPlayground": true, + "lines": 135, + "author": "Sayakb03" + }, + { + "id": "career-guide-bot", + "folder": "career-guide-bot", + "name": "Career Guide", + "blurb": "The Career Guide is a Python-based interactive program that helps users explore various career options. It provides information on different career…", + "emoji": "🐍", + "runnable": false, + "hasPlayground": false, + "lines": 1, + "author": "ShivaNagachander" + }, + { + "id": "character-picture-grid", + "folder": "character-picture-grid", + "name": "Character Picture Grid", + "blurb": "A small utility that rotates a 2D character grid 90 degrees and prints the resulting picture to the console.", + "emoji": "🖼️", + "runnable": true, + "hasPlayground": true, + "lines": 41, + "author": "ibra-kdbra" + }, + { + "id": "chat-application", + "folder": "Chat Application", + "name": "Chat Application", + "blurb": "Chat Application is a simple Python-based chat system that demonstrates basic communication between a server and clients using sockets. This exampl…", + "emoji": "💬", + "runnable": false, + "hasPlayground": false, + "lines": 71, + "author": "Nini010" + }, + { + "id": "chess", + "folder": "Chess", + "name": "Chess", + "blurb": "A two-player chess game built with Pygame. It renders the board and pieces in a desktop window using the image assets in the Assets/ folder.", + "emoji": "♟️", + "runnable": false, + "hasPlayground": false, + "lines": 662, + "author": "ImaginedTime" + }, + { + "id": "chore-assignment-emailer", + "folder": "chore-assignment-emailer", + "name": "Chore Assignment Emailer", + "blurb": "Randomly assigns a list of chores among a list of email addresses (distributing them round-robin) and emails each person their assigned chores via…", + "emoji": "📧", + "runnable": false, + "hasPlayground": false, + "lines": 64, + "author": "ibra-kdbra" + }, + { + "id": "code-reviewer", + "folder": "Code_Reviewer", + "name": "Code Reviewer", + "blurb": "A simple static code-review tool that parses a Python file into an AST and reports possible issues such as missing docstrings, undefined variables,…", + "emoji": "🔎", + "runnable": false, + "hasPlayground": false, + "lines": 91, + "author": "ibra-kdbra" + }, + { + "id": "coin-flip", + "folder": "Coin Flip", + "name": "Coin Flip", + "blurb": "This is a Python program used to simulate a coin toss, in which a user is asked to pick a side (heads or tails), and the program selects a result a…", + "emoji": "🪙", + "runnable": true, + "hasPlayground": true, + "lines": 82, + "author": "pranavdasan" + }, + { + "id": "collatz-conjecture", + "folder": "Collatz_Conjecture", + "name": "Collatz Conjecture Iterator", + "blurb": "This code snippet is a simple implementation of an iterator that generates the Collatz sequence for a given positive integer greater than 1. The Co…", + "emoji": "⚙️", + "runnable": true, + "hasPlayground": true, + "lines": 35, + "author": "lonelyH3b" + }, + { + "id": "comma-code", + "folder": "comma-code", + "name": "Comma Code", + "blurb": "Joins a list of strings into a single human-readable string with commas and the word \"and\" before the last item (e.g. apples, bananas, tofu, and ca…", + "emoji": "📖", + "runnable": true, + "hasPlayground": true, + "lines": 22, + "author": "ibra-kdbra" + }, + { + "id": "connect-four", + "folder": "Connect Four", + "name": "Connect Four", + "blurb": "A two-player Connect Four game with a graphical board rendered using pygame; players drop colored pieces by clicking columns until one connects fou…", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 206, + "author": "iamkunalpitale" + }, + { + "id": "conway-s-game-of-life", + "folder": "Conway’s-Game-Of-Life", + "name": "Conway's Game of Life", + "blurb": "An implementation of Conway's Game of Life cellular automaton, animated with matplotlib. Supports random grids as well as glider and Gosper glider…", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 160 + }, + { + "id": "countdown", + "folder": "Countdown", + "name": "Countdown Timer", + "blurb": "A simple console countdown timer: enter a number of seconds and it counts down in MM:SS format, printing \"Time Up\" when finished.", + "emoji": "⏳", + "runnable": true, + "hasPlayground": true, + "lines": 21, + "author": "vk0812" + }, + { + "id": "crud-with-postgresql", + "folder": "CRUD-with-postgresql", + "name": "CRUD CLI with 🐘 PostgreSQL", + "blurb": "Well, this is a simple CRUD CLI with PostgreSQL. I made this project to help others learn more about PostgreSQL driver for Python and how to create…", + "emoji": "🐍", + "runnable": false, + "hasPlayground": false, + "lines": 257, + "author": "newtoallofthis123" + }, + { + "id": "data-abstractor", + "folder": "Data_Abstractor", + "name": "CURD-API", + "blurb": "This repo is an example for creating an API using Flask and Python. It can search the database on any type of segmentation of data. It includes a s…", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 7, + "author": "Nachiket94" + }, + { + "id": "currency-converter", + "folder": "currency converter", + "name": "Currency Converter", + "blurb": "A real-time currency converter with a Tkinter GUI. It fetches the latest exchange rates from an online API and lets the user convert an amount betw…", + "emoji": "💱", + "runnable": false, + "hasPlayground": false, + "lines": 132, + "author": "Mrinank-Bhowmick" + }, + { + "id": "custom-invitations", + "folder": "custom-invitations", + "name": "Custom Invitations", + "blurb": "Generates a Word document (invitations.docx) containing a personalized party invitation page for each name listed in guests.txt.", + "emoji": "📖", + "runnable": false, + "hasPlayground": false, + "lines": 64, + "author": "ibra-kdbra" + }, + { + "id": "custom-seating-cards", + "folder": "custom-seating-cards", + "name": "Custom Seating Cards", + "blurb": "Creates a decorative PNG seating/place card for each guest listed in guests.txt, drawing their name over a flower image with a custom font.", + "emoji": "🃏", + "runnable": false, + "hasPlayground": false, + "lines": 46, + "author": "ibra-kdbra" + }, + { + "id": "customencryptiondecryption", + "folder": "CustomEncryptionDecryption", + "name": "CustomEncryptionDecryption", + "blurb": "CustomEncryptionDecryption is a simple Python program that allows users to encrypt and decrypt their data or text using a SECRETKEY and CIPHERTEXT.…", + "emoji": "🔐", + "runnable": false, + "hasPlayground": false, + "lines": 123 + }, + { + "id": "data-entry-automation", + "folder": "Data Entry Automation", + "name": "Data Entry Automation", + "blurb": "In this project, We have Scrapped data(for educational Purpose only) from a rental properties website and upload it to as response of Google Form a…", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 98, + "author": "khushhalgarg112" + }, + { + "id": "desktop-notification", + "folder": "Desktop-Notification", + "name": "Desktop Notification", + "blurb": "Displays a simple Windows toast notification using the win10toast library.", + "emoji": "🔔", + "runnable": false, + "hasPlayground": false, + "lines": 15, + "author": "ZackeryRSmith" + }, + { + "id": "desktop-weather-notifier", + "folder": "Desktop Weather Notifier", + "name": "Desktop Weather Notifier", + "blurb": "Fetches current weather for a city from weatherapi.com once per hour and shows it as a native desktop notification, including temperature, wind, an…", + "emoji": "⛅", + "runnable": false, + "hasPlayground": false, + "lines": 68, + "author": "TERNION-1121" + }, + { + "id": "diabetes-monitoring-dashboard", + "folder": "Diabetes Monitoring Dashboard", + "name": "Diabetes Monitoring Dashboard", + "blurb": "This is a healthcare web-application that uses Machine Learning algorithms to predict whether a person is diabetic or not, while also providing val…", + "emoji": "🌐", + "runnable": false, + "hasPlayground": false, + "lines": 15, + "author": "Akarsh3053" + }, + { + "id": "dice-roll-simulator", + "folder": "Dice-Roll-Simulator", + "name": "Dice Rolling Simulator", + "blurb": "This is a simple Python program that simulates rolling a 6-sided dice.", + "emoji": "🎲", + "runnable": true, + "hasPlayground": true, + "lines": 26, + "author": "ZackeryRSmith" + }, + { + "id": "dice-simulator", + "folder": "Dice Simulator", + "name": "Dice Simulator", + "blurb": "Dual Dice Simulator", + "emoji": "🎲", + "runnable": true, + "hasPlayground": true, + "lines": 74, + "author": "Subha-5" + }, + { + "id": "dictionary-com-scraper", + "folder": "dictionary.com-scraper", + "name": "Dictionary.com Word of the Day Scraper", + "blurb": "Scrapes the \"Word of the Day\" from dictionary.com, can fetch all its meanings, generate a text-to-speech pronunciation, and open the word's page in…", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 99, + "author": "ibra-kdbra" + }, + { + "id": "discord-bot", + "folder": "Discord Bot", + "name": "Discord Bot", + "blurb": "A simple Discord bot built with discord.py. It responds to !hello and !random messages and provides an ?ask command that gives a random Magic 8-Bal…", + "emoji": "💬", + "runnable": false, + "hasPlayground": false, + "lines": 95, + "author": "Mrinank-Bhowmick" + }, + { + "id": "dnd-dice", + "folder": "DnD Dice", + "name": "DnD Dice", + "blurb": "A console dice roller for tabletop role-playing games. Pick a dice type (D4, D6, D8, D10, D12, D20, or D100) and it prints a random roll.", + "emoji": "🎲", + "runnable": true, + "hasPlayground": true, + "lines": 32, + "author": "Ramisky" + }, + { + "id": "drowsiness-detector", + "folder": "Drowsiness Detector", + "name": "Drowsiness Detector", + "blurb": "We already did an eye-blink detector python project earlier, using Facial landmarks. Now we will extend this feature to determine for how long does…", + "emoji": "😴", + "runnable": false, + "hasPlayground": false, + "lines": 168, + "author": "MonitSharma" + }, + { + "id": "duplicate-search", + "folder": "duplicate_search", + "name": "Duplicate File Search", + "blurb": "Recursively searches a directory for duplicate files by comparing MD5 checksums, then interactively lets you delete unwanted copies.", + "emoji": "🔍", + "runnable": false, + "hasPlayground": false, + "lines": 53, + "author": "Zedeldi" + }, + { + "id": "browser", + "folder": "Browser", + "name": "EFFLUX Browser", + "blurb": "A minimal desktop web browser built with PyQt5 and QtWebEngine, featuring back/forward/reload buttons and a URL bar.", + "emoji": "🌐", + "runnable": false, + "hasPlayground": false, + "lines": 64, + "author": "Mrinank-Bhowmick" + }, + { + "id": "encryptor-and-decryptor", + "folder": "Encryptor and Decryptor", + "name": "Encryptor and Decryptor", + "blurb": "A Tkinter GUI application that encrypts and decrypts text using Base64 encoding, gated behind a secret key.", + "emoji": "🔐", + "runnable": false, + "hasPlayground": false, + "lines": 129, + "author": "amunjuluri" + }, + { + "id": "english-thesaurus", + "folder": "English Thesaurus", + "name": "English Thesaurus", + "blurb": "A console thesaurus/dictionary that looks up word meanings from a local data.json file, suggesting close matches when a word is misspelled.", + "emoji": "📖", + "runnable": false, + "hasPlayground": false, + "lines": 51, + "author": "Surya-29" + }, + { + "id": "excel-to-csv-converter", + "folder": "excel-to-csv-converter", + "name": "Excel to CSV Converter", + "blurb": "Converts every sheet of every .xlsx file in a folder into separate CSV files.", + "emoji": "📊", + "runnable": false, + "hasPlayground": false, + "lines": 47, + "author": "ibra-kdbra" + }, + { + "id": "expense-tracker", + "folder": "Expense-Tracker", + "name": "Expense Tracker Application", + "blurb": "The Expense Tracker Application is a GUI application built using Python and Customtkinter/Tkinter for managing expenses. It allows users to add, up…", + "emoji": "💰", + "runnable": false, + "hasPlayground": false, + "lines": 430, + "author": "Mrinank-Bhowmick" + }, + { + "id": "eye-blink-detection", + "folder": "Eye Blink Detection", + "name": "Eye Blink Detection", + "blurb": "Here we are going to build upon this knowledge and develop a computer vision application that is capable of detecting and counting blinks in video…", + "emoji": "🎬", + "runnable": false, + "hasPlayground": false, + "lines": 153, + "author": "MonitSharma" + }, + { + "id": "facerecoginition", + "folder": "facerecoginition", + "name": "Face Recognition", + "blurb": "A two-part OpenCV face recognition demo. FaceCapture.py captures face crops from your webcam and saves them; FaceDetection.py trains an LBPH recogn…", + "emoji": "📷", + "runnable": false, + "hasPlayground": false, + "lines": 45, + "author": "AbhinandanSingla" + }, + { + "id": "facebook-video-downloader", + "folder": "facebook_video_downloader", + "name": "Facebook Video Downloader", + "blurb": "A python script to download Facebook videos.", + "emoji": "🎬", + "runnable": false, + "hasPlayground": false, + "lines": 28, + "author": "Prabinshrestha737" + }, + { + "id": "fantasy-game-inventory", + "folder": "fantasy-game-inventory", + "name": "Fantasy Game Inventory", + "blurb": "A small demo that manages a fantasy game player's inventory. It displays items and their counts, and supports adding loot (a list of items) to an e…", + "emoji": "🎮", + "runnable": true, + "hasPlayground": true, + "lines": 47, + "author": "ibra-kdbra" + }, + { + "id": "fidget-spinner-game", + "folder": "Fidget Spinner Game", + "name": "Fidget Spinner Game", + "blurb": "A small interactive toy built with Python's turtle module. Press the space bar to \"flick\" an on-screen fidget spinner; it spins and gradually slows…", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 45 + }, + { + "id": "file-organizer", + "folder": "File_Organizer", + "name": "File Organizer", + "blurb": "A command-line tool that organizes a directory by file type. It scans the given directory and moves files into category folders (Music, Videos, Doc…", + "emoji": "🎵", + "runnable": false, + "hasPlayground": false, + "lines": 175, + "author": "JohnRTitor" + }, + { + "id": "fill-gaps", + "folder": "fill-gaps", + "name": "Fill Gaps", + "blurb": "A utility for renaming numbered files in a folder. fillGaps closes gaps in a numbered sequence (e.g. spam001, spam003 becomes spam001, spam002), an…", + "emoji": "🔢", + "runnable": false, + "hasPlayground": false, + "lines": 109, + "author": "ibra-kdbra" + }, + { + "id": "find-imbd-rating", + "folder": "Find_imbd_rating", + "name": "Find IMDb Rating", + "blurb": "Reads the names of film files from a local directory, searches IMDb for each title, scrapes the rating and genre, and writes the results to filmrat…", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 88, + "author": "Mrinank-Bhowmick" + }, + { + "id": "find-unneeded-files", + "folder": "find-unneeded-files", + "name": "Find Unneeded Files", + "blurb": "Walks a folder tree and reports files or subfolders whose size exceeds a given threshold, to help locate large items that could be cleaned up.", + "emoji": "🐍", + "runnable": false, + "hasPlayground": false, + "lines": 46, + "author": "ibra-kdbra" + }, + { + "id": "flappy", + "folder": "Flappybird_game", + "name": "Flappy Bird Game", + "blurb": "A clone of the classic Flappy Bird game built with Pygame. Tap the space bar to flap the bird through gaps in the pipes; the game tracks score and…", + "emoji": "🐦", + "runnable": false, + "hasPlayground": false, + "lines": 185, + "author": "Nikita0509" + }, + { + "id": "full-calendar", + "folder": "Full_Calendar", + "name": "Full Calendar", + "blurb": "A Tkinter GUI application that asks for a year and displays the full calendar for that year in a new window.", + "emoji": "📅", + "runnable": false, + "hasPlayground": false, + "lines": 65, + "author": "DevTomilola-OS" + }, + { + "id": "game-of-cricket", + "folder": "Game of Cricket", + "name": "Game of Cricket", + "blurb": "A text-based cricket game played against the computer. You pick a number from 1 to 6 each ball; matching the computer's number loses a wicket, othe…", + "emoji": "🎮", + "runnable": true, + "hasPlayground": true, + "lines": 114, + "author": "whoisjayd" + }, + { + "id": "goodreads-quotes-scraper", + "folder": "goodreads-quotes-scraper", + "name": "Goodreads Quotes Scraper", + "blurb": "Scrapes quotes from Goodreads — top popular, recently added, by tag, or by page number — and saves the results to temp.json.", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 1203, + "author": "ibra-kdbra" + }, + { + "id": "gpa-calculator", + "folder": "GPA-Calculator", + "name": "GPA Calculator", + "blurb": "A console tool that computes a semester GPA. It asks for the number of courses, each course's credits, and marks (either a total or split into mid-…", + "emoji": "🧮", + "runnable": true, + "hasPlayground": true, + "lines": 75, + "author": "nilay-banerjee" + }, + { + "id": "gradient-image", + "folder": "Gradient-Image", + "name": "Gradient Image", + "blurb": "An OpenCV demo that loads an image and displays edge-detection results — Laplacian, combined Sobel gradient, and Canny — in separate windows for a…", + "emoji": "📷", + "runnable": false, + "hasPlayground": false, + "lines": 36, + "author": "smw-1211" + }, + { + "id": "guess-number", + "folder": "Guess Number", + "name": "Guess Number", + "blurb": "A number guessing game. Two versions are included:", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 70, + "author": "maglionaire" + }, + { + "id": "guess-the-word", + "folder": "Guess The Word", + "name": "Guess The Word", + "blurb": "A word guessing game (Hangman-style without the drawing). The program picks a random programming-language word and you have six attempts to reveal…", + "emoji": "🪢", + "runnable": true, + "hasPlayground": true, + "lines": 76, + "author": "smw-1211" + }, + { + "id": "gui-based-image-display-and-transfer-in-python", + "folder": "GUI based image display and transfer in python", + "name": "GUI Image Display and Transfer", + "blurb": "A Tkinter desktop application for viewing and copying images. It lets you open an image through a file dialog, displays it in the window, and saves…", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 44 + }, + { + "id": "handcricket", + "folder": "HandCricket", + "name": "Hand Cricket Game", + "blurb": "This is a simple command-line-based Hand Cricket game implemented in Python. It allows you to play cricket against a computer opponent. You can eit…", + "emoji": "🎮", + "runnable": true, + "hasPlayground": true, + "lines": 168, + "author": "Mrinank-Bhowmick" + }, + { + "id": "handtrack", + "folder": "HandTrack", + "name": "Hand Track", + "blurb": "Real-time hand tracking with OpenCV and MediaPipe. base.py and HTrackMod.py detect and draw hand landmarks from the webcam, and VolumeControl.py ma…", + "emoji": "📷", + "runnable": false, + "hasPlayground": false, + "lines": 45, + "author": "bibaswanroy" + }, + { + "id": "hangman", + "folder": "Hangman", + "name": "Hangman", + "blurb": "The classic Hangman game in the console. Pick a difficulty (easy/medium/hard), then guess the secret word letter by letter while ASCII art tracks y…", + "emoji": "🪢", + "runnable": true, + "hasPlayground": true, + "lines": 235, + "author": "Mrinank-Bhowmick" + }, + { + "id": "healthmanagementsystem", + "folder": "healthmanagementsystem", + "name": "Health Management System", + "blurb": "A console-based health log. It lets you log exercise or food entries (timestamped) for one of three users into text files, and retrieve those logs…", + "emoji": "🖥️", + "runnable": false, + "hasPlayground": false, + "lines": 93, + "author": "anishaxtha" + }, + { + "id": "higher-lower", + "folder": "Higher-Lower", + "name": "Higher-Lower Game", + "blurb": "A number guessing game. The program picks a random number between 0 and 100 and tells you whether to guess higher or lower until you find it.", + "emoji": "🎮", + "runnable": true, + "hasPlayground": true, + "lines": 24, + "author": "ZackeryRSmith" + }, + { + "id": "hill-cipher", + "folder": "hill_cipher", + "name": "hill_cipher", + "blurb": "

HillCipher

This can be used to encrypt three lowercase characters using a built-in key, the decryption of the code generated is…", + "emoji": "🔐", + "runnable": false, + "hasPlayground": false, + "lines": 63, + "author": "payallenka" + }, + { + "id": "historical-data-breaches", + "folder": "Historical Data Breaches", + "name": "Historical Data Breach Search", + "blurb": "The historical data breach python program, uses the HaveIBeenPwned (HIBP) API to act as a search engine for organisations/domains that have had kno…", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 44, + "author": "u749929" + }, + { + "id": "image-compressor", + "folder": "Image compressor", + "name": "Image Compressor", + "blurb": "A command-line tool that compresses an image using Pillow. You pass an input image and optionally an output directory, quality, and format; it writ…", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 51, + "author": "Preeray" + }, + { + "id": "image-manipulation", + "folder": "Image Manipulation", + "name": "Image Manipulation", + "blurb": "A small Pillow script that opens a local image (mars.jpg), resizes it to 200x200 pixels, and saves the result as newImage.jpg.", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 11, + "author": "manav0702" + }, + { + "id": "image-site-downloader", + "folder": "image-site-downloader", + "name": "Image Site Downloader", + "blurb": "Searches Imgur for a query term, scrapes the resulting image thumbnails, and downloads up to a chosen number of them into a local results/ folder.", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 63, + "author": "ibra-kdbra" + }, + { + "id": "image-sketcher", + "folder": "Image Sketcher", + "name": "Image Sketcher", + "blurb": "An OpenCV demo that captures live webcam frames and renders them as a pencil-sketch effect using grayscale conversion, Gaussian blur, and Canny edg…", + "emoji": "📷", + "runnable": false, + "hasPlayground": false, + "lines": 21, + "author": "sudipg4112001" + }, + { + "id": "image-to-art", + "folder": "Image-to-art", + "name": "Image To ASCII ART Generator", + "blurb": " A simple Python Program to convert your images to ascii art.", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 48, + "author": "DevTomilola-OS" + }, + { + "id": "image-to-pdf-converter", + "folder": "Image to PDF Converter", + "name": "Image to PDF Converter", + "blurb": "Resizes every image in a convert/ directory to A4 size and merges them into a single output.pdf file using FPDF.", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 40, + "author": "ibra-kdbra" + }, + { + "id": "image-to-text-generation-project", + "folder": "Image to text generation project", + "name": "Image to Text Generation", + "blurb": "A Jupyter notebook that generates a caption for an image. It loads a pretrained Vision Encoder-Decoder model (bipin/image-caption-generator) via Hu…", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 0, + "author": "anish2105" + }, + { + "id": "image-toonification", + "folder": "Image Toonification", + "name": "Image Toonifier", + "blurb": "An App to toonify images in PC. Have Fun Toonifying yourselves.", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 56 + }, + { + "id": "image-upscale", + "folder": "Image-Upscale", + "name": "Image Upscale", + "blurb": "Upscales an image using the ESRGAN super-resolution model built on PyTorch.", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 34, + "author": "Mrinank-Bhowmick" + }, + { + "id": "imagegenchatbot", + "folder": "ImagegenChatbot", + "name": "ImagegenChatbot", + "blurb": "Now you'll be able to create your own AI 😎", + "emoji": "🧠", + "runnable": false, + "hasPlayground": false, + "lines": 23, + "author": "Santhoshnov" + }, + { + "id": "indeed-scraper", + "folder": "indeed-scraper", + "name": "Indeed Job Scraper", + "blurb": "Scrapes job listings from Indeed for several cities and job titles, then saves the results to a CSV file.", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 110, + "author": "ibra-kdbra" + }, + { + "id": "instagram-post-creation", + "folder": "Instagram Post Creation", + "name": "Instagram Post Creation", + "blurb": "Generates an Instagram post image by drawing centered text onto a template image using Pillow.", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 41, + "author": "vagxrth" + }, + { + "id": "instant-messenger-bot", + "folder": "instant-messenger-bot", + "name": "Instant Messenger Bot", + "blurb": "Automates sending messages to active Slack contacts by simulating keyboard and mouse input.", + "emoji": "🤖", + "runnable": false, + "hasPlayground": false, + "lines": 52, + "author": "ibra-kdbra" + }, + { + "id": "blind-auction", + "folder": "Blind Auction", + "name": "Instructions", + "blurb": "Click \"Open Preview\" above to see this file rendered with the markdown.", + "emoji": "🐍", + "runnable": true, + "hasPlayground": true, + "lines": 31, + "author": "Polqt" + }, + { + "id": "internet-speed-tester", + "folder": "Internet Speed Tester", + "name": "Internet Speed Tester", + "blurb": "Check the Download and Upload Speed of your Internet Connection.", + "emoji": "⬇️", + "runnable": false, + "hasPlayground": false, + "lines": 13, + "author": "niirmaaltwaatii" + }, + { + "id": "internet-speed-test", + "folder": "Internet-speed-test", + "name": "Internet Speed Tester", + "blurb": "This is a internet speed tester made in python", + "emoji": "📶", + "runnable": false, + "hasPlayground": false, + "lines": 47, + "author": "spyke7" + }, + { + "id": "inventory-management", + "folder": "inventory management", + "name": "Inventory Management", + "blurb": "A simple supermarket inventory system with a Tkinter login window and add/display/search/update/delete operations backed by a MySQL database.", + "emoji": "🔑", + "runnable": false, + "hasPlayground": false, + "lines": 164, + "author": "kanchanraiii" + }, + { + "id": "inverse-matrix-calculator", + "folder": "Inverse Matrix Calculator", + "name": "Inverse Matrix Calculator", + "blurb": "A console program that asks for the order and elements of a square matrix, then computes and prints its inverse using minors, cofactors, and the ad…", + "emoji": "🧮", + "runnable": true, + "hasPlayground": true, + "lines": 123, + "author": "farisfaikar" + }, + { + "id": "ip-blacklist-checker", + "folder": "IP Blacklist Checker", + "name": "IP Blacklist Checker", + "blurb": "A CLI script that checks whether given IP addresses are blacklisted by querying the blacklistchecker.com API.", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 81, + "author": "dimonalik" + }, + { + "id": "ipv4-calculator-main", + "folder": "IPv4_Calculator-main", + "name": "IPv4_Calculator", + "blurb": "IPv4Calculator is developed for: 1.Determine the network class (A, B, C); 2.Determine which category the address belongs to (private, public); 3.De…", + "emoji": "🧮", + "runnable": true, + "hasPlayground": true, + "lines": 201, + "author": "bim22614" + }, + { + "id": "jarvis-py", + "folder": "JARVIS.PY", + "name": "JARVIS.PY", + "blurb": "", + "emoji": "🤖", + "runnable": false, + "hasPlayground": false, + "lines": 134, + "author": "Morbius00" + }, + { + "id": "jokenpo", + "folder": "Jokenpo", + "name": "Jokenpo (Rock, Paper, Scissors)", + "blurb": "A console card-based rock-paper-scissors game (in Portuguese) where you can play against the computer or watch a simulated match between two players.", + "emoji": "✊", + "runnable": true, + "hasPlayground": true, + "lines": 47, + "author": "jrbublitz" + }, + { + "id": "jokey", + "folder": "Jokey", + "name": "Jokey", + "blurb": "A console joke teller that fetches jokes from the JokeAPI, with settings to change joke category and language.", + "emoji": "😂", + "runnable": false, + "hasPlayground": false, + "lines": 65, + "author": "DevTomilola-OS" + }, + { + "id": "kbdxylo", + "folder": "KbdXylo", + "name": "Keyboard Xylophone", + "blurb": "Turns your keyboard into a xylophone by listening for key presses and playing a tone based on each key's character value.", + "emoji": "⌨️", + "runnable": false, + "hasPlayground": false, + "lines": 49, + "author": "Zedeldi" + }, + { + "id": "language-learning-assistant", + "folder": "Language_learning_assistant", + "name": "Language Learning Assistant", + "blurb": "This is a Python script for a language learning assistant that helps users practice their target language. It provides interactive conversation pra…", + "emoji": "🤖", + "runnable": false, + "hasPlayground": false, + "lines": 378, + "author": "ibra-kdbra" + }, + { + "id": "language-translate", + "folder": "Language-Translate", + "name": "Language Translate", + "blurb": "A simple script to help translate your text into different languages.", + "emoji": "🌍", + "runnable": false, + "hasPlayground": false, + "lines": 30, + "author": "SirRomey" + }, + { + "id": "link-verification", + "folder": "link-verification", + "name": "Link Verification", + "blurb": "Fetches a web page, extracts all of its links, and reports which ones are good or broken.", + "emoji": "🌐", + "runnable": false, + "hasPlayground": false, + "lines": 47, + "author": "ibra-kdbra" + }, + { + "id": "linkedin-scrape", + "folder": "linkedin-scrape", + "name": "LinkedIn Profile Scraper", + "blurb": "Logs into LinkedIn with Selenium (Firefox) and scrapes contact information (email, phone, website) from a connection's profile.", + "emoji": "📧", + "runnable": false, + "hasPlayground": false, + "lines": 42, + "author": "ibra-kdbra" + }, + { + "id": "live-aqi", + "folder": "Live AQI", + "name": "Live AQI", + "blurb": "A Tkinter app that fetches the live Air Quality Index for a given city, state, and country from the AirVisual API.", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 89, + "author": "kanchanraiii" + }, + { + "id": "loan-calculator", + "folder": "Loan Calculator", + "name": "Loan Calculator", + "blurb": "Welcome to the Loan Calculator project! This Python program calculates the monthly payment amount for a fixed-rate loan based on the loan amount, a…", + "emoji": "🧮", + "runnable": true, + "hasPlayground": true, + "lines": 56, + "author": "rudy3333" + }, + { + "id": "location-search-app-gui", + "folder": "Location Search App (GUI)", + "name": "Location Search App (GUI)", + "blurb": "A Tkinter app with a search box that opens the entered location in Google Maps in your web browser.", + "emoji": "🌐", + "runnable": false, + "hasPlayground": false, + "lines": 45, + "author": "nebulaanish" + }, + { + "id": "looking-busy", + "folder": "looking-busy", + "name": "Looking Busy", + "blurb": "Moves the mouse slightly every 10 seconds to keep your computer and apps from going idle.", + "emoji": "🖱️", + "runnable": false, + "hasPlayground": false, + "lines": 24, + "author": "ibra-kdbra" + }, + { + "id": "love-calculator", + "folder": "love-calculator", + "name": "Love Calculator", + "blurb": "A console program that takes two names and computes a playful \"love score\" percentage based on shared vowels, consonants, first letters, and name l…", + "emoji": "🧮", + "runnable": true, + "hasPlayground": true, + "lines": 135 + }, + { + "id": "lyrics-extractor", + "folder": "Lyrics-Extractor", + "name": "Lyrics Extractor", + "blurb": "A console program that takes a song name and fetches its title and lyrics using the lyrics-extractor library and Google Custom Search.", + "emoji": "🔍", + "runnable": false, + "hasPlayground": false, + "lines": 19, + "author": "sirKiraUzumaki" + }, + { + "id": "madlibs", + "folder": "Madlibs Generator", + "name": "Madlibs Generator", + "blurb": "A Tkinter app that lets you pick one of three story templates and fill in a noun, adjective, verb, and adverb to generate a Mad Lib story.", + "emoji": "✏️", + "runnable": true, + "hasPlayground": true, + "lines": 93, + "author": "ZackeryRSmith" + }, + { + "id": "make-api", + "folder": "Make-API", + "name": "Make-API", + "blurb": "A small Flask web API that returns a random quote (read from quote.txt) as JSON on the root route.", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 18, + "author": "ZackeryRSmith" + }, + { + "id": "market-financial-sentiment-predictor", + "folder": "Market Financial Sentiment Predictor", + "name": "Market Financial Sentiment Predictor", + "blurb": "All day various news about companies comes and not all people can track this news. Due to this they can miss great investment opportinities or they…", + "emoji": "📰", + "runnable": false, + "hasPlayground": false, + "lines": 71, + "author": "rik-chatterjee" + }, + { + "id": "mastermind", + "folder": "Mastermind", + "name": "Mastermind", + "blurb": "A console number-guessing game: the program picks a random 4-digit number and tells you how many digits you got right after each guess until you cr…", + "emoji": "🎮", + "runnable": true, + "hasPlayground": true, + "lines": 65, + "author": "mr-desilva" + }, + { + "id": "medium-article-reader", + "folder": "Medium Article Reader", + "name": "Medium Article Reader", + "blurb": "Scrapes an article from a URL, summarizes it with an OpenAI LLM via LangChain, and reads the summary or full text aloud with text-to-speech.", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 58, + "author": "ZackeryRSmith" + }, + { + "id": "merge-pdfs", + "folder": "Merge PDFs", + "name": "Merge PDFs", + "blurb": "A CLI tool that lists PDF files in a directory, lets you include/exclude files interactively, and merges the selected PDFs into a single file.", + "emoji": "📄", + "runnable": false, + "hasPlayground": false, + "lines": 133, + "author": "SuhasBk" + }, + { + "id": "minecraft-in-python-main", + "folder": "Minecraft-in-Python-main", + "name": "Minecraft in Python", + "blurb": "A simple Minecraft-style voxel sandbox built with the ursina 3D game engine. You can walk around in first-person, place and break blocks of several…", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 112, + "author": "benedictprajwal" + }, + { + "id": "mineral-processing-technology-image-analytics", + "folder": "Mineral Processing Technology-Image Analytics", + "name": "Mineral-Processing-Technology-Image-Analytics", + "blurb": "In the field of Mineral Processing Technology, size analysis of the various particles of an extracted sample is of importance in determining the qu…", + "emoji": "📊", + "runnable": false, + "hasPlayground": false, + "lines": 125, + "author": "manishkumar00208" + }, + { + "id": "minesweeper", + "folder": "minesweeper", + "name": "Minesweeper", + "blurb": "A console version of the classic Minesweeper game. It builds a board, randomly plants bombs, and asks the player where to dig (entered as row,col).…", + "emoji": "💣", + "runnable": true, + "hasPlayground": true, + "lines": 213, + "author": "myudak" + }, + { + "id": "ml-notebooks-beginners", + "folder": "ML-Notebooks_Beginners", + "name": "ML Notebooks for Beginners", + "blurb": "A collection of beginner-friendly machine learning / mathematics notes. notebooks/maths/algebra.py contains notes on algebra concepts for ML (funct…", + "emoji": "🧠", + "runnable": false, + "hasPlayground": false, + "lines": 0, + "author": "ibra-kdbra" + }, + { + "id": "mobile-document-scanner", + "folder": "Mobile Document Scanner", + "name": "Mobile Document Scanner", + "blurb": "Turns a photo of a document into a clean, top-down \"scanned\" image. It detects the document edges, finds its contour, applies a four-point perspect…", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 80, + "author": "MonitSharma" + }, + { + "id": "mongo-crud", + "folder": "Mongo CRUD", + "name": "Mongo CRUD", + "blurb": "A small example of create/read/update/delete operations against a MongoDB database using pymongo. conexao.py opens the database connection and usua…", + "emoji": "📊", + "runnable": false, + "hasPlayground": false, + "lines": 6, + "author": "jrbublitz" + }, + { + "id": "morse-code-translator", + "folder": "Morse-Code-Translator", + "name": "Morse Code Translator", + "blurb": "An interactive Morse code translator. Choose E to encrypt text into Morse code or D to decrypt Morse code back into plain text.", + "emoji": "🔐", + "runnable": true, + "hasPlayground": true, + "lines": 127, + "author": "ahmedalhamad7" + }, + { + "id": "morsecode-translator", + "folder": "MorseCode Translator", + "name": "Morse Code Translator", + "blurb": "Encrypts plain text into Morse code and decrypts Morse code back into text using a Morse code dictionary. The main() function runs a hard-coded dem…", + "emoji": "🔐", + "runnable": true, + "hasPlayground": true, + "lines": 119, + "author": "ZackeryRSmith" + }, + { + "id": "motivatebot", + "folder": "MotivateBot", + "name": "MotivateBot", + "blurb": "Generates an inspirational message using the OpenAI GPT API. Given a prompt, it calls the OpenAI completion endpoint and prints the generated motiv…", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 27, + "author": "EFFLUX110" + }, + { + "id": "autogui", + "folder": "AutoGui", + "name": "Mouse Activity Script", + "blurb": "This Python script periodically moves the mouse and performs a click action. It’s useful for keeping the system active to avoid timeouts or showing…", + "emoji": "🖱️", + "runnable": false, + "hasPlayground": false, + "lines": 22, + "author": "jadkinsgr" + }, + { + "id": "movieapi-and-ml", + "folder": "MovieApi_and_ML", + "name": "Movie API and ML", + "blurb": "A Django web application that exposes a movie API and a machine-learning recommendation system. It combines movie metadata with ML algorithms to pr…", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 23 + }, + { + "id": "movie-rater", + "folder": "movie-rater", + "name": "Movie Ratings API", + "blurb": "This project is a simple Python API that manages movies and their respective ratings. It serves as a centralized hub for movie enthusiasts to share…", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 58, + "author": "Mrinank-Bhowmick" + }, + { + "id": "movie-recommendation", + "folder": "Movie recommendation", + "name": "Movie Recommendation Engine", + "blurb": "A content-based movie recommender. It combines movie keywords, cast, genres and director into a single text feature, vectorises it with CountVector…", + "emoji": "👍", + "runnable": false, + "hasPlayground": false, + "lines": 145, + "author": "rajdeepdas2000" + }, + { + "id": "mqtt-client", + "folder": "MQTT Client", + "name": "MQTT Client", + "blurb": "This file includes:", + "emoji": "🐍", + "runnable": false, + "hasPlayground": false, + "lines": 55, + "author": "jonascarvalh" + }, + { + "id": "multiplayer-socket", + "folder": "multiplayer_socket", + "name": "Multiplayer Socket Games", + "blurb": "A two-player networked game system using TCP sockets. server.py hosts the games and client.py connects to it. Players can choose between a multipla…", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 108, + "author": "QubitMatrix" + }, + { + "id": "music-player", + "folder": "Music Player", + "name": "Music Player (Sangeet)", + "blurb": "A desktop music player built with Tkinter. It lets you load a directory of audio tracks into a playlist and play, pause, resume and stop songs usin…", + "emoji": "🎵", + "runnable": false, + "hasPlayground": false, + "lines": 157, + "author": "GargiMittal" + }, + { + "id": "nasa-apod", + "folder": "NASA-APOD", + "name": "NASA-APOD", + "blurb": "Astronomy Picture of the Day is a popular website from NASA. This app retrives the metadata of an image from APOD and the image itself (including t…", + "emoji": "🌐", + "runnable": false, + "hasPlayground": false, + "lines": 17, + "author": "dapoadedire" + }, + { + "id": "neurons", + "folder": "Neurons", + "name": "Neurons", + "blurb": "A terminal animation that simulates \"neurons\" on a grid. Each neuron randomly dies or moves in one of four directions on every tick, producing an e…", + "emoji": "🧠", + "runnable": true, + "hasPlayground": true, + "lines": 104, + "author": "Zedeldi" + }, + { + "id": "computer-algebra", + "folder": "computer-algebra", + "name": "Newton CAS Python Wrapper and GUI", + "blurb": "This project aims to provide a Python wrapper and GUI for the Newton API, a Computer Algebra System (CAS) that allows users to perform various math…", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 130, + "author": "ca20110820" + }, + { + "id": "number-guessing-app", + "folder": "Number Guessing App", + "name": "Number Guessing App", + "blurb": "A two-mode number guessing game. In mode 1 the player guesses a random number chosen by the computer; in mode 2 the computer guesses the number the…", + "emoji": "🎮", + "runnable": true, + "hasPlayground": true, + "lines": 79, + "author": "cypherab01" + }, + { + "id": "watermarker", + "folder": "Watermarker", + "name": "olomi", + "blurb": "A python program that allows users to generate watermarked images.", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 39, + "author": "highb33kay" + }, + { + "id": "video-subtitle-generator", + "folder": "Video-subtitle-generator", + "name": "on Ubuntu or Debian", + "blurb": "It also requires the command-line tool ffmpeg to be installed on your system, which is available from most package managers:", + "emoji": "🖥️", + "runnable": false, + "hasPlayground": false, + "lines": 119, + "author": "Mrinank-Bhowmick" + }, + { + "id": "online-trivia", + "folder": "Online Trivia", + "name": "Online Trivia", + "blurb": "A console trivia quiz that fetches true/false questions from the Open Trivia Database API. It displays each question, accepts the player's answer,…", + "emoji": "❓", + "runnable": false, + "hasPlayground": false, + "lines": 72, + "author": "whoisjayd" + }, + { + "id": "gpt-and-langchain", + "folder": "Gpt-And-Langchain", + "name": "OpenAI and Langchain", + "blurb": "+ About + Getting Started", + "emoji": "🐍", + "runnable": false, + "hasPlayground": false, + "lines": 52 + }, + { + "id": "opencv-color-detect-in-live-feed", + "folder": "OpenCV_color_detect_in_live_feed", + "name": "OpenCV Color Detection in Live Feed", + "blurb": "Captures a live video feed from a webcam and detects the colour at the centre of the frame. It converts each frame to HSV, reads the hue value at t…", + "emoji": "🎬", + "runnable": false, + "hasPlayground": false, + "lines": 62, + "author": "GigabyteZX1" + }, + { + "id": "organize-directory", + "folder": "Organize_Directory", + "name": "Organize Directory", + "blurb": "A file organiser. Given a directory path, it sorts the files inside it into category folders (images, music, video, executables, archives, torrent,…", + "emoji": "🎬", + "runnable": false, + "hasPlayground": false, + "lines": 91, + "author": "ibra-kdbra" + }, + { + "id": "othello", + "folder": "Othello", + "name": "Othello", + "blurb": "

⚫ Othello/Reversi ⚪

", + "emoji": "⚫", + "runnable": false, + "hasPlayground": false, + "lines": 5, + "author": "TERNION-1121" + }, + { + "id": "otp-generator", + "folder": "Otp_Generator", + "name": "OTP Generator", + "blurb": "Generates one-time passwords (OTPs) of a given length. The Otp class can produce OTPs made of digits only, digits with uppercase letters, digits wi…", + "emoji": "🔑", + "runnable": true, + "hasPlayground": true, + "lines": 73, + "author": "iamdestinychild" + }, + { + "id": "otp-verfication-system", + "folder": "OTP-Verfication-System", + "name": "OTP Verification System", + "blurb": "Generates a random 6-digit OTP, emails it to a user via Gmail's SMTP server, and then verifies the code the user types back in.", + "emoji": "📧", + "runnable": false, + "hasPlayground": false, + "lines": 31, + "author": "mzaryabrafique" + }, + { + "id": "pwd", + "folder": "Password Projects/Password Generator", + "name": "Password Generator", + "blurb": "Generate random, strong passwords from the command line.", + "emoji": "🔐", + "runnable": true, + "hasPlayground": true, + "lines": 30, + "author": "u749929" + }, + { + "id": "password-projects", + "folder": "Password Projects", + "name": "Password Projects", + "blurb": "The Password Projects directory contains several mini-projects related to passwords and their security.", + "emoji": "🔑", + "runnable": false, + "hasPlayground": false, + "lines": 34, + "author": "u749929" + }, + { + "id": "pdf-reader", + "folder": "PDF_Reader", + "name": "PDF Reader", + "blurb": "An AI-powered PDF question-answering tool. It loads a PDF with LangChain, splits and embeds the text into a Chroma vector store, summarises the doc…", + "emoji": "🧠", + "runnable": false, + "hasPlayground": false, + "lines": 69, + "author": "aryangulati" + }, + { + "id": "audiobook", + "folder": "Audiobook", + "name": "PDF Text-to-Speech Reader", + "blurb": "This Python script uses pyttsx3 and PyPDF2 to read the text content of a PDF file and convert it to speech using text-to-speech synthesis. It allow…", + "emoji": "🔊", + "runnable": false, + "hasPlayground": false, + "lines": 53, + "author": "ZackeryRSmith" + }, + { + "id": "pdf-to-text", + "folder": "pdf_to_text", + "name": "PDF to Text Converter", + "blurb": "Certainly, here's an updated readme.md based on your provided code:", + "emoji": "📄", + "runnable": false, + "hasPlayground": false, + "lines": 81, + "author": "Mrinank-Bhowmick" + }, + { + "id": "pdf-paranoia", + "folder": "pdf-paranoia", + "name": "pdf-paranoia", + "blurb": "Bulk-encrypts and decrypts PDF files. It walks a directory tree, encrypts every unencrypted PDF with a user-supplied password (writing the result i…", + "emoji": "🔐", + "runnable": false, + "hasPlayground": false, + "lines": 74, + "author": "ibra-kdbra" + }, + { + "id": "personal-finance-tracker", + "folder": "Personal-Finance-tracker", + "name": "Personal Finance Tracker", + "blurb": "A console-based personal finance manager. It lets you add expenses (name, amount, category), list all expenses, and calculate the total spent, all…", + "emoji": "💰", + "runnable": true, + "hasPlayground": true, + "lines": 54, + "author": "Manishak798" + }, + { + "id": "pig-latin", + "folder": "Pig_latin", + "name": "Pig Latin Converter", + "blurb": "Converts an English sentence into Pig Latin. function.py holds the conversion logic and main.py prompts the user for a string and prints the conver…", + "emoji": "🔄", + "runnable": true, + "hasPlayground": true, + "lines": 12 + }, + { + "id": "ping-pong", + "folder": "ping_pong", + "name": "Ping Pong", + "blurb": "A two-player Pong game built with Python's turtle graphics. Player A uses W/S and Player B uses the arrow keys to move their paddles; the ball boun…", + "emoji": "🏓", + "runnable": false, + "hasPlayground": false, + "lines": 160, + "author": "AtharvaDeshmukh0909" + }, + { + "id": "pokemon-battle", + "folder": "Pokemon Battle", + "name": "Pokemon Battle", + "blurb": "A turn-based Pokemon battle game in the console. Two Pokemon (with types, moves, attack and defense stats) fight each other; type advantages affect…", + "emoji": "🎮", + "runnable": true, + "hasPlayground": true, + "lines": 202, + "author": "EpicNesh26" + }, + { + "id": "pong", + "folder": "PONG", + "name": "PONG", + "blurb": "A single-player Pong game built with pygame. It features a difficulty menu (Easy / Medium / Hard), an AI-controlled opponent paddle, sound effects,…", + "emoji": "🏓", + "runnable": false, + "hasPlayground": false, + "lines": 379, + "author": "ambushneupane" + }, + { + "id": "port-scaner", + "folder": "Port_Scaner", + "name": "Port Scanner", + "blurb": "Port Scanner is developed for: checking if the port is open", + "emoji": "🌐", + "runnable": false, + "hasPlayground": false, + "lines": 31, + "author": "bim22614" + }, + { + "id": "prettified-stopwatch", + "folder": "prettified-stopwatch", + "name": "Prettified Stopwatch", + "blurb": "A console stopwatch that records lap times. Press ENTER to start, ENTER again to mark each lap, and Ctrl-C to stop. Results are copied to the syste…", + "emoji": "🕐", + "runnable": false, + "hasPlayground": false, + "lines": 47, + "author": "ibra-kdbra" + }, + { + "id": "print-colored-text", + "folder": "Print_Colored_Text", + "name": "Print Colored Text", + "blurb": "Prints a few lines of text to the terminal in different foreground and background colors using the colorama library.", + "emoji": "🎨", + "runnable": false, + "hasPlayground": false, + "lines": 16, + "author": "Nowshin1077" + }, + { + "id": "reciept-generator", + "folder": "reciept generator", + "name": "Project Name:", + "blurb": "
", + "emoji": "🏷️", + "runnable": false, + "hasPlayground": false, + "lines": 98, + "author": "Subha-5" + }, + { + "id": "projecteuler", + "folder": "ProjectEuler", + "name": "Project-Euler", + "blurb": "Project Euler is a series of challenging mathematical/computer programming problems that will require more than just mathematical insights to solve…", + "emoji": "➗", + "runnable": true, + "hasPlayground": true, + "lines": 11, + "author": "ZackeryRSmith" + }, + { + "id": "proxy-scrapper", + "folder": "proxy-scrapper", + "name": "Proxy Scrapper", + "blurb": "Install BeautifulSoup", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 20, + "author": "jakbin" + }, + { + "id": "battleship", + "folder": "Battleship", + "name": "Py-Battleship", + "blurb": "![License](https://opensource.org/license/GPL-3-0)", + "emoji": "🚢", + "runnable": true, + "hasPlayground": true, + "lines": 125, + "author": "robertlent" + }, + { + "id": "python-banking-system", + "folder": "Python Banking System", + "name": "Python Banking System", + "blurb": "The Python Banking System is a simple console-based banking application that allows users to create accounts, perform transactions (deposits and wi…", + "emoji": "🖥️", + "runnable": true, + "hasPlayground": true, + "lines": 194 + }, + { + "id": "python-story-generator", + "folder": "Python story generator", + "name": "Python Story Generator", + "blurb": "A Tkinter desktop application that generates a random short story (at least 100 words) by combining randomly chosen phrases. Click the \"Generate St…", + "emoji": "🪟", + "runnable": false, + "hasPlayground": false, + "lines": 95, + "author": "Ylavish64" + }, + { + "id": "qr-code-generator-with-window-and-simple-ui", + "folder": "qr-code-generator-with-window-and-simple-ui", + "name": "QR Code Generator with Window and Simple UI", + "blurb": "A Tkinter desktop application that takes a text/URL, save location, file name, and size, then generates a QR code image and saves it to the chosen…", + "emoji": "▦", + "runnable": false, + "hasPlayground": false, + "lines": 108, + "author": "VedSadh" + }, + { + "id": "qr", + "folder": "QRCode-Generator", + "name": "QRCode Generator", + "blurb": "A console script that asks for a website/URL, encodes it into a QR code, and saves the image to the static folder as qrcode.jpg.", + "emoji": "▦", + "runnable": true, + "hasPlayground": true, + "lines": 19, + "author": "SubramanyaKS" + }, + { + "id": "qrcode-scanner", + "folder": "QRCode Scanner", + "name": "QRCode Scanner", + "blurb": "An app to scan QRcode via image or Webcam available in PC.", + "emoji": "▦", + "runnable": false, + "hasPlayground": false, + "lines": 49 + }, + { + "id": "qt5-youtube", + "folder": "Qt5_YouTube", + "name": "Qt5 YouTube Player", + "blurb": "A PyQt5 desktop application that plays local video files and YouTube videos, with playback controls for position, volume, and speed.", + "emoji": "🎬", + "runnable": false, + "hasPlayground": false, + "lines": 226, + "author": "Zedeldi" + }, + { + "id": "quickwordcloud", + "folder": "QuickWordCloud", + "name": "QuickWordCloud", + "blurb": "Quick word cloud is a small and interesting project that can quickly create a word cloud out of a text file. Its a small & helpful project that can…", + "emoji": "📖", + "runnable": false, + "hasPlayground": false, + "lines": 37, + "author": "IamSoo" + }, + { + "id": "quiz-game", + "folder": "Quiz Game", + "name": "Quiz Game", + "blurb": "A console-based quiz game that asks three questions, scores the player's answers, and stores each player's name and score in a local SQLite databas…", + "emoji": "❓", + "runnable": true, + "hasPlayground": true, + "lines": 115, + "author": "ZackeryRSmith" + }, + { + "id": "advisor", + "folder": "Advisor", + "name": "Random Advisor", + "blurb": "A small Tkinter desktop app that fetches a random piece of advice from the Advice Slip API and shows it in a window. Click Get Advice for a new one.", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 39, + "author": "ibra-kdbra" + }, + { + "id": "random-quote-generator", + "folder": "Random-Quote-Generator", + "name": "Random Quote Generator", + "blurb": "git clone https://github.com/your-username/random-quote-generator.git cd random-quote-generator pip install requests python randomquotegenerator.py…", + "emoji": "🎰", + "runnable": false, + "hasPlayground": false, + "lines": 37, + "author": "saiuttejr" + }, + { + "id": "reddit-scraper", + "folder": "reddit-scraper", + "name": "Reddit Scraper", + "blurb": "Fetches the top posts from the r/python subreddit via Reddit's JSON API and stores them in a local SQLite database (redditnews.db).", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 97, + "author": "ibra-kdbra" + }, + { + "id": "regex-search", + "folder": "regex-search", + "name": "Regex Search", + "blurb": "A console tool that prompts for a regular expression and prints every line in the .txt files of a folder that matches it.", + "emoji": "🔍", + "runnable": false, + "hasPlayground": false, + "lines": 34, + "author": "ibra-kdbra" + }, + { + "id": "regex-strip", + "folder": "regex-strip", + "name": "Regex Strip", + "blurb": "A small demonstration that reimplements Python's str.strip() method using regular expressions to remove leading and trailing whitespace.", + "emoji": "🔣", + "runnable": true, + "hasPlayground": true, + "lines": 23, + "author": "ibra-kdbra" + }, + { + "id": "rename-images", + "folder": "Rename_Images", + "name": "Rename Images", + "blurb": "A console tool that walks a given directory and lets the user interactively rename each image file (jpg, png, jpeg).", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 30, + "author": "ibra-kdbra" + }, + { + "id": "chat-gpt-discord-bot", + "folder": "Chat-GPT-Discord-Bot", + "name": "Requirements", + "blurb": "This project requires python version 3.12.3 discord.py version 2.3.2, OpenAI version 1.30.1 and python-dotenv version 1.0.1 which can be installed…", + "emoji": "💬", + "runnable": false, + "hasPlayground": false, + "lines": 621, + "author": "Alby084" + }, + { + "id": "resize-image", + "folder": "Resize_Image", + "name": "Resize Image", + "blurb": "A console tool that resizes one image or all images in a directory to a user-specified width and height, then saves the results to an output folder.", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 44, + "author": "ibra-kdbra" + }, + { + "id": "restrauntapi", + "folder": "RestrauntAPI", + "name": "Restraunt_API", + "blurb": "The Restaurant API consists of: adding items to the menu, viewing the menu, searching for menu items based on a string, creating orders, and displa…", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 23, + "author": "KartikRN" + }, + { + "id": "reuters-scraper", + "folder": "reuters-scraper", + "name": "Reuters Scraper", + "blurb": "Scrapes technology news headlines, summaries, times, and links from the Reuters website and writes them to reutersscrape.csv.", + "emoji": "📰", + "runnable": false, + "hasPlayground": false, + "lines": 42, + "author": "ibra-kdbra" + }, + { + "id": "rps", + "folder": "Rock_Paper_Scissors", + "name": "Rock Paper Scissors", + "blurb": "A console Rock-Paper-Scissors game. The player picks a move each round, the computer picks randomly, and the score is tracked until the player choo…", + "emoji": "✊", + "runnable": true, + "hasPlayground": true, + "lines": 44, + "author": "ZackeryRSmith" + }, + { + "id": "roll-a-dice", + "folder": "Roll_A_dice", + "name": "Roll A Dice", + "blurb": "A console game where the player rolls a virtual dice and is given a task or riddle based on the number rolled. The player's name, roll, and answer…", + "emoji": "🎲", + "runnable": false, + "hasPlayground": false, + "lines": 106, + "author": "snehafarkya" + }, + { + "id": "rubik-tracking", + "folder": "Rubik-tracking", + "name": "Rubik Tracking", + "blurb": "Tracks a green-colored object (e.g., a Rubik's cube) in a live webcam feed using OpenCV color masking and draws its motion trail on screen.", + "emoji": "📷", + "runnable": false, + "hasPlayground": false, + "lines": 61, + "author": "this-mkhy" + }, + { + "id": "sales-optimizer", + "folder": "Sales Optimizer", + "name": "Sales Optimizer", + "blurb": "The retail stores always face a problem of tension between demand and supply. Sometimes they stock more products than it has demand or sales and so…", + "emoji": "📈", + "runnable": false, + "hasPlayground": false, + "lines": 231, + "author": "rik-chatterjee" + }, + { + "id": "scheduledshutdown", + "folder": "scheduledShutdown", + "name": "Scheduled Shutdown", + "blurb": "A console utility that asks for a number of minutes and then shuts down the computer after that delay using a system shutdown command.", + "emoji": "🖥️", + "runnable": false, + "hasPlayground": false, + "lines": 20, + "author": "Prajwol-Shrestha" + }, + { + "id": "scientific-calculator", + "folder": "Scientific-Calculator", + "name": "Scientific-Calculator", + "blurb": "This Scientific Calculator is a calculator designed to help you calculate science, engineering, and mathematics problems. It has way more buttons t…", + "emoji": "🧮", + "runnable": false, + "hasPlayground": false, + "lines": 379, + "author": "tamojeetK" + }, + { + "id": "scrap-ycombinator", + "folder": "scrap-ycombinator", + "name": "Scrape Y Combinator", + "blurb": "Scrapes article titles and links from the Hacker News (Y Combinator) front page and writes them to ycombinatornews.csv.", + "emoji": "📰", + "runnable": false, + "hasPlayground": false, + "lines": 28, + "author": "dapoadedire" + }, + { + "id": "screenrecorder", + "folder": "ScreenRecorder", + "name": "Screen Recorder using Python", + "blurb": "pip install opencv-python", + "emoji": "📷", + "runnable": false, + "hasPlayground": false, + "lines": 46, + "author": "sb-decoder" + }, + { + "id": "seek-with-hand-track", + "folder": "Seek_with_hand_track", + "name": "Seek with Hand Track", + "blurb": "Uses a webcam and MediaPipe hand tracking to detect hand gestures and send left/right arrow keypresses, allowing you to seek a video by tilting you…", + "emoji": "🎬", + "runnable": false, + "hasPlayground": false, + "lines": 109, + "author": "QuantumNovice" + }, + { + "id": "selfie-with-python", + "folder": "Selfie_with_Python", + "name": "Selfie with Python", + "blurb": "Opens the webcam in an OpenCV window; press Space to capture a selfie (saved as a JPG) and Escape to quit.", + "emoji": "📷", + "runnable": false, + "hasPlayground": false, + "lines": 29 + }, + { + "id": "send-email", + "folder": "Send-Email", + "name": "Send Email", + "blurb": "A script that connects to Gmail's SMTP server and sends a plain-text email. The sender's username and password are read from environment variables.", + "emoji": "🔑", + "runnable": false, + "hasPlayground": false, + "lines": 36, + "author": "ZackeryRSmith" + }, + { + "id": "servo-motor-classifier", + "folder": "servo motor classifier", + "name": "Servo motor classifier", + "blurb": "Servos are basic components of many machines. There are different classes of servo for different function and to classify them can be hard for peop…", + "emoji": "🔮", + "runnable": false, + "hasPlayground": false, + "lines": 94, + "author": "rik-chatterjee" + }, + { + "id": "calc", + "folder": "Calculator", + "name": "Simple Calculator", + "blurb": "This Python script is a simple calculator that allows for basic arithmetic operations such as addition, subtraction, multiplication, division, and…", + "emoji": "🧮", + "runnable": false, + "hasPlayground": false, + "lines": 186, + "author": "shubham7668" + }, + { + "id": "simple-plagiarism-checker-project", + "folder": "Simple-Plagiarism-Checker-Project", + "name": "Simple-Plagiarism-Checker", + "blurb": "Web application of Plagiarism Checker using Python-Flask. TF-IDF and cosine similarity is a very common technique. It allows the system to quickly…", + "emoji": "🌐", + "runnable": false, + "hasPlayground": false, + "lines": 106, + "author": "sudhanshu-77" + }, + { + "id": "sketchifyme", + "folder": "SketchifyMe", + "name": "SketchifyMe", + "blurb": "Seamlessly convert your images into pencil-sketch renditions, facilitating the effortless creation of your sketches.", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 28, + "author": "emanalytic" + }, + { + "id": "skycast", + "folder": "Skycast", + "name": "SkyCast 🌤️", + "blurb": "SkyCast is a Python application that allows users to retrieve weather information for a specific location. It provides two main features: Today's W…", + "emoji": "⛅", + "runnable": false, + "hasPlayground": false, + "lines": 212, + "author": "Devparihar5" + }, + { + "id": "slice-audio", + "folder": "Slice-Audio", + "name": "Slice Audio from any mp3 file", + "blurb": "We all want only a portion form a particular audio file. This python scrip does the same for you in few very easy steps.", + "emoji": "🎵", + "runnable": false, + "hasPlayground": false, + "lines": 33, + "author": "Dishant10" + }, + { + "id": "sms-chatbot", + "folder": "SMS_ChatBot", + "name": "SMS CHATBOT", + "blurb": "![N|Solid", + "emoji": "💬", + "runnable": false, + "hasPlayground": false, + "lines": 35, + "author": "Vivek-GuptaXCode" + }, + { + "id": "snake", + "folder": "Snake Game", + "name": "Snake Game", + "blurb": "A classic Snake game built with Pygame. The player controls a snake that grows as it eats food, tracking a score and a high score, with game-over a…", + "emoji": "🐍", + "runnable": false, + "hasPlayground": false, + "lines": 6, + "author": "EbrG786" + }, + { + "id": "snake-water-gun-game", + "folder": "snake_water_gun_game", + "name": "Snake Water Gun Game", + "blurb": "A console version of the snake-water-gun game (a rock-paper-scissors variant). The snake drinks the water, the gun shoots the snake, and the gun ha…", + "emoji": "✊", + "runnable": false, + "hasPlayground": false, + "lines": 84, + "author": "d-coder111" + }, + { + "id": "game-snake-water-gun", + "folder": "game-snake_water_gun", + "name": "Sname Water and Gun", + "blurb": "Yes — game.py is a console game using only input()/print() and the standard-library random module.", + "emoji": "🎮", + "runnable": true, + "hasPlayground": true, + "lines": 117, + "author": "Yashparwal1" + }, + { + "id": "social-media-content-creation", + "folder": "Social_media_content_creation", + "name": "Social Media Content Creation", + "blurb": "Welcome to the socialmediacontentcreation folder! This space is dedicated to projects related to social media content creation.", + "emoji": "🪐", + "runnable": false, + "hasPlayground": false, + "lines": 0, + "author": "Mrinank-Bhowmick" + }, + { + "id": "socket", + "folder": "Socket", + "name": "Socket example", + "blurb": "No — it opens real TCP sockets for client/server networking, which is not available in the browser sandbox.", + "emoji": "🌐", + "runnable": false, + "hasPlayground": false, + "lines": 15, + "author": "Alex108-lab" + }, + { + "id": "songsmashup", + "folder": "SongsMashup", + "name": "Songs Mashup", + "blurb": "A Flask web app that downloads songs from YouTube for a given singer, slices each track into short snippets, merges them into a single mashup audio…", + "emoji": "🎬", + "runnable": false, + "hasPlayground": false, + "lines": 136, + "author": "asingh4451" + }, + { + "id": "space-battle", + "folder": "space_battle", + "name": "Space Battle", + "blurb": "A Space Invaders style game built with Pygame. Move a UFO around the screen, shoot down descending enemies, and play with sound effects and backgro…", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 201, + "author": "invalid-email-address" + }, + { + "id": "space-shooter", + "folder": "Space Shooter", + "name": "Space Shooter", + "blurb": "A 2D space shooter game built with Pygame. Pilot a spaceship, fire lasers at incoming enemy ships, dodge enemy lasers and homing missiles, and rack…", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 509, + "author": "DevTomilola-OS" + }, + { + "id": "speed-type-test", + "folder": "Speed-Type-test", + "name": "Speed Typing Test", + "blurb": "A typing speed test built with Pygame. A random sentence is displayed; type it as fast and accurately as you can, and the app reports your time, ac…", + "emoji": "⌨️", + "runnable": false, + "hasPlayground": false, + "lines": 175, + "author": "AdityaSahai123" + }, + { + "id": "split-tip", + "folder": "Split_Tip", + "name": "Split Tip Calculator", + "blurb": "A simple console tip calculator. Enter a bill amount, a tip percentage, and the number of people, and it prints how much each person owes in tip an…", + "emoji": "🧮", + "runnable": true, + "hasPlayground": true, + "lines": 13, + "author": "DevTomilola-OS" + }, + { + "id": "sponge-bob", + "folder": "sponge-bob", + "name": "SpongeBob Turtle Drawing", + "blurb": "A turtle-graphics script that draws a SpongeBob SquarePants character (body, pants, shirt, tie, eyes, and smile) on screen.", + "emoji": "🎨", + "runnable": false, + "hasPlayground": false, + "lines": 141, + "author": "JenilGajjar20" + }, + { + "id": "spotify-player", + "folder": "Spotify Player", + "name": "Spotify Player", + "blurb": "A command-line Spotify tool that can play a searched song, recommend songs based on three seed tracks, and generate a new playlist similar to an ex…", + "emoji": "🎵", + "runnable": false, + "hasPlayground": false, + "lines": 390, + "author": "omkarxpatel" + }, + { + "id": "stock-market-dashboard", + "folder": "Stock-Market-Dashboard", + "name": "Stock Market Dashboard", + "blurb": "A Streamlit dashboard that visualizes S&P 500 stock data with Bollinger Bands, MACD, and RSI indicators computed from closing prices. Companies are…", + "emoji": "📈", + "runnable": false, + "hasPlayground": false, + "lines": 105, + "author": "Crack-er-jack" + }, + { + "id": "student-management-system", + "folder": "student-management-system", + "name": "Student Management System", + "blurb": "For every school important task for administration department is to manage student information in a procedure oriented manner with latest updates f…", + "emoji": "✅", + "runnable": false, + "hasPlayground": false, + "lines": 194, + "author": "siddharth9300" + }, + { + "id": "subnetting-flsm", + "folder": "Subnetting Flsm", + "name": "Subnetting_FLSM", + "blurb": "SubnettingFLSM is developed for: 1.Calculation and analyzing number of hosts 2.Distribution hosts for each subnet by FLSM method 3.Transferring uni…", + "emoji": "🔢", + "runnable": true, + "hasPlayground": true, + "lines": 195, + "author": "bim22614" + }, + { + "id": "subtitle-synchronizer-py", + "folder": "Subtitle_synchronizer.py", + "name": "Subtitle Synchronizer", + "blurb": "A console tool that shifts all timestamps in an SRT subtitle file by a given number of milliseconds, reading from input.srt and writing the synchro…", + "emoji": "🔢", + "runnable": false, + "hasPlayground": false, + "lines": 66, + "author": "shriyansnaik" + }, + { + "id": "sudoku-solver", + "folder": "Sudoku_solver", + "name": "Sudoku Solver", + "blurb": "A console Sudoku generator and solver. It randomly generates a valid Sudoku board, removes cells to create a puzzle, then solves it with a backtrac…", + "emoji": "🔢", + "runnable": true, + "hasPlayground": true, + "lines": 175, + "author": "saltX5" + }, + { + "id": "sudoku-solver-2", + "folder": "Sudoku-Solver", + "name": "Sudoku Solver (GUI)", + "blurb": "A Pygame-based Sudoku game. It generates a random grid you can play interactively, supports hints, tracks wrong answers and elapsed time, and can v…", + "emoji": "🔢", + "runnable": false, + "hasPlayground": false, + "lines": 317 + }, + { + "id": "takeimage", + "folder": "takeImage", + "name": "takeImage", + "blurb": "This python script is used for taking images from your webcam and saving it on your local device. Path to the folder can be specified using the fol…", + "emoji": "📷", + "runnable": false, + "hasPlayground": false, + "lines": 35, + "author": "anju-chhetri" + }, + { + "id": "tennistournamentsim", + "folder": "TennisTournamentSim", + "name": "Tennis Tournament Simulator", + "blurb": "A console simulation of a four-player single-elimination tennis tournament. Players get random names, schools, and skill levels; match outcomes are…", + "emoji": "🎰", + "runnable": true, + "hasPlayground": true, + "lines": 435, + "author": "jpyces" + }, + { + "id": "tesla", + "folder": "Tesla", + "name": "Tesla Self-Driving Car", + "blurb": "A Pygame simulation of a self-driving car. A car follows a track image by sampling pixel colors ahead of it to decide when to drive straight or turn.", + "emoji": "🖼️", + "runnable": false, + "hasPlayground": false, + "lines": 77, + "author": "ZackeryRSmith" + }, + { + "id": "tetris-game", + "folder": "Tetris Game", + "name": "Tetris Game", + "blurb": "A classic Tetris game built with Pygame. Falling tetrominoes can be moved, rotated, and hard-dropped; full lines are cleared for points, and the ga…", + "emoji": "🟦", + "runnable": false, + "hasPlayground": false, + "lines": 237, + "author": "Jishnu2608" + }, + { + "id": "textdetection", + "folder": "TextDetection", + "name": "Text Detection", + "blurb": "A script that detects and prints text found in an image using the Google Cloud Vision API.", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 26, + "author": "Mrinank-Bhowmick" + }, + { + "id": "text-editor", + "folder": "Text Editor", + "name": "Text Editor", + "blurb": "Notepad-style text editor applications built with Tkinter. Small-version/txteditor.py is a compact editor with open/save/close and light/dark theme…", + "emoji": "🪟", + "runnable": false, + "hasPlayground": false, + "lines": 153, + "author": "samayita1606" + }, + { + "id": "text-summarizer", + "folder": "Text Summarizer", + "name": "Text Summarizer", + "blurb": "Long texts can be condensed into concise, understandable summaries using a Python this text summarizer project. It's helpful for immediately graspi…", + "emoji": "📝", + "runnable": false, + "hasPlayground": false, + "lines": 58, + "author": "VykSI" + }, + { + "id": "text-to-spreadsheet", + "folder": "Text_to_SpreadSheet", + "name": "Text to Spreadsheet", + "blurb": "A script that reads every .txt file in a directory and writes each file's lines into a separate column of an Excel worksheet, saving the result as…", + "emoji": "📊", + "runnable": false, + "hasPlayground": false, + "lines": 34, + "author": "ibra-kdbra" + }, + { + "id": "text-to-image-generation-project", + "folder": "Text-to-Image Generation Project", + "name": "Text-to-Image Generation Project", + "blurb": "A Jupyter notebook (Texttoimage.ipynb) that demonstrates generating images from text prompts using a deep-learning text-to-image model.", + "emoji": "🧠", + "runnable": false, + "hasPlayground": false, + "lines": 0, + "author": "anish2105" + }, + { + "id": "text-to-speech", + "folder": "Text to Speech", + "name": "Text-to-Speech", + "blurb": "No — gTTS sends text to Google's Translate TTS API over the network and writes/plays an mp3 file locally.", + "emoji": "🌍", + "runnable": false, + "hasPlayground": false, + "lines": 9, + "author": "sudipg4112001" + }, + { + "id": "text-translate", + "folder": "text-translate", + "name": "text-translate", + "blurb": "No — main.py uses the translate package which makes network requests, and translate GUIaudio.py additionally uses tkinter, gtts, and pygame.", + "emoji": "🌍", + "runnable": false, + "hasPlayground": false, + "lines": 80, + "author": "Mrinank-Bhowmick" + }, + { + "id": "tictactoe", + "folder": "Tic-Tac-Toe", + "name": "Tic-Tac-Toe", + "blurb": "Two implementations of the classic 3×3 game:", + "emoji": "⭕", + "runnable": true, + "hasPlayground": true, + "lines": 190, + "author": "Mrinank-Bhowmick" + }, + { + "id": "tictactoe-tylerpear", + "folder": "TicTacToe-TylerPear", + "name": "Tic-Tac-Toe (TylerPear)", + "blurb": "A two-player console Tic-Tac-Toe game. Players enter moves using position codes (TL, TM, TR, ML, MM, MR, BL, BM, BR) and the board is redrawn after…", + "emoji": "⭕", + "runnable": true, + "hasPlayground": true, + "lines": 181, + "author": "tylerapear" + }, + { + "id": "tile-matching", + "folder": "Tile Matching", + "name": "Tile Matching Game", + "blurb": "A memory tile-matching game built with Tkinter. Click pairs of tiles to reveal hidden colors and match them all before the 60-second timer runs out…", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 151, + "author": "kanchanraiii" + }, + { + "id": "timer", + "folder": "Timer", + "name": "Timer", + "blurb": "A simple countdown timer. It asks for a duration in seconds and counts up on screen, one second at a time, until the time is up.", + "emoji": "⏳", + "runnable": true, + "hasPlayground": true, + "lines": 23, + "author": "cj-praveen" + }, + { + "id": "tkinter", + "folder": "Tkinter", + "name": "Tkinter Demos", + "blurb": "A collection of Tkinter GUI examples. 1.py is a multi-frame student/college login and account-creation interface, and 2.py is a college-system wind…", + "emoji": "🔑", + "runnable": false, + "hasPlayground": false, + "lines": 230, + "author": "sudipg4112001" + }, + { + "id": "todolist", + "folder": "ToDoList", + "name": "ToDo List in Python 📒", + "blurb": "This is a simple command-line ToDo List application written in Python.", + "emoji": "✅", + "runnable": false, + "hasPlayground": false, + "lines": 118, + "author": "otahina" + }, + { + "id": "turtle-graphics", + "folder": "Turtle_Graphics", + "name": "Turtle Graphics", + "blurb": "Draws layered fractal tree patterns using Python's turtle graphics. Recursive draw calls render branching trees in multiple colors and sizes across…", + "emoji": "🎨", + "runnable": false, + "hasPlayground": false, + "lines": 282, + "author": "Nowshin1077" + }, + { + "id": "turtle-pattern", + "folder": "Turtle Pattern", + "name": "Turtle Pattern", + "blurb": "Draws a random walking pattern using Python's turtle graphics. A turtle moves in randomly chosen directions with random pen colors for 200 steps, p…", + "emoji": "🎨", + "runnable": false, + "hasPlayground": false, + "lines": 56, + "author": "Adhiraj-11" + }, + { + "id": "twitter-bot", + "folder": "Twitter-Bot", + "name": "Twitter-Bot", + "blurb": "This basic python project involves creating a Twitter bot using Tweepy to retweet tweets that contain certain hashtags related to Python and progra…", + "emoji": "🤖", + "runnable": false, + "hasPlayground": false, + "lines": 43, + "author": "harshhes" + }, + { + "id": "type-racer-game", + "folder": "Type Racer Game", + "name": "Type Racer Game", + "blurb": "A typing-speed game built with Tkinter. A random sentence appears and you type it against a countdown timer, with live progress, color feedback for…", + "emoji": "🎮", + "runnable": false, + "hasPlayground": false, + "lines": 201, + "author": "kanchanraiii" + }, + { + "id": "audioapi", + "folder": "AudioAPI", + "name": "Unofficial Whisper API", + "blurb": "Welcome to the Unofficial Whisper API documentation. This API allows you to utilize OpenAI's Whisper, a versatile speech recognition model trained…", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 68, + "author": "akghosh111" + }, + { + "id": "url-shortener", + "folder": "url_shortener", + "name": "URL Shortener", + "blurb": "I built code quality tools into project (linting, formatting, type annotation checks)", + "emoji": "🔗", + "runnable": false, + "hasPlayground": false, + "lines": 52, + "author": "justinjohnson-dev" + }, + { + "id": "video-reversal", + "folder": "Video Reversal", + "name": "Video reversing", + "blurb": "This python project reverses an input video", + "emoji": "🎬", + "runnable": false, + "hasPlayground": false, + "lines": 28, + "author": "blindaks" + }, + { + "id": "video-transcoder-project", + "folder": "video_transcoder_project", + "name": "video_transcoder_project", + "blurb": "Access the application in your browser at http://127.0.0.1:5000/.", + "emoji": "🌐", + "runnable": false, + "hasPlayground": false, + "lines": 4, + "author": "shashaaankkkkk" + }, + { + "id": "voice-to-text", + "folder": "Voice-to-Text", + "name": "Voice-to-Text", + "blurb": "Captures audio from the microphone and converts it to text using Google's speech recognition service.", + "emoji": "🔊", + "runnable": false, + "hasPlayground": false, + "lines": 30, + "author": "Mrinank-Bhowmick" + }, + { + "id": "weather-2", + "folder": "Weather", + "name": "Weather", + "blurb": "A console weather app. Enter a city name and it fetches the current weather description and temperature from the OpenWeatherMap API.", + "emoji": "⛅", + "runnable": false, + "hasPlayground": false, + "lines": 19, + "author": "ZackeryRSmith" + }, + { + "id": "weather", + "folder": "API Based Weather Report", + "name": "Weather Information App", + "blurb": "Welcome to the Weather Information App! This application allows users to fetch current weather information for a specific city using the OpenWeathe…", + "emoji": "☁️", + "runnable": false, + "hasPlayground": false, + "lines": 228, + "author": "srujan-landeri" + }, + { + "id": "web-crawler-movie-extract", + "folder": "web-crawler(movie extract)", + "name": "Web Crawler (Movie Extract)", + "blurb": "A web-scraping script intended to crawl Rotten Tomatoes' top-movies list, extract movie URLs, names, and synopses, and save them into an .xls sprea…", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 43, + "author": "sudipg4112001" + }, + { + "id": "web-scraping-jujustu-kaisen-manga", + "folder": "Web Scraping Jujustu Kaisen Manga", + "name": "Web Scraping Jujustu Kaisen Manga", + "blurb": "A simple yet interesting web scraping project to download all the chapters from Jujustu Kaisen Manga till date.", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 213, + "author": "Vishvam10" + }, + { + "id": "webbuttonsimpelgui", + "folder": "WebButtonSimpelGUI", + "name": "WebButtonSimpelGUI", + "blurb": "This project is a simple GUI application built using Python and the Tkinter library. It consists some buttons, each associated with a different act…", + "emoji": "🪟", + "runnable": false, + "hasPlayground": false, + "lines": 42, + "author": "Sameer-choudhary-git" + }, + { + "id": "website-blocker", + "folder": "Website Blocker", + "name": "Website Blocker", + "blurb": "The objective of Python website blocker is to block some certain websites which can distract the user during the specified amount of time. In this,…", + "emoji": "🌐", + "runnable": false, + "hasPlayground": false, + "lines": 51, + "author": "Dhruvil-Lakhtaria" + }, + { + "id": "weights-in-different-planets-gui", + "folder": "Weights_in_different_planets_GUI", + "name": "Weight Calculator for Different Planets", + "blurb": "This is a simple Python mini-project created using Tkinter to calculate weight on different planets in our solar system.", + "emoji": "🧮", + "runnable": false, + "hasPlayground": false, + "lines": 210, + "author": "Astrasv" + }, + { + "id": "what-for-dinner", + "folder": "what-for-dinner", + "name": "What For Dinner", + "blurb": "A console app that suggests a random meal for dinner. It fetches a random recipe from TheMealDB API and prints the meal name, origin, category, coo…", + "emoji": "🔌", + "runnable": false, + "hasPlayground": false, + "lines": 55, + "author": "ta-brook" + }, + { + "id": "message-spam", + "folder": "Message-Spam", + "name": "WhatsApp Random Message Sender", + "blurb": "No — it uses pyautogui to control the keyboard and opens WhatsApp Web in a desktop browser.", + "emoji": "💬", + "runnable": false, + "hasPlayground": false, + "lines": 30, + "author": "Toheed07" + }, + { + "id": "windows-logo", + "folder": "Windows Logo", + "name": "Windows Logo", + "blurb": "Draws the classic Windows logo using Python's turtle graphics on a black background.", + "emoji": "🎨", + "runnable": false, + "hasPlayground": false, + "lines": 26, + "author": "its-100rabh" + }, + { + "id": "wine-quality-predictor", + "folder": "Wine_quality_predictor", + "name": "Wine quality Predictor", + "blurb": "During production of wine there may occur an reduction in quality due to some error in production process. Since wine is produced in large quantiti…", + "emoji": "🔮", + "runnable": false, + "hasPlayground": false, + "lines": 82, + "author": "rik-chatterjee" + }, + { + "id": "word-predictor", + "folder": "Word_Predictor", + "name": "Word Predictor", + "blurb": "A next-word prediction tool that learns from an exported WhatsApp chat. It reads a Chats.txt file, builds word-frequency and next-word tables, and…", + "emoji": "💬", + "runnable": false, + "hasPlayground": false, + "lines": 103, + "author": "nik-6174" + }, + { + "id": "worksheet-to-text", + "folder": "Worksheet_to_text", + "name": "Worksheet to Text", + "blurb": "Reads an Excel workbook and writes each column of data into its own plain-text file (text-1.txt, text-2.txt, ...).", + "emoji": "📊", + "runnable": false, + "hasPlayground": false, + "lines": 27, + "author": "ibra-kdbra" + }, + { + "id": "world-cup-player-comparison", + "folder": "World-Cup-Player-Comparison", + "name": "World Cup Player Comparison", + "blurb": "Scrapes 2022 World Cup team statistics from fbref.com, lets you pick two players from two countries, and saves a bar chart comparing their combined…", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 161, + "author": "MattBlodgettProjects" + }, + { + "id": "comics-scraper", + "folder": "Comics_Scraper", + "name": "XKCD Comics Scraper", + "blurb": "Downloads comic images from xkcd.com — either every comic in bulk, or a single comic by number — saving them to a local xkcd folder.", + "emoji": "🕷️", + "runnable": false, + "hasPlayground": false, + "lines": 147, + "author": "ibra-kdbra" + }, + { + "id": "xls-to-xlsx", + "folder": "xls_to_xlsx", + "name": "XLS TO XLSX", + "blurb": "![View My Profile](https://github.com/odhyp)", + "emoji": "🐍", + "runnable": false, + "hasPlayground": false, + "lines": 52, + "author": "odhyp" + }, + { + "id": "yt", + "folder": "YouTube Video Downloader", + "name": "YouTube Video Downloader", + "blurb": "A console tool that downloads a YouTube video using yt-dlp. It lists the available formats and lets you pick one or auto-selects the highest resolu…", + "emoji": "⬇", + "runnable": false, + "hasPlayground": false, + "lines": 39, + "author": "vk0812" + } +] diff --git a/web/src/lib/catalog.ts b/web/src/lib/catalog.ts new file mode 100644 index 000000000..6d48addea --- /dev/null +++ b/web/src/lib/catalog.ts @@ -0,0 +1,23 @@ +// The full project catalog — every folder under projects/. +// catalog.json is generated by scripts/gen-catalog.mjs (`npm run catalog`). + +import type { CatalogProject } from "@/types"; +import { REPO_URL } from "@/lib/data"; +import catalog from "./catalog.json"; + +export const CATALOG: CatalogProject[] = catalog as CatalogProject[]; + +export const RUNNABLE = CATALOG.filter((p) => p.runnable); + +export function getCatalogProject(id: string): CatalogProject | undefined { + return CATALOG.find((p) => p.id === id); +} + +/** GitHub URL of a project's source folder in the repo. */ +export function catalogFolderUrl(p: CatalogProject): string { + const path = p.folder + .split("/") + .map((seg) => encodeURIComponent(seg)) + .join("/"); + return `${REPO_URL}/tree/main/projects/${path}`; +} diff --git a/web/src/lib/data.ts b/web/src/lib/data.ts index d55b07b5d..f72d8249b 100644 --- a/web/src/lib/data.ts +++ b/web/src/lib/data.ts @@ -17,17 +17,17 @@ import type { // `repoPath` is that project's folder path under projects/ in the repo. export const PROJECTS: Project[] = [ { id: "snake", name: "Snake Game", cat: "games", blurb: "Slither, eat, grow longer. The classic.", lines: 92, deps: "pygame", emoji: "🐍", author: "EbrG786", repoPath: "Snake Game" }, - { id: "tictactoe", name: "Tic-Tac-Toe", cat: "games", blurb: "Three in a row, terminal showdown.", lines: 64, deps: "stdlib", emoji: "⭕", author: "Mrinank-Bhowmick", repoPath: "Tic-Tac-Toe", runnable: true, playground: true }, - { id: "hangman", name: "Hangman", cat: "games", blurb: "Guess letters before the gallows complete.", lines: 78, deps: "stdlib", emoji: "🪢", author: "Mrinank-Bhowmick", repoPath: "Hangman", runnable: true, playground: true }, + { id: "tictactoe", name: "Tic-Tac-Toe", cat: "games", blurb: "Three in a row, terminal showdown.", lines: 64, deps: "stdlib", emoji: "⭕", author: "Mrinank-Bhowmick", repoPath: "Tic-Tac-Toe", playground: true }, + { id: "hangman", name: "Hangman", cat: "games", blurb: "Guess letters before the gallows complete.", lines: 78, deps: "stdlib", emoji: "🪢", author: "Mrinank-Bhowmick", repoPath: "Hangman", playground: true }, { id: "flappy", name: "Flappy Bird", cat: "games", blurb: "Tap to flap. Avoid the pipes. Die. Repeat.", lines: 140, deps: "pygame", emoji: "🐦", author: "Nikita0509", repoPath: "Flappybird_game" }, - { id: "rps", name: "Rock · Paper · Scissors", cat: "games", blurb: "Best of three vs the random module.", lines: 48, deps: "stdlib", emoji: "✊", author: "ZackeryRSmith", repoPath: "Rock_Paper_Scissors", runnable: true, playground: true }, + { id: "rps", name: "Rock · Paper · Scissors", cat: "games", blurb: "Best of three vs the random module.", lines: 48, deps: "stdlib", emoji: "✊", author: "ZackeryRSmith", repoPath: "Rock_Paper_Scissors", playground: true }, { id: "calc", name: "Calculator", cat: "tools", blurb: "Tkinter buttons. Operator precedence. Beep.", lines: 110, deps: "tkinter", emoji: "🧮", author: "shubham7668", repoPath: "Calculator" }, - { id: "bmi", name: "BMI Calculator", cat: "tools", blurb: "Height + weight → a single questionable number.", lines: 36, deps: "stdlib", emoji: "⚖️", author: "Mrinank-Bhowmick", repoPath: "BMI_calculator", runnable: true, playground: true }, + { id: "bmi", name: "BMI Calculator", cat: "tools", blurb: "Height + weight → a single questionable number.", lines: 36, deps: "stdlib", emoji: "⚖️", author: "Mrinank-Bhowmick", repoPath: "BMI_calculator", playground: true }, { id: "weather", name: "Weather App", cat: "web", blurb: "OpenWeather API, your city, today.", lines: 88, deps: "requests", emoji: "☁️", author: "srujan-landeri", repoPath: "API Based Weather Report" }, - { id: "qr", name: "QR Code Generator", cat: "tools", blurb: "Text in, scannable square out.", lines: 22, deps: "qrcode", emoji: "▦", author: "SubramanyaKS", repoPath: "QRCode-Generator", runnable: true, playground: true }, + { id: "qr", name: "QR Code Generator", cat: "tools", blurb: "Text in, scannable square out.", lines: 22, deps: "qrcode", emoji: "▦", author: "SubramanyaKS", repoPath: "QRCode-Generator", playground: true }, { id: "pwd", name: "Password Generator", cat: "tools", blurb: "Random, strong, immediately forgotten.", lines: 30, deps: "secrets", emoji: "🔐", author: "jrbublitz", repoPath: "Password Projects/Password Generator", playground: true }, { id: "yt", name: "YouTube Downloader", cat: "web", blurb: "pytube wrapper. Save the lecture.", lines: 54, deps: "pytube", emoji: "⬇", author: "vk0812", repoPath: "YouTube Video Downloader" }, - { id: "madlibs", name: "Madlibs Generator", cat: "fun", blurb: "Fill in nouns. Receive nonsense. Laugh.", lines: 40, deps: "stdlib", emoji: "✏️", author: "ZackeryRSmith", repoPath: "Madlibs Generator", runnable: true, playground: true }, + { id: "madlibs", name: "Madlibs Generator", cat: "fun", blurb: "Fill in nouns. Receive nonsense. Laugh.", lines: 40, deps: "stdlib", emoji: "✏️", author: "ZackeryRSmith", repoPath: "Madlibs Generator", playground: true }, ]; export const CATEGORIES: Category[] = [ diff --git a/web/src/types.ts b/web/src/types.ts index 672b17699..949113eae 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -14,8 +14,6 @@ export interface Project { author: string; /** Folder path under projects/ in the repo (for the source link). */ repoPath: string; - /** Has a server-backed "Try it" demo via the Cloudflare Worker. */ - runnable?: boolean; /** Can run fully in-browser via Pyodide (pure-stdlib console programs). */ playground?: boolean; } @@ -26,6 +24,30 @@ export interface Category { count: number; } +/** + * One entry in the full project catalog (every folder under projects/). + * Generated by scripts/gen-catalog.mjs into src/lib/catalog.json. + */ +export interface CatalogProject { + /** URL slug — unique across the catalog. */ + id: string; + /** Folder path under projects/ in the repo. */ + folder: string; + /** Display name (from the project's README title). */ + name: string; + /** One-line description (from the README). */ + blurb: string; + emoji: string; + /** Can run in the in-browser Pyodide playground. */ + runnable: boolean; + /** A playground source file exists for this project. */ + hasPlayground: boolean; + /** Line count of the project's main .py file. */ + lines: number; + /** GitHub handle of the contributor who first added this project. */ + author?: string; +} + export interface Contributor { handle: string; name: string; diff --git a/wrangler.jsonc b/wrangler.jsonc deleted file mode 100644 index 3b202d8be..000000000 --- a/wrangler.jsonc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "node_modules/wrangler/config-schema.json", - "name": "pybegin-api", - "main": "src/worker.py", - "compatibility_date": "2024-09-23", - "compatibility_flags": ["python_workers"], - "routes": [ - { "pattern": "pybegin.its-mrinank.com", "custom_domain": true } - ], - "observability": { "enabled": true } -} From 65813ef443bd4891aa0ddba78967e94241053ed9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 18:57:46 +0000 Subject: [PATCH 05/14] contrib-readme-action has updated readme --- README.md | 468 +++++++++++++++++++++++++----------------------------- 1 file changed, 220 insertions(+), 248 deletions(-) diff --git a/README.md b/README.md index 09c9967a7..bf9645689 100644 --- a/README.md +++ b/README.md @@ -825,8 +825,8 @@ - - PythonicBoat + + itsyashvardhan
Yashvardhan Singh
@@ -918,10 +918,10 @@ - - Tyler-Pear + + tylerapear
- Tyler-Pear + Tyler Pearson
@@ -1119,32 +1119,32 @@ - - Xia3412 + + Harry830
- Xia + Harry830
- - VykSI + + ayan-joshi
- Vishal S Murali + Ayan Joshi
- - sb-decoder + + Albinary
- Sowham Bhuin + Albert
- - Sameer-choudhary-git + + Blazier07
- Sameer Choudhary + Blazier
@@ -1155,34 +1155,41 @@ - - MrB141107 + + Sameer-choudhary-git
- Mr.B + Sameer Choudhary
- - Albinary + + sb-decoder
- Albert + Sowham Bhuin
- - ayan-joshi + + VykSI
- Ayan Joshi + Vishal S Murali
- - Harry830 + + Xia3412
- Harry830 + Xia
+ + + amunjuluri +
+ Anand Munjuluri +
+ vk0812 @@ -1217,15 +1224,15 @@
Hina Ota
- + + parth-verma7
Parth Verma
- - + sirKiraUzumaki @@ -1260,27 +1267,13 @@
Chirag Aggarwal
- - - - rahul0x00 -
- Rahul Kumar -
- - kom-senapati + + k0msenapati
- Kom Senapati -
- - - - Subha-5 -
- Subha Sadhu + K Om Senapati
@@ -1298,10 +1291,17 @@ - - Ratna2 + + Subha-5
- Ratnadeep Bhowmik + Subha Sadhu +
+ + + + rahul0x00 +
+ Rahul Kumar
@@ -1312,20 +1312,6 @@ - - - advik-student-dev -
- Advik Sharma -
- - - - tamojeetK -
- Ahaan -
- Alex108-lab @@ -1334,10 +1320,10 @@ - - amunjuluri + + jpyces
- Anand Munjuluri + Advik Sharma
@@ -1353,8 +1339,7 @@
Soonam Kalyan Panda - - + Logadheep @@ -1362,6 +1347,14 @@ Logadheep + + + tamojeetK +
+ Ahaan +
+ + MattBlodgettProjects @@ -1370,17 +1363,24 @@ - - rajdeepdas2000 + + vrup0408
- Rajdeep Das + VRUSHANG PARIKH
- - Preeray + + its-100rabh
- Preeray + Saurabh Mahapatra +
+ + + + Santhoshnov +
+ Santhosh S
@@ -1391,13 +1391,20 @@ - - Santhoshnov + + rajdeepdas2000
- Santhosh S + Rajdeep Das
+ + + Preeray +
+ Preeray +
+ Nikita0509 @@ -1420,24 +1427,17 @@ - - its-100rabh -
- Saurabh Mahapatra -
- - - - vrup0408 + + Sayakb03
- VRUSHANG PARIKH + Sayakb03
- - Sayakb03 + + iamshreeram
- Sayakb03 + Shreeram
@@ -1448,13 +1448,6 @@ Samuel Peters - - - iamshreeram -
- Shreeram -
- saanvibk @@ -1476,6 +1469,13 @@ Syeda Nowshin Ibnat + + + Toheed07 +
+ Toheed +
+ RishavRaj20 @@ -1484,13 +1484,6 @@ - - - Toheed07 -
- Toheed -
- Utkarsh-Raj20 @@ -1525,20 +1518,20 @@
Hritik Bhattacharya
- - + maglionaire
Maglionaire
- + + - - rakinplaban + + lonelyH3b
- Rakin Shahriar Plaban + らきん
@@ -1562,14 +1555,28 @@ Yest3a + + + rudy3333 +
+ Rudy3 +
+ + + + kkchx +
+ Kkchx +
+ + Guillotine189
Sarthak Singhal
- - + newtoallofthis123 @@ -1584,13 +1591,6 @@ Josh - - - Rathish-Rajendran -
- Rathish R -
- siddharth9300 @@ -1599,17 +1599,17 @@ - - kkchx + + Rathish-Rajendran
- Kkchx + Rathish R
- - rudy3333 + + Ashani-Sansala
- Rudy3 + Ashani Sansala Kodithuwakku
@@ -1621,17 +1621,24 @@ - - amoghakancharla + + cj-praveen
- Amogha Kancharla + CJ Praveen
- - cj-praveen + + akghosh111
- CJ Praveen + Anukiran Ghosh +
+ + + + amoghakancharla +
+ Amogha Kancharla
@@ -1647,22 +1654,8 @@
Ankit_Anmol - - - - Ashani-Sansala -
- Ashani Sansala Kodithuwakku -
- - - akghosh111 -
- Anukiran Ghosh -
- Chloe23077 @@ -1697,14 +1690,21 @@
GigabyteZ
- - + TimTemi
Flourish
+ + + + + Ratna2 +
+ Ratnadeep Bhowmik +
@@ -1735,10 +1735,10 @@ - - rust-master + + mzaryabrafique
- Rust Master + Muhammad Zaryab Rafique
@@ -1778,8 +1778,8 @@ - - WhoIsJayD + + whoisjayd
Jaydeep
@@ -1799,13 +1799,6 @@ Harsh Mahadev Duche - - - g0v1ndN -
- Govind S Nair -
- Vivek-GuptaXCode @@ -1826,15 +1819,15 @@
Ylavish64
- - + yogesh78026
Yogeshwar Kumar
- + + ahmedalhamad7 @@ -1869,15 +1862,15 @@
Prajwal Benedict A
- - + chimerson
Emmanuel Ogu
- + + Vishvam10 @@ -1912,15 +1905,15 @@
Sumit Baroniya
- - + - - SulimanSagindykov + + sagindykovsl
Suliman Sagindykov
- + + sudhanshu-77 @@ -1936,8 +1929,8 @@ - - CapedDemon + + spyke7
Shreejan Dolai
@@ -1950,20 +1943,20 @@ - - JustProgramJesus + + MamaevSergey
- JustProgramJesus + Мамаев Сергей
- - + sdthinlay
Sdthinlay
- + + samayita1606 @@ -1971,13 +1964,6 @@ Samayita Kali - - - Polqt -
- Pol Hidalgo -
- NishantPacharne @@ -1998,8 +1984,7 @@
Tanawat Jirawttanakul
- - + rathoreshreya @@ -2013,7 +1998,8 @@
Shayaanyar
- + + samualmartin @@ -2041,8 +2027,7 @@
Prathamesh Phalke
- - + Prabinshrestha737 @@ -2056,7 +2041,8 @@
Abhishek Ghimire
- + + paartheee @@ -2084,8 +2070,7 @@
Muchamad Yuda Tri Ananda
- - + mr-desilva @@ -2099,7 +2084,8 @@
Zainab Ibraheem
- + + 0silverback0 @@ -2121,14 +2107,6 @@ Kodingkin - - - kish7105 -
- Kish -
- - jonascarvalh @@ -2149,7 +2127,8 @@
Faiz Aslam
- + + darshbaxi @@ -2170,8 +2149,7 @@
D-coder111
- - + HimanshuSinghNegi @@ -2192,7 +2170,8 @@
Gargi
- + + emanalytic @@ -2213,8 +2192,7 @@
Dishant Nagpal
- - + Dhruvil-Lakhtaria @@ -2235,7 +2213,8 @@
David Ioffe
- + + Crack-er-jack @@ -2256,8 +2235,7 @@
Bhanushri Chinta
- - + ayushi-ras @@ -2278,7 +2256,8 @@
Sudharsan Vanamali
- + + hiarijit @@ -2299,8 +2278,7 @@
Anju Chhetri
- - + anishaxtha @@ -2321,7 +2299,8 @@
Aleena
- + + AdityaSahai123 @@ -2342,8 +2321,7 @@
Abhinandan Singla
- - + abc-is-here @@ -2358,20 +2336,14 @@ ARYAN GULATI - - - contra156 -
- Contra -
- Surya-29
Surya Narayan
- + + RishiPastor05 @@ -2385,8 +2357,7 @@
Ved Sadh
- - + Ramisky @@ -2414,7 +2385,8 @@
Praveen SV
- + + Pratham-H-S @@ -2428,8 +2400,7 @@
Pranav Dasan
- - + Prajwol-Shrestha @@ -2445,8 +2416,8 @@ - - NooBIE-Nilay + + nilay-banerjee
Nilay Banerjee
@@ -2457,7 +2428,8 @@
Nischay Goyal - + + nik-6174 @@ -2471,8 +2443,7 @@
Nesh
- - + jusinamine @@ -2500,7 +2471,8 @@
MANISH KUMAR CHINTHA
- + + LightxAman @@ -2514,8 +2486,7 @@
Kunal Pitale
- - + Krishna13515 @@ -2523,13 +2494,6 @@ Krishna13515 - - - Jyothi-Dk -
- Jyothika Dileepkumar -
- Josephtobi @@ -2551,5 +2515,13 @@ Jenil Gajjar + + + + Polqt +
+ Pol Hidalgo +
+ From a0b8b72152cc5350ae68bfb3f6b5a54c63f5a30d Mon Sep 17 00:00:00 2001 From: Mrinank-Bhowmick <77621953+Mrinank-Bhowmick@users.noreply.github.com> Date: Fri, 22 May 2026 00:38:55 +0530 Subject: [PATCH 06/14] README update --- README.md | 803 +++--------------------------------------------------- 1 file changed, 34 insertions(+), 769 deletions(-) diff --git a/README.md b/README.md index 09c9967a7..694127f49 100644 --- a/README.md +++ b/README.md @@ -1,796 +1,61 @@

- - + + pyBegin — Python Beginner Projects

-GitHub stars -GitHub forks -GitHub license -Awesome + 270+ bite-sized Python projects to learn by building — each one a weekend's worth of fun.

+

+ Live site + Stars + Forks + License + PRs welcome +

-# 💭 **Why these awesome projects ??** -💠 _This repository offers a variety of fascinating mini-projects written in Python._ - -💠 _Working on Python projects will undoubtedly improve your skills and raise your profile in preparation for the globalised marketplace outside._ - -💠 _Projects are a potential method to begin your career in this area._ - -💠 _This language deserves a lot of attention in today's world, and why not since it can address so many real-world problems?_ - - -

📈 Tweet this repository

- - InfiniteGraph Logo - -## 📁 All Projects are under [projects](https://github.com/Mrinank-Bhowmick/python-beginner-projects/tree/main/projects) folder. +--- -

🖥️ Projects So Far ...

+## 🚀 Try it live — no install needed - - +**[pybegin.pages.dev](https://pybegin.pages.dev)** is a searchable gallery of every project in this repo — and **60+ of them run right in your browser**. - +The playground runs real CPython in the browser via [Pyodide](https://pyodide.org): a live code editor, an interactive console for `input()`, and an annotated, beginner-friendly walkthrough of each project. Pick one, hit **▶ Run code**, and start tinkering — nothing to install. - +> Browse all projects → **[pybegin.pages.dev](https://pybegin.pages.dev)** - +## ✨ Why this repo - +- **270+ projects** — from one-liners to small apps: games, tools, scrapers, GUIs, ML notebooks, and more. +- **Beginner-first.** Every project is small, self-contained, and an ideal first pull request. +- **Learn by reading _and_ running.** The in-browser playground lets you read real code and change it instantly. +- **A genuine open-source on-ramp.** Hundreds of people made their first contribution here. - - +## 🏃 Run a project locally - +```bash +git clone https://github.com/Mrinank-Bhowmick/python-beginner-projects.git +cd python-beginner-projects/projects/ +pip install -r requirements.txt # only if the project ships one +python main.py +``` - +Every project lives in its own folder under [`projects/`](./projects), each with a `README.md` explaining what it does and how to run it. - +## 🤝 Contributing - +New projects and improvements are very welcome — this is a friendly place for your first open-source contribution. - - - - - +1. Read [`CONTRIBUTING.md`](./CONTRIBUTING.md) and the [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md). +2. Add your project as a new folder in [`projects/`](./projects) with its own `README.md`. +3. Open a pull request. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Audiobook - - - - Desktop-Notification - - - - Dice-Roll-Simulator - - - - Madlibs-Generator - -
- - Make-API - - - - Medium Article Reader - - - - Quiz Game - - - - Rock-Paper-Scissors - -
- - Send-Email - - - - Tesla - - - - MorseCode Translator - - - - Weather - -
- - ProjectEuler - - - - AudioRecorder - - - - Hangman - - - - VoiceToText - -
- - TextTranslate - - - - Own_browser - - - - HandCricket - - - - English Thesaurus - -
- - BMI_calculator - - - - Higher-Lower - - - - IPv4_Calculator - - - - KbdXylo - -
- - NASA-APOD - - - - Neurons - - - - Qt5_YouTube - - - - Tic-Tac-Toe - -
- - TurtlePattern - - - - Duplicate-search - - - - Snake-Water-Gun - - - - AES256 - -
- - API Based Weather Report - - - - Audio Converter - - - - BFS visualizer - - - - BMI WebApp - -
- - Bigram_Autocomplete - - - - Bitcoin Mining - - - - BlackJack - - - - Calculator - -
- - Calendar - - - - Captcha_Genrator - - - - Card Game - - - - Connect Four - -
- - Countdown - - - - Dice Simulator - - - - DnD Dice - - - - Drowsiness Detector - -
- - Eye Blink Detection - - - - Fidget Spinner Game - - - - Find_imbd_rating - - - - Flappybird_game - -
- - Full_Calendar - - - - Guess Number - - - - HandCricket - - - - HandTrack - -
- - Hangman - - - - Higher-Lower - - - - Image compressor - - - - Image-to-art - -
- - Internet-speed-test - - - - Inverse Matrix Calculator - - - - JARVIS.PY - - - - Jokenpo - -
- - Jokey - - - - Language-Translate - - - - Location Search App (GUI) - - - - Madlibs Generator - -
- - Mastermind - - - - Minecraft-in-Python-main - - - - Mobile Document Scanner - - - - Mongo CRUD - -
- - Movie recommendation - - - - Number Guessing App - - - - OTP-Verfication-System - - - - OpenCV_color_detect_in_live_feed - -
- - Movie recommendation - - - - Otp_Generator - - - - PDF_Reader - - - - PONG - -
- - Password Projects - - - - Port_Scaner - - - - Print_Colored_Text - - - - ProjectEuler - -
- - Python story generator - - - - QRCode Scanner - - - - QRCode-Generator - - - - QuickWordCloud - -
- - RestrauntAPI - - - - Roll_A_dice - - - - Scientific-Calculator - - - - ScreenRecorder - -
- - Seek_with_hand_track - - - - Slice-Audio - - - - Snake Game - - - - Space Shooter - -
- - Split_Tip - - - - Stock-Market-Dashboard - - - - Space Shooter - - - - Subtitle_synchronizer.py - -
- - Sudoku-Solver - - - - Sudoku_solver - - - - Subtitle_synchronizer.py - - - - Text Editor - -
- - Text to Speech - - - - TextDetection - - - - Timer - - - - Tkinter - -
- - Turtle Pattern - - - - Turtle_Graphics - - - - Twitter-Bot - - - - Watermarker - -
- - Web Scraping Jujustu Kaisen Manga - - - - Website Blocker - - - - YouTube Video Downloader - - - - caesar_cipher - -
- - currency converter - - - - facebook_video_downloader - - - - facerecoginition - - - - game-snake_water_gun - -
- - healthmanagementsystem - - - - maths - - - - minesweeper - - - - pdf_to_text - -
- - ping_pong - - - - proxy-scrapper - - - - reciept generator - - - - scheduledShutdown - -
- - scrap-ycombinator - - - - takeImage - - - - web-crawler(movie extract) - - - - what-for-dinner - -
- - Calculate Age - -
- - -## Run these projects online : - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Mrinank-Bhowmick/python-beginner-projects.git) - - -## Stargazers over time +## ⭐ Stargazers over time [![Stargazers over time](https://starchart.cc/Mrinank-Bhowmick/python-beginner-projects.svg)](https://starchart.cc/Mrinank-Bhowmick/python-beginner-projects) - -

Contributors

From c2168da5e7cf4d1934ce295e041b34aa1608a4d0 Mon Sep 17 00:00:00 2001 From: Mrinank-Bhowmick <77621953+Mrinank-Bhowmick@users.noreply.github.com> Date: Fri, 22 May 2026 01:07:11 +0530 Subject: [PATCH 07/14] web: contributors page, real avatars, bookmark redesign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New /contributors page listing all 240 contributors (avatar, name, GitHub profile link), generated from the README contributors block by scripts/gen-contributors.mjs. - Replace initial-letter avatars with real GitHub avatars on the homepage. Use direct avatars.githubusercontent.com URLs so they satisfy the site's COEP: require-corp header — github.com/.png 302-redirects and the redirect is blocked under COEP. - Expand the homepage contributor wall from 12 to 30 cards; cards link out to GitHub profiles. - Redesign the nav bookmark control as a labelled toggle button (lucide icon + count) with a clear active state, replacing the bare circle. Co-Authored-By: Claude Opus 4.7 --- web/scripts/gen-contributors.mjs | 41 + web/src/app/contributors/page.tsx | 62 ++ web/src/app/globals.css | 77 +- web/src/components/Home.tsx | 74 +- web/src/lib/contributors.json | 1202 +++++++++++++++++++++++++++++ web/src/lib/data.ts | 2 +- web/src/types.ts | 7 + 7 files changed, 1439 insertions(+), 26 deletions(-) create mode 100644 web/scripts/gen-contributors.mjs create mode 100644 web/src/app/contributors/page.tsx create mode 100644 web/src/lib/contributors.json diff --git a/web/scripts/gen-contributors.mjs b/web/scripts/gen-contributors.mjs new file mode 100644 index 000000000..b529abae4 --- /dev/null +++ b/web/scripts/gen-contributors.mjs @@ -0,0 +1,41 @@ +// Generates src/lib/contributors.json — every contributor, scraped from the +// contrib-readme-action block in the repo's README.md (handle, display name, +// GitHub avatar URL). +// +// Run from the web/ directory: node scripts/gen-contributors.mjs +// Re-run whenever the README's contributors list changes. + +import fs from "fs"; +import path from "path"; + +const WEB = process.cwd(); +const README = path.join(WEB, "..", "README.md"); +const OUT = path.join(WEB, "src", "lib", "contributors.json"); + +const BOTS = new Set(["lint-action", "github-actions", "dependabot"]); + +const md = fs.readFileSync(README, "utf8"); +const start = md.indexOf(""); +const end = md.indexOf(""); +if (start < 0 || end < 0) { + throw new Error("contributors block not found in README.md"); +} +const block = md.slice(start, end); + +// Each entry:
NAME +const re = + /
\s*]*\/>\s*\s*([^<]+)<\/b><\/sub>/g; + +const seen = new Set(); +const contributors = []; +let m; +while ((m = re.exec(block))) { + const handle = m[1].trim(); + const key = handle.toLowerCase(); + if (seen.has(key) || BOTS.has(key) || key.endsWith("[bot]")) continue; + seen.add(key); + contributors.push({ handle, name: m[3].trim(), avatar: m[2].trim() }); +} + +fs.writeFileSync(OUT, JSON.stringify(contributors, null, 2) + "\n"); +console.log(`contributors.json: ${contributors.length} contributors`); diff --git a/web/src/app/contributors/page.tsx b/web/src/app/contributors/page.tsx new file mode 100644 index 000000000..35bf82fe2 --- /dev/null +++ b/web/src/app/contributors/page.tsx @@ -0,0 +1,62 @@ +import type { Metadata } from "next"; +import SiteNav from "@/components/SiteNav"; +import SiteFooter from "@/components/SiteFooter"; +import { REPO_URL } from "@/lib/data"; +import type { RepoContributor } from "@/types"; +import contributors from "@/lib/contributors.json"; + +// The full contributor roll, scraped from the README by scripts/gen-contributors.mjs. +const CONTRIBUTORS = contributors as RepoContributor[]; + +export const metadata: Metadata = { + title: "Contributors — pyBegin", + description: `The ${CONTRIBUTORS.length} people who built the pyBegin beginner-project collection.`, +}; + +export default function ContributorsPage() { + return ( + + ); +} diff --git a/web/src/app/globals.css b/web/src/app/globals.css index cff8c2680..4fa13c3b7 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -82,17 +82,24 @@ a { color: inherit; } .s-navlinks a.active, .s-navlinks button.active { background: var(--s-ink); color: var(--s-bg); } .s-navlinks a:hover:not(.active), .s-navlinks button:hover:not(.active) { background: rgba(29,24,48, 0.08); } .s-nav-right { display: flex; gap: 10px; align-items: center; } -.s-bm-count { - width: 38px; height: 38px; border-radius: 50%; +.s-bm-btn { + display: inline-flex; align-items: center; gap: 6px; + height: 38px; padding: 0 14px; border-radius: var(--s-radius-pill); background: var(--s-surface); border: var(--s-border-thin); - display: grid; place-items: center; font-weight: 700; font-size: 14px; - box-shadow: 3px 3px 0 var(--s-ink); position: relative; cursor: pointer; -} -.s-bm-count.has::after { - content: ''; position: absolute; top: 4px; right: 4px; - width: 8px; height: 8px; border-radius: 50%; background: var(--s-accent); - border: 1.5px solid var(--s-ink); -} + box-shadow: 3px 3px 0 var(--s-ink); + font-weight: 800; font-size: 13px; color: var(--s-ink); cursor: pointer; + transition: transform .1s, box-shadow .1s, background .12s, color .12s; +} +.s-bm-btn:hover { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 var(--s-ink); } +.s-bm-btn:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--s-ink); } +.s-bm-btn.active { background: var(--s-ink); color: var(--s-bg); } +.s-bm-n { font-family: var(--s-mono); font-size: 12.5px; font-weight: 800; } +.s-bm-btn.active .s-bm-n { color: var(--s-accent); } +.s-bm-word { + font-size: 11px; font-weight: 800; letter-spacing: 0.04em; + text-transform: uppercase; +} +@media (max-width: 520px) { .s-bm-word { display: none; } } .s-cta { padding: 12px 22px; border-radius: var(--s-radius-pill); background: var(--s-ink); color: var(--s-bg); @@ -390,6 +397,56 @@ a { color: inherit; } background: var(--s-ink); color: var(--s-bg); font-size: 11px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; border-radius: var(--s-radius-pill); } +/* real GitHub avatars (replace the initials) */ +img.s-avatar { display: block; object-fit: cover; padding: 0; } +a.s-contrib { display: block; text-decoration: none; color: inherit; } + +/* ---- /contributors — the full crew page ---- */ +.s-crew-head { margin: 40px 0 28px; max-width: 720px; } +.s-crew-title { + font-family: var(--s-display); font-weight: 900; + font-size: clamp(34px, 5vw, 56px); letter-spacing: -0.03em; line-height: 1; +} +.s-crew-title em { + font-style: normal; color: var(--s-accent); + -webkit-text-stroke: 1.5px var(--s-ink); +} +.s-crew-sub { + margin-top: 14px; font-size: 16px; line-height: 1.55; + color: rgba(29, 24, 48, 0.7); +} +.s-crew-sub a { color: var(--s-accent); font-weight: 700; } +.s-crew-sub a:hover { text-decoration: underline; } +.s-crew-grid { + display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 14px; margin-bottom: 56px; +} +.s-crew-card { + display: flex; flex-direction: column; align-items: center; text-align: center; + padding: 20px 12px; border-radius: 16px; + background: var(--s-surface); border: var(--s-border-thin); + box-shadow: 4px 4px 0 var(--s-ink); + text-decoration: none; color: inherit; + transition: transform .15s, box-shadow .15s; +} +.s-crew-card:hover { + transform: translate(-2px, -2px); box-shadow: 6px 6px 0 var(--s-accent); +} +.s-crew-avatar { + width: 66px; height: 66px; border-radius: 50%; object-fit: cover; + border: var(--s-border-thin); background: var(--s-bg); +} +.s-crew-name { + margin-top: 12px; font-weight: 800; font-size: 14px; line-height: 1.25; + overflow-wrap: anywhere; +} +.s-crew-handle { + margin-top: 3px; font-family: var(--s-mono); font-size: 11px; + color: rgba(29, 24, 48, 0.55); overflow-wrap: anywhere; +} +@media (max-width: 640px) { + .s-crew-grid { grid-template-columns: repeat(auto-fill, minmax(116px, 1fr)); } +} .s-more { display: flex; justify-content: center; margin-top: 28px; } .s-more button, .s-more a { diff --git a/web/src/components/Home.tsx b/web/src/components/Home.tsx index abf955a5e..a2d2b2037 100644 --- a/web/src/components/Home.tsx +++ b/web/src/components/Home.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; +import { Bookmark } from "lucide-react"; import StickerLogo from "./StickerLogo"; import ScribbleSvg from "./ScribbleSvg"; import HeroStickers from "./HeroStickers"; @@ -13,7 +14,21 @@ import ProjectModal from "./ProjectModal"; import SiteFooter from "./SiteFooter"; import { CATEGORIES, CONTRIBUTORS, PATHS, PROJECTS, REPO_URL, getProject } from "@/lib/data"; import { getBookmarks, toggleBookmark } from "@/lib/bookmarks"; -import type { Project } from "@/types"; +import contributors from "@/lib/contributors.json"; +import type { Project, RepoContributor } from "@/types"; + +const CREW = contributors as RepoContributor[]; + +// Resolve a handle to its GitHub avatar. Uses the direct avatars.github URL +// (not github.com/.png — that 302-redirects and the redirect is +// blocked by the site's COEP: require-corp header). +const AVATARS = new Map( + contributors.map((c) => [c.handle.toLowerCase(), c.avatar]), +); +const avatarFor = (handle: string, size: number) => { + const url = AVATARS.get(handle.toLowerCase()); + return url ? `${url}&s=${size}` : `https://github.com/${handle}.png`; +}; const ACCENT = "#ff7a59"; const SORTS = ["default", "alpha", "short", "bookmarks"] as const; @@ -81,16 +96,29 @@ export default function Home() {
-
{ setShowBmOnly((v) => !v); scrollTo("gallery"); }} > - ★{bm.length} -
+
-
{CONTRIBUTORS[0].name[0]}
+
{CONTRIBUTORS[0].name}
@{CONTRIBUTORS[0].handle}

@@ -315,9 +349,22 @@ export default function Home() {

Maintainer · 412 commits
- {CONTRIBUTORS.slice(1, 13).map((c) => ( - + ))}
- - See all 241 contributors → - + See all contributors →
diff --git a/web/src/lib/contributors.json b/web/src/lib/contributors.json new file mode 100644 index 000000000..8964a278b --- /dev/null +++ b/web/src/lib/contributors.json @@ -0,0 +1,1202 @@ +[ + { + "handle": "Mrinank-Bhowmick", + "name": "Mrinank Bhowmick", + "avatar": "https://avatars.githubusercontent.com/u/77621953?v=4" + }, + { + "handle": "ibra-kdbra", + "name": "Ibra-kdbra", + "avatar": "https://avatars.githubusercontent.com/u/135609275?v=4" + }, + { + "handle": "Alby084", + "name": "Alby084", + "avatar": "https://avatars.githubusercontent.com/u/99786431?v=4" + }, + { + "handle": "itsyashvardhan", + "name": "Yashvardhan Singh", + "avatar": "https://avatars.githubusercontent.com/u/68675629?v=4" + }, + { + "handle": "ca20110820", + "name": "Cedric Anover", + "avatar": "https://avatars.githubusercontent.com/u/140507928?v=4" + }, + { + "handle": "kanchanraiii", + "name": "Kanchan Rai", + "avatar": "https://avatars.githubusercontent.com/u/114416916?v=4" + }, + { + "handle": "TERNION-1121", + "name": "Vikrant Singh Bhadouriya", + "avatar": "https://avatars.githubusercontent.com/u/97667653?v=4" + }, + { + "handle": "Guyunjeong", + "name": "Chloe", + "avatar": "https://avatars.githubusercontent.com/u/116140212?v=4" + }, + { + "handle": "rik-chatterjee", + "name": "Rik Chatterjee", + "avatar": "https://avatars.githubusercontent.com/u/82376158?v=4" + }, + { + "handle": "yanliutafewa", + "name": "Yanliutafewa", + "avatar": "https://avatars.githubusercontent.com/u/141901817?v=4" + }, + { + "handle": "Tark-pea", + "name": "Tark-pea", + "avatar": "https://avatars.githubusercontent.com/u/119817620?v=4" + }, + { + "handle": "omkarxpatel", + "name": "Omkar Patel", + "avatar": "https://avatars.githubusercontent.com/u/106450097?v=4" + }, + { + "handle": "UffaModey", + "name": "Fafa Modey", + "avatar": "https://avatars.githubusercontent.com/u/83600229?v=4" + }, + { + "handle": "anish2105", + "name": "Anish Vantagodi", + "avatar": "https://avatars.githubusercontent.com/u/71202304?v=4" + }, + { + "handle": "JohnRTitor", + "name": "Masum Reza", + "avatar": "https://avatars.githubusercontent.com/u/50095635?v=4" + }, + { + "handle": "sudipg4112001", + "name": "Sudip Ghosh", + "avatar": "https://avatars.githubusercontent.com/u/60208804?v=4" + }, + { + "handle": "tylerapear", + "name": "Tyler Pearson", + "avatar": "https://avatars.githubusercontent.com/u/128009824?v=4" + }, + { + "handle": "justinjohnson-dev", + "name": "Justin Johnson", + "avatar": "https://avatars.githubusercontent.com/u/23105078?v=4" + }, + { + "handle": "Gabriela20103967", + "name": "Gabriela20103967", + "avatar": "https://avatars.githubusercontent.com/u/129247240?v=4" + }, + { + "handle": "shashaaankkkkk", + "name": "Shashank Shekhar", + "avatar": "https://avatars.githubusercontent.com/u/61085117?v=4" + }, + { + "handle": "bim22614", + "name": "Bim22614", + "avatar": "https://avatars.githubusercontent.com/u/64481896?v=4" + }, + { + "handle": "ng-lis", + "name": "Ng-lis", + "avatar": "https://avatars.githubusercontent.com/u/141615255?v=4" + }, + { + "handle": "jrbublitz", + "name": "Jefferson Bublitz", + "avatar": "https://avatars.githubusercontent.com/u/67718795?v=4" + }, + { + "handle": "shriyansnaik", + "name": "Shriyans Naik", + "avatar": "https://avatars.githubusercontent.com/u/66057990?v=4" + }, + { + "handle": "vagxrth", + "name": "Vagarth Pandey", + "avatar": "https://avatars.githubusercontent.com/u/83217083?v=4" + }, + { + "handle": "xlo-u", + "name": "Yash Upadhyay", + "avatar": "https://avatars.githubusercontent.com/u/67045160?v=4" + }, + { + "handle": "payallenka", + "name": "Payallenka", + "avatar": "https://avatars.githubusercontent.com/u/83643171?v=4" + }, + { + "handle": "DevTomilola-OS", + "name": "Oluwatomilola", + "avatar": "https://avatars.githubusercontent.com/u/114832204?v=4" + }, + { + "handle": "herepete", + "name": "Peter White", + "avatar": "https://avatars.githubusercontent.com/u/40490116?v=4" + }, + { + "handle": "nayan2306", + "name": "Nayan Chandak", + "avatar": "https://avatars.githubusercontent.com/u/79744453?v=4" + }, + { + "handle": "ZackeryRSmith", + "name": "Zackery .R. Smith", + "avatar": "https://avatars.githubusercontent.com/u/72983221?v=4" + }, + { + "handle": "SirRomey", + "name": "SirRomey", + "avatar": "https://avatars.githubusercontent.com/u/71877936?v=4" + }, + { + "handle": "KartikRN", + "name": "Kartik Nandagaon", + "avatar": "https://avatars.githubusercontent.com/u/93515675?v=4" + }, + { + "handle": "snehafarkya", + "name": "Sneha Farkya", + "avatar": "https://avatars.githubusercontent.com/u/63949465?v=4" + }, + { + "handle": "SyedShagufta", + "name": "Syed Shagufta Noval", + "avatar": "https://avatars.githubusercontent.com/u/62433926?v=4" + }, + { + "handle": "odhyp", + "name": "Odhy", + "avatar": "https://avatars.githubusercontent.com/u/78688788?v=4" + }, + { + "handle": "bibaswanroy", + "name": "Bibaswan Roy", + "avatar": "https://avatars.githubusercontent.com/u/50978591?v=4" + }, + { + "handle": "SuhasBk", + "name": "Suhas Kowligi", + "avatar": "https://avatars.githubusercontent.com/u/28872641?v=4" + }, + { + "handle": "MonitSharma", + "name": "Monit Sharma", + "avatar": "https://avatars.githubusercontent.com/u/65262068?v=4" + }, + { + "handle": "SubramanyaKS", + "name": "Subramanya K S", + "avatar": "https://avatars.githubusercontent.com/u/70049779?v=4" + }, + { + "handle": "guiszk", + "name": "Guiszk", + "avatar": "https://avatars.githubusercontent.com/u/23110500?v=4" + }, + { + "handle": "asingh4451", + "name": "Asingh4451", + "avatar": "https://avatars.githubusercontent.com/u/108614474?v=4" + }, + { + "handle": "Zedeldi", + "name": "Zack Didcott", + "avatar": "https://avatars.githubusercontent.com/u/66186954?v=4" + }, + { + "handle": "Yashparwal1", + "name": "Yash Parwal", + "avatar": "https://avatars.githubusercontent.com/u/54176283?v=4" + }, + { + "handle": "Harry830", + "name": "Harry830", + "avatar": "https://avatars.githubusercontent.com/u/114953309?v=4" + }, + { + "handle": "ayan-joshi", + "name": "Ayan Joshi", + "avatar": "https://avatars.githubusercontent.com/u/96243602?v=4" + }, + { + "handle": "Albinary", + "name": "Albert", + "avatar": "https://avatars.githubusercontent.com/u/69315591?v=4" + }, + { + "handle": "Blazier07", + "name": "Blazier", + "avatar": "https://avatars.githubusercontent.com/u/114798102?v=4" + }, + { + "handle": "EFFLUX110", + "name": "Mrinank Bhowmick", + "avatar": "https://avatars.githubusercontent.com/u/98869100?v=4" + }, + { + "handle": "Sameer-choudhary-git", + "name": "Sameer Choudhary", + "avatar": "https://avatars.githubusercontent.com/u/137263706?v=4" + }, + { + "handle": "sb-decoder", + "name": "Sowham Bhuin", + "avatar": "https://avatars.githubusercontent.com/u/108983339?v=4" + }, + { + "handle": "VykSI", + "name": "Vishal S Murali", + "avatar": "https://avatars.githubusercontent.com/u/116289447?v=4" + }, + { + "handle": "Xia3412", + "name": "Xia", + "avatar": "https://avatars.githubusercontent.com/u/157871580?v=4" + }, + { + "handle": "amunjuluri", + "name": "Anand Munjuluri", + "avatar": "https://avatars.githubusercontent.com/u/100225249?v=4" + }, + { + "handle": "vk0812", + "name": "Vidit Khazanchi", + "avatar": "https://avatars.githubusercontent.com/u/92437375?v=4" + }, + { + "handle": "cryptoracing", + "name": "Cryptoracing", + "avatar": "https://avatars.githubusercontent.com/u/101093104?v=4" + }, + { + "handle": "highb33kay", + "name": "Ibukun Alesinloye", + "avatar": "https://avatars.githubusercontent.com/u/89009222?v=4" + }, + { + "handle": "mmakrin", + "name": "Mmakrin", + "avatar": "https://avatars.githubusercontent.com/u/115393688?v=4" + }, + { + "handle": "otahina", + "name": "Hina Ota", + "avatar": "https://avatars.githubusercontent.com/u/108225969?v=4" + }, + { + "handle": "parth-verma7", + "name": "Parth Verma", + "avatar": "https://avatars.githubusercontent.com/u/96720577?v=4" + }, + { + "handle": "sirKiraUzumaki", + "name": "Ayush Lakra", + "avatar": "https://avatars.githubusercontent.com/u/48526201?v=4" + }, + { + "handle": "srujan-landeri", + "name": "Srujan-landeri", + "avatar": "https://avatars.githubusercontent.com/u/66351075?v=4" + }, + { + "handle": "exeayush18", + "name": "Ayush Singh", + "avatar": "https://avatars.githubusercontent.com/u/121486966?v=4" + }, + { + "handle": "ChefYeshpal", + "name": "ChefYeshpal", + "avatar": "https://avatars.githubusercontent.com/u/63582504?v=4" + }, + { + "handle": "ChiragAgg5k", + "name": "Chirag Aggarwal", + "avatar": "https://avatars.githubusercontent.com/u/110609663?v=4" + }, + { + "handle": "k0msenapati", + "name": "K Om Senapati", + "avatar": "https://avatars.githubusercontent.com/u/92045934?v=4" + }, + { + "handle": "u749929", + "name": "U749929", + "avatar": "https://avatars.githubusercontent.com/u/141889486?v=4" + }, + { + "handle": "ocryptocode", + "name": "Ocryptocode", + "avatar": "https://avatars.githubusercontent.com/u/156216265?v=4" + }, + { + "handle": "Subha-5", + "name": "Subha Sadhu", + "avatar": "https://avatars.githubusercontent.com/u/97968307?v=4" + }, + { + "handle": "rahul0x00", + "name": "Rahul Kumar", + "avatar": "https://avatars.githubusercontent.com/u/104289350?v=4" + }, + { + "handle": "Adrija-G", + "name": "Adrija", + "avatar": "https://avatars.githubusercontent.com/u/108509197?v=4" + }, + { + "handle": "Alex108-lab", + "name": "Alexander Monterrosa", + "avatar": "https://avatars.githubusercontent.com/u/81384567?v=4" + }, + { + "handle": "jpyces", + "name": "Advik Sharma", + "avatar": "https://avatars.githubusercontent.com/u/70201060?v=4" + }, + { + "handle": "oyeanmol", + "name": "Anmol Shah", + "avatar": "https://avatars.githubusercontent.com/u/99896373?v=4" + }, + { + "handle": "IamSoo", + "name": "Soonam Kalyan Panda", + "avatar": "https://avatars.githubusercontent.com/u/11900201?v=4" + }, + { + "handle": "Logadheep", + "name": "Logadheep", + "avatar": "https://avatars.githubusercontent.com/u/76146062?v=4" + }, + { + "handle": "tamojeetK", + "name": "Ahaan", + "avatar": "https://avatars.githubusercontent.com/u/89387048?v=4" + }, + { + "handle": "MattBlodgettProjects", + "name": "Matt Blodgett", + "avatar": "https://avatars.githubusercontent.com/u/62157141?v=4" + }, + { + "handle": "vrup0408", + "name": "VRUSHANG PARIKH", + "avatar": "https://avatars.githubusercontent.com/u/91781599?v=4" + }, + { + "handle": "its-100rabh", + "name": "Saurabh Mahapatra", + "avatar": "https://avatars.githubusercontent.com/u/98408932?v=4" + }, + { + "handle": "Santhoshnov", + "name": "Santhosh S", + "avatar": "https://avatars.githubusercontent.com/u/108118100?v=4" + }, + { + "handle": "ShivaNagachander", + "name": "SHIVA NC", + "avatar": "https://avatars.githubusercontent.com/u/123239393?v=4" + }, + { + "handle": "rajdeepdas2000", + "name": "Rajdeep Das", + "avatar": "https://avatars.githubusercontent.com/u/53941109?v=4" + }, + { + "handle": "Preeray", + "name": "Preeray", + "avatar": "https://avatars.githubusercontent.com/u/116193524?v=4" + }, + { + "handle": "Nikita0509", + "name": "Nikita0509", + "avatar": "https://avatars.githubusercontent.com/u/74171229?v=4" + }, + { + "handle": "niirmaaltwaatii", + "name": "N11rm44L 7w44711", + "avatar": "https://avatars.githubusercontent.com/u/60023424?v=4" + }, + { + "handle": "Monish-Kumar-D", + "name": "Monish-Kumar-D", + "avatar": "https://avatars.githubusercontent.com/u/141710569?v=4" + }, + { + "handle": "Sayakb03", + "name": "Sayakb03", + "avatar": "https://avatars.githubusercontent.com/u/108454817?v=4" + }, + { + "handle": "iamshreeram", + "name": "Shreeram", + "avatar": "https://avatars.githubusercontent.com/u/7752805?v=4" + }, + { + "handle": "Petsamuel", + "name": "Samuel Peters", + "avatar": "https://avatars.githubusercontent.com/u/65086865?v=4" + }, + { + "handle": "saanvibk", + "name": "Saanvibk", + "avatar": "https://avatars.githubusercontent.com/u/90634648?v=4" + }, + { + "handle": "robertlent", + "name": "Robert Lent", + "avatar": "https://avatars.githubusercontent.com/u/7595802?v=4" + }, + { + "handle": "Nowshin1077", + "name": "Syeda Nowshin Ibnat", + "avatar": "https://avatars.githubusercontent.com/u/47534922?v=4" + }, + { + "handle": "Toheed07", + "name": "Toheed", + "avatar": "https://avatars.githubusercontent.com/u/71722636?v=4" + }, + { + "handle": "RishavRaj20", + "name": "Rishav Raj", + "avatar": "https://avatars.githubusercontent.com/u/81917305?v=4" + }, + { + "handle": "Utkarsh-Raj20", + "name": "Utkarsh Raj", + "avatar": "https://avatars.githubusercontent.com/u/91374703?v=4" + }, + { + "handle": "KhwajaYousuf", + "name": "KhwajaYousuf", + "avatar": "https://avatars.githubusercontent.com/u/135570643?v=4" + }, + { + "handle": "dapoadedire", + "name": "Dapo Adedire", + "avatar": "https://avatars.githubusercontent.com/u/95668340?v=4" + }, + { + "handle": "dimonalik", + "name": "Dimonalik", + "avatar": "https://avatars.githubusercontent.com/u/114773527?v=4" + }, + { + "handle": "hritikbhattacharya", + "name": "Hritik Bhattacharya", + "avatar": "https://avatars.githubusercontent.com/u/44370586?v=4" + }, + { + "handle": "maglionaire", + "name": "Maglionaire", + "avatar": "https://avatars.githubusercontent.com/u/106713745?v=4" + }, + { + "handle": "lonelyH3b", + "name": "らきん", + "avatar": "https://avatars.githubusercontent.com/u/53707628?v=4" + }, + { + "handle": "smw-1211", + "name": "Smw-1211", + "avatar": "https://avatars.githubusercontent.com/u/61089886?v=4" + }, + { + "handle": "Limyangtyin", + "name": "Limyangtyin", + "avatar": "https://avatars.githubusercontent.com/u/46011496?v=4" + }, + { + "handle": "yest3a", + "name": "Yest3a", + "avatar": "https://avatars.githubusercontent.com/u/154103665?v=4" + }, + { + "handle": "rudy3333", + "name": "Rudy3", + "avatar": "https://avatars.githubusercontent.com/u/46790388?v=4" + }, + { + "handle": "kkchx", + "name": "Kkchx", + "avatar": "https://avatars.githubusercontent.com/u/162550478?v=4" + }, + { + "handle": "Guillotine189", + "name": "Sarthak Singhal", + "avatar": "https://avatars.githubusercontent.com/u/114574134?v=4" + }, + { + "handle": "newtoallofthis123", + "name": "Ishan Joshi", + "avatar": "https://avatars.githubusercontent.com/u/78465651?v=4" + }, + { + "handle": "jadkinsgr", + "name": "Josh", + "avatar": "https://avatars.githubusercontent.com/u/45151891?v=4" + }, + { + "handle": "siddharth9300", + "name": "Siddharth", + "avatar": "https://avatars.githubusercontent.com/u/83226178?v=4" + }, + { + "handle": "Rathish-Rajendran", + "name": "Rathish R", + "avatar": "https://avatars.githubusercontent.com/u/61904970?v=4" + }, + { + "handle": "Ashani-Sansala", + "name": "Ashani Sansala Kodithuwakku", + "avatar": "https://avatars.githubusercontent.com/u/85635476?v=4" + }, + { + "handle": "blindaks", + "name": "Akrati Verma", + "avatar": "https://avatars.githubusercontent.com/u/128223364?v=4" + }, + { + "handle": "cj-praveen", + "name": "CJ Praveen", + "avatar": "https://avatars.githubusercontent.com/u/80583308?v=4" + }, + { + "handle": "akghosh111", + "name": "Anukiran Ghosh", + "avatar": "https://avatars.githubusercontent.com/u/59087982?v=4" + }, + { + "handle": "amoghakancharla", + "name": "Amogha Kancharla", + "avatar": "https://avatars.githubusercontent.com/u/133768988?v=4" + }, + { + "handle": "Akarsh3053", + "name": "Akarsh Bajpai", + "avatar": "https://avatars.githubusercontent.com/u/72140851?v=4" + }, + { + "handle": "Quantum-Codes", + "name": "Ankit_Anmol", + "avatar": "https://avatars.githubusercontent.com/u/87054411?v=4" + }, + { + "handle": "Chloe23077", + "name": "Chloe23077", + "avatar": "https://avatars.githubusercontent.com/u/141309342?v=4" + }, + { + "handle": "iamdestinychild", + "name": "Destiny", + "avatar": "https://avatars.githubusercontent.com/u/105343197?v=4" + }, + { + "handle": "Devparihar5", + "name": "Devendra Parihar", + "avatar": "https://avatars.githubusercontent.com/u/54232149?v=4" + }, + { + "handle": "dikshant182004", + "name": "Dikshant Jha", + "avatar": "https://avatars.githubusercontent.com/u/122460149?v=4" + }, + { + "handle": "GigabyteZX1", + "name": "GigabyteZ", + "avatar": "https://avatars.githubusercontent.com/u/95098507?v=4" + }, + { + "handle": "TimTemi", + "name": "Flourish", + "avatar": "https://avatars.githubusercontent.com/u/93915253?v=4" + }, + { + "handle": "Ratna2", + "name": "Ratnadeep Bhowmik", + "avatar": "https://avatars.githubusercontent.com/u/112869261?v=4" + }, + { + "handle": "Morbius00", + "name": "Raj Saha", + "avatar": "https://avatars.githubusercontent.com/u/102956488?v=4" + }, + { + "handle": "QubitMatrix", + "name": "QubitMatrix", + "avatar": "https://avatars.githubusercontent.com/u/60323193?v=4" + }, + { + "handle": "QuantumNovice", + "name": "Haseeb / 하시브", + "avatar": "https://avatars.githubusercontent.com/u/43876848?v=4" + }, + { + "handle": "Nachiket94", + "name": "Mr._CG_04", + "avatar": "https://avatars.githubusercontent.com/u/83511180?v=4" + }, + { + "handle": "mzaryabrafique", + "name": "Muhammad Zaryab Rafique", + "avatar": "https://avatars.githubusercontent.com/u/43227117?v=4" + }, + { + "handle": "yashd26", + "name": "Meep_", + "avatar": "https://avatars.githubusercontent.com/u/65943606?v=4" + }, + { + "handle": "manav0702", + "name": "Manav Sanghvi", + "avatar": "https://avatars.githubusercontent.com/u/94554798?v=4" + }, + { + "handle": "saltX5", + "name": "Salty", + "avatar": "https://avatars.githubusercontent.com/u/92469140?v=4" + }, + { + "handle": "Adhiraj-11", + "name": "Konran", + "avatar": "https://avatars.githubusercontent.com/u/94843702?v=4" + }, + { + "handle": "khushhalgarg112", + "name": "Khushhal Garg", + "avatar": "https://avatars.githubusercontent.com/u/76150649?v=4" + }, + { + "handle": "whoisjayd", + "name": "Jaydeep", + "avatar": "https://avatars.githubusercontent.com/u/88286024?v=4" + }, + { + "handle": "HridayAg0102", + "name": "Hriday Agrawal", + "avatar": "https://avatars.githubusercontent.com/u/76818035?v=4" + }, + { + "handle": "ducheharsh", + "name": "Harsh Mahadev Duche", + "avatar": "https://avatars.githubusercontent.com/u/79721045?v=4" + }, + { + "handle": "Vivek-GuptaXCode", + "name": "Vivek Kumar Gupta", + "avatar": "https://avatars.githubusercontent.com/u/145761266?v=4" + }, + { + "handle": "YashTariyal", + "name": "Yash Tariyal", + "avatar": "https://avatars.githubusercontent.com/u/77398467?v=4" + }, + { + "handle": "Ylavish64", + "name": "Ylavish64", + "avatar": "https://avatars.githubusercontent.com/u/105187742?v=4" + }, + { + "handle": "yogesh78026", + "name": "Yogeshwar Kumar", + "avatar": "https://avatars.githubusercontent.com/u/77293859?v=4" + }, + { + "handle": "ahmedalhamad7", + "name": "Ahmedalhamad7", + "avatar": "https://avatars.githubusercontent.com/u/120293173?v=4" + }, + { + "handle": "allisonfintz", + "name": "Allisonfintz", + "avatar": "https://avatars.githubusercontent.com/u/117038166?v=4" + }, + { + "handle": "ambushneupane", + "name": "Ambush", + "avatar": "https://avatars.githubusercontent.com/u/56769633?v=4" + }, + { + "handle": "wre9-tesh", + "name": "Amitesh Kumar", + "avatar": "https://avatars.githubusercontent.com/u/96977189?v=4" + }, + { + "handle": "benedictprajwal", + "name": "Prajwal Benedict A", + "avatar": "https://avatars.githubusercontent.com/u/114901414?v=4" + }, + { + "handle": "chimerson", + "name": "Emmanuel Ogu", + "avatar": "https://avatars.githubusercontent.com/u/110922266?v=4" + }, + { + "handle": "Vishvam10", + "name": "Vishvam S", + "avatar": "https://avatars.githubusercontent.com/u/78094956?v=4" + }, + { + "handle": "vikash485", + "name": "Vikash", + "avatar": "https://avatars.githubusercontent.com/u/114328709?v=4" + }, + { + "handle": "ImaginedTime", + "name": "Uday Srivastava", + "avatar": "https://avatars.githubusercontent.com/u/68231861?v=4" + }, + { + "handle": "swapnadeepmohapatra", + "name": "Swapnadeep Mohapatra", + "avatar": "https://avatars.githubusercontent.com/u/41564532?v=4" + }, + { + "handle": "sumitbaroniya", + "name": "Sumit Baroniya", + "avatar": "https://avatars.githubusercontent.com/u/75536160?v=4" + }, + { + "handle": "sagindykovsl", + "name": "Suliman Sagindykov", + "avatar": "https://avatars.githubusercontent.com/u/115887342?v=4" + }, + { + "handle": "sudhanshu-77", + "name": "Sudhanshu Tripathi", + "avatar": "https://avatars.githubusercontent.com/u/97780892?v=4" + }, + { + "handle": "shubham7668", + "name": "Shubham Kumar", + "avatar": "https://avatars.githubusercontent.com/u/54644253?v=4" + }, + { + "handle": "spyke7", + "name": "Shreejan Dolai", + "avatar": "https://avatars.githubusercontent.com/u/93109967?v=4" + }, + { + "handle": "imshivamrai282", + "name": "Shivam Rai", + "avatar": "https://avatars.githubusercontent.com/u/129401321?v=4" + }, + { + "handle": "MamaevSergey", + "name": "Мамаев Сергей", + "avatar": "https://avatars.githubusercontent.com/u/153006768?v=4" + }, + { + "handle": "sdthinlay", + "name": "Sdthinlay", + "avatar": "https://avatars.githubusercontent.com/u/129248162?v=4" + }, + { + "handle": "samayita1606", + "name": "Samayita Kali", + "avatar": "https://avatars.githubusercontent.com/u/64488010?v=4" + }, + { + "handle": "NishantPacharne", + "name": "Nishant Pacharne", + "avatar": "https://avatars.githubusercontent.com/u/71060551?v=4" + }, + { + "handle": "LionLostInCode", + "name": "Leon Heinrich", + "avatar": "https://avatars.githubusercontent.com/u/133058949?v=4" + }, + { + "handle": "ta-brook", + "name": "Tanawat Jirawttanakul", + "avatar": "https://avatars.githubusercontent.com/u/85091595?v=4" + }, + { + "handle": "rathoreshreya", + "name": "Shreya Rathore", + "avatar": "https://avatars.githubusercontent.com/u/109817678?v=4" + }, + { + "handle": "shayaanyar", + "name": "Shayaanyar", + "avatar": "https://avatars.githubusercontent.com/u/119527644?v=4" + }, + { + "handle": "samualmartin", + "name": "Samual Martin", + "avatar": "https://avatars.githubusercontent.com/u/38009832?v=4" + }, + { + "handle": "saipranavrguduru", + "name": "Saipranavrguduru", + "avatar": "https://avatars.githubusercontent.com/u/116751855?v=4" + }, + { + "handle": "pratyusha0710", + "name": "PRATYUSHA CHATURVEDI", + "avatar": "https://avatars.githubusercontent.com/u/115498333?v=4" + }, + { + "handle": "prathamesh19p", + "name": "Prathamesh Phalke", + "avatar": "https://avatars.githubusercontent.com/u/56258432?v=4" + }, + { + "handle": "Prabinshrestha737", + "name": "Prabin Shrestha", + "avatar": "https://avatars.githubusercontent.com/u/41283995?v=4" + }, + { + "handle": "cypherab01", + "name": "Abhishek Ghimire", + "avatar": "https://avatars.githubusercontent.com/u/149231513?v=4" + }, + { + "handle": "paartheee", + "name": "Paartheee", + "avatar": "https://avatars.githubusercontent.com/u/66585214?v=4" + }, + { + "handle": "ThanasisNov", + "name": "ThanasisNovatsidis", + "avatar": "https://avatars.githubusercontent.com/u/47293617?v=4" + }, + { + "handle": "nebulaanish", + "name": "Balaram Neupane", + "avatar": "https://avatars.githubusercontent.com/u/50112251?v=4" + }, + { + "handle": "myudak", + "name": "Muchamad Yuda Tri Ananda", + "avatar": "https://avatars.githubusercontent.com/u/69108782?v=4" + }, + { + "handle": "mr-desilva", + "name": "Tharindu De Silva", + "avatar": "https://avatars.githubusercontent.com/u/47147935?v=4" + }, + { + "handle": "mojiibraheem", + "name": "Zainab Ibraheem", + "avatar": "https://avatars.githubusercontent.com/u/111240638?v=4" + }, + { + "handle": "0silverback0", + "name": "Marcell", + "avatar": "https://avatars.githubusercontent.com/u/38266625?v=4" + }, + { + "handle": "mangnez", + "name": "Mangnez", + "avatar": "https://avatars.githubusercontent.com/u/110713609?v=4" + }, + { + "handle": "kodingkin", + "name": "Kodingkin", + "avatar": "https://avatars.githubusercontent.com/u/125667216?v=4" + }, + { + "handle": "jonascarvalh", + "name": "Jonas Carvalho", + "avatar": "https://avatars.githubusercontent.com/u/79672134?v=4" + }, + { + "handle": "jakbin", + "name": "Dhruv Jagdish", + "avatar": "https://avatars.githubusercontent.com/u/65591973?v=4" + }, + { + "handle": "faizaslam11", + "name": "Faiz Aslam", + "avatar": "https://avatars.githubusercontent.com/u/60103930?v=4" + }, + { + "handle": "darshbaxi", + "name": "Darsh Baxi", + "avatar": "https://avatars.githubusercontent.com/u/119887723?v=4" + }, + { + "handle": "dab07", + "name": "Harshit Sharma", + "avatar": "https://avatars.githubusercontent.com/u/78215681?v=4" + }, + { + "handle": "d-coder111", + "name": "D-coder111", + "avatar": "https://avatars.githubusercontent.com/u/82580017?v=4" + }, + { + "handle": "HimanshuSinghNegi", + "name": "Himanshu Singh Negi", + "avatar": "https://avatars.githubusercontent.com/u/72004239?v=4" + }, + { + "handle": "harshhes", + "name": "HarsH", + "avatar": "https://avatars.githubusercontent.com/u/93470145?v=4" + }, + { + "handle": "GargiMittal", + "name": "Gargi", + "avatar": "https://avatars.githubusercontent.com/u/98906186?v=4" + }, + { + "handle": "emanalytic", + "name": "Eman", + "avatar": "https://avatars.githubusercontent.com/u/142586747?v=4" + }, + { + "handle": "EbrG786", + "name": "Ebrahim G", + "avatar": "https://avatars.githubusercontent.com/u/82704148?v=4" + }, + { + "handle": "Dishant10", + "name": "Dishant Nagpal", + "avatar": "https://avatars.githubusercontent.com/u/84343829?v=4" + }, + { + "handle": "Dhruvil-Lakhtaria", + "name": "Dhruvil-Lakhtaria", + "avatar": "https://avatars.githubusercontent.com/u/77167073?v=4" + }, + { + "handle": "Dhandeep10", + "name": "Dhandeep", + "avatar": "https://avatars.githubusercontent.com/u/106858770?v=4" + }, + { + "handle": "David-hosting", + "name": "David Ioffe", + "avatar": "https://avatars.githubusercontent.com/u/67858186?v=4" + }, + { + "handle": "Crack-er-jack", + "name": "Crack-er-jack", + "avatar": "https://avatars.githubusercontent.com/u/82202860?v=4" + }, + { + "handle": "Carl283583", + "name": "Carl283583", + "avatar": "https://avatars.githubusercontent.com/u/153916649?v=4" + }, + { + "handle": "bhanushri123", + "name": "Bhanushri Chinta", + "avatar": "https://avatars.githubusercontent.com/u/110604493?v=4" + }, + { + "handle": "ayushi-ras", + "name": "Ayushi Rastogi", + "avatar": "https://avatars.githubusercontent.com/u/112415152?v=4" + }, + { + "handle": "AtharvaDeshmukh0909", + "name": "AtharvaDeshmukh0909", + "avatar": "https://avatars.githubusercontent.com/u/97836434?v=4" + }, + { + "handle": "Astrasv", + "name": "Sudharsan Vanamali", + "avatar": "https://avatars.githubusercontent.com/u/116169969?v=4" + }, + { + "handle": "hiarijit", + "name": "Arijit", + "avatar": "https://avatars.githubusercontent.com/u/68233664?v=4" + }, + { + "handle": "ArifShariar", + "name": "Arif Shariar Rahman", + "avatar": "https://avatars.githubusercontent.com/u/43639863?v=4" + }, + { + "handle": "anju-chhetri", + "name": "Anju Chhetri", + "avatar": "https://avatars.githubusercontent.com/u/57993069?v=4" + }, + { + "handle": "anishaxtha", + "name": "Anisha Nayaju", + "avatar": "https://avatars.githubusercontent.com/u/98213145?v=4" + }, + { + "handle": "amersbahi", + "name": "Amer Sbahi", + "avatar": "https://avatars.githubusercontent.com/u/135067809?v=4" + }, + { + "handle": "Nini010", + "name": "Aleena", + "avatar": "https://avatars.githubusercontent.com/u/146352836?v=4" + }, + { + "handle": "AdityaSahai123", + "name": "Aditya Sahai", + "avatar": "https://avatars.githubusercontent.com/u/72221032?v=4" + }, + { + "handle": "adixoo", + "name": "Aditya Kumar", + "avatar": "https://avatars.githubusercontent.com/u/124980341?v=4" + }, + { + "handle": "AbhinandanSingla", + "name": "Abhinandan Singla", + "avatar": "https://avatars.githubusercontent.com/u/52960031?v=4" + }, + { + "handle": "abc-is-here", + "name": "Abc", + "avatar": "https://avatars.githubusercontent.com/u/110215279?v=4" + }, + { + "handle": "aryangulati", + "name": "ARYAN GULATI", + "avatar": "https://avatars.githubusercontent.com/u/42711978?v=4" + }, + { + "handle": "Surya-29", + "name": "Surya Narayan", + "avatar": "https://avatars.githubusercontent.com/u/75674235?v=4" + }, + { + "handle": "RishiPastor05", + "name": "RishiPastor05", + "avatar": "https://avatars.githubusercontent.com/u/87607061?v=4" + }, + { + "handle": "VedSadh", + "name": "Ved Sadh", + "avatar": "https://avatars.githubusercontent.com/u/86703661?v=4" + }, + { + "handle": "Ramisky", + "name": "Ramii Ahmed", + "avatar": "https://avatars.githubusercontent.com/u/19819186?v=4" + }, + { + "handle": "Raashika0201", + "name": "Raashika0201", + "avatar": "https://avatars.githubusercontent.com/u/110974003?v=4" + }, + { + "handle": "farisfaikar", + "name": "Faris Faikar", + "avatar": "https://avatars.githubusercontent.com/u/87220004?v=4" + }, + { + "handle": "Praveensv11", + "name": "Praveen SV", + "avatar": "https://avatars.githubusercontent.com/u/112382643?v=4" + }, + { + "handle": "Pratham-H-S", + "name": "Pratham-H-S", + "avatar": "https://avatars.githubusercontent.com/u/126776410?v=4" + }, + { + "handle": "pranavdasan", + "name": "Pranav Dasan", + "avatar": "https://avatars.githubusercontent.com/u/62214486?v=4" + }, + { + "handle": "Prajwol-Shrestha", + "name": "Prajwol Shrestha", + "avatar": "https://avatars.githubusercontent.com/u/70046321?v=4" + }, + { + "handle": "KushalPareek", + "name": "Kushal Pareek", + "avatar": "https://avatars.githubusercontent.com/u/105929422?v=4" + }, + { + "handle": "nilay-banerjee", + "name": "Nilay Banerjee", + "avatar": "https://avatars.githubusercontent.com/u/66667272?v=4" + }, + { + "handle": "NischayGoyal1", + "name": "Nischay Goyal", + "avatar": "https://avatars.githubusercontent.com/u/81116984?v=4" + }, + { + "handle": "nik-6174", + "name": "Nikhil Kumar Jha", + "avatar": "https://avatars.githubusercontent.com/u/78644716?v=4" + }, + { + "handle": "EpicNesh26", + "name": "Nesh", + "avatar": "https://avatars.githubusercontent.com/u/130682376?v=4" + }, + { + "handle": "jusinamine", + "name": "Mohammed El Amine Benkorreche", + "avatar": "https://avatars.githubusercontent.com/u/36046405?v=4" + }, + { + "handle": "this-mkhy", + "name": "Mohamed Khaled Yousef", + "avatar": "https://avatars.githubusercontent.com/u/14186989?v=4" + }, + { + "handle": "Manishak798", + "name": "Manisha Kundnani", + "avatar": "https://avatars.githubusercontent.com/u/90680330?v=4" + }, + { + "handle": "manishkumar00208", + "name": "MANISH KUMAR CHINTHA", + "avatar": "https://avatars.githubusercontent.com/u/76589863?v=4" + }, + { + "handle": "LightxAman", + "name": "Aman Sharma", + "avatar": "https://avatars.githubusercontent.com/u/118755943?v=4" + }, + { + "handle": "iamkunalpitale", + "name": "Kunal Pitale", + "avatar": "https://avatars.githubusercontent.com/u/26413565?v=4" + }, + { + "handle": "Krishna13515", + "name": "Krishna13515", + "avatar": "https://avatars.githubusercontent.com/u/103298281?v=4" + }, + { + "handle": "Josephtobi", + "name": "Josephtobi", + "avatar": "https://avatars.githubusercontent.com/u/49555155?v=4" + }, + { + "handle": "Jishnu2608", + "name": "Jishnudeep Borah", + "avatar": "https://avatars.githubusercontent.com/u/85721902?v=4" + }, + { + "handle": "JenilGajjar20", + "name": "Jenil Gajjar", + "avatar": "https://avatars.githubusercontent.com/u/68738624?v=4" + }, + { + "handle": "Polqt", + "name": "Pol Hidalgo", + "avatar": "https://avatars.githubusercontent.com/u/80904366?v=4" + } +] diff --git a/web/src/lib/data.ts b/web/src/lib/data.ts index f72d8249b..f72bedb71 100644 --- a/web/src/lib/data.ts +++ b/web/src/lib/data.ts @@ -41,7 +41,7 @@ export const CATEGORIES: Category[] = [ export const CONTRIBUTORS: Contributor[] = [ { handle: "Mrinank-Bhowmick", name: "Mrinank Bhowmick", commits: 412, role: "maintainer" }, { handle: "ibra-kdbra", name: "Ibra-kdbra", commits: 38 }, - { handle: "PythonicBoat", name: "Yashvardhan Singh", commits: 27 }, + { handle: "itsyashvardhan", name: "Yashvardhan Singh", commits: 27 }, { handle: "Alby084", name: "Alby084", commits: 24 }, { handle: "ca20110820", name: "Cedric Anover", commits: 21 }, { handle: "kanchanraiii", name: "Kanchan Rai", commits: 18 }, diff --git a/web/src/types.ts b/web/src/types.ts index 949113eae..19641c1d4 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -55,6 +55,13 @@ export interface Contributor { role?: string; } +/** One entry in the full contributor list (scraped from the README). */ +export interface RepoContributor { + handle: string; + name: string; + avatar: string; +} + export interface Stats { projects: number; contributors: number; From e4c3061b8dd90940422f8fc05590e39ad5ba75a9 Mon Sep 17 00:00:00 2001 From: Mrinank-Bhowmick <77621953+Mrinank-Bhowmick@users.noreply.github.com> Date: Fri, 22 May 2026 11:28:18 +0530 Subject: [PATCH 08/14] update pnpm-workspace.yaml: add empty packages section --- Banner.png | Bin 751370 -> 979079 bytes web/pnpm-workspace.yaml | 2 ++ 2 files changed, 2 insertions(+) diff --git a/Banner.png b/Banner.png index a29ef27e0435be4100769164ba661aa43c423ffd..739bcffc4a879ae8945a8372e7cda53ec90146b0 100644 GIT binary patch literal 979079 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®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%0DR@e%bAeH3tZ&x}R8lsMF6t!en# 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

gX5YAZI9)Ka*Dm(Ahhr`qNJQFIo5P4;aR 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�k`fh|D-@*s^j{!SWEp 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ⅈ@9JFwXtjL487&GA(BohoEXQAi2WdqE}dby8V z^?ZPp_S+^+&51`xIKpUUp;}eke-TQYkQ&GVaAK*LIA_$P59@wJ{Q$ei+aAxJ5|Lgd zGGdkWU;+LLm}I661faV-?pHmSqtV~>HUp(kJ?C1)-gY04Z}u=1Kv>va7`e!8?$75fBxfiiIyo0ZNocQG4lK~(JoxAf2P&*yl%xFA5|vi46h>E z%|_oSo*|2R6aza-p#aujt?Q$V+^>y@I_`1h$nRMz;j*8SyRLO;yrl6K1QF%T5RVxP z>zdIL6$Fo0RVF4equ07Nt3y&`)8=aV+AjFV-^-zK`iZoODt6pFb07RSS1)VQtAB9Az2EOvVTV{2(d1=WQiISGP5+@mG}?xQ~XmterJBz`8Q%f*O)!^UIH&r#Q++zzp5>8idXbTiD_kO`=`r}IT~|b<)#4?~nOEMA z1~;H}TQRCT`qYy)pd%ehvJ~|4^}>df+BtxazykJFheF2i5&K4O1Xy|Qdr6|JsWCj| zeYo8$hGYQf5m{`*d8#jrN;EIkygLF5dDop#uL6N)OrTDiSSa7LMvGec^>5+!vmPdx>(JT59 z&EZ{!ZPv$vJW59c)=&J8--_kTlGM0mHhT=7?WKRfvt<6SFJ1LC6;1BS5}p4bQbjAy z?#w24xqj`um=uWjdHXZ@bh65(z^+M3cly`iyK$g7y0HAvEgubajGm1WwpzM!BCNFh z7FI~IXo@-BD{hxyX$530kl7AmL|qj6?L77O;LTJIF>e+)PZ<255@CmJ2HK^iZx3$% z;q{6oOtu~_GJbkkbaTOcGv4FFz`gc9#Qq03a957o&(6RgllQ1KaOj<> zSOG1**6}X1ix1eJ{JDYaf^~ z$1yu~ddMqby~BbcTZ1<*;lXmF{)}H^Kf#12Z#OG|u%0a4X8@ z&pDwPxeD!iV&j$1dV-gx3;Jf1FM4^bEd8%ItuPF#f=@=vUF{u&uh6E1gK(kW$ahr| za(w`=2!wKi4FVrKx9~LlOTOCii`1i_oexzn11cnjMz3F9X{KcrCA!PR`l<_s(SIhB z3lptho1L)dHO=?2dfmX*%0a$mIlN*SO2-p}T>q_6?nnw4A@maZ=0(EcBtz zXS2;?GB~8GYKC6>UP3=YU&uiW=S$P?v>I}o7T92z5`83uy^rl~rAWBaKM9S52~!a< z;2X1hu@f3+zjBq#g{ZLk%hhRlpJ5!%irW~TcgXVdI0)kD?(nl?M>4)zGzF(is93Q~ zja^7&s#8mRI%SmdZa9F){SK&E9I9;WJkd|~88o<)nO_RIKd^I5|D||eQ2VEIhd%F- z9aQc7JXX=NVjbuMh+K6hfT2eg1MSotb4jPBwl&9 z1@AQ25|54$s(>iS$SidxDZULvl~qmNyWQk_-jQ-Lls^rhBt(ov`L@wNda+EvhU12; zF8=|pR~v${i$ugSX?nq865-(9iHobpUIEGJjgArLx)TE7AlB>oFYDHy1}#6yl@7_h zt&DP1scn(@jDK6+OiV;2yi4fjLt zXEWBx&<43Z$4Oz!To-hvF1L?!vIQq&K(9C`-tf#5Y6G|ADKVn>M`hl2c#=Pu2gNv$ zY@A4DkW6gUzL#MMH@JX_Y#lT7kaS7>Sg5&K@)ro!97^wk|1fL{6`dHtk|6?Zd-8#-H9nA!Jrb#!$-m5}6%8Tft>={2G-pUYJ`-*fdX>}HIQo(NNnOkA71 zD=1)!#|?YrBq9VSx~wT_MHCX>r8_z_I{Np-<#(oism1nA6U%V&6S*aPME<>|p(B+X zNMAmiA%$k<-?9*kB;@$;l_@7RJe3M(j4LvkIN>GmP6hmC#rd|^AATZm4?sz%1Mt$R zknfvpgsf>`L>}muz=z0u0H&J%Uw__?{ipVu_6%B^9CE;Tl<;s!nK!q;UBbM4s?1@o zW4%l`lpxgj-jMS)QBc}3p$OnEq5|F1+yR|VxvJPtHYJ4juz~G={r1%!=N7`xJ3|R- zto*LnHcQkKz~;yls5yXH+&YSD#ktKfBX-wPbzIN+==Pizw3m<^sc#+*8m}oKx-g>L z%}Ky@{*@9BOuB}EP-;(N53hgG-J8xUm(I|>$Ef6*JrDdC_|r+lT}TX8ifp z(>dyV3Hm3%D^lpU^y}-)-KVI$9`>Nx+Q^5O;q5j?w1xH$rr)OpeypF`Z|*D7`Tz%A z3~75=)BR^RhbGiCL3{cDuO5=w9T4k{1#CE_HBssebi*N^3rIY^Uz%~DtIj*qp^~C{ zWO?x~t%|=8Ca2F!$3(d?H5u?Ey zg4MV)^)nzjTkBqTH+3)^BNK~H@eLNcZbyU;J710Mw!?(A=6ApWVcIGzS)fplTyxB? z4Qky}5IIUN>M1Mu`v3@?Sr&t8Z9)TBGfzOpSj@r(h= zZLq_NGROSxOK;L%#vmIpU+rJG7#JpS@eDKq=fyI7EgnOia*5Z9vwy-P>Ns@>P5WM% zV3t{8WX+0wx&nT=%|F(>fxw;Em)%*7UdYqt71JUIz#^8De&Cul)Un7_!UH{SFE3~o z&UhiSzl&o*H;^%%);ERs68tKOkil($r(6ao&+eF%%T{%2RMvzRnK}65DL~r~HR) zH;7TD54Hwf@MKum(|(R(6ef1HO5| z;MgZgAk3Uih?ay1D#~h}?J=0tW_do)1?taLj~CN;`RlDXH4EYBMaWj| zTN)e}#hpj>p2-ZX+IG8s$XgF>R>_-7ZMs>G+S<^O>;38Bc5lu=t--U}ZmT>XAF9_> zEV6n=#B*#PJO~&4EPZcvY_2;p_dtyC_lUk(kTi48^nNY9%Cq}Vw!e}WS|7xV#ynZ_ zK+WgjzX((oZ!Ao*0xVw2w+cujbw=wf{5693ZlK*%aqb4@wQ?W;3LXnaZ-{wsw$b` zvKHIw)rdJBmu5Q`0RhAhGj2z1nYgD~;u%u9gPXr;JpkK1RxuaYOhnASClSJhW7V57M504p6%-B zXbW_Em%6zc6ifLoD_k({Y#4MWCYl`IRJ5*h#@@>YzWj~Rz=~xQ3lTd4Fz?^oc*iD0r=XMerKawkL^QaF(n)a6->OqW&vI%*-!5R1%Cv7-+(|nn?1Ln3g0b zNdSPuW${4^KCTF?9h4S z4{b)S*9r+BvZbFnf)`CNSEFAVSnYGg)t_&f5XdXai*`2&JmQrIl|wyR=aCUyWo%^? z5B1()x2!k1Rnntp=Xan1>;?K+#Vq0}82ABBgi(TerA*R!m?%5k-D{AgVh>J9-SlvX zIMi@jc1}3u{c|>)<=*M{;mAw+$Oo8_tnNO`NnIc{GJ9PWn|%0>Jv`fE(3iBn`y`JC z8QxPsWKfMvE+d5py!w{3?U{+*Utq{8MC-1!FupsQUKTBW<<>f4vnWM4(QsPE?6w+V zby3|SMkv0UT-c*J85r)VsQg>&7<|+Ap|=LD(w)b;R*f?EeH-1S{cP{_I9SZpZm7fL zQC>Gs%PooAkdAH_IcR!vrD&c%;I_ z?AA=0bdkT!kREC>0bk>%g2XB{zi8X39y)pSr++3dU^XY^U2wQ+Y)?s*C={X6XS(pl zWng}d{~mQ9K6!Ytu1)7dW+jW7Bumy5PFRb89Mx)BN3%Lp&?R$*$ljs-#b7UtO&DUO zAi)>?{m=3_h}=#>X7QwLChHlA+lDWL@g8ByN}g|0y}D2#J~(XO#~YK>nq zyYL_S{x>2uOUZG5{m);Tdb$R>6}R3eo(HainS)Oh9@RAe*I)dOh9b4BwN=4$sH)ek zbYA)Xrn>&ap2FD>JUgcth(Yv%0PDnk`183oopmfSzp@o`NyaGO(aPe9OiziwAAoBp zvqkXxth<{Fb>1bXdY2LYbsorIeVL#}&{X6OkFyFsA@Sv*&*K!-VdV)#Cwvyt%P|^P>QwzADmw!d=X} zXe%aykd2-1c%cGHwgXCdoHO{nO+bI>T=6QM&?tYn@ra?0AQ+$r%@PRR8cE()-|>io zW0wC-v)BNPBDJ?WIt0vo+@3Qfo)zg*$dCkKh`e(b?vss0h2i?H8O50(9Gg-dfSzqk zJLg#4=^c?*G>VMro~&Oz`{zb#_fy0F+OaFRhB4_v;~#N^a?6OV66!lwxL=*Ty+0l0{3p8hjh&stY%v(k!&|Yt!L|ADWxi+pC~fX( zSs8JI4Uqo!j{(Wc*+YGvNP*2N+bn_YDXz@$GbuZP(_Cky(-FT5{ZU*uaq_L*_ss0Y zfTL}X24$g35<=b^lkn_;qq59D9M2ls#`TFT=|zpibLM$CA~Zm|H-a7fj~@O*ihz@H znQ89yeK0gLy%)DXR!A+Sg}v<+rRRZ(pv~hGM+pZC0enZwZ;p+CWVSWo;g5~zVoGwe zD09PYF?2xGMF{M!o;@0^4(XSRLBEeCO~Q&jQltFlcHT~q#Ir4bT`PAk)Rk^L@LHMVa=!?K+ z)I6@{u9?93JV*gbApqNmI+ha-9PccRjto`#048dc7O;%W!gE5Gq%amIv zOh&9@_C0jWl7&hY;Yb;OM3THdgvzaG{n8qs;p*l_e{|lh#w;yqK@+}ITb+2fpdb{O zADEw)aC|IBpbWgTMTg#2@5;YjmY<2nKd2K)(gdt;mK}Lbu0^&kF1Ap-FxZ^-<}Sa8 zFM?v+Tzzx0O16$o-a@ek73cU8_U0QA0U>4kMD%gdJ6C-#+VLpLGr9O0HsVv zvWDmt)W9=`+>QD#!~V;3{wgA5_WVe)Ljz0-b{14y*CGXIdE?XBl>hh#Vw0NVE*oMn z(Ohgv@|bCdj2U3Lf0PTO;T0Sag@MFGff#&*1i}@}lArzR5abb`Y|3U#+j+V)Ag~erNNps^kRitdM7B`|^C1 z+$3WG6XS#*ZWEJ<~h7l(vt1$`Emxw8_ zPfa<}wm?LJ$q)GcU0?|)CXQS{t(uWLI01@kA|g>f&M7-`OkNquX4S>>ztPK4>wDW1 zaO$jI!eR=&&@+qF1gvPW-8Y2S#w!2jA2zU#C?{@OjSwxE#4|TOx9_4MqJ`~sR=aPz zu@l|=0x{6SG>sjg1Vo$c;4VpY3K(S^p>&=Bm7FVf7Tb8d{bM*xBcEYgR}lf1aIvBD_&b2^O5rQ37+O75By6+k(QE)+8$ zh~Hu5fxmemCT5zh!u<?+LuTX&CWYa;Ll~(r&)K{_CK2^Yss%pd{ zj)^@g75BNXYxDQh9I?g8XDELS*V^kf`c|=jJ@YdUQTE1mauoq}Cmv)^ z@v3<|0VPTzmKk5Cc+y1U74_2%%3FZQO|BvD&x}U^DI5UfFbb-69N9y1tz98}z+*1V`rU^`< z-Ij3&IDR$@jpqu^ZL% zsw5X?@QBkDY4mkn8S*cZfaDp5JL1vG@(rDJS})tWdEXM7o%q6%AsM30%^80k4_H5q znO^sp@!e?gt8Ulheif9@*0S{52 z`wIvm3Vv?zXye;5s;H9Cr9f#jd)K0+b|=9GiU>ayv3^#s0An*Pq2qbexS21q7M^Hd zDHYj{*FS4fCK2=8z4l;*x0Skm!xLO4&c_tI1oAN05`N+ z?R2E=khx!2*tjz!aI(eZ zuS1?>{%>D_N4~tOKy=+0h16PJ@6%O^uRMe>HUo4;sj#8 zMAwG;A!KhsRwwD)kIm_7RBWQm3H_>M5&t-LSiQ399t*MVYRTpF^P9{V6EEUJBI-H7KhcL(=I#6DK2Fu1gpbfdzw#2upeX zCHf4itL@;zh%yu0w6)uV`D-Y5Ji%^xTc1?4!Op=`S@;6phZ-7Ky?R0#v}rsdbZn6T zX9Ztxa8jfhuwNHjJ^%G~*`9h%{xPCOB`k^a!qF!7)jUJF$s74d2Jb7kYf9tE{QdI% z9oFhYg|mHW$gZ>R)DPYyy^LiIIy)1H&`pH&FUa7Q=v0u$na4%3fzb0v3|)X;2D$Xg zpX824@@`Hq-38iZJ)SE!DT<_dXye4S`eF~)lhI!|E2jRh6}{}y|AaPzd%lKeJYLJp z7S)r=&Bt7{t9f{IG3jK9-UM$~4By^JlVCqeXa4vST1JR?Mn_=4X=7#QU;y$V%Az-y-gM`CN#?n(eEk@z01kGy+IalsMo zczY>c;id5XDqFsnB?b(*BNA0hr$gClT`&n@rJk5#>;sw8Yw$MZP9Ku>=-N4NonN7v zK;$>j9IUUf6W1FX9Myk=S;>V(O+t6_o(m4_XFqZ`iySx3%kHn?)=#Gsr}5bSaq(x7 zcp;oX5zAIBivS$`iFkdAP?46PT$mwNVX;uYJDX*=JN_Yiu((LxDn>y?3S#)XOG`9L zW{p^f=^*lns;wG|D*?4?#apj`q+HLDBOsIg0ycvOvjzOS` znTPs!LY>_~BvMgD#vu_mgi$_k{X zoNR)G>KxSz6TF=A@ZQKwEkK zS3aGS*$3@6H*QPpY2t>bmZ%X~|F7HoVI+yWWAe#ZDBf3|<50?I-@{g$A0Kx)oJ$MA zsM;j5R*#(Csk#sR;*zM!P_p9`9G5;!tL6un9{y4(w>V|sR|nx?hpI=}@aWClC!||h zQiS>!o!`qJv3A!&!YuDzGhawp6lmbjRN%RWiPE}^ev9~;R-}9I30@5B_fX#2N6W#k z8lb&hKe8PpKHpgPap{zU%6Vab&+lcPO)PYiGRKO!iDfb2_k02-9lzah%)(GMf>#cv zDW6tHu8t_VSP~}XJXjlxT2GHY^V+V_?REG+oQhIkbo<&4K{^>Qw50YtZNl?(!1q7K9 zSp`4kw~l?ro5ImObRjZ0cxb&Os=84`2V$0m=LmUp6f{yWnY7|!aEb#@wjtoVIY}}Kgmn>xiNk21vhC?+PYC3>F z>=EuOqrUpdhW=1--9=5X|YJ|;`&f;JO}eKtM$n> zH;S#f^7Y&9EBDEGlzzBi<3-$=cYJ~``K!x0=5tc}#8Ixnljk9uf)bU8T6f&0M{|pc z%V|Bn7eO_!0NpeoeJl}e_iCx(!UHjz`21a$Vt|ha8+q|9Gc_ zVJ%_e3XvS{?Zs2yl8jm85=;aT2f+6vOb0dOM|4Xa_8;V>3EkU!KY_bGJXoYr57=oa z?A`8l08mVELto*?2$ORj_3JrL7%mJV__11cKL=skx7{pJ&GFWgZ{2m^)gC%LVpBl=L}W9m&pLl0eYsF{lLH^6&8xYZ)`UbUT^oZdkC`Cffc|1^2V?* zBSqee3t}_ox)?4po;T;Ygp&ew%;?K~z5r~Dj3%uoNgcIqY>bkBq=aXinbkH2yUcLi zICfT@HB~9w-LqU_09ex{TvyE9AG&>3Ja^@?9Rx`_85Vl=#K_xdqZGdvCleK`6{LkKxn<|~ zhPqsOr;?{CjRYhjWA6bN15yN|dV&P01!vGZ6PdXi{;?I;|Ln{$Ga_K&kBF+PJ;|7~ zZJVJ8O(A(hjsX=0x($_YJaFR->xdZQCJ0OvKhy%yr8z|}kI7#()J(-hG+&BHCC~`t z1e~{SA2dNXC0EhA^&7U^*NjvDDyPgrjv}dPu|-q)S$%X7_ua5^n6Bj0`6a>GQ{Zs^ z^WkIa&p~2{CmBA4u9bK^W?TinR93V_kuS$LLx%Lb$K%Ql<9Onn{}_wHi1SeL1=Ic_G28WeTP9Am3#D z8|ZMco(TyO)5)2o9MF0XDj(C52G?B3n>t#EGaQ%rLSrkD7RRp$ZU)t8XpZ4xc($(d z=y%>69QAm-(07SordNY(Dae0!(Y!}$mPDi36zC|{Vfu4m2LNke4$b06P-8Zp0FP+M zR|)lSubn(X2sfIYrK7}~=mHnEIH%E)PXY*}=Y!1R@NTI~cx)X9ezg;6Q8lzFH%h}Y zL$4B`nBOSV^%F=(p9y2g!+_o9=x726G0?9nI&fWJDam4_c}+pzlIMn|sU1xZO<>>l zn#qk5gexi+vr>ihWpe77L;I8g3MbSsUE~6GwQ5zHDxdzuD`NlHN5Kk|`01YxYpUkr zo3oIcV5D#wMWv^h<(R);2oT{)tPJwD0hiDw0uv;LCUSsS#u6{5vib29#$L8|hZOgZ z6av@q@C0k9s~S^p+u?fw`LT%>qRC0iL0+vV4BT<6?CH3iDa8muz`@+ZC9Sf1Jsy`- zOhw8mHS(mSorLVvKpW|#bNz1vigXkgCh|n~T)b;m9wY=2k?0wmSGX9wy6xlqE{erc z8!vSE?I)=6AZ_RZZ2BAx><@sE0M@UU?DQ1m%tS2D_Xk`Ng;AMKl1L4p^K*fX>U4pJ zH4M5plxN5@-cQrh#n2|pu?+!kBg`cGxF+cOwyb^UM;E!c?Q0Z6SJMSQp4-HKHsk1o zwYM|;ad-?hz|k0Ir`9}PQ%nPae@_~ywj%YvC@H?7;gav>E~Be3x5+l;Kc<>T-~aPE z0QNILGm8Vr8FVLEfkzVgpwPmENlGGvzswnOI%ivN+abxs4E_8ooS^pvyN)(qF^RBw z&fWSY#?H7&qHO69G5RjoSmq?(n2IIYq0247iOYvcK6ma8eT}*Sn8F=Fi=B z>>f7Wvo-I2Z6%&TZBne~6*k4AvD``yfo%5nmtMCm3Hs$OD8_cCh&zm+N~@G% z@QE$sn^Al2_(RmG*dp$>&kN#0PK-D0+I6S(ZuZF~a%puHz_skZfb)0U#8gV@^Ri(I z-%YjG1392=o~$dK6{Wns}A zGS0|+v%?JtD}t2E=xV>1CVBto<#U#tW0{u=iB9Uqp-16m$bl%0Am;z#q&mxEO7nd( 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 =_#9T7D{XQn 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 Z{$H{khg=j Date: Fri, 22 May 2026 11:34:11 +0530 Subject: [PATCH 09/14] updated banner --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68caec832..fa07fea74 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- pyBegin — Python Beginner Projects + pyBegin — Python Beginner Projects

From 8b942bce88ef79bb874019983d5cb39c1aba1380 Mon Sep 17 00:00:00 2001 From: Mrinank-Bhowmick <77621953+Mrinank-Bhowmick@users.noreply.github.com> Date: Fri, 22 May 2026 12:31:52 +0530 Subject: [PATCH 10/14] Migrate Pyodide-runnable verdicts from READMEs to todo.json The per-project `## Pyodide-runnable` README section was noisy. Move every verdict into projects/todo.json as a structured registry ({name, status, reason}) with a status_values legend. Replaces the old todo.md checklist. gen-catalog.mjs no longer parses that README section; it derives the catalog `runnable` flag from whether a playground file exists. catalog.json is unchanged (270 projects, 59 runnable). Co-Authored-By: Claude Opus 4.7 --- projects/todo.json | 311 ++++++++++++++++++++++++++++++++++++ projects/todo.md | 293 --------------------------------- web/scripts/gen-catalog.mjs | 27 +--- web/src/lib/catalog.json | 38 ++--- 4 files changed, 337 insertions(+), 332 deletions(-) create mode 100644 projects/todo.json delete mode 100644 projects/todo.md diff --git a/projects/todo.json b/projects/todo.json new file mode 100644 index 000000000..24c762d9c --- /dev/null +++ b/projects/todo.json @@ -0,0 +1,311 @@ +{ + "title": "Projects TODO", + "description": "Working checklist for processing every project folder. The Pyodide-runnable verdict for each project lives here (status + reason) instead of in the individual README files.", + "task_per_project": [ + "Read the project — understand what it does and what it depends on.", + "Pyodide assessment — decide if it can run in the browser playground.", + "If it can run as-is or with small modifications, modify it for Pyodide compatibility.", + "If it cannot run in Pyodide, leave the code unchanged.", + "Add README.md to the folder with project description, dependencies, and run instructions.", + "Record the Pyodide verdict (status + reason) in the `projects` list below." + ], + "status_values": { + "runnable": "Runs as-is in the in-browser Pyodide playground.", + "modified": "Runs in Pyodide after a small edit (e.g. removing an os.system clear).", + "partial": "Some scripts in the folder run in Pyodide, others do not.", + "desktop-only": "Cannot run in Pyodide — needs a GUI, network, hardware or filesystem.", + "placeholder": "Folder has no runnable Python code yet." + }, + "projects": [ + { "name": "Adjactive_compartive_superlative", "status": "desktop-only", "reason": "nltk WordNet corpus download" }, + { "name": "Advisor", "status": "desktop-only", "reason": "tkinter + HTTP request" }, + { "name": "AES256", "status": "modified", "reason": "removed os.system clear; uses pycryptodome" }, + { "name": "Alarm Clock", "status": "desktop-only", "reason": "tkinter + pygame audio + threads" }, + { "name": "Amazon Product Availbility Checker", "status": "desktop-only", "reason": "HTTP scraping of Amazon" }, + { "name": "AnalogClock", "status": "desktop-only", "reason": "tkinter canvas window" }, + { "name": "API Based Weather Report", "status": "desktop-only", "reason": "OpenWeatherMap HTTP API" }, + { "name": "Audio Converter", "status": "desktop-only", "reason": "pydub + disk audio files" }, + { "name": "AudioAPI", "status": "desktop-only", "reason": "Flask server + Whisper" }, + { "name": "Audiobook", "status": "desktop-only", "reason": "pyttsx3 + keyboard + PDF files" }, + { "name": "AudioRecorder", "status": "desktop-only", "reason": "microphone capture + disk files" }, + { "name": "AutoGui", "status": "desktop-only", "reason": "pyautogui mouse/keyboard control" }, + { "name": "AWS_s3_upload", "status": "desktop-only", "reason": "boto3 AWS S3 network calls" }, + { "name": "Battleship", "status": "modified", "reason": "removed os.system clear; stdlib console game" }, + { "name": "BFS visualizer", "status": "desktop-only", "reason": "curses terminal UI" }, + { "name": "Bigram_Autocomplete", "status": "runnable", "reason": "stdlib console (re + input/print)" }, + { "name": "Bitcoin Mining", "status": "runnable", "reason": "stdlib only (hashlib, time)" }, + { "name": "bittorrent-downloader", "status": "desktop-only", "reason": "IMAP + subprocess + Twilio API" }, + { "name": "BlackJack", "status": "runnable", "reason": "stdlib console card game" }, + { "name": "Blender_tools", "status": "desktop-only", "reason": "Blender bpy module" }, + { "name": "Blind Auction", "status": "runnable", "reason": "stdlib console (local art.py module)" }, + { "name": "Blind_Auction", "status": "modified", "reason": "removed unused distutils import; stdlib console" }, + { "name": "BMI WebApp", "status": "desktop-only", "reason": "PyWebIO web server" }, + { "name": "BMI_calculator", "status": "desktop-only", "reason": "tabulate package + CSV file" }, + { "name": "Book Data Extractor", "status": "desktop-only", "reason": "requests scraping + openpyxl" }, + { "name": "Browser", "status": "desktop-only", "reason": "PyQt5 + QtWebEngine GUI" }, + { "name": "Budget-manager", "status": "desktop-only", "reason": "tkinter GUI + SQLite file" }, + { "name": "caesar_cipher", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "Calculate Age", "status": "runnable", "reason": "stdlib console (calendar/time)" }, + { "name": "Calculator", "status": "modified", "reason": "removed os.system clear; stdlib console" }, + { "name": "Calculator-GUI-With-Python", "status": "desktop-only", "reason": "Kivy GUI" }, + { "name": "Calendar", "status": "runnable", "reason": "stdlib console (calendar module)" }, + { "name": "Captcha_Genrator", "status": "desktop-only", "reason": "tkinter + pygame GUI" }, + { "name": "car game", "status": "desktop-only", "reason": "pygame window + audio" }, + { "name": "Card Game", "status": "runnable", "reason": "stdlib console (random, re)" }, + { "name": "career-guide-bot", "status": "placeholder", "reason": "source file is empty" }, + { "name": "character-picture-grid", "status": "runnable", "reason": "stdlib, prints only" }, + { "name": "Chat Application", "status": "desktop-only", "reason": "TCP sockets + threading" }, + { "name": "Chat-GPT-Discord-Bot", "status": "desktop-only", "reason": "Discord bot + OpenAI API" }, + { "name": "Chess", "status": "desktop-only", "reason": "pygame window + image assets" }, + { "name": "chore-assignment-emailer", "status": "desktop-only", "reason": "smtplib SMTP to Gmail" }, + { "name": "Code_Reviewer", "status": "desktop-only", "reason": "reads source file + pycodestyle" }, + { "name": "Coin Flip", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "Collatz_Conjecture", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "Comics_Scraper", "status": "desktop-only", "reason": "scrapes xkcd + writes images" }, + { "name": "comma-code", "status": "runnable", "reason": "stdlib, strings + print" }, + { "name": "computer-algebra", "status": "desktop-only", "reason": "dearpygui + Newton API" }, + { "name": "Connect Four", "status": "desktop-only", "reason": "pygame window + mouse input" }, + { "name": "Conway’s-Game-Of-Life", "status": "desktop-only", "reason": "matplotlib animation + argparse" }, + { "name": "Countdown", "status": "runnable", "reason": "stdlib (time); sleep blocks in browser" }, + { "name": "CRUD-with-postgresql", "status": "desktop-only", "reason": "PostgreSQL via psycopg2" }, + { "name": "currency converter", "status": "desktop-only", "reason": "tkinter GUI + HTTP rates" }, + { "name": "custom-invitations", "status": "desktop-only", "reason": "python-docx writes .docx file" }, + { "name": "custom-seating-cards", "status": "desktop-only", "reason": "reads/writes image assets" }, + { "name": "CustomEncryptionDecryption", "status": "desktop-only", "reason": "PyJWT + dotenv + cipher file" }, + { "name": "Data Entry Automation", "status": "desktop-only", "reason": "selenium Chrome + requests" }, + { "name": "Data_Abstractor", "status": "desktop-only", "reason": "Flask web server" }, + { "name": "Desktop Weather Notifier", "status": "desktop-only", "reason": "weather API + plyer notifications" }, + { "name": "Desktop-Notification", "status": "desktop-only", "reason": "win10toast Windows notifications" }, + { "name": "Diabetes Monitoring Dashboard", "status": "desktop-only", "reason": "Streamlit + scikit-learn + OpenAI" }, + { "name": "Dice Simulator", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "Dice-Roll-Simulator", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "dictionary.com-scraper", "status": "desktop-only", "reason": "scrapes site + gtts + browser" }, + { "name": "Discord Bot", "status": "desktop-only", "reason": "Discord gateway bot" }, + { "name": "DnD Dice", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "Drowsiness Detector", "status": "desktop-only", "reason": "dlib + OpenCV + webcam" }, + { "name": "duplicate_search", "status": "desktop-only", "reason": "walks/deletes real files" }, + { "name": "Encryptor and Decryptor", "status": "desktop-only", "reason": "tkinter GUI" }, + { "name": "English Thesaurus", "status": "desktop-only", "reason": "loads data.json by disk path" }, + { "name": "excel-to-csv-converter", "status": "desktop-only", "reason": "scans disk + writes CSV" }, + { "name": "Expense-Tracker", "status": "desktop-only", "reason": "customtkinter GUI + reportlab/pandas" }, + { "name": "Eye Blink Detection", "status": "desktop-only", "reason": "dlib + OpenCV + webcam" }, + { "name": "facebook_video_downloader", "status": "desktop-only", "reason": "requests + wget downloads" }, + { "name": "facerecoginition", "status": "desktop-only", "reason": "OpenCV webcam + GUI windows" }, + { "name": "fantasy-game-inventory", "status": "runnable", "reason": "pure Python, prints only" }, + { "name": "Fidget Spinner Game", "status": "desktop-only", "reason": "turtle (Tkinter) graphics" }, + { "name": "File_Organizer", "status": "desktop-only", "reason": "walks/moves real files" }, + { "name": "fill-gaps", "status": "desktop-only", "reason": "renames/moves real files" }, + { "name": "Find_imbd_rating", "status": "desktop-only", "reason": "IMDb HTTP + directory listing" }, + { "name": "find-unneeded-files", "status": "desktop-only", "reason": "walks real filesystem" }, + { "name": "Flappybird_game", "status": "desktop-only", "reason": "pygame graphics + sound" }, + { "name": "Full_Calendar", "status": "desktop-only", "reason": "tkinter GUI" }, + { "name": "Game of Cricket", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "game-snake_water_gun", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "goodreads-quotes-scraper", "status": "desktop-only", "reason": "network fetch + Selenium" }, + { "name": "GPA-Calculator", "status": "runnable", "reason": "stdlib console (math)" }, + { "name": "Gpt-And-Langchain", "status": "desktop-only", "reason": "Telegram bot + OpenAI/Notion/SerpAPI" }, + { "name": "Gradient-Image", "status": "desktop-only", "reason": "OpenCV GUI windows" }, + { "name": "Guess Number", "status": "partial", "reason": "guess_number.py runs; v2 needs tkinter" }, + { "name": "Guess The Word", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "GUI based image display and transfer in python", "status": "desktop-only", "reason": "tkinter file dialogs + disk copy" }, + { "name": "HandCricket", "status": "runnable", "reason": "stdlib console (random, time)" }, + { "name": "HandTrack", "status": "desktop-only", "reason": "webcam + OpenCV + MediaPipe" }, + { "name": "Hangman", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "healthmanagementsystem", "status": "desktop-only", "reason": "reads/appends real files" }, + { "name": "Higher-Lower", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "hill_cipher", "status": "desktop-only", "reason": "MySQL via mysql.connector" }, + { "name": "Historical Data Breaches", "status": "desktop-only", "reason": "HaveIBeenPwned API" }, + { "name": "Image compressor", "status": "desktop-only", "reason": "image files + tkinter.filedialog" }, + { "name": "Image Manipulation", "status": "desktop-only", "reason": "reads/writes image files" }, + { "name": "Image Sketcher", "status": "desktop-only", "reason": "OpenCV webcam + GUI" }, + { "name": "Image to PDF Converter", "status": "desktop-only", "reason": "reads images + writes PDF" }, + { "name": "Image to text generation project", "status": "desktop-only", "reason": "PyTorch + Transformers notebook" }, + { "name": "Image Toonification", "status": "desktop-only", "reason": "OpenCV image files" }, + { "name": "image-site-downloader", "status": "desktop-only", "reason": "HTTP requests + disk writes" }, + { "name": "Image-to-art", "status": "desktop-only", "reason": "Pillow + disk image files" }, + { "name": "Image-Upscale", "status": "desktop-only", "reason": "PyTorch + local images" }, + { "name": "ImagegenChatbot", "status": "desktop-only", "reason": "OpenAI API + image downloads" }, + { "name": "indeed-scraper", "status": "desktop-only", "reason": "HTTP scraping of Indeed" }, + { "name": "Instagram Post Creation", "status": "desktop-only", "reason": "Pillow + disk image files" }, + { "name": "instant-messenger-bot", "status": "desktop-only", "reason": "pyautogui keyboard/mouse" }, + { "name": "Internet Speed Tester", "status": "desktop-only", "reason": "speedtest network measurement" }, + { "name": "Internet-speed-test", "status": "desktop-only", "reason": "speedtest network measurement" }, + { "name": "inventory management", "status": "desktop-only", "reason": "tkinter GUI + MySQL" }, + { "name": "Inverse Matrix Calculator", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "IP Blacklist Checker", "status": "desktop-only", "reason": "blacklist HTTP API" }, + { "name": "IPv4_Calculator-main", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "JARVIS.PY", "status": "desktop-only", "reason": "microphone + TTS + browser control + filesystem" }, + { "name": "Jokenpo", "status": "runnable", "reason": "stdlib console game" }, + { "name": "Jokey", "status": "desktop-only", "reason": "JokeAPI HTTP requests" }, + { "name": "KbdXylo", "status": "desktop-only", "reason": "pynput key capture + audio" }, + { "name": "Language_learning_assistant", "status": "desktop-only", "reason": "speech_recognition microphone" }, + { "name": "Language-Translate", "status": "desktop-only", "reason": "translate library HTTP service" }, + { "name": "link-verification", "status": "desktop-only", "reason": "HTTP link checking" }, + { "name": "linkedin-scrape", "status": "desktop-only", "reason": "Selenium browser scraping" }, + { "name": "Live AQI", "status": "desktop-only", "reason": "tkinter GUI + AirVisual API" }, + { "name": "Loan Calculator", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "Location Search App (GUI)", "status": "desktop-only", "reason": "tkinter GUI + web browser" }, + { "name": "looking-busy", "status": "desktop-only", "reason": "pyautogui mouse control" }, + { "name": "love-calculator", "status": "runnable", "reason": "stdlib console (random, time)" }, + { "name": "Lyrics-Extractor", "status": "desktop-only", "reason": "Google Custom Search API" }, + { "name": "Madlibs Generator", "status": "desktop-only", "reason": "tkinter GUI" }, + { "name": "Make-API", "status": "desktop-only", "reason": "Flask web server" }, + { "name": "Market Financial Sentiment Predictor", "status": "desktop-only", "reason": "scikit-learn + CSV dataset" }, + { "name": "Mastermind", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "maths", "status": "runnable", "reason": "stdlib console (cmath)" }, + { "name": "Medium Article Reader", "status": "desktop-only", "reason": "HTTP/API + pyttsx3 TTS" }, + { "name": "Merge PDFs", "status": "desktop-only", "reason": "filesystem + pikepdf" }, + { "name": "Message-Spam", "status": "desktop-only", "reason": "pyautogui + WhatsApp Web" }, + { "name": "Minecraft-in-Python-main", "status": "desktop-only", "reason": "ursina 3D engine (OpenGL)" }, + { "name": "Mineral Processing Technology-Image Analytics", "status": "desktop-only", "reason": "OpenCV + image files" }, + { "name": "minesweeper", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "ML-Notebooks_Beginners", "status": "desktop-only", "reason": "Jupyter notebook document" }, + { "name": "Mobile Document Scanner", "status": "desktop-only", "reason": "OpenCV GUI windows" }, + { "name": "Mongo CRUD", "status": "desktop-only", "reason": "MongoDB network connection" }, + { "name": "Morse-Code-Translator", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "MorseCode Translator", "status": "runnable", "reason": "stdlib, hard-coded input + print" }, + { "name": "MotivateBot", "status": "desktop-only", "reason": "OpenAI API requests" }, + { "name": "Movie recommendation", "status": "desktop-only", "reason": "scikit-learn" }, + { "name": "movie-rater", "status": "desktop-only", "reason": "FastAPI server + TMDB API" }, + { "name": "MovieApi_and_ML", "status": "desktop-only", "reason": "Django server + scikit-learn" }, + { "name": "MQTT Client", "status": "desktop-only", "reason": "external MQTT broker connection" }, + { "name": "multiplayer_socket", "status": "desktop-only", "reason": "TCP sockets + threading" }, + { "name": "Music Player", "status": "desktop-only", "reason": "tkinter GUI + pygame.mixer" }, + { "name": "NASA-APOD", "status": "desktop-only", "reason": "NASA APOD API + wget" }, + { "name": "Neurons", "status": "modified", "reason": "removed os.system clear; stdlib prints" }, + { "name": "Number Guessing App", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "Online Trivia", "status": "desktop-only", "reason": "opentdb.com API" }, + { "name": "OpenCV_color_detect_in_live_feed", "status": "desktop-only", "reason": "OpenCV webcam + GUI" }, + { "name": "Organize_Directory", "status": "desktop-only", "reason": "walks/moves real files" }, + { "name": "Othello", "status": "desktop-only", "reason": "pygame window + assets" }, + { "name": "Otp_Generator", "status": "runnable", "reason": "stdlib, prints generated OTP" }, + { "name": "OTP-Verfication-System", "status": "desktop-only", "reason": "Gmail SMTP connection" }, + { "name": "Password Projects", "status": "partial", "reason": "console sub-tools run; GUI/network ones don't" }, + { "name": "Password Projects/Password Breach Frequency", "status": "desktop-only", "reason": "HaveIBeenPwned API" }, + { "name": "Password Projects/Password Generator", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "Password Projects/Password Hashing", "status": "runnable", "reason": "stdlib, password via argv" }, + { "name": "Password Projects/Password Meter", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "Password Projects/Password_manager", "status": "desktop-only", "reason": "tkinter GUI + JSON file" }, + { "name": "Password Projects/strong-password-detector", "status": "runnable", "reason": "stdlib, hard-coded input + print" }, + { "name": "Password Projects/WiFi Password Generator", "status": "desktop-only", "reason": "Windows netsh via subprocess" }, + { "name": "PDF_Reader", "status": "desktop-only", "reason": "OpenAI API + LangChain/Chroma" }, + { "name": "pdf_to_text", "status": "desktop-only", "reason": "PyQt5 GUI + PDF files" }, + { "name": "pdf-paranoia", "status": "desktop-only", "reason": "walks filesystem + PDF files" }, + { "name": "Personal-Finance-tracker", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "Pig_latin", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "ping_pong", "status": "desktop-only", "reason": "turtle graphics + keyboard" }, + { "name": "Pokemon Battle", "status": "runnable", "reason": "numpy console game (numpy in Pyodide)" }, + { "name": "PONG", "status": "desktop-only", "reason": "pygame window + sound + assets" }, + { "name": "Port_Scaner", "status": "desktop-only", "reason": "real TCP sockets" }, + { "name": "prettified-stopwatch", "status": "desktop-only", "reason": "pyperclip clipboard access" }, + { "name": "Print_Colored_Text", "status": "desktop-only", "reason": "colorama not bundled with Pyodide" }, + { "name": "ProjectEuler", "status": "runnable", "reason": "stdlib math, prints results" }, + { "name": "proxy-scrapper", "status": "desktop-only", "reason": "requests fetches live site" }, + { "name": "Python Banking System", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "Python story generator", "status": "desktop-only", "reason": "tkinter + ttkthemes GUI" }, + { "name": "qr-code-generator-with-window-and-simple-ui", "status": "desktop-only", "reason": "tkinter GUI + disk image" }, + { "name": "QRCode Scanner", "status": "desktop-only", "reason": "OpenCV webcam + pyzbar" }, + { "name": "QRCode-Generator", "status": "desktop-only", "reason": "qrcode/pillow + disk image" }, + { "name": "Qt5_YouTube", "status": "desktop-only", "reason": "PyQt5 GUI + youtube_dl" }, + { "name": "QuickWordCloud", "status": "desktop-only", "reason": "wordcloud + matplotlib window" }, + { "name": "Quiz Game", "status": "runnable", "reason": "stdlib console (sqlite3 in-memory)" }, + { "name": "Random-Quote-Generator", "status": "desktop-only", "reason": "tkinter GUI + requests API" }, + { "name": "reciept generator", "status": "desktop-only", "reason": "fpdf PDF rendering" }, + { "name": "reddit-scraper", "status": "desktop-only", "reason": "requests fetches Reddit" }, + { "name": "regex-search", "status": "desktop-only", "reason": "walks filesystem for .txt files" }, + { "name": "regex-strip", "status": "runnable", "reason": "stdlib (re + print)" }, + { "name": "Rename_Images", "status": "desktop-only", "reason": "walks/renames real files" }, + { "name": "Resize_Image", "status": "desktop-only", "reason": "pillow + image files" }, + { "name": "RestrauntAPI", "status": "desktop-only", "reason": "Django server + database" }, + { "name": "reuters-scraper", "status": "desktop-only", "reason": "urllib fetches live site" }, + { "name": "Rock_Paper_Scissors", "status": "runnable", "reason": "stdlib console (random)" }, + { "name": "Roll_A_dice", "status": "desktop-only", "reason": "MySQL database server" }, + { "name": "Rubik-tracking", "status": "desktop-only", "reason": "webcam + OpenCV windows" }, + { "name": "Sales Optimizer", "status": "desktop-only", "reason": "seaborn/scikit-learn/matplotlib" }, + { "name": "scheduledShutdown", "status": "desktop-only", "reason": "os.system shutdown command" }, + { "name": "Scientific-Calculator", "status": "desktop-only", "reason": "tkinter GUI" }, + { "name": "scrap-ycombinator", "status": "desktop-only", "reason": "requests fetches live site" }, + { "name": "ScreenRecorder", "status": "desktop-only", "reason": "pyautogui screen + win32api" }, + { "name": "Seek_with_hand_track", "status": "desktop-only", "reason": "webcam + MediaPipe + pyautogui" }, + { "name": "Selfie_with_Python", "status": "desktop-only", "reason": "webcam + OpenCV windows" }, + { "name": "Send-Email", "status": "desktop-only", "reason": "Gmail SMTP connection" }, + { "name": "servo motor classifier", "status": "desktop-only", "reason": "CSV over network + matplotlib" }, + { "name": "Simple-Plagiarism-Checker-Project", "status": "desktop-only", "reason": "Flask web server" }, + { "name": "SketchifyMe", "status": "desktop-only", "reason": "OpenCV GUI + disk image" }, + { "name": "Skycast", "status": "desktop-only", "reason": "Streamlit + Weatherbit API" }, + { "name": "Slice-Audio", "status": "desktop-only", "reason": "pydub + ffmpeg + mp3 files" }, + { "name": "SMS_ChatBot", "status": "desktop-only", "reason": "Flask + Twilio + OpenAI" }, + { "name": "Snake Game", "status": "desktop-only", "reason": "pygame graphics + keyboard" }, + { "name": "snake_water_gun_game", "status": "desktop-only", "reason": "rich package not bundled" }, + { "name": "Social_media_content_creation", "status": "placeholder", "reason": "folder has no Python code" }, + { "name": "Socket", "status": "desktop-only", "reason": "real TCP sockets" }, + { "name": "SongsMashup", "status": "desktop-only", "reason": "Flask + YouTube + pydub + SMTP" }, + { "name": "Space Shooter", "status": "desktop-only", "reason": "pygame graphics + assets" }, + { "name": "space_battle", "status": "desktop-only", "reason": "pygame graphics + audio + keyboard" }, + { "name": "Speed-Type-test", "status": "desktop-only", "reason": "pygame graphics + assets" }, + { "name": "Split_Tip", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "sponge-bob", "status": "desktop-only", "reason": "turtle graphics window" }, + { "name": "Spotify Player", "status": "desktop-only", "reason": "Spotify/Twilio APIs + browser" }, + { "name": "Stock-Market-Dashboard", "status": "desktop-only", "reason": "Streamlit + network data" }, + { "name": "student-management-system", "status": "desktop-only", "reason": "MySQL via mysql.connector" }, + { "name": "Subnetting Flsm", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "Subtitle_synchronizer.py", "status": "desktop-only", "reason": "reads/writes .srt files on disk" }, + { "name": "Sudoku_solver", "status": "runnable", "reason": "stdlib console (random + print)" }, + { "name": "Sudoku-Solver", "status": "desktop-only", "reason": "pygame graphics + assets" }, + { "name": "takeImage", "status": "desktop-only", "reason": "OpenCV webcam + GUI windows" }, + { "name": "TennisTournamentSim", "status": "runnable", "reason": "stdlib console (random, time)" }, + { "name": "Tesla", "status": "desktop-only", "reason": "pygame graphics + image assets" }, + { "name": "Tetris Game", "status": "desktop-only", "reason": "pygame graphics + keyboard" }, + { "name": "Text Editor", "status": "desktop-only", "reason": "tkinter GUI" }, + { "name": "Text Summarizer", "status": "desktop-only", "reason": "spaCy + large language model" }, + { "name": "Text to Speech", "status": "desktop-only", "reason": "gTTS network API + mp3 file" }, + { "name": "Text_to_SpreadSheet", "status": "desktop-only", "reason": "walks disk + writes .xlsx" }, + { "name": "Text-to-Image Generation Project", "status": "desktop-only", "reason": "deep-learning notebook" }, + { "name": "text-translate", "status": "desktop-only", "reason": "translate network + tkinter/gtts" }, + { "name": "TextDetection", "status": "desktop-only", "reason": "Google Cloud Vision API" }, + { "name": "Tic-Tac-Toe", "status": "partial", "reason": "terminal version runs; tkinter GUI doesn't" }, + { "name": "Tic-Tac-Toe/Tic-Tac-Toe-Terminal", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "Tic-Tac-Toe/TicTacToe-GUI", "status": "desktop-only", "reason": "pygame window" }, + { "name": "TicTacToe-TylerPear", "status": "runnable", "reason": "stdlib console (input/print)" }, + { "name": "Tile Matching", "status": "desktop-only", "reason": "tkinter GUI" }, + { "name": "Timer", "status": "runnable", "reason": "stdlib console; sleep blocks tab" }, + { "name": "Tkinter", "status": "desktop-only", "reason": "tkinter GUI" }, + { "name": "ToDoList", "status": "desktop-only", "reason": "telebot Telegram bot" }, + { "name": "Turtle Pattern", "status": "desktop-only", "reason": "turtle graphics window" }, + { "name": "Turtle_Graphics", "status": "desktop-only", "reason": "turtle graphics window" }, + { "name": "Twitter-Bot", "status": "desktop-only", "reason": "Telegram API bot" }, + { "name": "Type Racer Game", "status": "desktop-only", "reason": "tkinter GUI" }, + { "name": "url_shortener", "status": "desktop-only", "reason": "URL-shortener API + pywin32" }, + { "name": "Video Reversal", "status": "desktop-only", "reason": "OpenCV video + desktop window" }, + { "name": "video_transcoder_project", "status": "desktop-only", "reason": "Flask server + FFmpeg" }, + { "name": "Video-subtitle-generator", "status": "desktop-only", "reason": "Whisper + FFmpeg on local files" }, + { "name": "Voice-to-Text", "status": "desktop-only", "reason": "microphone + network speech recognition" }, + { "name": "Watermarker", "status": "desktop-only", "reason": "Pillow + matplotlib + filesystem" }, + { "name": "Weather", "status": "desktop-only", "reason": "requests live web API" }, + { "name": "Web Scraping Jujustu Kaisen Manga", "status": "desktop-only", "reason": "Selenium + requests scraping" }, + { "name": "web-crawler(movie extract)", "status": "desktop-only", "reason": "requests scraping" }, + { "name": "WebButtonSimpelGUI", "status": "desktop-only", "reason": "tkinter + webbrowser" }, + { "name": "Website Blocker", "status": "desktop-only", "reason": "modifies OS hosts file" }, + { "name": "Weights_in_different_planets_GUI", "status": "desktop-only", "reason": "tkinter GUI" }, + { "name": "what-for-dinner", "status": "desktop-only", "reason": "TheMealDB live API" }, + { "name": "Windows Logo", "status": "desktop-only", "reason": "turtle graphics window" }, + { "name": "Wine_quality_predictor", "status": "desktop-only", "reason": "pandas/scikit-learn + CSV file" }, + { "name": "Word_Predictor", "status": "desktop-only", "reason": "reads local Chats.txt file" }, + { "name": "Worksheet_to_text", "status": "desktop-only", "reason": "reads local worksheet.xlsx" }, + { "name": "World-Cup-Player-Comparison", "status": "desktop-only", "reason": "requests scraping" }, + { "name": "xls_to_xlsx", "status": "desktop-only", "reason": "pywin32 automates Excel" }, + { "name": "YouTube Video Downloader", "status": "desktop-only", "reason": "yt-dlp internet download" } + ], + "notes": { + "pyodide_definition": "Pyodide = CPython compiled to WebAssembly for browser execution.", + "pyodide_supported": ["Pure stdlib console apps", "input()", "print()"], + "pyodide_not_supported": [ + "tkinter", + "pygame", + "OpenCV webcam", + "network-heavy apps", + "filesystem-heavy apps", + "hardware-dependent apps" + ] + } +} diff --git a/projects/todo.md b/projects/todo.md deleted file mode 100644 index 45343175b..000000000 --- a/projects/todo.md +++ /dev/null @@ -1,293 +0,0 @@ -# Projects TODO - -Working checklist for processing every project folder, one at a time. - -## Task per project - -For each folder below: - -1. **Read the project** — understand what it does and what it depends on. -2. **Pyodide assessment** — decide if it can run in the browser playground - (Pyodide = CPython in WebAssembly: pure-stdlib console code works; no GUI, - no sockets, no threads-heavy or hardware/file-system-dependent code). - - If it **can run as-is or with small modifications** → make those edits so it - runs under Pyodide (keep behavior the same; prefer stdlib `input()`/`print()`). - - If it **cannot** (pygame, tkinter, OpenCV, network, OS automation, etc.) → - leave the code unchanged. -3. **Add `README.md`** to the folder — what the project does, how to run it, - dependencies, and whether it's Pyodide-runnable. -4. Tick the box here and note the outcome (`pyodide` / `modified` / `desktop-only`). - -> Process in order. Do not modify unrelated projects in the same change. - -## Checklist - -- [x] Adjactive_compartive_superlative — desktop-only (nltk corpus download) -- [x] Advisor — desktop-only (tkinter + network) -- [x] AES256 — modified (removed os.system clear; pyodide via pycryptodome) -- [x] Alarm Clock — desktop-only (tkinter + pygame + threads) -- [x] Amazon Product Availbility Checker — desktop-only (network scraping) -- [x] AnalogClock — desktop-only (tkinter canvas) -- [x] API Based Weather Report — desktop-only (OpenWeather HTTP API) -- [x] Audio Converter — desktop-only (pydub + disk files) -- [x] AudioAPI — desktop-only (Flask + Whisper) -- [x] Audiobook — desktop-only (pyttsx3 + PDFs) -- [x] AudioRecorder — desktop-only (microphone) -- [x] AutoGui — desktop-only (pyautogui) -- [x] AWS_s3_upload — desktop-only (boto3/AWS) -- [x] Battleship — modified (removed os.system clears; pyodide) -- [x] BFS visualizer — desktop-only (curses UI) -- [x] Bigram_Autocomplete — pyodide -- [x] Bitcoin Mining — pyodide -- [x] bittorrent-downloader — desktop-only (IMAP/subprocess/Twilio) -- [x] BlackJack — pyodide -- [x] Blender_tools — desktop-only (Blender bpy) -- [x] Blind Auction — pyodide -- [x] Blind_Auction — modified (removed unused distutils; pyodide) -- [x] BMI WebApp — desktop-only (PyWebIO) -- [x] BMI_calculator — desktop-only (tabulate + bmi.csv) -- [x] Book Data Extractor — desktop-only (scraping + xlsx) -- [x] Browser — desktop-only (PyQt5 WebEngine) -- [x] Budget-manager — desktop-only (tkinter + SQLite) -- [x] caesar_cipher — pyodide -- [x] Calculate Age — pyodide -- [x] Calculator — modified (removed os.system clears; pyodide) -- [x] Calculator-GUI-With-Python — desktop-only (Kivy) -- [x] Calendar — pyodide -- [x] Captcha_Genrator — desktop-only (tkinter) -- [x] car game — desktop-only (pygame) -- [x] Card Game — pyodide -- [x] career-guide-bot — desktop-only (source file empty) -- [x] character-picture-grid — pyodide -- [x] Chat Application — desktop-only (TCP sockets) -- [x] Chat-GPT-Discord-Bot — desktop-only (Discord + OpenAI) -- [x] Chess — desktop-only (pygame) -- [x] chore-assignment-emailer — desktop-only (smtplib) -- [x] Code_Reviewer — desktop-only (disk files + pycodestyle) -- [x] Coin Flip — modified (removed os.system clear; pyodide) -- [x] Collatz_Conjecture — pyodide -- [x] Comics_Scraper — desktop-only (xkcd scrape + files) -- [x] comma-code — pyodide -- [x] computer-algebra — desktop-only (dearpygui + API) -- [x] Connect Four — desktop-only (pygame) -- [x] Conway’s-Game-Of-Life — desktop-only (matplotlib animation) -- [x] Countdown — pyodide -- [x] CRUD-with-postgresql — desktop-only (PostgreSQL) -- [x] currency converter — desktop-only (tkinter + API) -- [x] CustomEncryptionDecryption — desktop-only (PyJWT + env/files) -- [x] custom-invitations — desktop-only (python-docx) -- [x] custom-seating-cards — desktop-only (Pillow assets) -- [x] Data Entry Automation — desktop-only (selenium) -- [x] Data_Abstractor — desktop-only (Flask server) -- [x] Desktop Weather Notifier — desktop-only (API + OS notifications) -- [x] Desktop-Notification — desktop-only (win10toast) -- [x] Diabetes Monitoring Dashboard — desktop-only (Streamlit + ML) -- [x] Dice Simulator — pyodide -- [x] Dice-Roll-Simulator — pyodide -- [x] dictionary.com-scraper — desktop-only (scrape + gtts) -- [x] Discord Bot — desktop-only (Discord gateway) -- [x] DnD Dice — pyodide -- [x] Drowsiness Detector — desktop-only (dlib/OpenCV/webcam) -- [x] duplicate_search — desktop-only (filesystem) -- [x] Encryptor and Decryptor — desktop-only (tkinter) -- [x] English Thesaurus — desktop-only (relative-path data.json) -- [x] excel-to-csv-converter — desktop-only (disk scan + openpyxl) -- [x] Expense-Tracker — desktop-only (tkinter + reportlab) -- [x] Eye Blink Detection — desktop-only (dlib/OpenCV/webcam) -- [x] facebook_video_downloader — desktop-only (network + downloads) -- [x] facerecoginition — desktop-only (OpenCV webcam) -- [x] fantasy-game-inventory — pyodide -- [x] Fidget Spinner Game — desktop-only (turtle) -- [x] File_Organizer — desktop-only (filesystem) -- [x] fill-gaps — desktop-only (filesystem) -- [x] Find_imbd_rating — desktop-only (IMDb scraping) -- [x] find-unneeded-files — desktop-only (filesystem walk) -- [x] Flappybird_game — desktop-only (pygame) -- [x] Full_Calendar — desktop-only (tkinter) -- [x] Game of Cricket — pyodide -- [x] game-snake_water_gun — pyodide -- [x] goodreads-quotes-scraper — desktop-only (Selenium scraping) -- [x] GPA-Calculator — pyodide -- [x] Gpt-And-Langchain — desktop-only (Telegram bot + APIs) -- [x] Gradient-Image — desktop-only (OpenCV GUI) -- [x] Guess Number — pyodide (v2 is tkinter) -- [x] Guess The Word — pyodide -- [x] GUI based image display and transfer in python — desktop-only (tkinter) -- [x] HandCricket — pyodide -- [x] HandTrack — desktop-only (webcam + MediaPipe) -- [x] Hangman — pyodide -- [x] healthmanagementsystem — desktop-only (disk files) -- [x] Higher-Lower — pyodide -- [x] hill_cipher — desktop-only (MySQL) -- [x] Historical Data Breaches — desktop-only (HIBP API) -- [x] Image compressor — desktop-only (Pillow + filedialog) -- [x] Image Manipulation — desktop-only (Pillow + disk) -- [x] Image Sketcher — desktop-only (OpenCV webcam) -- [x] Image to PDF Converter — desktop-only (disk I/O) -- [x] Image to text generation project — desktop-only (PyTorch notebook) -- [x] Image Toonification — desktop-only (OpenCV + disk) -- [x] ImagegenChatbot — desktop-only (OpenAI API) -- [x] image-site-downloader — desktop-only (live HTTP) -- [x] Image-to-art — desktop-only (Pillow + image files) -- [x] Image-Upscale — desktop-only (PyTorch + image files) -- [x] indeed-scraper — desktop-only (HTTP scraping) -- [x] Instagram Post Creation — desktop-only (Pillow + image I/O) -- [x] instant-messenger-bot — desktop-only (pyautogui automation) -- [x] Internet Speed Tester — desktop-only (network measurement) -- [x] Internet-speed-test — desktop-only (network measurement) -- [x] inventory management — desktop-only (tkinter + MySQL) -- [x] Inverse Matrix Calculator — pyodide -- [x] IP Blacklist Checker — desktop-only (live API) -- [x] IPv4_Calculator-main — pyodide -- [x] JARVIS.PY — desktop-only (mic/TTS/browser) -- [x] Jokenpo — pyodide -- [x] Jokey — desktop-only (JokeAPI HTTP) -- [x] KbdXylo — desktop-only (pynput + audio) -- [x] Language_learning_assistant — desktop-only (mic) -- [x] Language-Translate — desktop-only (HTTP) -- [x] linkedin-scrape — desktop-only (Selenium) -- [x] link-verification — desktop-only (HTTP) -- [x] Live AQI — desktop-only (tkinter + API) -- [x] Loan Calculator — pyodide -- [x] Location Search App (GUI) — desktop-only (tkinter) -- [x] looking-busy — desktop-only (pyautogui) -- [x] love-calculator — pyodide -- [x] Lyrics-Extractor — desktop-only (Search API) -- [x] Madlibs Generator — desktop-only (tkinter) -- [x] Make-API — desktop-only (Flask server) -- [x] Market Financial Sentiment Predictor — desktop-only (sklearn + CSV) -- [x] Mastermind — pyodide -- [x] maths — pyodide -- [x] Medium Article Reader — desktop-only (HTTP/OpenAI + TTS) -- [x] Merge PDFs — desktop-only (filesystem + pikepdf) -- [x] Message-Spam — desktop-only (pyautogui + WhatsApp Web) -- [x] Minecraft-in-Python-main — desktop-only (ursina 3D engine) -- [x] Mineral Processing Technology-Image Analytics — desktop-only (OpenCV + disk) -- [x] minesweeper — pyodide -- [x] ML-Notebooks_Beginners — desktop-only (Jupyter notebook) -- [x] Mobile Document Scanner — desktop-only (OpenCV GUI) -- [x] Mongo CRUD — desktop-only (MongoDB) -- [x] MorseCode Translator — pyodide -- [x] Morse-Code-Translator — pyodide -- [x] MotivateBot — desktop-only (OpenAI API) -- [x] Movie recommendation — desktop-only (scikit-learn) -- [x] MovieApi_and_ML — desktop-only (Django + sklearn) -- [x] movie-rater — desktop-only (FastAPI + TMDB) -- [x] MQTT Client — desktop-only (MQTT broker network) -- [x] multiplayer_socket — desktop-only (TCP sockets) -- [x] Music Player — desktop-only (tkinter + pygame) -- [x] NASA-APOD — desktop-only (network + download) -- [x] Neurons — modified (removed os.system clear; pyodide) -- [x] Number Guessing App — pyodide -- [x] Online Trivia — desktop-only (opentdb API) -- [x] OpenCV_color_detect_in_live_feed — desktop-only (webcam) -- [x] Organize_Directory — desktop-only (filesystem) -- [x] Othello — desktop-only (pygame) -- [x] Otp_Generator — pyodide -- [x] OTP-Verfication-System — desktop-only (Gmail SMTP) -- [x] Password Projects — mixed (Generator/Hashing/Meter/strong-detector pyodide; Breach/Manager/WiFi desktop-only) -- [x] PDF_Reader — desktop-only (OpenAI + LangChain) -- [x] pdf_to_text — desktop-only (PyQt5) -- [x] pdf-paranoia — desktop-only (filesystem PDFs) -- [x] Personal-Finance-tracker — pyodide -- [x] Pig_latin — pyodide -- [x] ping_pong — desktop-only (turtle) -- [x] Pokemon Battle — pyodide -- [x] PONG — desktop-only (pygame) -- [x] Port_Scaner — desktop-only (TCP sockets) -- [x] prettified-stopwatch — desktop-only (pyperclip clipboard) -- [x] Print_Colored_Text — desktop-only (colorama) -- [x] ProjectEuler — pyodide -- [x] proxy-scrapper — desktop-only (live HTTP) -- [x] Python Banking System — pyodide -- [x] Python story generator — desktop-only (tkinter) -- [x] QRCode Scanner — desktop-only (OpenCV webcam) -- [x] QRCode-Generator — desktop-only (qrcode + disk) -- [x] qr-code-generator-with-window-and-simple-ui — desktop-only (tkinter) -- [x] Qt5_YouTube — desktop-only (PyQt5 + network) -- [x] QuickWordCloud — desktop-only (wordcloud + matplotlib) -- [x] Quiz Game — pyodide -- [x] Random-Quote-Generator — desktop-only (tkinter + API) -- [x] reciept generator — desktop-only (fpdf) -- [x] reddit-scraper — desktop-only (live HTTP) -- [x] regex-search — desktop-only (filesystem walk) -- [x] regex-strip — pyodide -- [x] Rename_Images — desktop-only (filesystem) -- [x] Resize_Image — desktop-only (pillow + disk) -- [x] RestrauntAPI — desktop-only (Django) -- [x] reuters-scraper — desktop-only (live HTTP) -- [x] Rock_Paper_Scissors — pyodide -- [x] Roll_A_dice — desktop-only (MySQL) -- [x] Rubik-tracking — desktop-only (OpenCV webcam) -- [x] Sales Optimizer — desktop-only (sklearn + matplotlib) -- [x] scheduledShutdown — desktop-only (os.system shutdown) -- [x] Scientific-Calculator — desktop-only (tkinter) -- [x] scrap-ycombinator — desktop-only (live HTTP) -- [x] ScreenRecorder — desktop-only (pyautogui + win32) -- [x] Seek_with_hand_track — desktop-only (webcam + mediapipe) -- [x] Selfie_with_Python — desktop-only (OpenCV webcam) -- [x] Send-Email — desktop-only (SMTP) -- [x] servo motor classifier — desktop-only (network CSV + matplotlib) -- [x] Simple-Plagiarism-Checker-Project — desktop-only (Flask server) -- [x] SketchifyMe — desktop-only (OpenCV GUI) -- [x] Skycast — desktop-only (Streamlit + API) -- [x] Slice-Audio — desktop-only (pydub/ffmpeg) -- [x] SMS_ChatBot — desktop-only (Flask + Twilio/OpenAI) -- [x] Snake Game — desktop-only (pygame) -- [x] snake_water_gun_game — desktop-only (rich package) -- [x] Social_media_content_creation — desktop-only (no Python code) -- [x] Socket — desktop-only (TCP sockets) -- [x] SongsMashup — desktop-only (Flask + downloads) -- [x] Space Shooter — desktop-only (pygame) -- [x] space_battle — desktop-only (pygame) -- [x] Speed-Type-test — desktop-only (pygame) -- [x] Split_Tip — pyodide -- [x] sponge-bob — desktop-only (turtle) -- [x] Spotify Player — desktop-only (Spotify/Twilio APIs) -- [x] Stock-Market-Dashboard — desktop-only (Streamlit) -- [x] student-management-system — desktop-only (MySQL) -- [x] Subnetting Flsm — pyodide -- [x] Subtitle_synchronizer.py — desktop-only (disk .srt files) -- [x] Sudoku_solver — pyodide -- [x] Sudoku-Solver — desktop-only (pygame) -- [x] takeImage — desktop-only (OpenCV webcam) -- [x] TennisTournamentSim — pyodide -- [x] Tesla — desktop-only (pygame) -- [x] Tetris Game — desktop-only (pygame) -- [x] Text Editor — desktop-only (tkinter) -- [x] Text Summarizer — desktop-only (spaCy model) -- [x] Text to Speech — desktop-only (gTTS + playback) -- [x] Text_to_SpreadSheet — desktop-only (disk walk + xlsx) -- [x] TextDetection — desktop-only (Google Vision API) -- [x] Text-to-Image Generation Project — desktop-only (GPU DL notebook) -- [x] text-translate — desktop-only (network + tkinter/gtts) -- [x] Tic-Tac-Toe — pyodide -- [x] TicTacToe-TylerPear — pyodide -- [x] Tile Matching — desktop-only (tkinter) -- [x] Timer — pyodide -- [x] Tkinter — desktop-only (tkinter) -- [x] ToDoList — pyodide -- [x] Turtle Pattern — desktop-only (turtle) -- [x] Turtle_Graphics — desktop-only (turtle) -- [x] Twitter-Bot — desktop-only (Telegram API) -- [x] Type Racer Game — desktop-only (tkinter) -- [x] url_shortener — desktop-only (API + pywin32) -- [x] Video Reversal — desktop-only (OpenCV video) -- [x] video_transcoder_project — desktop-only (Flask + FFmpeg) -- [x] Video-subtitle-generator — desktop-only (Whisper + FFmpeg) -- [x] Voice-to-Text — desktop-only (microphone) -- [x] Watermarker — desktop-only (Pillow + matplotlib) -- [x] Weather — desktop-only (live web API) -- [x] Web Scraping Jujustu Kaisen Manga — desktop-only (Selenium) -- [x] WebButtonSimpelGUI — desktop-only (tkinter) -- [x] web-crawler(movie extract) — desktop-only (live HTTP) -- [x] Website Blocker — desktop-only (hosts file) -- [x] Weights_in_different_planets_GUI — desktop-only (tkinter) -- [x] what-for-dinner — desktop-only (TheMealDB API) -- [x] Windows Logo — desktop-only (turtle) -- [x] Wine_quality_predictor — desktop-only (sklearn + CSV) -- [x] Word_Predictor — desktop-only (reads local file) -- [x] Worksheet_to_text — desktop-only (reads local xlsx) -- [x] World-Cup-Player-Comparison — desktop-only (live HTTP) -- [x] xls_to_xlsx — desktop-only (pywin32 + Excel) -- [x] YouTube Video Downloader — desktop-only (yt-dlp) diff --git a/web/scripts/gen-catalog.mjs b/web/scripts/gen-catalog.mjs index 602f3b4da..b21f36606 100644 --- a/web/scripts/gen-catalog.mjs +++ b/web/scripts/gen-catalog.mjs @@ -215,20 +215,7 @@ function parseReadme(file) { .trim(); if (blurb.length > 150) blurb = blurb.slice(0, 147).trimEnd() + "…"; - // Pyodide-runnable verdict - let runnable = false; - const idx = lines.findIndex((l) => - /^#+\s*pyodide-runnable/i.test(l.trim()), - ); - if (idx >= 0) { - for (let i = idx + 1; i < lines.length; i++) { - const l = lines[i].trim(); - if (!l) continue; - runnable = /^(yes|partly)/i.test(l); - break; - } - } - return { name, blurb, runnable }; + return { name, blurb }; } const folders = fs @@ -257,13 +244,11 @@ for (const folder of folders) { let name = folder; let blurb = ""; - let verdictRunnable = false; const readmeFile = findReadme(dir); if (readmeFile) { const parsed = parseReadme(readmeFile); name = parsed.name || folder; blurb = parsed.blurb; - verdictRunnable = parsed.runnable; } const mainPy = findMainPy(dir, folder); @@ -271,15 +256,17 @@ for (const folder of folders) { ? fs.readFileSync(mainPy, "utf8").split(/\r?\n/).length : 0; - const runnable = curated ? curated.playground : verdictRunnable; - const emoji = curated ? curated.emoji : emojiFor(name, blurb); - const author = AUTHORS[folder]; - // A project is playable iff a hand-written annotated tutorial file exists // at public/playground/.py. The originals in projects/ are never copied. + // This file is authored only for projects whose todo.json status is + // runnable/modified/partial, so `hasPlayground` is the catalog's runnable flag. const pgFile = path.join(PLAYGROUND_DIR, `${id}.py`); const hasPlayground = fs.existsSync(pgFile); + const runnable = curated ? curated.playground : hasPlayground; + const emoji = curated ? curated.emoji : emojiFor(name, blurb); + const author = AUTHORS[folder]; + entries.push({ id, folder, name, blurb, emoji, runnable, hasPlayground, lines, ...(author ? { author } : {}), diff --git a/web/src/lib/catalog.json b/web/src/lib/catalog.json index abb425aeb..8eabe594e 100644 --- a/web/src/lib/catalog.json +++ b/web/src/lib/catalog.json @@ -1,15 +1,4 @@ [ - { - "id": "maths", - "folder": "maths", - "name": "3D Shape Volume Calculator", - "blurb": "A console program that calculates the volume of various 3D shapes (cube, pyramid, cylinder, sphere, cone, ellipsoid, torus, hexagonal prism) from u…", - "emoji": "🧮", - "runnable": true, - "hasPlayground": true, - "lines": 137, - "author": "ChefYeshpal" - }, { "id": "adjactive-compartive-superlative", "folder": "Adjactive_compartive_superlative", @@ -1367,6 +1356,17 @@ "lines": 58, "author": "ZackeryRSmith" }, + { + "id": "maths", + "folder": "maths", + "name": "Mensuration Calculator", + "blurb": "A console program that calculates the volume of various 3D shapes (cube, pyramid, cylinder, sphere, cone, ellipsoid, torus, hexagonal prism) from u…", + "emoji": "🧮", + "runnable": true, + "hasPlayground": true, + "lines": 137, + "author": "ChefYeshpal" + }, { "id": "merge-pdfs", "folder": "Merge PDFs", @@ -2311,8 +2311,8 @@ "id": "game-snake-water-gun", "folder": "game-snake_water_gun", "name": "Sname Water and Gun", - "blurb": "Yes — game.py is a console game using only input()/print() and the standard-library random module.", - "emoji": "🎮", + "blurb": "", + "emoji": "🐍", "runnable": true, "hasPlayground": true, "lines": 117, @@ -2333,8 +2333,8 @@ "id": "socket", "folder": "Socket", "name": "Socket example", - "blurb": "No — it opens real TCP sockets for client/server networking, which is not available in the browser sandbox.", - "emoji": "🌐", + "blurb": "", + "emoji": "🐍", "runnable": false, "hasPlayground": false, "lines": 15, @@ -2585,8 +2585,8 @@ "id": "text-to-speech", "folder": "Text to Speech", "name": "Text-to-Speech", - "blurb": "No — gTTS sends text to Google's Translate TTS API over the network and writes/plays an mp3 file locally.", - "emoji": "🌍", + "blurb": "", + "emoji": "🔊", "runnable": false, "hasPlayground": false, "lines": 9, @@ -2596,7 +2596,7 @@ "id": "text-translate", "folder": "text-translate", "name": "text-translate", - "blurb": "No — main.py uses the translate package which makes network requests, and translate GUIaudio.py additionally uses tkinter, gtts, and pygame.", + "blurb": "", "emoji": "🌍", "runnable": false, "hasPlayground": false, @@ -2860,7 +2860,7 @@ "id": "message-spam", "folder": "Message-Spam", "name": "WhatsApp Random Message Sender", - "blurb": "No — it uses pyautogui to control the keyboard and opens WhatsApp Web in a desktop browser.", + "blurb": "", "emoji": "💬", "runnable": false, "hasPlayground": false, From 6f8e62368f5b8d6bd39c7284868e265a9ffb64dd Mon Sep 17 00:00:00 2001 From: Mrinank-Bhowmick <77621953+Mrinank-Bhowmick@users.noreply.github.com> Date: Fri, 22 May 2026 12:32:19 +0530 Subject: [PATCH 11/14] Clean up project READMEs: drop Pyodide section, add usage examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two README-wide changes: - Removed the trailing `## Pyodide-runnable` section from every project README; that verdict now lives in projects/todo.json. - Added an `## Example` section to ~265 READMEs so visitors can see what each program does before running it — an input/output transcript for console programs, a numbered walk-through for GUI/game/web apps. Each example was written against the project's actual source. Also: renamed the maths project to "Mensuration Calculator" so it no longer sorts first in the playground sidebar, fixed two wrong run commands (BMI calculator, Calculator), and dropped a personal banner from the maths playground file. Co-Authored-By: Claude Opus 4.7 --- projects/AES256/README.md | 31 +++++++++--- projects/API Based Weather Report/README.md | 26 +++++++--- projects/AWS_s3_upload/readme.md | 25 +++++++--- .../README.md | 19 +++++--- projects/Advisor/README.md | 16 ++++--- projects/Alarm Clock/README.md | 18 ++++--- .../README.md | 23 ++++++--- projects/AnalogClock/readme.md | 16 ++++--- projects/Audio Converter/README.md | 13 +++-- projects/AudioAPI/ReadMe.md | 24 ++++++++-- projects/AudioRecorder/README.md | 16 +++++-- projects/Audiobook/Readme.md | 14 ++++-- projects/AutoGui/ReadMe.md | 18 ++++--- projects/BFS visualizer/Readme.md | 16 +++++-- projects/BMI WebApp/README.md | 18 +++++-- projects/BMI_calculator/Readme.md | 28 ++++++++--- projects/Battleship/README.md | 44 ++++++++++++++--- projects/Bigram_Autocomplete/README.md | 14 ++++-- projects/Bitcoin Mining/Readme.md | 16 ++++--- projects/BlackJack/ReadMe.md | 38 +++++++++++++-- projects/Blender_tools/README.md | 18 +++++-- projects/Blind Auction/README.md | 7 --- projects/Blind_Auction/README.md | 24 ++++++++-- projects/Book Data Extractor/README.md | 16 +++++-- projects/Browser/README.md | 13 +++-- projects/Budget-manager/README.md | 13 +++-- projects/CRUD-with-postgresql/README.md | 4 -- projects/Calculate Age/README.md | 21 ++++---- projects/Calculator-GUI-With-Python/README.md | 13 +++-- projects/Calculator/Readme.md | 32 ++++++++++--- projects/Calendar/README.md | 20 ++++++-- projects/Captcha_Genrator/README.md | 11 +++-- projects/Card Game/Readme.md | 4 -- projects/Chat Application/readme.md | 5 -- projects/Chat-GPT-Discord-Bot/README.md | 16 +++++-- projects/Chess/README.md | 13 +++-- projects/Code_Reviewer/README.md | 22 +++++++-- projects/Coin Flip/README.md | 17 +++++-- projects/Collatz_Conjecture/README.md | 17 ++++--- projects/Comics_Scraper/README.md | 19 ++++++-- projects/Connect Four/README.md | 15 ++++-- .../README.md" | 15 ++++-- projects/Countdown/README.md | 18 +++++-- projects/CustomEncryptionDecryption/README.md | 39 +++++++++++++-- projects/Data Entry Automation/Readme.md | 8 +++- projects/Data_Abstractor/README.md | 11 +++-- projects/Desktop Weather Notifier/README.md | 18 +++++-- projects/Desktop-Notification/README.md | 12 +++-- .../Diabetes Monitoring Dashboard/README.md | 11 +++-- projects/Dice Simulator/Readme.md | 25 ++++++++-- projects/Dice-Roll-Simulator/README.md | 21 ++++++-- projects/Discord Bot/README.md | 25 ++++++++-- projects/DnD Dice/README.md | 24 ++++++++-- projects/Drowsiness Detector/Readme.md | 38 ++++++--------- projects/Encryptor and Decryptor/README.md | 15 ++++-- projects/English Thesaurus/README.md | 25 ++++++++-- projects/Expense-Tracker/README.md | 6 --- projects/Eye Blink Detection/Readme.md | 24 ++++++---- projects/Fidget Spinner Game/README.md | 13 +++-- projects/File_Organizer/README.md | 20 ++++++-- projects/Find_imbd_rating/README.md | 24 ++++++++-- projects/Flappybird_game/README.md | 14 ++++-- projects/Full_Calendar/README.md | 13 +++-- projects/GPA-Calculator/README.md | 26 ++++++++-- .../README.md" | 15 ++++-- projects/Game of Cricket/README.md | 36 ++++++++++++-- projects/Gpt-And-Langchain/README.md | 14 +++--- projects/Gradient-Image/README.md | 15 ++++-- projects/Guess Number/README.md | 26 ++++++++-- projects/Guess The Word/README.md | 33 +++++++++++-- projects/HandCricket/Readme.md | 35 ++++++++++++-- projects/HandTrack/README.md | 14 ++++-- projects/Hangman/README.md | 41 ++++++++++++++-- projects/Higher-Lower/README.md | 24 ++++++++-- projects/Historical Data Breaches/README.md | 4 -- projects/IP Blacklist Checker/README.md | 22 +++++++-- projects/IPv4_Calculator-main/README.md | 29 +++++++++-- projects/Image Manipulation/README.md | 12 +++-- projects/Image Sketcher/README.md | 12 +++-- projects/Image Toonification/Readme.md | 10 ++-- projects/Image compressor/README.md | 13 +++-- projects/Image to PDF Converter/README.md | 12 +++-- .../README.md | 15 ++++-- projects/Image-Upscale/README.md | 11 +++-- projects/Image-to-art/Readme.md | 21 ++++++-- projects/ImagegenChatbot/readme.md | 19 ++++++-- projects/Instagram Post Creation/README.md | 14 ++++-- projects/Internet Speed Tester/README.md | 14 ++++-- projects/Internet-speed-test/README.md | 23 +++++++-- projects/Inverse Matrix Calculator/README.md | 31 ++++++++++-- projects/JARVIS.PY/jarvisreadme.md | 17 +++++-- projects/Jokenpo/README.md | 25 ++++++++-- projects/Jokey/README.md | 30 ++++++++++-- projects/KbdXylo/README.md | 12 +++-- projects/Language-Translate/README.md | 11 +++-- .../Language_learning_assistant/README.md | 31 ++++++++++-- projects/Live AQI/README.md | 15 ++++-- projects/Loan Calculator/README.md | 13 +++-- projects/Location Search App (GUI)/README.md | 12 +++-- projects/Lyrics-Extractor/README.md | 18 +++++-- projects/ML-Notebooks_Beginners/README.md | 13 +++-- projects/MQTT Client/README.md | 15 ++++-- projects/Madlibs Generator/README.md | 12 +++-- projects/Make-API/README.md | 16 +++++-- .../readme.md | 23 +++++++-- projects/Mastermind/README.md | 21 ++++++-- projects/Medium Article Reader/README.md | 15 ++++-- projects/Merge PDFs/README.md | 31 ++++++++++-- projects/Message-Spam/README.md | 10 ++-- projects/Minecraft-in-Python-main/README.md | 14 ++++-- .../README.md | 21 ++++---- projects/Mobile Document Scanner/README.md | 18 +++++-- projects/Mongo CRUD/README.md | 27 +++++++++-- projects/Morse-Code-Translator/README.md | 20 ++++++-- projects/MorseCode Translator/README.md | 16 +++++-- projects/MotivateBot/README.md | 15 ++++-- projects/Movie recommendation/README.md | 20 ++++++-- projects/MovieApi_and_ML/README.md | 13 +++-- projects/MovieApi_and_ML/movieapi/README.md | 7 +++ projects/Music Player/README.md | 14 ++++-- projects/NASA-APOD/README.md | 18 ++++--- projects/Neurons/README.md | 18 +++++-- projects/Number Guessing App/README.md | 39 +++++++++++++-- projects/OTP-Verfication-System/README.md | 21 ++++++-- projects/Online Trivia/README.md | 29 +++++++++-- .../README.md | 14 ++++-- projects/Organize_Directory/README.md | 16 +++++-- projects/Othello/README.md | 13 +++-- projects/Otp_Generator/README.md | 14 ++++-- projects/PDF_Reader/README.md | 21 ++++++-- projects/PONG/README.md | 14 ++++-- .../Password Breach Frequency/README.md | 3 -- .../Password Generator/README.md | 21 ++++++-- .../Password Hashing/readme.md | 3 -- .../Password Meter/README.md | 28 +++++++++-- .../Password_manager/README.md | 14 ++++-- projects/Password Projects/README.md | 26 ++++++++-- .../WiFi Password Generator/README.md | 16 +++++-- .../strong-password-detector/README.md | 15 ++++-- projects/Personal-Finance-tracker/README.md | 30 ++++++++++-- projects/Pig_latin/README.md | 17 +++++-- projects/Pokemon Battle/README.md | 44 +++++++++++++++-- projects/Port_Scaner/README.md | 25 ++++++++-- projects/Print_Colored_Text/README.md | 16 +++++-- projects/ProjectEuler/Problem 1/Readme.md | 7 +++ projects/ProjectEuler/Problem 2/Readme.md | 8 ++++ projects/ProjectEuler/Problem 3/Readme.md | 7 +++ projects/ProjectEuler/Problem 4/Readme.md | 7 +++ projects/ProjectEuler/Problem 5/Readme.md | 7 +++ projects/ProjectEuler/Problem 6/Readme.md | 7 +++ projects/ProjectEuler/Problem 7/Readme.md | 7 +++ projects/ProjectEuler/Problem 8/Readme.md | 7 +++ projects/ProjectEuler/Problem 9/Readme.md | 8 ++++ projects/ProjectEuler/README.md | 34 +++++++------ projects/Python Banking System/readme.md | 35 +++++++++++--- projects/Python story generator/README.md | 12 +++-- projects/QRCode Scanner/Readme.md | 27 +++++++++-- projects/QRCode-Generator/README.md | 15 ++++-- projects/Qt5_YouTube/README.md | 14 ++++-- projects/QuickWordCloud/README.md | 10 ++-- projects/Quiz Game/README.md | 25 ++++++++-- projects/Random-Quote-Generator/README.md | 13 ++--- projects/Rename_Images/README.md | 17 +++++-- projects/Resize_Image/README.md | 19 ++++++-- projects/RestrauntAPI/README.md | 48 +++++++++++++++---- projects/Rock_Paper_Scissors/README.md | 22 +++++++-- projects/Roll_A_dice/README.md | 31 ++++++++++-- projects/Rubik-tracking/README.md | 13 +++-- projects/SMS_ChatBot/README.md | 13 +++-- projects/Sales Optimizer/Readme.md | 17 +++++-- projects/Scientific-Calculator/ReadMe.md | 15 +++--- projects/ScreenRecorder/README.md | 11 +++-- projects/Seek_with_hand_track/README.md | 14 ++++-- projects/Selfie_with_Python/README.md | 18 +++++-- projects/Send-Email/README.md | 17 +++++-- .../README.md | 14 ++++-- projects/SketchifyMe/README.md | 35 ++++++++------ projects/Skycast/README.md | 11 +++-- projects/Slice-Audio/README.md | 18 +++++-- projects/Snake Game/README.md | 13 +++-- .../Social_media_content_creation/README.md | 4 -- projects/Socket/README.md | 23 ++++++--- projects/SongsMashup/README.md | 13 +++-- projects/Space Shooter/README.md | 14 ++++-- projects/Speed-Type-test/README.md | 16 +++++-- projects/Split_Tip/README.md | 17 +++++-- projects/Spotify Player/README.md | 26 ++++++++-- projects/Stock-Market-Dashboard/README.md | 14 ++++-- projects/Subnetting Flsm/README.md | 25 ++++++++-- projects/Subtitle_synchronizer.py/README.md | 28 +++++++++-- projects/Sudoku-Solver/README.md | 14 ++++-- projects/Sudoku_solver/README.md | 35 ++++++++++++-- projects/TennisTournamentSim/README.md | 40 ++++++++++++++-- projects/Tesla/README.md | 14 ++++-- projects/Tetris Game/README.md | 15 ++++-- projects/Text Editor/README.md | 15 ++++-- projects/Text Summarizer/READme.md | 16 +++++-- projects/Text to Speech/readme.md | 14 ++++-- .../README.md | 14 ++++-- projects/TextDetection/README.md | 19 ++++++-- projects/Text_to_SpreadSheet/README.md | 28 +++++++++-- projects/Tic-Tac-Toe/README.md | 36 +++++++++++--- .../Tic-Tac-Toe-Terminal/README.md | 31 ++++++++++-- projects/Tic-Tac-Toe/TicTacToe-GUI/README.md | 14 ++++-- projects/TicTacToe-TylerPear/README.md | 31 ++++++++++-- projects/Tile Matching/README.md | 15 ++++-- projects/Timer/README.md | 18 +++++-- projects/Tkinter/README.md | 21 ++++++-- projects/ToDoList/README.md | 14 +++--- projects/Turtle Pattern/README.md | 14 ++++-- projects/Turtle_Graphics/README.md | 14 ++++-- projects/Twitter-Bot/README.md | 18 ++++--- projects/Type Racer Game/README.md | 14 ++++-- projects/Video Reversal/README.md | 17 +++++-- projects/Video-subtitle-generator/readme.md | 25 ++++++++-- projects/Voice-to-Text/README.md | 16 +++++-- projects/Watermarker/README.md | 11 +++-- projects/Weather/README.md | 16 +++++-- .../README.md | 25 ++++++++-- projects/WebButtonSimpelGUI/README.md | 13 +++-- projects/Website Blocker/readme.md | 16 +++++-- .../README.md | 13 +++-- projects/Windows Logo/README.md | 13 +++-- projects/Wine_quality_predictor/Readme.md | 21 ++++++-- projects/Word_Predictor/README.md | 16 +++++-- projects/Worksheet_to_text/README.md | 18 +++++-- .../World-Cup-Player-Comparison/README.md | 17 +++++-- projects/YouTube Video Downloader/README.md | 22 +++++++-- projects/bittorrent-downloader/README.md | 18 +++++-- projects/caesar_cipher/README.md | 21 ++++++-- projects/car game/README.md | 13 +++-- projects/career-guide-bot/readme.md | 6 --- projects/character-picture-grid/README.md | 19 ++++++-- projects/chore-assignment-emailer/README.md | 18 +++++-- projects/comma-code/README.md | 14 ++++-- projects/computer-algebra/README.md | 11 +++-- projects/currency converter/README.md | 13 +++-- projects/custom-invitations/README.md | 25 ++++++++-- projects/custom-seating-cards/README.md | 18 +++++-- projects/dictionary.com-scraper/README.md | 25 ++++++++-- projects/duplicate_search/README.md | 21 ++++++-- projects/excel-to-csv-converter/README.md | 17 +++++-- projects/facebook_video_downloader/README.md | 13 +++-- projects/facerecoginition/README.md | 14 ++++-- projects/fantasy-game-inventory/README.md | 24 ++++++++-- projects/fill-gaps/README.md | 22 +++++++-- projects/find-unneeded-files/README.md | 18 +++++-- projects/game-snake_water_gun/README.md | 4 -- projects/goodreads-quotes-scraper/README.md | 25 ++++++++-- projects/healthmanagementsystem/README.md | 28 +++++++++-- projects/hill_cipher/Readme.md | 16 +++++-- projects/image-site-downloader/README.md | 12 +++-- projects/indeed-scraper/README.md | 16 +++++-- projects/instant-messenger-bot/README.md | 18 +++++-- projects/inventory management/README.md | 20 ++++++-- projects/link-verification/README.md | 15 ++++-- projects/linkedin-scrape/README.md | 15 ++++-- projects/looking-busy/README.md | 14 ++++-- projects/love-calculator/README.md | 17 +++++-- projects/maths/README.md | 31 ++++++++++-- projects/minesweeper/README.md | 24 ++++++++-- projects/movie-rater/README.md | 13 +++-- projects/multiplayer_socket/README.md | 38 +++++++++++++-- projects/pdf-paranoia/README.md | 14 ++++-- projects/pdf_to_text/README.md | 3 -- projects/ping_pong/README.md | 15 ++++-- projects/prettified-stopwatch/README.md | 23 +++++++-- projects/proxy-scrapper/README.md | 22 +++++++-- .../README.md | 12 +++-- projects/reciept generator/README.md | 4 -- projects/reddit-scraper/README.md | 24 ++++++++-- projects/regex-search/README.md | 16 +++++-- projects/regex-strip/README.md | 15 ++++-- projects/reuters-scraper/README.md | 23 +++++++-- projects/scheduledShutdown/README.md | 17 +++++-- projects/scrap-ycombinator/README.md | 15 ++++-- projects/servo motor classifier/Readme.md | 18 +++++-- projects/snake_water_gun_game/README.md | 28 +++++++++-- projects/space_battle/README.md | 14 ++++-- projects/sponge-bob/README.md | 13 +++-- projects/student-management-system/README.md | 45 ++++++++++++++--- projects/takeImage/README.md | 11 ++++- projects/text-translate/README.md | 20 ++++++-- projects/url_shortener/README.md | 3 -- projects/video_transcoder_project/README.md | 12 +++-- projects/web-crawler(movie extract)/README.md | 19 ++++++-- projects/what-for-dinner/README.md | 19 ++++++-- projects/xls_to_xlsx/README.md | 22 ++++++--- web/public/playground/maths.py | 9 ---- 289 files changed, 3892 insertions(+), 1392 deletions(-) diff --git a/projects/AES256/README.md b/projects/AES256/README.md index f22ddd9a7..af839fdac 100644 --- a/projects/AES256/README.md +++ b/projects/AES256/README.md @@ -7,7 +7,30 @@ key derived from your password via `scrypt`. Choose `1` to encrypt a message (prints the cipher text, salt, nonce and tag) or `2` to decrypt by entering those values back. -## How to run +## Example + +```text + AES 256 Encryption and Decryption Algorithm + ------------------------------------------- + +Enter 1 to encrypt and 2 to decrypt: 1 +Enter the Password: hunter2 + +Enter the Secret Message: meet me at noon + +Encrypted: +--------------- + +cipher_text: 9pX2k7Qe1A== +salt: Hh0r2Hk9pQqVz8m1Yw3aBQ== +nonce: Lm4nKp7Rt2sVx8yZ +tag: Tz1qWe4rUi7oPa2sDf5gHj== +``` + +To recover the message, run the program again, choose `2`, and enter the four +values above along with the same password. + +## How to run on localhost ```bash pip install pycryptodomex @@ -17,9 +40,3 @@ python AES256.py ## Dependencies - `pycryptodomex` — the `Cryptodome` AES implementation - -## Pyodide-runnable - -Yes. `pycryptodome` is available in the Pyodide playground, and the program is a -pure `input()`/`print()` console app. The screen-clearing `os.system` call was -removed so it runs cleanly in-browser. diff --git a/projects/API Based Weather Report/README.md b/projects/API Based Weather Report/README.md index f56eb1d08..3992d3bfd 100644 --- a/projects/API Based Weather Report/README.md +++ b/projects/API Based Weather Report/README.md @@ -13,6 +13,26 @@ Welcome to the Weather Information App! This application allows users to fetch c - Python 3.x installed on your system. - OpenWeatherMap API key. You can obtain it by signing up at [OpenWeatherMap](https://home.openweathermap.org/users/sign_up). +## Example + +```text +Welcome to the Weather Information App! +You need an API key to access weather data from OpenWeatherMap. +You can obtain your API key by signing up at https://home.openweathermap.org/users/sign_up +Please enter your OpenWeatherMap API key: YOUR_API_KEY +Enter the city name: London +Enter the temperature unit. 'C' for Celsius and 'F' for Fahrenheit: C +Current City : London, GB +Current temperature is: 15.2 C +Current weather desc : light rain +Current Humidity : 82 % +Current wind speed : 5.1 kmph +Current wind direction: SW +Today's sunrise time : 05:12:34 +Today's sunset time : 20:45:12 +Weather information written to weatherinfo.txt +``` + ## Usage 1. Obtain your OpenWeatherMap API key by signing up at [OpenWeatherMap](https://home.openweathermap.org/users/sign_up). @@ -42,12 +62,6 @@ Welcome to the Weather Information App! This application allows users to fetch c - Alternative Names: Use alternative or local names if known (e.g., "Mumbai" for "Bombay"). - City Name with Spaces: Input the city name with spaces as it appears (e.g., "Los Angeles", "San Francisco"). - City District or Area (Optional): Specify a district or area within larger cities for more localized weather data (e.g., "Manhattan, New York", "Shinjuku, Tokyo"). - -## Pyodide-runnable - -No. The app calls the OpenWeatherMap HTTP API at runtime, which is blocked from -the in-browser Pyodide playground. - ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/projects/AWS_s3_upload/readme.md b/projects/AWS_s3_upload/readme.md index 1b271c2a5..9978951ac 100644 --- a/projects/AWS_s3_upload/readme.md +++ b/projects/AWS_s3_upload/readme.md @@ -1,17 +1,30 @@ - ## Simple Python script for AWS S3 file upload. ### Prerequisites + boto3 (pip install boto3)
-### How to run the script +### Example + +After filling in the credentials and file details in `main.py` and running +`python main.py`, the console prints: + +```text +Upload Successful +``` + +If the local file cannot be found the output is: + +```text +The file was not found +``` + +### How to run on localhost the script + - Specify both ACCESS_KEY and SECRET_KEY. You can get them both on your AWS account in "My Security Credentials" section.
- Specify the local file name, bucket name and the name that you want the file to have inside s3 bucket using LOCAL_FILE, BUCKET_NAME and S3_FILE_NAME variables.
- Run "python main.py"
### Author Name -Yashvardhan Singh https://github.com/pythonicboat -## Pyodide-runnable - -No - uses boto3 to make network calls to the AWS S3 API. +Yashvardhan Singh https://github.com/pythonicboat diff --git a/projects/Adjactive_compartive_superlative/README.md b/projects/Adjactive_compartive_superlative/README.md index 3ee95fa14..11ff57fc0 100644 --- a/projects/Adjactive_compartive_superlative/README.md +++ b/projects/Adjactive_compartive_superlative/README.md @@ -4,7 +4,19 @@ A console tool that takes a comma-separated list of adjectives and prints the comparative and superlative form of each, using WordNet (via NLTK) with a JSON fallback list of irregular adjectives. -## How to run +## Example + +```text +Enter a list of adjectives (comma-separated): happy, bad, large + +Adjective Comparative Superlative +------------------------------------------ +happy happier happiest +bad worse worst +large more large most large +``` + +## How to run on localhost ```bash pip install -r requirements.txt @@ -16,8 +28,3 @@ On first run NLTK downloads the `wordnet` corpus. ## Dependencies - `nltk` — and its `wordnet` corpus (downloaded at runtime) - -## Pyodide-runnable - -No. `nltk.download()` fetches the WordNet corpus over the network at runtime, -which is not available in the in-browser Pyodide playground. diff --git a/projects/Advisor/README.md b/projects/Advisor/README.md index fab97ccb1..a1eadb1be 100644 --- a/projects/Advisor/README.md +++ b/projects/Advisor/README.md @@ -4,7 +4,16 @@ A small Tkinter desktop app that fetches a random piece of advice from the [Advice Slip API](https://api.adviceslip.com/) and shows it in a window. Click **Get Advice** for a new one. -## How to run +## Example + +1. The window titled "Random Advisor Application" opens and immediately fetches a + piece of advice, displaying it in the centre of the window (e.g. "Always be + yourself; everyone else is already taken."). +2. Click **Get Advice** to fetch a new random tip from the Advice Slip API. +3. The label updates with the new advice text. +4. Close the window to exit. + +## How to run on localhost ```bash pip install requests @@ -15,8 +24,3 @@ python advisor.py - `requests` — to call the Advice Slip API - `tkinter` — GUI (ships with the standard Python installer) - -## Pyodide-runnable - -No. It uses a Tkinter window and a live HTTP request, neither of which works in -the in-browser Pyodide playground. diff --git a/projects/Alarm Clock/README.md b/projects/Alarm Clock/README.md index 1b275e952..a525ec30a 100644 --- a/projects/Alarm Clock/README.md +++ b/projects/Alarm Clock/README.md @@ -7,7 +7,18 @@ plays `sound.wav` on a loop until you press **Stop Alarm**. The folder also contains `clock.py`, a standalone digital clock that shows the current time and greets you based on the time of day. -## How to run +## Example + +1. The window opens showing three drop-down menus labelled with hours, minutes, + and seconds, along with **Set Alarm** and **Stop Alarm** buttons. +2. Select `07`, `30`, `00` from the drop-downs to set an alarm for 07:30:00. +3. Click **Set Alarm**. The program checks the system clock once per second in + the background, printing `07:29:55 07:30:00` to the console each tick. +4. When the system clock reaches `07:30:00`, `sound.wav` starts playing on a + loop and "Wake Up now!" is printed to the console. +5. Click **Stop Alarm** to stop the sound. + +## How to run on localhost ```bash pip install pygame @@ -19,8 +30,3 @@ python clock.py # the digital clock - `tkinter` — GUI (ships with the standard Python installer) - `pygame` — plays the alarm sound (`alarm_clock.py` only) - -## Pyodide-runnable - -No. Both scripts use a Tkinter window, and the alarm also uses `pygame` audio -and background threads — none of which work in the in-browser Pyodide playground. diff --git a/projects/Amazon Product Availbility Checker/README.md b/projects/Amazon Product Availbility Checker/README.md index bdff5a8e0..d0f88d85a 100644 --- a/projects/Amazon Product Availbility Checker/README.md +++ b/projects/Amazon Product Availbility Checker/README.md @@ -4,10 +4,26 @@ A console script that fetches an Amazon product page, parses it with BeautifulSoup, and reports whether the product is in stock. Before running, edit `amazon.py`: + - Set `product_url` to the Amazon product page you want to track. - Set a valid `User-Agent` string in `headers` (Amazon blocks the default one). -## How to run +## Example + +After setting `product_url` and `User-Agent` in `amazon.py` and running the +script, the console prints one of: + +```text +Wireless Bluetooth Headphones is available on Amazon. +``` + +or + +```text +Wireless Bluetooth Headphones is currently out of stock on Amazon. +``` + +## How to run on localhost ```bash pip install requests beautifulsoup4 @@ -18,8 +34,3 @@ python amazon.py - `requests` — fetches the product page - `beautifulsoup4` — parses the HTML - -## Pyodide-runnable - -No. It makes a live HTTP request to Amazon, and browsers block cross-origin -scraping requests, so it cannot run in the in-browser Pyodide playground. diff --git a/projects/AnalogClock/readme.md b/projects/AnalogClock/readme.md index b98a53ea7..bd675ae38 100644 --- a/projects/AnalogClock/readme.md +++ b/projects/AnalogClock/readme.md @@ -6,7 +6,16 @@ numbers, and moving hour/minute/second hands. The clock always starts at ![img.png](img.png) -## How to run +## Example + +1. Run `python analog_clock.py`. A window opens showing a circular clock face + with hour numbers 1–12 drawn around the edge. +2. The clock starts at 10 o'clock with all three hands positioned accordingly. +3. Every second the second hand advances one tick; the minute and hour hands + move proportionally. +4. Close the window to exit. + +## How to run on localhost ```bash python analog_clock.py @@ -16,8 +25,3 @@ python analog_clock.py - `tkinter` — GUI (ships with the standard Python installer) - `math` — standard library - -## Pyodide-runnable - -No. It draws to a Tkinter canvas window, which is not available in the -in-browser Pyodide playground. diff --git a/projects/Audio Converter/README.md b/projects/Audio Converter/README.md index eda82c4a3..92f9cac04 100644 --- a/projects/Audio Converter/README.md +++ b/projects/Audio Converter/README.md @@ -10,14 +10,17 @@ Use the package manager [pip](https://pip.pypa.io/en/stable/) to install pydub. pip install pydub ``` +## Example + +```text +$ python audioconverter.py song.mp3 wav +Converting from mp3 to wav +File saved to /home/user/music/song.wav +``` + ## Usage ```bash python audioconverter.py ``` Run `ffmpeg -formats` to view supported formats, since Pydub uses ffmpeg. - - -## Pyodide-runnable - -No - uses the pydub package and reads/converts audio files from disk via sys.argv, which is not available in a browser sandbox. diff --git a/projects/AudioAPI/ReadMe.md b/projects/AudioAPI/ReadMe.md index 0f9d6fc26..72b19f499 100644 --- a/projects/AudioAPI/ReadMe.md +++ b/projects/AudioAPI/ReadMe.md @@ -17,6 +17,25 @@ Before using the Unofficial Whisper API, ensure you have the following prerequis ```bash pip install Flask +## Example + +1. Start the server by running `python transcription.py`. Flask listens on + `http://localhost:5000`. +2. From another terminal, send an audio file with: + ``` + curl -X POST -F "audio=@recording.mp3" http://localhost:5000/transcribe + ``` +3. The server loads the Whisper model, transcribes the audio, and returns a + `subtitles.vtt` file as a download containing timestamped transcript + segments, e.g.: + ``` + WEBVTT + + 1 + 0:00:00.000 --> 0:00:04.000 + Hello, welcome to the demonstration. + ``` + ## Getting Started 1. Clone the repository @@ -31,8 +50,3 @@ The Unofficial Whisper API provides transcription output in various formats: Text: The transcribed text as a plain text string. VTT (WebVTT): Transcribed text with timestamps in the WebVTT format for easy integration with video players. SRT (SubRip): Transcribed text with timestamps in the SubRip format, suitable for subtitles. - - -## Pyodide-runnable - -No - it is a Flask web server that also depends on OpenAI Whisper, neither of which runs under Pyodide. diff --git a/projects/AudioRecorder/README.md b/projects/AudioRecorder/README.md index 8d09e8a20..d13146bea 100644 --- a/projects/AudioRecorder/README.md +++ b/projects/AudioRecorder/README.md @@ -2,7 +2,17 @@ Records audio from the system microphone and saves it to RAW, WAV, AIFF, and FLAC files. -## How to run +## Example + +```text +Listening +``` + +Speak into the microphone. When you finish, the program saves the recording to +`microphone-results.raw`, `microphone-results.wav`, `microphone-results.aiff`, +and `microphone-results.flac` in the current directory. + +## How to run on localhost ``` pip install SpeechRecognition pyaudio @@ -13,7 +23,3 @@ python main.py - SpeechRecognition - pyaudio (microphone input backend) - -## Pyodide-runnable - -No - it captures live audio from the microphone and writes audio files to disk. diff --git a/projects/Audiobook/Readme.md b/projects/Audiobook/Readme.md index 0b4b83a7f..8f1fb332b 100644 --- a/projects/Audiobook/Readme.md +++ b/projects/Audiobook/Readme.md @@ -8,6 +8,15 @@ This Python script uses `pyttsx3` and `PyPDF2` to read the text content of a PDF - `PyPDF2`: A library to handle PDF files. - `keyboard`: A library to listen for keyboard events. +## Example + +```text +Enter your PDF file name: sample.pdf +``` + +The program begins reading the PDF aloud, page by page, using the system +text-to-speech engine. Press **q** at any time to stop playback. + ## Usage 1. Install the required libraries using pip: @@ -27,8 +36,3 @@ python pdf_text_to_speech.py ## How it Works The script takes a PDF file as input, extracts the text content from each page, and uses `pyttsx3` to read it out loud. The playback can be stopped at any time by pressing the 'q' key. - - -## Pyodide-runnable - -No - depends on pyttsx3 (offline TTS engine), the keyboard library, and reads PDFs from the local disk. diff --git a/projects/AutoGui/ReadMe.md b/projects/AutoGui/ReadMe.md index ed2b31bdf..92497bcb9 100644 --- a/projects/AutoGui/ReadMe.md +++ b/projects/AutoGui/ReadMe.md @@ -2,13 +2,19 @@ This Python script periodically moves the mouse and performs a click action. It’s useful for keeping the system active to avoid timeouts or showing as idle in applications like Microsoft Teams. +## Example + +1. Run `python AutoGui.py`. The script runs silently in the background. +2. Every 20 seconds the mouse moves 100 pixels diagonally and then returns to + its original position, followed by a click. +3. This keeps the system active and prevents idle status in applications such + as Microsoft Teams. +4. Press **Ctrl+C** in the terminal to stop the script. The console prints: + ```text + Script terminated by user + ``` + ## Features - Moves the mouse slightly every 20 seconds - Simulates a mouse click periodically - Prevents idle status on Microsoft Teams - - - -## Pyodide-runnable - -No - relies on pyautogui to control the real mouse and keyboard, which has no meaning in a browser. diff --git a/projects/BFS visualizer/Readme.md b/projects/BFS visualizer/Readme.md index c9cbe0700..037261151 100644 --- a/projects/BFS visualizer/Readme.md +++ b/projects/BFS visualizer/Readme.md @@ -2,6 +2,18 @@ This project helps us to visualize BFS algorithm by searching end point from start in a maze. +## Example + +1. Run `python main.py`. A curses terminal window opens displaying the 9x8 maze + with `#` walls, spaces for open paths, `O` as the start position, and `X` as + the end. +2. The BFS algorithm explores cells level by level. Each visited cell on the + current path is highlighted in red (`X`) while the walls and open cells are + shown in green. +3. After a 0.2-second delay per step, the algorithm finds the shortest path from + `O` to `X` and the final path remains highlighted on screen. +4. Press any key to exit. + # Tech Stack: Python @@ -14,7 +26,3 @@ # Output: ![output](./output.gif) - -## Pyodide-runnable - -No - uses the curses library for terminal UI, which is not available in Pyodide. diff --git a/projects/BMI WebApp/README.md b/projects/BMI WebApp/README.md index 6e388833d..354ecc2f3 100644 --- a/projects/BMI WebApp/README.md +++ b/projects/BMI WebApp/README.md @@ -2,7 +2,19 @@ A small BMI calculator built with PyWebIO. It asks for height and weight in a browser form and reports the BMI along with a weight classification. -## How to run +## Example + +1. Run `python main.py`. PyWebIO opens a browser page with two input fields. +2. Enter your height in metres (e.g. `1.75`) and your weight in kilograms + (e.g. `70`). +3. The page displays the result, for example: + ``` + Your BMI is 22.857142857142858 and the person is : normal + ``` +4. Different weight ranges produce classifications such as "underweight", + "overweight", or "severely obese". + +## How to run on localhost ``` pip install pywebio @@ -12,7 +24,3 @@ python main.py ## Dependencies - pywebio - -## Pyodide-runnable - -No - it depends on the PyWebIO framework, which runs its own web server and is not available under Pyodide. diff --git a/projects/BMI_calculator/Readme.md b/projects/BMI_calculator/Readme.md index 22b22a57d..43d4f738b 100644 --- a/projects/BMI_calculator/Readme.md +++ b/projects/BMI_calculator/Readme.md @@ -2,6 +2,25 @@ This Python script calculates Body Mass Index (BMI) based on a person's height and weight. It then interprets the BMI to provide a classification ranging from underweight to clinically obese. +## Example + +```text +Here You can take the reference chart + +╒════════════════╤═════════════════╕ +│ BMI │ Weight Status │ +╞════════════════╪═════════════════╡ +│ Below 18.5 │ Underweight │ +│ 18.5 – 24.9 │ Normal weight │ +│ 25.0 – 29.9 │ Overweight │ +│ 30.0 and above │ Obese │ +╘════════════════╧═════════════════╛ + +Enter your height in meters: 1.75 +Enter your weight in kilograms: 68 +Your BMI is 22.2, you have a normal weight. +``` + ## How to Use 1. Make sure you have Python installed on your system. @@ -13,7 +32,7 @@ This Python script calculates Body Mass Index (BMI) based on a person's height a 4. Run the script by executing the following command: ```bash - python bmi_calculator.py + python "BMI calculator.py" ``` 5. Follow the on-screen instructions to input your height (in meters) and weight (in kilograms). @@ -22,7 +41,7 @@ This Python script calculates Body Mass Index (BMI) based on a person's height a ## Code Structure -- `bmi_calculator.py`: The main Python script containing the BMI calculation and interpretation logic. +- `BMI calculator.py`: The main Python script containing the BMI calculation and interpretation logic. - `README.md`: This file, providing an overview of the BMI calculator and usage instructions. ## Functions @@ -55,8 +74,3 @@ The main function to execute the BMI calculation and interpretation. ## Error Handling The script handles invalid input, ensuring that only numerical values for height and weight are accepted. - - -## Pyodide-runnable - -No - it depends on the third-party tabulate package and reads the bmi.csv file from the local filesystem. diff --git a/projects/Battleship/README.md b/projects/Battleship/README.md index f63c00907..cf7fe155e 100644 --- a/projects/Battleship/README.md +++ b/projects/Battleship/README.md @@ -27,6 +27,43 @@ The game handles incorrect entries and can be replayed repeatedly. +## Example + +```text +Do you want to start a new game of Battleship? yes +Enter a number between 5 and 15. + +This will determine how big the playing board is and how many turns you have to find the Battleship. (5 rows, 5 columns, 5 turns, etc.): 5 +Welcome to Battleship. + +A ship, one cell long, has been randomly placed on the below 5x5 grid. +You have 5 turns to find it. + +O O O O O +O O O O O +O O O O O +O O O O O +O O O O O + +Turn 1 of 5 + +Guess Row: 3 +Guess Column: 2 +Miss! + +O O O O O +O O O O O +O O O O O +O O X O O +O O O O O + +Turn 2 of 5 + +Guess Row: 1 +Guess Column: 4 +Congratulations! You sank my battleship! +``` + ## Installation Download main.py, change to the directory where you downloaded the file and run it using `python3 main.py` @@ -55,10 +92,3 @@ Py-Battleship is built with the following tools and libraries:
  • Python Preferences > Add-ons > Install**, then + select `mesh_morpher.py` (or `vertex_animation.py`). +2. Enable the add-on by ticking the checkbox next to its name. +3. In the 3D Viewport, open the sidebar (**N** key) and navigate to the + **Unreal Tools** tab to find the tool's panel. +4. For **Mesh Morpher**: select a mesh with shape keys and click the operator + button to bake the vertex offsets into UV layers. +5. For **Vertex Animation**: select mesh objects, set the frame range, and run + the operator to export per-frame vertex data into image textures ready for + use in a game engine vertex shader. + ## Getting Started These tools can be installed as add-ons or ran as scripts. Each tool has a panel located in the 3D View's sidebar under the Unreal Tools tab. @@ -43,8 +56,3 @@ These tools can be installed as add-ons or ran as scripts. Each tool has a panel * While in Blender use the text editor to open the tool you want to use. * Then either click the run script operator (the **arrow** icon in header) or use **alt+p** shortcut. - - -## Pyodide-runnable - -No - these are Blender add-ons that import the bpy module, which only exists inside Blender. diff --git a/projects/Blind Auction/README.md b/projects/Blind Auction/README.md index fe96b85ed..7c75b08fb 100644 --- a/projects/Blind Auction/README.md +++ b/projects/Blind Auction/README.md @@ -39,10 +39,3 @@ This will happen if you’re using an IDE other than replit (e.g., VSCode, PyCha # Solution [https://replit.com/@appbrewery/blind-auction-completed](https://replit.com/@appbrewery/blind-auction-completed?v=1) - - -## Pyodide-runnable - -Yes — `main.py` is a pure-stdlib console program (`art.py` is a local module in -this folder). The instructions above mention a `replit` `clear()` call, but the -code here doesn't use it. diff --git a/projects/Blind_Auction/README.md b/projects/Blind_Auction/README.md index 9ad5949b5..b54d6bcfe 100644 --- a/projects/Blind_Auction/README.md +++ b/projects/Blind_Auction/README.md @@ -2,7 +2,25 @@ A console blind-auction game. Each bidder enters a name and a secret bid; once bidding ends, the program announces the highest bidder. -## How to run +## Example + +```text +Welcome to Blind Auction +Enter your name: Alice +Enter your bidding amount: 250 +Are there any other bidders? Type 'yes' or 'no': yes +Continue bidding +Enter your name: Bob +Enter your bidding amount: 400 +Are there any other bidders? Type 'yes' or 'no': yes +Continue bidding +Enter your name: Carol +Enter your bidding amount: 310 +Are there any other bidders? Type 'yes' or 'no': no +The winner is Bob with the highest bid of 400. +``` + +## How to run on localhost ``` python main.py @@ -11,7 +29,3 @@ python main.py ## Dependencies Standard library only (`art.py` is a local module containing ASCII art). - -## Pyodide-runnable - -Yes - after removing an unused `distutils` import it is a pure-stdlib console program. diff --git a/projects/Book Data Extractor/README.md b/projects/Book Data Extractor/README.md index 1dc8abe1a..c5dfa9151 100644 --- a/projects/Book Data Extractor/README.md +++ b/projects/Book Data Extractor/README.md @@ -1,5 +1,17 @@ This project is designed to extract large datasets of book information from Goodreads based on user ratings. It includes a Python script, main.py, which scrapes book data from Goodreads pages and saves it to an Excel file. +## Example + +```text +Enter the exact link of the book list from the goodreads website: https://www.goodreads.com/list/show/1.Best_Books_Ever +Enter the minimum ratings you want to extract: 4.0 +Enter the total number of Web Pages in Your List: 2 +Extracting the Data.......................... +Successfully extracted page 1 +Successfully extracted page 2 +Excel File Saved Succesfully, Check Your Folder For The File +``` + Provide Input: Enter the exact link to the book list on the Goodreads website. Specify the minimum rating threshold for the books you want to extract. @@ -12,7 +24,3 @@ The extracted book information will be saved in this file. Excel File: You can find the extracted book data in the books.xlsx file in the project folder. - -## Pyodide-runnable - -No - uses requests to scrape Goodreads and writes an .xlsx file with openpyxl. diff --git a/projects/Browser/README.md b/projects/Browser/README.md index cb91cf951..e5685605c 100644 --- a/projects/Browser/README.md +++ b/projects/Browser/README.md @@ -2,7 +2,14 @@ A minimal desktop web browser built with PyQt5 and QtWebEngine, featuring back/forward/reload buttons and a URL bar. -## How to run +## Example + +1. Run `python main.py`. A maximised desktop window titled "EFFLUX browser" opens, loading `http://www.google.com`. +2. Type a URL (e.g. `https://www.python.org`) in the URL bar at the top and press Enter. The page loads and the URL bar updates to the new address. +3. Click the back button (`⮜`) to return to the previous page, or the forward button (`⮞`) to go forward again. +4. Click the reload button (`⟳`) to refresh the current page. + +## How to run on localhost ``` pip install PyQt5 PyQtWebEngine @@ -13,7 +20,3 @@ python main.py - PyQt5 - PyQtWebEngine - -## Pyodide-runnable - -No - it is a PyQt5 desktop GUI application using QtWebEngine. diff --git a/projects/Budget-manager/README.md b/projects/Budget-manager/README.md index 469cf2f97..4816db1b6 100644 --- a/projects/Budget-manager/README.md +++ b/projects/Budget-manager/README.md @@ -2,7 +2,14 @@ A personal budget manager desktop app. It lets you add and delete transactions, categorise them, and view a running balance, storing everything in a local SQLite database. -## How to run +## Example + +1. Run `python main.py`. The "Personal Budget Manager" window opens with fields for Date, Description, Amount ($), and Category, plus a transaction list and a balance display showing `Current Balance: $0.00`. +2. Fill in the fields — e.g. Date: `2024-01-15`, Description: `Salary`, Amount: `3000`, Category: `Income` — then click **Add Transaction**. A confirmation dialog appears and the new entry is shown in the list. +3. Add an expense: Date: `2024-01-16`, Description: `Groceries`, Amount: `-120`, Category: `Food`. The balance label updates to reflect the running total. +4. Click a transaction in the list to select it, then click **Delete Transaction** to remove it. The balance recalculates immediately. + +## How to run on localhost ``` python main.py @@ -11,7 +18,3 @@ python main.py ## Dependencies Standard library only (`tkinter`, `sqlite3`). - -## Pyodide-runnable - -No - it is a Tkinter desktop GUI and persists data to a SQLite database file. diff --git a/projects/CRUD-with-postgresql/README.md b/projects/CRUD-with-postgresql/README.md index 47eabe843..d1a21cbbd 100644 --- a/projects/CRUD-with-postgresql/README.md +++ b/projects/CRUD-with-postgresql/README.md @@ -83,7 +83,3 @@ python main.py ``` It's done! Now you can use the CLI. - -## Pyodide-runnable - -No — it requires a PostgreSQL database connection via `psycopg2`, which cannot run in the browser. diff --git a/projects/Calculate Age/README.md b/projects/Calculate Age/README.md index a798eaca8..969e36f98 100644 --- a/projects/Calculate Age/README.md +++ b/projects/Calculate Age/README.md @@ -1,37 +1,32 @@ # Calculate Your Age! -This script prints your age in three different ways : +This script prints your age in three different ways : + 1. Years 2. Months 3. Days - ## Prerequisites You only need Python to run this script. You can visit [here](https://www.python.org/downloads/) to download Python. - -## How to run the script +## How to run on localhost the script Running the script is really simple! Just open a terminal in the folder where your script is located and run the following command : `python calculate.py` - ## Sample use of the script + + ``` -$ python calculate.py +$ python calculate.py input your name: XYZ - input your age: 33 + input your age: 33 XYZ's age is 33 years or 406 months or 12328 days ``` -## *Author Name* +## _Author Name_ [Yash Upadhyay](https://github.com/xlo-u) - - -## Pyodide-runnable - -Yes - pure-stdlib console program using only the calendar/time modules and input()/print(). diff --git a/projects/Calculator-GUI-With-Python/README.md b/projects/Calculator-GUI-With-Python/README.md index 6fa7be8d3..117e2f823 100644 --- a/projects/Calculator-GUI-With-Python/README.md +++ b/projects/Calculator-GUI-With-Python/README.md @@ -2,7 +2,14 @@ A graphical calculator built with the Kivy framework, featuring a button grid, an evaluation display, and a clear button. -## How to run +## Example + +1. Run `python app.py`. A Kivy window opens with a display label at the top and a 4-column grid of buttons (`1`–`9`, `0`, `+`, `-`, `*`, `/`, `.`, `=`) plus a **Clear** button at the bottom. +2. Click `7`, `*`, `6` — the display shows `7*6`. +3. Click `=` — the display updates to `42`. +4. Click **Clear** to reset the display and start a new calculation. + +## How to run on localhost ``` pip install kivy @@ -12,7 +19,3 @@ python app.py ## Dependencies - kivy - -## Pyodide-runnable - -No - it is a Kivy desktop GUI application. diff --git a/projects/Calculator/Readme.md b/projects/Calculator/Readme.md index d53b01486..ed29469b5 100644 --- a/projects/Calculator/Readme.md +++ b/projects/Calculator/Readme.md @@ -2,12 +2,36 @@ This Python script is a simple calculator that allows for basic arithmetic operations such as addition, subtraction, multiplication, division, and average calculation. It takes user input to perform the desired operation and displays the result. +## Example + +```text +Enter '1' for addition +Enter '2' for subtraction +Enter '3' for multiplication +Enter '4' for division +Enter '5' for average +... +Enter '-1' to exit. + +Your choice is: 1 +Enter all numbers separated by space: 4 8 15 16 +The answer is 43 + +Your choice is: 4 +Enter first number: 20 +Enter second number: 8 +The answer is 2.5 + +Your choice is: -1 +Thank you for using the calculator! +``` + ## Usage 1. Run the script: ```bash - python calculator.py + python main.py ``` 2. Follow the prompts to choose an operation and input the required numbers. @@ -29,9 +53,3 @@ To exit the calculator, enter `-1` when prompted for your choice. ## Note This calculator clears the console after each operation for a better user experience. - - - -## Pyodide-runnable - -Yes - pure-stdlib console calculator (os.system screen-clears were removed). diff --git a/projects/Calendar/README.md b/projects/Calendar/README.md index 70b594bf5..b716710b3 100644 --- a/projects/Calendar/README.md +++ b/projects/Calendar/README.md @@ -2,7 +2,21 @@ A console program that prints the calendar for a year and month entered by the user, with input validation. -## How to run +## Example + +```text +Enter year: 2024 +Enter month: 3 + March 2024 +Mo Tu We Th Fr Sa Su + 1 2 3 + 4 5 6 7 8 9 10 +11 12 13 14 15 16 17 +18 19 20 21 22 23 24 +25 26 27 28 29 30 31 +``` + +## How to run on localhost ``` python displayCalendar.py @@ -11,7 +25,3 @@ python displayCalendar.py ## Dependencies Standard library only (`calendar`). - -## Pyodide-runnable - -Yes - it is a pure-stdlib console program using only the `calendar` module and `input()`/`print()`. diff --git a/projects/Captcha_Genrator/README.md b/projects/Captcha_Genrator/README.md index 20d5280a6..9b1c9765c 100644 --- a/projects/Captcha_Genrator/README.md +++ b/projects/Captcha_Genrator/README.md @@ -1,6 +1,12 @@ # Captcha Genrator A simple image captcha genrator +## Example + +1. Run `Captcha_Genrator.py`. A Tkinter window opens displaying a randomly generated 6-digit image CAPTCHA (e.g. an image of `482931`), a text area, a **Submit** button, and a **Refresh** button. +2. Type the digits shown in the CAPTCHA image into the text area and click **Submit**. If the entry matches, a dialog shows "verified"; otherwise it shows "Not verified" and generates a new CAPTCHA automatically. +3. Click **Refresh** at any time to replace the current CAPTCHA image with a new one. + ### Prerequisites 1. Install the dependencies by executing the following command ```pip install -r requirements.txt``` @@ -12,8 +18,3 @@ A simple image captcha genrator ![image](https://user-images.githubusercontent.com/39544459/137623915-1e837ada-f199-4513-a15d-ecbb969fd53e.png) ## *Mayur Singal* - - -## Pyodide-runnable - -No - both scripts use tkinter (and pygame/captcha) for a desktop GUI. diff --git a/projects/Card Game/Readme.md b/projects/Card Game/Readme.md index 54206fa7d..4cadbfbcf 100644 --- a/projects/Card Game/Readme.md +++ b/projects/Card Game/Readme.md @@ -41,7 +41,3 @@ War is over. Alice wins ## ENJOY THE GAME - -## Pyodide-runnable - -Yes - pure-stdlib console card game using only random and re. diff --git a/projects/Chat Application/readme.md b/projects/Chat Application/readme.md index 707387376..f3aa45341 100644 --- a/projects/Chat Application/readme.md +++ b/projects/Chat Application/readme.md @@ -39,8 +39,3 @@ This project is open-source and available under the [MIT License](LICENSE). Feel free to expand and customize this code to build more advanced chat applications with additional features, security, and user interfaces. If you have questions or encounter issues, please create an issue in the [GitHub repository](https://github.com/yourusername/chat-application). Enjoy exploring the world of chat systems with Python! - - -## Pyodide-runnable - -No - it uses raw TCP sockets and threading to run a network chat server and client. diff --git a/projects/Chat-GPT-Discord-Bot/README.md b/projects/Chat-GPT-Discord-Bot/README.md index 6ba517203..5582ace51 100644 --- a/projects/Chat-GPT-Discord-Bot/README.md +++ b/projects/Chat-GPT-Discord-Bot/README.md @@ -60,6 +60,17 @@ If you want to change your ``.env`` file name as well add this reference to the dotenv_path = os.path.join("path/to/env", "Env_Name_Here.env") ``` +## Example + +Once the bot is running and invited to your server, use slash commands in any channel: + +1. Type `/test_bot` — the bot replies privately with `Hello, @YourName`. +2. Type `/ping` — the bot replies privately with e.g. `Pong! Latency: 42ms`. +3. Type `/gpt_general_question prompt:What is the speed of light? gpt_model:gpt-3.5` — the bot posts an embed titled `General Question - "What is the speed of light?"` containing the ChatGPT answer. +4. Type `/gpt_correct_grammar text_to_correct:their going to the store` — the bot returns a grammar-corrected version in an embed. +5. Type `/dalle_3 prompt:a sunset over mountains img_dimensions:1024x1024 img_quality:standard img_style:vivid` — the bot posts a link to the generated image with an expiry countdown. +6. Type `/help` to see a list of all available commands. + # How to run Open a new command line in the same folder as the main.py script (Make sure python is installed and/or your python venv is active) and type: @@ -115,8 +126,3 @@ and click ``Copy Server ID`` as shown below: ### Note on GPT ``Credit balance``: Ensure you have an available ``Credit balance``. You can check on the ``Billing`` page in ``Settings`` or by clicking [Here](https://platform.openai.com/settings/organization/billing/overview). If you do not have a ``Credit balance`` you will need to add money (credit) to your account otherwise this discord bot's chat GPT functionality will not work. - - -## Pyodide-runnable - -No - it is a Discord bot that needs discord.py, network access, and the OpenAI API. diff --git a/projects/Chess/README.md b/projects/Chess/README.md index d08935518..78a716247 100644 --- a/projects/Chess/README.md +++ b/projects/Chess/README.md @@ -2,7 +2,14 @@ A two-player chess game built with Pygame. It renders the board and pieces in a desktop window using the image assets in the `Assets/` folder. -## How to run +## Example + +1. Run `python main.py`. A Pygame window opens showing an 8×8 chessboard with all pieces in their starting positions, rendered using the image assets from the `Assets/` folder. +2. Click a piece to select it; valid move squares are highlighted on the board. +3. Click a highlighted square to move the selected piece there. The turn passes to the other player. +4. Continue taking turns until a player's king is in checkmate or the game ends. + +## How to run on localhost ``` pip install pygame @@ -12,7 +19,3 @@ python main.py ## Dependencies - pygame - -## Pyodide-runnable - -No - it is a Pygame application that opens a desktop window and loads image assets. diff --git a/projects/Code_Reviewer/README.md b/projects/Code_Reviewer/README.md index dfae174a5..83225fb5a 100644 --- a/projects/Code_Reviewer/README.md +++ b/projects/Code_Reviewer/README.md @@ -2,7 +2,23 @@ A simple static code-review tool that parses a Python file into an AST and reports possible issues such as missing docstrings, undefined variables, style violations, and poorly formatted comments. -## How to run +## Example + +```text +Code Review Feedback: +- Function 'generate_recipe' should have a docstring or 'pass' statement. +- Variable 'recipe' is used but not defined. +- Improve comment style in line 12: '#TODO fix this' +- Code style issues found. Please check and fix them. +``` + +If no issues are found the output is: + +```text +No coding errors found. Code looks good! +``` + +## How to run on localhost ``` pip install pycodestyle @@ -15,7 +31,3 @@ By default it analyzes `../Recipe_Generator/recipe_generator.py`; edit the `pyth - `pycodestyle` - `ast` (standard library) - -## Pyodide-runnable - -No — it reads a Python source file from the real filesystem by relative path, and `pycodestyle` is not available in Pyodide. diff --git a/projects/Coin Flip/README.md b/projects/Coin Flip/README.md index acbc71211..a1212aee7 100644 --- a/projects/Coin Flip/README.md +++ b/projects/Coin Flip/README.md @@ -2,6 +2,19 @@ This is a Python program used to simulate a coin toss, in which a user is asked to pick a side (heads or tails), and the program selects a result at random between the two options. If the user's choice matches the result, they win the coin toss. +## Example + +```text +Pick a side for the coin toss (heads/tails): heads +You got... tails +OOF. Better luck next time. +Wanna play again? (yes/no): yes +Pick a side for the coin toss (heads/tails): tails +You got... tails +Nice, you won the coin toss!! +Wanna play again? (yes/no): no +``` + ### How to Play 1. When the program is initiated, you will be prompted to choose either "heads" or "tails". @@ -10,7 +23,3 @@ This is a Python program used to simulate a coin toss, in which a user is asked 4. After the end of each round, you will be asked if you want to play again. Type "yes" to continue playing or "no" to exit the program. Enjoy! - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program that only uses `random` and `input`/`print`. diff --git a/projects/Collatz_Conjecture/README.md b/projects/Collatz_Conjecture/README.md index 190e58e77..bb5d61f5f 100644 --- a/projects/Collatz_Conjecture/README.md +++ b/projects/Collatz_Conjecture/README.md @@ -9,6 +9,15 @@ This code snippet is a simple implementation of an iterator that generates the C ### Overview The provided code defines a Collatz class that initializes with a positive integer n and implements the iterator pattern to generate the Collatz sequence. The `__next__()` method computes the next number in the sequence according to the rules above. It raises a StopIteration exception when the sequence reaches 1. + +## Example + +```text +To generate a Collatz Sequence, +Please enter a positive integer number: 6 +3 10 5 16 8 4 2 1 +``` + #### Usage 1. When executed, the script prompts the user for a positive integer. @@ -16,17 +25,13 @@ The provided code defines a Collatz class that initializes with a positive integ 3. The sequence ends when the iterator reaches 1, triggering the StopIteration exception. 4. If an invalid input is entered (like a non-integer), a ValueError is raised, prompting the user with an error message. -#### How to Run +#### How to run on localhost - Run the script in your Python environment. - In terminal write this command - > python collatz.py - + > python collatz.py #### Error Handling - If the input is not a valid integer, a ValueError exception is raised, prompting the user to enter a valid integer. - If the iterator reaches 1, a StopIteration exception is raised, indicating the end of the sequence. -## Pyodide-runnable - -Yes — it is a pure-stdlib console program; the iterator and `input`/`print` work unchanged in the browser. diff --git a/projects/Comics_Scraper/README.md b/projects/Comics_Scraper/README.md index 7efa8b421..7701ea52d 100644 --- a/projects/Comics_Scraper/README.md +++ b/projects/Comics_Scraper/README.md @@ -2,7 +2,20 @@ Downloads comic images from xkcd.com — either every comic in bulk, or a single comic by number — saving them to a local `xkcd` folder. -## How to run +## Example + +```text +Choose your option: +1.Download all images 2.Download Specific image +2 +Enter any comic number between 1-2990: 42 +Download image http://imgs.xkcd.com/comics/geeks_and_nerds.png +Finished +``` + +To download every comic, choose option `1`. The script traverses pages backwards from the latest, printing each URL as it downloads into the `xkcd/` folder. + +## How to run on localhost ``` pip install requests beautifulsoup4 lxml @@ -14,7 +27,3 @@ python comicXCD_scraper.py - `requests` - `beautifulsoup4` (`bs4`) - `lxml` - -## Pyodide-runnable - -No — it scrapes the live xkcd.com website over the network and writes image files to disk. diff --git a/projects/Connect Four/README.md b/projects/Connect Four/README.md index b1b9c6bb0..1a1804a0f 100644 --- a/projects/Connect Four/README.md +++ b/projects/Connect Four/README.md @@ -2,7 +2,16 @@ A two-player Connect Four game with a graphical board rendered using pygame; players drop colored pieces by clicking columns until one connects four in a row. -## How to run +## Example + +1. The pygame window opens showing a 7-column by 6-row blue grid with black holes. +2. Player 1 (red) moves the mouse over the board — a red circle tracks the cursor along the top row. +3. Player 1 clicks a column; a red piece drops to the lowest available row in that column. +4. Player 2 (yellow) clicks a column; a yellow piece drops. +5. Players alternate until one connects four pieces horizontally, vertically, or diagonally. +6. The winning message (e.g. "Player 1 wins!!") appears in red at the top of the window, then the game pauses for 3 seconds and closes. + +## How to run on localhost ``` pip install pygame numpy @@ -13,7 +22,3 @@ python main.py - `pygame` - `numpy` - -## Pyodide-runnable - -No — it uses `pygame` for a graphical window and mouse input, which cannot run in a browser via Pyodide. diff --git "a/projects/Conway\342\200\231s-Game-Of-Life/README.md" "b/projects/Conway\342\200\231s-Game-Of-Life/README.md" index bd3a97a37..57933f94e 100644 --- "a/projects/Conway\342\200\231s-Game-Of-Life/README.md" +++ "b/projects/Conway\342\200\231s-Game-Of-Life/README.md" @@ -2,7 +2,16 @@ An implementation of Conway's Game of Life cellular automaton, animated with matplotlib. Supports random grids as well as glider and Gosper glider gun demos via command-line flags. -## How to run + +## Example + +1. Run with no flags: a 100x100 window opens showing a randomly populated grid that evolves each frame according to Conway's rules. +2. Run `python conwaygame.py --glider` to open a blank grid with a single glider pattern placed at position (1, 1) and watch it travel diagonally. +3. Run `python conwaygame.py --gosper` to start with a Gosper Glider Gun that continuously fires gliders across the grid. +4. Pass `--grid-size 50 --interval 100` to use a smaller 50x50 grid updated every 100 ms. +5. Pass `--mov-file output.mp4` to save the animation to a video file instead of displaying it. + +## How to run on localhost ``` pip install numpy matplotlib @@ -15,7 +24,3 @@ Optional flags: `--grid-size`, `--interval`, `--glider`, `--gosper`, `--mov-file - `numpy` - `matplotlib` - -## Pyodide-runnable - -No — it relies on `matplotlib` interactive animation (`plt.show`) and command-line argument parsing, which do not work in the Pyodide browser environment. diff --git a/projects/Countdown/README.md b/projects/Countdown/README.md index b9b847d3c..986a4193a 100644 --- a/projects/Countdown/README.md +++ b/projects/Countdown/README.md @@ -2,7 +2,19 @@ A simple console countdown timer: enter a number of seconds and it counts down in `MM:SS` format, printing "Time Up" when finished. -## How to run +## Example + +```text +Enter the time in seconds: 10 +00:10 +00:09 +00:08 +... +00:01 +Time Up +``` + +## How to run on localhost ``` python countdown.py @@ -11,7 +23,3 @@ python countdown.py ## Dependencies Standard library only (`time`). - -## Pyodide-runnable - -Yes — it uses only `time` and `print`/`input`; note that `time.sleep` blocks, so the countdown updates will appear all at once at the end in a browser console. diff --git a/projects/CustomEncryptionDecryption/README.md b/projects/CustomEncryptionDecryption/README.md index affc6e2f0..473894efb 100644 --- a/projects/CustomEncryptionDecryption/README.md +++ b/projects/CustomEncryptionDecryption/README.md @@ -9,6 +9,41 @@ # Copy code pip install -r requirements.txt +## Example + +```text +Welcome to Custom Encryption & Decryption Program! + +Do you already have a CIPHER TEXT ? + a. Press 'c' to continue with the default CIPHER TEXT + b. Press 'y' if you already have a CIPHER TEXT + c. Press 'g' to generate a CIPHER TEXT + d. Press 'r' to report a BUG + +You have chosen: g + +Your CIPHER TEXT has been generated successfully! and added into the 'cipher_text.txt' file! + +Please choose one option from below: + a. Press 'e' to ENCRYPT a TEXT + b. Press 'd' to DECRYPT a ENCRYPTED TEXT + c. Press 'q' to QUIT the program ? + +You have chosen: e + +Please enter your text : Hello World + +Your ENCRYPTED TEXT text is: dGhpcyBpcyBhbiBleGFtcGxl... + +Do you want to continue to the program ? + a. Press 'c' to continue! + b. Press 'any other key' to quit the program! + +You have chosen: q + +Thanks for using custom encryption & decryption program! Have great day :) +``` + # Getting Started Clone the Repository: @@ -57,7 +92,3 @@ Thank you for using this program. If you have any issues or questions, feel free to open an issue or contact me. Happy encrypting and decrypting! 🚀 - -# Pyodide-runnable - - No — it depends on `PyJWT` and `python-dotenv` (not available in Pyodide) and reads environment variables and a cipher-text file. diff --git a/projects/Data Entry Automation/Readme.md b/projects/Data Entry Automation/Readme.md index f8f63ab1d..b2431f8f6 100644 --- a/projects/Data Entry Automation/Readme.md +++ b/projects/Data Entry Automation/Readme.md @@ -8,6 +8,10 @@ _-> bs4_ _-> time_ If you find any Difficulty in understanding things, then check for the docs of these library. -## Pyodide-runnable -No — it uses `selenium` to drive a Chrome browser, plus `requests` for web scraping. +## Example + +1. Set `chrome_path`, `FORM`, and `HEADERS["User-Agent"]` in `main.py` with your ChromeDriver path, Google Form URL, and browser user-agent string. +2. Run `python main.py`. The script sends an HTTP request to Zillow and scrapes rental property addresses, prices, and links into three lists. +3. A Chrome browser window opens automatically. For each scraped listing, Selenium navigates to the Google Form, fills in the address, price, and link fields, and clicks Submit. +4. After all entries are submitted, download the form responses as a CSV from Google Forms to get the collected data in spreadsheet format. diff --git a/projects/Data_Abstractor/README.md b/projects/Data_Abstractor/README.md index d2112a75a..e72c0e66a 100644 --- a/projects/Data_Abstractor/README.md +++ b/projects/Data_Abstractor/README.md @@ -6,13 +6,16 @@ For example, if you want to get data of male customers whose age is between 18 a Please note that this a sample project to understand the concepts of SQL and web development using Flask. It does not contain any personal data. You can take inspiration from this algorithm to create your own projects. +## Example + +1. Run `python3 main.py` and open `http://127.0.0.1:5000/` in a browser. +2. The home page shows a filter form. Tick the "Age" checkbox and enter `18` and `25` in the age-range fields; tick the "Gender" checkbox and select "Male". +3. Click the submit button. The app builds a SQL query against the loaded CSV data using DuckDB and redirects to `/query_output`. +4. A table is rendered showing columns: User ID, User Name, Age, Gender, Country, Sign-UP Date, Subscription Plan, Device, Login, Added To Cart, Purchased Item, and Time of Event — filtered to male customers aged 18–25. + ## Usage 1. To install the required packages, run `pip3 install -r ./requirements.txt` in Terminal while working in the CURD-API directory. 2. Run: `python3 main.py` If everything works successfully, the website will be available on the localhost server -> http://127.0.0.1:5000/ - -## Pyodide-runnable - -No — it runs a Flask web server, which requires a server process and cannot run in the Pyodide browser sandbox. diff --git a/projects/Desktop Weather Notifier/README.md b/projects/Desktop Weather Notifier/README.md index b588cb27c..5370ac51b 100644 --- a/projects/Desktop Weather Notifier/README.md +++ b/projects/Desktop Weather Notifier/README.md @@ -2,7 +2,19 @@ Fetches current weather for a city from weatherapi.com once per hour and shows it as a native desktop notification, including temperature, wind, and precipitation. -## How to run +## Example + +1. Set your weatherapi.com API key in the `API_KEY` variable and optionally change `CITY` (default: `Haridwar`). +2. Run the script. After startup it immediately fetches weather data and shows a desktop notification titled e.g. **"Weather in Haridwar on 22 May"** with a body like: + ``` + Partly cloudy at about 3pm + 14kmph winds from the NorthEast + Feels like 28 °C + Precipation: 0.0mm + ``` +3. The script then sleeps and repeats the notification every hour while running. + +## How to run on localhost ``` pip install plyer requests @@ -15,7 +27,3 @@ Set your weatherapi.com API key in the `API_KEY` variable. - `plyer` - `requests` - -## Pyodide-runnable - -No — it makes network requests to a weather API and uses `plyer` to display OS-level desktop notifications. diff --git a/projects/Desktop-Notification/README.md b/projects/Desktop-Notification/README.md index cf27a510b..929cb3786 100644 --- a/projects/Desktop-Notification/README.md +++ b/projects/Desktop-Notification/README.md @@ -2,7 +2,13 @@ Displays a simple Windows toast notification using the `win10toast` library. -## How to run +## Example + +1. Run `python main.py`. +2. A Windows toast notification appears in the bottom-right corner titled **"Python project"** with the body text **"Here is your notification body"**. +3. The notification stays visible for 20 seconds, then dismisses automatically. + +## How to run on localhost ``` pip install win10toast @@ -12,7 +18,3 @@ python main.py ## Dependencies - `win10toast` (Windows only) - -## Pyodide-runnable - -No — it relies on `win10toast` to show native Windows toast notifications, which is not available in a browser. diff --git a/projects/Diabetes Monitoring Dashboard/README.md b/projects/Diabetes Monitoring Dashboard/README.md index 54a4766c1..c32a8292d 100644 --- a/projects/Diabetes Monitoring Dashboard/README.md +++ b/projects/Diabetes Monitoring Dashboard/README.md @@ -27,6 +27,13 @@ Here are some performance metrics for our model: - SVM F1 Score: 0.6464646464646464 +## Example + +1. Run `streamlit run webApp.py`. The dashboard opens in a browser. +2. Fill in the real-time health parameters requested by the form (e.g. blood glucose level, age, BMI). +3. Click the predict button. The SVM model returns a prediction — either diabetic or non-diabetic — along with a confidence indicator. +4. The integrated ChatGPT panel (via the OpenAI API key set in `chat.py`) provides personalised lifestyle improvement suggestions based on the result. + # Requirements Install the till-needed packages using the command : @@ -46,7 +53,3 @@ streamlit run webApp.py ``` The application will deploy a webapp on localhost which then can be accesed through web browsers (Chrome recommended!) by any client on that network. - -# Pyodide-runnable - -No — it runs a Streamlit web server, makes network requests, uses scikit-learn, and calls the OpenAI API. diff --git a/projects/Dice Simulator/Readme.md b/projects/Dice Simulator/Readme.md index 4e7baa339..8acb5c0e3 100644 --- a/projects/Dice Simulator/Readme.md +++ b/projects/Dice Simulator/Readme.md @@ -3,6 +3,27 @@ Dual Dice Simulator
    +## Example + +```text +Roll the dice? (y/n) : y +dice rolled 3 and 5 + __________ +| | +| 3 ● | +| ● | +| ● | +|__________| + __________ +| | +| ● 5 ● | +| ● | +| ● ● | +|__________| + +Roll again? (y/n): n +``` + ## Project Description : A simple python program which uses basic concepts to show a simulation of two dices on the screen with their valid output. @@ -17,7 +38,3 @@ A simple python program which uses basic concepts to show a simulation of two di - Very Useful when you doubt the judgement and choice of traditional physical dices. - Run the dice any number of times till the end of the game - Any further improvements in this project are fully welcomed. - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using only `random` and `input`/`print`. diff --git a/projects/Dice-Roll-Simulator/README.md b/projects/Dice-Roll-Simulator/README.md index 54a43c78a..4eaaf867b 100644 --- a/projects/Dice-Roll-Simulator/README.md +++ b/projects/Dice-Roll-Simulator/README.md @@ -2,6 +2,22 @@ This is a simple Python program that simulates rolling a 6-sided dice. +## Example + +```text +1. Roll the dice +2. Exit +What do you want to do? 1 +You rolled: 4 +1. Roll the dice +2. Exit +What do you want to do? 1 +You rolled: 2 +1. Roll the dice +2. Exit +What do you want to do? 2 +``` + ## How to Use 1. Clone or download the repository to your local machine. @@ -31,8 +47,3 @@ This script requires Python to be installed on your system. It also uses the `ra ## Contribution Contributions are welcome! If you find any bugs or have suggestions for improvement, please open an issue or create a pull request on the [GitHub repository](https://github.com/example/dice-roller). - - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using only the `random` module and `input`/`print`. diff --git a/projects/Discord Bot/README.md b/projects/Discord Bot/README.md index 8205e19a4..d13f30b84 100644 --- a/projects/Discord Bot/README.md +++ b/projects/Discord Bot/README.md @@ -2,7 +2,26 @@ A simple Discord bot built with discord.py. It responds to `!hello` and `!random` messages and provides an `?ask` command that gives a random Magic 8-Ball style answer. -## How to run +## Example + +``` +# In a Discord channel: + +User: !hello +Bot: Hello @User! + +User: !random +Bot: 47 + +User: ?ask Will it rain tomorrow? +Bot: @User asks: **Will it rain tomorrow?** + My reply: **Outlook good** + +User: ?ask +Bot: You didn't provide me with a question! +``` + +## How to run on localhost ``` pip install discord.py @@ -14,7 +33,3 @@ Replace `"Your Token Here"` in the script with a valid Discord bot token. ## Dependencies - `discord.py` - -## Pyodide-runnable - -No — it connects to Discord's gateway over the network and runs a long-lived bot event loop. diff --git a/projects/DnD Dice/README.md b/projects/DnD Dice/README.md index e5b66eb62..0ed6dc3ff 100644 --- a/projects/DnD Dice/README.md +++ b/projects/DnD Dice/README.md @@ -2,7 +2,25 @@ A console dice roller for tabletop role-playing games. Pick a dice type (D4, D6, D8, D10, D12, D20, or D100) and it prints a random roll. -## How to run +## Example + +```text +Pick a Dice type: +1. D4 +2. D6 +3. D8 +4. D10 +5. D12 +6. D20 +7. D100 + +3 +7 +``` + +(Chose option 3 for a D8; the program printed a random roll of 7.) + +## How to run on localhost ``` python main.py @@ -13,7 +31,3 @@ Requires Python 3.10+ (uses the `match` statement). ## Dependencies Standard library only (`random`). - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using `input`, `print`, and `random`. diff --git a/projects/Drowsiness Detector/Readme.md b/projects/Drowsiness Detector/Readme.md index 8b1ef853c..e873884e3 100644 --- a/projects/Drowsiness Detector/Readme.md +++ b/projects/Drowsiness Detector/Readme.md @@ -39,31 +39,23 @@ The return value of the eye aspect ratio will be approximately constant when the If the eye is closed, the eye aspect ratio will again remain approximately constant, but will be much smaller than the ratio when the eye is open. -## Command Line Argument - -
  • --shape-predictor : This is the path to dlib’s pre-trained facial landmark detector. You can download the detector along with the source code to this tutorial by using the “Downloads” section at the bottom of this blog post. -
  • --alarm : Here you can optionally specify the path to an input audio file to be used as an alarm. -
  • --webcam : This integer controls the index of your built-in webcam/USB camera.
  • - - - - - - - - - - - - - - +## Example +```text +$ python main.py --shape-predictor shape_predictor_68_face_landmarks.dat \ + --alarm alarm.wav --webcam 0 +[INFO} loading facial landmark predictor... +[INFO] starting video stream thread... +``` +1. A webcam window opens titled "Frame" showing the live video feed with green contours drawn around detected eye regions. +2. The current Eye Aspect Ratio (EAR) is displayed in the top-right corner of the frame. +3. If the driver's eyes remain closed long enough (EAR stays below 0.3 for 48 consecutive frames), the text **"DROWSINESS ALERT!"** appears in red and the alarm audio file plays in a background thread. +4. Press `q` to quit the window. +## Command Line Argument - -## Pyodide-runnable - -No — it uses `dlib`, `OpenCV`, a live webcam, audio playback, and threading. +
  • --shape-predictor : This is the path to dlib’s pre-trained facial landmark detector. You can download the detector along with the source code to this tutorial by using the “Downloads” section at the bottom of this blog post. +
  • --alarm : Here you can optionally specify the path to an input audio file to be used as an alarm. +
  • --webcam : This integer controls the index of your built-in webcam/USB camera.
  • diff --git a/projects/Encryptor and Decryptor/README.md b/projects/Encryptor and Decryptor/README.md index e6ff92966..019146041 100644 --- a/projects/Encryptor and Decryptor/README.md +++ b/projects/Encryptor and Decryptor/README.md @@ -2,7 +2,16 @@ A Tkinter GUI application that encrypts and decrypts text using Base64 encoding, gated behind a secret key. -## How to run +## Example + +1. The Tkinter window opens (375x398) titled "Encrypt Decrypt". +2. Type `Hello World` into the "Enter text" area. +3. Enter `1234` in the "Enter secret key" field (the correct key). +4. Click **ENCRYPT** — a new red window opens titled "Encryption" showing the Base64-encoded result, e.g. `SGVsbG8gV29ybGQK`. +5. Paste that encoded string back into the main text area, enter `1234` again, and click **DECRYPT** — a green window opens showing `Hello World`. +6. Click **RESET** to clear both the text area and the key field. + +## How to run on localhost ``` python encrypt.py @@ -11,7 +20,3 @@ python encrypt.py ## Dependencies Standard library only (`tkinter`, `base64`). - -## Pyodide-runnable - -No — it builds a graphical interface with `tkinter`, which is unavailable in Pyodide. diff --git a/projects/English Thesaurus/README.md b/projects/English Thesaurus/README.md index de33e98eb..cb2070920 100644 --- a/projects/English Thesaurus/README.md +++ b/projects/English Thesaurus/README.md @@ -2,7 +2,26 @@ A console thesaurus/dictionary that looks up word meanings from a local `data.json` file, suggesting close matches when a word is misspelled. -## How to run +## Example + +```text +Ctrl-C to exit + +Enter the word to find meaning: happy +Meaning 1: feeling or showing pleasure or contentment + +Enter the word to find meaning: happpy +Did u mean the word happy ? + +Press Yes-Y,No-N,Exit-E: Y +Meaning 1: feeling or showing pleasure or contentment + +Enter the word to find meaning: xyzabc +``` + +(No close match found — the program prints nothing and prompts again.) + +## How to run on localhost ``` python App.py @@ -13,7 +32,3 @@ Run from the repository root so the `projects\English Thesaurus\data.json` path ## Dependencies Standard library only (`json`, `difflib`). - -## Pyodide-runnable - -No — it loads `data.json` using a hard-coded Windows relative path; it could only run in Pyodide if that data file were loaded into the virtual filesystem first. diff --git a/projects/Expense-Tracker/README.md b/projects/Expense-Tracker/README.md index 41eb68805..9d6b46500 100644 --- a/projects/Expense-Tracker/README.md +++ b/projects/Expense-Tracker/README.md @@ -112,9 +112,3 @@ which would use dummy data to test the app. Excel for easy sharing or printing. - [ ] Currency Converter: For users dealing with multiple currencies, this feature provides an option to convert expenses to a preferred currency. - - - -## Pyodide-runnable - -No — it is a `customtkinter`/`tkinter` GUI application and also depends on `reportlab`, `pandas`, and `XlsxWriter`. diff --git a/projects/Eye Blink Detection/Readme.md b/projects/Eye Blink Detection/Readme.md index 9a5c8ad1f..5766c68dc 100644 --- a/projects/Eye Blink Detection/Readme.md +++ b/projects/Eye Blink Detection/Readme.md @@ -14,18 +14,24 @@ The eye aspect ratio is instead a much more elegant solution that involves a ver This method for eye blink detection is fast, efficient, and easy to implement. -### Understanding the “eye aspect ratio” (EAR) - -In terms of blink detection, we are only interested in two sets of facial structures — the eyes. - -Each eye is represented by 6 (x, y)-coordinates, starting at the left-corner of the eye (as if you were looking at the person), and then working clockwise around the remainder of the region. +## Example +```text +$ python main.py --shape-predictor shape_predictor_68_face_landmarks.dat \ + --video sample_video.mp4 +[INFO] loading facial landmark predictor... +[INFO] starting video stream thread... +``` +1. An OpenCV window opens titled “Frame” showing the video with green convex-hull outlines drawn around both eyes. +2. The top-left corner displays the running blink count, e.g. `Blinks: 3`. +3. The top-right corner shows the current EAR value, e.g. `EAR: 0.31`. +4. Each time the EAR drops below 0.3 for 3 consecutive frames, the blink counter increments. +5. Press `q` to stop playback and close the window. +### Understanding the “eye aspect ratio” (EAR) +In terms of blink detection, we are only interested in two sets of facial structures — the eyes. - -## Pyodide-runnable - -No — it uses `dlib`, `OpenCV`, and a video/webcam stream, none of which are available in Pyodide. +Each eye is represented by 6 (x, y)-coordinates, starting at the left-corner of the eye (as if you were looking at the person), and then working clockwise around the remainder of the region. diff --git a/projects/Fidget Spinner Game/README.md b/projects/Fidget Spinner Game/README.md index 275e0202d..84fcf7f09 100644 --- a/projects/Fidget Spinner Game/README.md +++ b/projects/Fidget Spinner Game/README.md @@ -2,7 +2,14 @@ A small interactive toy built with Python's `turtle` module. Press the space bar to "flick" an on-screen fidget spinner; it spins and gradually slows down. -## How to run +## Example + +1. A 420×420 turtle window opens showing a three-armed spinner (red, green, and blue dots) at rest. +2. Press the space bar — each press adds spin, causing the arms to rotate. +3. The spinner gradually slows down on its own after each flick. +4. Press space repeatedly to keep it spinning faster. + +## How to run on localhost ``` python main.py @@ -11,7 +18,3 @@ python main.py ## Dependencies Standard library only (`turtle`). - -## Pyodide-runnable - -No — the `turtle` module depends on Tkinter, which has no browser support. diff --git a/projects/File_Organizer/README.md b/projects/File_Organizer/README.md index b1721038d..b6112f1f5 100644 --- a/projects/File_Organizer/README.md +++ b/projects/File_Organizer/README.md @@ -2,7 +2,21 @@ A command-line tool that organizes a directory by file type. It scans the given directory and moves files into category folders (Music, Videos, Documents, Pictures, etc.) based on their extensions. -## How to run +## Example + +```text +$ python main.py /home/user/Downloads -v +Creating 'Documents' folder. +Moved 'report.pdf' to 'Documents' folder. +Moved 'notes.txt' to 'Documents' folder. +Creating 'Pictures' folder. +Moved 'photo.jpg' to 'Pictures' folder. +Creating 'Music' folder. +Moved 'song.mp3' to 'Music' folder. +Organizing files complete! +``` + +## How to run on localhost ``` python main.py [-v] @@ -13,7 +27,3 @@ python main.py [-v] ## Dependencies Standard library only (`argparse`, `os`, `logging`, `shutil`). - -## Pyodide-runnable - -No — it walks and moves files on the real filesystem, which is not available in a browser sandbox. diff --git a/projects/Find_imbd_rating/README.md b/projects/Find_imbd_rating/README.md index 436018f61..c54c308bd 100644 --- a/projects/Find_imbd_rating/README.md +++ b/projects/Find_imbd_rating/README.md @@ -2,7 +2,25 @@ Reads the names of film files from a local directory, searches IMDb for each title, scrapes the rating and genre, and writes the results to `film_ratings.csv`. -## How to run +## Example + +```text +Enter the path where your films are: /home/user/films +https://www.imdb.com/search/title/?title=inception +https://www.imdb.com/search/title/?title=the+dark+knight +https://www.imdb.com/search/title/?title=interstellar +``` + +After the script finishes, `film_ratings.csv` is written with columns `Film Name`, `Rating`, and `Genre`: + +```text +Film Name,Rating,Genre +Inception,8.8," Sci-Fi, Thriller" +The Dark Knight,9.0," Action, Crime, Drama" +Interstellar,8.6," Adventure, Drama, Sci-Fi" +``` + +## How to run on localhost ``` pip install beautifulsoup4 requests pandas @@ -14,7 +32,3 @@ python Find_imbd_rating.py - beautifulsoup4 - requests - pandas - -## Pyodide-runnable - -No — it makes live HTTP requests to IMDb and reads a real directory listing, neither of which is available in a browser sandbox. diff --git a/projects/Flappybird_game/README.md b/projects/Flappybird_game/README.md index c58c65af6..348d8fc69 100644 --- a/projects/Flappybird_game/README.md +++ b/projects/Flappybird_game/README.md @@ -2,7 +2,15 @@ A clone of the classic Flappy Bird game built with Pygame. Tap the space bar to flap the bird through gaps in the pipes; the game tracks score and high score. -## How to run +## Example + +1. The game window (288×512) opens showing a night-sky background, a scrolling floor, and a blue bird in the centre. +2. Press space bar to flap the bird upward; gravity pulls it down continuously. +3. Green pipes scroll in from the right with gaps to fly through. The score increments while the bird stays alive. +4. If the bird hits a pipe or the floor, a hit sound plays and the game-over screen appears showing `Score:` and `High Score:`. +5. Press space bar again to restart with the score reset to 0. + +## How to run on localhost ``` pip install pygame @@ -12,7 +20,3 @@ python main.py ## Dependencies - pygame - -## Pyodide-runnable - -No — it uses Pygame for graphics, sound, and an event loop, which is not available in a browser sandbox. diff --git a/projects/Full_Calendar/README.md b/projects/Full_Calendar/README.md index f2d2c9363..21890d943 100644 --- a/projects/Full_Calendar/README.md +++ b/projects/Full_Calendar/README.md @@ -2,7 +2,14 @@ A Tkinter GUI application that asks for a year and displays the full calendar for that year in a new window. -## How to run +## Example + +1. The main window (500×400) opens with the title "HOME", a green "Welcome to the calendar Application" label, a "Please enter a year" prompt, and a text entry field. +2. Type `2024` in the entry field and click "Show Calendar". +3. A second window (550×600) opens titled "Calendar" and displays the full 12-month calendar for 2024 in a monospaced font. +4. Click "Exit" on the main window to close the application. + +## How to run on localhost ``` python GUI_calendar_generator.py @@ -11,7 +18,3 @@ python GUI_calendar_generator.py ## Dependencies Standard library only (`tkinter`, `calendar`). - -## Pyodide-runnable - -No — it builds a Tkinter GUI, which has no browser support. diff --git a/projects/GPA-Calculator/README.md b/projects/GPA-Calculator/README.md index dd15c526f..a0dec2f16 100644 --- a/projects/GPA-Calculator/README.md +++ b/projects/GPA-Calculator/README.md @@ -2,7 +2,27 @@ A console tool that computes a semester GPA. It asks for the number of courses, each course's credits, and marks (either a total or split into mid-sem, internal and end-sem), then derives grade points and a weighted GPA. -## How to run +## Example + +```text +------------------------------------------ +GPA Calculator: +------------------------------------------ +Enter Number Of Courses: 2 +Enter 1 to individually enter Mid Sem, Internals & End Sem Marks +Else Enter 0 to enter Total Marks: 0 +Enter Course Name: Mathematics +Enter Total Credits Of Mathematics: 4 +Enter Total Marks Acquired for Mathematics: 82 + +Enter Course Name: Physics +Enter Total Credits Of Physics: 3 +Enter Total Marks Acquired for Physics: 74 + +Your Credits for this Semester is: 8.857142857142858 +``` + +## How to run on localhost ``` python GPA-Calculator.py @@ -11,7 +31,3 @@ python GPA-Calculator.py ## Dependencies Standard library only (`math`). - -## Pyodide-runnable - -Yes — console program using only `input()`/`print()` and the `math` module. diff --git "a/projects/GUI based image display and transfer in\302\240python/README.md" "b/projects/GUI based image display and transfer in\302\240python/README.md" index 75a027c96..931584f4f 100644 --- "a/projects/GUI based image display and transfer in\302\240python/README.md" +++ "b/projects/GUI based image display and transfer in\302\240python/README.md" @@ -2,7 +2,16 @@ A Tkinter desktop application for viewing and copying images. It lets you open an image through a file dialog, displays it in the window, and saves (copies) it to a new location of your choice. -## How to run + +## Example + +1. The window opens titled "Image Viewer and Transfer" with an "Open Image" button and a "Save Image" button. +2. Click "Open Image" — a file dialog appears; select any image file (e.g. `photo.jpg`). +3. The image is displayed in the window. +4. Click "Save Image" — a save-as dialog appears; choose a destination path and filename (e.g. `copy.png`). +5. The image is copied to the chosen location. + +## How to run on localhost ``` pip install pillow @@ -12,7 +21,3 @@ python GUIimage.py ## Dependencies - pillow - -## Pyodide-runnable - -No — it builds a Tkinter GUI with file dialogs and copies files on disk, none of which are available in a browser. diff --git a/projects/Game of Cricket/README.md b/projects/Game of Cricket/README.md index 476eebbe8..887d96eba 100644 --- a/projects/Game of Cricket/README.md +++ b/projects/Game of Cricket/README.md @@ -2,7 +2,37 @@ A text-based cricket game played against the computer. You pick a number from 1 to 6 each ball; matching the computer's number loses a wicket, otherwise you score runs. Includes a toss and a two-over innings for each side. -## How to run +## Example + +```text +~ Welcome to the Game of Cricket ~ + +---------- Start Game ---------- +Choose heads or tails: heads + +Toss Result: Heads +User won the toss +Choose to bat or bowl: bat + +You's Innings Begins +Choose any number from 1 to 6: 4 +Your choice: 4 +Computer's choice: 2 +Total Score: 4/0 +Balls remaining: 11 +Choose any number from 1 to 6: 3 +Your choice: 3 +Computer's choice: 3 +Total Score: 4/1 +Balls remaining: 10 +... +~~~~~~~~~~ Result ~~~~~~~~~~ +Your total runs: 22 +Computer's total runs: 18 +Congratulations! You won the Match by 4 runs. +``` + +## How to run on localhost ``` python main.py @@ -11,7 +41,3 @@ python main.py ## Dependencies Standard library only (`random`). - -## Pyodide-runnable - -Yes — console game using only `input()`/`print()` and the `random` module. diff --git a/projects/Gpt-And-Langchain/README.md b/projects/Gpt-And-Langchain/README.md index 8d75b6ada..853887d89 100644 --- a/projects/Gpt-And-Langchain/README.md +++ b/projects/Gpt-And-Langchain/README.md @@ -38,14 +38,14 @@ DATABASE_ID = 'Notion Database ID' SERPAPI_API_KEY = 'SERP API Key' ``` +## Example + +1. Start the bot with `py telegram-bot.py`. +2. Open Telegram and send `/start` to the bot — it replies "Howdy, how are you doing?". +3. Send `/Notion` — the bot prompts "split differnet data with *,*". +4. Reply with your content; the bot uses the LangChain/OpenAI pipeline to generate a title and YouTube URL, then creates a Notion page with those values. + ### Runnning in local Host ``` py telegram-bot.py ``` - -## Pyodide-runnable - -No — it runs a Telegram bot and calls the OpenAI, Notion, and SerpAPI services over the network, none of which is available in a browser sandbox. - - - diff --git a/projects/Gradient-Image/README.md b/projects/Gradient-Image/README.md index cfcde3c60..be9be567e 100644 --- a/projects/Gradient-Image/README.md +++ b/projects/Gradient-Image/README.md @@ -2,7 +2,16 @@ An OpenCV demo that loads an image and displays edge-detection results — Laplacian, combined Sobel gradient, and Canny — in separate windows for a pencil-shaded style representation. -## How to run +## Example + +1. Place an image at `Photos/image.jpg` and run `python gradient.py`. +2. Three windows open side by side: + - **Laplacian Image** — highlights rapid intensity changes as bright edges on a black background. + - **Sobel Final** — shows combined horizontal and vertical gradient magnitude. + - **Canny** — clean single-pixel edge outlines using Canny detection (thresholds 150–175). +3. Press any key to close all windows. + +## How to run on localhost ``` pip install opencv-python numpy @@ -13,7 +22,3 @@ python gradient.py - opencv-python - numpy - -## Pyodide-runnable - -No — it uses OpenCV GUI windows (`cv.imshow`), which are not available in a browser. diff --git a/projects/Guess Number/README.md b/projects/Guess Number/README.md index 39d51051f..9cbf72df9 100644 --- a/projects/Guess Number/README.md +++ b/projects/Guess Number/README.md @@ -5,7 +5,27 @@ A number guessing game. Two versions are included: - `guess_number.py` — a console version where the program picks a random number 1-10 and gives higher/lower hints until you find it, with replay support. - `guess_num_v2.py` — a Tkinter GUI version of the same game. -## How to run +## Example + +```text +Welcome to Number Guesser. If you'd like to play, press 'Y' or press 'X' if you want to exit: Y + +Enter number between 1 to 10: 5 + +Number is higher than 5 + +Enter number between 6 to 10: 8 + +Number is lower than 8 + +Enter number between 6 to 7: 7 + +Congrats! You've guessed the correct number! It was 7. + +You have tried 3 times to find the number. +``` + +## How to run on localhost ``` python guess_number.py @@ -20,7 +40,3 @@ python guess_num_v2.py ## Dependencies Standard library only (`random`, plus `tkinter` for the GUI version). - -## Pyodide-runnable - -Partial — `guess_number.py` runs in Pyodide (console, `input()`/`print()` only); `guess_num_v2.py` does not because it uses Tkinter, which has no browser support. diff --git a/projects/Guess The Word/README.md b/projects/Guess The Word/README.md index d8e5f221d..e1ee03238 100644 --- a/projects/Guess The Word/README.md +++ b/projects/Guess The Word/README.md @@ -2,7 +2,34 @@ A word guessing game (Hangman-style without the drawing). The program picks a random programming-language word and you have six attempts to reveal it one letter at a time. -## How to run +## Example + +```text +Welcome to the Word Guessing Game! +You have 6 attempts to guess the word. +_ _ _ _ _ _ +Guess a letter: p +Correct guess! +p _ _ _ _ _ +Guess a letter: y +Correct guess! +p y _ _ _ _ +Guess a letter: t +Correct guess! +p y t _ _ _ +Guess a letter: h +Correct guess! +p y t h _ _ +Guess a letter: o +Correct guess! +p y t h o _ +Guess a letter: n +Correct guess! +p y t h o n +Congratulations! You've guessed the word: python +``` + +## How to run on localhost ``` python Guess_the_word.py @@ -11,7 +38,3 @@ python Guess_the_word.py ## Dependencies Standard library only (`random`). - -## Pyodide-runnable - -Yes — console game using only `input()`/`print()` and the `random` module. diff --git a/projects/HandCricket/Readme.md b/projects/HandCricket/Readme.md index 1c49e372e..fa887d406 100644 --- a/projects/HandCricket/Readme.md +++ b/projects/HandCricket/Readme.md @@ -12,6 +12,37 @@ This is a simple command-line-based Hand Cricket game implemented in Python. It - Realistic cricket scoring and gameplay. +## Example + +```text +Welcome Hand Cricket +You will be playing against another player +Enter the number of overs (1-10): 2 +Toss time! +Choose heads (1) or tails (2): 1 +It's Heads! +Player 1 won the toss! +Player 1, choose 1 to bat first, 2 to bowl first: 1 +Select difficulty level (1-Easy, 2-Medium, 3-Hard): 1 + +Match Summary +============= +Overs: 2 + +Over 1, Player 1: 10 wickets left, Player 2: 10 wickets left +Player's turn - Batting +Over 1, Ball 1: Enter your shot (1-6): 4 +You chose 4, Opponent chose 2 +Player's score is 4 +... +Match Result +============ +Player 1's score = 28 +Player 2's score = 19 +Player 1 won +Thank you for playing and have a good day :) +``` + ### Installation 1. Clone this repository to your local machine: @@ -59,7 +90,3 @@ Contributions are welcome! If you'd like to contribute to this project, please f ## Acknowledgments - This game was inspired by the popular hand cricket game played by many cricket enthusiasts. - Special thanks to the Python community for providing helpful libraries and resources. - -## Pyodide-runnable - -Yes — console game using only `input()`/`print()` and the standard-library `random` and `time` modules. diff --git a/projects/HandTrack/README.md b/projects/HandTrack/README.md index 1fad71907..6854caf4a 100644 --- a/projects/HandTrack/README.md +++ b/projects/HandTrack/README.md @@ -2,7 +2,15 @@ Real-time hand tracking with OpenCV and MediaPipe. `base.py` and `HTrackMod.py` detect and draw hand landmarks from the webcam, and `VolumeControl.py` maps the pinch distance between fingers to the system volume. -## How to run +## Example + +1. Run `python base.py`. A webcam window opens showing the live feed. +2. Hold a hand in front of the camera — MediaPipe detects it and draws 21 landmarks (green dots and connecting lines) over each finger joint. +3. The frame rate (FPS) is printed to the console each second. +4. To control system volume, run `python VolumeControl.py` instead. Pinch your thumb and index finger together to lower the volume; spread them apart to raise it. +5. Press `q` to quit. + +## How to run on localhost ``` pip install opencv-python mediapipe numpy pycaw comtypes @@ -15,7 +23,3 @@ python base.py - mediapipe - numpy - pycaw, comtypes (for `VolumeControl.py`) - -## Pyodide-runnable - -No — it relies on webcam capture, OpenCV GUI windows, MediaPipe, and Windows audio APIs, none of which are available in a browser. diff --git a/projects/Hangman/README.md b/projects/Hangman/README.md index a1406f50f..34e5722ff 100644 --- a/projects/Hangman/README.md +++ b/projects/Hangman/README.md @@ -2,7 +2,42 @@ The classic Hangman game in the console. Pick a difficulty (easy/medium/hard), then guess the secret word letter by letter while ASCII art tracks your remaining tries. Word lists live in `RandomWords.py`. -## How to run +## Example + +```text +Choose difficulty level (easy, medium, hard): medium + +-------------Welcome to Hangman------------- + + -------- + | | + | + | + | + | + - +______ +Guess the word:- e +Good job, E is in the word! +_ E _ _ _ _ + +Guess the word:- a +A is not in the word. + -------- + | | + | O + | + | + | + - +_ E _ _ _ _ + +Guess the word:- PYTHON +Congrats, you guessed the word! You win! +Do you want to play Hangman? (y/n): n +``` + +## How to run on localhost ``` python main.py @@ -11,7 +46,3 @@ python main.py ## Dependencies Standard library only (`random`). - -## Pyodide-runnable - -Yes — console game using only `input()`/`print()` and the `random` module. diff --git a/projects/Higher-Lower/README.md b/projects/Higher-Lower/README.md index d5efb3624..f50adead9 100644 --- a/projects/Higher-Lower/README.md +++ b/projects/Higher-Lower/README.md @@ -2,7 +2,25 @@ A number guessing game. The program picks a random number between 0 and 100 and tells you whether to guess higher or lower until you find it. -## How to run +## Example + +```text +Welcome to the Higher-Lower Game! +Guess the number: 50 +You entered: 50 +Higher +Guess the number: 75 +You entered: 75 +Lower +Guess the number: 63 +You entered: 63 +Higher +Guess the number: 69 +You entered: 69 +You win! the number is 69! +``` + +## How to run on localhost ``` python main.py @@ -11,7 +29,3 @@ python main.py ## Dependencies Standard library only (`random`). - -## Pyodide-runnable - -Yes — console game using only `input()`/`print()` and the `random` module. diff --git a/projects/Historical Data Breaches/README.md b/projects/Historical Data Breaches/README.md index 1181737c1..2663b533a 100644 --- a/projects/Historical Data Breaches/README.md +++ b/projects/Historical Data Breaches/README.md @@ -63,7 +63,3 @@ Data contained in breach: * Social media profiles * Usernames ``` - -## Pyodide-runnable - -No — it fetches data from the HaveIBeenPwned API over the network, which is not available in a browser sandbox. \ No newline at end of file diff --git a/projects/IP Blacklist Checker/README.md b/projects/IP Blacklist Checker/README.md index 5f4dc4063..794c52bf8 100644 --- a/projects/IP Blacklist Checker/README.md +++ b/projects/IP Blacklist Checker/README.md @@ -2,7 +2,24 @@ A CLI script that checks whether given IP addresses are blacklisted by querying the blacklistchecker.com API. -## How to run +## Example + +```text +$ python blacklist_checker.py 192.168.1.1 8.8.8.8 +Blacklist check result is: +IP blacklists check returned next results: +8.8.8.8 is detected in ['SORBS-SPAM', 'UCEPROTECT-LEVEL1'] blacklists +``` + +When run without arguments, the script prompts interactively: +```text +Type in ip to check +1.2.3.4 +IPs not blacklisted! +IP blacklists check returned next results: +``` + +## How to run on localhost ``` pip install requests @@ -14,6 +31,3 @@ Requires a free API key from blacklistchecker.com. ## Dependencies requests. - -## Pyodide-runnable -No — it makes live HTTP API requests to an external blacklist service. diff --git a/projects/IPv4_Calculator-main/README.md b/projects/IPv4_Calculator-main/README.md index 001f3b3f4..b9b36466a 100644 --- a/projects/IPv4_Calculator-main/README.md +++ b/projects/IPv4_Calculator-main/README.md @@ -5,7 +5,31 @@ IPv4_Calculator is developed for: 2.Determine which category the address belongs to (private, public); 3.Determine subnet attributes. -## How to run +## Example + +```text +Enter IP address of host: 192.168.1.10 +Enter IP prefix: 24 +IP address: 192.168.1.10/24 +Class of IP address: C +Address category: Private +Host address (decimal): [192, 168, 1, 10] +Mask (decimal): [255, 255, 255, 0] +Network address (decimal): [192, 168, 1, 0] +Broadcast address (decimal): [192, 168, 1, 255] +First available host (decimal): [192, 168, 1, 1] +Last available host (decimal): [192, 168, 1, 254] +Host address(binary): ['11000000', '10101000', '00000001', '00001010'] +Mask(binary): ['11111111', '11111111', '11111111', '00000000'] +Network address(binary): ['11000000', '10101000', '00000001', '00000000'] +Broadcast address(binary): ['11000000', '10101000', '00000001', '11111111'] +First available host (binary): ['11000000', '10101000', '00000001', '00000001'] +Last available host (binary): ['11000000', '10101000', '00000001', '11111110'] +Total Number of Hosts: 256 +Number of Usable Hosts: 254 +``` + +## How to run on localhost ``` python ipv4_calc.py @@ -14,6 +38,3 @@ python ipv4_calc.py ## Dependencies Standard library only. - -## Pyodide-runnable -Yes — it is a pure-stdlib console program using only input() and print(). diff --git a/projects/Image Manipulation/README.md b/projects/Image Manipulation/README.md index 4d7472108..a10465227 100644 --- a/projects/Image Manipulation/README.md +++ b/projects/Image Manipulation/README.md @@ -2,7 +2,13 @@ A small Pillow script that opens a local image (`mars.jpg`), resizes it to 200x200 pixels, and saves the result as `newImage.jpg`. -## How to run +## Example + +1. Place `mars.jpg` in the project directory. +2. Run `python resizingImage.py`. +3. The script opens `mars.jpg`, resizes it to 200x200 pixels, and saves the result as `newImage.jpg` in the same directory. + +## How to run on localhost ``` pip install pillow @@ -12,7 +18,3 @@ python resizingImage.py ## Dependencies - pillow - -## Pyodide-runnable - -No — it reads and writes image files on the real filesystem, which is not available in a browser sandbox. diff --git a/projects/Image Sketcher/README.md b/projects/Image Sketcher/README.md index e2374cc44..f7a7ede04 100644 --- a/projects/Image Sketcher/README.md +++ b/projects/Image Sketcher/README.md @@ -2,7 +2,13 @@ An OpenCV demo that captures live webcam frames and renders them as a pencil-sketch effect using grayscale conversion, Gaussian blur, and Canny edge detection. -## How to run +## Example + +1. Run `python main.py` with a webcam connected. +2. A window titled "Output" opens showing your live webcam feed rendered as a pencil-sketch effect (grayscale edges on a white background). +3. Press Enter to close the window and stop the program. + +## How to run on localhost ``` pip install opencv-python numpy @@ -13,7 +19,3 @@ python main.py - opencv-python - numpy - -## Pyodide-runnable - -No — it uses OpenCV webcam capture and GUI windows, which are not available in a browser. diff --git a/projects/Image Toonification/Readme.md b/projects/Image Toonification/Readme.md index b8a54483d..585c4e44b 100644 --- a/projects/Image Toonification/Readme.md +++ b/projects/Image Toonification/Readme.md @@ -7,6 +7,12 @@ Just apply your images the cartoon effect and Enjoy!! - opencv-python - numpy +## Example + +1. Place your input image as `images/filename.jpg` inside the project folder. +2. Run `python3 main.py`. +3. The script applies K-Means color quantization (49 colors), detects edges with adaptive thresholding, and combines them with a bilateral filter to produce a cartoon-style image saved as `images/filename.png`. + ## Installation Simple, Just clone this repository to your local storage and run the below command. ` pip install -r requirements.txt ` @@ -18,7 +24,3 @@ Simple, Just clone this repository to your local storage and run the below comma `python3 main.py` ### If something does not work try changing the inner values such as blur value and line size. It should work now. - -## Pyodide-runnable - -No — it reads and writes image files on the real filesystem via OpenCV, which is not available in a browser sandbox. diff --git a/projects/Image compressor/README.md b/projects/Image compressor/README.md index a1229a497..e4b3f15d8 100644 --- a/projects/Image compressor/README.md +++ b/projects/Image compressor/README.md @@ -2,7 +2,14 @@ A command-line tool that compresses an image using Pillow. You pass an input image and optionally an output directory, quality, and format; it writes a compressed copy. -## How to run +## Example + +```text +$ python imagecompressor.py photo.jpg -o ./out -q 70 -f JPEG +Image compressed and saved as 'out/compressed_photo.jpg' +``` + +## How to run on localhost ``` pip install pillow @@ -12,7 +19,3 @@ python imagecompressor.py [-o output_dir] [-q quality] [-f format] ## Dependencies - pillow - -## Pyodide-runnable - -No — it reads and writes image files on the real filesystem and imports `tkinter.filedialog`, neither of which is available in a browser sandbox. diff --git a/projects/Image to PDF Converter/README.md b/projects/Image to PDF Converter/README.md index 1a0812db0..61174d338 100644 --- a/projects/Image to PDF Converter/README.md +++ b/projects/Image to PDF Converter/README.md @@ -2,7 +2,13 @@ Resizes every image in a `convert/` directory to A4 size and merges them into a single `output.pdf` file using FPDF. -## How to run +## Example + +1. Place the images you want to convert inside the `convert/` directory (e.g., `page1.jpg`, `page2.jpg`). +2. Run `python image_to_pdf_converter.py`. +3. Each image is resized to A4 dimensions (210x297 mm) and added as a separate page; the merged PDF is saved as `output.pdf` in the current directory. + +## How to run on localhost ``` pip install fpdf @@ -12,7 +18,3 @@ python image_to_pdf_converter.py ## Dependencies - fpdf - -## Pyodide-runnable - -No — it reads images from a real directory and writes a PDF to disk, which is not available in a browser sandbox. diff --git a/projects/Image to text generation project/README.md b/projects/Image to text generation project/README.md index 0723810b3..a4604cf9e 100644 --- a/projects/Image to text generation project/README.md +++ b/projects/Image to text generation project/README.md @@ -2,7 +2,16 @@ A Jupyter notebook that generates a caption for an image. It loads a pretrained Vision Encoder-Decoder model (`bipin/image-caption-generator`) via Hugging Face Transformers and produces a text description of a given image. -## How to run +## Example + +1. Open `Image_to_text_generation.ipynb` in Jupyter or Google Colab. +2. Set `img_name` to the path of your image, e.g. `img_name = "dog.jpg"`. +3. Run all cells. The notebook loads the `bipin/image-caption-generator` model and prints a generated caption such as: + ``` + A woman in a white shirt and blue jeans is running on the sidewalk. + ``` + +## How to run on localhost ``` pip install transformers torch pillow @@ -15,7 +24,3 @@ Open `Image_to_text_generation.ipynb` in Jupyter (or Google Colab) and run the c - transformers - torch - pillow - -## Pyodide-runnable - -No — it requires PyTorch and Hugging Face Transformers (large native ML libraries) and is delivered as a notebook, none of which run under Pyodide. diff --git a/projects/Image-Upscale/README.md b/projects/Image-Upscale/README.md index 446c8b5ec..9631d60b4 100644 --- a/projects/Image-Upscale/README.md +++ b/projects/Image-Upscale/README.md @@ -2,7 +2,13 @@ Upscales an image using the ESRGAN super-resolution model built on PyTorch. -## How to run +## Example + +1. Update `main.py` with the paths to your ESRGAN model file and input image. +2. Run `python main.py`. +3. The script loads the ESRGAN model, resizes the input image to 224x224 for processing, passes it through the network, and saves the upscaled result to the output path you specified. + +## How to run on localhost ``` pip install torch torchvision pillow @@ -14,6 +20,3 @@ You also need a trained ESRGAN model file and an input image; update the paths i ## Dependencies torch, torchvision, Pillow (and an ESRGAN module/model). - -## Pyodide-runnable -No — it relies on PyTorch and local image files, which are not available in the browser. diff --git a/projects/Image-to-art/Readme.md b/projects/Image-to-art/Readme.md index 0e2665fa7..13a6e1772 100644 --- a/projects/Image-to-art/Readme.md +++ b/projects/Image-to-art/Readme.md @@ -1,16 +1,29 @@ # Image To ASCII ART Generator + + A simple Python Program to convert your images to ascii art. ### Prerequisites + + Make sure you have the latest version of Python, pip and Pillow installed on your PC. Don't forget to save image to be coverted to ascii art in this directory as 'test1.jpg'. +## Example + +1. Save your image as `test1.jpg` in the project directory. +2. Run `python3 ascii_art.py`. +3. The script resizes the image to 100x100 pixels, maps each pixel's brightness to an ASCII character from the palette `` `^",:;Il!i~+_-?][}{1)(|\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$ ``, and writes the result to `image.txt`: + ``` + @@@@@@@@@@@@@@MMM##**oooo... + @@@@@@@@@@@MMM###**ooo....,, + @@@@@@@@@@MMM##**ooo...,,;;; + ``` + +### How to run on localhost the script -### How to run the script -Open and run this script easily with the Python IDLE or the command line by running the following command on this directory: python3 ascii_art.py. -## Pyodide-runnable -No — it depends on the Pillow imaging library and reads/writes image files from the local disk. +Open and run this script easily with the Python IDLE or the command line by running the following command on this directory: python3 ascii_art.py. diff --git a/projects/ImagegenChatbot/readme.md b/projects/ImagegenChatbot/readme.md index 21a875e9b..2d18387ab 100644 --- a/projects/ImagegenChatbot/readme.md +++ b/projects/ImagegenChatbot/readme.md @@ -1,4 +1,19 @@ ## openai-chatgpt-imagegenerator +## Example + +**Text chatbot (`gpt3.py`):** +```text +>> What is the capital of France? +The capital of France is Paris. +>> bye +``` + +**Image generator (`imagegen.py`):** +```text +>> a sunset over a mountain lake +``` +The script calls the OpenAI Images API, downloads the generated 1024x1024 image as `image.png`, and opens it automatically. + ## setup: - get unique api key. @@ -6,7 +21,3 @@ - Variable name: OPENAI_API_KEY , Variable value: your unique api key Now you'll be able to create your own AI 😎 - -## Pyodide-runnable - -No — it calls the OpenAI API over the network, downloads generated images, and opens them with Pillow, none of which is available in a browser sandbox. diff --git a/projects/Instagram Post Creation/README.md b/projects/Instagram Post Creation/README.md index fef0bc4b9..9184fc048 100644 --- a/projects/Instagram Post Creation/README.md +++ b/projects/Instagram Post Creation/README.md @@ -2,7 +2,16 @@ Generates an Instagram post image by drawing centered text onto a template image using Pillow. -## How to run +## Example + +1. Run `python main.py`. +2. The script opens `templates/basic_template.jpg`, draws the text `"My Instagram Post"` centered near the bottom in white using the Poppins font at size 30. +3. The finished image is saved to `output/post.jpg` and the following is printed: + ``` + Instagram post saved to output/post.jpg + ``` + +## How to run on localhost ``` pip install pillow @@ -14,6 +23,3 @@ Requires the bundled `templates/` and `fonts/` assets. ## Dependencies Pillow. - -## Pyodide-runnable -No — it depends on Pillow and reads/writes image files from the local disk. diff --git a/projects/Internet Speed Tester/README.md b/projects/Internet Speed Tester/README.md index a0b12be53..72f35ac21 100644 --- a/projects/Internet Speed Tester/README.md +++ b/projects/Internet Speed Tester/README.md @@ -6,10 +6,18 @@ Check the Download and Upload Speed of your Internet Connection. 2. speedtest 3. speedtest-cli +## Example + +```text +{'url': 'http://speedtest.lon1.linode.com:8080/speedtest/upload.php', 'lat': '51.5085', + 'lon': '-0.1257', 'name': 'London', 'country': 'United Kingdom', ...} +94371840.25 +8921034.11 +``` + +The three lines show the best server details, the download speed in bits/second, and the upload speed in bits/second. + ### How to use ``` ./main.py ``` - -## Pyodide-runnable -No — it uses the speedtest library to measure real network speed against remote servers. diff --git a/projects/Internet-speed-test/README.md b/projects/Internet-speed-test/README.md index 809ba321a..e84f903b0 100644 --- a/projects/Internet-speed-test/README.md +++ b/projects/Internet-speed-test/README.md @@ -2,6 +2,26 @@ **This is a internet speed tester made in python** +## Example + +```text +Loading server list... + +Choosing best server... +Found: speedtest.lon1.linode.com:8080 located in United Kingdom +What speed do you want to test: + + 1) Download Speed + + 2) Upload Speed + + 3) Ping + + Your Choice: 1 +Performing download test... +Download speed: 85.34 Mbit/s +``` + ## Getting Started - Clone this repo @@ -48,6 +68,3 @@ pip3 install speedtest-cli ![Shreejan-35](https://github.com/Shreejan-35) That's all. - -## Pyodide-runnable -No — it uses the speedtest library to measure real network speed against remote servers. diff --git a/projects/Inverse Matrix Calculator/README.md b/projects/Inverse Matrix Calculator/README.md index 1194944b0..7512e0e04 100644 --- a/projects/Inverse Matrix Calculator/README.md +++ b/projects/Inverse Matrix Calculator/README.md @@ -2,7 +2,33 @@ A console program that asks for the order and elements of a square matrix, then computes and prints its inverse using minors, cofactors, and the adjugate. -## How to run +## Example + +```text +===== Inverse Matrix Calculator ===== +>>> Input order of matrix +Choose matrix rows and columns: 2 +Matrix A: 2x2 +-------------------- +>>> Input elements of matrix +A 1, 1: 4 +A 1, 2: 7 +A 2, 1: 2 +A 2, 2: 6 +-------------------- +>>> Calculation Results +A: + [ 4 7 ] + [ 2 6 ] +A^-1: + [ 0.6 -0.7 ] + [ -0.2 0.4 ] +-------------------- +>>> Would you like to make another calculation? (y): n +Thank you for using this program :) +``` + +## How to run on localhost ``` python main.py @@ -11,6 +37,3 @@ python main.py ## Dependencies Standard library only. - -## Pyodide-runnable -Yes — it is a pure-stdlib console program using only input() and print(). diff --git a/projects/JARVIS.PY/jarvisreadme.md b/projects/JARVIS.PY/jarvisreadme.md index 872e0db51..9fdca0e7d 100644 --- a/projects/JARVIS.PY/jarvisreadme.md +++ b/projects/JARVIS.PY/jarvisreadme.md @@ -1,6 +1,18 @@ # Your-Desktop-Artificial-Assistant-(JARVIS) Jarvis is a Python coding companion or your personal assistant . Point it to a python function, and it will execute it. As soon as you change and save your code, Jarvis will detect it, and will rerun the function. If an exception is raised, it will be displayed in the error panel. Enjoy your assistant at free of cost. +## Example + +1. Run `python JARVIS2.0.py`. JARVIS greets you based on the time of day (e.g., "Good Morning!") and says "jarvis here at your service sir. How may I help you?". +2. Speak a command into your microphone. The console shows: + ``` + Listening... + Recognizing... + User said: open youtube + ``` +3. JARVIS opens YouTube in your default browser. Other supported voice commands include "wikipedia [topic]", "what is the time", "tell me a joke", "open google", "open stackoverflow", "play music", and "email to Raj". +4. Say "quit" or "exit" to stop. + ## Installation ### For windows users (run those in command prompt/cmt/terminal) @@ -17,7 +29,4 @@ To get wikipedia data `pip install wikipedia` To get funny jokes -`pip install pyjokes` - -## Pyodide-runnable -No — it relies on a microphone, text-to-speech, web browser control, and file-system access. \ No newline at end of file +`pip install pyjokes` \ No newline at end of file diff --git a/projects/Jokenpo/README.md b/projects/Jokenpo/README.md index e04cba7a3..f100d0eac 100644 --- a/projects/Jokenpo/README.md +++ b/projects/Jokenpo/README.md @@ -2,7 +2,27 @@ A console card-based rock-paper-scissors game (in Portuguese) where you can play against the computer or watch a simulated match between two players. -## How to run +## Example + +```text +Escolha um opção: +1 - Jogar +2 - Simular um jogo +0 - Sair + +Digite: 1 +====== JOGAR ====== +Digite seu nome: Alice +Suas Cartas: Stone Paper Scissors +Escolha sua carta: 0 +Stone x Paper +Suas Cartas: Paper Scissors +Escolha sua carta: 1 +Stone x Scissors Paper x Scissors +Jogador 1 - Venceu +``` + +## How to run on localhost ``` python main.py @@ -11,6 +31,3 @@ python main.py ## Dependencies Standard library only. - -## Pyodide-runnable -Yes — it is a pure-stdlib console game using only input() and print(). diff --git a/projects/Jokey/README.md b/projects/Jokey/README.md index 6a2381582..7cc8773aa 100644 --- a/projects/Jokey/README.md +++ b/projects/Jokey/README.md @@ -2,7 +2,32 @@ A console joke teller that fetches jokes from the JokeAPI, with settings to change joke category and language. -## How to run +## Example + +```text +Do you want to read a joke?(y => yes; n => exit; s=> settings): y + +Why do Java developers wear glasses? Because they don't C#. +... + +Do you want to read a joke?(y => yes; n => exit; s=> settings): s +Which settings would you like to edit? (c => category, l => language): c +Current selected category: Any + +Selectable categories: + a => Any + p => Programming + m => Misc + d => dark + s => Spooky + c => Christmas + > p +Category Programming set! + +Do you want to read a joke?(y => yes; n => exit; s=> settings): n +``` + +## How to run on localhost ``` pip install requests @@ -12,6 +37,3 @@ python joketeller.py ## Dependencies requests. - -## Pyodide-runnable -No — it makes live HTTP requests to the JokeAPI to fetch jokes. diff --git a/projects/KbdXylo/README.md b/projects/KbdXylo/README.md index ecb5c8182..6ce931034 100644 --- a/projects/KbdXylo/README.md +++ b/projects/KbdXylo/README.md @@ -2,7 +2,14 @@ Turns your keyboard into a xylophone by listening for key presses and playing a tone based on each key's character value. -## How to run +## Example + +1. Run `python main.py`. The program starts listening for key presses silently in the background. +2. Press any character key (e.g., `a`, `s`, `d`). Each key plays a tone whose frequency is the ASCII value of that character multiplied by 10 (e.g., pressing `a` plays 970 Hz, `s` plays 1150 Hz). +3. Each tone lasts 250 ms at a low volume. +4. Press Escape to stop the program. + +## How to run on localhost ``` pip install -r requirements.txt @@ -12,6 +19,3 @@ python main.py ## Dependencies boombox, pynput. - -## Pyodide-runnable -No — it uses pynput to capture global keyboard events and plays audio through the system sound device. diff --git a/projects/Language-Translate/README.md b/projects/Language-Translate/README.md index ad8ac4028..57dc2f299 100644 --- a/projects/Language-Translate/README.md +++ b/projects/Language-Translate/README.md @@ -6,6 +6,14 @@ A simple script to help translate your text into different languages. - Install Python if you already haven't. - Install all required dependencies by running ``pip install -r requirements.txt``. +## Example + +```text +$ python3 language_translate.py --from_lang Spanish --to_lang English +Enter text to translate: Hola, ¿cómo estás? +Hello, how are you? +``` + ## Usage ```bash python3 language_translate.py @@ -20,6 +28,3 @@ options: Language to translate from. --to_lang TO_LANG Language to translate to. (defaults to English) ``` - -## Pyodide-runnable -No — the `translate` library makes live HTTP requests to an online translation service. diff --git a/projects/Language_learning_assistant/README.md b/projects/Language_learning_assistant/README.md index 82a2b2b4e..c277fb4a5 100755 --- a/projects/Language_learning_assistant/README.md +++ b/projects/Language_learning_assistant/README.md @@ -17,6 +17,34 @@ This is a Python script for a language learning assistant that helps users pract - random - speech_recognition +## Example + +```text +AI Language Learning Assistant: Vocabulary Quiz +What does 'ocean' mean? a large body of salt water +Correct! +What does 'mountain' mean? a big rock +Wrong. The correct answer is: a large natural elevation of the earth's surface +Quiz completed! Your score: 1/2 + +AI Language Learning Assistant: Grammar Exercise +He ___ to the store every day. goes +Correct! +They ___ in the park yesterday. play +Wrong. The correct answer is: played +Grammar exercise completed! Your score: 1/2 + +AI Language Learning Assistant: Interactive Conversation Practice +Type 'exit' to end the conversation. +ChatBot: What is your favorite color? blue +ChatBot: My favorite color is blue. +ChatBot: Do you enjoy learning new things? exit + +AI Language Learning Assistant: Pronunciation Errors Detection +Speak a sentence for pronunciation evaluation: +There might be some pronunciation errors. Keep practicing! +``` + ## Getting Started 1. Clone this repository to your local machine. @@ -47,6 +75,3 @@ The script offers a set of conversation questions and answers to engage in inter ## Grammar Exercises The grammar exercise section contains fill-in-the-blank sentences to help you improve your grasp of grammar rules. You can extend this section by adding more sentences and exercises to the `grammar_exercise` list in the script. - -## Pyodide-runnable -No — the pronunciation feature uses speech_recognition with a microphone, which is unavailable in Pyodide. diff --git a/projects/Live AQI/README.md b/projects/Live AQI/README.md index 5979d5346..290bdb836 100644 --- a/projects/Live AQI/README.md +++ b/projects/Live AQI/README.md @@ -2,7 +2,17 @@ A Tkinter app that fetches the live Air Quality Index for a given city, state, and country from the AirVisual API. -## How to run +## Example + +1. The window opens showing three labeled entry fields: City, State, and Country, plus a blue "Get AQI" button. +2. Type `Los Angeles` in City, `California` in State, and `USA` in Country. +3. Click "Get AQI". +4. The labels below update to show: + - City: Los Angeles + - State: California + - AQI: 42 + +## How to run on localhost ``` pip install requests @@ -14,6 +24,3 @@ Requires an AirVisual API key set in `main.py`. ## Dependencies tkinter (standard library), requests. - -## Pyodide-runnable -No — it uses a Tkinter GUI and makes live HTTP requests to the AirVisual API. diff --git a/projects/Loan Calculator/README.md b/projects/Loan Calculator/README.md index 004db6b92..7aa7614eb 100644 --- a/projects/Loan Calculator/README.md +++ b/projects/Loan Calculator/README.md @@ -9,6 +9,16 @@ Welcome to the Loan Calculator project! This Python program calculates the month - [Contributing](#contributing) - [License](#license) +## Example + +```text +Welcome to the Loan Calculator! +Enter the loan amount: $10000 +Enter the annual interest rate (as a percentage): 5.5 +Enter the loan term (in months): 24 +Your monthly payment will be: $441.17 +``` + ## Getting Started To use the Loan Calculator, follow these steps: @@ -37,6 +47,3 @@ To use the Loan Calculator, follow these steps: - The program will calculate and display the monthly payment amount. Enjoy using the Loan Calculator! If you have any questions or encounter any issues, please feel free to reach out or open an issue. - -## Pyodide-runnable -Yes — it is a pure-stdlib console program using only input() and print(). \ No newline at end of file diff --git a/projects/Location Search App (GUI)/README.md b/projects/Location Search App (GUI)/README.md index 72706cd0d..6ac32b9b1 100644 --- a/projects/Location Search App (GUI)/README.md +++ b/projects/Location Search App (GUI)/README.md @@ -2,7 +2,14 @@ A Tkinter app with a search box that opens the entered location in Google Maps in your web browser. -## How to run +## Example + +1. The window opens with an entry box and a "Search" button on a teal and orange background. +2. Type `Eiffel Tower, Paris` into the entry box. +3. Click "Search". +4. Your default web browser opens `https://google.com/maps/place/Eiffel Tower, Paris`. + +## How to run on localhost ``` pip install pyperclip @@ -12,6 +19,3 @@ python "Location_Search_GUI.py" ## Dependencies tkinter (standard library), pyperclip. - -## Pyodide-runnable -No — it uses a Tkinter GUI and opens an external web browser. diff --git a/projects/Lyrics-Extractor/README.md b/projects/Lyrics-Extractor/README.md index 6c7d0d359..0c72f66b3 100644 --- a/projects/Lyrics-Extractor/README.md +++ b/projects/Lyrics-Extractor/README.md @@ -2,7 +2,20 @@ A console program that takes a song name and fetches its title and lyrics using the lyrics-extractor library and Google Custom Search. -## How to run +## Example + +```text +Enter the song name: Bohemian Rhapsody + + Title: Bohemian Rhapsody + + Lyrics: + + Is this the real life? Is this just fantasy? + Caught in a landslide, no escape from reality... +``` + +## How to run on localhost ``` pip install lyrics-extractor @@ -14,6 +27,3 @@ Requires a Google Custom Search API key and engine ID. ## Dependencies lyrics-extractor. - -## Pyodide-runnable -No — it makes live HTTP requests to the Google Custom Search API to fetch lyrics. diff --git a/projects/ML-Notebooks_Beginners/README.md b/projects/ML-Notebooks_Beginners/README.md index c7e0e47c4..a2496ff7c 100644 --- a/projects/ML-Notebooks_Beginners/README.md +++ b/projects/ML-Notebooks_Beginners/README.md @@ -2,7 +2,14 @@ A collection of beginner-friendly machine learning / mathematics notes. `notebooks/maths/algebra.py` contains notes on algebra concepts for ML (functions, plotting, etc.) stored in Jupyter-notebook JSON form. -## How to run +## Example + +1. Open `notebooks/maths/algebra.py` in Jupyter (`jupyter notebook notebooks/maths/algebra.py`). +2. The notebook loads with cells covering algebra concepts used in machine learning (functions, plotting). +3. Run cells sequentially; matplotlib plots (e.g. linear and quadratic function graphs) are rendered inline. +4. Modify the example values in a cell and re-run to experiment with the concepts. + +## How to run on localhost The file is a Jupyter notebook saved with a `.py` extension. Open it with Jupyter: @@ -16,7 +23,3 @@ jupyter notebook notebooks/maths/algebra.py - jupyter - matplotlib - numpy - -## Pyodide-runnable - -No — the file is a Jupyter notebook document (JSON), not a runnable Python script, and it depends on a notebook environment to execute. diff --git a/projects/MQTT Client/README.md b/projects/MQTT Client/README.md index 0f0dfd882..989a110e8 100644 --- a/projects/MQTT Client/README.md +++ b/projects/MQTT Client/README.md @@ -15,6 +15,18 @@ paho-mqtt==1.6.1 python-decouple==3.8 ``` +## Example + +```text +[MQTT] Connected to server. +[MQTT] Received message from sensors/temperature topic: +Hello MQTT +[MQTT] Received message from sensors/temperature topic: +Custom Messages Working! +``` + +The client connects to the configured broker, subscribes to the topic, then publishes two messages: `"Hello MQTT"` and `"Custom Messages Working!"`. + ## How to execute? 1. You need a Broker MQTT running. I recommend [Mosquitto](https://mosquitto.org/download/). Ex (Do not need to do): @@ -31,6 +43,3 @@ python3 MqttClient.py ## Code Running ![Running Code](docs/imgs/running_code.png) -## Pyodide-runnable - -No — it connects over the network to an external MQTT broker, which is not possible in a browser sandbox. diff --git a/projects/Madlibs Generator/README.md b/projects/Madlibs Generator/README.md index 6a7a78817..7513ee955 100644 --- a/projects/Madlibs Generator/README.md +++ b/projects/Madlibs Generator/README.md @@ -2,7 +2,14 @@ A Tkinter app that lets you pick one of three story templates and fill in a noun, adjective, verb, and adverb to generate a Mad Lib story. -## How to run +## Example + +1. The window opens showing a story picker (choices 1–3), and entry fields for a noun, adjective, verb, and adverb. +2. Enter `1` for the story choice, noun `dragon`, adjective `ancient`, verb `fly`, adverb `swiftly`. +3. Click "Generate Mad Lib". +4. The generated story appears below the button, filling the template with your words — e.g. "In a mystical and distant land, there was a brave and ancient explorer named dragon…" + +## How to run on localhost ``` python main.py @@ -11,6 +18,3 @@ python main.py ## Dependencies tkinter (standard library). - -## Pyodide-runnable -No — it uses a Tkinter GUI, which cannot run in the browser. diff --git a/projects/Make-API/README.md b/projects/Make-API/README.md index 13a26b98c..4eff14e6f 100644 --- a/projects/Make-API/README.md +++ b/projects/Make-API/README.md @@ -2,7 +2,18 @@ A small Flask web API that returns a random quote (read from `quote.txt`) as JSON on the root route. -## How to run +## Example + +After starting the server, open `http://127.0.0.1:5000/` in a browser or with curl: + +```text +$ curl http://127.0.0.1:5000/ +"The only way to do great work is to love what you do." +``` + +Each request returns a randomly selected quote from `quote.txt` as a JSON string. + +## How to run on localhost ``` pip install flask @@ -14,6 +25,3 @@ Then open http://127.0.0.1:5000/ in a browser. ## Dependencies flask. - -## Pyodide-runnable -No — it runs a Flask web server, which requires a server process and networking. diff --git a/projects/Market Financial Sentiment Predictor/readme.md b/projects/Market Financial Sentiment Predictor/readme.md index 3258aed06..8009fb02c 100644 --- a/projects/Market Financial Sentiment Predictor/readme.md +++ b/projects/Market Financial Sentiment Predictor/readme.md @@ -1,13 +1,31 @@ # Market Financial Sentiment Predictor + ## Descirption + All day various news about companies comes and not all people can track this news. Due to this they can miss great investment opportinities or they can avoid risky situations thus saving them from losses.This tool solves this problem by analyzing financial news and then predicting the stock price. ## Libraries Used: + - Numpy - Pandas - Scikit-learn -## How to run +## Example + +```text + precision recall f1-score support + + 0 0.85 0.82 0.84 130 + 1 0.83 0.86 0.84 130 + + accuracy 0.84 260 + macro avg 0.84 0.84 0.84 260 +weighted avg 0.84 0.84 0.84 260 +``` + +The script loads `Financial Market News.csv`, trains a Random Forest classifier on the top-25 daily headlines, and prints a classification report showing sentiment prediction accuracy. + +## How to run on localhost ``` pip install numpy pandas scikit-learn @@ -15,6 +33,3 @@ python main.py ``` Requires the bundled `Financial Market News.csv` dataset. - -## Pyodide-runnable -No — it depends on scikit-learn and reads a CSV dataset from the local disk. \ No newline at end of file diff --git a/projects/Mastermind/README.md b/projects/Mastermind/README.md index bb316e533..29554097c 100644 --- a/projects/Mastermind/README.md +++ b/projects/Mastermind/README.md @@ -2,7 +2,23 @@ A console number-guessing game: the program picks a random 4-digit number and tells you how many digits you got right after each guess until you crack it. -## How to run +## Example + +```text +Guess the 4 digit number: 1234 +None of the numbers in your input match. +Enter your next choice of numbers: 5678 +Not quite the number. But you did get 2 digit(s) correct! +Also these numbers in your input were correct. +X 6 X 8 + + +Enter your next choice of numbers: 3698 +You've become a Mastermind! +It took you only 3 tries. +``` + +## How to run on localhost ``` python main.py @@ -11,6 +27,3 @@ python main.py ## Dependencies Standard library only. - -## Pyodide-runnable -Yes — it is a pure-stdlib console game using only input(), print(), and random. diff --git a/projects/Medium Article Reader/README.md b/projects/Medium Article Reader/README.md index f26cd1be5..9e81d2560 100644 --- a/projects/Medium Article Reader/README.md +++ b/projects/Medium Article Reader/README.md @@ -2,7 +2,17 @@ Scrapes an article from a URL, summarizes it with an OpenAI LLM via LangChain, and reads the summary or full text aloud with text-to-speech. -## How to run +## Example + +```text +Paste article URL: https://medium.com/some-article +This article discusses the rise of renewable energy and its impact on global +power markets, highlighting solar adoption trends and policy developments... +Enter 1 for summary of article or Enter 2 for whole article: 1 +(text-to-speech reads the summary aloud) +``` + +## How to run on localhost ``` pip install pyttsx3 requests beautifulsoup4 openai python-dotenv langchain @@ -14,6 +24,3 @@ Requires an OpenAI API key in a `.env` file. ## Dependencies pyttsx3, requests, beautifulsoup4, openai, python-dotenv, langchain. - -## Pyodide-runnable -No — it makes live HTTP/API requests and uses pyttsx3 text-to-speech. diff --git a/projects/Merge PDFs/README.md b/projects/Merge PDFs/README.md index ffe91a66b..e9f090946 100644 --- a/projects/Merge PDFs/README.md +++ b/projects/Merge PDFs/README.md @@ -2,7 +2,33 @@ A CLI tool that lists PDF files in a directory, lets you include/exclude files interactively, and merges the selected PDFs into a single file. -## How to run +## Example + +```text +Contents of directory: + +0 : chapter1.pdf +1 : chapter2.pdf +2 : notes.pdf + +The final list of PDFs to be merged: + +0 : chapter1.pdf +1 : chapter2.pdf +2 : notes.pdf + +Total: 3 + +CONTINUE? ['y'/'Y'] OR MODIFY THIS LIST? ['n'/'N'] +> y + +Enter the name of the final merged pdf (without the extension - 'pdf'): +> combined + +The PDFs have been succesfully merged as/in: /home/user/docs/combined.pdf ✅ +``` + +## How to run on localhost ``` pip install -r requirements.txt @@ -12,6 +38,3 @@ python main.py [directory] ## Dependencies pikepdf. - -## Pyodide-runnable -No — it walks the real file system and uses pikepdf to read and write PDF files on disk. diff --git a/projects/Message-Spam/README.md b/projects/Message-Spam/README.md index f579119d6..46ff1e95f 100644 --- a/projects/Message-Spam/README.md +++ b/projects/Message-Spam/README.md @@ -1,5 +1,12 @@ # WhatsApp Random Message Sender +## Example + +1. Run `python mesaage_spam.py`. A browser window opens to `https://web.whatsapp.com`. +2. Scan the QR code with your phone to log in to WhatsApp Web (you have 10 seconds). +3. Navigate to any open chat in WhatsApp Web. +4. The script automatically types and sends 10 random messages from `("Hello", "Hey", "Good Morning")`, pausing 1–3 seconds between each. + ## Usage 1. Install required libraries: @@ -19,6 +26,3 @@ - Edit the `messages` variable to change the list of messages to send. - Adjust the sleep interval for message timing.. - -## Pyodide-runnable -No — it uses pyautogui to control the keyboard and opens WhatsApp Web in a desktop browser. \ No newline at end of file diff --git a/projects/Minecraft-in-Python-main/README.md b/projects/Minecraft-in-Python-main/README.md index 10f7cd83f..8d0925d63 100644 --- a/projects/Minecraft-in-Python-main/README.md +++ b/projects/Minecraft-in-Python-main/README.md @@ -2,7 +2,15 @@ A simple Minecraft-style voxel sandbox built with the `ursina` 3D game engine. You can walk around in first-person, place and break blocks of several textures, and switch block types with the number keys. -## How to run +## Example + +1. Run the script — a 3D first-person window opens showing a 20×20 flat terrain of grass blocks with a sky sphere. +2. Use W/A/S/D to walk and the mouse to look around. +3. Left-click a block to place a new block of the currently selected type adjacent to it (a punch sound plays). +4. Right-click a block to remove it. +5. Press keys 1–4 to switch the active block texture: 1 = grass, 2 = stone, 3 = brick, 4 = dirt. + +## How to run on localhost ``` pip install ursina @@ -14,7 +22,3 @@ python "Minecraft-in-Python-main/UrsaCraft_video.py" ## Dependencies - ursina - -## Pyodide-runnable - -No — `ursina` is a desktop 3D game engine that requires OpenGL, a windowing system and local asset files, none of which are available in the browser. diff --git a/projects/Mineral Processing Technology-Image Analytics/README.md b/projects/Mineral Processing Technology-Image Analytics/README.md index f97ad976c..1269133fb 100644 --- a/projects/Mineral Processing Technology-Image Analytics/README.md +++ b/projects/Mineral Processing Technology-Image Analytics/README.md @@ -54,12 +54,15 @@ Example – Read the input images for the challenge form the “input” folder, process each image based on the calculations mentioned and save the output image in the “output” folder. - - - - - - -## Pyodide-runnable - -No — it requires the OpenCV (`cv2`) library, which is not available in Pyodide, and reads/writes image files from the real filesystem. +## Example + +1. Place particle images (`.jpg` or `.png`) in the `input/` folder. +2. Run `python code.py`. +3. For each image, the script detects particle contours and overlays the following measurements in red: + - Minimum enclosing circle drawn around each particle. + - `Total surface area: 3142.50` (pixel area of the contour). + - `Centroid: (128, 95)` marker dot at the particle centre. + - `Total Perimeter: 201.34` along the contour edge. + - `Major Axis Length: 87.21` as a red line through the longest internal axis. +4. Annotated images are saved to the `output/` folder with the same filenames. +5. `Processing complete.` is printed when all images are done. diff --git a/projects/Mobile Document Scanner/README.md b/projects/Mobile Document Scanner/README.md index 1489f98eb..b311bcef2 100644 --- a/projects/Mobile Document Scanner/README.md +++ b/projects/Mobile Document Scanner/README.md @@ -2,7 +2,19 @@ Turns a photo of a document into a clean, top-down "scanned" image. It detects the document edges, finds its contour, applies a four-point perspective transform, and thresholds the result to a black-and-white scan. -## How to run +## Example + +```text +$ python scan.py --image photo.jpg +STEP 1: Detect the Edges +(two windows open: "Image" showing the resized photo, "Edged" showing Canny edge map — press any key to continue) +STEP 2: Find Contours on paper +(window "Outline" shows the detected document boundary in green — press any key to continue) +STEP 3: Apply perspective transform +(windows "Original" and "Scanned" show the colour photo and the black-and-white top-down scan — press any key to exit) +``` + +## How to run on localhost ``` pip install opencv-python numpy imutils scikit-image @@ -15,7 +27,3 @@ python scan.py --image path/to/photo.jpg - numpy - imutils - scikit-image - -## Pyodide-runnable - -No — it relies on OpenCV (`cv2`) with `imshow`/`waitKey` GUI windows, which cannot run in a browser. diff --git a/projects/Mongo CRUD/README.md b/projects/Mongo CRUD/README.md index 176b3fdcd..da5aaf60d 100644 --- a/projects/Mongo CRUD/README.md +++ b/projects/Mongo CRUD/README.md @@ -2,7 +2,28 @@ A small example of create/read/update/delete operations against a MongoDB database using `pymongo`. `conexao.py` opens the database connection and `usuario.py` defines a `Usuario` class with methods to insert, search, update, list and delete user records. -## How to run +## Example + +```python +# Insert a new user +user = Usuario("Alice", "12345", 30, 1.65) +user.inserir_um_registro() + +# List all users +user.listar_registros() +# ======= Listagem de usuários ======= +# nome cpf idade altura +# 0 Alice 12345 30 1.65 + +# Update a field +user = Usuario(nome="Alice Updated", idade=31) +user.alterar_registro("12345") + +# Delete a user +user.deletar_registro("12345") +``` + +## How to run on localhost Requires a running MongoDB server on `localhost:27017`. @@ -15,7 +36,3 @@ python usuario.py - pymongo - pandas - -## Pyodide-runnable - -No — it needs a network connection to a MongoDB database server, which is unavailable in the browser. diff --git a/projects/Morse-Code-Translator/README.md b/projects/Morse-Code-Translator/README.md index 9652fe151..602310a0f 100644 --- a/projects/Morse-Code-Translator/README.md +++ b/projects/Morse-Code-Translator/README.md @@ -2,7 +2,21 @@ An interactive Morse code translator. Choose `E` to encrypt text into Morse code or `D` to decrypt Morse code back into plain text. -## How to run +## Example + +```text +Enter 'E' for encryption (text to Morse code) or 'D' for decryption (Morse code to text): E +Enter the text you want to encrypt: Hello World +Encrypted Morse code: .... . .-.. .-.. --- .-- --- .-. .-.. -.. +``` + +```text +Enter 'E' for encryption (text to Morse code) or 'D' for decryption (Morse code to text): D +Enter the Morse code you want to decrypt (separate symbols with spaces): .... . .-.. .-.. --- +Decrypted text: HELLO +``` + +## How to run on localhost ``` python morse.py @@ -11,7 +25,3 @@ python morse.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/MorseCode Translator/README.md b/projects/MorseCode Translator/README.md index cdd2e0b27..38508235e 100644 --- a/projects/MorseCode Translator/README.md +++ b/projects/MorseCode Translator/README.md @@ -2,7 +2,17 @@ Encrypts plain text into Morse code and decrypts Morse code back into text using a Morse code dictionary. The `main()` function runs a hard-coded demonstration of both operations. -## How to run +## Example + +```text +-- .-. .. -. .- -. -.- -....- -... .... --- .-- -- .. -.-. -.- +MRINANK-BHOWMICK +``` + +The hard-coded `main()` encrypts `"Mrinank-Bhowmick"` to Morse code and then +decrypts it back to the original text. + +## How to run on localhost ``` python main.py @@ -11,7 +21,3 @@ python main.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — it is a pure-stdlib program with hard-coded input and `print()` output. diff --git a/projects/MotivateBot/README.md b/projects/MotivateBot/README.md index bf4c27ba6..fbfd12143 100644 --- a/projects/MotivateBot/README.md +++ b/projects/MotivateBot/README.md @@ -2,7 +2,16 @@ Generates an inspirational message using the OpenAI GPT API. Given a prompt, it calls the OpenAI completion endpoint and prints the generated motivational text. -## How to run +## Example + +The script uses the fixed prompt `"You are capable of achieving great things because"` and prints the GPT-generated continuation, for example: + +```text +you have the resilience to face challenges head-on and the creativity to find +solutions where others see only obstacles. +``` + +## How to run on localhost ``` pip install openai @@ -17,7 +26,3 @@ python main.py ## Dependencies - openai - -## Pyodide-runnable - -No — it makes network requests to the OpenAI API, which is not possible from a browser sandbox. diff --git a/projects/Movie recommendation/README.md b/projects/Movie recommendation/README.md index cac660f30..1e98d5372 100644 --- a/projects/Movie recommendation/README.md +++ b/projects/Movie recommendation/README.md @@ -2,7 +2,21 @@ A content-based movie recommender. It combines movie keywords, cast, genres and director into a single text feature, vectorises it with `CountVectorizer`, computes cosine similarity between all movies, and prints the top 5 movies most similar to a chosen film. -## How to run +## Example + +The script is hard-coded to find movies similar to `"Avatar"` and prints the top 5 results: + +```text +Top 5 similar movies to Avatar are: + +Guardians of the Galaxy +Aliens +Star Wars: Clone Wars: Volume 1 +Star Trek Into Darkness +Alien +``` + +## How to run on localhost ``` pip install pandas numpy scikit-learn @@ -16,7 +30,3 @@ python movie_recommendation_engine.py - pandas - numpy - scikit-learn - -## Pyodide-runnable - -No — it depends on `scikit-learn`, which is not available in Pyodide. diff --git a/projects/MovieApi_and_ML/README.md b/projects/MovieApi_and_ML/README.md index 5af11c813..872e6303a 100644 --- a/projects/MovieApi_and_ML/README.md +++ b/projects/MovieApi_and_ML/README.md @@ -2,7 +2,14 @@ A Django web application that exposes a movie API and a machine-learning recommendation system. It combines movie metadata with ML algorithms to provide personalised movie recommendations through a web interface. -## How to run +## Example + +1. Start the Django development server with `python movieapi/manage.py runserver`. +2. Open a browser or API client and navigate to `http://127.0.0.1:8000/`. +3. Browse the available endpoints to query movie metadata and receive ML-based personalised recommendations. +4. The server returns JSON responses with movie details and recommendation results. + +## How to run on localhost ``` pip install -r movieapi/requirements.txt @@ -14,7 +21,3 @@ See `movieapi/README.md` for details on configuring an API key. ## Dependencies - Django and the packages listed in `movieapi/requirements.txt` (includes scikit-learn, pandas) - -## Pyodide-runnable - -No — it is a Django web server project that depends on a server runtime, external APIs and scikit-learn. diff --git a/projects/MovieApi_and_ML/movieapi/README.md b/projects/MovieApi_and_ML/movieapi/README.md index 44d398448..fbb1b4efb 100644 --- a/projects/MovieApi_and_ML/movieapi/README.md +++ b/projects/MovieApi_and_ML/movieapi/README.md @@ -7,6 +7,13 @@ ## About A Movie API and ML Recommendation system is a dynamic software solution that combines data from various movie databases with machine learning algorithms. It provides users with personalized movie recommendations based on their preferences and viewing history. This technology enhances the user experience by suggesting movies that align with their tastes, ultimately increasing user engagement and satisfaction. +## Example + +1. Start the Django development server with `py manage.py runserver`. +2. Open a browser or API client and navigate to `http://127.0.0.1:8000/`. +3. Browse the available endpoints to query movie metadata and receive ML-based personalised recommendations. +4. The server returns JSON responses with movie details and recommendation results. + ## Getting Started These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [deployment](#deployment) for notes on how to deploy the project on a live system. diff --git a/projects/Music Player/README.md b/projects/Music Player/README.md index f30d9e64f..8a119140b 100644 --- a/projects/Music Player/README.md +++ b/projects/Music Player/README.md @@ -2,7 +2,15 @@ A desktop music player built with Tkinter. It lets you load a directory of audio tracks into a playlist and play, pause, resume and stop songs using `pygame.mixer`. -## How to run +## Example + +1. The window opens showing the "Sangeet" player with a playlist panel on the right and playback controls (Pause, Stop, Play, Resume) on the left. +2. Click "Load Directory" to open a folder picker and select a folder of audio tracks; all files in that folder appear in the playlist. +3. Highlight a track in the playlist and click "Play" — the "CURRENTLY PLAYING" label updates with the track name and the status bar shows `Song PLAYING`. +4. Click "Pause" to pause playback (status shows `Song PAUSED`) and "Resume" to continue (status shows `Song RESUMED`). +5. Click "Stop" to stop the current track (status shows `Song STOPPED`). + +## How to run on localhost ``` pip install pygame @@ -12,7 +20,3 @@ python main.py ## Dependencies - pygame - -## Pyodide-runnable - -No — it is a Tkinter GUI application that uses `pygame.mixer` for audio and reads tracks from the local filesystem. diff --git a/projects/NASA-APOD/README.md b/projects/NASA-APOD/README.md index 8bea7f357..8f6864f6a 100644 --- a/projects/NASA-APOD/README.md +++ b/projects/NASA-APOD/README.md @@ -1,17 +1,23 @@ # NASA-APOD + [Astronomy Picture of the Day](https://apod.nasa.gov/apod/astropix.html) is a popular website from NASA. This app retrives the metadata of an image from APOD and the image itself (including the explanation of the picture). +## Example + +```text +Image Successfully Downloaded: Pillars_of_Creation.jpg +``` + +The script fetches today's APOD metadata from the NASA API, downloads the image file into the current directory, and prints a confirmation message with the saved filename. + ### Pre-Req + ``` pip install -r requirements.txt ``` -### How to run the app +### How to run on localhost the app + 1. Create a account in nasa.gov and generate your API KEY 2. Pass it in `credentials.py` 3. Run the application `python main.py` - - -## Pyodide-runnable - -No — it makes network requests to the NASA APOD API and downloads the image file with `wget`. diff --git a/projects/Neurons/README.md b/projects/Neurons/README.md index 0d6489702..4ceb06039 100644 --- a/projects/Neurons/README.md +++ b/projects/Neurons/README.md @@ -2,7 +2,19 @@ A terminal animation that simulates "neurons" on a grid. Each neuron randomly dies or moves in one of four directions on every tick, producing an evolving pattern printed to the terminal. -## How to run +## Example + +The terminal fills with a live-updating grid of block characters (`██`) representing neurons. On each tick every neuron either dies or moves one cell in a random direction, producing a shifting, organic-looking pattern that runs until all neurons have died, then restarts. + +```text + ██ + ██ + ██ + ██ ██ + ██ +``` + +## How to run on localhost ``` python main.py @@ -11,7 +23,3 @@ python main.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — after a small edit removing the `os.system` terminal-clear (and the now-unused `os`/`sys` imports), it is a pure-stdlib program that only prints to the console. diff --git a/projects/Number Guessing App/README.md b/projects/Number Guessing App/README.md index 76db94d9e..8e05e845b 100644 --- a/projects/Number Guessing App/README.md +++ b/projects/Number Guessing App/README.md @@ -2,7 +2,40 @@ A two-mode number guessing game. In mode 1 the player guesses a random number chosen by the computer; in mode 2 the computer guesses the number the player picked, using feedback (too high / too low / correct). -## How to run +## Example + +**Mode 1 — player guesses the computer's number:** + +```text +Select gaming mode + Press 1 to guess the number +Press 2 to choose the number +1 +Enter any number: 10 +OOPS! Too high! +Guess again: 5 +OOPS! Too low +Guess again: 7 +Congratulations!! You guessed the number 7 correctly +``` + +**Mode 2 — computer guesses the player's number:** + +```text +2 +Enter your number: 15 +Is 9 too high (h), too low (l), or correct (c)? +=>l +Is 12 too high (h), too low (l), or correct (c)? +=>l +Is 14 too high (h), too low (l), or correct (c)? +=>l +Is 15 too high (h), too low (l), or correct (c)? +=>c +Yay! The computer guessed your number, 15, correctly! +``` + +## How to run on localhost ``` python main.py @@ -11,7 +44,3 @@ python main.py ## Dependencies Standard library only (`random`). - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/OTP-Verfication-System/README.md b/projects/OTP-Verfication-System/README.md index ed4bd8691..f3759dee3 100644 --- a/projects/OTP-Verfication-System/README.md +++ b/projects/OTP-Verfication-System/README.md @@ -2,7 +2,22 @@ Generates a random 6-digit OTP, emails it to a user via Gmail's SMTP server, and then verifies the code the user types back in. -## How to run +## Example + +```text +Enter your email: user@example.com +Enter Your OTP >>: 482951 +Verified +``` + +If the wrong code is entered: + +```text +Enter Your OTP >>: 000000 +Please Check your OTP again +``` + +## How to run on localhost ``` python main.py @@ -13,7 +28,3 @@ Edit `main.py` with valid Gmail credentials (an app password) before running. ## Dependencies Standard library only (`os`, `math`, `random`, `smtplib`). - -## Pyodide-runnable - -No — it connects to Gmail's SMTP server over the network to send email, which is not possible in a browser sandbox. diff --git a/projects/Online Trivia/README.md b/projects/Online Trivia/README.md index df5bad9e4..7c491e187 100644 --- a/projects/Online Trivia/README.md +++ b/projects/Online Trivia/README.md @@ -2,7 +2,30 @@ A console trivia quiz that fetches true/false questions from the Open Trivia Database API. It displays each question, accepts the player's answer, keeps score, and shows a final score at the end. -## How to run +## Example + +```text +================================================== + Question number: 1 +================================================== + Difficulty Level: easy + Category: Science & Nature +================================================== + +Question: The mantis shrimp can see more colors than any other animal on Earth. + +Enter Answer (To skip type 'skip'): true +Correct Answer! +Your score is 1 +Wait moving to the next question... +``` + +```text +Your final score is 3 ! +Thanks for playing! 💜 +``` + +## How to run on localhost ``` pip install requests @@ -12,7 +35,3 @@ python main.py ## Dependencies - requests - -## Pyodide-runnable - -No — it fetches quiz questions from the opentdb.com API over the network using `requests`. diff --git a/projects/OpenCV_color_detect_in_live_feed/README.md b/projects/OpenCV_color_detect_in_live_feed/README.md index b346fa221..af054ae24 100644 --- a/projects/OpenCV_color_detect_in_live_feed/README.md +++ b/projects/OpenCV_color_detect_in_live_feed/README.md @@ -2,7 +2,15 @@ Captures a live video feed from a webcam and detects the colour at the centre of the frame. It converts each frame to HSV, reads the hue value at the centre pixel, classifies it into a named colour, and overlays the result on the video. -## How to run +## Example + +1. A window titled "Video" opens showing the live webcam feed. +2. A small green circle marks the centre of the frame. +3. A white rectangle overlays the top-centre area, displaying the detected colour name (e.g. `GREEN`, `BLUE`, `RED`) rendered in the actual BGR colour of the centre pixel. +4. As you move the camera or change what is in the centre, the displayed colour name updates in real time. +5. Press `q` to close the window and stop the feed. + +## How to run on localhost ``` pip install opencv-python numpy @@ -13,7 +21,3 @@ python Open_CV_color_detect_in_live_feed.py - opencv-python - numpy - -## Pyodide-runnable - -No — it requires webcam access via OpenCV's `VideoCapture` and `imshow` GUI windows, which are unavailable in the browser. diff --git a/projects/Organize_Directory/README.md b/projects/Organize_Directory/README.md index b781f51f6..ea3cec2e9 100644 --- a/projects/Organize_Directory/README.md +++ b/projects/Organize_Directory/README.md @@ -2,7 +2,17 @@ A file organiser. Given a directory path, it sorts the files inside it into category folders (images, music, video, executables, archives, torrent, documents, code, design files) based on their file extensions. -## How to run +## Example + +```text +Enter the directory path of the files: /home/user/Downloads +Organising your files into [ images - music - video - executable - archive - torrent - document - code - design files] +Finished organising your files +``` + +Files in the given directory are moved into sub-folders (`images/`, `music/`, `video/`, `executables/`, `archives/`, `torrent/`, `documents/`, `code/`, `design-files/`) based on their extension. + +## How to run on localhost ``` python organizer.py @@ -11,7 +21,3 @@ python organizer.py ## Dependencies Standard library only (`os`, `shutil`). - -## Pyodide-runnable - -No — it walks and moves files on the real filesystem, which the browser sandbox does not provide. diff --git a/projects/Othello/README.md b/projects/Othello/README.md index f7e8eef85..c67a355f6 100644 --- a/projects/Othello/README.md +++ b/projects/Othello/README.md @@ -46,6 +46,15 @@ Currently the algorithm makes use of the following heuristics for a position's e - Mobility - Corner Values +## Example + +1. Run `python main.py` from the `src/` directory; a 1200×800 pygame window titled "Othello/Reversi" opens showing a mode-selection screen. +2. Press `H` to start a two-player game, or `A` to play against the computer (minimax AI). +3. The 8×8 board appears with two black and two white discs in the centre. Legal moves for the current player are shown as translucent circles. +4. Click a highlighted square to place your disc and flip the opponent's outflanked discs. +5. Press `L` at any time to toggle between light and dark colour themes. +6. When no moves remain for either player, a win/draw/loss screen fades in; press `R` to restart or `Q` to quit. + ### How to Play the Game 🎮 1. Download the source code. (Either clone the repository or download the whole code from GitHub) 2. Make sure to install Python3 on your Computer along with `pip`. (Python3.10+ is required) @@ -55,7 +64,3 @@ Currently the algorithm makes use of the following heuristics for a position's e ``` 5. Navigate to the `src` directory, run the `main.py` file and play the game! > P.S. You can change color modes using the 'L' key! - -## Pyodide-runnable - -No — it is a `pygame` GUI game that requires a display window and local image/font assets. diff --git a/projects/Otp_Generator/README.md b/projects/Otp_Generator/README.md index 477e60b5e..8370ad141 100644 --- a/projects/Otp_Generator/README.md +++ b/projects/Otp_Generator/README.md @@ -2,7 +2,15 @@ Generates one-time passwords (OTPs) of a given length. The `Otp` class can produce OTPs made of digits only, digits with uppercase letters, digits with lowercase letters, or all three combined. -## How to run +## Example + +```text +OTP:7392841650 +``` + +Running `otpGen.py` directly prints a 10-digit numeric OTP generated by `Otp(10).digits`. + +## How to run on localhost ``` python otpGen.py @@ -11,7 +19,3 @@ python otpGen.py ## Dependencies Standard library only (`random`). - -## Pyodide-runnable - -Yes — it is a pure-stdlib program that simply prints a generated OTP. diff --git a/projects/PDF_Reader/README.md b/projects/PDF_Reader/README.md index 856de1be0..1e11e49f6 100644 --- a/projects/PDF_Reader/README.md +++ b/projects/PDF_Reader/README.md @@ -2,7 +2,22 @@ An AI-powered PDF question-answering tool. It loads a PDF with LangChain, splits and embeds the text into a Chroma vector store, summarises the document with an OpenAI model, and answers user questions about the content interactively. -## How to run +## Example + +```text +Text in pdf ... [full extracted text from promptEngineering.pdf] ... +Summary of text in pdf Prompt engineering is the practice of designing and +refining input prompts to guide large language models toward desired outputs... + +INSTRUCTIONS: +Enter the question you want to ask from pdf text OR press "-1" to STOP +Enter your question: What is prompt engineering? +Prompt engineering is the process of crafting inputs that steer an AI model +to produce accurate, relevant, and useful responses. +Enter your question: -1 +``` + +## How to run on localhost ``` pip install -r requirements.txt @@ -17,7 +32,3 @@ python PDF_Reader.py ## Dependencies - openai, langchain, chromadb, pypdf and others (see `requirements.txt`) - -## Pyodide-runnable - -No — it makes network calls to the OpenAI API and depends on LangChain/Chroma, none of which work in a browser sandbox. diff --git a/projects/PONG/README.md b/projects/PONG/README.md index 359c4a20f..7a62423c5 100644 --- a/projects/PONG/README.md +++ b/projects/PONG/README.md @@ -2,7 +2,15 @@ A single-player Pong game built with `pygame`. It features a difficulty menu (Easy / Medium / Hard), an AI-controlled opponent paddle, sound effects, and a score counter. -## How to run +## Example + +1. A 1000x800 window titled "Pong" opens showing a "Difficulty" menu with three buttons: EASY, MEDIUM, and HARD. +2. Click EASY to start a game. The screen shows two paddles, a centre dividing strip, and the ball appears after a 3-second countdown. +3. Press the Up / Down arrow keys to move your (right) paddle; the AI opponent controls the left paddle. +4. The ball bounces off walls and paddles with sound effects. Each time it passes a paddle, the scoring player's counter increments in the centre of the screen. +5. Press Escape at any time to return to the difficulty menu. + +## How to run on localhost ``` pip install pygame @@ -14,7 +22,3 @@ Run from the `PONG` directory so the `assets/` files are found. ## Dependencies - pygame - -## Pyodide-runnable - -No — it is a `pygame` GUI game that requires a display window, sound, and local image/font assets. diff --git a/projects/Password Projects/Password Breach Frequency/README.md b/projects/Password Projects/Password Breach Frequency/README.md index 257a53bc4..566828d58 100644 --- a/projects/Password Projects/Password Breach Frequency/README.md +++ b/projects/Password Projects/Password Breach Frequency/README.md @@ -15,6 +15,3 @@ This will launch the interactive program, which will request the password from t Enter password: 12345678 Your password hash has appeared 5,172,909 times in known data breaches. ``` -## Pyodide-runnable - -No — it queries the HaveIBeenPwned API over the network using `requests`. diff --git a/projects/Password Projects/Password Generator/README.md b/projects/Password Projects/Password Generator/README.md index 1cf782fab..ff8c6141e 100644 --- a/projects/Password Projects/Password Generator/README.md +++ b/projects/Password Projects/Password Generator/README.md @@ -2,7 +2,22 @@ Generates a random password of a user-chosen length, drawn from lowercase and uppercase letters, digits and punctuation symbols. -## How to run +## Example + +```text +_____________________________________ +| Welcome to this Password Generator | +------------------------------------- + +how long do you want your password to be (minimum of 8 number)12 + +Your password is: gT$3@kLp!mZq +__________________________________________ +| Thanks for using the Password Generator | +------------------------------------------ +``` + +## How to run on localhost ``` python main.py @@ -11,7 +26,3 @@ python main.py ## Dependencies Standard library only (`random`, `string`). - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/Password Projects/Password Hashing/readme.md b/projects/Password Projects/Password Hashing/readme.md index 4ebd99060..e5a5775b8 100644 --- a/projects/Password Projects/Password Hashing/readme.md +++ b/projects/Password Projects/Password Hashing/readme.md @@ -25,6 +25,3 @@ $ python hashing_passwords.py Github < hash-type : sha256 > 1720d8eaff790da6af4406905ba663d0cc6a6cea2b3e54e7384ac334a037f59d ``` -## Pyodide-runnable - -Yes — it is a pure-stdlib program; the password is supplied via command-line arguments rather than `input()`. diff --git a/projects/Password Projects/Password Meter/README.md b/projects/Password Projects/Password Meter/README.md index 8be26dff3..de66725a1 100644 --- a/projects/Password Projects/Password Meter/README.md +++ b/projects/Password Projects/Password Meter/README.md @@ -2,7 +2,29 @@ Scores the strength of a password using a set of rules. It awards points for character count, uppercase/lowercase letters, numbers and symbols, and deducts points for weaknesses such as letters-only passwords or consecutive/sequential characters. -## How to run +## Example + +```text +Type your password: +Tr0ub4dor! +40 % Number of Characters +16 % Uppercase Letters +0 % Lowercase Letters +16 % Numbers +6 % Symbols +8 % Middle Numbers or Symbols +8 % Requirements +============Deductions============= +0 % Letters Only +0 % Numbers Only +-2 % Consecutive Uppercase Letters +0 % Consecutive Lowercase Letters +0 % Consecutive Numbers +0 % Sequential Letters +0 % Sequential Numbers +``` + +## How to run on localhost ``` python main.py @@ -11,7 +33,3 @@ python main.py ## Dependencies Standard library only (`string`). - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/Password Projects/Password_manager/README.md b/projects/Password Projects/Password_manager/README.md index e49ad058f..3c699b840 100644 --- a/projects/Password Projects/Password_manager/README.md +++ b/projects/Password Projects/Password_manager/README.md @@ -2,7 +2,15 @@ A desktop password manager built with Tkinter. Users can store a website, email and password, generate random passwords, save the entries to a `data.json` file, and search for stored credentials. -## How to run +## Example + +1. The "Password Managger" window opens with fields for Website, Email, and Password, plus a logo image. +2. Type a website name (e.g. `github.com`) in the Website field and your email in the Email field. +3. Click "genetrate" to fill the Password field with a randomly generated password. +4. Click "ADD" — a confirmation dialog shows the entered password and email; click OK to save to `data.json`. +5. To retrieve saved credentials, type a website name and click "Search"; a dialog shows the stored email and password for that site. + +## How to run on localhost ``` pip install scipy @@ -13,7 +21,3 @@ python main.py - scipy (imported by `passgen.py`) - Tkinter (bundled with Python) - -## Pyodide-runnable - -No — it is a Tkinter GUI application that also reads and writes a JSON file on the local filesystem. diff --git a/projects/Password Projects/README.md b/projects/Password Projects/README.md index cee78ee3e..083979302 100644 --- a/projects/Password Projects/README.md +++ b/projects/Password Projects/README.md @@ -2,6 +2,28 @@ The Password Projects directory contains several mini-projects related to passwords and their security. +## Example + +The sub-projects are independent console programs. For example, running the Password Generator: + +```text +$ python "Password Generator/main.py" +_____________________________________ +| Welcome to this Password Generator | +------------------------------------- + +how long do you want your password to be (minimum of 8 number)12 + +Enter the length of password: 12 + +Your password is: aB3$kL!mQr9p +__________________________________________ +| Thanks for using the Password Generator | +------------------------------------------ +``` + +See each sub-project's own README for its specific usage and output. + The projects are: 1. [Password Breach Frequency](https://github.com/u749929/python-beginner-projects/tree/main/projects/Password%20Projects/Password%20Breach%20Frequency) @@ -21,7 +43,3 @@ The projects are: 6. [WiFi Password Generator](https://github.com/u749929/python-beginner-projects/tree/main/projects/Password%20Projects/WiFi%20Password%20Generator) * This program will attempt to get the password for the conected wifi network and retunr this password to the user. If no password is found, the program will return nothing. - -## Pyodide-runnable - -Mixed — see each sub-project README. The console tools (Password Generator, Password Hashing, Password Meter, strong-password-detector) run in Pyodide; Password Breach Frequency needs network access, Password Manager is a tkinter GUI, and the WiFi Password Generator uses Windows-only OS commands. diff --git a/projects/Password Projects/WiFi Password Generator/README.md b/projects/Password Projects/WiFi Password Generator/README.md index dc7a8a148..9048ad402 100644 --- a/projects/Password Projects/WiFi Password Generator/README.md +++ b/projects/Password Projects/WiFi Password Generator/README.md @@ -2,7 +2,17 @@ Retrieves the saved Wi-Fi passwords on a Windows machine. It runs `netsh wlan` commands to list the stored wireless profiles and prints each network name alongside its password. -## How to run +## Example + +```text +HomeNetwork | MySecretPass123 +OfficeWifi | workpassword99 +GuestNetwork | +``` + +Each saved wireless profile is printed with its name left-aligned in a 30-character column, followed by `|` and its stored password. Profiles that have no saved password (open networks) show an empty value. + +## How to run on localhost ``` python wifi.py @@ -13,7 +23,3 @@ python wifi.py ## Dependencies Standard library only (`subprocess`). - -## Pyodide-runnable - -No — it shells out to Windows-only `netsh` system commands via `subprocess`, which the browser sandbox cannot run. diff --git a/projects/Password Projects/strong-password-detector/README.md b/projects/Password Projects/strong-password-detector/README.md index d47481106..3ef2e1976 100644 --- a/projects/Password Projects/strong-password-detector/README.md +++ b/projects/Password Projects/strong-password-detector/README.md @@ -2,7 +2,16 @@ Checks whether a password is "strong" using regular expressions: it must be at least eight characters long and contain uppercase letters, lowercase letters and at least one digit. -## How to run +## Example + +```text +$ python strong-password.py +True +``` + +Running the script tests the hardcoded sample password `"A&dsas9$_"` against the strength rules (≥8 characters, uppercase, lowercase, digit) and prints `True` because it passes all four checks. Change the `password` variable in the script to test other passwords. + +## How to run on localhost ``` python strong-password.py @@ -11,7 +20,3 @@ python strong-password.py ## Dependencies Standard library only (`re`). - -## Pyodide-runnable - -Yes — it is a pure-stdlib program with hard-coded input and `print()` output. diff --git a/projects/Personal-Finance-tracker/README.md b/projects/Personal-Finance-tracker/README.md index 79ec4e08b..3626d32b4 100644 --- a/projects/Personal-Finance-tracker/README.md +++ b/projects/Personal-Finance-tracker/README.md @@ -2,7 +2,31 @@ A console-based personal finance manager. It lets you add expenses (name, amount, category), list all expenses, and calculate the total spent, all from a simple text menu. -## How to run +## Example + +```text +Personal Finance Manager +1. Add Expense +2. List Expenses +3. Calculate Total Expenses +4. Exit +Enter your choice: 1 +Expense Name: Groceries +Expense Amount: 45.50 +Expense Category: Food +Personal Finance Manager +1. Add Expense +2. List Expenses +3. Calculate Total Expenses +4. Exit +Enter your choice: 2 +Name: Groceries, Amount: 45.5, Category: Food +Enter your choice: 3 +Total Expenses: 45.5 +Enter your choice: 4 +``` + +## How to run on localhost ``` python main.py @@ -11,7 +35,3 @@ python main.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/Pig_latin/README.md b/projects/Pig_latin/README.md index 5355fc36d..ccd81bbb8 100644 --- a/projects/Pig_latin/README.md +++ b/projects/Pig_latin/README.md @@ -2,7 +2,18 @@ Converts an English sentence into Pig Latin. `function.py` holds the conversion logic and `main.py` prompts the user for a string and prints the converted result. -## How to run +## Example + +```text +Hello! Welcome to a Pig Latin converter +======================================= +Please type the string you would like to convert to pig latin: hello world + +Your converted string is: +ellohay orldway +``` + +## How to run on localhost ``` python main.py @@ -11,7 +22,3 @@ python main.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using `input()` and `print()`. diff --git a/projects/Pokemon Battle/README.md b/projects/Pokemon Battle/README.md index db106eaac..52d33dbe9 100644 --- a/projects/Pokemon Battle/README.md +++ b/projects/Pokemon Battle/README.md @@ -2,7 +2,45 @@ A turn-based Pokemon battle game in the console. Two Pokemon (with types, moves, attack and defense stats) fight each other; type advantages affect damage, health bars update each turn, and text is printed one character at a time for effect. -## How to run +## Example + +```text +-----POKEMONE BATTLE----- + +Blastoise +TYPE/ Water +ATTACK/ 10 +DEFENSE/ 10 +LVL/ 33.0 + +VS + +Squirtle +TYPE/ Water +ATTACK/ 3 +DEFENSE/ 3 +LVL/ 12.0 + +Blastoise HLTH ==================== +Squirtle HLTH ==================== + +Go Blastoise! +1. Water Gun +2. Bubblebeam +3. Hydro Pump +4. Surf +Pick a move: 3 +Blastoise used Hydro Pump! +Its not very effective... + +Blastoise HLTH ==================== +Squirtle HLTH ========== + +...Squirtle fainted. +Opponent paid you $2341. +``` + +## How to run on localhost ``` pip install numpy @@ -12,7 +50,3 @@ python pokemon.py ## Dependencies - numpy - -## Pyodide-runnable - -Yes — `numpy` is available in Pyodide and the program is otherwise a console game using `input()` and `print()`. diff --git a/projects/Port_Scaner/README.md b/projects/Port_Scaner/README.md index 0dd574388..7600ece6c 100644 --- a/projects/Port_Scaner/README.md +++ b/projects/Port_Scaner/README.md @@ -3,7 +3,26 @@ Port Scanner is developed for: checking if the port is open -## How to run +## Example + +```text + ____ _ ____ +| _ \ ___ _ __| |_ / ___| ___ __ _ _ __ _ __ ___ _ __ +| |_) / _ \| '__| __| \___ \ / __/ _` | '_ \| '_ \ / _ \ '__| +| __/ (_) | | | |_ ___) | (_| (_| | | | | | | | __/ | +|_| \___/|_| \__| |____/ \___\__,_|_| |_|_| |_|\___|_| + +Port Scanner +Enter the port to check: 80 +Port is open +``` + +```text +Enter the port to check: 9999 +Port is not open +``` + +## How to run on localhost ```sh pip install pyfiglet @@ -14,7 +33,3 @@ python port_scanner.py - pyfiglet - socket (standard library) - -## Pyodide-runnable - -No - it uses real TCP sockets (`socket.connect_ex`), which are not available in the browser sandbox. \ No newline at end of file diff --git a/projects/Print_Colored_Text/README.md b/projects/Print_Colored_Text/README.md index b2e26bdf1..4085ee781 100644 --- a/projects/Print_Colored_Text/README.md +++ b/projects/Print_Colored_Text/README.md @@ -2,7 +2,17 @@ Prints a few lines of text to the terminal in different foreground and background colors using the colorama library. -## How to run +## Example + +```text +Hi, My name is Nowshin I love open-source contribution +Hi, My name is Nowshin +Hi My name is Nowshin +``` + +The first line prints with a blue foreground on a yellow background, then switches to yellow text on a blue background mid-sentence. The second line uses a cyan background. The third line uses red text on a green background. + +## How to run on localhost ```sh pip install colorama @@ -12,7 +22,3 @@ python print_coloured_text.py ## Dependencies - colorama - -## Pyodide-runnable - -No - colorama emits ANSI escape codes for a real terminal and is not bundled with Pyodide. diff --git a/projects/ProjectEuler/Problem 1/Readme.md b/projects/ProjectEuler/Problem 1/Readme.md index 6be229201..308fba9b9 100644 --- a/projects/ProjectEuler/Problem 1/Readme.md +++ b/projects/ProjectEuler/Problem 1/Readme.md @@ -1,3 +1,10 @@ +## Example + +```text +$ python p1.py +233168 +``` + ## Multiples of 3 and 5 --- diff --git a/projects/ProjectEuler/Problem 2/Readme.md b/projects/ProjectEuler/Problem 2/Readme.md index 623a83c7e..9947c1e21 100644 --- a/projects/ProjectEuler/Problem 2/Readme.md +++ b/projects/ProjectEuler/Problem 2/Readme.md @@ -1,4 +1,12 @@ # Fibonacci Numbers + +## Example + +```text +$ python p2.py +4613732 +``` + --- Each new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with $1$ and $2$ , the first $10$ terms will be: $1,2,3,5,8,13,21,34,55,89...$ diff --git a/projects/ProjectEuler/Problem 3/Readme.md b/projects/ProjectEuler/Problem 3/Readme.md index ddc5d8ac3..1ab014738 100644 --- a/projects/ProjectEuler/Problem 3/Readme.md +++ b/projects/ProjectEuler/Problem 3/Readme.md @@ -1,5 +1,12 @@ # Largest Prime Factor +## Example + +```text +$ python p3.py +6857 +``` + The Prime factors of $13195$ are $5$ , $7$ , $13$ and $29$. What is the largest prime factor of the number $600851475143$?? diff --git a/projects/ProjectEuler/Problem 4/Readme.md b/projects/ProjectEuler/Problem 4/Readme.md index b283fdf98..3570e98bf 100644 --- a/projects/ProjectEuler/Problem 4/Readme.md +++ b/projects/ProjectEuler/Problem 4/Readme.md @@ -1,5 +1,12 @@ # Largest Palindrome Product +## Example + +```text +$ python p4.py +906609 +``` + A Palindromic nuber reads the same both ways. The largest palindrome made from the product of two 2-digit numbers is $9009 = 91\times 99$ Find the largest palindrome made from the product of two 3-digit number. diff --git a/projects/ProjectEuler/Problem 5/Readme.md b/projects/ProjectEuler/Problem 5/Readme.md index 6d91a7caa..8ecf579d9 100644 --- a/projects/ProjectEuler/Problem 5/Readme.md +++ b/projects/ProjectEuler/Problem 5/Readme.md @@ -1,5 +1,12 @@ # Smallest Multiple +## Example + +```text +$ python p5.py +232792560 +``` + $2520$ is the smallest number that can be divided by each of the numbers from $1$ to $10$ without any remainder. diff --git a/projects/ProjectEuler/Problem 6/Readme.md b/projects/ProjectEuler/Problem 6/Readme.md index 14c82ff33..280d40c66 100644 --- a/projects/ProjectEuler/Problem 6/Readme.md +++ b/projects/ProjectEuler/Problem 6/Readme.md @@ -1,5 +1,12 @@ # Sum Square Difference +## Example + +```text +$ python p6.py +25164150 +``` + The sum of the squares of the first ten natural numbers is: $1^2 + 2^2 + ... + 10^2 = 385$ diff --git a/projects/ProjectEuler/Problem 7/Readme.md b/projects/ProjectEuler/Problem 7/Readme.md index 6b02b2e85..eedb32a79 100644 --- a/projects/ProjectEuler/Problem 7/Readme.md +++ b/projects/ProjectEuler/Problem 7/Readme.md @@ -1,5 +1,12 @@ # 10001st prime +## Example + +```text +$ python p7.py +104743 +``` + By listing the first six prime numbers: $2,3,5,7,11$ and $13$, we can see that the $6^{th}$ prime is $13$. What is the $10001^{st}$ prime number? ----- diff --git a/projects/ProjectEuler/Problem 8/Readme.md b/projects/ProjectEuler/Problem 8/Readme.md index b6b480455..f2a5fa6f8 100644 --- a/projects/ProjectEuler/Problem 8/Readme.md +++ b/projects/ProjectEuler/Problem 8/Readme.md @@ -1,5 +1,12 @@ # Largest product in a series +## Example + +```text +$ python p8.py +23514624000 +``` + The four adjacent digits in the $1000$ digit number that have the greatest product are $9 \times 9 \times 8 \times 9 = 5832$. diff --git a/projects/ProjectEuler/Problem 9/Readme.md b/projects/ProjectEuler/Problem 9/Readme.md index 5fb814981..4e71be34d 100644 --- a/projects/ProjectEuler/Problem 9/Readme.md +++ b/projects/ProjectEuler/Problem 9/Readme.md @@ -1,4 +1,12 @@ # Special Pythagorean triplet + +## Example + +```text +$ python p9.py +31875000 +``` + A Pythagorean triplet is a set of three natural numbers, $a < b < c$, for which, $$a^2 + b^2 = c^2$$ diff --git a/projects/ProjectEuler/README.md b/projects/ProjectEuler/README.md index 5a626ce0a..5498acdba 100644 --- a/projects/ProjectEuler/README.md +++ b/projects/ProjectEuler/README.md @@ -1,13 +1,29 @@ # Project-Euler + Project Euler is a series of challenging mathematical/computer programming problems that will require more than just mathematical insights to solve. Although mathematics will help you arrive at elegant and efficient methods, the use of a computer and programming skills will be required to solve most problems. +## ![alt text](https://projecteuler.net/images/clipart/euler_portrait.png) -![alt text](https://projecteuler.net/images/clipart/euler_portrait.png) --------- ## Where should I start? + That depends on your background. There are two tables containing problems. The Recent problems table lists the ten most recently published problems, so if you are new to Project Euler then you may prefer to start with the Archives to get a feel for the different types/difficulties of our problems. The first one-hundred or so problems are generally considered to be easier than the problems which follow. In the archives table you will be able to see how many people have solved each problem; as a general rule of thumb the more people that have solved it, the easier it is. To assist further there is a difficulty rating system which may also help you decide where to start. You are able to sort the problems in the archives table on ID, Solved By, or Difficulty. -## How to run +## Example + +```text +$ python "Problem 1/p1.py" +233168 + +$ python "Problem 2/p2.py" +4613732 + +$ python "Problem 7/p7.py" +104743 +``` + +Each script prints a single number — the answer to that problem — and exits immediately. + +## How to run on localhost Each problem lives in its own `Problem N` folder. Run a solution directly, e.g.: @@ -18,15 +34,3 @@ python "Problem 1/p1.py" ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes - every solution is a pure-stdlib mathematical computation that prints a result, with no I/O beyond `print`. - - - - - - - - diff --git a/projects/Python Banking System/readme.md b/projects/Python Banking System/readme.md index 7d1c25e69..57976c998 100644 --- a/projects/Python Banking System/readme.md +++ b/projects/Python Banking System/readme.md @@ -20,7 +20,35 @@ The Python Banking System is a simple console-based banking application that all - Check your account balance at any time. - View your transaction history. -## How to run +## Example + +```text +Python Banking System +1. Create Account +2. Perform Transaction +3. Check Balance +4. Transaction History +5. Exit +Enter your choice: 1 +Enter your name: Alice +Enter initial balance: 1000 +Account created successfully. Account Number: 47382910 + +Enter your choice: 2 +Enter account number: 47382910 +Enter transaction type (deposit/withdraw): deposit +Enter transaction amount: 500 +Transaction completed. + +Enter your choice: 3 +Enter account number: 47382910 +Account Balance: $1500.0 + +Enter your choice: 5 +Exiting the Python Banking System. Goodbye! +``` + +## How to run on localhost ```sh python Banking.py @@ -29,8 +57,3 @@ python Banking.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes - it is a pure-stdlib console program using only `input()`/`print()` and the `random` module. - diff --git a/projects/Python story generator/README.md b/projects/Python story generator/README.md index 4810ef12f..53133b337 100644 --- a/projects/Python story generator/README.md +++ b/projects/Python story generator/README.md @@ -2,7 +2,13 @@ A Tkinter desktop application that generates a random short story (at least 100 words) by combining randomly chosen phrases. Click the "Generate Story" button to produce a new story. -## How to run +## Example + +1. The window titled "Python Story Generator" opens with a "Generate Story" button and a scrollable text area. +2. Click "Generate Story". The text area fills with a multi-sentence story built from random phrase combinations, for example: "A few years ago, a rabbit named Ali, who lived in Barcelona, decided to go to the cinema. Ali made a lot of friends while at the cinema." +3. Click "Generate Story" again to replace the text with a completely new story of at least 100 words. + +## How to run on localhost ```sh pip install ttkthemes @@ -13,7 +19,3 @@ python story.py - ttkthemes - tkinter (standard library) - -## Pyodide-runnable - -No - it builds a Tkinter GUI with `ttkthemes`, which is not available in the browser sandbox. diff --git a/projects/QRCode Scanner/Readme.md b/projects/QRCode Scanner/Readme.md index 49492eaea..4d06ef0b3 100644 --- a/projects/QRCode Scanner/Readme.md +++ b/projects/QRCode Scanner/Readme.md @@ -6,6 +6,29 @@ - opencv-python - numpy +## Example + +**Scan from an image file:** +```text +1. Scan via image +2. Scan via WebCam + Choice: 1 +Enter Image path: sample_qr.png +QRCode Encoded Data: https://example.com +``` + +**Scan via webcam:** +```text +1. Scan via image +2. Scan via WebCam + Choice: 2 +``` +A live webcam window opens. When a QR code comes into frame, a green polygon is drawn around it and the decoded text is printed: +```text +Barcode: https://example.com | Type: QRCODE +``` +Press `q` to close the window. + ## Installation Simple, Just clone this repository to your local storage and run the below command. ` pip install -r requirements.txt ` @@ -19,7 +42,3 @@ Run the below command in command prompt or terminal. - opencv-python - numpy - pyzbar - -## Pyodide-runnable - -No - it uses OpenCV (`cv2.imshow`, webcam capture) and `pyzbar`, none of which are available in the browser sandbox. diff --git a/projects/QRCode-Generator/README.md b/projects/QRCode-Generator/README.md index 3856f373d..d7845df3b 100644 --- a/projects/QRCode-Generator/README.md +++ b/projects/QRCode-Generator/README.md @@ -2,7 +2,16 @@ A console script that asks for a website/URL, encodes it into a QR code, and saves the image to the `static` folder as `qrcode.jpg`. -## How to run +## Example + +```text +Enter the website you need to make QR Code +https://github.com +``` + +The script encodes `https://github.com` into a QR code image and saves it to `static\qrcode.jpg` with no further output. + +## How to run on localhost ```sh pip install qrcode pillow @@ -13,7 +22,3 @@ python main.py - qrcode - pillow - -## Pyodide-runnable - -No - it depends on the `qrcode` and `pillow` packages and writes an image file to a real disk path. diff --git a/projects/Qt5_YouTube/README.md b/projects/Qt5_YouTube/README.md index 698148fce..3cfc16511 100644 --- a/projects/Qt5_YouTube/README.md +++ b/projects/Qt5_YouTube/README.md @@ -2,7 +2,15 @@ A PyQt5 desktop application that plays local video files and YouTube videos, with playback controls for position, volume, and speed. -## How to run +## Example + +1. The window titled "QTube" opens with a search box at the top, a blank video area, playback controls (play/pause button, position slider, elapsed/duration labels), a volume slider, and a speed slider. +2. Type a YouTube URL or search query (e.g. `lofi beats`) into the search box and press Enter. The player resolves the stream URL via youtube_dl and enables the play button. +3. Click the play button to start playback. The elapsed label updates in real time (e.g. `0:00:45`) and the buffer percentage is shown (e.g. `Buffer: 80%`). +4. Drag the volume slider to adjust volume; the label updates to show `🔈 75%`. Drag the speed slider to change playback rate (e.g. `Speed: 2x`). +5. Use File > Open (Ctrl+O) to load and play a local video file instead. + +## How to run on localhost ```sh pip install PyQt5 youtube_dl @@ -13,7 +21,3 @@ python main.py - PyQt5 - youtube_dl - -## Pyodide-runnable - -No - it builds a PyQt5 GUI and uses `youtube_dl` for network access, neither of which works in the browser sandbox. diff --git a/projects/QuickWordCloud/README.md b/projects/QuickWordCloud/README.md index 5abe5b7c8..7a500f49d 100644 --- a/projects/QuickWordCloud/README.md +++ b/projects/QuickWordCloud/README.md @@ -2,6 +2,12 @@ Quick word cloud is a small and interesting project that can quickly create a word cloud out of a text file. Its a small & helpful project that can be used in websites or for different analysis. +## Example + +1. Place a plain-text file at `./data/test_data.txt` (or update `file_name` in `main.py`). +2. Run `python main.py`. The script reads all words from the file, converts them to lowercase, and generates a 1600x800 word cloud. +3. A matplotlib window opens displaying the word cloud image with a white background. Frequently occurring words appear larger. Close the window to exit. + ## Installation ### Install and activate a virtual env @@ -30,7 +36,3 @@ pytest From IDE right click and run.(As per the IDE options) From terminal python main.py - -## Pyodide-runnable - -No - it depends on the `wordcloud` package and opens a `matplotlib` GUI window, which are not supported in the Pyodide sandbox. diff --git a/projects/Quiz Game/README.md b/projects/Quiz Game/README.md index d3124f1ce..5eae41995 100644 --- a/projects/Quiz Game/README.md +++ b/projects/Quiz Game/README.md @@ -2,7 +2,26 @@ A console-based quiz game that asks three questions, scores the player's answers, and stores each player's name and score in a local SQLite database (`quiz_game.db`). Previous scores are displayed after each game. Run `setup_db.py` once first to create the database and table. -## How to run +## Example + +```text +Welcome to AskPython Quiz +Are you ready to play the Quiz? (yes/no) :yes +Question 1: What is your Favourite programming language?python +correct +Question 2: Do you follow any author on AskPython? yes +correct +Question 3: What is the name of your favourite website for learning Python?askpython +correct +Thank you for Playing this small quiz game, you attempted 3 questions correctly! +Marks obtained: 100% +Enter your name: Bob +Previous scores: +Name: Bob, Score: 3, Date: 2024-01-15 10:23:45 +BYE! +``` + +## How to run on localhost ```sh python setup_db.py @@ -12,7 +31,3 @@ python main.py ## Dependencies Standard library only (uses `sqlite3`). - -## Pyodide-runnable - -Yes - it is a pure-stdlib console program; `sqlite3` is supported in Pyodide and the database is created in the in-memory virtual filesystem. diff --git a/projects/Random-Quote-Generator/README.md b/projects/Random-Quote-Generator/README.md index 5abdaaaa8..54e0e2d96 100644 --- a/projects/Random-Quote-Generator/README.md +++ b/projects/Random-Quote-Generator/README.md @@ -20,6 +20,12 @@ The Random Quote Generator is a simple Python application that fetches and displ - Tkinter library (usually included with Python) - Requests library (for making HTTP requests) +## Example + +1. The window titled "Random Quote Generator" opens and immediately fetches a quote from the Quotable API, displaying it in the label, for example: `"The only way to do great work is to love what you do." - Steve Jobs` +2. Click "Get a New Quote" to fetch and display another random quote with its author. +3. If the internet connection is unavailable, the label shows: `Failed to fetch a quote. Check your internet connection.` + ## Getting Started 1. Clone this repository to your local machine. @@ -77,11 +83,6 @@ We welcome contributions to improve and expand this Random Quote Generator proje 6. Create a pull request from your branch to the main repository. 7. Wait for your pull request to be reviewed and merged. - -## Pyodide-runnable - -No - it builds a Tkinter GUI and uses `requests` to call an external API, neither of which works in the browser sandbox. - ## Acknowledgments - Thanks to [Quotable API](https://quotable.io/) for providing the quotes. @@ -94,4 +95,4 @@ Happy quoting! 🚀 ``` Feel free to customize it further to fit your project's specific details and needs. -This README provides a clear and organized guide for users and potential contributors to understand and interact with your Random Quote Generator project. If you have any more questions or need further assistance, just hit me up, pal! \ No newline at end of file +This README provides a clear and organized guide for users and potential contributors to understand and interact with your Random Quote Generator project. If you have any more questions or need further assistance, just hit me up, pal! diff --git a/projects/Rename_Images/README.md b/projects/Rename_Images/README.md index 626408319..ac42505f2 100644 --- a/projects/Rename_Images/README.md +++ b/projects/Rename_Images/README.md @@ -2,7 +2,18 @@ A console tool that walks a given directory and lets the user interactively rename each image file (jpg, png, jpeg). -## How to run +## Example + +```text +Enter the directory path of the images: /home/alice/photos +Enter the new name for IMG_001.jpg (without extension): vacation_beach +Enter the new name for IMG_002.png (without extension): sunset_view +Done renaming images. +``` + +Each image in the directory is presented one at a time; entering a name that already exists prompts for a different name. + +## How to run on localhost ```sh python rename_images.py @@ -11,7 +22,3 @@ python rename_images.py ## Dependencies Standard library only (uses `os`). - -## Pyodide-runnable - -No - it walks and renames files on the real filesystem with `os.listdir`/`os.rename`, which is not available in the browser sandbox. diff --git a/projects/Resize_Image/README.md b/projects/Resize_Image/README.md index e92816a21..2359a0e3d 100644 --- a/projects/Resize_Image/README.md +++ b/projects/Resize_Image/README.md @@ -2,7 +2,20 @@ A console tool that resizes one image or all images in a directory to a user-specified width and height, then saves the results to an output folder. -## How to run +## Example + +```text +Do you want to resize multiple images? (yes/no): no +Enter the width: 800 +Enter the height: 600 +Enter the image path: /home/alice/photo.jpg +Enter the output folder: /home/alice/resized +Enter the new name for photo.jpg: photo_resized.jpg +Resized image is saved as /home/alice/resized/photo_resized.jpg. +Done resizing images. +``` + +## How to run on localhost ```sh pip install pillow @@ -12,7 +25,3 @@ python resize.py ## Dependencies - pillow - -## Pyodide-runnable - -No - it uses the `pillow` package and reads/writes image files on the real filesystem, which is not available in the browser sandbox. diff --git a/projects/RestrauntAPI/README.md b/projects/RestrauntAPI/README.md index a14e41113..797012dd7 100644 --- a/projects/RestrauntAPI/README.md +++ b/projects/RestrauntAPI/README.md @@ -1,27 +1,61 @@ # Restraunt_API + The Restaurant API consists of: adding items to the menu, viewing the menu, searching for menu items based on a string, creating orders, and displaying previous orders for a user. Add to Menu: + - Endpoint: POST http://127.0.0.1:8000/menu/ - + View Menu: + - Endpoint: GET http://127.0.0.1:8000/menu/ Both the "Add to Menu" and "View Menu" functions are implemented using a single endpoint with POST and GET methods, respectively. Search Menu Based on String: + - Endpoint: GET http://127.0.0.1:8000/menu_item// -This endpoint allows you to search for menu items based on a provided string. + This endpoint allows you to search for menu items based on a provided string. Create Order: + - Endpoint: POST http://127.0.0.1:8000/order/ -This endpoint is used to create orders. + This endpoint is used to create orders. View Past Orders: + - Endpoint: GET http://127.0.0.1:8000/orders// -You can use this endpoint to view the previous orders of a user by providing their email address. + You can use this endpoint to view the previous orders of a user by providing their email address. + +## Example + +**Add a menu item (POST):** +```text +POST http://127.0.0.1:8000/menu/ +Body: { "Item_name": "Margherita Pizza", "price": 12.99 } +→ 201 Created +``` -## How to run +**View the full menu (GET):** +```text +GET http://127.0.0.1:8000/menu/ +→ 200 OK +[{"Item_name": "Margherita Pizza", "price": 12.99}, ...] +``` + +**Search for an item:** +```text +GET http://127.0.0.1:8000/menu_item/Margherita Pizza/ +→ 200 OK {"Item_name": "Margherita Pizza", "price": 12.99} +``` + +**View past orders for a user:** +```text +GET http://127.0.0.1:8000/orders/alice@example.com/ +→ 200 OK [{"user": "alice@example.com", "items": [...], "ord_date": "2024-01-15"}] +``` + +## How to run on localhost ```sh pip install -r requirements.txt @@ -35,7 +69,3 @@ python manage.py runserver - Django - djangorestframework - Pillow - -## Pyodide-runnable - -No - it is a Django web application that requires a running HTTP server and database, which cannot run in the browser sandbox. diff --git a/projects/Rock_Paper_Scissors/README.md b/projects/Rock_Paper_Scissors/README.md index 576353848..ab95f32de 100644 --- a/projects/Rock_Paper_Scissors/README.md +++ b/projects/Rock_Paper_Scissors/README.md @@ -2,7 +2,23 @@ A console Rock-Paper-Scissors game. The player picks a move each round, the computer picks randomly, and the score is tracked until the player chooses to exit. -## How to run +## Example + +```text +1. Rock, 2. Paper, 3. Scissors, 4. Exit: 1 +You chose Rock and computer chose Scissors +You Win! + +1. Rock, 2. Paper, 3. Scissors, 4. Exit: 2 +You chose Paper and computer chose Paper +Its a draw. + +1. Rock, 2. Paper, 3. Scissors, 4. Exit: 4 +Your score is 1/1 +Thanks for playing. +``` + +## How to run on localhost ```sh python main.py @@ -11,7 +27,3 @@ python main.py ## Dependencies Standard library only (uses `random`). - -## Pyodide-runnable - -Yes - it is a pure-stdlib console program using only `input()`/`print()` and the `random` module. diff --git a/projects/Roll_A_dice/README.md b/projects/Roll_A_dice/README.md index 72c2f8128..163bbad71 100644 --- a/projects/Roll_A_dice/README.md +++ b/projects/Roll_A_dice/README.md @@ -2,7 +2,32 @@ A console game where the player rolls a virtual dice and is given a task or riddle based on the number rolled. The player's name, roll, and answer are recorded in a MySQL database. -## How to run +## Example + +```text +Your name: Alice +Hey Alice! Welcome to Roll A Dice game🎲 +Rules: +1. You have to roll a dice. +2. Whatever number comes in, an associated task will be given to you. +3. You have to perform the task. +4. After you're done, press enter. +5. Don't forget to have fun🥳 + +Choose any one: + 1. Move ahead with the game + 2. Exit + 1 +🎲 - 5 +Yayyyy!! You got 5!. + What has two banks but no money🤔? +Your ans: River bank +Viola! You are right +Have a good day! +Your response has been recorded ✅! +``` + +## How to run on localhost ```sh pip install mysql-connector-python @@ -15,7 +40,3 @@ You must have a MySQL server running and update the connection credentials in `d - mysql-connector-python - random (standard library) - -## Pyodide-runnable - -No - it connects to a MySQL database server, which cannot run in the browser sandbox. diff --git a/projects/Rubik-tracking/README.md b/projects/Rubik-tracking/README.md index b6cdfb0c2..fefcb3e04 100644 --- a/projects/Rubik-tracking/README.md +++ b/projects/Rubik-tracking/README.md @@ -2,7 +2,14 @@ Tracks a green-colored object (e.g., a Rubik's cube) in a live webcam feed using OpenCV color masking and draws its motion trail on screen. -## How to run +## Example + +1. Run `python rubik-tracking.py`. The webcam starts and a window titled "Rubik's cube tracking" opens showing the live feed. +2. Hold a green-colored object (e.g., a green face of a Rubik's cube) in front of the camera. The script detects the green region using HSV color masking and draws a green contour around it. +3. Move the object across the frame. A trailing line is drawn on screen, thicker near the most recent position and thinner further back, tracing the motion path. +4. Press `q` to stop the stream and close the window. + +## How to run on localhost ```sh pip install opencv-python imutils numpy @@ -14,7 +21,3 @@ python rubik-tracking.py - opencv-python - imutils - numpy - -## Pyodide-runnable - -No - it captures live webcam video and shows OpenCV windows, neither of which works in the browser sandbox. diff --git a/projects/SMS_ChatBot/README.md b/projects/SMS_ChatBot/README.md index 0041294ed..442bb5ccd 100644 --- a/projects/SMS_ChatBot/README.md +++ b/projects/SMS_ChatBot/README.md @@ -3,6 +3,14 @@ [![N|Solid](https://imgs.search.brave.com/TwFKAEHis0XNgwUaDKCTjeG0Hs2V46pM9vMyV9-1zGo/rs:fit:500:0:0/g:ce/aHR0cHM6Ly9sb2dv/cy13b3JsZC5uZXQv/d3AtY29udGVudC91/cGxvYWRzLzIwMjEv/MTAvUHl0aG9uLVN5/bWJvbC03MDB4Mzk0/LnBuZw) +## Example + +1. Start the Flask server with `python main.py` (or `python3 main.py`). +2. In a second terminal run `ngrok http 5000` and copy the forwarding URL. +3. Paste the ngrok URL into your Twilio phone number's "A Message Comes In" webhook field. +4. Text your Twilio number from your phone — for example: `What is the capital of France?` +5. The server receives the SMS, forwards the text to OpenAI's API, and replies via Twilio with the generated answer (e.g. `The capital of France is Paris.`). + ## PREREQUISITIES * A Twilio account - sign up for a free one here - https://www.twilio.com/try-twilio * A Twilio phone number with SMS capabilities - learn how to buy a Twilio Phone Number here - https://support.twilio.com/hc/en-us/articles/223135247-How-to-Search-for-and-Buy-a-Twilio-Phone-Number-from-Console @@ -92,7 +100,4 @@ python3 main.py Now take out your phone and text your Twilio Phone Number a question or prompt so that OpenAI can answer it or generate text! -![picture alt](https://assets.cdn.prod.twilio.com/images/Screenshot_2023-01-12_at_1.44.48_PM.format-webp.webp) -## Pyodide-runnable - -No — it is a Flask web server that integrates the Twilio SMS API and the OpenAI API, all requiring network access and a server that Pyodide cannot provide. +![picture alt](https://assets.cdn.prod.twilio.com/images/Screenshot_2023-01-12_at_1.44.48_PM.format-webp.webp) diff --git a/projects/Sales Optimizer/Readme.md b/projects/Sales Optimizer/Readme.md index 6e7988e93..4f067849c 100644 --- a/projects/Sales Optimizer/Readme.md +++ b/projects/Sales Optimizer/Readme.md @@ -1,7 +1,11 @@ # Sales Optimizer + ## Descirption + The retail stores always face a problem of tension between demand and supply. Sometimes they stock more products than it has demand or sales and sometimes they stock less than demand, due to which they either suffer from loss or dissatisfied customers. This problem can be solved using this software. It uses various inventory data to predict the sales pattern of the retail store so that the retail store can stock their products accordingly. + ### Parameters Used: + - Item weight - Item fat content - Item fat content @@ -13,18 +17,21 @@ The retail stores always face a problem of tension between demand and supply. So - Outlet location. ## Libraries Used: + - Numpy - Pandas - Seaborn - Scikit-learn -## How to run +## Example + +1. Place `sales_data.csv` (containing columns such as `Item_Weight`, `Item_Fat_Content`, `Item_Visibility`, `Item_Type`, `Item_MRP`, `Outlet_Identifier`, `Outlet_Establishment_Year`, `Outlet_Size`, `Outlet_Location_Type`, `Outlet_Type`, and `Item_Outlet_Sales`) in the project directory. +2. Run `python main.py`. The script preprocesses the data (fills missing weights, encodes categorical columns), trains a `RandomForestRegressor`, and prints dataset info and evaluation metrics to the console. +3. Two matplotlib windows open: a seaborn pairplot of the features, and a scatter plot comparing actual vs predicted `Item_Outlet_Sales` values. + +## How to run on localhost ```sh pip install numpy pandas seaborn scikit-learn matplotlib python main.py ``` - -## Pyodide-runnable - -No - it depends on `seaborn`, `scikit-learn`, and `matplotlib` GUI plotting, which are not available in the Pyodide sandbox. \ No newline at end of file diff --git a/projects/Scientific-Calculator/ReadMe.md b/projects/Scientific-Calculator/ReadMe.md index 898dd9abd..819660cb5 100644 --- a/projects/Scientific-Calculator/ReadMe.md +++ b/projects/Scientific-Calculator/ReadMe.md @@ -1,9 +1,16 @@ -*This* **Scientific Calculator** *is a calculator designed to help you calculate science, engineering, and mathematics problems. It has way more buttons than your standard calculator that just lets you do your four basic arithmetic operations of addition, subtraction, multiplication, and division. This Scientific calculator is made with python which can be easily used by anyone.* +_This_ **Scientific Calculator** *is a calculator designed to help you calculate science, engineering, and mathematics problems. It has way more buttons than your standard calculator that just lets you do your four basic arithmetic operations of addition, subtraction, multiplication, and division. This Scientific calculator is made with python which can be easily used by anyone.*

    ![image](https://user-images.githubusercontent.com/89387048/196713806-d9a3febc-5896-4ef7-a31d-373ee533347f.png) -## How to run +## Example + +1. The window titled "Scientific CalC" opens showing a dark-themed calculator with a yellow-text entry field and rows of buttons including digits, `+`, `-`, `*`, `/`, `^`, `(`, `)`, `sin`, `cos`, `tan`, `lg`, `ln`, `Sqrt`, `x!`, `1/x`, `pi`, `e`, `deg`, `mod`, `C` (clear), `Bksp`, and `=`. +2. Click `3`, `6`, then `Sqrt` to compute the square root of 36. The entry field updates to show `6.0`. +3. Click `C` to clear, then type `5`, `x!` to compute 5 factorial. The entry field shows `120`. +4. Click `C`, enter `3.14159`, then click `sin` to get the sine of that value (approximately `2.653...e-06`). + +## How to run on localhost ```sh python Scientific_CalC.py @@ -12,7 +19,3 @@ python Scientific_CalC.py ## Dependencies Standard library only (uses `tkinter` and `math`). - -## Pyodide-runnable - -No - it builds a Tkinter GUI, which is not available in the browser sandbox. diff --git a/projects/ScreenRecorder/README.md b/projects/ScreenRecorder/README.md index 28d978cbe..72677ec97 100644 --- a/projects/ScreenRecorder/README.md +++ b/projects/ScreenRecorder/README.md @@ -10,6 +10,13 @@ pip install numpy pip install pyautogui +## Example + +1. Run `python main.py`. The script immediately starts capturing screenshots of your full screen at 30 fps. +2. After 10 seconds the recording stops automatically. +3. A file named `ScreenRecord.mp4` appears in the project folder. +4. The console prints `____Screen Recording finished________`. + # Now run the main.py ## Great.............You Did it, Check your folder and find "ScreenRecord.mp4" file @@ -20,7 +27,3 @@ pip install pyautogui - pywin32 - numpy - pyautogui - -## Pyodide-runnable - -No - it captures the real screen with `pyautogui` and uses Windows-specific `win32api`, neither of which works in the browser sandbox. diff --git a/projects/Seek_with_hand_track/README.md b/projects/Seek_with_hand_track/README.md index ec6c62056..5539748ce 100644 --- a/projects/Seek_with_hand_track/README.md +++ b/projects/Seek_with_hand_track/README.md @@ -2,7 +2,15 @@ Uses a webcam and MediaPipe hand tracking to detect hand gestures and send left/right arrow keypresses, allowing you to seek a video by tilting your hand. -## How to run +## Example + +1. Run `python main.py`. A window titled "MediaPipe Hands" opens showing the live webcam feed. +2. Hold your hand in front of the camera. The script detects hand landmarks and draws circles on the wrist and index-finger base. +3. Tilt your hand so the wrist is to the left of the index finger base (slope < -78°) — a left-arrow keypress is sent and `left` is printed to the console. +4. Tilt your hand in the opposite direction (slope between 0° and 78°) — a right-arrow keypress is sent and `right` is printed. +5. Press Escape to close the window. + +## How to run on localhost ```sh pip install opencv-python mediapipe pyautogui numpy @@ -15,7 +23,3 @@ python main.py - mediapipe - pyautogui - numpy - -## Pyodide-runnable - -No - it captures live webcam video, runs MediaPipe, and simulates keypresses with `pyautogui`, none of which work in the browser sandbox. diff --git a/projects/Selfie_with_Python/README.md b/projects/Selfie_with_Python/README.md index 755456f3d..90e169944 100644 --- a/projects/Selfie_with_Python/README.md +++ b/projects/Selfie_with_Python/README.md @@ -2,7 +2,19 @@ Opens the webcam in an OpenCV window; press Space to capture a selfie (saved as a JPG) and Escape to quit. -## How to run +## Example + +```text +Press Space-bar to click Selfie +Press Escape key to terminate the window +``` + +1. A window titled "Take selfie with python" opens showing the live webcam feed. +2. Press Space to capture a photo — `Selfie_0.jpg` is saved in the current directory and `Selfie taken!` is printed. +3. Press Space again to save `Selfie_1.jpg`, and so on. +4. Press Escape to close the window — `Escape hit, closing the window` is printed. + +## How to run on localhost ```sh pip install opencv-python @@ -13,7 +25,3 @@ python Selfie_with_Python.py - opencv-python - time (standard library) - -## Pyodide-runnable - -No - it captures live webcam video and shows OpenCV windows, neither of which works in the browser sandbox. diff --git a/projects/Send-Email/README.md b/projects/Send-Email/README.md index 315ba63c2..cc216c7ad 100644 --- a/projects/Send-Email/README.md +++ b/projects/Send-Email/README.md @@ -2,7 +2,18 @@ A script that connects to Gmail's SMTP server and sends a plain-text email. The sender's username and password are read from environment variables. -## How to run +## Example + +```text +$ export username=sender@gmail.com +$ export password=yourpassword +$ python main.py +Successfully the mail is sent +``` + +The script reads `username` and `password` from environment variables, connects to Gmail's SMTP server, and sends the hardcoded plain-text message to `reciver789@gmail.com`. + +## How to run on localhost ```sh export username=your@gmail.com @@ -13,7 +24,3 @@ python main.py ## Dependencies Standard library only (uses `smtplib` and `os`). - -## Pyodide-runnable - -No - it opens an SMTP network connection to Gmail's mail server, which is blocked in the browser sandbox. diff --git a/projects/Simple-Plagiarism-Checker-Project/README.md b/projects/Simple-Plagiarism-Checker-Project/README.md index e137687ff..5ab58de55 100644 --- a/projects/Simple-Plagiarism-Checker-Project/README.md +++ b/projects/Simple-Plagiarism-Checker-Project/README.md @@ -2,6 +2,15 @@ Web application of Plagiarism Checker using Python-Flask. TF-IDF and cosine similarity is a very common technique. It allows the system to quickly retrieve documents similar to a search query. Similarly, based on the same concept instead of retrieving documents similar to a query, it checks for how similar the query is to the existing database file. +## Example + +1. Run `python plag.py` and open `http://127.0.0.1:5000` in a browser. +2. Type or paste a passage into the query text box and submit the form. +3. The app compares the query against `database1.txt` using TF-IDF cosine similarity. +4. The page refreshes and displays a result such as: + + `Input query text matches 73.45% with database.` + ## Steps: 1. User enters a query 2. Query gets processed (Uppercase to lowercase, Removal of punctuationmarks, etc.) @@ -13,8 +22,3 @@ Web application of Plagiarism Checker using Python-Flask. TF-IDF and cosine simi 2. Easy to work with (Same syntax as of Python) 3. While Flask addresses itself as a "micro-framework", it is not lacking in features or power, especially with a clutch of extensions to support features such as authentication, databases and so on 4. Comprehensive documentation available - - -## Pyodide-runnable - -No — it is a Flask web server application, and Flask cannot serve requests under Pyodide in the browser. diff --git a/projects/SketchifyMe/README.md b/projects/SketchifyMe/README.md index d05c8fe61..b421f6bcf 100644 --- a/projects/SketchifyMe/README.md +++ b/projects/SketchifyMe/README.md @@ -1,17 +1,22 @@ # SketchifyMe -Seamlessly convert your images into pencil-sketch renditions, facilitating the effortless creation of your sketches. -## How to run - -``` -pip install opencv-python numpy -python image_to_sketch.py -``` - -## Dependencies - -opencv-python (cv2), numpy - -## Pyodide-runnable - -No — it relies on OpenCV's `cv2.imshow`/`waitKey` GUI windows and reads an image file from the local disk. +Seamlessly convert your images into pencil-sketch renditions, facilitating the effortless creation of your sketches. + +## Example + +1. Open `image_to_sketch.py` and set `image = "photo.jpg"` (replace with your image path). +2. Run `python image_to_sketch.py`. +3. A window titled "Pencil Sketch" opens displaying the grayscale pencil-sketch version of the image. +4. Press any key to close the window. +5. The sketch is saved as `sketch_ofphoto.jpg` in the same directory and `Saved the Sketch!` is printed. + +## How to run on localhost + +``` +pip install opencv-python numpy +python image_to_sketch.py +``` + +## Dependencies + +opencv-python (cv2), numpy diff --git a/projects/Skycast/README.md b/projects/Skycast/README.md index 29f6a399f..796962329 100644 --- a/projects/Skycast/README.md +++ b/projects/Skycast/README.md @@ -17,6 +17,14 @@ SkyCast is a Python application that allows users to retrieve weather informatio - Display data including temperature, wind speed, cloud coverage, visibility, weather description, wind direction, UV index, and dew point for each day. - Visualize the temperature forecast on a line chart. +## Example + +1. Run `streamlit run skycast.py`. The SkyCast web app opens in your browser. +2. In the sidebar, select "Today's Weather", type `London` in the city name field, and click Submit. +3. The main panel shows current conditions for London, GB — temperature, wind speed, cloud coverage, visibility, weather description, wind direction, UV index, and dew point. +4. Switch the sidebar option to "Forecast Weather", enter `Tokyo`, set days to `3`, and click Submit. +5. The app displays per-day forecast cards for three days and renders a line chart of temperature (min/max/average) over those dates. + ## Installation 1. Clone the repository: @@ -73,6 +81,3 @@ Contributions to SkyCast are welcome! Here are some ways you can contribute: - Fix bugs and issues reported by users. Thank you for your contributions to SkyCast! 🌤️ -## Pyodide-runnable - -No — it is a Streamlit web app that also makes live HTTP requests to the Weatherbit API; neither Streamlit's server nor network calls run under Pyodide. diff --git a/projects/Slice-Audio/README.md b/projects/Slice-Audio/README.md index e098b7420..59d79350f 100644 --- a/projects/Slice-Audio/README.md +++ b/projects/Slice-Audio/README.md @@ -13,6 +13,19 @@ What all do you have to enter? 5.) End Minute - Till what minute you want the final audio to be. 6.) End Second - Till what second of the End minute you want to final audio to be. +## Example + +```text +Enter your path to the audio file/home/user/music/song.mp3 +Enter the path you want to save the file at/home/user/music/clip.mp3 +Enter Start Minute 1 +Enter Start Second 30 +Enter End Minute 2 +Enter End Second 15 +``` + +The script extracts the segment from 1:30 to 2:15 of `song.mp3` and saves it as `clip.mp3` at the export path. + # Requirements Clone Repo @@ -20,8 +33,3 @@ You need to intall pydub library for this script to run. You can use pip - pip install pydub More information on pydub here - https://pypi.org/project/pydub/ Use the python script slicingAudio.py - - -## Pyodide-runnable - -No — pydub depends on ffmpeg and reads/writes mp3 files on the local disk, which is unavailable in Pyodide. diff --git a/projects/Snake Game/README.md b/projects/Snake Game/README.md index 015df7158..24f206c02 100644 --- a/projects/Snake Game/README.md +++ b/projects/Snake Game/README.md @@ -2,7 +2,14 @@ A classic Snake game built with Pygame. The player controls a snake that grows as it eats food, tracking a score and a high score, with game-over and play-again screens. -## How to run +## Example + +1. Run `python src/main.py`. A Pygame window opens showing the snake and a food item. +2. Use the arrow keys to steer the snake toward the food. Each food eaten increases the score by 1 and grows the snake. +3. If the snake hits a wall or its own body the game-over screen appears. If you set a new high score it is displayed and saved to `high_score.json`. +4. A "Play Again?" prompt appears — press Y or Enter to restart, N or Escape to quit. + +## How to run on localhost ``` pip install pygame @@ -12,7 +19,3 @@ python src/main.py ## Dependencies pygame - -## Pyodide-runnable - -No — it uses Pygame for windowed graphics and real-time keyboard input, which require a desktop display. diff --git a/projects/Social_media_content_creation/README.md b/projects/Social_media_content_creation/README.md index acaf749a8..339e3d6ed 100644 --- a/projects/Social_media_content_creation/README.md +++ b/projects/Social_media_content_creation/README.md @@ -14,7 +14,3 @@ To get started with these projects, simply clone this repository and navigate to We welcome contributions! If you have a project idea related to social media content creation, feel free to add it to this folder. Happy coding! - -## Pyodide-runnable - -No — this folder is a placeholder containing no Python code to run. diff --git a/projects/Socket/README.md b/projects/Socket/README.md index 275125d10..2f92f971c 100644 --- a/projects/Socket/README.md +++ b/projects/Socket/README.md @@ -1,9 +1,24 @@ - # Socket example ### Implementation of a socket in python -## How to run +## Example + +**Terminal 1 (server):** +```text +$ python ./server.py +Server runing in port 3000 +New conection ('127.0.0.1', 54321) +b'Hello from client' +``` + +**Terminal 2 (client):** +```text +$ python ./client.py +b' Hello from server!' +``` + +## How to run on localhost ```bash # fist run the server @@ -14,7 +29,3 @@ python ./server.py # then run the client python ./client.py ``` - -## Pyodide-runnable - -No — it opens real TCP sockets for client/server networking, which is not available in the browser sandbox. diff --git a/projects/SongsMashup/README.md b/projects/SongsMashup/README.md index 24d466649..5f1a30870 100644 --- a/projects/SongsMashup/README.md +++ b/projects/SongsMashup/README.md @@ -2,7 +2,14 @@ A Flask web app that downloads songs from YouTube for a given singer, slices each track into short snippets, merges them into a single mashup audio file, zips it, and emails the result to the user. -## How to run +## Example + +1. Run `python SongsMashup.py` and open `http://0.0.0.0:5000` in a browser. +2. Fill in the form: singer name (e.g. `Ed Sheeran`), number of videos (e.g. `5`), snippet duration in seconds (e.g. `20`), and your email address. +3. Submit the form. The page redirects to a success screen while the server downloads songs from YouTube in the background. +4. Each track is sliced into a random snippet, all snippets are merged into `output.mp3`, zipped as `output.zip`, and emailed to the address you provided. + +## How to run on localhost ``` pip install flask numpy pandas pytube pydub youtube-search @@ -12,7 +19,3 @@ python SongsMashup.py ## Dependencies flask, numpy, pandas, pytube, pydub, youtube-search, smtplib (standard library) - -## Pyodide-runnable - -No — it is a Flask server that downloads from YouTube, processes audio with pydub/ffmpeg, and sends email via SMTP, none of which work in Pyodide. diff --git a/projects/Space Shooter/README.md b/projects/Space Shooter/README.md index 691578918..78d92777a 100644 --- a/projects/Space Shooter/README.md +++ b/projects/Space Shooter/README.md @@ -2,7 +2,15 @@ A 2D space shooter game built with Pygame. Pilot a spaceship, fire lasers at incoming enemy ships, dodge enemy lasers and homing missiles, and rack up a score, with menu and game-over screens. -## How to run +## Example + +1. Run `python main.py`. A 640×480 Pygame window opens showing the "Main Menu" with Start and Exit options. +2. Press Enter to start. Your spaceship appears on the left; enemy ships approach from the right. +3. Press W/S to move up and down. Click the left mouse button to fire a laser. +4. Lasers that hit enemies increase your score (shown at the top-left). Every 10 points an additional enemy spawns. +5. If an enemy laser or homing missile hits your ship, the Game Over screen appears with a "Play Again?" prompt. + +## How to run on localhost ``` pip install pygame @@ -12,7 +20,3 @@ python main.py ## Dependencies pygame - -## Pyodide-runnable - -No — it uses Pygame for windowed graphics and loads image/font assets, which require a desktop display. diff --git a/projects/Speed-Type-test/README.md b/projects/Speed-Type-test/README.md index a733972f2..95628b451 100644 --- a/projects/Speed-Type-test/README.md +++ b/projects/Speed-Type-test/README.md @@ -2,7 +2,17 @@ A typing speed test built with Pygame. A random sentence is displayed; type it as fast and accurately as you can, and the app reports your time, accuracy percentage, and words per minute. -## How to run +## Example + +1. Run the script. A 950×500 Pygame window titled "Type Speed test" opens showing a randomly chosen sentence near the center. +2. Click inside the yellow input box to activate it, then start typing the sentence. The timer begins on the first click. +3. Press Enter when finished. The results line is displayed in red, for example: + + `Time:18 secs Accuracy:94% Wpm: 52` + +4. Click the "Reset" area to load a new sentence and try again. + +## How to run on localhost ``` pip install pygame @@ -12,7 +22,3 @@ python "Speed Typing Test Python/speed typing.py" ## Dependencies pygame - -## Pyodide-runnable - -No — it uses Pygame for windowed graphics and loads image assets, which require a desktop display. diff --git a/projects/Split_Tip/README.md b/projects/Split_Tip/README.md index c67044a6b..9e123436a 100644 --- a/projects/Split_Tip/README.md +++ b/projects/Split_Tip/README.md @@ -2,7 +2,18 @@ A simple console tip calculator. Enter a bill amount, a tip percentage, and the number of people, and it prints how much each person owes in tip and in total. -## How to run +## Example + +```text +Welcome to the Tip calculator +Enter your bill amount ($): 85.00 +what percentage of tip you want to give? 5, 10, 12 or 15? +15 +How many people to split the bill amount: 4 +Each person has to pay $3.0 in tip and $24.0 in total +``` + +## How to run on localhost ``` python Tip_calculator.py @@ -11,7 +22,3 @@ python Tip_calculator.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program that only uses `input()`/`print()`. diff --git a/projects/Spotify Player/README.md b/projects/Spotify Player/README.md index daa0adcac..47681864a 100644 --- a/projects/Spotify Player/README.md +++ b/projects/Spotify Player/README.md @@ -2,7 +2,27 @@ A command-line Spotify tool that can play a searched song, recommend songs based on three seed tracks, and generate a new playlist similar to an existing one. It also texts the generated playlist link via Twilio. -## How to run +## Example + +```text +Enter a choice: 1 +Enter Song Name: Blinding Lights +Would you like to open this song in your browser [yes/no]: yes +✅ Playing Blinding Lights - The Weeknd + +Would you like to try again [yes/no]: yes + +Enter a choice: 2 +Enter song 1: Shape of You +Enter song 2: Perfect +Enter song 3: Photograph + +Recommended song: Castle on the Hill - Ed Sheeran +Would you like to play this song [yes/no]: yes +✅ Playing Castle on the Hill - Ed Sheeran +``` + +## How to run on localhost ``` pip install spotipy colorama twilio @@ -14,7 +34,3 @@ Configuration values must be placed in `utils/user_secrets.py` (see `utils/user_ ## Dependencies spotipy, colorama, twilio - -## Pyodide-runnable - -No — it calls the Spotify Web API and Twilio API over the network, opens a browser, and clears the terminal with `os.system`. diff --git a/projects/Stock-Market-Dashboard/README.md b/projects/Stock-Market-Dashboard/README.md index 7b10b96df..d04da2523 100644 --- a/projects/Stock-Market-Dashboard/README.md +++ b/projects/Stock-Market-Dashboard/README.md @@ -2,7 +2,15 @@ A Streamlit dashboard that visualizes S&P 500 stock data with Bollinger Bands, MACD, and RSI indicators computed from closing prices. Companies are grouped by sector and selected by ticker. -## How to run +## Example + +1. Run `streamlit run dashboard.py`. The dashboard opens in your browser showing a title and description. +2. In the sidebar, select a sector from the "Sector" multiselect — for example "Information Technology". +3. Choose a start and end date, then pick a ticker such as `AAPL` from the dropdown. +4. The dashboard displays Apple's company logo and name, followed by three charts: Stock Bollinger Bands, MACD, and RSI, each computed from the closing price over the selected date range. +5. A table of the 10 most recent trading days of data appears at the bottom. + +## How to run on localhost ``` pip install pandas yfinance ta streamlit @@ -12,7 +20,3 @@ streamlit run dashboard.py ## Dependencies pandas, yfinance, ta, streamlit - -## Pyodide-runnable - -No — it is a Streamlit web app that downloads stock data and the S&P 500 company list over the network. diff --git a/projects/Subnetting Flsm/README.md b/projects/Subnetting Flsm/README.md index fb5454f84..818e32c9b 100644 --- a/projects/Subnetting Flsm/README.md +++ b/projects/Subnetting Flsm/README.md @@ -5,7 +5,26 @@ Subnetting_FLSM is developed for: 2.Distribution hosts for each subnet by FLSM method 3.Transferring unique network address, broadcast address, usable hosts -## How to run +## Example + +```text +Network address: 192.168.1.0 +Prefix of network: 24 +Num of subnets: 4 +Max value of hosts for one sub: 30 + + Subnetting - FLSM +Network address/prefix Number of subnets Max num of hosts for one sub + 192.168.1.0/24 4 30 + +№Net Value of hosts Network address Broadcast address Usable hosts +1 30 192.168.1.0 192.168.1.31 192.168.1.1 - 192.168.1.30 +2 30 192.168.1.32 192.168.1.63 192.168.1.33 - 192.168.1.62 +3 30 192.168.1.64 192.168.1.95 192.168.1.65 - 192.168.1.94 +4 30 192.168.1.96 192.168.1.127 192.168.1.97 - 192.168.1.126 +``` + +## How to run on localhost ``` python subnetting_flsm.py @@ -14,7 +33,3 @@ python subnetting_flsm.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program that only uses `input()`/`print()`. diff --git a/projects/Subtitle_synchronizer.py/README.md b/projects/Subtitle_synchronizer.py/README.md index 695b93019..4944acf82 100644 --- a/projects/Subtitle_synchronizer.py/README.md +++ b/projects/Subtitle_synchronizer.py/README.md @@ -2,7 +2,29 @@ A console tool that shifts all timestamps in an SRT subtitle file by a given number of milliseconds, reading from `input.srt` and writing the synchronized result to `output.srt`. -## How to run +## Example + +```text +Please enter the shift in milliseconds: 2000 +``` + +Given an `input.srt` containing timestamps like: + +```text +1 +00:00:05,000 --> 00:00:08,500 +Hello, world! +``` + +The resulting `output.srt` shifts every timestamp back by 2000 ms: + +```text +1 +00:00:03,000 --> 00:00:06,500 +Hello, world! +``` + +## How to run on localhost ``` python main.py @@ -13,7 +35,3 @@ Place an `input.srt` file in the same folder before running. ## Dependencies Standard library only. - -## Pyodide-runnable - -No — it reads `input.srt` and writes `output.srt` from the real local filesystem, which is not accessible under Pyodide. diff --git a/projects/Sudoku-Solver/README.md b/projects/Sudoku-Solver/README.md index 7bf654799..7abec1e19 100644 --- a/projects/Sudoku-Solver/README.md +++ b/projects/Sudoku-Solver/README.md @@ -2,7 +2,15 @@ A Pygame-based Sudoku game. It generates a random grid you can play interactively, supports hints, tracks wrong answers and elapsed time, and can visually solve the puzzle with a backtracking animation. -## How to run +## Example + +1. The window opens showing a 9×9 Sudoku grid with some cells pre-filled and the rest blank. +2. Click an empty cell to select it, then type a digit (1–9) to place your answer. +3. A wrong answer increments the "Strikes" counter displayed at the top. +4. Press the spacebar or click the Solve button to watch the backtracking algorithm fill in the remaining cells with a visual animation. +5. Elapsed time is shown in the top-right corner throughout the game. + +## How to run on localhost ``` pip install pygame @@ -12,7 +20,3 @@ python SudokuGUI.py ## Dependencies pygame - -## Pyodide-runnable - -No — it uses Pygame for windowed graphics, mouse/keyboard input, and loads image assets. diff --git a/projects/Sudoku_solver/README.md b/projects/Sudoku_solver/README.md index 6369052e5..593fc2646 100644 --- a/projects/Sudoku_solver/README.md +++ b/projects/Sudoku_solver/README.md @@ -2,7 +2,36 @@ A console Sudoku generator and solver. It randomly generates a valid Sudoku board, removes cells to create a puzzle, then solves it with a backtracking algorithm, printing the full, unsolved, and solved boards. -## How to run +## Example + +```text +=======full board======== +5 3 4 | 6 7 8 | 9 1 2 +6 7 2 | 1 9 5 | 3 4 8 +1 9 8 | 3 4 2 | 5 6 7 +- - - - - - - - - - - - - +8 5 9 | 7 6 1 | 4 2 3 +4 2 6 | 8 5 3 | 7 9 1 +7 1 3 | 9 2 4 | 8 5 6 +- - - - - - - - - - - - - +9 6 1 | 5 3 7 | 2 8 4 +2 8 7 | 4 1 9 | 6 3 5 +3 4 5 | 2 8 6 | 1 7 9 + +======solvable board===== +5 3 0 | 6 7 0 | 9 1 0 +0 7 2 | 1 9 5 | 3 4 8 +1 0 8 | 3 0 2 | 5 6 7 +- - - - - - - - - - - - - +... + +======solved board======= +5 3 4 | 6 7 8 | 9 1 2 +6 7 2 | 1 9 5 | 3 4 8 +... +``` + +## How to run on localhost ``` python main.py @@ -11,7 +40,3 @@ python main.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program that only uses `random` and `print()`. diff --git a/projects/TennisTournamentSim/README.md b/projects/TennisTournamentSim/README.md index bf742775d..b697abda9 100644 --- a/projects/TennisTournamentSim/README.md +++ b/projects/TennisTournamentSim/README.md @@ -2,7 +2,41 @@ A console simulation of a four-player single-elimination tennis tournament. Players get random names, schools, and skill levels; match outcomes are decided by skill plus random variation, and the champion and runner-up are announced. -## How to run +## Example + +```text +Program Starts from here +Player's info - Bracket 1, Round 1 +Player 1 +Name: Max Kenshin, School: Newport + +Player 2 +Name: Corey Roosevelt, School: Eastlake + +Bracket Winner Name: Max Kenshin, Winning School Name: Newport, Winner Skill: 4.5, Wins: 1 +Bracket Loser Name: Corey Roosevelt, Losing School Name: Eastlake, Loser Skill: 3, Losses: 1 + +------------------------------------- + +Player's info - Bracket 2, Round 1 +Player 3 +Name: Evander Wong, School: Sammamish + +Player 4 +Name: Timothy Holyfield, School: Inglemore + +Bracket Winner Name: Timothy Holyfield, Winning School Name: Inglemore, Winner Skill: 5.5, Wins: 1 +... + +##################################### + +Player's info - Finals +... +Champion Name: Max Kenshin, Champion School: Newport, Champion Skill: 5.0 +Runner Up Name: Timothy Holyfield, Running Up School: Inglemore, Runner Up Skill: 5.5 +``` + +## How to run on localhost ``` python main.py @@ -11,7 +45,3 @@ python main.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using only `random`, `time`, and `print()`. diff --git a/projects/Tesla/README.md b/projects/Tesla/README.md index 2bfa31f43..55c6ddff7 100644 --- a/projects/Tesla/README.md +++ b/projects/Tesla/README.md @@ -2,7 +2,15 @@ A Pygame simulation of a self-driving car. A car follows a track image by sampling pixel colors ahead of it to decide when to drive straight or turn. -## How to run +## Example + +1. The Pygame window opens displaying the track image as a background. +2. The car sprite appears on the track and begins moving forward automatically. +3. As the car approaches a curve, it samples pixel colors ahead of it and steers left or right to stay on the light-colored road surface. +4. The car continues navigating the track autonomously with no user input required. +5. Close the window to exit the simulation. + +## How to run on localhost ``` pip install pygame @@ -12,7 +20,3 @@ python tesla.py ## Dependencies pygame - -## Pyodide-runnable - -No — it uses Pygame for windowed graphics and loads track/car image assets, which require a desktop display. diff --git a/projects/Tetris Game/README.md b/projects/Tetris Game/README.md index 2051e6028..7b006cec5 100644 --- a/projects/Tetris Game/README.md +++ b/projects/Tetris Game/README.md @@ -2,7 +2,16 @@ A classic Tetris game built with Pygame. Falling tetrominoes can be moved, rotated, and hard-dropped; full lines are cleared for points, and the game ends when pieces stack to the top. -## How to run +## Example + +1. The Pygame window opens showing an empty Tetris grid with the current score displayed. +2. A random tetromino (e.g. an L-piece) appears at the top of the grid and falls downward. +3. Press the left/right arrow keys to move the piece horizontally, or the up arrow to rotate it. +4. Press the down arrow to soft-drop the piece faster. +5. When a full horizontal line is formed, it is cleared and the score increases. +6. The game ends when pieces stack up and reach the top of the grid. + +## How to run on localhost ``` pip install pygame @@ -12,7 +21,3 @@ python tetrisGame.py ## Dependencies pygame - -## Pyodide-runnable - -No — it uses Pygame for windowed graphics and real-time keyboard input. diff --git a/projects/Text Editor/README.md b/projects/Text Editor/README.md index e901beb3c..c71c7d4c1 100644 --- a/projects/Text Editor/README.md +++ b/projects/Text Editor/README.md @@ -2,7 +2,16 @@ Notepad-style text editor applications built with Tkinter. `Small-version/txteditor.py` is a compact editor with open/save/close and light/dark themes; `Text-Editor-master/Text_Editor.py` is a fuller-featured editor. -## How to run +## Example + +1. The Tkinter window opens showing a blank text area with a menu bar at the top. +2. Type or paste text directly into the editing area. +3. Use **File → Open** to load an existing `.txt` file into the editor. +4. Edit the content, then use **File → Save** to write the changes back to disk. +5. In the small version, use the theme toggle to switch between light and dark mode. +6. Use **File → Close** to clear the editor, or close the window to exit. + +## How to run on localhost ``` pip install pygments @@ -18,7 +27,3 @@ python Text-Editor-master/Text_Editor.py ## Dependencies tkinter (standard library); the small version also imports pygments. - -## Pyodide-runnable - -No — both editors use the Tkinter GUI toolkit, which is not available in the browser sandbox. diff --git a/projects/Text Summarizer/READme.md b/projects/Text Summarizer/READme.md index 19a0fab90..23e2431ed 100644 --- a/projects/Text Summarizer/READme.md +++ b/projects/Text Summarizer/READme.md @@ -3,6 +3,18 @@ Long texts can be condensed into concise, understandable summaries using a Pytho
    +## Example + +```text +Enter the text: Artificial intelligence is transforming industries worldwide. +Companies are investing heavily in machine learning to automate tasks. +Researchers continue to develop new models that improve accuracy and efficiency. +The impact of AI on jobs and society remains a topic of debate. + +Artificial intelligence is transforming industries worldwide. +Researchers continue to develop new models that improve accuracy and efficiency. +``` + **To run the text summarizer project follow these instructions. This project uses Python and the spaCy library for text processing. Here's a step-by-step guide for someone who is not familiar with the process:** Step 1: Install Required Libraries
    @@ -32,7 +44,3 @@ That's it! You've successfully run the text summarizer project. It will tokenize Make sure to replace **"text_summarizer.py"** with the name you've given to your Python script if it's different. ### HAPPY CODING ALL ☺️ - -## Pyodide-runnable - -No — it depends on spaCy and the large `en_core_web_md` language model, which are not available in Pyodide. diff --git a/projects/Text to Speech/readme.md b/projects/Text to Speech/readme.md index 2f503699a..8667ac104 100644 --- a/projects/Text to Speech/readme.md +++ b/projects/Text to Speech/readme.md @@ -3,6 +3,16 @@ - This python script basically takes text from user as input and gives output in form of a voice. - Uses Google Translate Text to Speech API. +## Example + +```text +python main.py +``` + +The script converts the text `"Hacktoberfest"` to speech using the gTTS library, +saves it as `result.mp3` in the current directory, and opens the file in the +system's default audio player so the spoken word is heard immediately. + ### Note: - Output is available on an audio player. @@ -11,7 +21,3 @@ - gTTS - os - -## Pyodide-runnable - -No — gTTS sends text to Google's Translate TTS API over the network and writes/plays an mp3 file locally. diff --git a/projects/Text-to-Image Generation Project/README.md b/projects/Text-to-Image Generation Project/README.md index 48510735a..3d01401f5 100644 --- a/projects/Text-to-Image Generation Project/README.md +++ b/projects/Text-to-Image Generation Project/README.md @@ -2,7 +2,15 @@ A Jupyter notebook (`Text_to_image.ipynb`) that demonstrates generating images from text prompts using a deep-learning text-to-image model. -## How to run +## Example + +1. Open `Text_to_image.ipynb` in Jupyter and run the setup cells to load the model. +2. Locate the cell that defines the text prompt and set it to a description such as `"a sunset over the ocean"`. +3. Run the generation cell — the model processes the prompt and produces an image. +4. The generated image is displayed inline in the notebook output below the cell. +5. Change the prompt to a different description and re-run the cell to generate a new image. + +## How to run on localhost Open the notebook in Jupyter: @@ -15,7 +23,3 @@ Install the deep-learning dependencies referenced inside the notebook (e.g. `dif ## Dependencies Deep-learning libraries such as `diffusers`, `torch`, and `transformers` (see the notebook cells). - -## Pyodide-runnable - -No — it is a Jupyter notebook relying on heavy GPU-oriented deep-learning libraries that cannot run in the browser. diff --git a/projects/TextDetection/README.md b/projects/TextDetection/README.md index d7c28fa04..befe11095 100644 --- a/projects/TextDetection/README.md +++ b/projects/TextDetection/README.md @@ -2,7 +2,20 @@ A script that detects and prints text found in an image using the Google Cloud Vision API. -## How to run +## Example + +```text +python TextDetection.py sign.jpg + +OPEN +Mon-Fri 9am-5pm +Sat 10am-3pm +Closed Sunday +``` + +Each detected text annotation from the image is printed on its own line. + +## How to run on localhost ``` pip install google-cloud-vision @@ -14,7 +27,3 @@ Requires Google Cloud credentials configured in the environment. ## Dependencies google-cloud-vision - -## Pyodide-runnable - -No — it calls the Google Cloud Vision API over the network and requires cloud authentication. diff --git a/projects/Text_to_SpreadSheet/README.md b/projects/Text_to_SpreadSheet/README.md index f935696a5..cf7144266 100644 --- a/projects/Text_to_SpreadSheet/README.md +++ b/projects/Text_to_SpreadSheet/README.md @@ -2,7 +2,29 @@ A script that reads every `.txt` file in a directory and writes each file's lines into a separate column of an Excel worksheet, saving the result as an `.xlsx` file. -## How to run +## Example + +Suppose the current directory contains `notes.txt` and `tasks.txt`: + +``` +notes.txt tasks.txt +--------- --------- +Buy milk Fix bug #42 +Call Alice Write tests +``` + +After running the script, `text-to-cols.xlsx` is created with: + +``` +Column A Column B +-------- -------- +Buy milk Fix bug #42 +Call Alice Write tests +``` + +Each `.txt` file becomes one column in the Excel worksheet. + +## How to run on localhost ``` pip install openpyxl @@ -12,7 +34,3 @@ python textToSheet.py ## Dependencies openpyxl - -## Pyodide-runnable - -No — it walks the real local directory with `os.listdir()` and writes an `.xlsx` file to disk. diff --git a/projects/Tic-Tac-Toe/README.md b/projects/Tic-Tac-Toe/README.md index eeed1c75c..cb7603a57 100644 --- a/projects/Tic-Tac-Toe/README.md +++ b/projects/Tic-Tac-Toe/README.md @@ -6,7 +6,36 @@ Two implementations of the classic 3×3 game: - **`TicTacToe-GUI/`** — a Tkinter version (`tic tac.py`, plus a one-player mode in `oneplayermode.py`) using `X.png` / `O.jpg` images. -## How to run +## Example + +**Terminal version:** + +```text +Welcome to Tic Tac Toe! +Do you want to be X or O? +X +The player will go first. + | | +----------- + | | +----------- + | | +What is your next move? (1-9) +5 + | | +----------- + | X | +----------- + | | +... +Hooray! You have won the game! +Do you want to play again? (yes or no) +no +``` + +**GUI version:** A Tkinter window opens with a 3×3 grid of buttons; click a cell to place your mark (X or O image tile), and the computer responds immediately. + +## How to run on localhost ```bash python Tic-Tac-Toe-Terminal/main.py # terminal version @@ -17,8 +46,3 @@ python TicTacToe-GUI/tic\ tac.py # GUI version - Terminal version: standard library only. - GUI version: `tkinter` (ships with the standard Python installer). - -## Pyodide-runnable - -Partly. The terminal version (`Tic-Tac-Toe-Terminal/main.py`) is pure-stdlib and -runs in the in-browser Pyodide playground. The Tkinter GUI version does not. diff --git a/projects/Tic-Tac-Toe/Tic-Tac-Toe-Terminal/README.md b/projects/Tic-Tac-Toe/Tic-Tac-Toe-Terminal/README.md index b0f5f5a0e..b61a52534 100644 --- a/projects/Tic-Tac-Toe/Tic-Tac-Toe-Terminal/README.md +++ b/projects/Tic-Tac-Toe/Tic-Tac-Toe-Terminal/README.md @@ -2,7 +2,32 @@ A console Tic-Tac-Toe game where a human plays against a simple AI computer opponent. The board uses numbers 1-9, the computer follows a basic win/block/corner/center strategy, and you can keep playing rounds until you choose to stop. -## How to run +## Example + +```text +Welcome to Tic Tac Toe! +Do you want to be X or O? +X +The computer will go first. + O | | +----------- + | | +----------- + | | +What is your next move? (1-9) +5 + O | | +----------- + | X | +----------- + | | +... +Hooray! You have won the game! +Do you want to play again? (yes or no) +no +``` + +## How to run on localhost ```bash python main.py @@ -11,7 +36,3 @@ python main.py ## Dependencies Standard library only (`random`). - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using `input()`/`print()`. diff --git a/projects/Tic-Tac-Toe/TicTacToe-GUI/README.md b/projects/Tic-Tac-Toe/TicTacToe-GUI/README.md index 6da2802f6..10f00984c 100644 --- a/projects/Tic-Tac-Toe/TicTacToe-GUI/README.md +++ b/projects/Tic-Tac-Toe/TicTacToe-GUI/README.md @@ -2,7 +2,15 @@ A graphical Tic-Tac-Toe game built with `pygame`. `tic tac.py` provides a mouse-driven board with X/O image tiles and a computer opponent; `oneplayermode.py` is a terminal-driven variant of the same game logic. -## How to run +## Example + +1. The Pygame window opens showing an empty 3×3 Tic-Tac-Toe grid. +2. Click any empty cell to place your mark (displayed as the X or O image tile). +3. The computer opponent responds with its move automatically. +4. The game continues until one side fills three cells in a row, column, or diagonal. +5. Close the window to exit. + +## How to run on localhost ```bash pip install pygame @@ -12,7 +20,3 @@ python "tic tac.py" ## Dependencies - pygame - -## Pyodide-runnable - -No — it depends on `pygame`, which opens a desktop window and cannot run in a browser. diff --git a/projects/TicTacToe-TylerPear/README.md b/projects/TicTacToe-TylerPear/README.md index f2ac60ae4..451e571cf 100644 --- a/projects/TicTacToe-TylerPear/README.md +++ b/projects/TicTacToe-TylerPear/README.md @@ -2,7 +2,32 @@ A two-player console Tic-Tac-Toe game. Players enter moves using position codes (TL, TM, TR, ML, MM, MR, BL, BM, BR) and the board is redrawn after every turn until someone wins or the game ends in a cat's game. -## How to run +## Example + +```text +KEY: TL = Top Left, TM = Top Middle, TR = Top Right + ML = Middle Left, MM = Middle Middle, MR = Middle Right + BL = Bottom Left, BM = Bottom Middle, BR = Bottom Right +Player O, Type Your Move: MM + O | | +___.___.__ + | | +___.___.__ + | | + +KEY: TL = Top Left, ... +Player X, Type Your Move: TL + X| O | +___.___.__ + | | +___.___.__ + | | + +... +Congrats! X Wins! +``` + +## How to run on localhost ```bash python main.py @@ -11,7 +36,3 @@ python main.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program using `input()`/`print()`. diff --git a/projects/Tile Matching/README.md b/projects/Tile Matching/README.md index 65f7f7720..1907aa2cf 100644 --- a/projects/Tile Matching/README.md +++ b/projects/Tile Matching/README.md @@ -2,7 +2,16 @@ A memory tile-matching game built with Tkinter. Click pairs of tiles to reveal hidden colors and match them all before the 60-second timer runs out, with a live score and attempts counter. -## How to run +## Example + +1. The Tkinter window opens with a 4×4 grid of gray tiles, a "Score: 0" label, an "Attempts: 0" label, and a "Time: 60" countdown. +2. Click any gray tile to reveal its hidden color (e.g. lightcoral). +3. Click a second tile — if its color matches the first, both stay revealed and the score increments. +4. If the colors differ, both tiles flip back to gray after a brief pause and the attempt counter increases. +5. Match all 8 pairs before the timer reaches zero to win; a congratulations dialog appears. +6. Click "Reset Game" to start a new game or "Exit" to close the window. + +## How to run on localhost ```bash python tile_matching.py @@ -11,7 +20,3 @@ python tile_matching.py ## Dependencies Standard library only (`tkinter`, `random`). - -## Pyodide-runnable - -No — it uses `tkinter`, which is a desktop GUI toolkit unavailable in the browser. diff --git a/projects/Timer/README.md b/projects/Timer/README.md index 73d29286f..2b32dfba3 100644 --- a/projects/Timer/README.md +++ b/projects/Timer/README.md @@ -2,7 +2,19 @@ A simple countdown timer. It asks for a duration in seconds and counts up on screen, one second at a time, until the time is up. -## How to run +## Example + +```text +Enter the duration in seconds: 5 +0 +1 +2 +3 +4 +Your time is up! +``` + +## How to run on localhost ```bash python main.py @@ -11,7 +23,3 @@ python main.py ## Dependencies Standard library only (`time`). - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program; note that `time.sleep` blocks the browser tab while counting. diff --git a/projects/Tkinter/README.md b/projects/Tkinter/README.md index 8a6800f6c..e5a8cc63d 100644 --- a/projects/Tkinter/README.md +++ b/projects/Tkinter/README.md @@ -2,7 +2,22 @@ A collection of Tkinter GUI examples. `1.py` is a multi-frame student/college login and account-creation interface, and `2.py` is a college-system window with a button menu and a database entry popup. -## How to run +## Example + +**`1.py`** — Student/college login demo: + +1. The window opens with a "You are Student or College Authority?" prompt and two buttons. +2. Click "Student" or "College Authority" to proceed to the welcome screen. +3. Click "Create New Account" to open a form with fields for name, email, department, roll number, year, and gender. +4. Click "Login" to open a login form with name and password fields. + +**`2.py`** — College system window: + +1. A window titled "COLLEGE SYSTEM" opens with a menu of buttons (BOOK buy, BOOK sell, canteen, helpdesk, healthcare, notice, exit). +2. Click any button to print a confirmation message to the terminal (e.g. `success buy`). +3. Click "Database" to open a popup for entering a student name and roll number. + +## How to run on localhost ```bash python 1.py @@ -12,7 +27,3 @@ python 2.py ## Dependencies Standard library only (`tkinter`). - -## Pyodide-runnable - -No — these scripts use `tkinter`, a desktop GUI toolkit unavailable in the browser. diff --git a/projects/ToDoList/README.md b/projects/ToDoList/README.md index 24c6a0027..6d729f90e 100644 --- a/projects/ToDoList/README.md +++ b/projects/ToDoList/README.md @@ -12,6 +12,14 @@ This is a simple command-line ToDo List application written in Python. - **Quit:** You can quit the application at any time. +## Example + +1. Start the bot in Telegram by sending `/start`. +2. A reply keyboard appears with buttons such as "Добавить заметку" (Add note), "Удалить заметку" (Delete note), "Изменить заметку" (Edit note), "Добавить событие" (Add event), and "Открыть текущие заметки/события" (View notes/events). +3. Tap "Добавить заметку" — the bot replies "Вы создали заметку" (You created a note). +4. Tap "Открыть текущие заметки/события" — the bot replies with the current notes/events list. +5. Send `/help` to see a formatted list of all available commands. + ## Usage To run the program, you will need Python installed on your computer. Once you have Python, you can run the program from the command line by navigating to the directory containing the Python file and running the following command: @@ -20,9 +28,3 @@ To run the program, you will need Python installed on your computer. Once you ha python to_do_list.py python3 to_do_list.py ``` - -## Pyodide-runnable - -No — `to_do_list.py` is a Telegram bot built on the `telebot` library; it -needs network access and a bot token, so it cannot run in the browser -playground. Run it on a desktop Python environment. diff --git a/projects/Turtle Pattern/README.md b/projects/Turtle Pattern/README.md index 6417e55c9..30aebd245 100644 --- a/projects/Turtle Pattern/README.md +++ b/projects/Turtle Pattern/README.md @@ -2,7 +2,15 @@ Draws a random walking pattern using Python's `turtle` graphics. A turtle moves in randomly chosen directions with random pen colors for 200 steps, producing a colorful abstract pattern. -## How to run +## Example + +1. A Turtle graphics window opens with a white canvas. +2. The turtle begins drawing from the center, moving 30 pixels at a time in a randomly chosen direction (forward, backward, left 90°, or right 90°). +3. Each step uses a new random RGB pen color, producing a multi-colored path. +4. After 200 steps the drawing is complete and the window stays open. +5. Click anywhere on the window to close it. + +## How to run on localhost ```bash python randomPattern.py @@ -11,7 +19,3 @@ python randomPattern.py ## Dependencies Standard library only (`turtle`, `random`, `time`). - -## Pyodide-runnable - -No — it uses the `turtle` module, which requires a desktop graphics window. diff --git a/projects/Turtle_Graphics/README.md b/projects/Turtle_Graphics/README.md index 14e9a50c4..e80c7d373 100644 --- a/projects/Turtle_Graphics/README.md +++ b/projects/Turtle_Graphics/README.md @@ -2,7 +2,15 @@ Draws layered fractal tree patterns using Python's `turtle` graphics. Recursive `draw` calls render branching trees in multiple colors and sizes across the screen. -## How to run +## Example + +1. Run the script: a black Tkinter window titled "Fractal Tree Pattern" opens. +2. A recursive branching tree is drawn in yellow, then the turtle turns and draws another in magenta, then red, then white — each at a small scale. +3. The turtle speeds up and draws three more larger trees (light green, red, yellow, white) branching at 4/5 length. +4. Finally, three even larger trees (cyan, yellow, magenta, white) are drawn at 6/7 length, filling the screen with overlapping fractal patterns. +5. Click the window to close it. + +## How to run on localhost ```bash python Turtle_Graphics.py @@ -11,7 +19,3 @@ python Turtle_Graphics.py ## Dependencies Standard library only (`turtle`). - -## Pyodide-runnable - -No — it uses the `turtle` module, which requires a desktop graphics window. diff --git a/projects/Twitter-Bot/README.md b/projects/Twitter-Bot/README.md index dd5c8d89c..3fb84501f 100644 --- a/projects/Twitter-Bot/README.md +++ b/projects/Twitter-Bot/README.md @@ -1,3 +1,15 @@ +## Example + +When the script is started (with valid API credentials), it prints `Testing The bot` and begins streaming tweets in English that contain `#Python`, `#python`, `#programming`, or `#coding`. For each original tweet (not a retweet or reply) it prints the tweet text and then `Retweeted`. If the rate limit is hit, it prints `Error 420: Enhance Your Calm - Rate Limited` and stops the stream. + +```text +Testing The bot +Just finished a fun #Python project — built a web scraper from scratch! +Retweeted +#programming tip: name your variables clearly, your future self will thank you. +Retweeted +``` + ## TWITTER-BOT This basic python project involves creating a Twitter bot using Tweepy to retweet tweets that contain certain hashtags related to Python and programming. @@ -18,9 +30,3 @@ This file includes the detailed and concise explanation of the project code as f 5. Make sure to replace "enter your API key here", "enter your API secret key here", "enter your access token here", and "enter your access token secret here" with your actual Twitter API credentials. ## Note: Remember that running a Twitter bot should comply with Twitter's terms of service and automation rules. Be cautious not to exceed rate limits or engage in spammy behavior. - - - -## Pyodide-runnable - -No — it is a Telegram bot that connects to the Telegram API over the network. diff --git a/projects/Type Racer Game/README.md b/projects/Type Racer Game/README.md index 9b926bdc3..54c5026c5 100644 --- a/projects/Type Racer Game/README.md +++ b/projects/Type Racer Game/README.md @@ -2,7 +2,15 @@ A typing-speed game built with Tkinter. A random sentence appears and you type it against a countdown timer, with live progress, color feedback for correct/incorrect text, and a words-per-minute score. -## How to run +## Example + +1. The window opens titled "TypeRacer" (800×400). A random sentence appears, e.g. `Python is an interpreted high-level programming language`. +2. A 5-second countdown ("Get ready!", 5, 4, 3, 2, 1, "Go!") plays, then the 30-second game timer starts. +3. You type in the entry box. Correct characters turn the text green; any mismatch turns it red. The progress bar updates as you type. +4. If you finish the sentence before time runs out, the result label briefly shows `Correct! Time taken: 18.42 seconds, WPM: 52` and a new sentence is loaded automatically. +5. If the timer reaches zero, it shows `Time's up!` and the score is submitted. + +## How to run on localhost ```bash python type_racer.py @@ -11,7 +19,3 @@ python type_racer.py ## Dependencies Standard library only (`tkinter`, `random`, `time`). - -## Pyodide-runnable - -No — it uses `tkinter`, a desktop GUI toolkit unavailable in the browser. diff --git a/projects/Video Reversal/README.md b/projects/Video Reversal/README.md index 2006462bd..b2e89e7a0 100644 --- a/projects/Video Reversal/README.md +++ b/projects/Video Reversal/README.md @@ -1,11 +1,20 @@ # Video reversing This python project reverses an input video +## Example + +Place your video file in the project folder and update the path in `video_reversing.py` (default: `sampleVideo.mp4`). Running the script prints frame count and FPS, then displays each frame in reverse order in a window titled `reversed`: + +```text +No. of frames: 300.0 +FPS: 30.0 +200 +100 +``` + +The reversed video plays in a pop-up window. Press any key to step through or let it play at 10 ms per frame. + ## Techstack used python **Note** Store the input video in this folder and update its path in the code. - -## Pyodide-runnable - -No — it uses OpenCV (`cv2`) to read a video file and display frames in a desktop window. diff --git a/projects/Video-subtitle-generator/readme.md b/projects/Video-subtitle-generator/readme.md index f210f0293..da606ebf7 100644 --- a/projects/Video-subtitle-generator/readme.md +++ b/projects/Video-subtitle-generator/readme.md @@ -1,3 +1,25 @@ +## Example + +```text +Please enter the path of the audio file: lecture.mp3 +Please enter the output file type (SRT is selected by default): +1.SRT +2.JSON +3.TXT +1 +Please enter the name of the whisper model you want to use (base is selected by default): +1.Tiny +2.Base +3.Small +4.Medium +5.Large +2 +Whisper model loaded. +Your subtitles are ready. You can find them in lecture.srt +``` + +The generated `SrtFiles/lecture.srt` file contains timestamped subtitle entries for each spoken segment. + ## Required Modules ``` @@ -23,6 +45,3 @@ choco install ffmpeg scoop install ffmpeg ``` -## Pyodide-runnable - -No — it uses OpenAI Whisper and FFmpeg to transcribe local audio/video files. diff --git a/projects/Voice-to-Text/README.md b/projects/Voice-to-Text/README.md index 0ee128d33..03df5b1fe 100644 --- a/projects/Voice-to-Text/README.md +++ b/projects/Voice-to-Text/README.md @@ -2,7 +2,17 @@ Captures audio from the microphone and converts it to text using Google's speech recognition service. -## How to run +## Example + +```text +Say something! +Recognising +hello how are you +``` + +The script listens for up to 8 seconds of speech, sends it to Google's speech recognition service, and prints the recognised text in lowercase. If the audio is unclear it prints `Could not understand audio`. + +## How to run on localhost ```bash pip install SpeechRecognition googletrans pyaudio @@ -14,7 +24,3 @@ python main.py - SpeechRecognition - googletrans - pyaudio (microphone access) - -## Pyodide-runnable - -No — it requires microphone hardware access and network speech-recognition calls, neither available in Pyodide. diff --git a/projects/Watermarker/README.md b/projects/Watermarker/README.md index 4b82b42b5..c80935c6d 100644 --- a/projects/Watermarker/README.md +++ b/projects/Watermarker/README.md @@ -2,6 +2,13 @@ A python program that allows users to generate watermarked images. +## Example + +1. Place your JPEG images in an `images/` folder and your watermark image as `mord.png` in the project root. +2. Run `python main.py`. The script opens each `images/*.jpeg` file, resizes the watermark to at most 500×50 pixels, and pastes it at position (20, 20) on the image. +3. Watermarked copies are saved to the `output/` folder (created automatically if it does not exist). +4. The script prints the list of processed image objects when done. + ## DEPENDENCIES Pillow @@ -98,7 +105,3 @@ pip install -r requirements.txt ### **`If you are asked to make changes on the same feature, repeat steps 8 to 13 to add more commits to your pull request.`** "https://github.com/highb33kay/Watermarker.git" - -## Pyodide-runnable - -No — it reads local image files with Pillow and uses `matplotlib`, walking the real filesystem. diff --git a/projects/Weather/README.md b/projects/Weather/README.md index 18e9b6727..e638c87ec 100644 --- a/projects/Weather/README.md +++ b/projects/Weather/README.md @@ -2,7 +2,17 @@ A console weather app. Enter a city name and it fetches the current weather description and temperature from the OpenWeatherMap API. -## How to run +## Example + +```text +Enter a city name: London +Weather: light rain +Temperature: 12.45 celsius +``` + +If the city is not found or the API key is invalid, the script prints `An error occurred.` + +## How to run on localhost ```bash pip install requests @@ -14,7 +24,3 @@ You need an OpenWeatherMap API key; set it in the `API_KEY` variable. ## Dependencies - requests - -## Pyodide-runnable - -No — it uses `requests` to call a live web API, which is not available in Pyodide. diff --git a/projects/Web Scraping Jujustu Kaisen Manga/README.md b/projects/Web Scraping Jujustu Kaisen Manga/README.md index 50a4d374d..9fa73fe9c 100644 --- a/projects/Web Scraping Jujustu Kaisen Manga/README.md +++ b/projects/Web Scraping Jujustu Kaisen Manga/README.md @@ -25,6 +25,28 @@ Web scraping is an essential tool for gathering data and there are plenty of exa +## Example + +Running `python app.py` executes three stages and prints progress messages to the console: + +```text +1/7 Website loading . . . +2/7 Website loaded . . . +3/7 List elements found . . . +4/7 Iterating through links . . . +5/7 Iterating through links completed . . . +6/7 Writing link to files . . . +7/7 Writing link to files completed . . . +******************* CHAPTER NO ******************* 1 +Downloading Images . . . +Downloaded images +... +Zipping chapter_1/chapter_1_0.jpg . . . +Zipping chapter_1/chapter_1_1.jpg . . . +``` + +Chapter images are saved in per-chapter folders and then zipped into `Jujutsu_Kaisen.cbz`. + # Usage Install the following packages : @@ -52,6 +74,3 @@ python app.py
    **Made with 💙 by [Vishvam](https://github.com/Vishvam10)** -## Pyodide-runnable - -No — it uses Selenium and `requests` to scrape a live website and download images. diff --git a/projects/WebButtonSimpelGUI/README.md b/projects/WebButtonSimpelGUI/README.md index e4dfc2032..3b53957fb 100644 --- a/projects/WebButtonSimpelGUI/README.md +++ b/projects/WebButtonSimpelGUI/README.md @@ -1,5 +1,14 @@ # WebButtonSimpelGUI +## Example + +1. Run the script: a small window opens containing three icon buttons stacked vertically. +2. Click Button 1 — the default browser opens `https://www.google.com/` and the console prints `Button 1 clicked!`. +3. Click Button 2 — the browser opens `https://www.youtube.com/` and prints `Button 2 clicked!`. +4. Click Button 3 — the browser opens `https://github.com/` and prints `Button 3 clicked!`. + + + # Project Description This project is a simple GUI application built using Python and the Tkinter library. It consists some buttons, each associated with a different action. Clicking on Button 1 opens Google in the default web browser, Button 2 opens YouTube, and Button 3 opens GitHub (just for example). @@ -9,7 +18,3 @@ The project demonstrates how to create a basic GUI application using Tkinter and To run the project, ensure you have the necessary dependencies installed (Tkinter and Pillow) and execute the script. The main window with the buttons will appear, allowing you to interact with the application. Feel free to clone or download the repository and modify the code according to your requirements. - -## Pyodide-runnable - -No — it uses `tkinter` and `webbrowser`, a desktop GUI that opens external browser pages. diff --git a/projects/Website Blocker/readme.md b/projects/Website Blocker/readme.md index 59c5bea31..327e9fce6 100644 --- a/projects/Website Blocker/readme.md +++ b/projects/Website Blocker/readme.md @@ -3,6 +3,19 @@ The objective of Python website blocker is to block some certain websites which In this, we will block the access to the list of some particular websites during the working hours so that the user can only access those websites during the free time only. The working time in this python application is considered from 9 AM to 5 PM. The time period except that time will be considered as free time. +## Example + +Run the script as root/administrator. It checks the time every 5 seconds and prints its status: + +```text +Working hours... +Working hours... +Fun hours... +Fun hours... +``` + +During working hours (8:00–16:00) it appends lines like `127.0.0.1 www.facebook.com` to the hosts file for each site in the block list. Outside those hours it removes those entries so the sites become accessible again. + ### The hosts file >To block the access to a specific website on the computer, we need to configure the **hosts** file. @@ -22,6 +35,3 @@ We need to know the following python modules to build the python website blocker 1. **file handling:** file handling is used to do the modifications to the hosts file. 2. **time:** The time module is used to control the frequency of the modifications to the hosts file. 3. **datetime:** The datetime module is used to keep track of the free time and working time. -## Pyodide-runnable - -No — it modifies the operating system `hosts` file, which is not accessible in the browser sandbox. diff --git a/projects/Weights_in_different_planets_GUI/README.md b/projects/Weights_in_different_planets_GUI/README.md index 4c17e9031..2dfe83089 100644 --- a/projects/Weights_in_different_planets_GUI/README.md +++ b/projects/Weights_in_different_planets_GUI/README.md @@ -13,6 +13,14 @@ This application allows users to input their weight on Earth and then calculates - **Real-time Calculation**: The weight on each planet is calculated instantly as the user selects a planet. - **Stylish Design**: The application features colorful buttons and labels to enhance the visual experience. +## Example + +1. The window opens titled "Weights in different planet" with a red label "Enter Weight in earth:" and an input field. +2. Type `70` (kilograms) in the entry field. +3. Click the green "Mercury" button — the label below shows `Your Weight In Mercury: 26.46`. +4. Click the red "Mars" button — the label updates to `Your Weight In Mars: 26.53`. +5. Click the yellow "Moon" button — the label updates to `Your Weight In Moon: 11.61`. + ## Usage 1. Enter your weight in the provided input field. @@ -26,8 +34,3 @@ This application allows users to input their weight on Earth and then calculates - Python 3 and above - Tkinter (Python GUI library) - - -## Pyodide-runnable - -No — it uses `tkinter`, a desktop GUI toolkit unavailable in the browser. diff --git a/projects/Windows Logo/README.md b/projects/Windows Logo/README.md index 341fc6014..cd07b5d02 100644 --- a/projects/Windows Logo/README.md +++ b/projects/Windows Logo/README.md @@ -2,7 +2,14 @@ Draws the classic Windows logo using Python's `turtle` graphics on a black background. -## How to run +## Example + +1. Run the script: a black turtle graphics window opens. +2. The turtle draws a blue quadrilateral (the Windows flag shape) filled with `#00adef`. +3. A black vertical stripe and a black horizontal stripe are drawn across the shape, dividing it into four panes — forming the classic Windows logo. +4. The window stays open until closed manually. + +## How to run on localhost ```bash python windows_logo.py @@ -11,7 +18,3 @@ python windows_logo.py ## Dependencies Standard library only (`turtle`). - -## Pyodide-runnable - -No — it uses the `turtle` module, which requires a desktop graphics window. diff --git a/projects/Wine_quality_predictor/Readme.md b/projects/Wine_quality_predictor/Readme.md index 5cad94868..1fd98b03a 100644 --- a/projects/Wine_quality_predictor/Readme.md +++ b/projects/Wine_quality_predictor/Readme.md @@ -15,10 +15,25 @@ During production of wine there may occur an reduction in quality due to some er - alchol - quality +## Example + +Running `python main.py` trains an SVM classifier on `WhiteWineQuality.csv` and prints a confusion matrix and classification report showing per-quality-class precision, recall, and F1 scores: + +```text +[[ 0 0 1 0 0 0] + [ 0 5 12 3 0 0] + ... + ] + precision recall f1-score support + 3 0.00 0.00 0.00 1 + 4 0.45 0.25 0.32 20 + 5 0.58 0.72 0.64 326 + 6 0.55 0.54 0.55 399 + 7 0.47 0.38 0.42 141 + 8 0.33 0.08 0.13 37 +``` + ## Libraries Used: - Numpy - Pandas - Scikit-learn - Support Machine Vector -## Pyodide-runnable - -No — it reads a local CSV with `pandas` and trains a `scikit-learn` model; scikit-learn is not available in Pyodide and the script reads from the real filesystem. diff --git a/projects/Word_Predictor/README.md b/projects/Word_Predictor/README.md index 0b9b9adc4..86def8250 100644 --- a/projects/Word_Predictor/README.md +++ b/projects/Word_Predictor/README.md @@ -2,7 +2,17 @@ A next-word prediction tool that learns from an exported WhatsApp chat. It reads a `Chats.txt` file, builds word-frequency and next-word tables, and predicts the most likely words to follow a given word. -## How to run +## Example + +With `Chats.txt` in place, running `python main.py` loads the chat, builds word-frequency tables, and prints the 3 most likely words to follow `"good"`: + +```text +['morning', 'night', 'luck'] +``` + +The predictions reflect the actual words that most frequently appear after `"good"` in the exported chat file. + +## How to run on localhost ```bash pip install pandas @@ -14,7 +24,3 @@ Place an exported WhatsApp chat as `Chats.txt` in the same folder. ## Dependencies - pandas - -## Pyodide-runnable - -No — although `pandas` is available in Pyodide, the script reads a local `Chats.txt` file from the real filesystem, which is not present in the browser sandbox. diff --git a/projects/Worksheet_to_text/README.md b/projects/Worksheet_to_text/README.md index 435219896..088bfa88d 100644 --- a/projects/Worksheet_to_text/README.md +++ b/projects/Worksheet_to_text/README.md @@ -2,7 +2,19 @@ Reads an Excel workbook and writes each column of data into its own plain-text file (`text-1.txt`, `text-2.txt`, ...). -## How to run +## Example + +Given a `worksheet.xlsx` with three columns, running `python sheetToTextFile.py` creates three text files: + +``` +text-1.txt — contents of column A +text-2.txt — contents of column B +text-3.txt — contents of column C +``` + +Each file contains the cell values from its column written consecutively with no delimiter. + +## How to run on localhost ```bash pip install openpyxl @@ -12,7 +24,3 @@ python sheetToTextFile.py ## Dependencies - openpyxl - -## Pyodide-runnable - -No — it reads a local `worksheet.xlsx` file from the real filesystem, which is not available in the Pyodide browser sandbox. diff --git a/projects/World-Cup-Player-Comparison/README.md b/projects/World-Cup-Player-Comparison/README.md index 07e66e839..3342eaf8f 100644 --- a/projects/World-Cup-Player-Comparison/README.md +++ b/projects/World-Cup-Player-Comparison/README.md @@ -2,7 +2,18 @@ Scrapes 2022 World Cup team statistics from fbref.com, lets you pick two players from two countries, and saves a bar chart comparing their combined goals and assists. -## How to run +## Example + +```text +Please type country name for first player: France +Please type name of player from France: Kylian Mbappe +Please type country name for second player: Argentina +Please type name of player from Argentina: Lionel Messi +``` + +A bar chart titled "2022 World Cup Player G/A Comparison" is generated comparing the two players' combined goals and assists, and saved as a PNG file in the `Results/` folder. + +## How to run on localhost ```bash pip install pandas requests beautifulsoup4 numpy matplotlib @@ -16,7 +27,3 @@ python main.py - beautifulsoup4 - numpy - matplotlib - -## Pyodide-runnable - -No — it uses `requests` to scrape a live website, which is not available in Pyodide. diff --git a/projects/YouTube Video Downloader/README.md b/projects/YouTube Video Downloader/README.md index 095553472..5b9d67eb1 100644 --- a/projects/YouTube Video Downloader/README.md +++ b/projects/YouTube Video Downloader/README.md @@ -2,7 +2,23 @@ A console tool that downloads a YouTube video using `yt-dlp`. It lists the available formats and lets you pick one or auto-selects the highest resolution. -## How to run +## Example + +```text +Enter the URL: https://www.youtube.com/watch?v=dQw4w9WgXcQ +140: webm (144p) +160: mp4 (144p) +18: mp4 (360p) +22: mp4 (720p) +Do you want to select from the available options? (y/n): y +Enter the format id of the video: 22 +[download] Rick Astley - Never Gonna Give You Up.mp4 100% ... +Download complete using yt-dlp! +``` + +The video is saved in the current directory using the title as the filename. + +## How to run on localhost ```bash pip install yt-dlp @@ -12,7 +28,3 @@ python you_tube_analyzer.py ## Dependencies - yt-dlp - -## Pyodide-runnable - -No — it uses `yt-dlp` to download from the internet, which is not available in Pyodide. diff --git a/projects/bittorrent-downloader/README.md b/projects/bittorrent-downloader/README.md index e6beb2d7e..4417a8f45 100644 --- a/projects/bittorrent-downloader/README.md +++ b/projects/bittorrent-downloader/README.md @@ -2,7 +2,19 @@ Checks an email inbox for torrent links sent from a verified account, launches a torrent client to download them, and sends an SMS notification when each download finishes. -## How to run +## Example + +```text +Enter your email: myemail@gmail.com +Enter your email password: •••••••••••• +``` + +The script connects to the Gmail IMAP server, searches the inbox for messages +from the verified sender, extracts any torrent links found, launches the +torrent client for each link, and sends an SMS notification via Twilio when +each download finishes. + +## How to run on localhost ``` pip install -r requirements.txt @@ -14,7 +26,3 @@ python download_torrent.py - imapclient - pyzmail - twilio - -## Pyodide-runnable - -No - it connects to an IMAP email server, launches an external torrent client via subprocess, and sends SMS through the Twilio API. diff --git a/projects/caesar_cipher/README.md b/projects/caesar_cipher/README.md index 6eda4909a..46fea0367 100644 --- a/projects/caesar_cipher/README.md +++ b/projects/caesar_cipher/README.md @@ -2,7 +2,22 @@ A console implementation of the classic Caesar cipher. It encodes or decodes a message by shifting each letter a chosen number of positions through the alphabet. -## How to run +## Example + +```text +Type 'encode' to encrypt, type 'decode' to decrypt: +encode +Type your message: +hello world +Type the shift number: +3 +the encoded message is khoor zruog +``` + +Running it again with `decode`, the same message `khoor zruog`, and shift `3` +gives back `hello world`. + +## How to run on localhost ``` python caesar-cipher.py @@ -11,7 +26,3 @@ python caesar-cipher.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes - it is a pure-stdlib console program using only `input()`/`print()`. diff --git a/projects/car game/README.md b/projects/car game/README.md index a47111af5..305c43017 100644 --- a/projects/car game/README.md +++ b/projects/car game/README.md @@ -2,7 +2,14 @@ A 2D car-dodging arcade game built with Pygame. Steer your car to avoid oncoming traffic, with sound effects, background music, and a high-score tracker. -## How to run +## Example + +1. Run `python Game.py`. A Pygame window opens showing a road with your car at the bottom and oncoming vehicles moving down the screen. Background music begins playing. +2. Press the left and right arrow keys to steer your car and avoid the oncoming traffic. +3. Each vehicle you successfully dodge increases your score. The current score is displayed on screen. +4. If your car collides with an oncoming vehicle the game ends, and the high score is updated if you beat it. + +## How to run on localhost ``` pip install pygame @@ -12,7 +19,3 @@ python Game.py ## Dependencies - pygame - -## Pyodide-runnable - -No - it is a Pygame application that opens a desktop window and plays audio files. diff --git a/projects/career-guide-bot/readme.md b/projects/career-guide-bot/readme.md index 10f3eec08..dc3699ed9 100644 --- a/projects/career-guide-bot/readme.md +++ b/projects/career-guide-bot/readme.md @@ -37,9 +37,3 @@ The Career Guide is a Python-based interactive program that helps users explore ## DEMO image - - - -## Pyodide-runnable - -No - the carreer_guide.py source file is empty, so there is nothing to run. diff --git a/projects/character-picture-grid/README.md b/projects/character-picture-grid/README.md index b5f7f6f26..a4aa2c4fe 100644 --- a/projects/character-picture-grid/README.md +++ b/projects/character-picture-grid/README.md @@ -2,7 +2,20 @@ A small utility that rotates a 2D character grid 90 degrees and prints the resulting picture to the console. -## How to run +## Example + +Running the script prints the built-in grid rotated 90 degrees: + +```text +..OO.OO.. +.OOOOOOO. +.OOOOOOO. +..OOOOO.. +...OOO... +....O.... +``` + +## How to run on localhost ``` python character-picture-grid.py @@ -11,7 +24,3 @@ python character-picture-grid.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes - it is a pure-stdlib console program that only prints to standard output. diff --git a/projects/chore-assignment-emailer/README.md b/projects/chore-assignment-emailer/README.md index 4e7784760..81cacf946 100644 --- a/projects/chore-assignment-emailer/README.md +++ b/projects/chore-assignment-emailer/README.md @@ -2,7 +2,19 @@ Randomly assigns a list of chores among a list of email addresses (distributing them round-robin) and emails each person their assigned chores via Gmail's SMTP server. -## How to run +## Example + +```text +Enter your email: alice@gmail.com +Enter your email password: •••••••••••• +Sending email to example@yahoo.com... +Sending email to example2@yahoo.com... +``` + +Each recipient receives an email with subject `Your Chores.` listing their randomly assigned chores, e.g.: +`Hi There!, dishes, vacuum are your chores` + +## How to run on localhost ``` python chore-emailer.py @@ -13,7 +25,3 @@ You will be prompted for your Gmail address and password so the script can log i ## Dependencies Standard library only (`random`, `smtplib`). - -## Pyodide-runnable - -No — it uses `smtplib` to make an outbound SMTP connection to Gmail, which is not possible in the browser sandbox. diff --git a/projects/comma-code/README.md b/projects/comma-code/README.md index 86980dfe1..af7acc0dd 100644 --- a/projects/comma-code/README.md +++ b/projects/comma-code/README.md @@ -2,7 +2,15 @@ Joins a list of strings into a single human-readable string with commas and the word "and" before the last item (e.g. `apples, bananas, tofu, and cats`). -## How to run +## Example + +Running the script with the built-in list `["apples", "bananas", "tofu", "cats"]` prints: + +```text +apples, bananas, tofu, and cats +``` + +## How to run on localhost ``` python comma-code.py @@ -11,7 +19,3 @@ python comma-code.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — it is a pure-stdlib script that only manipulates strings and prints output. diff --git a/projects/computer-algebra/README.md b/projects/computer-algebra/README.md index 2a0998a82..fc3087761 100644 --- a/projects/computer-algebra/README.md +++ b/projects/computer-algebra/README.md @@ -33,6 +33,13 @@ System (CAS) that allows users to perform various mathematical computations. The pip install -r requirements.txt ``` +## Example + +1. Run `python main.py`. A DearPyGui window opens with an **Input** panel (radio buttons for operations and a text field) and an **Output** panel below it. +2. Select the `derive` radio button, type `x^3` in the expression field, and click **Evaluate**. The Output panel displays `3 x^2`. +3. Select `integrate`, type `x^2`, and click **Evaluate**. The Output panel displays `1/3 x^3`. +4. Select `factor`, type `x^2 - 4`, and click **Evaluate**. The Output panel displays `(x - 2) (x + 2)`. + ## Usage 1. Run the main script `main.py`: @@ -58,7 +65,3 @@ System (CAS) that allows users to perform various mathematical computations. The ## License [![License](https://img.shields.io/static/v1?label=Licence&message=GPL-3-0&color=blue)](https://opensource.org/license/GPL-3-0) - -## Pyodide-runnable - -No — it uses the `dearpygui` GUI toolkit and makes HTTP requests to the Newton API. diff --git a/projects/currency converter/README.md b/projects/currency converter/README.md index 30140530c..b3a220c78 100644 --- a/projects/currency converter/README.md +++ b/projects/currency converter/README.md @@ -2,7 +2,14 @@ A real-time currency converter with a Tkinter GUI. It fetches the latest exchange rates from an online API and lets the user convert an amount between any two currencies via dropdown menus. -## How to run +## Example + +1. The Tkinter window opens (500x200) titled "Currency Converter" and shows the current INR-to-USD rate and today's date. +2. Select the source currency (e.g. INR) from the left dropdown and the target currency (e.g. EUR) from the right dropdown. +3. Type an amount (e.g. `5000`) into the entry field on the left. +4. Click the "Convert" button — the converted amount (e.g. `55.23`) appears in the label on the right. + +## How to run on localhost ``` pip install requests @@ -13,7 +20,3 @@ python "currency converter.py" - `requests` - `tkinter` (standard library) - -## Pyodide-runnable - -No — it builds a `tkinter` GUI and fetches live exchange rates over the network, neither of which works in the Pyodide browser environment. diff --git a/projects/custom-invitations/README.md b/projects/custom-invitations/README.md index c46630c88..104c1f021 100644 --- a/projects/custom-invitations/README.md +++ b/projects/custom-invitations/README.md @@ -2,7 +2,26 @@ Generates a Word document (`invitations.docx`) containing a personalized party invitation page for each name listed in `guests.txt`. -## How to run +## Example + +Given a `guests.txt` containing: +``` +Alice +Bob +Carol +``` + +Running the script produces `invitations.docx` with three pages. Each page reads: + +> *It would be a pleasure to have the company of* +> **Alice** +> *at 11101 Memory lane on the evening of* +> April 31st +> *at 24 O'Clock* + +A new page break separates each guest's invitation. + +## How to run on localhost ``` pip install python-docx @@ -12,7 +31,3 @@ python customInvitations.py ## Dependencies - `python-docx` - -## Pyodide-runnable - -No — it depends on `python-docx` and writes a `.docx` file to the filesystem. diff --git a/projects/custom-seating-cards/README.md b/projects/custom-seating-cards/README.md index 7c4fe2476..8bdb08fa1 100644 --- a/projects/custom-seating-cards/README.md +++ b/projects/custom-seating-cards/README.md @@ -2,7 +2,19 @@ Creates a decorative PNG seating/place card for each guest listed in `guests.txt`, drawing their name over a flower image with a custom font. -## How to run +## Example + +Given a `guests.txt` containing: +``` +Alice +Bob +``` + +Running the script creates an `imageCards/` folder containing: +- `Alice_card.png` — a 291x363 PNG with the flower background and "Alice" drawn in red using the Pacifico font. +- `Bob_card.png` — the same layout with "Bob". + +## How to run on localhost ``` pip install Pillow @@ -12,7 +24,3 @@ python custom_cards.py ## Dependencies - `Pillow` (`PIL`) - -## Pyodide-runnable - -No — it reads image and font assets from disk and writes generated card images to an output folder. diff --git a/projects/dictionary.com-scraper/README.md b/projects/dictionary.com-scraper/README.md index ba1347b6a..821229c00 100644 --- a/projects/dictionary.com-scraper/README.md +++ b/projects/dictionary.com-scraper/README.md @@ -2,7 +2,26 @@ Scrapes the "Word of the Day" from dictionary.com, can fetch all its meanings, generate a text-to-speech pronunciation, and open the word's page in a browser. -## How to run +## Example + +```text +Word of the day is: ephemeral +Do you want to know the meanings of the word ? Press 1.. else Press 0 +1 +Fetching all meanings of the word: ephemeral +1. lasting for a very short time + +Do you want to know how the word is pronounced ? Press 1.. else Press 0 +1 +Generating text-to-speech for ephemeral +Check current directory where script resides. + +Do you want to know more about the word ? Press 1.. else Press 0 +1 +Opening url with word details.. +``` + +## How to run on localhost ``` pip install beautifulsoup4 gtts @@ -13,7 +32,3 @@ python wordOfTheDay.py - `beautifulsoup4` (`bs4`) - `gtts` - -## Pyodide-runnable - -No — it fetches pages from dictionary.com over the network, uses the online `gtts` service, and launches an external browser/media player. diff --git a/projects/duplicate_search/README.md b/projects/duplicate_search/README.md index f38f280b2..f218ef472 100644 --- a/projects/duplicate_search/README.md +++ b/projects/duplicate_search/README.md @@ -2,7 +2,22 @@ Recursively searches a directory for duplicate files by comparing MD5 checksums, then interactively lets you delete unwanted copies. -## How to run +## Example + +```text +$ python main.py /home/user/downloads + +0. /home/user/downloads/report.pdf +1. /home/user/downloads/backup/report.pdf +Delete? (e.g. 0,1,3) 1 + +0. /home/user/downloads/photo.jpg +1. /home/user/downloads/photos/photo.jpg +2. /home/user/downloads/photos/copy/photo.jpg +Delete? (e.g. 0,1,3) 1,2 +``` + +## How to run on localhost ``` python main.py [directory] @@ -13,7 +28,3 @@ If no directory is given, it searches the current directory. ## Dependencies Standard library only (`hashlib`, `sys`, `collections`, `pathlib`). - -## Pyodide-runnable - -No — it walks and deletes files on the real filesystem, which is not meaningful in the Pyodide browser sandbox. diff --git a/projects/excel-to-csv-converter/README.md b/projects/excel-to-csv-converter/README.md index 69be07705..df206b6ad 100644 --- a/projects/excel-to-csv-converter/README.md +++ b/projects/excel-to-csv-converter/README.md @@ -2,7 +2,18 @@ Converts every sheet of every `.xlsx` file in a folder into separate CSV files. -## How to run +## Example + +Given a folder containing `sales_data.xlsx` with two sheets named `Q1` and `Q2`, running the script produces: + +``` +sales_data_Q1.csv +sales_data_Q2.csv +``` + +Each CSV file contains the rows and columns from its corresponding sheet. + +## How to run on localhost ``` pip install openpyxl @@ -13,7 +24,3 @@ python excelToCsv.py - `openpyxl` - `csv`, `os` (standard library) - -## Pyodide-runnable - -No — it scans the real filesystem for Excel files and writes CSV files to disk. diff --git a/projects/facebook_video_downloader/README.md b/projects/facebook_video_downloader/README.md index 479dbd47d..a6883aa29 100644 --- a/projects/facebook_video_downloader/README.md +++ b/projects/facebook_video_downloader/README.md @@ -2,6 +2,15 @@ A python script to download Facebook videos. +## Example + +```text +Enter the link of the video: https://www.facebook.com/watch/?v=123456789 +Downloading... +``` + +The video is saved to the `downloads/` folder in the current directory. + ## Installation ``` @@ -15,7 +24,3 @@ python facebook_video_downloader.py ``` The script will save the video in the downloads folder of the current directory. - -## Pyodide-runnable - -No — it makes network requests to Facebook and downloads video files to disk using `requests` and `wget`. diff --git a/projects/facerecoginition/README.md b/projects/facerecoginition/README.md index 712bc874a..67cbadda4 100644 --- a/projects/facerecoginition/README.md +++ b/projects/facerecoginition/README.md @@ -2,7 +2,15 @@ A two-part OpenCV face recognition demo. `FaceCapture.py` captures face crops from your webcam and saves them; `FaceDetection.py` trains an LBPH recognizer on those crops and live-detects faces, displaying an "Unlocked"/"locked" overlay based on confidence. -## How to run +## Example + +1. Run `FaceCapture.py`. A webcam window titled "face crop" opens and begins capturing your face. +2. The script saves up to 100 grayscale face crops as `faces/faces1.jpg`, `faces/faces2.jpg`, etc. and prints the bounding-box coordinates for each detected face. +3. If no face is detected in a frame, it prints `no face found` and waits for the next frame. +4. Run `FaceDetection.py`. It trains the LBPH recognizer on the saved crops and opens a live webcam feed. +5. Recognised faces show an "Unlocked" overlay; unrecognised faces show "locked". + +## How to run on localhost ``` pip install opencv-contrib-python numpy @@ -14,7 +22,3 @@ python FaceDetection.py - opencv-contrib-python - numpy - -## Pyodide-runnable - -No — it relies on OpenCV webcam capture (`cv2.VideoCapture`) and GUI windows, which are not available in a browser. diff --git a/projects/fantasy-game-inventory/README.md b/projects/fantasy-game-inventory/README.md index 3b9d805fc..c09c17615 100644 --- a/projects/fantasy-game-inventory/README.md +++ b/projects/fantasy-game-inventory/README.md @@ -2,7 +2,25 @@ A small demo that manages a fantasy game player's inventory. It displays items and their counts, and supports adding loot (a list of items) to an existing inventory dictionary. -## How to run +## Example + +```text +Inventory: +1 rope +6 torch +42 gold coin +1 dagger +12 arrow +Total number of items: 62 +Inventory: +45 gold coin +1 rope +1 dagger +1 ruby +Total number of items: 48 +``` + +## How to run on localhost ``` python game-inventory.py @@ -11,7 +29,3 @@ python game-inventory.py ## Dependencies Standard library only. - -## Pyodide-runnable - -Yes — pure-Python logic with only `print()` output, no I/O or external packages. diff --git a/projects/fill-gaps/README.md b/projects/fill-gaps/README.md index fbfe07ec6..7495d6ebd 100644 --- a/projects/fill-gaps/README.md +++ b/projects/fill-gaps/README.md @@ -2,7 +2,23 @@ A utility for renaming numbered files in a folder. `fillGaps` closes gaps in a numbered sequence (e.g. `spam001`, `spam003` becomes `spam001`, `spam002`), and `insertGaps` opens a gap at a chosen index. -## How to run +## Example + +Given files `spam001.txt` and `spam003.txt` in the current directory, running `fillGaps` closes the gap: + +```text +Before: spam001.txt, spam003.txt +After: spam001.txt, spam002.txt +``` + +Calling `insertGaps('.', 'spam', 2)` on `spam001.txt`, `spam002.txt` would shift files up to open a slot at index 2: + +```text +Before: spam001.txt, spam002.txt +After: spam001.txt, spam003.txt +``` + +## How to run on localhost ``` python fill_gaps.py @@ -11,7 +27,3 @@ python fill_gaps.py ## Dependencies Standard library only (`os`, `re`, `shutil`). - -## Pyodide-runnable - -No — it lists, renames, and moves files on the real filesystem, which is not available in a browser sandbox. diff --git a/projects/find-unneeded-files/README.md b/projects/find-unneeded-files/README.md index ac20b9456..122322b97 100644 --- a/projects/find-unneeded-files/README.md +++ b/projects/find-unneeded-files/README.md @@ -2,7 +2,19 @@ Walks a folder tree and reports files or subfolders whose size exceeds a given threshold, to help locate large items that could be cleaned up. -## How to run +## Example + +Running with the default threshold of 1000 bytes against the parent folder: + +```text +/home/user/projects/node_modules: 45231890 +/home/user/projects/videos/demo.mp4: 8723456 +/home/user/projects/dataset.zip: 2048300 +``` + +Files and folders smaller than 1000 bytes are not listed. + +## How to run on localhost ``` python find_unneeded.py @@ -11,7 +23,3 @@ python find_unneeded.py ## Dependencies Standard library only (`os`). - -## Pyodide-runnable - -No — it walks the real filesystem and reads file sizes, which is not available in a browser sandbox. diff --git a/projects/game-snake_water_gun/README.md b/projects/game-snake_water_gun/README.md index c3c9c54cc..fd6282615 100644 --- a/projects/game-snake_water_gun/README.md +++ b/projects/game-snake_water_gun/README.md @@ -14,7 +14,3 @@ 1. Python Should be installed in your system 2. Just download the game.py file and open it (bydefault it should be open in terminal). * If you do not have or not want to install python in your system, you can download the game.exe file and directly run that executable file. - -## Pyodide-runnable - -Yes — `game.py` is a console game using only `input()`/`print()` and the standard-library `random` module. diff --git a/projects/goodreads-quotes-scraper/README.md b/projects/goodreads-quotes-scraper/README.md index 13567e2be..681272ab4 100644 --- a/projects/goodreads-quotes-scraper/README.md +++ b/projects/goodreads-quotes-scraper/README.md @@ -2,7 +2,26 @@ Scrapes quotes from Goodreads — top popular, recently added, by tag, or by page number — and saves the results to `temp.json`. -## How to run +## Example + +```text +Enter choice.. +1. Top Popular Quotes.. +2. Top Recent Quotes.. +3. Top Quotes by Tag.. +4. Top Quotes by Pagination.. +..3 +To view complete list of tags press '' or visit goodreads.com +Tag please..love +Tag found. +Getting top quotes for the tag: love +"The best thing to hold onto in life is each other." -- Audrey Hepburn +"I am not afraid of storms, for I am learning how to sail my ship." -- Louisa May Alcott +``` + +Results are saved to `temp.json`. + +## How to run on localhost ``` pip install bs4 selenium @@ -13,7 +32,3 @@ python goodreadsScrape.py - bs4 - selenium - -## Pyodide-runnable - -No — it fetches live web pages over the network (and imports Selenium), which is not available in a browser sandbox. diff --git a/projects/healthmanagementsystem/README.md b/projects/healthmanagementsystem/README.md index 8d724c0c1..2540bd15c 100644 --- a/projects/healthmanagementsystem/README.md +++ b/projects/healthmanagementsystem/README.md @@ -2,7 +2,29 @@ A console-based health log. It lets you log exercise or food entries (timestamped) for one of three users into text files, and retrieve those logs later. -## How to run +## Example + +```text +health management system: +Press 1 for log the value and 2 for retrieve 1 +Press 1 for anu 2 for simon 3 for john 1 +Enter 1 for excerise and 2 for food :1 +type here.. +30 min morning run +written successfully +``` + +Retrieving a log: + +```text +health management system: +Press 1 for log the value and 2 for retrieve 2 +Press 1 for anu 2 for simon 3 for john 1 +enter 1 for exercise and 2 for food1 +['2024-03-15 08:12:34.123456']: 30 min morning run +``` + +## How to run on localhost ``` python health.py @@ -11,7 +33,3 @@ python health.py ## Dependencies Standard library only (`datetime`). - -## Pyodide-runnable - -No — it reads and appends to real files on disk, which is not available in a browser sandbox. diff --git a/projects/hill_cipher/Readme.md b/projects/hill_cipher/Readme.md index fe6afac23..ab8f58292 100644 --- a/projects/hill_cipher/Readme.md +++ b/projects/hill_cipher/Readme.md @@ -10,10 +10,18 @@ https://dev.mysql.com/downloads/installer/

    After the setup checkout the 'requirements.py' file and run the code in your python compiler ;)

    +

    Example

    +

    Encrypting a three-character message:

    +
    +enter your message: cat
    + encrypted output: xqv
    +  
    +

    Decrypting the cipher text (using decryption.py):

    +
    +enter the coded text: xqv
    + decrypted output: cat
    +  
    +

    Aid for Exceptions/Errors

    Try: pip intall mysql or/and pip install mysql-connector-python

    - -## Pyodide-runnable - -No — it connects to a local MySQL database via `mysql.connector`, which is not available in a browser sandbox. diff --git a/projects/image-site-downloader/README.md b/projects/image-site-downloader/README.md index 45510be15..1cea58a0e 100644 --- a/projects/image-site-downloader/README.md +++ b/projects/image-site-downloader/README.md @@ -2,7 +2,13 @@ Searches Imgur for a query term, scrapes the resulting image thumbnails, and downloads up to a chosen number of them into a local `results/` folder. -## How to run +## Example + +1. The script is hardcoded to search Imgur for `"messi"` and download up to 10 thumbnail images. +2. Run `python imgur-downloader.py`. +3. Matching images are saved by their original filename into the `results/` directory (created automatically). + +## How to run on localhost ``` pip install requests bs4 @@ -13,7 +19,3 @@ python imgur-downloader.py - requests - bs4 - -## Pyodide-runnable - -No — it makes live HTTP requests and writes downloaded files to disk, neither of which is available in a browser sandbox. diff --git a/projects/indeed-scraper/README.md b/projects/indeed-scraper/README.md index 3c921fb5a..fb7a8f315 100644 --- a/projects/indeed-scraper/README.md +++ b/projects/indeed-scraper/README.md @@ -2,7 +2,18 @@ Scrapes job listings from Indeed for several cities and job titles, then saves the results to a CSV file. -## How to run +## Example + +1. Run `python indeed-scraper.py`. +2. The script iterates over 8 job titles (e.g., `full+stack+developer`, `data+scientist`) across 4 cities (Mumbai, Bangalore, Hyderabad, Pune), printing each URL as it fetches results: + ``` + http://www.indeed.co.in/jobs?q=full+stack+developer&l=mumbai&start=0 + full+stack+developer mumbai 0 + ... + ``` +3. When finished, all scraped listings (job title, company, summary, location, salary, date) are saved to `job_listing.csv`. + +## How to run on localhost ``` pip install -r requirements.txt @@ -12,6 +23,3 @@ python indeed-scraper.py ## Dependencies requests, beautifulsoup4, lxml, pandas. - -## Pyodide-runnable -No — it makes live HTTP requests to Indeed to scrape web pages, which Pyodide cannot do. diff --git a/projects/instant-messenger-bot/README.md b/projects/instant-messenger-bot/README.md index 2dd29d03a..d4d87409f 100644 --- a/projects/instant-messenger-bot/README.md +++ b/projects/instant-messenger-bot/README.md @@ -2,7 +2,20 @@ Automates sending messages to active Slack contacts by simulating keyboard and mouse input. -## How to run +## Example + +```text +Enter contact list, separated by space >> Messi Onazi John: Alice Bob +Enter the message you wish to send out to them: Happy Monday everyone! +5 seconds to navigate to slack app.. +Contact is active, sending message... +5 seconds to navigate to slack app.. +Contact is active, sending message... +``` + +The bot uses Slack's Jump To shortcut to open each contact's chat and types the message automatically. + +## How to run on localhost ``` pip install -r requirements.txt @@ -12,6 +25,3 @@ python slack_messenger.py ## Dependencies pyautogui. - -## Pyodide-runnable -No — it uses pyautogui to control the desktop keyboard, mouse, and screen. diff --git a/projects/inventory management/README.md b/projects/inventory management/README.md index 7b71dd64b..f0f5aa4f7 100644 --- a/projects/inventory management/README.md +++ b/projects/inventory management/README.md @@ -2,7 +2,22 @@ A simple supermarket inventory system with a Tkinter login window and add/display/search/update/delete operations backed by a MySQL database. -## How to run +## Example + +1. Run `python main.py`. A Tkinter login window titled "Authorised Login Only" opens. +2. Enter username `Admin` and password `1234`, then click Submit. +3. The main "Welcome to Super Market" inventory window appears with buttons: Add, Search, Update, Display, Delete, and Exit. +4. Clicking **Add** prompts in the console: + ``` + Enter the product id: 101 + Enter the product name: Apple + Enter the product cost: 50 + Enter the product quantity: 200 + Data Inserted successfully + ``` +5. Clicking **Display** prints all rows from the `inventory` table to the console. + +## How to run on localhost ``` pip install -r requirements.txt @@ -14,6 +29,3 @@ Requires a local MySQL server with a `records` database and an `inventory` table ## Dependencies tkinter (standard library), mysql-connector-python. - -## Pyodide-runnable -No — it uses a Tkinter GUI and connects to a MySQL database. diff --git a/projects/link-verification/README.md b/projects/link-verification/README.md index bf3ff18f0..b1c3130b0 100644 --- a/projects/link-verification/README.md +++ b/projects/link-verification/README.md @@ -2,7 +2,17 @@ Fetches a web page, extracts all of its links, and reports which ones are good or broken. -## How to run +## Example + +```text +Good: https://automatetheboringstuff.com/ +Good: https://nostarch.com/automatestuff2 +Broken: https://example.com/missing-page +Good: https://python.org +3 Good. 1 Broken +``` + +## How to run on localhost ``` pip install -r requirements.txt @@ -12,6 +22,3 @@ python verify_links.py ## Dependencies requests, beautifulsoup4. - -## Pyodide-runnable -No — it makes live HTTP requests to verify links across the internet. diff --git a/projects/linkedin-scrape/README.md b/projects/linkedin-scrape/README.md index 21633d0c2..e7d655256 100644 --- a/projects/linkedin-scrape/README.md +++ b/projects/linkedin-scrape/README.md @@ -2,7 +2,17 @@ Logs into LinkedIn with Selenium (Firefox) and scrapes contact information (email, phone, website) from a connection's profile. -## How to run +## Example + +```text +Enter email address or number with country code: you@example.com +Enter your password: +Andriy Burkov +Email Address: andriy@example.com +Phone Number: +1-555-0100 +``` + +## How to run on localhost ``` pip install -r requirements.txt @@ -14,6 +24,3 @@ Requires Firefox and the bundled geckodriver. ## Dependencies selenium, beautifulsoup4, requests, lxml. - -## Pyodide-runnable -No — it drives a real browser with Selenium to scrape a live website. diff --git a/projects/looking-busy/README.md b/projects/looking-busy/README.md index a586e9612..c157f8dbc 100644 --- a/projects/looking-busy/README.md +++ b/projects/looking-busy/README.md @@ -2,7 +2,16 @@ Moves the mouse slightly every 10 seconds to keep your computer and apps from going idle. -## How to run +## Example + +```text +Press CTRL-C to quit. +(mouse moves 5 pixels right then back every 10 seconds) +^C +Process has quit... +``` + +## How to run on localhost ``` pip install -r requirements.txt @@ -12,6 +21,3 @@ python look_busy.py ## Dependencies pyautogui. - -## Pyodide-runnable -No — it uses pyautogui to control the desktop mouse. diff --git a/projects/love-calculator/README.md b/projects/love-calculator/README.md index 7cf9a7d30..dd9d16250 100644 --- a/projects/love-calculator/README.md +++ b/projects/love-calculator/README.md @@ -2,7 +2,19 @@ A console program that takes two names and computes a playful "love score" percentage based on shared vowels, consonants, first letters, and name length, then describes the relationship. -## How to run +## Example + +```text +Please type Name 1. +Alice Smith +Please type Name 2. +Bob Smith +Calculating... +Alice Smith and Bob Smith have a 78% relationship. +They have a strong relationship that will most likely lead to a marriage. +``` + +## How to run on localhost ``` python "Love Calculator.py" @@ -11,6 +23,3 @@ python "Love Calculator.py" ## Dependencies Standard library only. - -## Pyodide-runnable -Yes — it is a pure-stdlib console program using only input(), print(), random, and time. diff --git a/projects/maths/README.md b/projects/maths/README.md index ee3c070ef..e18abc86f 100644 --- a/projects/maths/README.md +++ b/projects/maths/README.md @@ -1,8 +1,32 @@ -# 3D Shape Volume Calculator +# Mensuration Calculator A console program that calculates the volume of various 3D shapes (cube, pyramid, cylinder, sphere, cone, ellipsoid, torus, hexagonal prism) from user-supplied dimensions. -## How to run +## Example + +```text +----------------------- +Select operation. +1.Cube/cuboid +2.Pyramid(RightRectangular) +3.Cylinder +4.Sphere +5.Cone +6.Ellipsoid +7.Torus(Donut) +8.Hexagonal Prism +----------------------- +Enter choice(1/2/3/4/5/6/7/8): 3 +----------------------- +Enter the raduis: 5 +Enter the height: 10 +----------------------- +Finding the volume for Cylinder +----------------------- +The volume of the Cylinder is: 785.7142857142857 unit's +``` + +## How to run on localhost ``` python 3dShapeVolume.py @@ -11,6 +35,3 @@ python 3dShapeVolume.py ## Dependencies Standard library only. - -## Pyodide-runnable -Yes — it is a pure-stdlib console program using only input(), print(), and cmath. diff --git a/projects/minesweeper/README.md b/projects/minesweeper/README.md index 750558776..7eb942f29 100644 --- a/projects/minesweeper/README.md +++ b/projects/minesweeper/README.md @@ -2,7 +2,25 @@ A console version of the classic Minesweeper game. It builds a board, randomly plants bombs, and asks the player where to dig (entered as `row,col`). Digging an empty cell recursively reveals neighbouring cells; hitting a bomb ends the game. -## How to run +## Example + +```text + 0 1 2 3 4 5 6 7 8 9 +---------------------------------- +0 | | | | | | | | | | | +1 | | | | | | | | | | | +... +Where would you like to dig? Input as row,col: 3,5 + 0 1 2 3 4 5 6 7 8 9 +---------------------------------- +0 | | | | | | | | | | | +... +3 | | | | | |2 | | | | | +Where would you like to dig? Input as row,col: 0,0 +SORRY GAME OVER :( +``` + +## How to run on localhost ``` python main.py @@ -11,7 +29,3 @@ python main.py ## Dependencies Standard library only (`random`, `re`). - -## Pyodide-runnable - -Yes — it is a pure-stdlib console program that only uses `input()` and `print()`. diff --git a/projects/movie-rater/README.md b/projects/movie-rater/README.md index 629ab5a23..5de7e95c1 100644 --- a/projects/movie-rater/README.md +++ b/projects/movie-rater/README.md @@ -29,6 +29,16 @@ Before you begin, ensure you have met the following requirements: pip install -r requirements.txt ``` +## Example + +Once the server is running, interact with the API at `http://127.0.0.1:8000`: + +```text +GET /movies → returns a list of popular movies from TMDB +GET /movies/550 → returns details for the movie with id 550 +POST /movies/550/rate?rating=8 → responds with {"message": "Movie rated successfully."} +``` + 4. ## Running the Application ### To run the FastAPI application, use the following command: @@ -36,6 +46,3 @@ Before you begin, ensure you have met the following requirements: uvicorn main:app --reload ``` -## Pyodide-runnable - -No — it is a FastAPI web server that makes network requests to The Movie Database API. diff --git a/projects/multiplayer_socket/README.md b/projects/multiplayer_socket/README.md index a774d12be..79629608f 100644 --- a/projects/multiplayer_socket/README.md +++ b/projects/multiplayer_socket/README.md @@ -2,7 +2,39 @@ A two-player networked game system using TCP sockets. `server.py` hosts the games and `client.py` connects to it. Players can choose between a multiplayer Hangman game and a Rock-Paper-Scissors game. -## How to run +## Example + +**Rock-Paper-Scissors (Player 1 terminal):** + +```text +Enter your choice(1 or 2) +2 +Get ready to play Rock, Paper, Scissor +You are Player 1 +Play +rock +ROCK,PAPER,0,1 +Play +paper +Player 2 won +``` + +**Hangman (Player 1 choosing the word):** + +```text +Enter your choice(1 or 2) +1 +Get ready to play hangman +Choose a word +python +Word sent to server +Waiting for guess... +Guess= P +Enter the positions of occurances +1 +``` + +## How to run on localhost Start the server first, then run a client on each player's machine: @@ -14,7 +46,3 @@ python client.py ## Dependencies Standard library only (`socket`, `_thread`). - -## Pyodide-runnable - -No — it uses raw TCP sockets and threading to communicate between machines, which is not possible in a browser sandbox. diff --git a/projects/pdf-paranoia/README.md b/projects/pdf-paranoia/README.md index 76a917735..2b8d548af 100644 --- a/projects/pdf-paranoia/README.md +++ b/projects/pdf-paranoia/README.md @@ -2,7 +2,15 @@ Bulk-encrypts and decrypts PDF files. It walks a directory tree, encrypts every unencrypted PDF with a user-supplied password (writing the result into an `untitled folder`), and can decrypt the `_encrypted` PDFs back again. -## How to run +## Example + +```text +Enter encryption password: mysecretpass +``` + +The script walks the current directory, encrypts every unencrypted `.pdf` it finds with `mysecretpass`, and writes the result to `untitled folder/` with `_encrypted` inserted before the extension (e.g. `report.pdf` becomes `untitled folder/report_encrypted.pdf`). It then decrypts any `_encrypted.pdf` files found using the same password, writing the plain copies alongside them. + +## How to run on localhost ``` pip install PyPDF2 @@ -12,7 +20,3 @@ python pdfParanoia.py ## Dependencies - PyPDF2 - -## Pyodide-runnable - -No — it walks the real filesystem with `os.walk` and reads/writes PDF files on disk, which is unavailable in the browser. diff --git a/projects/pdf_to_text/README.md b/projects/pdf_to_text/README.md index 8f377fdf5..951b0d70e 100644 --- a/projects/pdf_to_text/README.md +++ b/projects/pdf_to_text/README.md @@ -35,6 +35,3 @@ This Python script is designed to convert a PDF file into a plain text (.txt) fi - [pi1814](https://github.com/pi1814) This script simplifies the process of converting PDF files to text. If you encounter any issues or have suggestions for improvements, please don't hesitate to reach out to the author on their GitHub page. Feel free to customize and use this script for your PDF to text conversion needs. -## Pyodide-runnable - -No — it is a PyQt5 desktop GUI application and reads PDF files from the local filesystem. diff --git a/projects/ping_pong/README.md b/projects/ping_pong/README.md index 6f0492004..1847397c8 100644 --- a/projects/ping_pong/README.md +++ b/projects/ping_pong/README.md @@ -2,7 +2,16 @@ A two-player Pong game built with Python's `turtle` graphics. Player A uses `W`/`S` and Player B uses the arrow keys to move their paddles; the ball bounces around the screen and the score updates on goals. -## How to run +## Example + +1. An 800x600 green window titled "The Pong Game" opens with two white paddles and a red ball. +2. The score display at the top reads "Player A: 0 Player B: 0". +3. Player A presses `W` / `S` to move the left paddle up or down; Player B uses the Up / Down arrow keys for the right paddle. +4. The ball bounces off the top and bottom walls and off each paddle. +5. When the ball passes the right edge, Player A scores a point; when it passes the left edge, Player B scores. The score display updates immediately. +6. The ball resets to the centre after each goal and play continues. + +## How to run on localhost ``` python ping_pong.py @@ -11,7 +20,3 @@ python ping_pong.py ## Dependencies Standard library only (`turtle`). - -## Pyodide-runnable - -No — it uses the `turtle` graphics module and keyboard events, which require a desktop windowing environment. diff --git a/projects/prettified-stopwatch/README.md b/projects/prettified-stopwatch/README.md index 3cd9609d3..cc2bc376b 100644 --- a/projects/prettified-stopwatch/README.md +++ b/projects/prettified-stopwatch/README.md @@ -2,7 +2,24 @@ A console stopwatch that records lap times. Press ENTER to start, ENTER again to mark each lap, and Ctrl-C to stop. Results are copied to the system clipboard. -## How to run +## Example + +```text +Press ENTER to begin. Afterwards, press ENTER to "click" the stopwatch. Press Ctrl-C to quit. + +Started. + +Lap # 1: 3.14 ( 3.14) +Lap # 2: 7.82 ( 4.68) +Lap # 3: 12.05 ( 4.23) +^C +Done. +Results available in clipboard +``` + +Each ENTER press records a lap. The first column is total elapsed time, the second (in parentheses) is the individual lap time. All results are copied to the clipboard on exit. + +## How to run on localhost ```sh pip install pyperclip @@ -13,7 +30,3 @@ python stopwatch.py - pyperclip - time (standard library) - -## Pyodide-runnable - -No - it relies on `pyperclip` for system clipboard access, which is not available in the Pyodide sandbox. diff --git a/projects/proxy-scrapper/README.md b/projects/proxy-scrapper/README.md index 8576c361c..5952bdca1 100644 --- a/projects/proxy-scrapper/README.md +++ b/projects/proxy-scrapper/README.md @@ -1,5 +1,23 @@ # Proxy Scrapper +## Example + +```text +$ python3 proxy_scrapper.py +``` + +The script silently fetches the proxy table from free-proxy-list.net and appends +each discovered proxy to `proxies.txt`: + +```text +185.199.228.220:7300 +103.149.162.195:80 +47.74.152.29:8888 +... +``` + +No interactive prompts are shown; all output goes directly into the file. + Install BeautifulSoup ```sh pip3 install beautifulsoup4 @@ -14,7 +32,3 @@ python3 proxy_scrapper.py - requests - beautifulsoup4 - -## Pyodide-runnable - -No - it uses `requests` to fetch a live website over the network, which is blocked in the browser sandbox. \ No newline at end of file diff --git a/projects/qr-code-generator-with-window-and-simple-ui/README.md b/projects/qr-code-generator-with-window-and-simple-ui/README.md index db368a9fe..db54b4f05 100644 --- a/projects/qr-code-generator-with-window-and-simple-ui/README.md +++ b/projects/qr-code-generator-with-window-and-simple-ui/README.md @@ -2,7 +2,13 @@ A Tkinter desktop application that takes a text/URL, save location, file name, and size, then generates a QR code image and saves it to the chosen directory. -## How to run +## Example + +1. The window titled "RavenyBoi's QR code generator" opens with four input fields: text/URL, save location, file name, and size (1–40). +2. Enter `https://github.com` in "Enter the text/URL", `C:\Users\Alice\Desktop` in the location field, `my_qr` as the file name, and `5` as the size. +3. Click "Generate Code". A QR code image is saved to `C:\Users\Alice\Desktop\my_qr.png` and a pop-up confirms "QR Code is saved successfully!". + +## How to run on localhost ```sh pip install qrcode pillow @@ -14,7 +20,3 @@ python main.py - qrcode - pillow - tkinter (standard library) - -## Pyodide-runnable - -No - it builds a Tkinter GUI and writes image files to real disk paths, neither of which works in the browser sandbox. diff --git a/projects/reciept generator/README.md b/projects/reciept generator/README.md index 80fe1f6ce..b7e358d81 100644 --- a/projects/reciept generator/README.md +++ b/projects/reciept generator/README.md @@ -43,7 +43,3 @@ With more contribution, this can be a very memorable open source project. ## Dependencies - fpdf - -## Pyodide-runnable - -No - it depends on the `fpdf` package (not bundled with Pyodide) to render a PDF receipt. diff --git a/projects/reddit-scraper/README.md b/projects/reddit-scraper/README.md index 5358a7f49..91c5124d7 100644 --- a/projects/reddit-scraper/README.md +++ b/projects/reddit-scraper/README.md @@ -2,7 +2,25 @@ Fetches the top posts from the r/python subreddit via Reddit's JSON API and stores them in a local SQLite database (`reddit_news.db`). -## How to run +## Example + +```text +$ python -c "import grabnews; grabnews.reddit_get()" +Inserted +Inserted +Inserted +Inserted +Inserted +Inserted +Inserted +Inserted +Inserted +Inserted +``` + +Each `Inserted` line means one top post from r/python was written to `reddit_news.db`. Re-running the script prints `Updated` for posts already in the database. + +## How to run on localhost ```sh pip install requests @@ -13,7 +31,3 @@ python -c "import grabnews; grabnews.reddit_get()" - requests - sqlite3 (standard library) - -## Pyodide-runnable - -No - it uses `requests` to fetch live data from Reddit over the network, which is blocked in the browser sandbox. diff --git a/projects/regex-search/README.md b/projects/regex-search/README.md index a392bfb9b..77fe89227 100644 --- a/projects/regex-search/README.md +++ b/projects/regex-search/README.md @@ -2,7 +2,17 @@ A console tool that prompts for a regular expression and prints every line in the `.txt` files of a folder that matches it. -## How to run +## Example + +```text +enter regex: \bpython\b +This is a python tutorial. +Learn python programming today. +``` + +The tool searches all `.txt` files in the current directory and prints every line that matches the supplied regular expression. + +## How to run on localhost ```sh python regex-search.py @@ -11,7 +21,3 @@ python regex-search.py ## Dependencies Standard library only (uses `os` and `re`). - -## Pyodide-runnable - -No - it walks the real filesystem with `os.listdir` to find and open `.txt` files on disk, which is not available in the browser sandbox. diff --git a/projects/regex-strip/README.md b/projects/regex-strip/README.md index 0855bbb49..eb083c378 100644 --- a/projects/regex-strip/README.md +++ b/projects/regex-strip/README.md @@ -2,7 +2,16 @@ A small demonstration that reimplements Python's `str.strip()` method using regular expressions to remove leading and trailing whitespace. -## How to run +## Example + +The script runs on the hard-coded string `" test ffs "` and prints the result with leading and trailing whitespace removed: + +```text +$ python regex-strip.py +test ffs +``` + +## How to run on localhost ```sh python regex-strip.py @@ -11,7 +20,3 @@ python regex-strip.py ## Dependencies Standard library only (uses `re`). - -## Pyodide-runnable - -Yes - it is pure-stdlib code using only the `re` module and `print`. diff --git a/projects/reuters-scraper/README.md b/projects/reuters-scraper/README.md index 8da0b695c..69ac48b74 100644 --- a/projects/reuters-scraper/README.md +++ b/projects/reuters-scraper/README.md @@ -2,7 +2,24 @@ Scrapes technology news headlines, summaries, times, and links from the Reuters website and writes them to `reuters_scrape.csv`. -## How to run +## Example + +```text +$ python scrape.py +``` + +The script scrapes the Reuters technology page and writes results to `reuters_scrape.csv`. While running it prints each article's headline, summary snippet, timestamp, and link to the console: + +```text +Tech giants face new antitrust scrutiny in Europe +Regulators are ramping up investigations into major technology companies... +2 hours ago +https://www.reuters.com/technology/tech-giants-face-antitrust-2024-01-15/ +``` + +The CSV file contains columns: `Headline`, `Summary`, `Time`, `Article Link`. + +## How to run on localhost ```sh pip install beautifulsoup4 lxml @@ -14,7 +31,3 @@ python scrape.py - beautifulsoup4 - lxml - urllib, csv (standard library) - -## Pyodide-runnable - -No - it uses `urllib` to fetch a live website over the network, which is blocked in the browser sandbox. diff --git a/projects/scheduledShutdown/README.md b/projects/scheduledShutdown/README.md index faa81e9d1..6c7becedc 100644 --- a/projects/scheduledShutdown/README.md +++ b/projects/scheduledShutdown/README.md @@ -2,7 +2,18 @@ A console utility that asks for a number of minutes and then shuts down the computer after that delay using a system shutdown command. -## How to run +## Example + +```text +Shutdown After ----> 30 +Computer Will Now Shutdown in 30 Minutes + +Computer Will Now Shutdown! +``` + +After the specified number of minutes the script executes the system shutdown command. + +## How to run on localhost ```sh python shutdown.py @@ -11,7 +22,3 @@ python shutdown.py ## Dependencies Standard library only (uses `os` and `time`). - -## Pyodide-runnable - -No - it calls `os.system("shutdown ...")` to power off the host machine, which is not available in the browser sandbox. diff --git a/projects/scrap-ycombinator/README.md b/projects/scrap-ycombinator/README.md index 2d6e1629f..37a6803bb 100644 --- a/projects/scrap-ycombinator/README.md +++ b/projects/scrap-ycombinator/README.md @@ -2,7 +2,16 @@ Scrapes article titles and links from the Hacker News (Y Combinator) front page and writes them to `ycombinatornews.csv`. -## How to run +## Example + +```text +$ python main.py +Done +``` + +After running, a file `ycombinatornews.csv` is created in the same directory containing two columns — `ARTICLE TITLE` and `ARTICLE LINKS` — with one row per story scraped from the Hacker News front page. + +## How to run on localhost ```sh pip install requests beautifulsoup4 lxml @@ -15,7 +24,3 @@ python main.py - beautifulsoup4 - lxml - csv (standard library) - -## Pyodide-runnable - -No - it uses `requests` to fetch a live website over the network, which is blocked in the browser sandbox. diff --git a/projects/servo motor classifier/Readme.md b/projects/servo motor classifier/Readme.md index bcac8a031..6673192c1 100644 --- a/projects/servo motor classifier/Readme.md +++ b/projects/servo motor classifier/Readme.md @@ -1,18 +1,30 @@ # Servo motor classifier + ## Descirption + Servos are basic components of many machines. There are different classes of servo for different function and to classify them can be hard for people of non-technical background.It can be solved by using a tool that classifies the servo in their respective classes. + ### Parameters Used: + - Motor type - Screw - Pgain - Vgain ## Libraries Used: + - Numpy - Pandas - Scikit-learn -## How to run +## Example + +1. Run `python main.py`. The script downloads the Servo Mechanism dataset from GitHub. +2. It encodes the `Motor` and `Screw` columns (A–E mapped to 0–4) and trains a linear regression model on 70% of the data. +3. Predictions are made on the remaining 30% and evaluation metrics (MSE, MAE, R²) are printed to the console. +4. A scatter plot window opens showing actual vs. predicted class values. + +## How to run on localhost ``` pip install pandas numpy scikit-learn matplotlib @@ -22,7 +34,3 @@ python main.py ## Dependencies pandas, numpy, scikit-learn, matplotlib - -## Pyodide-runnable - -No — it downloads a CSV over the network with `pd.read_csv()` and renders a Matplotlib window, neither of which works in Pyodide. diff --git a/projects/snake_water_gun_game/README.md b/projects/snake_water_gun_game/README.md index a53c86e39..6ac0679a9 100644 --- a/projects/snake_water_gun_game/README.md +++ b/projects/snake_water_gun_game/README.md @@ -2,7 +2,29 @@ A console version of the snake-water-gun game (a rock-paper-scissors variant). The snake drinks the water, the gun shoots the snake, and the gun has no effect on water. You play 10 rounds against the computer with colored terminal output. -## How to run +## Example + +```text + Snake,Water,Gun Game + +s for snake, w for water, g for gun + +Enter your choice >> s +You guessed s and Computer guessed g. +Computer wins 1 point +9 chance(s) are left out of 10 chances. + +Enter your choice >> g +You guessed g and Computer guessed s. +Human wins 1 point +8 chance(s) are left out of 10 chances. +... +Game over! +You won and Computer lost. +Your points: 6 Computer points: 3 +``` + +## How to run on localhost ``` pip install rich @@ -12,7 +34,3 @@ python Snake_Water_Gun_Game.py ## Dependencies rich - -## Pyodide-runnable - -No — it depends on the `rich` package, which is not bundled with Pyodide. diff --git a/projects/space_battle/README.md b/projects/space_battle/README.md index 3eb52116f..c6842820e 100644 --- a/projects/space_battle/README.md +++ b/projects/space_battle/README.md @@ -2,7 +2,15 @@ A Space Invaders style game built with Pygame. Move a UFO around the screen, shoot down descending enemies, and play with sound effects and background music. -## How to run +## Example + +1. Run `python main.py`. An 800×600 Pygame window titled "Space invaders" opens with a space background and background music. +2. Use the arrow keys to move your UFO around the screen; press Space to fire a bullet upward. +3. Ten enemy UFOs descend in rows, bouncing left and right and moving down each time they reach a screen edge. +4. Each enemy hit increments the score displayed in the top-left corner (`score:1`, `score:2`, …). +5. If an enemy reaches the bottom of the screen or collides with the player, a "Game-Over" message appears. Press 1 to restart. + +## How to run on localhost ``` pip install pygame @@ -12,7 +20,3 @@ python main.py ## Dependencies pygame - -## Pyodide-runnable - -No — it uses Pygame for windowed graphics, audio playback, and real-time keyboard input. diff --git a/projects/sponge-bob/README.md b/projects/sponge-bob/README.md index 28e2734c1..aeb25a31b 100644 --- a/projects/sponge-bob/README.md +++ b/projects/sponge-bob/README.md @@ -2,7 +2,14 @@ A turtle-graphics script that draws a SpongeBob SquarePants character (body, pants, shirt, tie, eyes, and smile) on screen. -## How to run +## Example + +1. Run `python main.py`. A turtle-graphics window opens. +2. The script draws a yellow square body, brown pants, a white shirt strip, a small red tie circle, two eyes (white circles with black pupils), and a red smile with white teeth. +3. The text "SpongeBob" is written below the figure in a bold Courier font. +4. The window stays open until you close it manually. + +## How to run on localhost ``` python main.py @@ -11,7 +18,3 @@ python main.py ## Dependencies Standard library only (uses the `turtle` module). - -## Pyodide-runnable - -No — it uses the `turtle` module, which needs a Tkinter-backed desktop window. diff --git a/projects/student-management-system/README.md b/projects/student-management-system/README.md index 861cee9f1..dfb584541 100644 --- a/projects/student-management-system/README.md +++ b/projects/student-management-system/README.md @@ -3,6 +3,45 @@ For every school important task for administration department is to manage student information in a procedure oriented manner with latest updates for every year which need to be available for easy access. This can be provided by a simple Students Management system to help administration so to efficiently manage student’s details. To store data MySQL is used by connecting MySQL with Python using MySQL Connector. There is also a Login Panel where now admin can register themselves and can Login. +## Example + +```text +------------------------------------- + Welcome to Student Management System +------------------------------------- +1: Login +2: Register +Enter your choice: 2 +------------------------- +--------Register--------- +------------------------- +Enter your Id: admin1 +Enter your Password: secret +Registered successfully! +--- login --- +Enter the id: admin1 +Enter the password: secret +------------------------------------- +--Welcome admin1 what you want to do!-- +------------------------------------- +1: Add New Student +2: View Students +3: Search Student +4: Update Student +5: Delete Student +6: Exit +Enter your choice: 1 +------------------------- +Add Student Information +------------------------- +Enter the sch no. of the student: 101 +Enter the name of the student: Alice +Enter the age of the student: 17 +Enter the email of the student: alice@school.edu +Enter the phone no. of the student: 9876543210 +Data saved successfully! +``` + ## Installation ##### 1. Install Mysql Python connector @@ -58,9 +97,3 @@ To deploy this project just run .py files. ## Author - [@siddharth9300](https://github.com/siddharth9300) - - - -## Pyodide-runnable - -No — both scripts connect to a MySQL database via `mysql.connector`, which requires a live database server unavailable in Pyodide. diff --git a/projects/takeImage/README.md b/projects/takeImage/README.md index 9c9e93c27..4e4d4895c 100644 --- a/projects/takeImage/README.md +++ b/projects/takeImage/README.md @@ -7,6 +7,13 @@ The default path would be your current directory. You can also give name to your image using following command: > python3 take_pictures_from_webcam.py --name ImageName -## Pyodide-runnable +## Example -No — it captures frames from a webcam with OpenCV (`cv2.VideoCapture`) and shows GUI windows, neither of which works in the browser sandbox. +```text +python3 WebcamImage.py --directory /home/user/photos --name photo +``` + +1. A live webcam preview window labelled "frame" opens at 600×600 pixels. +2. Press **Space** to capture the current frame — it is saved as `photo0.jpg` in `/home/user/photos` and the terminal prints `got photo0`. +3. Press **Space** again to save `photo1.jpg`, and so on. +4. Press **q** to close the window and exit. diff --git a/projects/text-translate/README.md b/projects/text-translate/README.md index 8cde428c8..711c4909c 100644 --- a/projects/text-translate/README.md +++ b/projects/text-translate/README.md @@ -1,9 +1,23 @@ +## Example + +```text +Enter the text to translate (or 'exit' to quit): Hello, how are you? +Available languages: +1) En +2) Es +3) Pt +4) Zh +5) Fr +... +Select a target language (1-20): 5 +Translated text (fr): Bonjour comment allez-vous? +Enter the text to translate (or 'exit' to quit): exit +Exiting the translator. +``` + ## Install these necessary libraries ```pip install pygame``` ```pip install gTTS``` ```pip install tkinter``` ```pip install googletrans==4.0.0-rc1``` -## Pyodide-runnable - -No — `main.py` uses the `translate` package which makes network requests, and `translate _GUIaudio.py` additionally uses `tkinter`, `gtts`, and `pygame`. diff --git a/projects/url_shortener/README.md b/projects/url_shortener/README.md index 64e8801b8..baaddbbdb 100644 --- a/projects/url_shortener/README.md +++ b/projects/url_shortener/README.md @@ -31,6 +31,3 @@ I built code quality tools into project (linting, formatting, type annotation ch Author: [justinjohnson-dev](https://github.com/justinjohnson-dev) Last Update: 05/25/2024 -## Pyodide-runnable - -No — it uses `pyshorteners`/`requests` to call a live URL-shortening API and depends on `pywin32`. diff --git a/projects/video_transcoder_project/README.md b/projects/video_transcoder_project/README.md index da616192a..d57db6a9e 100644 --- a/projects/video_transcoder_project/README.md +++ b/projects/video_transcoder_project/README.md @@ -1,4 +1,12 @@ +## Example + +1. Start the Flask app (`python app.py`) and open `http://127.0.0.1:5000/` in a browser. +2. The page shows a form to upload a raw video file. +3. Select a video (e.g. `clip.avi`) and submit the form. +4. The backend transcodes the video to the target format (e.g. 1080p MP4) using FFmpeg. +5. A download link for the transcoded file is shown on the page. + ## Components ### Flask App (`flask_app/`) @@ -45,7 +53,3 @@ This is a basic implementation; additional features like error handling, security measures, and frontend enhancements can be added for a more robust application. Feel free to modify the code and folder structure according to your project requirements. - -## Pyodide-runnable - -No — it is a Flask web server that transcodes videos with FFmpeg. diff --git a/projects/web-crawler(movie extract)/README.md b/projects/web-crawler(movie extract)/README.md index ffc05a8a4..a5cd2a038 100644 --- a/projects/web-crawler(movie extract)/README.md +++ b/projects/web-crawler(movie extract)/README.md @@ -2,7 +2,20 @@ A web-scraping script intended to crawl Rotten Tomatoes' top-movies list, extract movie URLs, names, and synopses, and save them into an `.xls` spreadsheet. -## How to run +## Example + +When the script runs successfully, it fetches the Rotten Tomatoes top-movies list and prints each entry to the console, then saves results to `movies_top100.xls`: + +```text +1 https://www.rottentomatoes.com/m/some_movie + Movie: Some Great Film +Movie info: A gripping story about... +2 https://www.rottentomatoes.com/m/another_movie + Movie: Another Classic +Movie info: An epic tale of... +``` + +## How to run on localhost ```bash pip install requests lxml beautifulsoup4 xlwt @@ -17,7 +30,3 @@ Note: the source file currently contains syntax errors and would need fixing bef - lxml - beautifulsoup4 - xlwt - -## Pyodide-runnable - -No — it uses `requests` to scrape live websites, which is not available in Pyodide. diff --git a/projects/what-for-dinner/README.md b/projects/what-for-dinner/README.md index 58f48adae..bac46469f 100644 --- a/projects/what-for-dinner/README.md +++ b/projects/what-for-dinner/README.md @@ -2,7 +2,20 @@ A console app that suggests a random meal for dinner. It fetches a random recipe from TheMealDB API and prints the meal name, origin, category, cooking instructions, and a YouTube link in a colorized format. -## How to run +## Example + +```text +------------------------------------------------------------- +Let's have a Beef Rendang for dinner! +This menu is Malaysian and it is Beef! +You can follow this link: https://www.youtube.com/watch?v=... or the instructions to cook it: +Combine the spice paste with coconut milk and beef... +------------------------------------------------------------- +``` + +A different random meal is suggested on every run. + +## How to run on localhost ```bash pip install requests @@ -12,7 +25,3 @@ python main.py ## Dependencies - requests - -## Pyodide-runnable - -No — it uses `requests` to call the live TheMealDB API, which is not available in Pyodide. diff --git a/projects/xls_to_xlsx/README.md b/projects/xls_to_xlsx/README.md index 0cc47b2ea..949c845be 100644 --- a/projects/xls_to_xlsx/README.md +++ b/projects/xls_to_xlsx/README.md @@ -2,7 +2,6 @@ ![pywin32 Version](https://img.shields.io/pypi/v/pywin32?label=pywin32) [![View My Profile](https://img.shields.io/badge/View-My_Profile-green?logo=GitHub)](https://github.com/odhyp) - # XLS TO XLSX
    @@ -15,12 +14,26 @@
    ## Description + A simple Python script that converts Microsoft Excel '.xls' file to '.xlsx' file. ## Languages or Frameworks Used + This script requires [Python 3](https://www.python.org/downloads/) and [pywin32](https://pypi.org/project/pywin32/). -## How to run +## Example + +```text +Input the '.xls' file path: +C:\Users\user\Documents\report.xls +Successfully converts report.xls +Do you want to delete the old report.xls file (y/n)? y +Successfully removes report.xls +``` + +The converted file `report.xlsx` is saved in the same directory as the input file. + +## How to run on localhost 1. Ensure you have [Python 3](https://www.python.org/downloads/) installed on your system. 2. Navigate to this project directory or where `xls_to_xlsx.py` is saved. @@ -42,8 +55,5 @@ python xls_to_xlsx.py 8. Input either 'y' to delete the old '.xls' file or 'n' to keep it instead. ## Author -[odhy](https://github.com/odhyp) -## Pyodide-runnable - -No — it depends on `pywin32` and automates the desktop Microsoft Excel application. +[odhy](https://github.com/odhyp) diff --git a/web/public/playground/maths.py b/web/public/playground/maths.py index d7067560c..e1d4ca106 100644 --- a/web/public/playground/maths.py +++ b/web/public/playground/maths.py @@ -3,15 +3,6 @@ from cmath import sqrt - -# Print a welcome banner -print("----------------") -print("created by: ") -print("https://github.com/ChefYeshpal") -print("for: hacktober") -print("----------------") - - # Basic arithmetic helper functions def add(x, y): return x + y From dcaccb0a822f422d1858e7eb9fa4c669f9d63329 Mon Sep 17 00:00:00 2001 From: Mrinank-Bhowmick <77621953+Mrinank-Bhowmick@users.noreply.github.com> Date: Fri, 22 May 2026 13:40:09 +0530 Subject: [PATCH 12/14] playground: LeetCode-style layout, collapsible sidebar, thin scrollbars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework the playground workspace into a LeetCode-style split: the README is now a full-height pane on the left, with the editor stacked over the terminal on the right. Vertical drag handle resizes README vs editor column; horizontal handle resizes editor vs terminal. - README pane collapses to a slim rail; project sidebar collapses to a rail too (state persisted to localStorage). - Thin, subtle scrollbars site-wide (scrollbar-width + ::-webkit-scrollbar). - Fix double border on the README body — it shared the .s-readme class, whose card chrome (border/shadow/radius) drew a second box inside the pane. Co-Authored-By: Claude Opus 4.7 --- web/src/app/globals.css | 181 +++++++++++------ web/src/components/Playground.tsx | 243 +++++++++++------------ web/src/components/PlaygroundSidebar.tsx | 46 ++++- 3 files changed, 279 insertions(+), 191 deletions(-) diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 4fa13c3b7..2b2bd5aa7 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -32,6 +32,25 @@ * { box-sizing: border-box; } html, body { margin: 0; padding: 0; } + +/* ---- Thin, subtle scrollbars site-wide ---- */ +* { + scrollbar-width: thin; + scrollbar-color: rgba(29, 24, 48, 0.3) transparent; +} +::-webkit-scrollbar { width: 9px; height: 9px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: rgba(29, 24, 48, 0.26); + border-radius: 99px; + border: 2.5px solid transparent; + background-clip: padding-box; +} +::-webkit-scrollbar-thumb:hover { + background: rgba(29, 24, 48, 0.46); + background-clip: padding-box; +} +::-webkit-scrollbar-corner { background: transparent; } body { background: var(--s-bg); background-image: @@ -674,9 +693,41 @@ a.s-contrib { display: block; text-decoration: none; color: inherit; } display: flex; flex-direction: column; background: rgba(255, 255, 255, 0.45); } -.pg-side-head { padding: 18px 18px 12px; } +.pg-side-head { + padding: 18px 18px 12px; + display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; +} .pg-side-title { font-family: var(--pg-display); font-size: 22px; font-weight: 900; letter-spacing: -0.02em; line-height: 1; } .pg-side-sub { font-size: 12px; color: var(--pg-muted); margin-top: 4px; } +.pg-side-collapse { + flex-shrink: 0; width: 30px; height: 30px; border-radius: 9px; + display: grid; place-items: center; + background: var(--pg-paper); color: var(--pg-ink); + border: var(--pg-border-thin); box-shadow: 2px 2px 0 var(--pg-ink); + transition: transform .1s, box-shadow .1s; +} +.pg-side-collapse:hover { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--pg-ink); } +.pg-side-collapse:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--pg-ink); } + +/* collapsed sidebar → slim rail */ +.pg-side.railed { + width: 50px; flex-shrink: 0; + align-items: center; gap: 16px; padding-top: 16px; +} +.pg-side-expand { + flex-shrink: 0; width: 34px; height: 34px; border-radius: 10px; + display: grid; place-items: center; + background: var(--pg-accent); color: var(--pg-ink); + border: var(--pg-border-thin); box-shadow: 2px 2px 0 var(--pg-ink); + transition: transform .1s, box-shadow .1s; +} +.pg-side-expand:hover { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--pg-ink); } +.pg-side-expand:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--pg-ink); } +.pg-side-rail-label { + writing-mode: vertical-rl; + font-family: var(--pg-mono); font-size: 11px; font-weight: 800; + letter-spacing: 0.16em; text-transform: uppercase; color: var(--pg-muted); +} .pg-side-search { margin: 0 14px 12px; padding: 10px 14px; border-radius: var(--pg-radius-pill); @@ -795,9 +846,16 @@ a.s-contrib { display: block; text-decoration: none; color: inherit; } .pg-btn.stop { background: #ffb4a8; } .pg-btn .ic { font-size: 13px; } -/* editor + console split */ +/* workspace split — README pane | editor-over-console column */ .pg-split { flex: 1; display: flex; gap: 0; min-height: 0; } .pg-split.row { flex-direction: row; } + +/* right column: editor stacked over console */ +.pg-right-col { + flex: 60 1 0; min-width: 0; min-height: 0; + display: flex; flex-direction: column; +} + .pg-window { display: flex; flex-direction: column; min-height: 0; background: var(--pg-dark); @@ -830,14 +888,14 @@ a.s-contrib { display: block; text-decoration: none; color: inherit; } .pg-winbar .iconbtn:hover { background: rgba(29,24,48,0.1); border-color: var(--pg-ink); } /* editor pane */ -.pg-editor { flex: var(--pg-ed-grow, 1.55) 1 0; min-width: 0; } +.pg-editor { flex: var(--pg-ed-grow, 58) 1 0; min-width: 0; min-height: 96px; } .pg-editor-body { flex: 1; min-height: 0; min-width: 0; display: flex; } .pg-editor-body > * { flex: 1; min-width: 0; height: 100%; } .pg-editor-body .cm-editor { height: 100%; } .pg-editor-body .cm-scroller { font-size: 13.5px; padding: 6px 0; } /* console pane */ -.pg-console { flex: var(--pg-co-grow, 1) 1 0; min-width: 280px; } +.pg-console { flex: var(--pg-co-grow, 42) 1 0; min-width: 0; min-height: 96px; } .pg-console-body { flex: 1; min-height: 0; display: flex; background: var(--pg-dark); @@ -851,8 +909,12 @@ a.s-contrib { display: block; text-decoration: none; color: inherit; } /* responsive */ @media (max-width: 1200px) { .pg-split.row { flex-direction: column; gap: 14px; } - .pg-split.row .pg-editor { flex: 1.4 1 0; } - .pg-split.row .pg-console { flex: 1 1 0; min-height: 200px; min-width: 0; } + .pg-readme-pane { min-width: 0; min-height: 200px; max-height: 40vh; } + .pg-readme-rail { + width: 100%; flex-direction: row; align-self: auto; + margin-right: 0; padding: 10px 14px; + } + .pg-readme-rail-label { writing-mode: horizontal-tb; } .pg-resize-split { display: none; } } @media (max-width: 1100px) { .pg-side { width: 240px; } } @@ -860,6 +922,11 @@ a.s-contrib { display: block; text-decoration: none; color: inherit; } .pg-app { height: auto; min-height: 100vh; } .pg-main { flex-direction: column; } .pg-side { width: 100%; } + .pg-side.railed { + width: 100%; flex-direction: row; align-items: center; + gap: 12px; padding: 12px 16px; + } + .pg-side.railed .pg-side-rail-label { writing-mode: horizontal-tb; } .pg-side-resize { display: none; } .pg-nav-mid { display: none; } .pg-split.row { min-height: 70vh; } @@ -991,59 +1058,44 @@ a.s-contrib { display: block; text-decoration: none; color: inherit; } text-transform: uppercase; color: rgba(29, 24, 48, 0.55); } -/* ---- Playground: collapsible README strip (above the editor) ---- */ -.pg-readme-bar { - flex-shrink: 0; - border: var(--pg-border); border-radius: var(--pg-radius); - background: var(--pg-paper); box-shadow: var(--pg-shadow); - overflow: hidden; -} -.pg-readme-toggle { - display: flex; align-items: center; gap: 8px; width: 100%; - padding: 10px 18px; cursor: pointer; - background: var(--pg-paper-warm); border: 0; - font-family: var(--s-display, var(--pg-mono)); - font-size: 13px; font-weight: 800; color: var(--pg-ink); - text-align: left; -} -.pg-readme-bar.open .pg-readme-toggle { - border-bottom: var(--pg-border-thin); -} -.pg-readme-emoji { color: var(--pg-accent); } -.pg-readme-label { letter-spacing: -0.01em; } -.pg-readme-hint { - font-family: var(--pg-mono); font-size: 11px; font-weight: 600; - color: var(--pg-muted); +/* ---- Playground: README pane (left side of the workspace) ---- */ +.pg-readme-pane { + flex: 38 1 0; min-width: 240px; + background: var(--pg-paper); } -/* The clearly-visible collapse / expand button at the end of the header */ -.pg-readme-btn { - margin-left: auto; - display: inline-flex; align-items: center; gap: 5px; - padding: 5px 11px 5px 8px; border-radius: var(--pg-radius-pill); - border: var(--pg-border-thin); background: var(--pg-paper); - box-shadow: 2px 2px 0 var(--pg-ink); - font-family: var(--pg-mono); font-size: 11px; font-weight: 800; - letter-spacing: 0.02em; text-transform: uppercase; - color: var(--pg-ink); +.pg-readme-pane .pg-winbar { background: var(--pg-paper-yellow); } +/* shares the .s-readme class for typography — strip its card chrome so it + doesn't draw a second box inside the pane window */ +.pg-readme-body { + flex: 1; min-height: 0; overflow: auto; + padding: 22px 26px; background: var(--pg-paper); + border: 0; border-radius: 0; box-shadow: none; margin: 0; +} +.pg-readme-body > :first-child { margin-top: 0; } +.pg-readme-body h1 { font-size: 22px; } +.pg-readme-body h2 { font-size: 18px; } +.pg-readme-body h3 { font-size: 15px; } + +/* collapsed README → slim clickable rail */ +.pg-readme-rail { + flex-shrink: 0; width: 46px; align-self: stretch; + margin-right: 14px; + display: flex; flex-direction: column; align-items: center; gap: 14px; + padding: 16px 0; cursor: pointer; + background: var(--pg-paper); color: var(--pg-ink); + border: var(--pg-border); border-radius: var(--pg-radius); + box-shadow: var(--pg-shadow); transition: transform .1s, box-shadow .1s; } -.pg-readme-toggle:hover .pg-readme-btn { - transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--pg-ink); -} -.pg-readme-toggle:active .pg-readme-btn { - transform: translate(2px, 2px); box-shadow: 0 0 0 var(--pg-ink); +.pg-readme-rail:hover { + transform: translate(-1px, -1px); box-shadow: 6px 6px 0 var(--pg-ink); } -.pg-readme-bar.open .pg-readme-btn { background: var(--pg-accent); } -.pg-readme-content { - margin: 0; border: 0; border-radius: 0; box-shadow: none; - background: var(--pg-paper); - max-height: 23vh; overflow: auto; - padding: 18px 26px; +.pg-readme-rail > svg { color: var(--pg-accent); flex-shrink: 0; } +.pg-readme-rail-label { + writing-mode: vertical-rl; + font-family: var(--pg-mono); font-size: 11px; font-weight: 800; + letter-spacing: 0.16em; text-transform: uppercase; } -.pg-readme-content > :first-child { margin-top: 0; } -.pg-readme-content h1 { font-size: 22px; } -.pg-readme-content h2 { font-size: 18px; } -.pg-readme-content h3 { font-size: 15px; } /* ---- Playground: draggable resize handles ---- */ .pg-resize { @@ -1066,7 +1118,7 @@ a.s-contrib { display: block; text-decoration: none; color: inherit; } opacity: 1; background: var(--pg-accent); } -/* between editor and console */ +/* between README pane and the editor column */ .pg-resize-split { width: 16px; align-self: stretch; display: flex; align-items: center; justify-content: center; @@ -1075,6 +1127,15 @@ a.s-contrib { display: block; text-decoration: none; color: inherit; } .pg-resize-split:hover .pg-resize-grip, .pg-resize-split.dragging .pg-resize-grip { height: 60px; } +/* between editor and console (stacked) */ +.pg-resize-vert { + height: 16px; width: 100%; + display: flex; align-items: center; justify-content: center; +} +.pg-resize-vert .pg-resize-grip { width: 40px; height: 4px; } +.pg-resize-vert:hover .pg-resize-grip, +.pg-resize-vert.dragging .pg-resize-grip { width: 60px; } + /* sidebar right edge */ .pg-side-resize { position: absolute; top: 0; right: -7px; width: 14px; height: 100%; @@ -1084,13 +1145,3 @@ a.s-contrib { display: block; text-decoration: none; color: inherit; } .pg-side-resize:hover .pg-resize-grip, .pg-side-resize.dragging .pg-resize-grip { height: 72px; } -/* under the README panel */ -.pg-resize-readme { - height: 13px; width: 100%; - display: flex; align-items: center; justify-content: center; - background: var(--pg-paper-warm); - border-top: var(--pg-border-thin); -} -.pg-resize-readme .pg-resize-grip { width: 46px; height: 4px; opacity: 0.32; } -.pg-resize-readme:hover .pg-resize-grip, -.pg-resize-readme.dragging .pg-resize-grip { width: 70px; } diff --git a/web/src/components/Playground.tsx b/web/src/components/Playground.tsx index c3d1770f9..e9b322c2b 100644 --- a/web/src/components/Playground.tsx +++ b/web/src/components/Playground.tsx @@ -25,8 +25,7 @@ import { Square, RotateCcw, BookOpen, - ChevronDown, - ChevronRight, + PanelLeftClose, Check, Copy, Eraser, @@ -115,13 +114,13 @@ export default function Playground({ const [code, setCode] = useState(initialCode); const [status, setStatus] = useState("idle"); const [copied, setCopied] = useState(false); - const [showReadme, setShowReadme] = useState(true); + const [readmeOpen, setReadmeOpen] = useState(true); - // Resizable layout — editor/console split ratio and README panel height. + // Resizable layout — README pane width (%) and editor/console height (%). + const [readmePct, setReadmePct] = useState(38); const [editorPct, setEditorPct] = useState(58); - const [readmeHeight, setReadmeHeight] = useState(null); const splitRef = useRef(null); - const readmeBodyRef = useRef(null); + const rightColRef = useRef(null); const workerRef = useRef(null); const sabRef = useRef(null); @@ -405,19 +404,18 @@ export default function Playground({ }); }; - // Drag the bar between editor and console — convert px delta to a percentage - // of the split container so the two panes keep filling the available width. - const resizeSplit = useCallback((dx: number) => { + // Drag the vertical bar between the README pane and the editor column — + // px delta as a percentage of the whole workspace width. + const resizeReadme = useCallback((dx: number) => { const w = splitRef.current?.clientWidth ?? 1; - setEditorPct((p) => clamp(p + (dx / w) * 100, 24, 80)); + setReadmePct((p) => clamp(p + (dx / w) * 100, 18, 62)); }, []); - // Drag the bar under the README to set its panel height. - const resizeReadme = useCallback((dy: number) => { - setReadmeHeight((h) => { - const cur = h ?? readmeBodyRef.current?.offsetHeight ?? 240; - return clamp(cur + dy, 90, 560); - }); + // Drag the horizontal bar between editor and console — px delta as a + // percentage of the right column's height. + const resizeEditor = useCallback((dy: number) => { + const h = rightColRef.current?.clientHeight ?? 1; + setEditorPct((p) => clamp(p + (dy / h) * 100, 20, 82)); }, []); const lineCount = code.split("\n").length; @@ -481,122 +479,123 @@ export default function Playground({ - {/* README — collapsible, resizable strip above the editor */} - {readmeHtml && ( -
    - - {showReadme && ( + {/* workspace — README on the left, editor stacked over console */} +
    + {/* README — collapsible left pane */} + {readmeHtml && + (readmeOpen ? ( <> -
    +
    +
    + +
    + README· what to build +
    +
    + +
    +
    +
    - )} -
    - )} - - {/* editor + console */} -
    - {/* editor */} -
    -
    - -
    - {project.id}.py· Python 3.x -
    -
    - {lineCount} lines - -
    -
    - -
    -
    - - {/* drag bar between editor and console */} - - - {/* console — real terminal */} -
    -
    - -
    - Console· terminal -
    -
    + ) : ( + ))} + + {/* editor + console, stacked vertically */} +
    + {/* editor */} +
    +
    + +
    + {project.id}.py· Python 3.x +
    +
    + {lineCount} lines + +
    +
    + +
    -
    -
    + + {/* drag bar between editor and console */} + + + {/* console — real terminal */} +
    +
    + +
    + Console· terminal +
    +
    + +
    +
    +
    +
    diff --git a/web/src/components/PlaygroundSidebar.tsx b/web/src/components/PlaygroundSidebar.tsx index d0ee02f87..b55f4ec7a 100644 --- a/web/src/components/PlaygroundSidebar.tsx +++ b/web/src/components/PlaygroundSidebar.tsx @@ -9,7 +9,7 @@ import { useEffect, useState, type CSSProperties } from "react"; import Link from "next/link"; -import { Search, X } from "lucide-react"; +import { Search, X, PanelLeftClose, PanelLeftOpen } from "lucide-react"; import { CATALOG } from "@/lib/catalog"; import ResizeHandle from "./ResizeHandle"; @@ -86,6 +86,8 @@ export default function PlaygroundSidebar({ const [collapsed, setCollapsed] = useState>({ desktop: true, }); + // Whole-sidebar collapse — folds the panel down to a slim rail. + const [railed, setRailed] = useState(false); // User-draggable sidebar width — resets to the default on every refresh. const [width, setWidth] = useState(280); @@ -93,11 +95,37 @@ export default function PlaygroundSidebar({ try { const saved = localStorage.getItem("pg_collapsed_v2"); if (saved) setCollapsed(JSON.parse(saved)); + setRailed(localStorage.getItem("pg_side_railed") === "1"); } catch { /* ignore */ } }, []); + const setRail = (v: boolean) => { + setRailed(v); + try { + localStorage.setItem("pg_side_railed", v ? "1" : "0"); + } catch { + /* ignore */ + } + }; + + if (railed) { + return ( + + ); + } + const toggle = (gid: string) => setCollapsed((prev) => { const next = { ...prev, [gid]: !prev[gid] }; @@ -118,10 +146,20 @@ export default function PlaygroundSidebar({ style={{ ["--pg-side-w"]: `${width}px` } as CSSProperties} >
    -
    Playground
    -
    - {CATALOG.length} projects · {RUNNABLE_ITEMS.length} runnable +
    +
    Playground
    +
    + {CATALOG.length} projects · {RUNNABLE_ITEMS.length} runnable +
    +
    diff --git a/web/src/app/terms/page.tsx b/web/src/app/terms/page.tsx index 7c8b33e29..ecb0a92c7 100644 --- a/web/src/app/terms/page.tsx +++ b/web/src/app/terms/page.tsx @@ -20,11 +20,6 @@ export default function TermsPage() { learning, education, and portfolio demonstration. They are not a commercial service, professional product, or managed platform.

    -

    - These terms are intended to reduce legal risk, not to prevent anyone - from using rights that cannot be waived under applicable law. -

    -

    No Professional Advice

    The content is for general educational and demonstration purposes only. @@ -97,14 +92,6 @@ export default function TermsPage() { any activity that violates the rights of others.

    -

    India

    -

    - If Indian law applies, these terms should be read subject to rights and - liabilities that cannot legally be excluded. Nothing here is intended - to absolutely restrict a person from approaching a competent court or - authority. -

    -

    Changes

    I may update, change, remove, or stop maintaining any part of this diff --git a/web/src/components/SiteFooter.tsx b/web/src/components/SiteFooter.tsx index 0d83031a3..f36e826c3 100644 --- a/web/src/components/SiteFooter.tsx +++ b/web/src/components/SiteFooter.tsx @@ -5,7 +5,7 @@ export default function SiteFooter() {

    Built with & the standard library · - MIT licensed · Educational project · No warranty + MIT licensed

l7~;h*rI_V6Yew{T8*TeR?LqRjNtx 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%0D3N-L#~(3h(<4I6OPuiQz|sIL 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 (wMKA{U0ee<1bu7xu^HTj{4vN3_Yh-pmdEn>} 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

$Tby^gqGx5VTV*wotdvnH2u(nS3_3UGf!X4{uyt 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

s?Q{Dd@XU6i4C5iR`LO&GNC6Ksq#; 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

%EOj!8)(`)5(5dv~`uL>4`L ztFHS;AnP3F@?Jt54ba9Z0Xu|7>&@`Oh8BkaC2zLupEKI%q%u*`?c(I})pgyk739E3 z=gJoTt4vP0AYxD~ukUv}pDN|aqaVL_`Eu%5(*H(ge!n=naOdZs96uLN?AIbTSNH~d zZrNS^V}y*9f|cD#fx1XRHR8kPc92TV#rL;Yz3_;$8_4oW-R229O|5-!met_0+~lHV zdY^kjg>>_}sNR>$xpPJPh^w!YBj-0}&wbV>5sQBjnlaU~)W39^Mv>q42ZoL-8+Hpi zXJrGj)LFhu(El-i{^2BfNzisHx1i#rvF~~pp1pJ^#^V(gpKGHPvCz9NA!fas=jc^A zq@ry}jl3ziypFG(j2>7g{=U<4adVj6W^0mFWuIkNvk#$5tNPIQC#XN#;=M}tcC--7 z?AKqBUe$&UZCUwy3DE<(-g{x}Zj-Gkec=$plQ|kq3NOD!70lKnA zVy-_;+7Qjjg`!^|+!vpMZA%qco7yTyzWuefv1(5D9}HTa9If%;>_e7$k@mGHS=S`P_b@`Ji}Aod?eU%bbP!E`Pct( zUrXlt&;8TaKp66-_ z87T9~BdS8FZPH^i3EixbDgZi;OL*>l-i^>~TZ_@{+9mUnuspwAkK@}jsPeq#SsUc`YaT{ z3UGogTDz=`@494Q1>_HdzRF}sQO`3Pjt(yWVV3qac4U&wa~U!B&7D&B2QlQb$m8?I zy%(1Kmj0MQY_Tb>zx*R=_BydUF)bS*@5*cfp8b-<57uJ3&!j10zMrG}@U1U2=)>O` z=D4<%D9`EOm@qQr!3)CC?XQnGmrUob%p76hmAq#EYbuS&N1^~WwJp~6#9h?yQ8kWU z!o6qjJY&_Q2WK4WzhwqweR9!U7pEl+!j*1@ovrrt=y|#TJ+l8^1WmJPRcl-!I=^`| z{VK@9qdhOv(76ZgiVo`pebLPz4aiszf4CQvIE_mVD%uZtkL$y}Rm5HIosqod-6DnS z=_T@~Hok0Tah;TQ%PS`(WWIZPjG*K2Aml;3GxXtlT;`F24o*q?7I8@<123CcpLKtb zI7L5U#zZ<)gjh&&|8fqz{tG8e?jpYPuD`<*Fx~L9hzUcx_Bokh><_z39MctN&6Z+Z zj(doPKf3StviToLLA-s67Y^1Vp%2!<9ka;3mEPWtHqlL~p)btav9-ZMrAOV_Z2S+R zyKaY&v##KYzl|RZ=A_7#H|Nb`F-O^#r(P+S@kz}uht&F6S6zYr@Sz}sfl#1Q0v=0q z=Pq_7pgJv6lgpRt%pZ~uxO-qbevAU*q5TXl2Z?p_V;)rlRKq)c(f(AAm<2fcpOp}p z^#_O7zgzr~q9q%O;f^D^E*+&8qQKm~p-c2ktY1r=D#Q-4AxK&OY3yWPHl=GeP=enW z)Df``fO(mgAfr<EO;q5L-KyZ%ncYc5n+;dxQU%O$NQYeGp0TZKMLxR> zai^~?JD1NJ_$Jf7mn8zhj_eISb133N#nF=OOnSsRMD#0ITT1_-g(d2Q|7Dk2q1QtW zaWs1k=oPe}_1KQ`+Nu`NW0%HPydWB(pLUeI@7arE{g7B~fDH*}F8{z?Uc}x@SXgG# zPBXSn$_Rd)SuH`!1U;E|XABB0-T`NZ*h{RtSwzN33U&Mwr^;Nafc@JiNpXd=-|PezssA z();|h=}!VBWGeua9MTGc&msI{Q;wjyEO$NYJ6_0z@#vnQuZqZw_MuF(4tzF3YgYbg zBMABDTFF_2S`9l=AERr2@VrJYEK9+ng5GCOYp}*+Is2hXSJ5h<0?uFu3@H_zLBaCR zgjY7zzCAryU0wMlYRXS6GJ{#xne{Q2vokk2wH^!J#GBN!|2n`dkrk=>Ml@ysa~Z9v z&&xCofcN;=CJjJDR$+RgLNm8>ilJ*3D1aSM_^#AOljk_ea6%aJ3eW0wB!NmuozCdh z>pcndyNBC?yFm_kd`dBS>MV4HdI(p&pDS-{S+dO(0GG0KeiokE^lpZvJ09<-42g|z z<1?o}{- z_zo)iUEFIJW271~mslq64F{Hfr9sG~X3k1G1E5-`+g%^Yb>F&CQe(g?FuwKOYlopB z9vDAU0?s3M_Aa`56}e&^^!9!Cf)B{sS}vK+Ac{Rh@P-0MT5#TvIE(l4LfvDT??PE( zjqa6Iqzy_{xz=l#b0JhZ0vdO2lTwW+>p@LaS z);kl15zjuMEl)b|Vo}G>#B_X3(9-g`JMh?u4j@ulXg$(ISs&Aq7-h!(SotgsFd|zq zz(&2fs1;cN`}&tvz8Akqi8{Qtfp{wmvO161JN`AJzjx{5@YxY z0n`JO`pn^?LWg`l0jD{EKEG#Y=^HH(nMz1e6ULwtj-{p8q~sgw$+kujnb6Z^DnkFq zl-RKE4KY|p$e0Zv3YI$zNjL=)oMS6r(S z@TxKzE0_q9tzZ935uWhJoS^u)yo~RZi2obn{2bIN5E|w|Ad5@hzDe%(%nkj-{hk)^ zXJ%=5O&hfy%>vZmK`;Qn=;IHF3zsm0Sf+MHKd9o=gm3lZI>TRVdG;Td8URF9fhi#Y zm;_i>U)^B6pOsL^A4i*bEhU3}Y#H`Y>}|KIWu9GFcYN%7Qw~_Q1E4yt1LYOS$rEd) z-VYmHhm);U8T>wU2SDlEI^=PXH&dR3>{h_)v=|HEb7*vPHRDp|)y|0#I)D^iy@(v$ zs~;tc8T>peMQ$ZK%`}J1;Rz*gafU2xTr6Ozc6?Gv@4-h*hPRr3r_e1v5BvR{`1PI_ zYXIw5ru_Zc&Nx#pITOjqbxO zeT~J9#=W>$kGN_~?<@#LLc%el5#z4Us%MHk+f0N~4S(0R6WyZ{tI^z)?|5lo_tuC? za^VEb{w~C|jMluzO^;vemi6jK-&Mz9!q!(UiXs66vq-@0So@dnVa!mrIS~(wT(Qtk zz(m5W~expn2jgP&Ccsum4-EBamDobaK+lDqDQ83OWCp4hL|CMHlJ7c+t7a?f)NjUEO3 zu0N&`TkNS9w0V68O}BB-61Bs=V7ORubDRkRH$*|9>RJXmZ?;p0vpd{dQftU63*=Em zvATBh4p@j$e~|aHbQN0x(%?!rm){=Mi02zyr6D_*B;Z%@YwvujZuHrLgkeGQoSsm7 zPA+)BXR->EQpbzVi~_E1q10K@thgqFd(OZclhl~tR}MbxF~178OvxEH9AWFxr-ApY z_m)!|_cKA=db(%KqI`heq(7d@l{}%kS=G-&m9UO~xnf6J)+EO27e@CH@RUQB7SRYk zHAL$2JNiuIO|U`qm4v;7HgcA~LI) zobuNdm^5FjrA63`Qi{a0kLS+zm`Up+TjwGE5)^;D$FGNJ&my zcMQ*%8~*j%AR7;Do1HMk9rs3@-y>K23uHoXj@phPcPC^9*li|1h_lFPRjj5#ojiZY zkS6>i3LPRA$wnh?V$9i%LIT4-CUs*oAnTM}pX^f%sXk2k>QvSp@Cw#JQ$AVN))(PoQ?z-X40QK!c6^FxC_Jh5?;b zsYFsl&~?Q+KVbSUEg;~myTI%jy4gOkpiJR_k&$eTXW`R}V{GVcOqV9v*p=}kPJ}-2 z5es-g0?t!Gjl{C(CiUu!@ZdgJ+ZN>O`0dJ{?DMkJ{p;^|&HGwRGwCi&yuW10`}QpE z@S+dIKPJ9U{p1Y^EVpFEb*)m)0p^zZk{MQZV2~R4=(ivE-1<+3=~pQ~_qwf2HU?(? zgn-_fT&x21+vw)2FGoXfyQ2Dpt8xN|Kbhhqd#lf^W)|j(P**znj*opVvSlYeHxsOj zNY)@d-9g~FaFkN9V(@;NxUS~uZPWFG&2JHr_?Id;$26OAE#^q1Dx%)pFIxJ z`Id^c*_o$MY129L>l?m)f4lKXsg_IOw$(rAE-Iv3-6-$P5Cw&)DhFkOU7e`9Nmfu7 zHzd5v8&oCY*s2HpIn4B~V83H??ln3SWG*Pp)ina{Lsso3vwVL(eHiK45eSIwh|O$j zV&WyNTMUz5ZqycYU_YZezk_8TWS)b#_0+M=OS?fyM!ervS*twRwcy^G?bW67hb zK;$om3&&ndhT2Bkq#MxO%pstw`W){(^yXmU-tp>KHu&;juijD&3zKJ^=9iUjqQl*) zH{N9<_xiE^ktP)erbM&ZSTXKMm%n!8Y-3B{XB{Hn=cRmV79GljWC8Tgpj4Zc(3(W+ z=P0JI01U#YhPce}&XAjPL$XuW_NmjOsN7<&>V{oSM%GH)wt`R}A8O!oPbc5v^Bi5K z&CThwv^2r0$=5-0fEalc{{d_mVPi99$i3@VXfT)fxxL}djxm6bI6{pWW`RQmz(lY- zpafSu+_k^50w8V=B1i#(Vc+obzMyS0`qC+J8RVU{3rZ|@lY--o8!hVUIPGIWIW<^K z%h*4u>Q1@yEI-2fY))U4VRWOnwG?I~by(SkJ0!+^j#D=-hdvUiO+>+E)-#Dlq~S8R zIeYHSDER}FzR|?w_lAtBGr}x9ChiB@D&gnnd%Sn;*fPRfiT6qowLX@Uj_})aVeJP4 zWP`f-5~v7Ouud1Yb%rxu&8@C&PR;9f_3JT(00|Q0`iU7dsU(gxv6l6j9YwAe>IZyLRRs&n`vuBQ8fDA1ghry z>_ffZe7J`qhJ6bm^4AStJ{C>;7?Ue0vmr$ONG&}YP*Gv0=k~Oy=^7@mg zzn)(@qN_QV=S9QwRyA{Nh4^`Qt2s2yH62$dMGd7MBDn8tIAZYX7A(95p{Gls2X(Q`nCZ7`k$?@UgjBEEUe0i zi7l@}PXBm$`d=MM=44inXe1|%a`SYjxe5~9`u#~5FB()rsrbD`;H9KcWhI$$4w1hg z{S$XtW!Y&G)3`JBBmCrohcUn)`Byy4PEH{up z1Xr>_F_=)q6U3UL)koemN(bCkug)qXczE7P0xM&7XY5m~%EFb7EAB@OH@{hEx7a{F z*>8kJzJv;eqxb6I#v!@^CRMSlm6BRxV^DNLC9j&6sJlar{Lu5=-g?KdfL7F!cA|)zJC< z4+$6$!17hRO|c*3l&eS#*eChGZ(YY)t%6p8dlP!}G>xsp!v8u9>R!GvXZUsYssQWL zfL>TBEBI|7m7XW-M$wC(Y|KACkuD!c6GoINeME0564|k+RpbLzEP0z2p!tT&RCdP0 zU)?O1xCR+&0y9B=(L|8*&!>_)s9AZ2XQ+UOFLbKdpM(%W_J`vpVKIE^aTnQ9JV|wb z%CxTFGm1?MJ&trc!K_yqgI3>6E4+A-YL`{F9T0YgC zPC!bYjqZ;$aFUr%Bz0$aZrFRyqwz*$c;f+PJvTLsCJ@Acf$%EMRMJ|6xlA~V*OOdz zygN~D;68{_ui|5HFNbK9U*Ai>!NC|dj-^f)(M*B;9eX@ySqQ~Xm?b%t&Az@NcH-gM z^WT8Z&2KhA?NMd>Y?zpyoh`xc`#mk04i1I2 z?7gflldx1Qt6fkjgj!h%oKsgkozr?$GMk6Xbc7>(yXeA|LRonI%6vjGcYO9C#7|$S zmZZRQ=66k2f#;mr_|%osA@1@$0rdq!o%{~hFp1#Tq$buJd*ZznsCOgjlF{hUC=!Pp z(qij&VL;f8kHE|FzhTV;3%*oh0Q(o|%M`CBO-mH7veOK56ST+SHog~P&RbVp6NthTr zR}V0Mk;Nlm-YX7|M~u{~bfS#1Y=2m4KZtmUoj^YXd9k4JE=Z$Gp#k=B0U_ImUxatg z7xj{H$XzB>`yZU-YX*2{)6+ZeSa9WQxI}XxwE~CH-dGT630z^*!$;TB;4q5N0^5Qy zlJ15Z->-xC##gR4=0Pw`ELYQJYHEd77d+`ULQDd0Jht?d1HNULkps;F7XI${Mvt-Y zDV4iqT8KMS&0j#3n7PtEmoD1@1buKf_?qHfb$csqH<@d4a(%+Wrr4>=Bekd)!_%yB z<_RtcCcAutlV$t$1P!ms*sZF+@k5^UYFP ziQSvy>d1kW6QpfE%+U^wX8Mf5&u3to__o-s94JQDOF19vn0olsZ}N-Bj$4xOze;yq zN_x^R19u^Gnkik6_s46!O#^apL%28<%gIp#m(O(7k4YClVorhNo6~w6E1DPO^Y@ho zPW&$%vnOHmw;G{J!V_JbmpGtq9JtQ1A;o-jW+X2-?dq>Sjf~E1dW|$@Czs$^n{A?@ zWysz6La5D=Cw5T5&~0H<9qm0XQ&dRAJ0kKg?wiUeVjueU8(+(D`d*4Arp{ zlN&F2mrwm*<|Po-m{6<_A1C18C`}RyuNP$Mvp7LJpJhFwb(0M|Mj#@SFp>d671|G_ z3hM-&)rlV_$HdVPb`_W*(Uj-APOc6%K37)3!M8OJf31SAHt=o=!bggsFP}OXroD!a zepi&pPz4W1{(?n2`n0whQIefoQUNk&F{M4-Cp-v{&tJpN0Lx@Up~2E{SC zwyxxRzcjgj@||YXi^Dn?6JxHjC;z1Vv=A6t&0-5*!cXrJvm0Wdm9s$xSk>$SGtX~O zstgmVfo{MNUT#`a2kA=UEN+9dMU;F)K#sT-Las&^H%l1Y|E3mjM5ggPubWcRoCzK8 zIVDnJai)yx5O`JMz>{QsHdH;izhEkAfiE32fB)#ij+dQ>Hk+XHGWfbd?loRA2BzOd z=n^FXsFtWF*NqqTE{L!p$rd2Dg5LsPSYV)Wyv=4Nl~-= zK!Vw?tkqvRPmfDoMR$)q z9PCw)a+7q=)6-&pshW=<482Oh9ai@ zWvd07S)*8%z9?I{W1bQ292))lj3dKJASh%r8ia4QzGh>PZE96O=2`JOvj5`Q`H&)UN!bi zPLc!fZqQJgv5RTkg|#d&?jE`?yD#bPy6LFhlKnne&AD{<+NlB(ahh+_$ugTU?{@gU zp|GMEqO8RSwzHG%JT9gex)WzCgHu?X#M{X)q53G#Yhh8K#lqJpJ4X|2$3A0umPg1U z(lJ|PA)1H$Fr$y|Ynb>IWb65clwZAyZ$9z+xIV9{B>ml(5s)~#dQ4@1JlDi(R6k>b zB{=T>*mUDq^(IqXijLh%M80M`O-=2Y+56`ee?EB6sL!j-z^)L1U8x26Msk3rPkGEu zI7ST1gc#7gVRK(%BhPOdsF9h*ZW;c zOQEXcdp%rm5jA)W(_3)TK9EwNONW-z^G1q0#>KB2;Fb_9pc)!YxjOTjz-gF z&5>A3__<@&sbU9;73S2uI;VJF&JK6EeA5ri%H*kcu45r#O52=3KG{fvRii|J#|Ko2 zlCo(zpkc8$XypS~;jLk$j!K7J^3{(G zR)wHTq@(X==f1 zrk`JO*rEc|agcd&L?~VaX!$KIv3LEVg@~0n&5uH`Li8UPfExEndN3!P%pPw~LhiNt zM88ijJK5t+*!AW<7fQJoNt`ek$BKC>p@A0s7CN`HW4@%v8Xky)?f719s=APga;f`+ z@(_XJY?t?L<)1uK%XCU1gY7Mhy}MyhrF1YI=|T}h9P!P7P2}w7wa$3!fJug~1XS=< zsPXj_wNBUXlP|<+wcB|l2I!U2@DItk(Iu!Vi8?wk)h1r&j^qzfaFadG zP%^zZX`47R&X3>3zU#Q1IuX2-y*RMc+U8TG6RM_084(?habefxTCXWPAy2k>e4 zfGG@&-u}xpEm^aDIkINd4!OLS2EVk1M_)!MM4uU?zguNIyKtNX%`&rX?p^S%O=W)0 z0}g2bQA1LUAJ1k?PC=)0n@zLGm2aao;wQI9)3=?7kOJUsbTTSxWP!|WsF0E@Vb4WO zt>{a_6Nd#~?pRCw9Rx6)MSm$}vL^2{MNkGhqB5v}=@9rb`y)e<3N2SBgFN781QsX! zr9WcrloHVFpGRyQ^e3jE{g!r_fH=VOd(#ggseNuGOTlQ1q_>)k&>d}Hq#(q%Sn#bo z)gCU^0~bBwsDCnwNupH^5007h$dp;Q5VZxWMYHCSKtFgj^*)LP@OF|aU0TofD*t;eJ4(teIepU&a+kYq{U1{$wBNxbZ|t|1}#|C3IkE#f*q6(0xws+ z`54Nf$kbmU_*#|)sE$Lt|c-?9%H&5@%gmjrmQe%MGF<1gTT<@dl(XbYilpgLuw1Lf3AJu5e>5b-mu+h^CQx zZJ%@RhzJU$DS)091k8L0Fv3wC!A$cr3H`w=)<*c_#@7xI)@HEq;cqTD1ipDnvkTj% zX8YTHItmw}$o(^#M=bJ|J=1n;;QfbI6a%shK=TAW0j`V7GArcA9GxDpEN&?2qjS<9 zaZD617w_u9vSQ+v5B%_tjseB^+&UOCaHPVVylEQC);ks_k8|-YE{cyrzym;HTx4Y! z|4Dh-;uW86+IHOD+0qi|e3obKHRq;FL#dCz zG}Ig$ELLSS00*6p+da#&p=ZvFW2x2qf=#Mas;C-``xjib#yHqzb3T?4v1b3XCiiI@ z4^s2t-25`_0prV33|5c|)b$|mGBN}qAnxQ(*g34FGmRm*K+1-t+x&(W@T zF#^ed@DUUe)o9l|PQIw0BS|s0$Lt*P#_V@IkOo8P1fXf{GoKdb6(1ltY#+PZ zL_gmz^~qZe#vi?%TZPm%3`tvot?wJpq542Rmm}r(z4BXoKyKkw6l;fLbCk@Mp2h##{^Jtu`iU}d4X`b^X*Ph0d20pr% z=HAh_#eCxfO1d9Gbx>($C4YXOWPJbM*FhYeLF3$1N%5b?jPbj92UN@DSwyL+kAP*P=l}RO1Jc z5UMP?7w%QGToE(cqqLQcS}TRl)=Xb@4PbJ5Wzd8l>t16nL2@Xfx!s)B*vnR=_5Jg9 z7R5tni`^zAW0)EI?CPTfj^RAN=06z&Z`-#&9u^GE_n6FCiSmb!ed=5{pZJVY&9S8( z$%Qo5RPq>ZdL;{TfuOcD;;e99>~_S&EOPI=&*FaS=FeTr&+Vt(ehAQZ8|W5gRUJFud^2qavE8+mr;x; z`&jn}%}c2(Vx(9A0%~`G4Ie!7qKwkY=pF`Ub^({=K>>;GMgw0_@^~t66HH)+fuCM3 z8%kUSOXl8o(8NfGdAN_qbdG=QY<;)x2$8{NxN{u;suODYQ2^Ikl0~YRoW{e`$WM*x zPJ4={q;|G&5_^=zY4nR(EPVCl+QU(Lp@4Yq_wWgW`HGZ0ug zLhCcJg%MRj^W`enyq#ExJ29OIGG*x_QGNR!Z3n*q=+y)dt`)nSzr$D&aNp{xYW(Z- z_Tc(y7aN>>b~rA1v^@ySY+Z$9A67cN8D1*F+BgeS2NX)pK1;Myz5o@jh9c zX(qi-_$0ytP@4sfmu|aK;qqtK(i^Z%P9%NjD_}s&Ag*7kebLAr-)8c309qGzYZLBp z%KM4(hhgvIqW9S;z%3KsJu~(R5PJkg3-v~nSNhPS@r-A*?Y2L$9YaW4zbrhh)2$EY zqN}YhiJU2}dY~gdrpw5aiTNFIeo34L3n(W1b{CXce#=h>;rUR&5yLnAx3t?Jsz>if z`gz?nYNO#&4ojDcMx<3se&V9_;^gvHFC`4~6j~A?oEBT8R6njaw zu6s;j92Rw_)Wzi}U8A9Wcs=F%&hpaio}~dw*A?Ng(>7<(=DUSbOSUU1BO-&5EkD5p zK_!+Tkm-n|SwE!9&7*d;7WhHgBftl3xXo!kV|+RVomT&qz!~>hAGy3T~w&B0!|= zWDqZfuTLAW`2r#nj)VM}*EY>0{RypSG^@gd zjqd2^VXS}B0{5C~z}BgI@nIWue%jrzf{y|oVSGcFb zhmTs9rIIb`jBmEYcLao1Nz>%f2dU?pe)iQ7u!Hb9<;p^>Yaf-){ZD?8n<0yr^^tiA zCBpJ^JJ4@v<~Mm!DjHP`SFeG+=_1L1vu&m@I-?y*BH* z;+(1wZ+sgaSi5&zOt`dP+<1b+#*U?afN~aXw6^)>kMyQ5KvudLGxOUXc<5KzmtB{= zn+c_?N9^qd=;G;j-Y9nN%ARlA`Z{0wmCld)ip5PGw&3LbRc4;ca_6$2{_6T_+#{*` zy{6X^!-5;`_w@{SpobzEZ%O`OYx>vyw?r6gh2*cMi*qoR$W`!n944Qw(9X%InsmqU z>-VR+U$J^PvGX@&H}A+j_MDCA3972giUPp0paqy?ePa~_x>M*NsOft;Xj-Td#8j{5 zjrN=mda^3K2Xw?}5V}@dH)Mo->3jKF2ciF>F3l*iG|8t~r*;4{qYHc*ygnQm+ZJ04 zc|7HN{E#>PZp|ETr5S4~v+r045D;^@VE}hl}y>wYWcE zC-KSs!0GUKG$;-er!pJlLQiFtJZYu^J|1qiymaab3i1D>HW3iRyW6n$G#f}E9Iw8h zU(YV5qB+&Qz3~~5qwUB3GIrjuvJz{k9BiwN9k&_a;7Uj{UVy{WH+GwXD#R%NDzzhv(Y-8n;-mg>%(@P75sOm=cgLQw{B6Vs_sJcHyYjZsOw2V&oWZt{ z#46Fq%t*%b2k#Dz-0Os*+up~$SUwe@m!GZ8`XR;yf2==m+n+l`n>Du~-rgkGb>n&c z+=?Da1|C*WEiC2KVJ?^8hd<=UQ1E$A&5)HXK~f@q=Oh*L{NP-9+wY>WRrs=)6JGRn zM=M&!>D#&Th()QbZI)y9ar}PLnjG7}cfxtR$E=1n=l3hr7FZT#L2q$CJg=>4Xz=*U zHH1oq9UEXsp8^GNM?2;pZ!vzy&ho`FVCO2GjM_r4T{zokswLjST_Xd`Lu9 zX@@+!y!HO`Gny;YBJ9P6q1pH&7dw1l;_7~Mj?X-BGf7mN7B!rW-tZYY9vUqLMGi%< zbI=Vnnm*)`<3}XnvBwW$QDV^bR=pX;7T32mog9j@bU*77;|X+Lfz60~{p%c>FSKBH z20iQp37FxV%Xb4|Ecbsiep--k*29`InosH~q{H$gx-{=*NA=D@c+pkQNYQ+RZ^$ru z8U}{}y_Dpk2^y0U6p3BfhM!L|bGB5OrMwBVbT|YXY@*R-0bZ7&tbBkbhC$pEX*fO_ zf(rmAT1FXBc9Kf$xWs?z+$#$UQ}S$=hM41}cd5h88&1W3-ah_URa{IRnL&a)058X& z{q=^NwD-RkKhXoefVWFjv^Vn|(A5Fk?kZM2CdB9?#90zwT2tS%rH*|;d2r>FC%l9T zzl+>~qQk=WNx<2K(cN6_N%Hdv!cO00&YJn0O>wgR;0WD`)E+v2^SOMs0Y6$6&xJ?h zr<1`r!=u}!`l9k#C4}F1Ve{!m2U=01d7S%Q(APKpy=8VPgYJ|pnAW1r=DyO{@M;6S zp?B5>wnf9vIhey|u<`)V*BVR^E*meRE6tjuZF1z?o=E8P`BJMEx83i@>c;lgJIRTZ zmn)VMrgvL)`r`u#Z)2u?@2b5vw>-mN24sA)nY-uK{gA6z*#`uAX3`dFF)sc-z4b2{ zZiVWvYi?aw2K19OQETd1ZQ~a^BfRbY9F$`r>WOZJUd~Yp4esCO4Uj`)RtDqlsHax< zsiV^_6;p2)KJs%}an&V+Cqe5ZebxtCIOGQu%_rj4;^9KIFI!5LZjecRZou>3V>(UP z4E$(6iG3-#?dkyrui<}}8PfK8q@(=;Q$ow*UZ>jn=mnZMTAa6Ddi^B<^H2#br087O z+nb5cY%0qDyL7ojYk!Ff3k&q4#k}R^HEsTmt?>3Xo$B}POv~DUzudh=D-4rbQvqKJ zTYopRG_^~`N{lE%UYt%wV(E;*Xd`Nt7Hm6jR4XJliL8?ftZ>_vH=NWZ_6}5!6pNsL zbsR=85Z3UZ-oLZtH&>sK6LOV3lw)^tdnV84WAvi&i<;#`6tVsnS%vo%{QG39a)(g} zO~i$Zv2jJyf!>te|r9 z3rB%GdMc9r;DpCVu%DS-f>kakT9zECNC_@<9+<5_&x<6v$gT#{--?Mx>WB{M_F9VA zl;qmSxsVNn+bV%iMUxj|qY+AMzMs1(7w<1Fc89ZSo6)%$CZgB+!N(-EmHBX5M9Q5A z^Q&PSLLuwsLOC9jL$A`I`6|Sa;j4+VIjb{4R_pH7L_ORIfuKL7;r3aovoxv#LV?v{ zksm~8BD9B9oTCOjVelJ$&bp&g{mQj}2ImH>;+e)-qBLLv9i- zCSc7kYGMh-e55!A#hXisGpot4bQ=%ub5CUJc11T)<+!U@oVC){5|w!LWHudUgI>5V z-MN=FSf$9Lb5l{^gu7KpHX|VPC~8)eY|Lgpo=Gmn0_fbuy&(hhB*_hmuKiv%X|*V^ zdX(%|OU?MUd7A;{JGj3fzsUe0?I3RK>Lw^AAsi!*7`+m^gO^YK3ztB>#0nSD0zglXOZyi)CZ`#fubszYiZCvqSw z+7~MF!M%FPzcagzbd#?` zjB%_fh#wXvGeLH@1YWimC<*B7JDCCo(Bsx1@a-a?DZvL#CsH zi_!+uMXb>R6H)C`=roe$-`2Ck_i;N;hJI;b!FMM6TuQhue-gx#)0PRM2 zb&q@06zW4C2mB(iKGlm>Y9blNWd_aIv+S;&?s5A0cLEDBL}-QZxJC5VQE%P7j0xCdG^L_S5t|#<-qSUZ=wc((bv zV+oCbszNExeh*mYu&2{Ts>tzrm^bznNv+oRVa#_qL0AJ0RH+bQPFVzF*vOjaGVS&SS(n}y6ZtTD(;{7^!HS~h0UgbhDFXD5gEnxRs{2= zRqS{stvwF6uZ#A>m{x<(Fe(MJ8lmHL;P5Dx-88C+pMISc13ha4Um@L>chFbkp5TZz zSTAaGTu?WfRY19WQaGFFLzbVq&#OT z`-PBt-Pc!0!xaA$hWE__sAYwIIYMS6OwiZ!3{c~HZ}gy$)1&9jG=nXTfRtQPsbs4W z$!Fcm5AIVRECZ;YPDekT_Y;&x0lRPnRF>@x_KUV)O1)}}=+>52dO8_>umkPDcCDia zn(@ZuP>_OPi>PlSCSv6c&xD_690J>tRK!CtT52%}f9Npy&pJpcJ~x=!8*!7CkeY83 z5f$vE<(@+!Z6f{Y{nII089UJ=i*stw?mYjq=KG|WZ~H)3{7;-cM5Z`t|hxj!PB_xZ8Ni&#S%H9xz9N4vH@^5zfnu7r11JD4M>y?>DV_6-fxqSEqAJ zAzL-biugkRrN-%A)|K)q7AIeM(M`)DD1PMgT-sMgC~d^P8(bqlRqROt7^*aJN7(9k z+L*yGW0!$Ab+V_O*P*Dbidh^3Liln5xnA=cBKi4Sz!ZW4?uaD@e zMx5SUWqwRcCkbYY^s<{e=&f@~5Bb><#%ZOjRvKDnEqC4Pc>^h3HC1Z^6L%#WXWgd9 z;IMDN#6K&`A()BPZ8?c>=RQ8C+}FP)nQW)o_&ipmq*pb>pXo-(3k1D^4fSRX z=TOnd;jY_YUiqs-Z?o;`_M1YEcT?$w9Vix=()hW`*p_mleDl<=AX@Ji*Y8!ZIpAX> zYGjseZ{K?0y=e1|rGf5!fl~{r?uMrK^jPjP>LNtgC>cWS2~?xKUJvCSzY@db-Y~Ru z{U{UD8(gc7y?6yZnd6z`vW%SFW6-XP%P5@2VMwH+@IC^viFsoDV#8g|e1TE@9jAQ! z!&m)yI<=@eE5n|eWj-B#d(V5qAKqRBE`0J^@}yD!@SC#q z!*Vsb_omXTc`^L^F<#c1MxW0)SsOnt*fD#=8n{TL-qGlqUK6PRJhc{S`hWxb}L?bnR}t=}oQO?P${kC=@7+z#DN$Ojc+j zw8F!Rp@}g+i?kQPPxzG9O3`U!nK4@`xCWSQz+W}c!}!O7clASj$Hdk`;_qYrSStZ5 z^Ny0X6!71Z#u`~dUdqb#Oh*XnnjoLk{0VMkM_>yCeBmVe`9-*~c7n2hhAo0@95=Z! zaMI>yQP9nI^16(My;0U(zx3{Ur>GcQPT~Ip<3Jq0M6>Ha+R@Wz_n!RV=tjWbhDhH8k(yHrIPT0W!Gj!{FEgE`ydwNId=XlFSZsm@ z=F%64c^)aoK>IEU;J$1ngc+o??`Mnpm6O2qi#P?$zh@Dcy(NNUp1Qz#MM}`3fM0d; zcfPFLERu_}${D~|jm|1wh{FIz7xXA@B&0P-&W9yVXqOG`lb>no$SYpy2+IvNUZalP z;+IsO8IwnK&IZCb;OVkIqvMh990v+|SqsoGQco`E!yivAW?EY=O?qZ96P(Gts&iHR z?mO5TNnCg@R#p^2UAPDs+pa5Md$*}9H{icnk_Q^MNX;6gcAC~p&`$KaNu;(e32t`d zG?3TD@PG^{JV%MNaP!1?suD9ZBo$^`GY(u?;*s`??|lS^1+dVW1E~C31(vCCv+0Lx zj80YnkD5I(-rf}ln~+lBbljZsTXPXtyge5alq;PA&fG|^52#FN6U4$ozqZ+-g!;ftFYZfwJLIhYHE z?(jKO!5w^pnt`RjW7F`3n-YW->2Ac7gRm0c1ZLRcpSkj*Nri`TCRJ(^u;`W{NeUJz zKWP3Ugl4@4zRCsYW!-Hr=~ps)#Pap?Vf20Gb{##d8?Oc`&%@Is>kO_bVKqnV{-pZ> zga=j$>lc?`4ty1{m}z)&7or#~2buz`2BhAj011UjcB^H$N2mzQB@JZhT#uD^O-)*Wdtp;X47AWAAzx&UsfGZ<` zE9ZiCCaf3#E%$(b^vU0_0ygrOf;jMm@Lg%t6tGKwZsPDSP_=`PalAe=gD*KdWVuRm zr<0Y|2waEJ-P!rpdw0?D>^DzYQ-ohe1yU5-J z=B&6DEPmB6hH zM_bdY+Ce$nq)rwATD{YGrs;O@wu{2owFd&7<61q~Rhx5o>)+uu`@f{xU0nKU#4)kEBTY%5l&fHpz+`7wT+t}eERsY(Z1im2lz#W`vCBJ zg`2)0e*YcrUAQM}8E#2&)ei7&TYt9r1>3XPG#Er~c#G8M1MS~0 z<*$p_`rImEf-rW1y)_zXKNuVxII?t6(p7ds`&ryAXmnzc`5Bm;M6=PE z?)ZnF!sg~DdoPb7eq|h-29Ty`7Mx1Z7x&QGd+%^=W=xVeU^x~l;0s%qF2b(hN8Rzcm8O3<*6aHKcV#K*!CWoIQ2EldE0AdY6-q5?F2Gz0j-8|T0U z?G*456^73*N*HuONzmK~=4g&Z58x_X2h6gN7BQ#-{PxpnO9S9sPy?Mu_ZEQckKBgP>nDnZk(G|an%9ZG#_0H-w< zneAs=0J|+nJdP9)SbpDiXTs7RFe+f&30z7GSc=dTElo*SMM_H;sQyfu3=5|h5(F^( zyYU>b3TF0P=_10)MPixNM^u9LI~iQMqZPpYU(s|m0Nta3^78V)5OBz2C9p%a?*90DO88rz*0{j|i7`U6LOO&QuD!ap0_QXr>$vCyonWi?$h*DQyIHiq8dX zumWz|@^M=vFx?a*fTcCuuF2@YI9-IZz*guwP-BV1ty~IpcRSc0DBde%SMC+1L9(kO z*G7iCugb)2kXp3z+HXfNtz?c()UV8hK`<3dq9rCF4! za&?Y~tIjkMtQi|@ZFb2UX}Zu8xtrDbJM5t5XXyQobg9V5TL*FH@7mj2TOYv6BPB2_ zTR6Z1GZ?Mjc<=@);7bqQdFxvT+rRr8zj4oPMY?f^mLTCyhP}I+7hdWRg26!9#YSV- zk0;Y_L%)iuUqx`0xoD2Ug%_LIqsm}6C_!he_|$-POviv_5csOy{d<+vub=&`5%K3= z{NMl0yT33~N5djFc7`e7+s~)((SQ~nY^n4HjDf#zR~eb86g z3(YlX`ip@4WhqPw82`Tsar?VqXT3oC2nI&!5j7fNd~^CP6zB z){9;Nzb^`Sok1lO#v=4eUlKS54$NTat@N z0oM5=DnRcjfL&Sm3oax>k|GW`w}$Qp-BdpK9N%VR=Q}oue|jL z);e@GAb4LGT5=w+J}ZKkHGgsle@oBcGOR?`8^Sfu^uBwa0leroj0b61naSrUGOvK~ zsi`PK`to~L0Pm-o!;xD4vJPf4g(c{agKlF(n@YX_VCO;hGr%SUi#GWBRY*2l!zyA< z!iooHmWDc^6HQ{~-3MOuCb5&rX81Po+XGXxAkeWe^6FP7fsGqlKAy85O`fFOcj823 z1h6wO1AwQ31u>@K`&=s)l%OL9RhmS`L{&Luk*Oq>ws6HCh6;G9C(2h0(@D5-L7g@c zEkB|dhEvT`_?!pwcWfF`C+99++1h;g@Y=)6TQ_fByn6?0&@lD;>1ROSO#m+l-zyiW z_WZ5C+Wy^FcpZ}2&hP}Y`)}N!-GrDE4RaYwXcxtObF^xfUdcq74PvCP4k5`buoJuW z{O2JZ;8}^llJYakKd2xbTV@*;B8lkB`u8f!-_>yZ*RMK44*ueqs$Z-KKYgmkE7$rJ z=T$JKyYoERdql??dutGOLYU{1ctn_{gzrmIM~%V!?N*%;dUe&k)}LJ#`f?%`)#ZV_ zSc2v$q}lZ|GjoUV@AKr-PY*ATLo=E`XBrqysU22HRm)0;< zz62f0`N1A=A^Nun1aK)SSlk6x1*|44$-<&8uqt4zKbIQB0RT=V2CE{{=3Vx-)2JCC zId#NV4J?zs;&b-#6jdoUXs>`Bc`NP}|0?a?&|u~98n%6d^MO9F3S)O`@V+}jsCtH? zIzs^en)ZN|!g8jo8E62BN0HoEl5USt@#_~O=?Ic6m`R=dm6mYZhi!<6ZJ05nG_8BI zQYk^lv8haJ;8h_CIW=3i?CL>bbfKvn^tGDO0}jj8#n6PwVEK2NYqQr#U~j!LgSU>v zX>D#d4T-A)IJE)JvM5lSu6)nviK%F?!J9e&j}CJ?r%4hGi!ML_M{0A9T&7jT)+Ht~ z@6mN?mg>?GMs$$^=FgIh{m9N9C62%dUSV*zJ4M+WFn6?d`R?A{?j9Tt_pV);n?MPS znHxx7Fg`!v-Y=-%x4!isp!Ivh)t>J?hHIdcpS%gCRxDc5t{hmC1S)uL44#^B(@i&8 zUyt1OME=!0d0G3i{mNxja{pH}a73r74&hnV!FuM0aGV6b$_jXy6)+<>1n}ps6#VWp z#xHk%1M&MaRlg3wJI_V%QLXBHWUlQT?Hmm?W*G=`LzrsO_n!q*!tm34&NE4?h~TTY zxT?Oo>{6lE`PW`Ac_#x2VXQv`gl91XD{6K&4&mPkJXmKZTLNG_VlCv%6dig1;F&Ar zf_~-T<*^Y-!MctW=&_9tKfF2C>0RD?t7^D?7lU$HJ|cfcTy^OA(M!T|1jQ?OT$Ro@f# z#TQ?6SnNKy3b+auANvofbPTDa+BzuU_k9nzTwh9P43Kuz+pt=5B(2_X>1xztRA#NVC!jsfgoikCrB!p=fQdvm|N6C(8x$9+)9&%ji&dDzPpr-_&iJ#et2GzD0Y; zpfC3MyGfeX79r^usyGM3MQ|iR?qfaI!cjVwZ3s2>S=)vX`2u#(mGxpc<)ImA180fZ zy4$lZXxjt!3OKjl%3X`>4Utp8{0;#(N|d~=eVPTMMP}HyfUOW%dgj}7-)o?ND>^^r zT+kFl$$Qqvwmvwdm1^7D?;U_Y_VUFEkigrZeP4g=(xnG@xI`UbP`}%#e(%YAFxIGP zBk-eN|DD%gf9IXIe*J(b45OkjG7KSD6Eol%rLgIB{Km7ggI1criK#$GJd&i%VD(%T z-uoOeaV7bAR{TXSb&(V>y05$gYgR{)R+YXMzdKHX3Vyx%^<%kq8c0_z)CBO(V88ds zm7I@^5;i*cNE173OW5vQy8q0XqwfdF(S+a~$)#K+1k-?(bb!HzMXy5;yb!^$4$8Me z@WSP(S=xC76TVNjpX{C8m|Pf<_zikAOu;6~-WhVlBY?Nig!RfRPY}RkE(v;Va&lv0 z8eUi3`~U#lLbVLNBiz>BO`HWkX+de4q8mW+&OizJo7d*1*QP1Z13q{OH83h*c&O|l zfVl&VVQf=0l)wLk5mBzrz#)E zu|ji5z5(1({F=kq&4(YHooVG@y^7iXEP0h6Xe;6>P&ip(0>^5e&dq2Na5t8+cFOtD zRio-R@aN9$h;L{n^K-y_Pf=IEDuY=8ud@Pv-zeZxTfjyES2C~*m!yEnXq9OkB|od> zIBwspC9^bpi73h1fY=7g|X(!>uxw(mx6BjRD+I&dy(Wrv44t-@3 zT3`{x8#XBfYgQWE8C%F?JrAKbVl`a3inMn=N1}xzqkn1cSL)B?G-4N)W?*Ryw1!Y> z4_N)rw^V*I__Y43Y0vMPpWxljkbjfm}k;k4!{ps0sr=&ivq5^{&f;qH!`pSW&p34;a_)U zzTfv9;1>sa!0Q^_2r@XI@6b91=eT0Js;1@J($37ut;-uTtq$WC+*l3DziOlu5uGO9 zfyLmC6RJI@V#Z42+(&99Ib~P&wf>xj74*^O+6BlZM7U8Qs;BsCRumZSL z`CGa}Y7r*V)8i;-MqY|yQ6(k!i@1T6}fD$xA#?7XU6(j-gXLe46l{PkNHn7##f zfUDu$tvQ?5%Q7f3FAuP{^h_NrE1P?cmTMx|elES~r(~#r3BYYX{kykN(2_!N0;X2U=I@0S17#s&aeMFEIzaPrJcYxz#V-T7Xkx( zCnjkpA!JBjxboV=-OoO~g8{iLf=O(nxX%c{neI$f>1#Y#ksbHtl5~|B^r{C-2W+Sb z>`TwfmcD)pSOu^xK<}9Pb8Xe&Q^{r?P`^9M;pcUKS8w~KFT4>R`9^d5?c46Xoui$a zH`=$OpW2`4K8ay8W__##R{VMe{Cm{+RtpyVIo5Fs80LT(zgU5ooo#l;0l*-DPh*s1 zgyyGH9KQhsKGDNSG;re+fbZNu0FSc^n#O=J6l4dT`IffFWFw@HWumZOgE8u?+1uRjNRq&dFYwa$FLlm%PL5mF< z75{pvVOWK>g1H1uHs}m1%oSyE-~)r76PFWQGe zfB68?vY*_oB}vv<>)$`Zi=(n8eeDMyIjuZk&I&AC1pxT)hwWJ_f4vWj%+UKPhlRaj z!z!I8S}j;Cja32f>n1{0s+pMuE*$0p1w813^6Ai{QwyVEpp`c zJ2wFb*he@HRmSP*$%%=%E1TCaNE!(YGf6kc6Ec3Wdgm7q!duq_*1~h8E#OM#kvPV9 zwlYm%sYC0WD~n%Q1FQ@d6V_Gg0gDG3NhKa=z!$}D>@a^_gPtM>Rue~!5Wr)X7{J?Sp>#aXE22+a$FP=( z)2Am#+mnwzh-rs3j`X4xiza~aUm1Hz%@)_DA#-B}d%zDbPWPacJu)IAzqor7=YS6m zw#IOw5(;2AnG*DWdg(fwuqX*jTv&@v{Q_`asGJ}^KmT9;GXa=1aNih+2}*7g5(42ERMc*2>(L=QQ&Wacy}K$&(jhyFqCf!KPYIpNP0L?rs3HJ&hcAi=DJ-%! z;xQx{*U{~{etm*eqO&A&8I*cc@NqJRl$n)BwEY`AcihKlO>spMUx6(JFUlS8wz2h}e6?*KV&K-KJ|tRsp;B1_qSE4N4ZmfA$3t z7*+hL22yej`YLaqVEwy}f0t-hwjbdUmoQ-IW9BZ@ey?o+OeJ}8>J-iYPGKJS6i)j> zj3fg1l{@!90Rw>7dSrsWj!7-!H!oj4d-LXpdmoO{{@^Ydp#v1hK5={grl)@3m zseV*qOV-vm5>`J@@xBNA4=#$*quZk~`zIL5a8P4|Bd%JZA*X8fga%hDfv|-gIgC(jX3lwk}4kKm8k>!qs>_nyh zZx%#TmfnZu*AQj`;fxuadIg*WW#44Da+MObQ^59v3Jsc+1~6&hIP_&%2(zCn?KZnf zFf2S|3j=TbbuC}4EXRW1I5mx5vz90sI)7wrTxpUd!7@T0C=k}Fv!Nl*Q_&EpLR}6@ z(A}=d$4!Q}fK6mZf|HPBNR7DQ9JwB^TBntAaw(3&#Znibb~F=_wFfv&rQJMLWuB~ z#uTk_mP+He&Ts;Tnl2-P55U&-2>iDA%0jjx@*H!Pc=|>r6B<>%ve&hMBfY3x*EjRh zS18PFPr~o-ffAG&G&g})-59W+0j2^p^7rYpkG}I;vg^pq{QT6@SKU`e{|?Ask-c(p zbXz&>6z~z<7#Oo`i`Z7E9fL&)1BEF$nh5;#=LBF{%Ag9EYtUE8gf%M^rWB6#Qv_ip zFeu>e%r&S%@7|ooey<{!74Vb0PWr_i%wMfK>rJeI2IFM=%Qus?aS~!0=-nY))hSnkv&>DM3Gw9xxD? zO3*8!Yx_J3jPjQbFSxlR3t%gMB?zk`57vu3h9nC369Mr2yp6)jD~_UEsa2p^1d{?@ zqGV_0FN)y~9vgAcVUzO^D{zQW~N?yiLt;?rqADC*<*H5U#&2SwOJ`2-mT7qTu zrp^IpqIjhcEuZUHTzV0#qSx&Zs=NZOh6Qgd+dSt%yOo_JdUbjDp?uEmN6-7fuujci zJj+M{XPMEvLo~30x6r~f4IsHOV7m&*r*N?1^|Qb@nv`Y%fXzbSq=rk{4&B;UyDscH?31QP_SvRo+wGuTKSno zY?KbBMyBcI?v{8LlVyue1D=X9O3?`2w z8IZuH2i!Cp7{G4>7x1JD8<1!x?1|p=E=phEFR0(mOBbM_i|c)%1dUU_Z$Rt!8{GN@ z`oi!Lbbldq67UNud1GQ?Z4%TkrdUoxeG#6dyO66j(pwlGr%LqY-9rqEK6rGqM=vb6 z3av5KrtO=|s?jQPLr0l5glVUq7samo%i!g+S&V?@WqgcaX3>Vy`tm4~d?Ctjm+r zI6*`Tm;wCg@EhMa+`D@bdc)VJXx<22S6zyq#1poKN#-JKTQfHiz*i;_z?>dUcPM}_ zGl1!<1U=yYNG2=RH^tkh1{`jLZ`2 z0armHr5dr=edT@8E_q`he50x%IM{Nu9GISMz|~;cc~y`8mMNXLXHNeHj%b|(E;7>rc9E22u#UkkLdshi!0n>Zk=lPcz zi3^~Bi{WF!ctPbB!fouoN_(gax`FX@$k^gJlH0^k273&&@?2Xrl%@e`)6fo`T8nfv zG$AsI5|hc{tV5c^tlw*@IUsDQ4c4t*cjE2o|gW-r4ynb$}`M`Rg$C ztN8t#j}E{1_S0d}&z`}d+9leu&|UQkm|=`Kwyj`y^3~|)Bz4fN6lnF2KKlJHSOSM@ z2}$bX@63MyM6eUT!r%lpF`Sy}oCN?M+@0&4AmMw4PMkq0W#}gC&^XI3=xqe>8ux&& zABSsW7vJ1|0=1IOIlP^k$hat{8;wQ~O__{FohIdAq54HHGywRsZ|+{2qzW``WT5J_ z0{Gg=77ij)Y!*VE0sH_;(7msz0+xNiG=fw*^Rry51i*iR0A8`=a@w%(hLDT`{-=Kg z0ILa$D$taKmDhT}D~7(Q>kDH8mys$m+TW_Q4PZR1hx>j9wkrhD8AA?2 z-c@;{Tb}o^gBo~a}KQy2TD3@dQ$)qrVwDo}2(C zA{E~6G+@HY{H8xZ=O_TU;V@b>2K4V}YhfKTkxigt2nI-)&WOz z?w!s?C);-eN3=vyh6*e_dybNXiQCXsRw-N z9ccZ6H;byyyqJOTd+8#SsW&z@*48H0sQ!!v=n=GZb$O#~7wu#tZ|JUI6mPN2iUp7Fmt_pcq-OWnl+gg}*lDIZAyd%pSG&q{d<@dh> z2!1LOm=`keQY2Ktv$Bxk_sDM%E>A#AT8}Ky$FIY+nJaH%YkzNZVhtw9$;O2)DwzGnA~af{$pg`384L>eZ36Jg zHCDhB(SarCgaQ2U&Xd621??5Ex}d2B4Uv?x zmSLSHd!Yw>LiZx^%K)S^{Ol3WTUrI2$>5TgyrP07e1qD+9>hTi))4R2dbmu`A6=pO z)#c|OJcd`@1)WxhTyv-%GAzuuKzw|bp5EIN01Ir7)dqnxY5JNI-6B0;u|=C0N+W`^ z)CF+x+BdfWA*s2b)r3_HRimJQkH?MlOAg~NpS>9u0_-A72fDxh3xf%36u)v2YUCvG zJAG5PsVGhT46lGK|0R*jR?XUabIJhr2*1JV_jfV$N>>u1_39cli8topRsRNVZh#H#a9p5OhOH??FvR_iU{7zV zJ32BsJ~lmZ>Dq7Jy}ZG-^@t~cDG@qy5;*dlr91h`tnp$++6VSUXy5v!vt>lElfUaw zi#^6MSh%sC@N~;{fJp+Q07d|R{EXdK;=Ka=TKekVqt9Il`gUyq>6Y<4YxCFP_eicv zHTsBdSygw|k?##7j#r26ZIExI=IAe;Lc97q&wg$I>;{o|5((<=>m-0rfd&@+J9~=K z&(MQ)=2UkS0KC2Z=w)o|g7|d`82=6N>;(K%wx$ulaM->^0H#&ISc2}2T|ouBeR%C8 z>f!||DWid~ivqZ*WoT~f!hNcMPht=UC}7gYpqT-{7ZJdBCuW)&MG57Y9srmWFwOyg z&-8#jdl$_}l3aBFUa_+~&V?m5=odYJKY8B+m@3eT4DRI4h1D3+1jZGc2@yDk<}cv4 zH8Xwb?(W{BM<33O^*ZXgiZuu;Q42IsEP=I)$lVV7=A?%c@j{bf8Z2C@XLGM_34v;` z0!}&;7R>++mZ-%CP4U+2_(|*Pf_4g+E6^l@E4PiIUNfzx5#@dw7+JJHXgwN7q$@WxhD# zkMfFnxFF_lcC0K;@BILKz+mlE#fv^qJOwsrab6XEAW3sLGUJ3Uvrq(!_sUzLv(#0f z#e^le8;qhXf;nIY@Nfo$Ty2~Nv*ZGLv65g1l5Gb&35;{VaXPg2oV$I%abnF<&&hcV z-$|2T_9(3n{-)88FfD>&u+B(Ir}IaS15$;;wR^VR~bTDYt!;H!EhLfvu| zXlVie6p8jORdt`qzCrA|y7uo+o%QOcLpBdu^?S>y-`gU51-nlAsv&ySR;P)@DuMYv zQ>!)z#&5_B{=Yu@&Ue24(Z_WF>m(8up-}>p{5?eyxCwu;Q?sY|UmgG)wJw7K-rjo| z|J7jl7ZJ<~cmW;_@Q(=q?lFLI!4(6T4#%$_#}f1-$V+?g@WYAm@e`<)iNGj-qi$0z zSk0~~;NHxIhXCN$2*Au=Rloq?|8TH7w}iHYF5wqqVORlgf&#w&()XkXyhH#-`;}F| zi!K#w<@|M{fVn2?XOZSB-3Dw-Sf6~N09MaZLPNmN172Yl)(Q>-FC{C^Qq>?$n|Jt} zs`u{B&Gb5SAE{k6K|`q-b?+$1-%+q&p$Hzu`>_i=$`xoC39fMXn5y6Q z1w`O>t}EcaiQeq{F(jJv)#Iy@gvAON?vffb5*?B$D~aUbrfzlocvZ+IBr-t*Ct#Zj zK2zn?15PNVgNB49z@y+gbSYCwqI4_G!m5S{-fEcr*%qK}++;b-FNIBCFMk=UOk{f= z$l+lMc=`D@u4ABpp#)9&dL=f3j|qDR`CC5&W!@r3@mE>GDs5uFgPYB+G#CeWAGf=+TwB=h;Qow02(aRs&?H!+D_BO+CEzftyS8W560&bF2v@<)N z_|9debHT=eo9GO!OzEF+>_)OSk2Jh5+zdN$@*WmY-sXROU3Sn1|R$cv>E`_*9#-Mfk z_No@05x=WGG|Q!W@VDd{;V*vw>tFvZ17IJ;LA7>jd|Uj&KRW!~IDr%^A_!pU0dGG6 zV;uE-r_ONOmy#&aghh^L0r0&WuiTwOn>(ssY6Y(WfS=rX@4Y7<+#CnLd+Q9@syKo} z+*nPqLen;+k(mtw@B{+5$u2DF0e4ve@6OGL{%vtA8!6z;uPl9yvYPc8`I#vkm6)*_k_y5Vcpx03YBaxQi4JELvK&t{yj0}!B0-9!!Vo<`8&)*70hyN>%A$bK%0+{R1M*ngjIQicHR;dLm<%JAsw})xpRfIj@GYM*5 z88pJ0b*8%rD;K^|RL=0si5u0R@~yDZ^LaUW4`u zc-i)fSDy>vl1k7}ZvN`tkN>zmOEEtgjRP+uaA!3ZdfCWd3dqX5D;g2(R4~Ws+77UH zKTGo$U*}}51sv%jdZ|JGKa9O^Xk2-E=Q}hSZH)v`o%&* zSbFb-%P`p`f!SrkEHki*lfa!9mrLO8eg6LboGNuvRjMkL{>xT<&i8q~&-aP^)GDuv zd?BtcjhtJ4#(kF)hsU2UUC^#EA^MV+3~drTMlN8iy;c z97NuH@vsj~X!8(}&-Q;uwgIeFXb9jamCP34iV?tFjlv=mv>SvaF_gdjAOG;Dzy5pw z^50OzBkAn_al z`0m;~_~hGc!NM`5c$5|J@n%DkD{12r9Nc~S`@etpF7<##0Rvd8gj>?U1E+)m`~v`R zmFmqNg4Ywf?TDi&xd;3c#|4es7{~!l73g)cVXe9aXdMT3^lzSO&vtY5JrDatM9RRq_?t&+fY30RcR$>1*4*K;Mo4xFcG4unlbP zoHe~#_1O@Q7JSi4fdP!#*O9to_^poF;b4JqspJ^4N}_^Q314!uD9u6!HenS=0TWX% znLP(_#A|^gNy3UUZC}TP1xZ+-fJd_Gcto@+REsZ6?1BQmQ1EKYmuw=X4FJKfRg8w_ zbwqXtgB=KVlAHs7<3V9q%lG_tFx&|SHU*j;4XSFb6)N&P$^>9H)~X;v*8rFvj)#KR zB3UFXj$_Isuv1lzobe)aTXGy(gRNh#O)tMW9g>vC04&`B9CHj7JmoIa209cqlau=Z z;42(_b@$21)2HvE`h9>4f3fomwdcbX(7uyiQJkIV68v51UMGH8pI)7 z>WBjFm>%$fl%W5=|M)-t6cq43V8G*Rn>_jYr{3(Z!LO-5KXd5o*Pr`t2|B}K>;jtt zbjRRVS*%rPy4RF08JMUmdXpxNaTG^oJbBn34m99 zS1_%}bygv{Jz(611OV0=G>rl`qH0C}BRP@3BeZt`w=dx0BUZrq1-L9+UV{~$@Y8`i zZ2`pX+?+Ijd15JI&on$Rn#bnvC^C2y%eteZG8sHa@3~G38IoD;0DEK1PCeNJ@Os95 zr8a1(L8BR~2LLuDXimaH70eR2iWTUTKOefDqv)stMF|WQ=;^oKY9oDU{UIsfG1j@*{>2(}lN2x&q#ZD;66Q?|8kx)Mfm@d}n!`*1li-yCw6uUr zwgfE-*vq24l^hdCL#7iB^FdwxJq2h=WWDhc4s|co#1UVt7xZfSn?AAPSh!OKaLfQ!92_ z%yuBj1aa^flJz><)~>me1g@LO>;1YEE&*VF^rND`!i$g>p0B6dtb(ut4cWg%Dm{keB#Y?x zH^LERuvFW1XODgu|u)6M{f3j!Ew&^xzi3<+BLi-)fez$e=~glZZ& z3i;?R42irUu^2|Kuls{8yrYF`zP$BrLdcP0%EKM-ab=UR1ub z+~YdmVm%b)dKPYHk*K@tuq|U;qCN=1FL1@z}@pJUDrgls;(nc1#CV{K^+T#r3f8Z?Q8|FGFB-ZvjQd?w7*L* z%CJ~BB%SAd=1}Nxx*YObYIYcy#()XH%QT0Cy}#+DoxT0N{S6oydIi!K#V=H!Km72A zBz`~N+D7%e1MG#$^Afgx>!^RHfxl%}0poWciVKHIlCe2LicLfbtM|y@H*hyHL=qFt zv%qF1iA8Zmiqn;fv;W9Cp>+$Aqkx4;P7l~bQDP7Hr!)tQJAnW6?=jxd+MZtvfjtA3 z5x@Os2NuD7D}aXy-!r}Vj|ri9DA@Y4&b$CErhJ&bH-7J5{?hh1E4oO0=-Y`LwMgp%gfU8w^JXWWW>0U!K4F{Q2(Vy?N;6@P4EbwpGy_FwvK;Mqz71lt&f=enD81 zRg2z2QaiaI3V7WVpx3!MYf?1^6>wGqOQ078Z6pj@PXUt&3rBzreN$bGL@1pF0CU(C zDPREb?A+cD-fEA?954geFxMEb>{zf2BAKlV$@$)^rn~&e^I2sf+_8<@&Y=KV9T`DMo zO=lQ>kBZVuwg^3_fX#-Y(vU(lny>)C=OL2-d`JMM1WEe|obADv9IYEg=eB<}DML+I z=kouZQeM3MdBAu zKg7&Qo;m{YyNIr5h~S6JjinvLFi2qVUs3;;;u|nNm_8Rj15*()2%|uM{16WW#>{LL z>E6Iif_7@rbU#JZLSr~$4V+3O7DCvMz|!ioY+>+wz!ZgLT+n^j2K|i3kbuCy1pfX5 zr8|oM>a~P#WU9{x&mIYSpY<(%ISQlif%V+enZAq4;WI_F&Fna{8d#>3SOND%1OMv3 zVHVaeX&ZwZ!=XK3{Fx~RtA+}=#{5ldDb=8%PAmY1Jwz)~f-Xw|x{Sdb*b2sS^SCPD z2Pd18Fz!wG1sM$eVC?~4oov&;e@J!Zd>;P?Q}-V;vD&C}$y_x@3i!!m05B?Gg>@Vv zfI$IYZP40=JboHT!XgC>0Cp5`Lrqxg{v2?k5_t8Weg7{Rz|~dHKJ1W5Owix-UC>~I zzL;td2b>$+{#~8J%N%Q1SvtsdXz2UO4K#oe!Q*SY&ySxx2AknH0(g!(yFy-8z6gr$>S>TVB);ww!!KR9gtGo{#Tdjn$G={A;tG zYA-Y|W#g+2w_|G5LyW-eIC5ObmQF7zA}LLQbhyWk>s%{4-+W?3MI@CUrKDlpF-lsq z&oSXsz^DW6KHQn$l^#y=pcvgAqStqiDEW+=m})@;6#{=Y=Jz3kl-@ZN-rP;54!eU? z&@+4qf$9nuu^Fe z-@pGl04b~EZqjx(kf(=W0Hbunal0(S|Kh(Lfq^4scT(Rlz!a?)r=lAOpGPu;E;zQE~J^O5dssL^B?bxhqSRvM$XG#lja4k zlvqG6cobP1h>s4>kwqx!A^LBRc8)+fr5ZqQ+ z;{YSR(Rk22rE~=-htNW4*#Uh7Zh}9M6N1#jLAtl9>E%V^AVGAkmAI?T{z$OQTzz^3 zDz(19rajc)?lT>#ut@x zNJUYqc*Ho**{;&N1-^HU$bgQBap9J}W0G zNoxf2M88vC*k)1GhCw)7Iue;|h~5;7C_ZgbJxM%=)vQhtAnkX;!%kXZT~~>__WxZ> z;Vo(+6+TH_PxA%O9Dj*wfuvPQOo47+YJHcX)b>5Mdgze9y?cslXa1AZ&P`AHHR3n5 zI#7@`&{+}_LK;BxMD_9~2B6RYV9-S~4Osst(oQyE$H?o<3bV2XaRUMh0Ex%L34oi6 z-NRqxx{(=K`G|Z8xf|jR;65=T5G>@Q!TuqNXSx)&@EW*b<`~!959j)wstyd_;ewi~ zefUj9&*Y;8_V{Uxcu(HxRJr{U?XI=1qN;69hkrJm1t7Z&N6s7=rz1B=-U+(N4+04J zcxL!_{^D;JU?Me80TKtBFHB)#le>1Tk=7u`WYvf&S^6c}ug6iA_Wv5*|LK_y{3T|9 zmYhXerZvhx8|#>$&LeSu?_1(Zo#csW3Mg)iks6mpRR25_WSt&}RAVU=$uGJ)CA#uD z*hY~0$PNvP=@OWMU8E2>^bE&TrDQpS-&{`FhAg^30qR61%0dGzoF*rMf7`b;tUG@H z=7Bbb$V&1|fpTBrL4!mbHO4{`vOHqeX8Em!UU55)FLjr9-Ma)h6PN^v|KQIf71bv$ z5i0ss_d7BfQdMLJnTH@hFJE8AN@l0KUN{U>Y zOQGWF@qfz7Qj{j=f&2R4YD|mf?uC3f=vzd#$KIz(y;PLW55@V-EHjX;gXSzV0~6+z ztfqL__d?S#FRJG#;yR6t?zM%4F&;1mH+>pGXmWJV^`PGV56d~h{aWvWtFHtL|3hzp8Ia|boWkLOOoVthspckSLt&d| zc?;A9&j@ei@atf9zs59%KMn0pWnQ%>sAE8SKMUEL^?ylL@*-8vKtB6A&CP(u zI>C&*MD`9`SnLVzPEqk?^Ju9c$SO!+>fmxrO52muB;+ zLqIj-PG1ijuiwMInFw^}8I>t;oBaFqv<%>8`0x;a@jIf(+WaGCfuxXwKB(RVsKlj} zI}?L1?OzhUFAYJ8uaw$$PwB{oUv3*;w30zH{l zWAoiSf3)U>2|jus0-v$~ojf->TpvWzAcT(R$x18iaqFLZaW-WfZ9YaYs*s6sA=fg3 zXjuar|yypxPvlY`g7YKxc5*hDz>cw);y z-|!aE!{h+Jd7SbKdi`iDZte7(Js3V|>%i4#5EoM79JT*8sT>PL&BQ-Wh~2fg0Pp%^XLX9d2z@ui;(zF@)I0Sap>M6!wzOu6* zPeub}0WO#BV#>g;)TF$~46uuUU5>`%_ZyO`fcO|barx$eS&&y7SiF1k@UeW0oiPU)DGR-_ zHVmMM>MUcAi@Jojd?*|K-ocJ*q6)-_>#l^k5_Bt(F1oO04!n&I3;V`yZ#3{J#7gV( z_A-J^_v$wB=I2pMTK}LW)qw3+%^7>Sn3>qDni(vxSX1Xl({CC$^((91ik{Rdav_i; z#r<~_iKV_9_E%41lE*o3vqsBE(E8SoS}zMbl+OdDu{gRkt?wCnR<+zA6%&VT+J*f= z4Ihr57tQw31f3lV~j>mYzLoB$)U(y{ZC+L?KQ*oj~}NU%LWD ztXoJm*0cU57uN+M}yjp97$3cYWwV?{y-eZw%0o&wpvyL$bI7HKl6pNq#-~ zLmi4__9Te4s#>;gFhD@#4dp)6kwq+pG)JHY;CDRp{&8Wc zXLj<`+~Ec4GHHggulnz*=*>wNvERY(NRy3s%!OS6Dh^+f6BkVPjZZ*ZFjqFlnlV}! zpbkjHPS<4jmw-3YlheA1^n|(p2&aITAb9%U2A^e- zT@*T~izPJWCGqUQhS=_Cq9hDmWVJ^-hPf8-5{E2URZWp=iUdW!zB2*&#H(b8G-)farugOD+Dnc znDu=IzQ@y?ZEQCIE+hP1xCHX?rjnZF=!8&R=oW?|s8m4PU1?$;B6b3>Vx&Fo z{QbcI$O@L4$@ShYt)K7?4dUEvlrs+br%zi8SM7yalw1foCoHDR`@14zmG*>Qoy`0( zySs_z1w{TcE6i+KRizQzeAl-xa>MYqSjC~0-o2#94_3ev?^3Mt3Qnyo@%X!F4t*os zfgxRe1j*YxEe#k>U#4=4w7bq%(HDNZ;=}{#Thrh>%AVvQR6g+<)FuG6*vtdRWK&#Gn0?&K ztn6#cG1nT90K^cHw&qhQop4V9DL!hu+&^|yUc`G}I)x*2I6o_ZcR{V4VD_#+iIm2+ zp@k;`qV)YH>p29^3Eh>g2Z$SW23y4fILVc`T#ONuahDD(pn3K z1j8$`7|0%NP`xBpzA{|41-E-d_}(snuy}Bebl~il_!iZ~)JwBBy)u4cUqrt>?mGq) z7w>uMX?woJrH{>z1Y?s-Sl!wU-Rp?vT#l6u3jc~Wl9>YZSaW|4x8=ru`h$X355>auj;ie;X4rgb&d`3n!1q z&ixPa7IMbGU_S>nU?XX*h=9WycF0^(IVRvK7|Y|xMM(sYT6R{bx46KhtEK08mXbe( zaj_a*Aj8YZf9Lc<*MINDZ_J4i+Xo+22fy>BFBY2q!TX?gD5mDE4p{eP6LO&|zpe1) z5Sk4SRevzOf=tLE#XK*+wD8BXdrVx~dn)vx^*S;-pO1m`xZl{)C`YJfd!?;7_+nIm zUr(kBd;>vA%UFn!hq`Bb(){zF_Tl{>$OUtWHIPq=Kr!}j?YZC5MZw>14dGYQ;4|Z* z`!04husv1=Qg`g&udoq9eS<>>jNzWdyu4@KCh~*dl!#I~#e&YKT!Eh5Ip)3F@cNa7 z8qgWD(o#tBI|jVq4LkdR9su=G4B%;DXYO-1wpsO2{dra2=d9%NT;0AMr>X; zOXvwrvEF}2^UCHR_qD>9@z*t#oUWEowFDo@;e7mq+M?xD4OOqS8OGBYG8}E0 zzYqQPZDXPL2`4UqO0f=?#xDCr>*CYQK)etGv`gzfmB7L7uvxQ>_Q}xgj9Pa zg#ehpKL8>50dv+;6%>ALfGPA|fesJ_vP9niIyXGENLgh6z4Hms_=GSv5jUR#bxn*2 z|C{l|qCih&3OdJz5@r8MxT*ZUFF%aY-m6V*8dL6sjciOC=9jhZHbf?xlS4lpC?Bm5{qHIA5d?1#6IKFJRy=vK zXh`Y+R+7JdBT*gcVzkHNviA%|W5!b)N6|`7?W6pNjl0PBTFtvDlR_@o zGMe1Q&WN3@%x6hn_wyhkL*@;FM6*WOnUGx#nnm9KE6z@9R?i4n@Llj%F{?4q0Qc=i z(@-aU*$XHpDl09rl9UkLt3RR2n7_8f>S@j$fBRtnAjG%)$*gf7TM?e#*!)iKefiL< zY%#{U1|=Is^44ZP&)IGx0M8M%Tw`NT)gCo)iqg5daqVn!qfTPp`4{s~#hxzbhMR+|{HmR8EGZNT;gt}G;hyBwmSuP%dpH*|{uq2ysFOp!p zqYtCF{!nVBF7R~fsUQoz4?iqu} zVct&I@QBCki`U5PvCIRwG5VcsCZwhiYS9?vk{18Co#*y$Hbco;FfiS$|J6-pJ4_n8 z$a=3<*er&Tj63n2Yw<0P-0y(Fo8wyStnT+&O{QD}x*`_ww>D)n^Tv(_BdNJP=J!a= z59ocR&^_;9(~k@OY=pGkO^2&)%Vr)RvEfG7a(!`VjZOw#BakOSuu8bz!nosSh;L_U~L{gw`(7WMim2V$FZW+tpnm~OIjG$ce+1u2QtLJnjVgHuC zHa@f`l`De_3nij5;piml?8v`f%jdoaPQEXJPfdD=ok1t0U3Mb6SCu=ARCCH;P5Erx^fZU$o_ zf0f1~4iN7jjq0NkLC;?tKk;EtLBs{9w~MFG8CYcCy1ub31lYX{!6D(?tVuu%q(%KGZVnh+OLsu);*A+Lk2j> zDylkNZGFGL_sr=owk32l0@(58Q69@GWiT6&X;wQ5u-Fy!Ya#A;h^@vZ?#eQntdZ}K zx&5sU?M|vBIzHv*doi!(6q8(N2l+}4cLj>Z3M#9BP#>%$vRw&LO;oBTK(gJslu3TU zmBZ?>Uqu$moDDjYe5`yBjq<`6{$2=@S(^5}(w$pxMGhg!pk#9FG~cGlYv$&oY=Z7% ze*GunFuDpcw@PQQmg=nHm-lR~KFW?`_ervIlNB%M>0@d!oSUc$g%8?z2u7@8B35u- zU6#?}LyPVyu9`u9>0AlByFn!7*H*of1LM>$DNoHXfgH(5O&kW%fiNwKHe8+Lpn z`p?K+cY$^_qNZ28#;t@F>5_y;7*sr|m~wxI6aM?p`0vGJ88>^YRT?UbtqxEAwP!nE z@?K9X3f}!4gM(0m(#{xn^|D!#FRFf9(_Q=K=WjV%YEiDc^_#Sda0}d38aCmw^C{V& znKxxKA)KwErRWXVBQ7HYFp{Yz@=P5k?#OCWkkzCb=A1<=0X*jtVu;h*wjxIffI11A zsh(sb`e!lS&R%!pnbU#qHz)Z)LTCl+d|C!X{fZ85X&v_Q&)w1PAH36wOnOQ_VOt`} zD?lQJ%vcW$fPkpZ)O(NW(XXRg4BB zJGW8l(oi(Va*@p%2y=wQ$E6BE-dfC(UduceZGxv(+!M6P8DQE~=-@D7gJndCMp;Aw z$jM3=mB}5*-kPt5Bm=J)V|Vp~2aMb406@Cj=y4RKnudnJDB}hE>@KcPkhZpJ0p*5i z{|Bl!P#1#@U9b~IVO>#pe7vK6$lc|cJ1q58Xr`%oFlq36^M`P)JRF{bfEG?-%CYSF#O(q+IySC z1;gfy$xUNrHH*W9ax0isId@N0X6q0Iu>Eyflx%$WFq0N_G{voJw>t;#v(>L3Y{c1` zl|cT`b`WJ#0bsv!c&E|MbV#f>?nd;kN#JMX7zD*YQ>SF5ZGR;riC*vK(9Fy`Z^wtiY73koW5Iot(oU&22cuRghVI zs8Eyt_jj(MuQpto^y^%v1w<@XnZiMaN#W#0{ZGt3wrC6&>1ebf%mZVdg-G>Z%xJHH zm4%AmQ=Z9y!4QtRoSr&ZAj2jh>OD3L>NFYB;9TrOmA8~|#n{y~9W2nPZPOSVfIJYcX^Egx6ac)YF@bIO87tAtq?NReN zVsOfe+1%oF#;Lyw+L_VRxaWvh@OVezk^bfrF60bIA%f|FBftlOt8ctZ9f~g)Ec++% z$D7lDH3HoKrMiD<0k>!>+U+8?#_EbW|ISKf4I_`;gZhM!Y;xgfIh5$b<}YgNoJ@G0 z`ezR|_FbE-sf4kO2`GDaDPAXXu{4gyf>P2OSDV+}WC+iDRg@D2em$SSvUlGmEz_lo9Qk z__*)9P^hq-FAt;9LE zj;F}H8EMICGRa%^KI-H53FHqBKg>2_ocjy*v>IR)39kfEzl6+}+>QSCAE(d~E~Z<> zMprU}l?{Ih$roumJ~gGEq!;cCbO;{UfQ%7@E0#2cx3rH~9m|&(yPCKjIJ?)u38_}~ z$6em|o$;Tb6{8@HcedO7*AxwQTKw9X2#odbfUv3?KC=jQ@@4=d&R0_2ix!OVo$JdM zjJ4jf7Oq-#F2K_7a(dZepDKW-UVH zQt{69ZtLYin6+3VoW%{}5$Gvt-$Tq7B3UmK?umh6If{A$rF27J{sF}<-Ijadb~o4@ zV8|12j|36$Zv%)(2>ujYMbPV=*D_mr+l7Dp01#<{8vbJYPC-Y~#WL#kve-vhwLJC( z2t|hJu{a&W5)rW=s{6_k7ZymE8)^bTT6`!+7qVi2JSZrHG_2IOOrCO>=g3-K22pv>(?p~&U|lnM}X zA49eZh~!R6U!5I8>hUPe69iUUb1#146R+ez#Vp~!`6Eu@(Dc&S75KJ6`TJWipSZ(9 z)bOSKSF{hD)fE>ZiG6?0$O~N^fzj^}dN^H)z_6wA?7)ygNCQT>6ZLu|3AUGYj7em* zV@0&tP?mCR#BKFEFG+op0hEb(0V zzUkf0TmxbR1I~~+ik&0OE#frm|FU!f$xDs7Oqg2!=uDF0hb9ilD> zoqcA>oq{i7jc}m;>NOQ(0FR)`t}!>*zAo<2RCQy_VXue9tqVW^c&uQeAy_@ZKkZ2-dg~2OE|r7v1msOC z=p^?!b#mG+fjtVuojeHg@feN+Ml%36WN#1&2^r_w(A1F+bZ-zV4s-#x7l*amwFw57 zRZ^oa0PEW)%=ay@^%!xFFgn0JF`!q;d(ahf08^tuoUKW(GT8%IGxUIsvX2+14Hjwv z_?m;=DYF{eCfB2>VOIjR^*642W+(pZ~etzmxI4?te1<> zSi~Vg0+xCne>20A3w>@2a*qUZkDp470xYexxnT_vj6xz^DtMArq&|v#)$bf&0&LbX z$8g2YEiQe1f)(`Sr0U&K{U2-Mr&h*;Y*v`*ZM9S9*35C~LX85EmE-}RxyQU{-{8XP z%(I+it-IKMtS_N0%gQRK7g-LYF}u0&osaWe1+*j9q>{%{iW|YZg{04IJ$b~+sVbgP z3>4FRyk^Q1fupfn1aJKpY>CxEhnlpz0y9=^C;-@yUI%D}^#GmQ$K#u{B00^v6Q?l| zB*eH3o4%l^H<^fc>P^WdZ7#_KC7Y#hrb8D^k}_DDo)0gVtjw1WH_TTUBq1!ec<#*ZU)u=tw@MwvzM z*!vsJl=o~O)2{_S(cyob?HBjOGkc$-cyUVVrE!8~boUV?etapyRX`fc{#g65Qh~{l zG;UAtsD$ItY7P-_YbJ4h^n7CDQFJNjVoKSn3+&K{wf6kMCv?f=*8r=ZDKox2p440KUyIbSVM5LT;Z(-YQb>Uh zTLE^SXsmLFLHl^TT9X4RaxNWSN-8z5O{d$3A$~xxd;sV&g-&mc@c~dLE1bGlRA^A` zBe?=_qr3Nf?b@0N9$(&Tr)}ast614+?=-hF2m(BrUb0S<*DWJI`c{M!!1~d%2KqLn z`7<}^k8oESmN&8~%>04R(Hmt^{aw8KW)MNs)e8gVQV6{%)x)sD!m4t z_SdEx;Kz+?_<)~ajET;+lX3I*;r`aw;GF@7v3t^+u%{u$kRMJ}mNAS-HHjp>MpIWc zTQf`X_1ms8MC(8!uYRlVgn`zgcG#`E+#LEib453et(N9u=<9-l#eUt?EcS(dBB1Rr zBi1a*UuCY_G5(bQd2Lf4$@gZ~+9o*~3m_PCU))s1YW@*~d>I>7A=I4B=I>a_a8+d~ zMB$|;Vg^Z&cxyCaN-|Y5LGec>?$w=y=w(9vhMfr1nzAgnbR64)ubDU*h!6xLwBEr4 zTCUj17yg?Rs|2kn4vM}hnN;DB{9u`mxcL?*gUR=>?(nRJ*DK+mn>07sRFeXP!Uaiw z%1TA#mvh;Fi_8XSmY@qLnKW3cIP`sdnaeWE?vxuFTG9tdO@vqNw+)|`MC)G{KA`*yaGzf=^c0{43JKzv{wyRYukchQlw zKhS|QkQ9O#JINJVDU@6cetvW>3PXQPJj7I@?A}CH5414oAuGU*`)}KEeDK zL&17!85spVqwt!|uS86MFbU2a4-s?w@I%_y+Qas}Jf!HL-os-hrW!_$Hw3L>qa~YQ zP;mTaxSm{TOf<82ghV^joh(!rXs>GshQA`K|4oU-Jm1}u!2CNh`x#*((%~h4catU# z7K>(&-zzVP&TB1J^}t>aa8Mz&OtM^ETiZ$P%DGITvN2aU$+BnEF9rijN*z3mLVLlf zBy+%HgAyHeQ;*c-W38z{l=wO?v&T_qY3sv+2zhvwX`u@)Q!#tM?>3~VJD%c!7a^A887PFtyJgy&U zBhBXDjVk`LvC(I!kY1L0np{$&?o{D-C$n=k;!SgnYLT10q_`GbWu$@S~JT*+N`qDG35oxTP17Jj}6-c^Wzo zxas$xyimyi>u?+_y?fQc{CuyFjMz7`(v?w=9F&CFmq) zUKy)sGAf+ldzyUXNkLDx0)~d@SQ9rsK0HAun0QbXSc~~8Lw~Z4rS6%p*2o_hFA4Q) zj^%$<)9#H+7l&H5YWI=0YRTuH_kZSB{aBTUckcEp+H(wy3RQG&_%o%ZocJ_PPEO77 zG2xGS>6%4RlC@TPj_I?}sFYLr*%)fNiL~Ys38>@Wq#q$cx)=t9!o0|Iid-<6#x5_k z=d}VZH~WF&z_92SCf^Wy3PhRqgQS4N{9C_BX)Ja>(!5dDet_)kkE*rst%0p$?p;^O z5D8{}9(^!9su!Mk>ouO6B_`PD%i*6yhEGx$?kv^rjqp>)0nm^7Epv(CNq(q+j^&cT zz1cdNy>{td;G!>(xnp;h&R}{EMc~kBqDwOgOTc;Bb+OTY~3Ww9y3pXQ!L@>YX%sNMX&sl4NM6%_KMFL#X@2zMS0 zV*uZ)w=^;`1Q>K4N0DL}#c7*Z*tt{H&Q@po{qi+u)OXG{o}qscviBt;7ybq)uL$VB zL@&Me-E%flN`!H~Oj$Vq>A>_q7Gp<$r}Du(+aX#rKX5sKGT-hk&~a!Dlk3Kfz6e?9 zhu%R|)FYlx7_h%2K&0@vYm?_(iAr#okcdCw#IPh>63GuWC?Ey5cI z8H|oyA$A7>QhCs&Iy5D>Z!0`+?~H)jB4}mJY$Nn$z1GF@i z!lvklb?}>}KDrcLP?aZraXi;?W>_!1^he=dTI6u{TSLak+;_UnzC~cHeU30ItcafP z7*ET=ZSo9Ack_`7pQ)MKL{FoG&wKgD{aK_my8b8*&+ zsb7Vh3}6LwBH|%xB&aIgD3bPEY=Q1wwlIX>Y;-k!H?*(4wT?{vc!j;FdTlcObUkTE zGg?p>xOmi)UWC!EayVO{R6fJdElr8@)^-?ovG{&if{T!M`XbCZ-;Lr2NFwR`OqI5C z^Ep+Ciy__$4b=7nA#wjCb5^My2p^VmO@fd<{^Df2Pt``E2^|ugd9u+bmFgdXse zU(_>)I-ho&zE2uDf{OOD*yEEjY`w%?26PTOUljHUuKnNvZPop|BuD8DZ{V%EbhC8Y zazGnXmpiK+CiKoI?(APL&51bCeO z%6YaeRmsv|`_h{d<7S5|RHSCr^Q#5@_;yhW{^m6P-^2 zx%N5u#KO|)I5EABrAYd<=2FhT@R$3%vdUFRNmd**@*qMwu~d3|%2(cd1i$g(0`JFO z%im+gmWt6?+DRa=apYr+F7HkP<2Ig{yUJ(ypDy;LPWI-P-g@1;`(EUQ#K~jm!GlFD zo`#B!y>^niT#?yUUVyr*7I0N$f@uaqfiXP@r9P)+Aqc~Zuq#*VNjsn_?mKVcaZt(F zQ%(9B&|bRtRCLm@BFopUS<12+W3x9=qJedp{VWk$l`#COF#$uNi0MS+oWy@hJ@X4D znYQ|f?9wy~4r(=}dqY^Eri^bu$XP(uw2k=Ay5O1N>$nV3lW6yEt6%p7jf?dj|Ei^* zmW_IlXY$NCeek6a$}atlg;>Sov)%J9n4IQ=aaWgs;L4Ar;oB^_VOuP?Z-i*2Xrw_K z9Nx;)s-*Hb<3s-_f#`@x^>=!Bl_J6$whN}W%)!=*#Jg~FS zkTVZD&Dp>MU;;=LjYoF3XlF+oJUHDU`EVnC*zko5>bDz4H846>QnYk5(#X_){-bt^{&P#VAV}Ex&kPpMFEviEP{>4{_^4K>}atR&Lx}`HXFe z*&8Tg(V?i}oc-%Ym(ZsE&i?-9=4N%JR|gRsoZ>X4O?dnp^0ygnnDvv?&3KEF?b<}t zUsX`UhKMi4SqR*$aupzcblECeh~a72!*QL>Vl(dpKe2Il!u zm@DRFs2sk`6~Kg!jvo`F5!+rNy)ghnmg%z(_eE-Hba(WfKON471(Vgn?s8;|n!R$G z90^QD!YS}d+g|b)*{T7tdy^H6L2=!+T&d6a?C;ei_)3pbswb_T2 zVjnS5bb<2cNOIKB>7Nw{bRAg>erHzQAc^A_H2D0F)ooyg`nH`b4r|oh$Cy=%YFYJ3 z5$xV$jDY-+6!dMy;CU0!&~k_O0`iY)0WeyPrvsucKnPEF6|PkLva%n~ZE_*TmN7Ez z6Y>o?yZXb1k(=dSZ$oUpX`J-Hb2(lh{vhN~tjwZI+Q9Ac7yfUWFMWHMh33B7oo5E} zo=^HWCVb&Bt1GTS6u0b(H!qO3q6OI+{A=6^`d9fJ!~RzqJ6`9!PmJ`N(w@nUfZr2Y zp{WQpsh`KReFsIJ5Na0_uE-oaJPnlDc3O*_xV26yt`<4!B3T{j_yzwz+1H5N=*yG4 zg%U=qwROuS@X1|dtDuw3;sUT`V8bNe##YqzHpa_aor9MIp`!Uy2oQF&!vmuBdZA8_ z0bA+%v$hXFI;Yq#w?4ytq!|z`}qmK;fLkfY9lY5{_4I)PlSGuCYEpokbJ1GE1fO31}0hP3QKhS zR?^7eZX5rNMW1A>FQsIVJcdu+xttr5*RzHZ$sB5}ydfWm+>|peyHmB2 zkZ#9cZ5yDs;3V%>45l%;0!8TEq%(E1ec8~A|LSR`1csBB>=F_*@kD|VV`Gxd9DEbI z9<+#@D~6eNJ2fraRvE`$Nj z4#9)n1wylo9(r1{?T^|8J2boOo&+kdt`~gbNCsMV%?N#XNd4)DaFDODQa*`(ZWf|c zaY$%;2L%NUcda>tphnGQ6$lqX2&b?hk%0H98l5$m5EI# z5v3*+h#K$fY)~gpyX<=Pf*>Rgl|V7dlKgCV%If~zg={)@oa{RJQ(c>13jz1XRnxun z8b#YQVA#j47?oVj7nX?u-o?L4lR$gm^Qv-|8St#{l12X&TR)Gz?1^LZVC}eACX+|<9HmL z=iebO`aC~B{4zXT_3wv%flxuzQ=dxm8Z5#_4WvXwtvTxqCp^5((A5CWUroRn0I-#9 zGXOHB#Qod8cPXq2n-BZ@epac@W!G*#{8u;t;O07M@r8U9rA$dAqg zOs3M3N5|j=P^bvyRvM)x>PInFEerqNpa)yek$<0(q%4(aSbsR)Tw&_R20Z>rDd`Ie z4o^_djI2ybyQEow|yKJN`Akj9!UI|=4@Y-c*{@bSM$$&pP<>JAcIBB^>wA#K`Etlg@F`^-kCbP zg)rS2w-AHB2uTj?4fndc2w+MPbyE12$YglWjC3LD+%Lnb{H(!u2JYHV9Z{$6l;Qw3`W|NF~d|pg=a+copv2n%(=zW-1!IL%dmo{w0 zRvRiQ*x%N6w{-f8=}TKkCHrNy-r!B2T5m!GaU!FGqL84%+v;MB(PD}~nyAiFf43&Q z;$KM_KR~JrqM)0{%e!l=;5gI{fV%0|FS6tWB0MW#FmO> zQ*~&AI^HfBBTB=c&|C{3Lbm=Tr~0Pp8-z<_yAIS`VC-|w#7u>kJN>l~6ikbAFDD6? zd;Ejl7#2d;z{mpIk4J28dt?Hs(DgJ-nl~1myjuZsi=_+CP1voV9>L=jDmM(na&1=}&f(WdX6$Anp|-qYoZS1tRu4M9Z_V)mCN zxW0GTVg4Ow6BdCl17$o`JpDfB=2>G8yp40Bb+1?eRTFx|U`M`4)&DNZmBM(q!gE-k zhKPq5a}`2~Bt6^7r3}-QSsHc9J&B!J@nWbU$$2}Nu(C!_g1Nj zH6TWfUkf`kNkSh*{BmfA9-d8K@_oOIxW7CD^(7*V^?|ec-Fslx&fSt)d5O3mZ-t6K&53_zEIoQyE?7-I4$;LL! z2{{AK0W0x-76S>!WInP2fb~T^Nds~_Ir@!mY(U?2h(9CGEOlIiuon;mok%n^UU*>f znN-!&N5g(x#45fGh(P=(gt|(f_|^1|u!0%E$wD9TDkLw4uG##f;=(sMi*sSf+CMa2 z(1+MEL#Dml*&8u|Z?<$fm$>q4(0JQgF!LRVL#5`JX?3+P;33mY^g- z5KJJWOhyr_3o$go3&8FDdBt6IrBKRQR0QGaWqZ)K*Q zealYe7WT|*icJ5{MRW*g5ki&KIEjFAnq^;>KC=iKI>Ep48%+_a%Hzk+D zT3N`g1T1G6r#q`KD-TKY-YcOV9$q=o-GhRl* zeTK{GpW6q0OD>-l3W(mz?i!#|9kJ)7!?LXia>U^hc7+;v#LEBMEx)bJ>9h|kGV+Lb z&Iv}GZdnEr$oqH6NTw&?Q3JIBcyZiT`@Y#7CQ(=bD^|ehpl|?$ETqV&u}^Z$r5gMM6t~_F@}=3a>A^}HBSd&Osu~3^m!sTpS_b5Hmt27OUU#fs1C=l zJX5-n-Eu>{zHi2C@VQ(a=9{RNz~wVM=xZ_m)F_3fJU=i`hTkdeh2P1TBvy1WoID&E z(|+xFHCq?qobw{M`i9~zt6qQG>{CqLn)4gPot4bemq72|6JFP!c<0qDY=>E5h*HqQ z#_t(*s{;(d0)sTo*?_t>bRg?NAIRs5>~;m{mV>a!DAA93Xr<6ag6^5c%9oE9YZYWu zn7TSZ{r4OuK-Qr1gOpK$#@etZ`9g0%qWfN*gNLFw?!WC|qDDou8%Rwl*rR}3K_V6L z{@-;{wnR*8e#j8OO}%m_XlrmGTEXff2;H@`gqZ|TCNdhr>jZvPXuzbr7A=*C<0>-C z&5h(wOvn%=p2U6Jzm^Ew_h0W%0EGP;NU>V$=^E(a@p$mO6EPRqzVR+Vt*Oq*+y-2a zuZ^C$ySVTPOe4p(DGg)Dgs(Le{dV5M)&sgazk5lJS76N6<9 zSVpsIloirDBn51#ZR*RR9*~zfVKQuR<6Z#ft zqAV!jQ2_Aq$^`EA16XniI^gD9sL*g%GGT=HS z+6~KfNv{NrA<+DS^0$a3XnJmC_QJCXKK*P$03nYe!S{z>V2mo^p_z4#BBN(yn{lKE z5q6oe5*Uv{AdL$Y@X3|1#zz=6szc*#g~o#MR>zR+Nz7NK4&AMA7If3(VKv1e+;Iny zs0*y8*EW?>Tfcojd!wJ#z&wN0?aM+UqpkY{UcxWDHrCBN$B5C{Jgn&LXhpiCZDGm+ z=+Nf@p28R~DBveIZ|)+1MFDHgyQ@DTC2z6?Iz3|ka<8FAeFiB(mj%EtKUV-#-I)Y1 z*qV{QK;XsA-IwISy8iU`wZq*T+oXLr;op5@4@0r0=4YSw{r|@EV0_ zL!o*G+r+4VSJ^)%KY@w1sw#&?&~g%1TJ>twt_!;AEkx=$+mKiROAi=dTBLxhDGwp> zG*TIRz<^)0UsZ_2IJUTq1O^*4^nk-4F+OM$!;v~?mA&*DF92ro+j2&a?D~aH&#wJd z?c3|PQ%4%Fp|c2X)3@)DbqILRJ9({PThQXaW{JcS0yBlj)P^MzxJ7#zs0K{~Nx*cH=nck2?v4$Kurr?HH2VDh~MR zpd|QJ_imJ3#S0HDhr<>nz!(w(7>*8VHqfdTx`H@j98j>Z=Bzhx!Yq9D2 ziUKB{QqBfHkg88B|a|T*2XA8a^sx zZL(gZ!ZW-&wKT=LcXEoxK~GK&Z!E%-D~u$?v}cD7J3@(~#Ne}7r>ZsiD?7<08aFMjHY$B}KK+2l>qHEbP;3G%>N*`F|uez|hHW0dRj98sv&MIm5+9AV( zf%dPNK@tS30`3ZVRk)6I?R?{z!(g>vof-Xm=44+r(L3EdgDdliK-M0W_YlB0zqnE( z1>6(`EEyw2;4#X;1iWg%LIsQ?Wc4zeuwEVSv;2jc^AtO3ieaJ=nkqzZSqR@b^>X?`ID`{un%1 zAAkJWryDdEya7in^OHbfFhjT7Jhp@+1`RxanWLlUaagI*KqVQ4D3XB(30z?qbkn4tXS%=C3MVKyZ){gA>|A zC^%*k5_hBrOaP9paXHG&QyJD8i*pz`wdy!tF7omtH;021FdvTxffc}|!3}`}Ta9#B2m!1M z>NEA{P*bfO0gNA3400C0KuA`=v6Pzq+B4sT3|#BLBFP&#L6i~%9ZFFeE6)L1a!>>> zhit?`02dcY0bj|LN%_h`202Ylz*98T+a@=+Z`^tN_6JYzfBReCzJ7aaiUKJy6I%RN z?4qHAApFOP#c3Y7vs=#Z(d=S?tGKcpO`Mz}fCI5Wqu?hFQ!XW6YJLlNAda$Y$B=}= zV@)%M)N~`EGt$AE|zai z&^Dx($3_7!Io;nKR=|7PhcBrI{POzg;nnTU%?&*5Z|rRVUvM|l<(W3uR?%gY19iCd z!^2!&O3!pvZ|fW|0r>73yS7jR<65Nl=)~^HZOEeh`1zBaJjJieJRkmve(jblWB7q? zLt4!yK&t|VH?97~&;I)RX00zFcvWL8{5d26@E?-`ru{(N17-yb8W{5+X$W}L=wEFA zPA%@kAqW`J506hE#smO-?JE98SpR|z8s?CI!4DtaeSc#Odcuz%@9)8aq!|#*xHFh{ z1=9fWWj1g9HZ_C+zt{nuT}>PIc)|95fu=3W;7F|tngN`e8njizQh{b07L5Zlfd7@< zhU6HbDU?zaFvmet512wJ*^~w75301gg%oh&C}3W_0Axw5^5w!ayi#dN54b6_z^U|q z9S2sAE4y9cZ_lPYyE_JXtAY&lo{!yUbUEiOw9@R{_Yg0ZT`bx)J4L%)w+20Cs?c^b zq3i@k^j2s)A>XU0^mTi{?g{WKX~vppYHxJXYKn)j&zADTkWC!J4s##%BWXRvn$i z^_qb(n_(#m3*Vtg)*QGgdjOV! z-|0G~9N}W%si}zx)V;^x-Ff$&ci(yEoo~VA{xR&p34p-MY=fp#%+>#x&rbb$$VpDwj!%Ok`z+bdDfu#M1Dz^3JFe=&banNkzxdgI{XX}Ar2<`*ez0i*OA=*90h`SQNhnR=2J#n%kQ%cz2MnXTa}$dz zyT|wN1lPCm^pX@X;1>n(Rpc-9vVp!2AAa^J02n8eV0wC!`ouHiTzFp4lxSq|W!xy- zAONEPZfNIO>* zI;FyMiEYpjw-M1CaFu$%K^XyDp$0J}ahURRqBblcaiwMaSCT+!27rzGncG6muH8F` zr|*#)s&Dw~WmyfDnH8+h3}+7nxmo7=?{VO}vDoD$k?vid2$m!)bL9zOY5?QURl+%p zri5>p1j-g~MiL;GN@n*07ocgyd4vMEkT8K!0yEYy5#4M?LPZ=zkw01#IVIy!1+W#q z6lck+2+4p$tjHNd!Y>th+ZF~-f#-aV`AYc9p&J2vvfN-v?M3S3Q5ZT7 zDs(|FO9|S(Ap1rQ2>ahfFJ@hhHai({u_9ykG5 zx|C3=&`^SoMh;JI1Ay_{ROffCga&UZNMjcmCxL6mg9TH+sWyW7<)~S4wh=bf1g$&k zRXO%Ev%o@Lyz@vDaQ{3DR^OyqvT;M|&+Zs78kZTtnH8sJ^p`!>$(XJ>XJdk6%#8-6 zjOFR38A5^+0C;Qb=I-6CE43p}0jmS6dju8e?%2^08KBVteZ(DLwmu_;A*E^=X1Z_f zunU?1yr(Ld?jv3Kys?b{2KruyA*9nS7Qh=w;P1nX(jN9Oi7l`Q2pSU;i-)JzA8d`c z+if)rNd37TvjTqj+$dnMV8t;kD9BBI4y}ymUp(F$9~q@*3ftN7d%z7H1V+*O^_IU5 z7!@!9_@W8I(kv`Su+hKOAto$Nj3RdP7%=hIDqtu;Pi!5N=6!kFIo&(n-4q4<>D`-m z-hS^lfAhV!9RT0BdGpSlJ2!6J01iXBd4Y$J-~v}bUji@&V>KEP#Belh-C16A!7pV> zs!v-LaFs33o)4OB(7caOQG9U_z_<)JO?e?O?L(?+`&I$WF&u1mu9m7CMM*BK6M`=b3S4aX^SpRAZSO<`LT}fPK-t`7c(6Vg7Se;$JGwuleo=aZC+hIsMkZ(~HCLJ?RlkhiideAVUQ+Rw0|R;^ ztAHg7OM#2A%}(BwNy5qqQ0c@HfFo09u29eIHtg{=Mg zye$m)r%T$4n!)77GQ3vfwVh820w?lW6mS?s?y^Bz;TuXkrPfV_&Eeph-pB+qSCp&_ zU>uGm6IE!Yk3x650=05u25R5}I3&9=JFb|gfoNTZYakLhwASOA+w|ow@v_QdCNZ^t zc`0Ei1|4bwmo0$D>%5N;B`~U@Ne%#h2@|2;f)i<9Jl(&4{pE2T*T3=@5~O~`_!W`9 zG5tf(sww>6LZ)rAnl9djW18otk3_g;Vv2M4k$``9`9WA-> zG%$CASr#Lf&orC9s~cH(2AF%m`&)Ne0Uu>x-_#a&#ROro3Hk^P>gBRbnjws7tb2N1 z0X)Bll6Q~ci}YOtTQADrjlIq7!w0a8;pz1UpTOFMs~g*(_do@2tU==wcOy+s0(WPi z%)0koXbrC%zkK@O#Z{ET%3vNiG79+l{|o@89&x)(Q{(M$bo}w%yLTV&?TqIGaX_jI zI&u{7WxEZDZCLIq28_W1{;mpusq>p84BpkMZUweY;BQ_uK^*FWwv)iPjsYsrn8X1J zSOoCG+~nbHLtnnRckk_MH?Q(R$yEUDHQ?_DAAE58;b)&=1^Tzvw(q=s?>0OD@A~$= zYgaLwQY5c{7)gxPXNbmXVER^6Z8*hpgW;4EFujtdP7j#d!r6sMV!kpiXq-agIpF_8 zO3=6tiN=sxdd6X4-p+tk?JvJfsqHbC~UW1;X=A|n_ux4@Kaj11c z3%7`eC2uRB+{heownSa9CMoeMlPVqo8qNkb5!$m)YZkOi;;%f#L8J;;;xj_q?~TNm zW%I299aW+pa4fzpOz<%!Xr20{_ap^e;(fqbhn3m9>zyiMM<)8=K43g9;E2Z1BX7;5 zOCo+lHD~z>I3#|W;xlJW;jAyYuL37yLmI9!u91Bbj5qVh$7eyhJ zb6kR6nE>wA+f&oi6v!~Sy>)zo(pOG@{;hYE!9d`{Hl=A5ag-~EW-NqoN!(XbQM4^! zr+Of@2}$>)Z5MEy5ylq|@7+7usxL36HJz`jNGvo@B-Mn*wKQYJI`RgP{7EDdlGcXB z>~b=pAGs73-<3^&9u|Sskr!R@2>gA2<1yj}%Fje#bop-IIDYYb zb8UZrbL-><6suos&cJGi9rDY!g~5=HmCH?H5*C)AwFwOWyWzNeJm1~{`8?C+F>+I@ zqQCC1ePnyUYJ$dHgzL%Iv&CqrL4WiY{|-vf7sbc4UKIwfdP$TlfFTR|@4PW2#+6mT zh+r5+TICWn%pgszXyy6#d(g9f59oX6CbY#DN$P$E8CW2HfA?Kz&bl( zU;lGB|BH9uy?t{7d%z79z@UE{3sl=i{(|~#0D-H9zgu`Xs6Fd#YMCNL>wxQ`Gvfs_njOqRqwg&4&lQIsjgK_h_ii4OxhXoGMCQH?Xf z=mX}NBwfXTwP)D{%*z)DzorIF|ABi=mA-vv;R640$$~*|;)tnGZdJe?od=fHDzcfut3o%q|0_-4mYoEqBp}lW76shmG%V4;XoBV$V7`f{1kEZq zv29>kM;M2W0(M={QKkk>Cj_uMmLoT?(jYiCyAg8 z7wZs5My&5{hM6!%faI7`xn$Vj*>9Xg?s@XC$PX;fuwxl6oUQ9?c zDl{5u0L-o59T+v*H}u8gGoSGG9k{si<_Y1etuYGZX0LJfj7wYz&q zPP;d5Ablqn*EWE^cToHOE}XC}^YNV{NK3;oK`jInl?66brpl_gYIb*B`IL12b|I{Fx|tg zNJL|{VSV{!Pqrhqi1G>eP2g}If}nx85h`GK@&ghF&?ByJCE5-jrD}k#5OiB;QzGJ~ zT(q_H?a>P0Zqpn5?Wy&u-*K|%{2VKRX#bh7a{XHKkh6W8!)+(9gO9U28m>dm#!l0w z%u%GCYr$d_O#NS)2rh9LWkrX6D>4zRE-VMZB!p?3o}yRVz}$#c1+25SK{Q~#QciLZ zM>JwvxzY?a{6*C}s`3|X(1BTwP$-zZ4>SE6WLE+^Iye|2fMw=Laf(~-d6lp3UUb62$NQ`|myl_#W=A>`!226A7H3SvlOjx(GP~Q+29F!&55F zB4HPpGB(W|lI;caQ4Ssoj$z;=`~st2+k!CYFQ$2Qt+cMmjHNCtZ3Oc$k}H5I09iKz zGlDB7ixLssv>6#4I}Dsvz(`)vy~y1IX$3#>JXkb+grB3Y3mE!czy3TX;2aaF+H5*j ztTDDTu^5Vhu2d|E=QepM@)vJ@|hd^KS`EI|zP4K1D#`cYy7!M8Q z>8GbF5Saxn3NTea_SO4cRhKc$ z&gEx#4r_CptW`J?Jr7gS8^~WEFZdGw#k*)reDBWH_W{6n@4b8f{s-^w*0^zzQ6 zn4p2?4Ql*y%rtxrXm3Ld2Pj}1gR%<<*>^=NfcZ)`{0e|w1zh!mIH>k4QW%#p3@G4~ z`oQRbCNCClXW&d|#BT4)GTy5&f*~LlUZEr`s6lg3R)F(I;Npb`(vUg5Y^Jh-P(nB~<+0PFGqKmn-WV)Ed+OIBSD`AW1HSSeJkaX{^JXiOZCDo0 zJ9R99``wOiMG^t5C1`8IYVkC%D&R3=fM)$$k|l*s5OhFREF`f^Aw`L6g-&D;I1Go7 zFQZEJRV+ad&92fOa1@WYf-NR20yzM@#e@!fvs?Q?FGQv&KP9BAJCO8yNG}*B5BV7Q8 zMgbcT$@s6vZODCkgV|dcviF&+R?_#Cu_F<}Tz)19i&1*H<^nhe;&%!3@5l!2yPEJjr{9Uf00AAQXdHKP;J1gM%gz=JmoP&6Vn4DNNl@v$u z;H;M2#9)qIq6;vVa{KU2Jo>=SH}=zY5inliYI#<_HL5w+oVKs#aqu4pi(hs_!(@)Q zusj89>d>wg`jKh%y2{wO-?b3#Xtmk7(i4i-qcQq=V=~t(>F#6Jg5~FPoO#wPQNTyK zi2(upcxx9F@U8){F_v*+EgfmF_{AY4Dnb{r^NSsR*cn*nFi}*%4B%JKw-LZ=`>20m z+u-qw>ySBl|2hzO`|;h~!^69GktQ%v1N?oT@C(GbvAcWi-n~~a9*m#jba!L^29!xq z0iSNoOOuys&}HIqj6Gmfz~2!7*Kd&zi&p=_4kQ>{V(%UvJqMl%%bf$39`GC71J=8V zn6N+r|5pTXLly9<$!fGYSn9!I1?(qbX#p5Vfg8Av0SeHffH@30j|0F{z}!iUBLVz^ z1r1U-wr_s?0sV>aLFzlV@6c${-M673eDB(O@4kagn|E&CoFA9Y??(#^U=_gl#ZUt$ z4fCz07Qx_w?x6w(En6kY+LI{CG*P1#hjb*Om}MCQk07A}mJ+n1fKmRE07mj&r22D@ zd%z6fluu?$0|a5g{~_!Frv%^-h#Qdx#!7UB&xl`4LT-utr4b}v800G7RPoC;2U@-$ zrl)>SQ#U#$cEjgyR^r-=dDXr5%(*w5(4G~#&rU3{VwoOr)8=q=D+5`Syb1}_Z?mKb zmc>Xd{H4fthE~RdMFnU;unZ#MZJ}g4yiO0;xS%vT-P-0i=5Ugk2ZQ~6gl;jK{=vqO!(3el7c@wz&)+N-!(L4E!iV+jBaC>8c0Q`aHs;_Uc;^~oEM?ueDai%pW&bSiw?L z3kYBA0do?jT|6jbzVK?VYEmgx0c#JqYKqVzeiJroRdpe7)&>1H+5=`IG+D4P4htrY zQ(X*9rykdzTbz#t3b>3C7(2fhlm%Wa>cOSUAb{yiEo57R=IskjRlunfpHsI0-6Y{l z2<~DJSW`C~1)PW*3cYxXfz=t6se zD>4b(s@PQM%qfyVU=qEq6}rS}&jnS$kvj+MGO;iK;K<}y4rk}c|4-ce#m04CX@ce@ zhnGc>AlXHVBuKGXRlX?|X$qt`T3Uasg$a@7by-${k&L8isXY&t6N0OeqV+b|P|ZUy ziwL&BwH--l7-~2#iMt6X7$8XRt{wtx;0@$aqc?E!kZ#Rl1|GC~W}=24{Ai4O&-urF0aP+n0EreWnZ~V1H!Ne2SSS!#j zRxpY(JzVy`ys}FyQcO$$l5|l1j0MUX4A{>0kNLzORE{GfD~o_!Nt< zDr+OFw=bdmOr-ZTi8)^+)_V$8z#{~&#Blr3?v*&Cq?Z7gB`ADP0^viO)5MQExem<- zSBqSXTV|(0fs)6YEp^l1iQMqvKBLfg65S*~?o}5CpyyQL$o=11Sb;VQ9Ih~(ENC2|hBHV+FnvHPg`6~jsrXFi zX%yC=`)m-n4Fi@MZiWK)d1#HnfcyNMQl_9hj*+9JDjjQx*iqf*f9O z)1>5DxGpzP9|OsBB1Tm-PwN4g@iLMLqSy+^C7TE~GHgv!$iDQ~RJFM5R>tvId%%`O z2+uvU%$R!YF`a8-BEHcsW zTt7qpSf@{a>sy4v@6(9nRVh-Fyyie`J|uAy{+3)zy`_LyJRELuQ09eWU|O=-nFn);ua}R8ml;mUIQ6FI!;^T|*dy17K%B zsU2XraKJZOA(-`7vIJ0{4%+62Azne6|Wb1K)RnAI*snT3l1s0tX z0Mi`Oo7cdAH#o#24-iiHpJvj_OgLcxj)Q+6Pt2sufbZP7v$h~{NbMIMLt0S)o)Z7b z6&V9wC@zE)cq(APKg%*;x4;V;2rN^;GwOsj0VBzMg1|(CCz%Kj96NFLI05i!A;1Q} zTsb(b&GKU`)*{nvAUG?aXI?||7h91F(U4x}ya3u>FVoi}Q%CHziwIg7Fu|nwPOh|s zF%*M1B!S{423!vc&>Buy)KDY`zjK_>fVf}BfC+2~GSCC&h~OMyFqfr~Mo9nvQaVNL zxd836%%y40T z)&PP%|8;3&ffZJvMT8|73@_+{r@+I*c5Vs5&JRnOvAQ4l?yq`Lng}cQ4$J`HMB%qZ z2So^LbHIkcZJ2VOW55_tF$2k_90tM7zTD%?IEI7bvq_sxJ1KS+rlIsu>!*HI{w(UM z#7t+Cumm0XzOiS(&Sj%u1?LzbocaSsNk@u22j&S(lP9~0AhXr55nfcJsRGS285qxP zVZebFNupg1-Gu<-3MoRXI~IgiYPlg2Xz}MLIZ2lJa4^Din1Tb~T?XJmcEBo+9J;W} z1?5v5KgXu>gS6_x1?GgOw*bOC@SOA>m^{HAm8ZYOdX(o*UwR)@cy$0XCON3E%qH4* zgG*{KCav{hdMgC%G_(?HTRT$Ov!3(tk$6&;N?`-I;S(9=bpDsnHPpITNtq904oAh z3HtwaC1@Vy%?A2qD$r95z@ZbCpcVIE$?u8hfuMmF1U_I&Xh|X@1m*>X=mQj2v3mCS zH;e&4^UO0(pZ*5*fTdeLNkVZJiyvmcl7p&zM)Oy?zhJ>djzOAn$h)G*%ixOrI{Lpy zcovw`1E+!>uy}Lm^sn`SMR>*R7iXw6-^$JbTO_cMU#x&$nF$62OGDV{vf>Q};3@3#$6woyr0CuBCz5wlG2H%mHBS^1; zChi`<8z625cJ-oN6L4T1N3xkGD?a13)PgBsA;2L9YY%uXGlj$i*k*u_s4uj0;wUii zrp}6^MT4bNNICI>c1>Z&fSZt2$BMuoo5z%bdv^`1!Il$dC5bv=?SZp>t>E3p3@b3r z$VuM4ST2hi8WTy%jB~ygEdpsPHs>U&w|NfO4XP3Z_!4w!5BW22O`urZGJuGv^s=@Y z0Bq`Iev(w}tV+(W88eW=U5x>k%?V4Gv6lq9!6WGzmz+9_f2;^ym8qjxUWZhj5ebVY z6bUps!zH1>`G5hR;R#?ezgpd42Yznng7gA;%Vn-QBW{kqM0F)VVFF;P;>W}gMyBa7 zeEu}~WU+MX(&_i#7bZN|+1o9q*a!?(EgU}90(8(cJIq)N;W#HtRiS#8FEc6KCoSNt zP%=x(f;NE*sgEISNDEl6s5w8%npr{vp2Ao{$A24{T*5ZZ;93TIjR9|MH%jyK2{|)w zXGG9w0hi)|SDro8U6qkt!C5Ej&|Smbn)ji!PB8807?SG&Z*LF)lL+f70Jss@d+C%! zqtMfkFou8u69BKRE$-YtG(yN&hC+%N@EHc+7j|ELnaZTc(fb9s?p}P8L_~)Hy~n;y zIX|ynL?j^y?`bB%mvBb+JpL-njo!FH<>(vFo|rs&^(@t7$(3?#WJ!j7tq;uG05f0# zVEQ7!fe^hs3(Q4m*q+DI2oBBFb5Bf}2rJ8gi>>^Z=766U0LCD&nr|GHDPZy8cuff~ zX|SFm74%0RnFxytwMv0!B$kl8C<*O>zYLWJB#7{UA}|?$5dcpPawC`l_%Z{qObIXp z{su%?47&upe~41`!4dO;Mgw?;#m_UMfu5NV20X4sXwh929R;>s=BuFap%I6`?uyB; zR#MYVPq}HNqRj#SAn=0bwy)~~b6S!>uXvHBlf$cL%i=O2tO8j z^;|aW*||GuA;G~su-3Y?0h~eL+FVwDrEv_HLgKD(kJO)m!bdV}*XEB_r2p$8XdLzm z6^{B8f#F=#=UtuS_%Tk*0>FKq0eji9m86~5WCHk&!-?S5tfMb+zG^p%q&^%n?ZUDu z`Xy1)=KwVh`oXGf_LIRZ0~r-3ESrzpYYLp$_^JA@ZwmEaqxQY_HiDyJz_<**{C z2?r||pMdi!>^As?BQt#;0C=&?MY(Dfqf6T6wco0(-;!g+s=p!{T@u=Be7EFsfJ-Iw zftCOUF@!F;oE+-8R+pv$!1pR_gf-Z|xjE2})bUO_+S)55MBY`wu0 zrZA{$9S5IBwG4CUL^Hs3%u)fr>KuE7fO-kVs=G zJ;xf0u3TB~&8sMV;!h~xg^ zDVjh%!vM^`axTz7Yb}~}-wnwF)-YfI@FW>2{)F6{r8T>@L1BVd!FYFfUVW3KSI_W1 z)7xDR5@7VDg(}c5zj}Q42B7(dU@*h*4LQ4cl6dm<$z!kH+`X}TW`G0s4pKJo;L_UQ z5@iw+0j5|&m_hS0dGhA#l(?`uEd+Q2{Fi93Fkhv=aW3}dECt~EZVV|bK_i5rc+|oO zc?v0HzzZ5k7^;_C3HqsemI2q(qO*h6LEsr40=7Kh1KI&bo}dCSQ(&G#G5~f6d`TGa z;lmOL%z1+cC5=#=ucQO~8h3vgeRb@~8*vG8n)5SLUV&bsz;#wsrJ)yex}tv3sX9?Q z4(tYqy$U)s;&8BAG~SDHbg+m&w287v22E{XZu~~oQM7`2WdJ5AZ$S&tybAon_*qKP zX21~&u%N-JOD0K8;ul;2+PSbxO*okDeP?gj4alt`@56_7YG>{ocktC~qrn!pWH}JN z1})l6TNs0fLmZYiF!Fy%3GMu#3rE_EuOdAm89AApF!*v2A%Yv04gPXLC@duN=q!A& z+Tk!t&>c<2Rb7v9&PddsGVB(&lUs#+I#YP|ILx!9i2$V94NI&&T}nyz?A6;x=W|6nhQpvJYW|rb?AYj24rF9}#U{&Jx#z2Cp_{Yw~NI!QeF z+BHJm!}OKlB$DY561N@bpFTksmdp+sI%s0R1i-?8L65CnOTvJU0e=rp{vn|+E&JGqk`Q=;5m?&4T!|*@4JN>7Rciy-r5-7i zDvG9cNSncDwFF%ZYtTZ04YZ3!>3tmG58NCuE3k@0MOk=NV{1~un@G$%y5%@jg8o&jN0`~|7 zb~@-`x04A$8kSk$oO)qN7J49r&M@GN6IOWhkywR|S?;-5HMC zw|Cl+N98CC>J3r@HGM&9#;9WP;3?uHa%QZNa~dl{;GkR$2AmiJ4l?tkq?`l^3y}|0 zD0%DVVlZ0xYG@U)e7<-XY1UOia;P=}^eqX2FFL#DdQ}*PI@9+MS(f7PGfPcCBCJTM9@enb` z4D6#W=i1sqPM04c59F)I7=YOk3t5#6z>_E65CCQ?Eb2u=f<<0ftaQkS4A`kIi~(QQ zIpCGn9x!j*|AiBl17L>W1@FVLQf$^>Ne}oV*8`TGX%>PXFkCku*hJ% zC6@GnlMoeshJj1g4MkX^`6hShKHnhDw1oRa3*9YAvAI14oBDD{P|DVwqqgv<;!XnKi#v_jjhd38Ev;YbYW5NY@BS6p`d1 z8ycJDN_I+q7+ByJGAyY;TL{BkTJRF+#++*c!- zF8p`7iTvUr+oPNBKmFYMW5B$o5;Oq#;-MVf*28%al2vFB3@jG-bz&@S4ELb>D8PQ)dZ8}aN0))Pd!GykV+@#(vs?NI?IQ{v??u)$1H1~k3G5jZ2 z;F3j0v1-aaV1{5I@QUDXA=T_(A;Mxp3GQnkTvG}R_?0+MCcyQ~{I8omYURDYzx&R^ zM_PDp1>Dbg+iOu8>>I&?!8V3uu}GLjQp+wigYIdu;@}xzsGw1RJ^~e%UKk!OG>rmB zt_W?^7X@e^i=-v!$fpmA5yx=Wha>SMmpD-9SA{&Jy{(+P5ZRkF!?QJa zs}qZo8FRq6AbLvTz~#+Do79=6uNXpkUBWMy(_=po3@giK1{?*EgmD~Z{iabibkT!Mz76*!=xW~TZq*8-L!?fAwruY@2W ztu|XJ+@r6;FYXs91!gJM(BS5dxEWO>3JFA*MvidCHbCUnaP|xXswA^fx%$vxS(qCMH>GD0coFI7b=xFjVAge((0 z?p%8ANAn86+mb=3YiaQ6g^O2{hJ$)+)4KrJnDa1?(Qm8psD%G+>}mNe=78s(3d#;jTZ@0zsJk} z#2lEo?{~h#HqcyaK2PfnwuF9B0Qmd6yKkPPz`d7G(m>SN)#*tNBs|bRK(%Ee!^bY3 z|Ni&SA3wn*X!L+F3!Lv%2FzyA7!wu-oEJ}MG2|c^HX&B*7%-Kftp_}_kX1li5+TFy z!opO*fHUIf6+Z)PQ%KeW{uuyR7_hhDh>PPA#V`RkXz`B}3XCo=TEI+y4^K}|A3IBP zz<>PQUn%}RbLmCSVvrv2L1_W+0|B0q60}yJ!GhsGIo?WvO+p1fNruoVktq|Mr4#D` zYqMAw{)$W_6=&}J>OLs&%#>y!fdT&}WWYv(>x{I}V$EqU7-L9qg_gM_XU(xe6=){F zY<!82lL&>8^TbcZ$E6)EJ=zFV8 z<@wRWhi;SQ5q3X&^cin$moNPK#TN>}4u4GtEpte27Rk9`Nv(-ZIObZwcg|$cbHhDH z1b=1lSLcvq_(*{_QWq>PLbKE;S3ul+po6v&bR1IJD9Qk75-`ztJlC6)bFD4hWX~pW zFOI{x_6#^`kHF#8uJWwZH)jWL?l1NNfV=xo-Tp~sf6kd=<$NDFj&x?&%FN1uBPYW0 zc9apkBz4la3hSzpubC=I6V)?H&_;{Za;j7k4VE{*@}^eKuSy4vqykNM>tz7mIkZ+r zuQslhf(EYJVl6tmg0u2li8UzVScv{;7V_vUUsdCM+a_`7_ORcK3D)01FZ) zJhwzm`6}O&-W4}FK!1@?p%5%*`Yop9n(dL*05AreW(&}zFbb(*O<@Ru&4iNmSPc_G zGen77RA8E+K>Xkr4;Wsh2)!jm=vo$nx82MV_%99nRt|A=Mk9PilP&l3pSf^hr&8iT zBkk!50oK~_oLW*tjDmwvICO|gxrwl(0zJC4O74(nt^$A?R(1vxp3`?%D2M{f27x&P zm;sp7&8vrr`Hrkjjt~Q$oILd-vN?N(!;Ua{r11Ma3buXYrnH4`ke4EPUY$SxO9}vb zA554L_?NrC{3V4m(A_R@*xpZfR%y;`Y2RSEbKk*{$z#vnIRD4rIDc`KaehdHk9Zzf z8885tE^aJIWtxRpP-DeQZV)MUV@MlXf<_nv*Ps{nm;x>yeeJd9|C0gm0&36;hy!*W z9A1Of%z&rrg3dOIL}-XOLK_5j5rSv}A5axEf30K&d=dbxMjQa(RnLHl0*`1AFbJ^r zfI)#5WCoblT+oA7+;xEpRCeYs2#kGf>pYcu25c&5G=o)vHU6u&?kp6i%!s211ZDuv z_JI8)Qd*y5XrI#zVK)mr#q`&BF*SRM1gD~T2Kw?|oI(Xn)hJ=W1+GD-X#8?PN_ntO z0n=^``YWTqHIYBpRDreJ!cQ^wyQFpbA}c zGAuudB%)5ogGB-@ec&U7VKt_d`m=H1f-}TgX=1=uh8FaVGCg20V3#Y167+Cib01is zZJd-iS-)oFE(C(9B%4}<0(a7+!t$nD?GRX)Y}C{XHUw1$408>P^W+l(;M~2X%xTeUv9Zm{w^UM-Iq=6Y=YMh_gERVwa$^{@PV3x|KZc`09 z-^eg-_;3mKO7an;(KTl|E7XAN&&xp;lB5xikV5|1XK!9TwN_#ThV2Guul&-2{BS9t zk?I_<0Pxw}3m0Fd5;V7J34j2;KtaDpMh137dc}GwX1|KRZ@zhRmprbR`<_Sk-Zw7% zl0f*y3(SFccQ23|$BW;%aN+Xt;}>A0#rnEa10!6O?dKZwAsB`x3#1I|D&LXk+2 zrJfwhe?ftD5{c<9Z5trKRbjw{w<}C}OAuip6p1eq-k=*?&>1B0zbaS**yR~GPgq7a zK=4*3ghA(zt~Ax2QEpcFee_utcmGaKzG#QKpU4_6bXgF1k1p_XsDd^bmVAYu%jlnv z*oiRUVfaDA0c#im4aR__-dqsqb#q9=NczDzlJ$Tct|plta2z#9B6xl~-nBH=-I2=$ zX-E!iTWGNQVL1kzG0tjhIvM4%609hgAi|FZB1MvbCl#zD86*I{+mT5ZwYFm%CmvvP zAp@CD|e7YlF6w4~B1y#*9l3dQiCib^gN5Or)dX;yoys;2q^lhf|S z9a>I}l`+Po#bd(XlA9$;ORh0olHB@KaaqgHpumM{wPEPXcM0fl)fJ^nnnno3CfPL? z4$DJCeHnkr=!cl^nH?fJr!TS1`48XDt3p0)7+e;V}kxlLKWhN)c?I z+ucjRTwaQS)9m?$U?ec$NZj{*;h_`&vFfj?@Yalcks#G5B=*3{Xs`@|=Nbi7LyHVr zCV){Ys!0V}r-D6S1?{c2^}M>TJcVfkE~vNarafuNgzm|leNiXx5dD^zpuXf>YF$Jc6V1Co{9VNwtM~t46t@( z^yvKM%P*d%+*|r*z{=>;=pKLP%)maLFs4e!$jG7H)8NfJn5140=($I~gj_Kdb9Fi2hO@##!G<-Kw(_j$;fHwy~7lzOhOUMY!1b9xFu$fRY z1tvaRuW<=l%0G4f61uz`W`bG;S`&}9JpOvh%T?w_9(F%#2f7}0^}HE~34yZ|SQ)U^ zpxrQ%6sY7&1CAbIz|j*Hp)HS4QjyG)!%^Qt*afyy^vba4pE+SK=m7@|IG8)qFoxkQ zFbal_CwG6?yFb_DXZ4sdBuO1h+R32%+Ku>zJ>X_+4sHDti%H;PJaLcP_qw{Qcx|yr zjMK3W1t(UL&S4BGmy|L+V9XPH*%i#M3XYqJp>rBwa8!ze?qwWFf7a-+*>J>KQ>7U? zP-a(BuAfkuKwiS-AcZydMc4~r2zNT6=GAls%n62*jfVa@)fDOMj^Q5>?!fu zP?*lMT^IL&X{{m@jy?^*Z&|4?fiH)xoks-Hsk4U=#5OPhCm;YYSD^FI(1`6bebzJ~ z9FeZ3S7p)I7@YHQBxA!?jmXm9oY!EbCG)*j_b+M;I=$K(ag3IF1VdX6mKhLRHnun3h2wDf5HpcG#genwtGGAq{D}!I1bAkZN z^?{iKs})Cn&PlG;Q!@y#37ZDM~L=sRmE6KLVv-4*-bVH7_gL}2~OLOAuYghl6t^kz#AfirXgY2 zaJV4CsX&B94ESR(U>idU(07G_5P85e8b`?3IngYGh7TnZ;1GbH_5gfXoj9lm%pLNq z1`8h0Otr`1fi(^pmKQ(^v?8C#Fj(ML+#?+T+pO=3O)V({KFTK05-T`5v%} zo=t|OZqRj^L`p+PXrjQ?D))e6=&y!Th_GTVO-C3(;>t5x!G%b?T4$qVm<@cXE>-b{SA32_x`1FCB~&5HtXmIEsVUd}jC`ua1^QiM)e z!`cB>3M{ZI(2Hd_Xah6mniaG+|C3SRz?D+)7jwYQ2wM1HUzU+N2HTha;ai>iGYq+} zwFlg%*9h_^iN+zd%qoR2JKipNAv1yGD#=Qy%nUN267AekpODeXq;nc{Lju|XsnLx6u!If1g6>FGLq$C%ZEx^rfWj2@qy2eUxxglHG z+?=nJtHS*RLerAcTt=oUz?kM@BVa1uA=QXMYZMbk3GyoV%9$`-SB;iPV1zLc4C}-) zXG4)$AuR)k=+MGtXJWn=j$b89$^mNXlE4|ls_Du!4GZ^oCR%{zCLXhxUYL-vsJuJw zO=L}a^ENQd(Gy9ac}|>YE!W29q@>z%>8slm6262GzLW}~A7!He>&b$7aF{2h!Y>*( zwL*^$%PIwKgk9kKzxs!U%CW2hT?=*3V9E_12iD4U5PIZ+Tw(|XnMX%URoFuIF#gwc zB6x0Dt)b^c7#)7e);6#&)P*9f`q2F1wd;$EhZZaRv__Hvx>{XlF^tu zFJ3)Hd~HC&e90~4>MM-BCyDsJe)-}BHG1YmqxX@kz}}NYZ(sNhsj*0p^#X8s_cFyX z-0<*AVGLKvZIsQCNiERN$}SM#zkTES8n=LlAcCf9^HT4~>KkvoaZ`H0jKI}c6M?Hb z2rNxt^J>BOYG%M|A2=2C!X94GC{KqaXsJ9O1^6EIS-`J(71kex3^-I_X(;dn_mCwA zNmbC|#Nh#0w1v~t;syP!zp@_i>5E!|o-_mo1D=pRvT2|R-HYPIVddwURH4+wSBk-e zzjaih0mzJ;48VVL#L!v^urXi(;39tyVF~o(7_e%f=M;kld+Ub5s*5%TTxI@Sm7L&6 zy1>eSnfuCkl3GIRJn)=f-2`w77fO%7b2@;(yqr-$!~BZKuL-bRv)7lLJ@$rN_c6Cb=&|L738iQkhPrx%Qt~r8L+3oR%-5Roq39~=99S~*bd5O zK>|K#CtU=0gU(Nt`oB>0pPZa(c)^8ZG;G}Y)sYYt=ye2N^4d!5`+MY zUnQ%xFxSctqrR>(JTdSP7s;wb$XKMWwv$uky-IjffUAHnY$~dSspx z8ZHn)(7DH1fXqpVV9gAn1&1Y+ArO1?G~=)20aH%Z+8R%Z@zZnu5ypN2y;gjl)82O| zgI=0^~#j%7{ z2ZH(Q8`EH)=?w;KBCJdgxR?neUbr2dn^mf=p+K0DA^}>7wfa3j%W!*ecL7D!^Jm0lGM`G9#)lanNx2 zfim!`{u_1ls2va$ZpGj=Hh=6~096+*}t(5>ZqHU=r=zYhcW6@66} zd%pyC6@6v-CVt+N<&!Ty^9(pkfMu<1)j|yxwuX;l7@jlm4GuO+gp~qk>d)B884^d@ z`i(FL?9DhJx?&QPga#N*D(C<-BLMKy{=UAhCLPvZDrlfDI*0x)Nn4}II3HXZ&X_+( zmT%-{c~LR$;-^ND69C+4_me~;`mdcJqBk*CFK0~v9AblX9#gNyl4Jlr(=eh^DTp+3 z;ebXs>AsX=9XrG52pC$>O0rgwr5J3ST8@~2MWwH)6pyP?ST0lzh{JQa3!MUcMXOW| zgjgkgChyYWR)OHW1w7E_m*?Qv^5CfpM15a8{>s@y(*t6N#luA--2CE055eysatRGz z^Mbw*5UBKgg0*(t@)wUZ9T@dz{(B|4pn*^ba+t;R#DT93R_myAq;H%kunR?EPw1wG zaAQtm31uj_p-tg}M)o-|mTTTFHw1tiHpsix*b0m~w#;Zl39#z7BzmY8l%RcjZ-yK< zdR#7{XSbS?R1sgMCXD@vp>6oV=>$)j`%c)?k5Oz;$vM_|W*6@Z^p z9}WTFZv%e^m;sNBWO~3eegsJc&@;YGt)i<`(+5!%o?@tELrO$3s0izCoMBec@*CuQ zf&rMmP;5-8DOOts94MiWz!NJC!;l;!_6r}32z_Irz{9DWSJ{?Q7%=9LR0A!ioWc;$ zU#>ydQqO$Z1xp$+=CLmDd*v45kG8D+`%Z@aTDP}FS4Here*8sjjJxlBD4_f4r=R}g zr=Rok-Os=K>4yZs*%okU#1UFiF1xU8vGeIM3S5{gcmf>2utg#j0zYUeG7pOqizkr^ z;)WHiNz+%5X0qc`5^y z^|e7B56=4@&KvYF5_IqEP~rNOo2d=mb~m8&raJ z6gUO#RR&D5Xe&HJ0-Xv5+YpjjVMz;E+3%bNmzWAGGX{L+iql>h|FxFy7mq_d1Yl!2;+W36a;SuPhc=;n#R@ z&Sen}%h8YFmkWPx5?Er3d=J>`h=-d=s`Q8jfPuQ>xh5TSGyF>bR|ruE!a*8Rq4z=EzS^#Wu!H>m26( zYJ8EZu$cHtGcwgqun2Wa&gogx{z`(12x^cLbgGd_RV^bEa-?2Q#S?V^;A#mhr^-Vl zX_fn&3ZFuP6+)fT*%hVv9u2T#;m88fReQ7s0m~S$JUY(_gq{G49VJfk0>CGpCHS2l z?Cf;xw-e>(fpw}t69li734!}N$>fM8d^__7!8m{>$WrF-KKy6q+5^^DoH+Oho4~pZ zUygF#_ax_JSz2>|zAe^4B*I*E-P9^Pp>N6v%(Amoz_!5$0RV5qLK?g`hx$@7QVAYQ zxwxkFVF6&B3YK=S>an)6Gs6w@+ptozK8MwrMd1}Jg@z-)P41Cy$Hn>z@EQxwP8M^} zf<0p+m9e8a!(pIr3Tm85V;}-NGQGpLpsZ>J?5c#>gjAPaxZz`tNbv%Izh}Dk01enX*!33atVXi1bpE-y@U&EWSYg8 zw1%vtC2bDp6^DVv`Mjv2BQfI;20RM}{4QF+3rGKlW*2DJ12zV%BCI0DfCC8DB6DjG z_>UwF$x&b)%8h3@BoI-+=ER|6NFIPEn*dl<(3dU;0E|f_0bs`8ULHM~iDzbVm;zpz zDf%b|)iMj@0#WNA!NPy78|)cynuZl{4Eo!| zeZOd1WOk4%L|3@3fi7>dy!6sb|LW(Te)5?!!&1}_8L&+t;mm4LNOh?d90&{b9nR9< zf(!r$6u3~35JHrnS5yP-`@lK`Y_Y((EC46!o1AN=sMZ5k0M4kb5;rECgq7!<9*>zR zHRmb~?~!$s%jEwC6gZ=u^^TDxX27h1-nrMW3|IwKiFhxO>7rLqBPuCr6=B7eG?c}?9+PJTMhO~-s?$W( zs@0nHih%plbt4lxFc2QOo0FJF!Y+uFfpNJr` zuRQ2uB9)74l5#5fNYV+9>@!T!5U%nWb%GlT!QzsYN>`WxO8dZ#pdajT(lKD3lh~>U zK+O$Ap`yZJz&DCqYJI&XPH8x^?Ba|Ua=-{K!L>vtEh9pT7q_Sz*CapC$m6Ifmg;? zi~%1wFyV}_d=GeJ(n`>t0YCjGO$=C6SR>K|*5O|s|K$l_esSYrR+Bb$>yk`FF~AJJ z!h2Kc2J;K`0BnkB^MYRBU6i1SM{lHJ#t|^!A3Fv-CG?j~ID&B`WxxW)@lMv=FRw^5Z2~jk&bxpEyqFyKOx~fMo*7I=~KpzpUf?p(nZ-s{1zR zE`#pdZxinR{9nEFHmxtc^!1m1{`n`0zgJufIP3t66NiK`0D*hN%NFR{<8@e00$oc( z1Qyse3|8w6>=d*GEJ(tEz<~%_@fQb9#zik7PP7Z#79mc*f0vxoR*^iz0|+OXYHwf=7&O+LQ1VbxO!FpEJYCE@$Wf&MT8ur2h7$?0fg^mTFV`Q&8Z1z~Jr||KJ<&TLJ zWqyD(1})S=rj|A1sp|8cs#W)eLoYJ)G`KDR9?11rE-s|fA+0uAmqL2~&V5oE6Su{- zjW%z?!!Coa%pPr9Ik+Z?uqWMEbz)D+MAf(lXJ*NT$4CGUx*n-ixCNfdzn{^9)!3*v}zx4_N1rCY1Y9?O9Y< zMQ=hW(M6CSqx`J0t0RKJQWmacwy_{RU>zQ&w_4wW0XyVP0~l5zbiFy7 zkgyk1z|4RPIth$4q|s_Ltfl8*#Ys)^x8_^G1)V~wq5X^ca~(kE3(&Vvg*FeYkF@~J z;QQY{`Qn?re&Q<4@4W{(Gvn{4zQ+7EF$3Cq+du#G3nRc+ zlmTD4;`pypV8ve}>|*EBQ!8XMfqRSrH=GVj9HGN5aD*+G1&00^I_OA7k-P{R2)xHU zZmzk5J09Ny0Ee+h-5Ph4h!iJ8p*xlV@Nh=;%;4LXWC?IstPCh@(6yyA5HV)J zKN(Cs{dKdwm;;vQEGc$VwKN}Pl(~HT$J7yzqe8YE#TEc;1HmP8qXciIS&xQW<*YiY zf+}4&8l6vY)u>(+QB8J{8?6`Y(bce*pS$XR3-);PCsltDH_2sw-n{)1-9f<_dqCcy za)oA)$cb{WGEHQc3@FcB-MLEkhi6Z+-iBn)@RsCM2IjwP6-^~*25-=pPCBSA2@+FF z59ghJEi1)gUj_zj^CFDt#wETl7VgO7JRdGn952bfYm%2*zg%RmYLu$k53k|BxbWxL z7`3J@FaHGyw)p(C^cAN0HZeYB-JKkuz58I4eaUT_`YDFxx8FktEd z|IC-5(t5D zv_oKT!%=tIE7f7SBnA({_0cL|Sm0L_S5%NDj-Xhq*eexisX>byM*|*|4d5{6ci9jU z!*3n|>{q!IIQESZaL4Ec4=Rap^YZYctUi`cy z2+V9ayL|qiKMM=cp$}zE#*Ykx(FUfmLX|-G_%5(hVwwBAbiIaCFU2w&O7Dn8_Rqq0 zA%YeaG%olqaFQrU4QEpTn_9Y}g6`*SWu z3C2h&dX6|mj^4*cKlkt7AL6qnP~G5dXd}F8h>nayL+|qT?kw-p4t!| zyX>ELW3=howoBenwsnDFQJGf`UMOS8abRFQmL1mnfA*` zz~o_B)aE4yp2tGKLBX+;w?Y6H-?>6`y6DB8}WXuHs7v0Kg)`+MsIiiURP0A}{!_cu@u==qZUpDrzvF zXTWr>gTPOL0sk!Q0cWkS1REC^Cnt1~67&SUOZCF)7cY*1X%E0p1At#V_wuSzVD5$= zR24MAIQ4)v1(?vCi_qgi3b7$65R(%aZlwaeQi!e>Q%wa10RKYM8j8gu?$LOH@jGw* zVqpev$S}CR2LK24XNw`kg=#(U;W*+La1{+;7*XPfeK_j$kC#A4=Z_469SIiitAd(f z5e4QcB;~(60I)1tEa;tRxwWMbn1Nu_pFa`gWtb0&p^2rC=|)+)41V+)GVt{pt= zEjermm^WF z-F`3{Ih-cAF~N%aB#f?0b`?SwGOKKKT<%4A(k+D11k_9JGI>^Z^6$p$!K-qIaXX}g zB6aoYd6&OMZkxbWy4t}X1dNwHJ?YaGdwo1U*C8i-@F#HS>flQqec@vNvitg0Y)1`U zVID_yBmVcxX6dp#i66PNA6p0Z=(K}>X;=8$yMh;~Z%&sEeo=i4O#1;&`}q<-qNA_F z-5>7-&u>5O4eHa;-;~P2{FP}Rn`Lm#ctlPI0kNDd&kze}#udzcM>umHHyQpORM@4= z1tNgh(C%gY1vl(vseHG?V1?jr{S(?Br5S|xXZDVE9PQ)(aSa~gdtzhh-FL^N2mBf} zffauP2E3vSn85YySX#gn-Mteez@ox4z&A&4CtH7(7A#kp*~9tc z|L|+aeQ}09urc6Ytg!xhD)sJ_Orj~%aN*8hFaU2%i5XUqhV+Bb2}^sxX*~@~(Wz?z za}a|u-~wx~Mq$Gdi~2dWlWNr%a4A!M#*U20nhmtTFEe1pU?sq2L|OOXYxw)n@b?pu zSAq3vclYhDQ$P150$BoJy8Ux5J-1e`q0=sLi2HU_Nzx2fT8>cF?)OrQ%nvE)cmeFp#4IACDgut}l1 zF)IrSoa9{QkQd6pd|A{pEtbTb2%VI>k_#VzI*rx-K_Yu4k5?fO#4bU6<+)=n`;lr2RrpN9Me^d)@b@$Q59{}4d z9WB?$QFLO051)d+-QnZA?M{M~Nq752U@~|GA({d{mt6rBszU zJ7U0Vo&hgt%Qq}UFJR*|!eRixg25|C{S1;Bal9q~JS6~ZJ>aQSeJhkk0)!Wez~33Q zg61(`X2AOtg8M}XJ)i*mS0Mv_S!GzH8FyaL6BFa~@0^8L3jp9EiL8stfu;Xj1ZW~Z zA)UJQ+aLS~H?=E3oT`QMt>6AvG2+mdyJDqtdi)FnFn`aen+D5=G5q)knK@vGx^+7@ z@U5rK4d4_;Sm*)|D<(7iN*aNSPc4AH=YwE?CZp zavA0FZpS_ts4XTaJ6j$9OjFkn=kbGgWApoe`L67LL4!#7Iy zQYVuU$Gx}4_D6wHN21v2o3}zag#e3fWYT6(DQdF5a)cXgWA$>@L8PeJOc*SY`6K!L zQ$Z2{Jl1UtSTwbgJj(#=W`ko_fc83SXWPs=E2wb&7%B(TmV9%r8a=sRu@n?#zdHc; zbu=H~GvL;h$l8sS&l6&oGRoXsT1~S3P}=X5lRURoQ8T7)p_|=g>O(c}o$MEQ|dt%gl^V%Bj z&*N?6!$g}C=6&m@o28{~=UDt-h%uf6e3`4sX^Z^}QVJZX4Sx#`^h>U{P|%J|5{ z{L4R4(()@ZPY}Kn_i4$%mwW!Q`%HsV{~6nd@2{69@*h??ygK}oP-nlg#w%^>ywXBe zqU!_G>#Vk-6Kr%M`2u{xdz^TC7=o)ctxX$Pqe0Be2udW(tr>>*Sv?~_h73*yRyw57+^4 zeX8iKIA-u5=D%Dk&UjIR0Uy9O4=N=LvL}r(V2S4x5f%k1pJV`DJ$daI$1$J>+&e*< zwBCu_!Z^|w5JspL(6FIQ6@oEqM6mhWAN=KSJpvc&5JnpV{;$7Hx%fPl7QG?X47~?1 z;5)^snRG+HRG0{>$qU+4&>;bCM&CLNu?hmg;JT7WSP%`CGT^9y3kAra8yP7U&;02$ za1AKTzU9;duA}r^7l#{heauWCfdd-_<__?Cp8|WitqU>F|DeL+B6M!p&ml35bS+?O*BX;mh%__jE(2g8z}D>LMz6Mj6Mal%SeoSa#`D-Q z-cSM^?YIVqPzeLfiBhVQ^&~%e4N;Np^Q_fZ&>b1`y*gFklF5C_E@C=#hSb z;jqUb7~I)h4at-&d?6wzNAK^;p|{gWpCAHkR+MZ`rTlln#Q@7j2+TET0^nafubvtU z8bi3?A{iV3eqL43K@YfSfxuHz$8{jAVMsEH1pGaK;i~>t2K=0gun2&^2LgPIJU^KM zPs$iDm!PQzO|VXSGcH795*QRXl?F67ei3=Z47mQ=zx?rUKmKtQ@{+jK-<^=Ss8Fc88Aq2 z!}M1v9F$XZe;ac?3v3zWS4`%75JO7`7y$g~cijjt6wah!{yEo}|M5L|I%7mh%s9RX za95z#PLbmfELr~R2yn)Sa!Vn2P6i!x}USFk2#Tg1rX4{d*l<^X-ni z!UEgg*JEFQJ_A-EqYdu*7&K@PRrxGbF%=bNoj9N^dpvYBu$+a+;6eso^6i*)6aMEz zFwHmJDf>M6CUDI=O6obJe5jrzNVr2H`enE$7O^zXL}$vQ-8_960(X8J1>cz{5+ zvQFqb2K3!rpWdVs**5iZ0Kas9LSM#UM&JR!Fr7`V4X#as1F!KY5?N6a1TTRWW|Vqt zp4Od}MTcFCj<6Tp&=m2XD zgV|uUW57i*qfC_m(H!n_G7p}h0`!c;FwCrkS&2G0RF@`j{o{{*Op>Wvx55&%>jD4fZ|bQfH)s=hh9P)r zX6D^H_gMt}loDW5LI0%ya6RM1k!Cx z>%#7n&I39dg2eEvu)8cfHK)RoIYKdAZ`NNGg66NZevjZh9S%&Pr4tNMH2YpD12!fM zCk~tffB{+__@vUS734@sttpxTz#R!DjJkrrBO6wd-f!ZH&{5Wi!{_`akyfsg$jPw+ zaQ405sL3=bgkl!BItDziJzyX(Gub@vjg~bJ9?VyWv9*BXzycbZV7Fc@6$F0?fVb~A zhQCJm3pq?jM_1UE&F%q$6NcaJ#8%eu+U5YaGjAvVKOC|jyNvR|oZyFZf{_*cJ1$=W zKlv=B{pWex`QPD|(Wbr8FF`YdUs)f2*Dt$ysNEea&6gN7hx~07kE?BdpQJa-T9&^M zPJzF8D%u7B!c2Y%d;zzc2D+O~ml=eob)(2D`dOPMCJZ(#AWZ1HhLx8ko(X3Pyd<^g z&VKKmM#wt>fn8^JOY`4#zg^{Mzc$fH!^ZPgjYft63;O!;qXlqa83Sel{F(sp%+zb% znK6sRKw&W8sel2)iV~_}rIHH4Gd2l)KsFvja$b}Qz^uX&0RAh%UjQ)h_mztmUm*aV zp4_)jW{71DX<{Zf&LGScXdv*4ci~_L%n-<@^^bqUnq^lq76fK+F5ddZ-~au`0N`S= zZjR4fgXT9T0L%n9%YY4lEsPm;xw0Y_#LEiGX0!_j~rYzkkB zw1ygV11`{r%}hN7uCdt-j{w)2dcdI#^dn3mefT*K7`=oUUI?E*#q5#xa$A>nperj- z-7n4hXB_~RpCE}r0T{09&h}LTPv;6*6D-GkgBEb1;F6Hg3Klo$RBF1zZjy%-SHptM zs)AMxw7OXs5*l_|g}5s*05-b~y~RB$5+zrnBCJ?y&`B8d;Mlc1#2hF2g>|_iw3AzT zZ)evFhRZ*6SOl}efN$S+3^-O4g!gbrPw7aXcM@k;%AxoCG}OKA>*Li zjVJ$r=`Xi?+e(6*kjaa633)ThU`U1;ayzBuDx7ogS~FmfkEn8xl_PV-j?Q|tbqDN9 za9r01$&MVjakD?7yS*WK&I(f}9Jy(4h%O9a(*?h*B1d_7QG!xK|BQgy_PHwMp|)@E zE3!j<_96R76~yGn;B06pdv1>#E$iLvjaT$#wSFXeFJYrY?neIORqSu4Z^M1=6(Oxd zf|u^kFoeCKvg{1$pP-B#hFbX}RRnj3NqtrO0N z18+kr(ET95RDwQ904y@-XP&km@XIfsySNJgK75!4srN|`p?Y!9I8tvnIa|-HL zr=aR^;$YAu-um%x{+y~c<*R=#qc=`{wQdMCUkGLK%KMOP94*K2 zgCBSXJY~h_0D6T0*EjUKsf-gPz;^>OXd%IH!m3tb4qZ?gbi>-fvLkj_HH-v9K|EKJ zG$dyREj&2nz*ocyD>R~H2K=^2ucYfM%r~IB4Xm44S%<+ z@Vx9zIMjnuOV9T{+}&G4&zV?k)4z-`ixq9~)>irjAx=95AOoDJ_yMC42( zlo!5rQ1-sTn@h<=u0X)!1?mUdS{1`+0W0Ipnspi zV(+|M@oz)EYuMYfSMMK4yW4tydvQa-8y&LOI24?g-4_@C1txQYU-X(a{mFjp<>2q7 ze>3@`4e4v#>raTvv!XA7>n0=Y*rr3=&CTny@^Z{0GSguyS638*2Y|kWziUc^2bBRs ziM1rFQedvq%5aiEFU<$<>zsfYmMcPg8G45!!a8h4Uu$y&z=g&WM1V~NZ2;WtgM}9G zf-&F?m#eU1^T1w*#hVXf@IB!ARN#e00w&O35n^EsX$D3d69)`}RfR{MjcY!Fow% zjz0hN!)E3yVM!U_edrL^Z4ckc5?~L%K>^x_e>?cCX*`*;n?uI9Jo40s!wv>A1_%7F4zz z`bWA!;7BY7+r<^y)uwb#louyPh_eP7N$6ZfZUoGK`IHsHWmZ8iBH%kV4K(2`+bvKD zI*FZqWo%)*k$|qORTFIq5vGm4@__)e_tCcFzvSSPo$?8$b?B^9!8Z~71^Mk|qYBjM z6oXl;EOMHq)-GPzp@*TWKaBgU{8_uD{JVham^;TRi@P?sLl%ZTu<0)v3c$ks?sE3k zltUVwfjVtl`m-VXJ;5D^JPI2J%-~Y^`rQ47f;(kk%Ds*3JJ}YrYxCu0NQwd#!@QOo z%kFzkrPAhWP5F2KqwM+p=P?&Ei`aAEScip4!^>x`3HoQ*e-@I`bxB|r% zM_>^M3l0wo2(M8l)?mL!Vj!?kM2EizH%8;dI}&W4pkFefhGpblvUxE-$u9M z@4o&)v8!oiz^^g^e*MMc=T4qjy>{&I>ZvoU`xwVL5^0II1C_t{>mTNij*rVAa4HJu z6k=>HGe1SZ$Bo|4ZrysvH-TjgNer>-Hn$7{EE&S|qN)B)-r4W+81RO(g3hR*eMLLu zz!HWeRCq)1eX8#9w+;eqX3(fN>nQLjn!t>^B{Sj>A#_cu(J3pjYAG%yRm0W}fRK%c+zX^;e z5{?gI&=n^s%+LUS`$Ozq8>*}+;~fexYwzWEJD!iVgIkBV2D`dnv8yWX8hyY&!&3GT z9D*!P%6l`pW=tCjA%7@f^h55cANxdpnZ*yU&G2mJvvlI`p?ht=n*6@Ym{L`4(~j9k zz5aXsiZR7qIvE?o^P%uOMzoi?@BH<7Lfm<5ioTn&C@5p28Z{JK(=xLJCt_Ni_$wf6_8j{dwRG)0z%$=8^qrX%=;&ywL_^tFWf9ieyX)Koz77ChaZ|uf z{tWnC0R^@murc7HX2ucZ0ZS8@OV9$qo&l$#L&i8RgCa*IQ2)1RX~1;a>j-eCGGNr8 zrxk#oa}1aO_;@|%D-TmV<5sXHz_<%qM zjrd;zU?{DAdkY*`F!PAg-zha}2K3g=LerYS8@%whj1B2*hYT1cXbWQqC~!EC1S71C z%qS9FlcM}ABCL|@1j}@i6`-vHoN5yo7+hmZ=$cw#K@1HWEN8{RD9q$HlwrXLy2*;; zL-w)Cz%NMeXYKV>R(EcIDG1tY3+^iIB|e6MPfV zOlbw$5Zvb~II5gPVLn~X)u5xC$6f3E24t9n2Jv-);I||IytA{|Sry-@yr67}32v_c@QxPCbP$*O~{b}rr&{*aV;y-Y{-kxgR zcrR_&H|M$VJTJ6&o*8cdzk0!OU|vs*X|uQ@+UNmHG68)_BE2@a=G0hX*g*qH+zA#G z1_lF$c?yRbpM=41T_M|*iH`1$Knty4ykDn*34tR9V3J@F1HOOfgC*g=3t1m50c2&s zCc;tz?66mivZibZSZdI>T8uaZ!DJ|R242t%!6bub2FxiEyea&mHH5&_KkuCX>wo*h4w^GA>LjrEP-7e!40wtG_@iHc%m};{ zs<5Q?tZ_wb8GU3*DR2=4m=Snth6(Uf^rkkr6rB}eg+1U9c4ghLOT{3Ea3eMLyHQmJ zEIQ~IxiX@HF3A*<^?@5YkHjHejK0|6D8D%?K`*ley1s0Q+hjy3Mp$&l9pHcgKV&No z9`bdX=g(Ssud?2-?%ayKvW4{b$}+7sd$esm;F{%+E^96*K`+T}>G*G>zco=`HJI{- zG&n7p3@deGz#6>}DG);nozp2Kt3XFCk#N|Im8AroSPR&iz##&6F`NQ`J0iz`^&yFM zk0tn33)d1x80EAHocO+P;>LPIVjTHj9~Pc-VGM(4s5pd>lNh2H#=i`}w>JfVW9|P| zfuqELm#hRW4wNO8ImHDA=^LMSP0ynJy+a~ZC+CdDDwLl)n;cE&*XQ_jyixMB>D7e4bI z1YaRDctyZeT%|}l>mk*Bu8;j5Gxp_Qa>m*}i+w(Vmj;;vy|}g_2u%LX@W`My%pwFH zWE6HLS?r*!1ek@y6VwZym@wVMM9={43j8>Dw7ygte&SuC!1u3z00!*f*Pj$|9WAeU z6|^$oqr4q0W(JW&h4sAa0e|GYpo=QQN=*jMMp!(lyI?^K0>RAaWkn5|$B+bobq@GX zi~)1R6w%*f?1gps*d*G=OG_k(CjPrd{;PNY&$seNgDBvsqP2iiX2A8ikAMBqM}Pk@ z5I7V<7iEOkg&-*fMh}?Y0io~n8wS6qKd1C$w*bJt2OO%P>wX|uo|$17Fu%fN7`@+( zR4A~-A@KkbYR;J+u!G;gj6+9|3MfJ|0vG1A1Z^6uEmL9DT@(^SFxQ1uhxN$iA$^gV zFM8zGCXw^QHrT7UE8rWl-z!%#?3YjQs+Mj3NM@EIXLwBvBoz7O|o%M&<%HxmcW7hD?@~!|F*Jsy(E;t45%%Z|t z$v|)`-2&42r>r!gQxhjIg1{XQ_;^lQF+nax0}!`jSkV{&a4sP|mv zzx%qS3*JeI~b$cn5+2FmnFAe@}1W!6YHqV5Yiq2z#yujS+i@;u{you(WI)17-mJ)Igb&ls+z27!(p#y z!94w|{1>%ojbacdXr{m%2<$4*SDLM`-g#s~EL#4Keja`HsFn3Td~DI#7_U~I-^q~Q zv8enrqr>=iBGq@MwJkpY;Tv8V){w?cBvCFod&kc1fvJeRx}T6Q7Y z0&bULguZJDdW#fTPwwyQ=KaLPy0>!`XF*GJ?T)X8TQAI5iqu$ug?hvDqg0+Vk~PD*ptw$X^f? zj~G_g-5Y79vX$Ql(>+Mt#})$f*jT`UXTJnevHBA_qeX6U=XO`#GZ5ay-exl{&*Lrg z6J00J48BCI2e`bx2ehyIXJF7Blizope(!a>E+f3xh5Ihf6YefD=x*wXT);)%WEu1A zU@1g{5A$piF<}N_^X8z5BnfVqR0L)W)|e#3*dGui<~?G-z+bli_9o!YdmHd8KYbCa zpw~aRPgWcoVgwy(uW&*fcx3@jSTnEvj{?Bvgar=FXna)SkfzW9Zr1|_YnC#uIz!HY z1=FnV!i3pi0eugE0V@Jy4wxA5vrSf%EW%<6Oxcg8ZXQ2Lc?<^_g7*;ulQHxR+QGeJ z50Y{8n&lU3BiDMs_4>zu|ItUk`S{_-AHNftVI4sz1MH$jhE=CfLS?`-hBPDaTO{}; zJVgz}3+Uw^!QMyhhK(NX(eU+aruVzd z8{_@58c>1)Z*SF{2Nu>IKrqFLKbS2E0*eq@AUEv&HUjz^1>?ZO!!8KPqL2iE1$G6B zwYLhB=p4M0HAmM(&}NPil#I0v>=|$({1*gT0*}-!BQdDe;tw%7C{Uf3UyrYh7yjoPl7U&?6mSsXWWHFUJP;8~yE95az%>H}=ud*1 zO4WBZ!^-BHiPd~7%bIPuXq!N z)HzXlH%ei^RD^#1-Tx>?9ALm10=yt$4AumG{;BL7@RWk@j88=xCw5A9krR>!Oazz< z=o6iu0fPVw1AexN0iRoCGw4@OesA^I$>SHUPIDR(%^*>V1p_b}TT)w@-s^&6z-j`W zq6bX)OO4xG1i&BuRw%I1)}qS=#$#4EGk7I6{#=9zi?$i1z+k`|^mMm81HKj1pwlb^ zu8RViPnqbh{Nhjl_dmPOS*L1(Wqw$03DOw+1d_%vCPily%{Nhhi@8 zd%)Tu#tkaDLM)zyC;^G30PV!p?D(NnI`T$ahPbZ!9QaMc?RIvF<>fPYpXZ*3|QAHI>3y-DdTdX0Tpy(_z(757EPb`o(zK5 z-1Kh{p37@qp?A=Hd~J+%tx3fURgkZ(&bEV~c?U0J4hP2UfbA}4z-4}KbPfRPON*-W z1{})t=39^~JehB%jq|}H0)TYbX0P6$ZocI;&d>XM+MR51@4aQo++w8E{n7wl#{z7e zZ}Thusmo^O%yu{Zm7A^CXI{sce`zhREqpBDEJ3bpOnM1>31?}W7x0x0;JbKDZo_?y z3eN`};W6t7<0nXOSXU+tKFFi7hQcF*`}+4~7B-P$CJ74ME%aA8ut~8*fQ4ivDndW8 zG4w9wYtLT4za+ihg@uLe7_bs!(m)dcXH?LX6l{}7)&YJ^8Ss=d;&2SO*@y$$sD*Jb z;29A>8~pCGF<_ZPnqGaj*@}|zmq&pqkb!E@S5J@+2aL2RL27MqA3fy^G7i~~vbZqg zjj%9_#7$r@U@Ad>1OVRhGUz%A&Mtv44eHlI@;8J6r<#T|B?aw}0eds(dK(!ucYQ&D ziShoIfAc^8-}i^^69c9yw2lH-QFl&Jc#aj4BluI6JpUCvRxKD0u1P5xoERK<4z*}N zZ(YL=mL(3Dz*np|)R6KKw}G*K2IN)p`^fcuA7voeH+sF`xvA;v%g?~yE6awz!Rn@f zdoVP`xjXao`QbqNY(~(H3;hfMd%~h{R|z1`&4A3T9lDQ#!38Ys>_D->h6PI&217UP_${ zWLUW{VbIbT0Q*c;$=Y%7(4YVN+dnBIc(=-B=@RCYnA{Hy#bVo0(kioLHB{CAO2{r2 znLeVYtd>xORuS}*;{V((y|2rVSLT1M`rIi4zx_^p)i2d&K2g66H<21toZECxAB;T+ zIavrpAy|xsD3pP=EE)wD9kRBdR$GwU1DVEx$Beam?V7QsH=Fr8 zgW)Rl>dRT=Uw?hkmg{zMoe@};Yr*Z;umYArh?P6fZ>ndyL&a}asq8&;=d>zWg{812u)5{vowe_`@y@af_AG1VQ0j_ z4ETfRr&I=gw5TqR0>V=&!V>s>KIj2|?0Ued2(V@ltxHKooZLGD=!E`R2Z1{S0*o58 z2Vm!fwfa5sqGSygYoMnQhIByc(36`ZOo3k`_?=Ohml`xu7*fPAfQT{xH#1-xMq=qz zT@A3z40=kYfNf0`ho9mmFf(8xz(4k33{ye>HF(Y9aF2+vv<}To-{m23)!o21GCec{FrUF- zk}J%<{A?O%pyROp2m=NMCj1rC$ehZUwSB8590Hu^0jm>MloP#{AtB#2Rtg*`1vd0e zTn{%9zU#(5&;YhCUCAIBAgJId?*Y1bp*JpBdkAnXA|Ph`>J28Hh3uq_**VJ z2(}`$=fA!QIcv=C0l+t8ZDS599y|t>La~2Kaj|U;Q;Ke)V7S;P<)f zq9E_$qNAFOZSo@+xajZH^s0h8>;13W)1(x^RdJnl;c;g62sa*8^V2#NAI7 zJpr~n;2-}?0obCDJPK14n)xqnGa$gwaSQ%>0B&KxPn#9xv6qh#{!X4acXE&bm{I)T z%t6v%{p8P=#tndHiXjHWjiYW&V8?(z_Fh;=qzEmzTZag%=!TH^)EF@Jf4TcBB4{Z= z|2Sa4#(qN(-oOab24;;2z6pYV_@{sR!`Y$x?6t9BGA#C@q$6hv3(6{F&?#!rRq)=D zU~tt&f@?e!jCmyS!fMRb8(Mvyvm*4CT48xJXr;kchX%|(a&>5>y|%z%5eVNJ}70JuC%V=z4ovkkW5a3ZWG`Bh0Vm|B)l zJg0!%;b?_Vp^8q00S7HrP)%Sj`9T9jV-7uy6Dt@e=6fY9R_rw-RiRq}*yL4FVuC7( zP-3o`Gt5k)0D)aI*iRT+^pa_w{q%7Z~ zh=UlCDqC_4&4Swt+^W@cHIi|oxGHf-()6wJD%nUS=D-E`U==8WFe|{a@n7e@ArRc# zDV1l&-p;b?{YvXsrO&LuY8^Cc>TEg|`_(I=4;)5gTWpU^0g5lS!?DE&i@z73Jr21R zA3P2w@6h_?2)pPJD12)SA8hG6{Taq$j+5mUXftM+yJHkMBuNepgTo| z#dE-ty=Xn)0Dz4FQydb*FMS}ZSI>?d)Dkpn&?fHvKZ{6smz{su842rS~O4F+V0pxed~W{S@n&JLOhF!A4wbd&)2 zzfcEYW3;+a)%vsWUSKa)Wxz3z7ob}ePiV)3)r2y|JTPynhc+t?sX&8y@+h!H0=qyY z-{*bwj>q1|G<{nLZyWR#;H4A%F#IkH{5tOIC~yz0{~u>>8yn@Fu6dpmh{6yAsGyyQ zVB=zHq1+fjytI?>A_W;bg&iohKohkaf{qN%M94~vq6pNEv{IiQiCAo@8_-S`GzTB~ zrD*x6CCj68{9)u(;s{mL@tn=!serp@_5i@eko@h+RA8kN*n-hH&0Gp{S~zl{J(n0jTNPGP+~f#G z6j>E8Byb*0yFrS^9;A}N(-m;e{5qV=C9kiF$W8fMN^PxjpCOLspr==mq2G03qNl{`j`55lOuRb%>8EVeqY#=&CjkBxS5*?GL;gf#Q{)) z%gh1)doP^Oty}3J>lfJLJQ(7XIDj*Rh~1$y`GR-wVwCI7>b5Vs@$ShBkxhy8%QBHK z&I5i5yP+rVb~^zhhFv}e&UV{#!*%ikJoFRY@loC3hsIC%g}?Oy`_*MvWtg%SkC;)+ z&5*tPJ>gnkbH;A-m)d<-ukUup-#2c?W8A@CFh39_Y3BIH;e~nXZh$M!@ws>HN#TJr z%G3Qb*k#w=8QFRjzP-35=l1KZnIGeRJ?hqTSQN)5h?B_lkAM8*?vt*Dk7!rYySsEd z@`Kl6N*I@2yI$Uf+ANX7$|!Nn+RRqr?JsRt0PD9?7GWuZoxaayy|5$-{U@EB13d#h zojpCB2NMDxjLpFA1>F&fuxuWo1fUPb7GRoD`Y?u5o&!$2piux7OfiB2PJN&)0IdWj zgSV>y4gemudB7J`0Rz9XoVuB-?{Tc@wWSM9VEz!O_P*$P2S_yWZ8 zSCVO9xix@vFz+nFBa1q?7~;?sR%D4}1dKx36RuQ@|35{&m#4h(CMa z`M)%mJ3ouQ#~r^h^!)Vwh~Fz5zTn_J9_h95Zx!`eXo2nFtALU~OG^Z)#5qQ>4V5P5yqD2?0^reZ0e2*w=^ch9a zf?_D(BBU|IF$<*2==arAA@{4ha?t|Ra;P>Oqg4Qy3bP*Lcp<=+tI4r1A~xdR^%?%fCtR@M%GnSf5h&nCDRq=odgmA2 z2~{FG_`>!k3i2;95BQ?##lkfns>q9YSBw~C$Vz~P(Dz3W|He;yV)%(??9u+?>6z~E z4&knVM*T7oegVmc`y#wHS*FyVftux;8-Hb0O)A{8hj-7!qwesFnn{0M-SHRan?6FG z7#{{|Dl{VxGwYA-qUu#2h}XeA{5R$;!E5h)T+htQ=Ii_o-OkwVjNBb=$(21tpWX{f z%azc%cYkm1eqVl!Rj<3T`!P6t@?9U`AV8M&$PbRZChkv1UfadJ5=dD5^5$K(Zr=Q| zgr;p!5(}ZVA<0a!B7;n$72s@AiW^h=Oz)-+DA%Vq1B;Z%JEt}F1^ea@r1hAAr%a8uUA4*8z#+hB) z_wIxdw3cD@Y`Qvo05gWQ2CI|S(^fRHwimGwX5UD z4Z(fp$TgZ~e@y{*DS$-*&%e;Fv2-fn(RRxM8-goVgGKdQ!6ph=>XEk0&(6Nf(F-l6 zd}+b4O#zm5RJJ0hoR$&mk~alx6O3{`3JFQxl94t6Fx{Sm6{UACMa>&?z@?T_E;Hme z*i7frX$vi-rl|`WI@XROhM@BniY{Ro17MlQFeJ$am&@!y!Z{dH7BvZ+69DgqJ*7bx zUBG1opY^(!QN>DN<*-WQ_VTDHU>SmB5;*X;S^HY`v*4H91%9PWWsh!q-q^A-=)A`o zI%Bl?i@@F>1HT}~4lw>1{(v|A_4mK_{A2G+ym$YMKg5pGHQr+{f(NCq7O(fmGnpqh zy`)RSCY;asj<47qKlRFWwa11JKa<>*{Pg=XA7v){>zSFES&7VW=*KI5ISO69zRTGHa{uuFA%jms_{6%JtQoH*Q`{mosN@bwk!wmop!qk>#AMy2$O` zv&T;E+Pp=p5;rQ&tQYAyHKud|r=3X8{fWS}6QnvA2u$pDm;hEDcbEeHj73;p&V1tF z2`b-%CWAc#9CN^l7qkEjGwsa@OX{%1C6aBxl)#)TK^H6v;4M_Z$I=kA=YXMrfB3__ zFa7w^$(Kw5cgh%so}KSb&hB9dTFa(5g-O2}RB^3|TPon+m;(M8?VxX0yFw?@AyL66 zs#XUbh9Onp!=WBnOans;x50_h6!5?MjVs_b-P%?vVAx*4PrCAO;(z<-o1RZT5dbTJ zJqsK{&}GWsa^CupRPGvsv6V;78QQYJqoWpmt|ok|dcfCuk%9`=EO2u>k}bc^Bz*tN zGs>LF-s89&Uq$*p4H0O??{N=0Uu(i|pY|?G2)dZ-osezZsevlXc?QNWPK+A7oT zLRb)7Uc33u+}wQW`IhH%K7SzxTgs6Ar6%^4V7&%l3%h#|h4v(H&T6GveAZEk3RtEg z9jLnkPD-$BqXZ6)9H9xxB6D$Vz&6o5rl9_&xwXD z0JH{ENf8&QfOq2-)N?tvH^o0Q49A?%0Ad~WaVMV6%3)PM>U`k2Y%!(%B%AfgpKbXm$mp}jW_|rch z7&>FOdmRwm=9La0MDM)ZnAbD&I6H$Acm}WMvu~As*Zob`^I^p+_2T9AP54IgHobV| z_`ubc-sYp;?BS{pRn_;bW#!AnWtO|EKqIzUtiYtfP`>Tja#eS@e;$xDP!g7SIB(-h zf!MUn$^W-{(W^P$T5XxK{p4!P1KmyO_CT*}nZo(0$+6@gg9kH4SfLXM{DlI(+t%hqSSJpKafDF;y8v!mISq;98aSSa zcF-qe75ML<0H&X&es}P~julnV2H>-*fTaNIr62CyyZ7aj$6ng*F=&y%om(cCx}~QH z8e18xIuZI5tBw5xXp^7s|-F5pa*GR zP{4m4bHFUZiV9e}kDS4ZU?|{^KJTFdwlZi7L0kT}VhWh^8Oof^8e=i?&rf;z#Pi4TmWxN7|gP0 zslqC`0#;W?8*LP7rC1Hk#4wh%>lDB(c_f5eED4;?yBSBe{K||t@<|f7lqlewPdzN< zp0Si%_?EmH3qu(47)Z!OuGCGj8L8+46$*J`7&I0Dw*tKgL7Ue`nQqXodrkc28H8p8 zx;Tn9q=%+}GwnxfSN8tWs!p&gjT!=5z1~9Tz9H*o7r(pAV-U_r?`B$I?jB&^^9#l8 z7oOjo@qlQMaT&IPqP_FIe+V)EUCV5|T6vfpC2YD%R^jO8JSf98UfC8gz&jn4U z%FYFzZ;C9Q&Acuje)Fc@bH|^Chfmv6n<)pGXuEi<*Nr<7dy}#zhm(G##G1rOK7ePP zboQjl%zgd>cH#k1I5IYgb!1F-Q&Ur8W4hrkJ)yVyWgU|T=&Hy4{bM8jBeL~#KN7CS z!cHGDhC@A}PsLezDsJncz7BW&Q?km{?_0+Dhjkqrp3=+Mo3adZ85@3c_)VPf<>z)i ztdGINrlwv$D*)bt815QN0h;bi z>cojS4R|O_LTXF;fd4W$VMPay3WP8Ih~X^lv={d?m&2Zymj;(0boSCh0z%>+AT<_8J**o46bB zYqrnL(PwyfwIc`IE(*9i*A6d;b}5Bk#d)LR?A4<(CI_^abE1H!CTDj)pECe&Lk8Go zZ@}+1AHhq%tAZ-vLXXCB13nGIx!^PKj zS5k({DR)WpB72UC93^Nxh847Cc~tQ( z7hX{`gJf#L+dqI#0pLwu`SB?SHwK|DBja>is{yv zYs~4k`a&c%;VZpq=O*VSBf}H)I*tZG3zXD;D13339z7VC25{ol{s))AKX(5h zuW@+Y?&NY*F2lHRO|IfHC99xzSP(p=$X&4v55F=zeBJ=O5egV0c*i=M?4b?W<@bFrBdei~#m!pqlZ19*C<| zL4Whnz0Z4kJ{f=;WtrW-<*0%4Wp~EP1qnQ=j+8>%DNU|W)qGrc#0(UjLh34L{--)^si zZYi0wWU0k+yzBs`5!TvuFHArID_rv=btwqorUGbh9}35463!Kxa z{9MKXI$Hs~W{tP%%QxgbP{5lvQ~5IJyhZW5=LOTgKZp)dE3~iIE~8%P?o5!qA(DAV zj_yoT9b%nciN^YB(M@Q-)gqe>6L^yitaG}T9q-U9<(odD;g7dV>B>hy_7RKPaM*A#F}0tW^Bp?g7R74S<6-@O|lf2|H{um0fD(A4P> zXcw*(?py)yyS;i9^dVMXp$gi+F;v4>hY*4u&~Xkr5~!aq;@ARTQwYca-_x_4;uW02dU% z%GS|r=`;0jn)@Y#0pKkFu)whxr`;n3ye%n!PAr$v6XXC!ITHpQWE8N+r0L(h?z;+L z_sk+|EhQZEtMv|%vIWvJ4NoGakx_Dr2A{7E~SU_l@CH z$KwbNNzb^9jXZN1Cx2z9EF2k(XdUbK_sF6zQF^!<`$GX)V)%Oh(Lv=@zpKdp@#{G3 z?;lq+X^$IheqI{Z?GNuBMCbM2gWmwufZK>%{~eFvm*+nMzAlXLQ(TwNB?KPC*94+B z?C#4NmVWyNflovT^Qs@Fi#S#W_rGEYwpcU5&Qn+wzt{o#m) z$2^LlF{JRh=dz7Re-h=dOW@cEY_k|%g%8I;$pQaMRzbT0K5-&CL1Pj_D1vUTg5GC+ zz#ZO)q|&rSlfWHNz&pY)V4ZKQJ{+*3+_XXg3xBDAA%F$I@ZO~U6$LB;SVl5z+@QCV zznzwy=7JQkUPJ)Do#udVM=K6NaK%fYtB2Y;ycxL7bhMxsKG3+T(uxup)@>?4YeC229n%+RJHxa$7(ARf~1b#~NJ`M7>E2S?#T^}RQ zQn)>;OCJGzV1EDZZB)SR#r2~sz#1iawF|fy@heS6#T*szXlc_;05~am-j<6Y=(NVE zCD<<`1-x6Tu=1vTy&);**3j%UvZJjtFRV2K4uF=GTeN^qTsOjehp2$zIL-R(JX5)) z;Q1U!|N8Trg3##t#c7+UfYu=vVrdXug}A~P7SQ>Ewf&ZL7z3veq6({w5@!>@33NRH zoDbDlMYn?fj>ex`(hlIYnHuPg-lwmv7hJ@~UivrQ{lOa|c|qP?2s^(U-F<_T!U1nR zWt+?@O9LEr>WxI$jby{8gV?|!1<*K7*{WNzj1Te=8YPuyU@_&mG(wI=9=n(f@5Ogj zeDfWX_lfdy&@fD88|lUz<(R|Q5bRtwg*xU}q^_-y&?c29`&Yz^fnkN9evtmbL=%og z=#E)V)ZWfM)z}|mZoda{P1VN5z^3T3$MDe#YQ>=4=7ac2n)K>*SF2g##s`OB@rcXb zfZ(z8r&gX_?~}gz`S0Ta@*^A-5quQe^?qLTmTF(;F@w>=N09}d zVkml;vUu1Mz`B#Lv_A5}v0bkzfHeq>TE(4f!ek^Sf+K#P&njS+VGa3I;DfKe>RI3p zu(??QvkWWo!pazNG%4VUWr4YNniZBR;7*&y;0loRbK2S0M25Kw#|J5q)L|y2}9EyVU?ZT5R|FtFW&C%&>sA9fHv1 z(weJOz!-ZFGQK&V0~|*kY|DkQz^Z`7s7aH+$O4!0-U^&EUugH+Q08vfJ@ehQ7J3dg z8N{gvh+`2n1_y$>CG(61h1EEk&nYE#llFwX@9iP#eC0z3QF~RwJYc=zj9-m9YwNF| z7|J&f1l#bVe2@5J<$?lcIaVu>*F?knq zs|irjAaQu9M0k&d05xw;HoYHTj^8yr@G5)@FMfTMBBQ1oW518WvZ_o(5c>M{K~ue= zfCm|3202IiW03p$(Lm%8eSwi|de%Qr3>f$eTp!`9>xUkfwSU|pj2bLg_ESlIIz{h@ zzMnizxjG`ml_!I~<5=%sw*#z$b_qdfUHc8gNBxIpJo+d!yPQlS(8I6D$2bZSPla%_ zBDsI~=+v>zuc=3t+Ei}ZuyZ2_%rCIQ-yJ(%B!a*5+;b~J&{*0!1Agg&MInr7NNuOm z9Pq&t?gc%xA_bf%;C~+!unhxd7&@;tSn39?2;ONqVCzCs`O69qSHJ>bn?|S%Rsg@| z5}5Ya%HK}7QSR-LL(M9y`t1w>=r(HA4)z8`1?+vmw~r?ikz4`OiP9>fkwuo>K2Ac? z@H0medJ&cb`0dmS8raonbyW+Xz5ZEOq4DR7k7piA5%j=7(uFi&h0jpF`MlLZrwzd# zk1kq1nC_HG5}0OKi2^nad>c-Xh(O=I(qs(%zO(mzo7E7pn@V3a^51f}h5$ z?^E9(fRA_Sz(E9`DS6k!>KTg-Kho-30r33RZDrHBj$a=^ScC*-4p?0_itYJ!WHCoe z>*fW(FTe{b7f2fmv8*sUwdJ4!?%B#dq?pUKZXv6|%6S!ZOKJCkJ?rwh-~gR7(X63n zTZ>lA6-mrm=n?|a`Amv9lm9h>gTO_=mi@mayHPB8QaHL+<%0(Y1JK+I%Hm4d%8F<; z-#~>{nO4ww+vmaBJasQSfdhwi7y}jX^C^B~0D1!j@Ea0-K63Kx)ti^K{7AB`7p4Z~ zuUP+`-XExg-wU=jy9s-tb9bi0D7#F|%uG)%F)qvtpaUk3k0dG2!LS?21Yl#rllYC2 zc*IMGM!1cPPmX}Wx;`3N5qM5j@MJ$?*dZW2lGZp`vKIo@KSNAgW(8n~s0LA&l=o8o zs^;~aaQ`Gz$Gkme$zqFC<9+(&M}o{FXh1%qS7~N%e0^kvT5MX5y5iM!BoiXGN@V#} z$?L1e)V%K?u$&54`^m0{G;H%X06A^TqmgHpfhj=y^&dwcG;UZP8*ylldE6lRtq!`T z*0&s_;JuGug(iUIBacHEU$^(~=f`vP4#jJ`u;8?QWPG>jAA2zNb18vFFnUVw8tYe6 zM(|kqD=g+ucm{Trw1}4fRd%`^#}1RgFQ!IVDS*F+V6-5Mepsr0Wq-m_z}XzIk3$N> zkl=)caSWaU-dFv*Ob)n>u*U6q?0Y-j!rVSX}j} zY*A<}!s=5OXoPsHp#Upw1ooi})hk&I?3K~X2{SG1*wrgs(vWkgx;mc8`(9}(ygF|D z?ds}M5uApg+r9aSA!tbn%drCZyW3cURg9B=E6fLvR=n`a)nY;OdmL=z7 z#Ccm9e?E~qEw^|Mc=uLFU{*5+FDy5%D!Brd-G;3PwzlMR*7}>*xGx)oT>45;HHk`) zmhyS;0cK=5@9H++5{t9KzC@o5m^tSucd_TofWtyoRzTBBqwM1qKw<3*E+~=9*76Gg zYt*@{x>$GRLYZAhWsYblGyI&-;&)UD+1AcbabKC>T;}&)H*l!5T8qpXYc1`T; zMdj}O@eqVwGKEd8JvsSkq^XMdnKwN=t~&&a`}zKZ-1U!mC6wOG-Ub*)`p4~qs{V^* zQ$Y-~LMte59!}YJ0lmS1wc_`FP!Syc=D2(CNVmSH+`0@q(2XFaoSzj?>{0fzF|H$02*G>wVbXegg zlq0RdY{IpA(XoW9RzVwpHP9?s;8%kemWtmKRs-D;6tLxhPn=LM=n#W$OPna7fd4M3 zg5GDNcta-=x{x$W3;yopJi;}sf!?%nH3j@8#P88pB7k2;8Fb|D4whlTgmQ;YA`C{* zBxjZ2%;bPm1?)~(>O5&4l(gSq1+~2jBhLf89^f#f41nLRzMa}ZLjn)@KJ+08j9c&C z{o>)nHfj#D%J!n3V7Yx{R-fQGKJ8fY*DB(L3Mf}* zWmO5CLqM;-JR`?Bb{ZvvjjVhMLRa#@Wfj4iB&Gs}OjhpZmDL1nrNSAv?+n9Td5Y+}xj8#NZF}-|k9=nC=g>q+loS zD(({#^Aj@?({Pz<0(50bRMJl4beb9=3#SOgvB|XS#*jX4{JrlGX3$yS3#2Zs3fh$? zlUq|HF~~FMA@6?FMnQSSZx8)x6g>#}tuN%H}N*pGUjV~nkFB!jd20spnXF? z^Dpb#nnnC||Dpr@LO;*Ri|8l65k7s=-K{ro-MV$>AF+yI*5zBid~oa5RatM`=-ofN z^s8U}YJBp}>le?Qxzu~9_sp4_55}eh%woB5`oe|t>_2)%>#wZ7Z20iGbC-^t+^iFW zr4>m6&^y;z4oCQF(-^+9S`~C`1ykb=;H{R6!5DZ=0bwJ6mSGEs-TCVfZs$F^pCs>Yp2a2L>d_J=poEPVv^a) zpebKF7=*6w8?ZJcI$_yFU^XHhs#>Dj#t|ZNUa=5#2xqIvM-6maHH4rAz<>TwZ|*Vk=UrfsMI-g4$Q%OW~W;TV+IV6M>&X`5J%q;xXtx zXx=_Vpx0|ennx0X76rUV0bHOnqT*NM&7mCHBGc5s?FtXHt%_m;fbSeA0l+QrPo@i& z%$NsvlX3UvR*7V;D&QVX0*5-R7W!712xeOnLeLu=!1_>fIMh^u#2{}m@AX*5S+p(T zO{swMc93gHtUE~JqJkR+l;Alg9Kw(iTOLl&lHCo~lN_WV%vH8R*%rJ9Nj)bEj8&Ti zX6U)>foS$5dHYeG0@#YRFpZ(L#T9VL2)rRRp@c>InFnLT!%`W^dKC6>97Vm(ovSeZ zqx~O!|I|ZCod3M%cGXoyP{WS&uY3xqaR<_>%N!k%-2O;;IOZlv4}1-MwXBu~gyk=L zWN=z;6{@DwQ1c%^awa=G$qX_?HFUM&(AGIy8v?mU(*g-u=BA|wYBCm9>67~>N3t9a zQSm8P!~J6vvHThIyTc%aJ$UrpY;s`x!8j+1IS`MUUD9hT+2%kRB+`t)1p&wqIS z`;@>(kt$0V4;z&lNC{SYZ+vUeLNq%$QZs5`o@nlZ`i~eZV0HY;8!#qz@Qr zVC_RP3#?Fvg>ZF>gR5%Aq3b>hU?0Zt zXW1&~D$wgjSOfHfZgT4Jow<`WpCqt#1W;Q$=jS=YZkgt9Uc$eQF6{pjWS+62X1G zj(W^_bkyb>F$)X_rfmY??h+lDy!hGre(k~rVXK2?47%My(02~3D=;#=%CarRuPfjl zDZ2pQhI&0Ej_Gn}xjG^@4Dv4!NV2dxXi zvIEg*YXOG(g|B6W^MD^-Yt4l!=n>WSIeNhD<_ zm2hw%p(MuV5dcqQUs!$Dh_6d^PdSL}qag&-3OJeNR~@R0NNE)O?JwbBOIb zj9T+QFR1nhn)AKR&!)6}BHM(*_bUK>b|U@*D2u7@ZwZQTV7aQhTev!NqqleV%b)(u z-+Vef-+M-suk6m9`S|0HFJ3$>0{G@-`EL38>la^t@AUb%PQRsJM2~tk_xZ!GUsOX% zGy|{Mxq%ZI5PuHS2-6(!P_qVx{j*O5wl*Yns~iddHY+U408;`p1040??IZ`h&%7v6 zO>8~D+J*$PDvcNKAb~fs4GBpmovY}>7*4waeuWD7*lPk{slGf3D~_ET9l?>oVH9B~ zf>ue}=FEL9BLM~Qc*;FGr+G7CD(SRez8AHMDssKoaxk`C_lG3{T21;PsRl)ot-#jtcLf1_E z>Rdslic2(vwhko1Rm0Or-@?{e#cdh7woqn{m(v$8rLfgxZ_zcgoG#OVqii0P%mZ_) zs8uwlA!!vf0Q|h_-|YNB*)~W6@R}`KUVa_?edkSg>U(84w3A4H?)-Uakeq_wpjr%$ z#j7#64UYAX1(!ifm$Hx-c1rANW%_n7o`A{=(0&xKO7segAMq@OChzh4(`BjAsH@6q zEPZ+~)vR1mBvKpz|yl ztc%FsPygblKWWVB%%n?L&-R|Vc=h7zuixZ+64bm{$di0JK{`n*eqr4%NWO2&Xn2R+2TUkEilQFK`t}R-l(& zSYkb8iQp?qDHfWM`mQ{4Ii3*M#|ggw`^4}1<3ir`7I}W!XW{3kkiW;*cftI*Ym^b_ z^-f;w)~ofTo+|F{o$p>#9z`RP5*L;no(TFuwnN3qw-?jGfoa z*qGikHjGs_KzBH4e;HfR!#Oh8@7+V%)r!r_mhU5AeE(Nes?8zbN70M%Q4qXycDhMz zOR9CUQP*PY2}Q7mp^d)m9QJx?)=Yb!v1@VfbDE5w)a$U98VU`7bw*J5jc|1as~qbg zF{`J)QReb2ph;TJ-&q(fBFCpLGiw2eO~a}dug5o)}coiuilwkTKdUPfAYKEef#ZS*L%;te)v6!MV}wG ze~gjAqZbsw$9Aa#)*1I3cA@|)wW16H__c({SJNu!eK6vvki9;Z@C2q2A_C1}4ABc30qBeZe&Y>D-x1yDSFH z9Ppp++!6I}qJS%&z|~S{jAGcA=756&4)c&8mZb&Q&}+R&?q|8r#!XcBRXqv3iUMv6 zR+JuxZmW6+l38(l{+s{&pa1r^c{o#+P5&keSa(uHWc*baE0c?r)EnA;urLg%I%?(6 zeJsUFT7glQWf;D8#p!$d_wqLsTwTeu8#OJ8**>^kJ!Q|4#Gd<9_bQcjg#hmA665D~ zSHMB}q83Z%FUWCj_x@P`*p03#7(y7_CyVXTff6L<;rh~=GXmglR6!$!n~g)anCnH! z{U2Klz`|dKpjii93Ta?#_zk7erP7+MK>_E{<(oG%4$!iccjJtxZ*wYm56pa*;|I~U ztAH-#9k=w}%JU^iU5eqnd0$Z!=hf~xM+azCz#?yn-(taSHsJUSjjK`^rj)9HtwqVF z01Jf+%oo#?Lk~pw1}{oez{Q||pNkb(YmfvM_|gSS8j#M8jlKEKJMw?!&!suz=d8t& z?ntdc8Jl|Zyhf(gnP6%d1SW>%3Jlg>PA*fi6PCQyowt{gzgX{sz4w2m%b@?Q5906- zElDQVx|bvuS)cjl^BGmaDDiO(JZSD6Bgrj?veI~*P9FLaT2{>~*Xi=2iokCGKF6&R z&W;VX*k=3|9%Bz|#MjGuh@{Ji{gp@DfGs+MP{0Xsze#uBtd6+|VH2zq32QUaT2r!q zT%Q|BGxGcw2wh=wU|iiFA77oy7VBeKb@>Oq=B5_~FEsDtiTY!E=mcR*0()S zN-(Rrbjzyi=Ftv;@Y18k^elv~f%ge@we0kU#_Mm9zfiw7e8nQuq$K?O)4#6GL;ebW ze<7=+fo0_2&AFwYeEaS1u>8NjTb$W{P5^xPy|;$#U!$J@)+{hW&@wrjZqV#PTKilU z!GH3-IED~&kFe`FX!w2AM6o1>J7NyF6IIc#`Va>0(FE*H9L+gkD&RKOU||wcUe*`Y z44PihbmG_v8!Wl3$>e~KnF1b;3iwB_{cFjJAA5Pbr+}$|JL$o(llUcqZ4yHz?4A&(3_*FElG9n@vyCIGx9R$F0ZsY?#0f>8FA*#5P=2emubG)R007gks${bdgZD-;PCmjDSZt**)~IvQ zI*|$}kJhzR&e8NaOd79-5W?->6k zzWFoRn=Z4N!&&299Y=x56Rw0+lhzxUIRVS;#tLuHMjg5}z5>|`Cf7;dIwAX*S1E$+ z2<&}~{S#rbKCM1G-5}-%>7>!Gzwm&5O$d{__I&`Qb&BBn>|=QW+3LCngGt{ygW|04 z270f~-k28+EDV;sdE>8t`qOXgIy3SYfUiJ!{?eu1-ZLVMfA_op_PcNYoACGBg~gff z{TDBve(&(%({HLbC+0pJoqGM;xpQX`gVr)=%p=?Z0lcHh2|6g??+tw~)?p1D>^SHI z?g(wb2Rly<-TjZtpAfMfu@iVGlwm>r4y~30HZx{eI(D?tSXmUXAQ&O&PAh@N=3ZDq z0dE!nznSEK$1a=|1uOtQdr}0j=-z3*tS%t;fhsXjW5UmWZ?9(DFLFR9~ z57?`)j+=!fbHzT9kdoajIpCzl3^074*#;cu0mmw6>p@}}7OvzXwP;q0r2-fL{>?uN ziU(RTr%>=)H7_hoX0RHpTm|81j%z4Zf&xZWG03pwKXX*b%YY@w{lkDT=HYJeYRjW zBry7qN>H(u2QHLoK?&Qd0XwsM{ai-!w`?86>|82)ACt`^ zjMEqtz}kgm0cccT#Tmfzf90;j=K)|m2 zFDZh7-zT=m_XAyLoxkW(LP_+E8`QuuUQ5EyKe;y{lelio|3W(FdVygD@I>QVfv>FJ ze!Dm^-`#!WR*TmU>VAQnG9;iu>le29W29o&f1WkOBL{t68Kd? z@b|n9%LWmWzeDT;4giJ~<*Oa((lid(0PG65tr~N{kigmm+|d#h@P`_51!ndkh z8iKA!m>@3~oET_rH3GL&^%iv?VFd&(Qvg@B5oweY87fQ#vm>~wrB@b&77Aa}3^2Qb z9l>_gmzF%c0=}|Z+?nh8coFg<-0Tahue#QA-v}znf5V%8$=^y5$zSaNu4EPP2Fn2>AY9DH8e>on@n?@kn*t_(3y47H%#MTDZ86hH z7r=!*FKp$x0^5Vo(yJG(p)L@~B{UKt{fj=L(wgp>7p#a&P75vFb;~-JeCDs>7he}V z>x;ZEY@l^R{}CrG6m9eoZLf;%{#=F&M}dY{mIF55tTM+E;+l@o<=DA|UalkpP5!n9 z1q=bqB=824zrx?1ja!afeiQlMciudI`r@&Zul?x9AHVlN>3iO%qq_c82R~@wss6Zu z!NCT~#^&@}Y%xj5Dt~3qULh*r@ewm421duP4}wpF_X%U(VQ3yy>)*j}4kv8Yzs+?| z#MYL4re2_J#4tk5gIODo`{5ej4Qwx@ToM8=#udC>3=!r@wu1k6Dy(q&F{l*%(6BC(heQL;WULU#ppy-#vWUq^|C9z zVtpcAoxP9i3@tC}rV?29ied2hv7RS_>wxcLdrCc0Q<&M9^ywKwSFHzw;iP^ALzKj# zC4>CJHa_04Zwb20I*9p7TB$ypB6uDFXpz5nmi|V<&pHV67Yg7TzmT4FO#w?E`-1RS z;k)?F%>Mn|@9sZ)QKGXK4qyEE@cI6urhs2@0B`0@BPIpBUJiA|55>OOcd~DDqzh4 zzoJ%H5`zYSPdb1%?j1@L@J{FN4)ekCKBTCC1&pgJU=zUPZnZ5;Lh1w%Ck$ZqWmR(yPmF>BSfHeWf63j zo&|`ZfMsv?li+W;iNSIybkEOh%8#-QSkQo8V3cElyJd6Uz^)zRaFrdSYc^dq05b;* z7iAPNxt(%8=Z!*Es2l-I1xyW`PwhA2kxo5O0CNN|Rz)O_il8_0$VmI*4g5y;%${{6 z`g*8l&6kKyPXK!>leH70){0*qEGuk1NO_y`3n5 zzZ&Lza+vhNF1H^-_|5*{RWQ|`Iu|mTX%$$UV5h#ugHg@SX?xGo(vo(gjOZjDKv#9} zI7AP2_rY6S-{&nwv|(7!IeWQ`Hv+M0jV1f5?!cqK8jRp`4%%|xRy!n+@84GdX9%qO zaQF?+CcE^pN~p$)WwL=Wh>f|%vzAHSsJy8q^|iiPoXD=p`|72hL2y`S4YY>bguQW_ zttWBo`Bk~@q&5)OoxWu0V*N2TSp~epUD@ev#qi@g&etD1cI$Ta*deO2TNm_Q)u$)N z@AFF*;XCN<-IlVr|Eo_0({(~OEKmGJW`SVz15^7F!>GQ3TZh1R{>(369@8tX9K92U zUnyqq?G=-mvu9*d{fBQ|zIg832j_BKNVi;l@QYeGYD&VS(0yHDaHgo~20PbW6Ix1k6Ld&vU z0KA0~xJdyI51+myT}TpwmNq0#0_H5jPKKYo2Ps-~nT<1}$CZ8iGCAOXYEr<5s%ATh zN-RqOs~2?DUP|Fx+2?J*)oczpD&Tz~2Tb~6l9Gm?;lrV>l(!Lr{@dTS(kF`n=vD=A zvjP^FbPhaxxU~#8TNJvS*g``9S4J(zii651`;mAdd^P)fElfwMM)G1-lFmyYf%`Ht zcop^Qk!R(rP?id~D_w#dUavJ+>k)(Y2y`nLtnJH1BQR8Nse8V6OKG%h6A9Zx1Ui^c zMg(gu8giVuG5|c+y{1&~z96%S_GH1RMqoA{*Qi#IKev zmkL`Az)-ja_thwsLGC;|kXh`U3|9~Zyk~7o4h={gAgBtMwN^PcF40C>9~}S-xYIoY zZAoP$eN_$@BUldgsHRlfPm%dGgZ7AG{~71hl5-*ch`18XmJbbW>^Y z8L{U{FLR28M^@031d>I@K>vvN86RP@()B@F?2i+x_YJWTvvwBe0bsw&s;NaDE!Bf! ztm_|i;w(Gl9@7m)b2TR8Doe^?hq3O2gp0DI=t~HgQrYgyLb9HKEJF;}Z?pA!J&ENe zODek;1BoYWm)wHP1n(0BXK3l{jW;Ihbx)(#pIyhCc}jAWE`rvYLH6;YLRfA%bsfCC zi5vCDk=xBbu!hIAJ&CT*sH7WYq20KrD{#5L@Ybzj5uMZfictcW zwJh4*DCMnGWvcSkaIbQ>%MeT@JgRYMbEUjy^9ga)mkMEUS1GKkLpKw7*4`okb{7k; z`nr(vT~EP#yo)MWPg)qdt6ddvyD8vSjX}dbMAyP7BhDg#OEL~=b6$(E+z>i(eXf}F z<(@4_Im`oIvqk#y?(AQ)Mv9aeP0q2I7P``kpY^(>1y#~5P{2$AGX!l2mM6F59lI?- z3d@~q_NW&$y)=9du*q2EC=@ghT*xsDjk^TEyVtgG7=!W`1yrcoQej)NJhCN)F?4}G zliG8H>AzMK&A>8DG@x?J1;Vz#NrY%ID)Iy#rd7~@GOM5wb!N*j`-0hsR5mj#GonN* zl2TYh&_(tEgTSRVVGNSRpf_$mdjSGi_3yh!guo|d9@04>@LM0gnGA;+b_O&3yJF_6 zw;hG5Zn zRvH|OitF|4YpmeEzxHCC1YWGyd8Hq=o~YXu(445R#=aW_}g<67O<1iFtM z#*a-F1HFdn@YI0e=@?Cx8%y_N+*zX1_9beZ;@24bsr=XTc=~Y?h^GFXH3VB3))O@# z5eRGZ@0ZhpU91qnH}qETnVXZJe){c#6lQ_OXU@I;-dpEI{+_-lD24*=&lX`RfVU99 zsDaiP^oz~>O%?F>K;T!igMpa`rUE`S^uPbr^8Y*pFX*<8j&u@Xn1vL^0ar5$cwZbx z7!V8vEGb|q#M-%o6M?M=%O?`L7qkJGeZWUm0gDrs0~l3UnhNerTaY>vCuj>mSJ=YK zHl(+ks-Uk(0NRZ>m;+V{hazYzgyw4)f2o11(FyA>|2!z*eX$S=kE>|-9o5gqU@^mF z5tahDit?*e0;?@n#cVjBfjfJ826}qb?3h-S$o-lvhr3{D4{%pIkNYTu4+ZpU6#5#j zujqlv-_am{9mBkeso%a#)|c{E6>zYb6oo6d6uNzoyGr5pOaWW|S3}UFUF&@oQY%)4 zZ!1I4BE0lwQ31S1i?GVYAbHDh-zbOC3j~FfVg(qN1i%-j=Ju~C!OFq|yH&z8b!^;76-6G9om)Z z8IF@y;>;0+Nc!g4@0$yjlCYO9pdVQGMfzYd&a77qKC1v$4NNl2P0sLBKo#3EcXywf#Gq1lIiTni7hyHf-2ngOFf^b@qaUq~wp!#goAAyE={V z>>2%wWcanvbBsDSHT@3u+c>+>>oejh*Z^NY8Xs)GamN;4{+Xd+v<*VM)H_!_%){ zJbPBOuoPhpbEvT@;KQnb&qe_6Shpkf!Llsyb18te3OWoQWam+oz(dvtJaiWT&NyKW zb#RASz#Sb4f&Z>a0izPxfn7ln^bUAYssi3YGidZ7?X)5+jX}Sp64(LEHl*RP%a_Co z`j`}79(&mW&>J_p6P6>mqtk~D)5OZ!cpD^)%e5%a2lX2!7@g0}jr zeUQPdf~ErgcYiCd^|p^AL;zaLu#ov}OD>uV-gg@c_&2}Btiq}cL?VAxDC-QQ3WqV| zIsoC;o}R4(D2gsq1S@&b6s&P+)xau!)e9PPf%~df6wOMkQMZPc9Pl-CB3;7-6;oBSxLqBlC-Z^Li`2db%FBtqKcg5q?%3+-^n>qpf6cs|es$)IlQz zE#r`O0l-jX<#w)`4aT@ys$lWv0C@9s;wTjaz;_hDQjl!(|8k)h2^~QifwsAV7G2h` zaAEfvBQSih6rGv$uXhKlKHjv|6>y1}-~x-FQ5q$0!MJciEwNbARZwZ{A!wRVLIH!Q z$n=&lIFO}RWH&6Yz+c62f$o!f+nCH6EfiUiz(pX}@r#n^j1dR2yJhBot^6wBH*XbL zc}isyz#4>R5ths=UF)UKQh~L>eJEK4eNHl8@4Silv#uvko|JspIsKa?o_s<5YU8i8 z9W_DNYpzz*@?MAZWm?xNe-S~J&~65P5)c;rg<_@V*#?{xtW%D3(9!tP$WoX$xTKpc zUp4@rIx$D~Ok|>^;ea90J9hg{eVV~)drgjS5c-P#B^Qm7Ch=SgJA3OXi6@eK?J@Nx zSWno4{E72B7#y<_;isTQ3=Ho%m+Xefq0kEs6YPLDqcOguvote)DRts^3dzh~GCqJTKYbv&TgLUcC76 zdjK#TQUGA#@{wI0f(}lUFRtY*;7DKr@Qz>w{VLMHO!^Kn2<MtTfv%dRCCj+vf`aI3)(HzC=&C1y z+p6Sm6@!DL{3U~{7J~k@XkQdz34Y~1jYNl$z(QQ{jgaesfu1+MyH#D3kObDiAvtWe z(B(K3seKf430Vi-K5CU%HX;e$983daR4|LN5&|DbF8H|ZJPO^FcwDV#x9?NW^hWX~ z^zHgLU5C03>0=Jji?UrZz_5K*`pTu;$`r6?fDOR!=Ba>-oOa|P=t{8|ba2Ju(|HD> z%cV^K@FoqvwwUFafNH6?(EoI5M~plE9J!HU(TLSs}BA zhYNI!q{~J=Pnil0%L1yx42!TBf;K0PoLN7^E}Ff+h%s~GZwX3PGr_d>(AaVbxn3`D zMg|y_&3R5hDzm^DET$+93b>dQKdWh$`%qH->UGXluvK6oE};roYOtQy0_f735=5|@ za6kcX766L?J}hM>k^q*qTL3K5;IL1JF$VV!ud0Ci2QwZ|L75KPE~v`;R&l)4@IUDO z_fzNPG^QJWaKVG7ldI{*bUhZvI)E1ID*)AeL!!}l>K2HeYlQqT1Jgu>>tBa&B38r2 z50ghmxW+pobL$4@`eJ~4JrTjHy;^-S1i~G&&DRNh7weg5@1p*i7n7i|s!?u_@l!sw z+mAy)Sx?sL^%!Ax)Y`!^sk=<{+RewH?;_|62LFucP=;O`57r431}pf8==wH*N7xN&bZqeKq4^PnliwTqDkeozJ$P@{l(_ZMq2dT6-FE>caO)eBMUp@@-(&}b!`KF*Q@r`S%jVg4h&`; z78`;A;4=hp*?cx)H?U`bahTH^IZFf=OPl8BrsjG#Y5diO|Dgqm6A&!zD>Hpd>jH?` zTV(pTz&fiW=-lGmEm?wijTB+64JBHIlDG6?Os~!Rr3O|ZTLNe$1Wf>25f+NI(A~>S zF&_-;jU0(C<=ZM^eF}rM0~a`9!DHVAQ@gBqcKB*7mgRuy1x>(00hcZEY^Uf>$pkRt z&gIY!T-JY%*4Bq8!fHt*umU(L;9VCL!0(;iwR!UqbBJ6cf4eqidR=Lje3B=F892M}~xYSHT{$h?p2ES)?>s3?AGZ#O& zb>XeI-nwvF@O$PQ_^S$70DQC`iIo6g=|kc?1|)&kdKa(>;HZFyq7_G42t73LiJasu z3?mc;{MBDQ`Qgwh`fv=L&@!x{$l#%DBd`J3a=>j?g4JdUxS|M_X$%5NltCj0ObNWv zjj+@US^&HY0HzTo6!7q6al$&uVGIgj=79G`1-xVEAYCXseH0RXK-df%im-koIpAa( z((&6cqCC{bdB9cwwqOVjMbK^s9h#8bh$G~HV;MB+ue57e5sV#%2{Q_yfC<|Oq61uO`@ zX05@a*XYSH>Vtym6y3)N^l?^1tMX+OdcC__vG2&6eC24pBNz(!P?s@R`1)(htT`V&P{DLcB#zZTlq;NTyZdBmiWc63%uP}EF0KBEZ8f0za$uWLw z1yktCU9)@By3kvMj^4r=1#H2B&t^JVNJ_7wM~C(SueEb1!$K|>xXhuzDpx45Oc}Xd z4M5AMZ~!9(teIpckJ&3lP0QY2Yg`Tp&cz;JdQcYfu7S(+z=9v8bs+J=T4+R>p@7S% zd}fT6hS7>WX){9ss+X(ssGF97&9u3$=yEvJ%9L2IR-i z-3a34y1%agUO=jEB>QQcz>)o%EX#j1GT&;6xD^Q38q2b8*ljRa510V1p#}>gILP2R zsh(b%<5Z|8=m&m60?(orOJ*~uALXSRx5V~MMi5Hw_sqFV=eUURq$uE5IE^p?Fp|I< zT>=Y%UyMaqF$cT@@#R>D72?sIpAEzctsxX)Ie>?l`qg0!9iG_Dj00Xx0h3`+a&Nl!dH{KQvWPf z&uEU{x#uj1;ge19=7T-PXK`VtP@Qlfew`$$^5b^1vG}d^fMP zO=Sx~SLsTrs+SZN-TROOlG26Z*AcAK6|i-6rCwJWQ5M)X3t(mMp|16ZaCk^}IDq_> z5@_LXA9KK325l5B1Hp=4*#y8>4ZvjwulPY1WAU?Qe!UUc5G;IJxBt%M+?g%Rzaq9< z%$Z#V<*s0E&F&ZWY|wzQ=-_oNIwa6g>{cDI@i&(xurhiL0BrM&=pJ1R8QC1VkgP4J zV76F_&o$NuYzo*#EE|vtEU3;|W3QP_as&gKktnbc)JD@$!Sla~1SWWsJTOb7Q3XxM zNhEnGgv*h=Xgms)SD_lKOa=T$SjOxsm-sfP8PgMhr&a*1~R~>8$JGgv(i}oIJS}h+A7+Oz0lpV3n87d;#z! zy*tWt`x)!uCta*H|BmC9;IP)j)=10ch}WjY;CpYmc29Qqbh{_`UA|Wn{4VIaEHJJu zEb7A+^s>CTTuWbWnc{i61_V#XBS5&OsBP$F39%A6zpRf^$~JH)LFbxE-&$Dfrht8w zZBF<*!Wo7-DOf|&=n9^nwE*-DHKROpjcmz(WDR9cc~pP)Gu|`9Q*b%?h}! z&AO0G0ILvgIe4NI(~u7C&_1M2oyV|-ZqOS|0dGnHtTk91N2m%|#sLd|U)pO5Sh(#9 zcnAdM9EQ#gSHK;i2?!mF59Eo zs0$rNUD^Ze<;_nScwUcOuNGddKh$L_uc(i;2&AYWQV8urN3(;H&S(A}pEo zWg8HG0YD>N0; zFoRx`ImEB=3M#BM16+jRhKlZ@1Gs<*3!rd;BN}XB1iCfvPLlIJ36tt zY6W>4O?oUM!F5=MuSwc5ED2d6dhHw;6hIvKl-v=|rSPrQC?g$^zAo4a(04&^E@T*3 zt7WjfI=UA#H)<(?7j#=lNzIcf)U$MNVs!i%M&J61UtB%=o6Ev2Mh(w7_=C1Ts|iH_qCT_ekoc(?{xvZ zbBE&mV24d(=xDPKXg$i+BnSMPI1LFoV8!q8s#k1RD#3@+i_9zrjr1>cFg>9az`ryr z%D<%oj`3%BKTGiW6Oq8F29_JQZ^sZcvcKp?8XYKisN^+6O7-G60V~Sy9(bYPQ<8+b zdDXo+gsb%si#}O^JMf0M5Vx`sxMGQ58_U4LXl)1Xdw9))(FS7+LWk~Ot$yy)b|VZm z>Pm9I9)D)@FZAyr2;lXC-EB|hYW=_2b_h5Q5?CvtQG}%mxYd?26T!t+6~N_^3i7gKEj1JpgbCb{JB9C2zj9 zbvHjn!4?&;n#U~$^5UVIDc*Bn_gW-?QSY4RBtjHbnKg$hR%U@UD6Nf6xzZ-Bf-YGV zv_90>O`EQQxluCTTVNELrO=>o5$FYlOICg5CD0az4n@ob_;3_LZ<73U1$W%A955fq zF$^IE?B!T)#KClY>qF##pVtudx@ZEuApm&GvCBVy^YD>fM~)nm{@?v4S7EP6U*YZy zh3||G!ZQZ@R2_KpucR*+*+>Lzf`oYe`|z7mH$#x*c+oBqzqQEW^lUs=tC75TWIe$r zu?yBN$hG2R5!cw`AiMH35e+^rklkUe5ww1;7O)$D&Uf=i*B0!x{IV+ZYvBt;{E?rz z)SCcHnu?GWG%jbO#0&TASn;Y4%21bQm+iJ*zP`7hB6xWL7={EUjB9=j{Nja{?@>VC zTgLBy85>}%)SX`Dr2)z|zWh{Ly9L0aj&*A+XE0owjvANVy5WwDs6a;VxK6?@LDF^Vs_co+98;Ou~0;^l3 zM4(@U5l83FohE^yf46VkE(`f<09O3=OA`3|3gFk2zkBz(569jZfR;s_yoA0()WGIM zS)r42u%i5>=79e#6|hu6S7}9g+numb@NCib>IoJ>TQ6}Cz$nAABya@q+cpk~Zj@@l zAuUG(`#w3exjk&l79Og_y{dQ5ttRpZtm{HPwBX46F%H^~G%k(b+3Og_=@R5sKq`JO$`37+!s|(+D>kc(MN)o9t>|*GOQl@(ZIJUam#i zTnkqUT-ospfFbyv31qo3%?Px_przxe)?h@MOa`%+1A;vOT?-0W2T9CfqM}8gQGKOX zO5Z1z0G2_7XKvgO{9b+_>i6o$g5NXP&XT|%U;Oxkiywp>aK8ii7y)b{XbC^BwG!x* zz<*)_*wt_3ubOdu_SGjtLwJCQ-l0ED6|l7dXUr&vjKQZ81#E3dS^U~!3Yb+`8`ubJ zHPG9Iz-$A4S*q!01=1q+M1OBQPfs(urd=1+1^MkGW*M)?o~70^iR*(-<_P&{+0e`K1FmDBuA) zVRh#7rhX*_+}Q%mN*~hhJnv9MQ}$MavP<9!6)+|Y*g+Aa`*MXf?{>c{0M65KqhKAt$i9}~ZB^*m zB>-M49Y`%&4_!hvafZ1?kc}Z;Do-P@=;YnIzk6VJ$rNm%hf@XfCTeLBiOObOQ3?&f zCIBwchQmZG)4t$#iRs=VhW(=R2oYt7;!@58Fm-RiohU)uH2F*N1{@d4J*t57HVVlk zav|cD(QcoQRQ4LI2w+#hYfS)`;wU6^AxT@{>9c3W{zR6`(i}>s}@_?AI1pjKP3z2CsYoWzuok%9qw zwamr$IEd#A(6!`&xI4bYLVTI@fll;Ala2PWT=@ph-&*)swl0Pr=#SxUHn8k`Rne+E zT+kChvEI;o7Vbg43O=c9?>%Grny&&0yzsTY+``ujdW@H&9JUv}N6D=FhGvgtFDkg9 zTjL(&xN&wFn^IPT(9(lcGx#n8$x3T#<-p%sq%f8_9b!01`D=z8Poe<6LH^1W@O$~a zTOZuGlq_e@Q2~E&@nTfK%>Wh!EdLv63RnnC{$@kaJBC<)wNe5fJoVMxQ$y~<@u#OU z3V0|*=@4ff0KszqseKOM|1-$}cbF9=&U?oy3!++&3X+VEO=ot7_IOXh^d}attOozy;tYoZS{$})WFoiqdp-CAb$AU zYojY9@RcMDeBA6eG95=xyHblQ+bfPi+ilwpZ6kjVt>3nO+qP}(huT>N-QLbBXn}1A zL64daE>HrO_Vixe?*J}ab1=s)%q#oRE!2J?$Sp@#$xvF1S5p+(z}!p#Z$5kBt;6STPQ9WQ&r>(gSmo7> zT;_cZ%fwCDl)b^?81@(S#hZ%}zA@fhYb>q~-qkd4hM<2)3@^mXLU`)JiZ%Oypl@r; zw>m&01Q!=-&s-N*+zJutnun)rl(DsVBOrSry@R*xYBBTji;?+J0@p&IJAT<{+G0f& zNehL^<5~h2p-3s5?3yaDc9@{kX9*sZ^6!d&CA!OpiKr7z7WI9!0`kXr5lS@N*17Ygs$+J4sW@ z>BJRFs;{0ToxsZ93xZ!cx6|?~Ol_mh25yx|kK&M^6&qW1n0{GQz z8P?EeU)}vv#-ODJD*~AKeU&-jA$Pk#3D(f>$pObYXe=+P0+tJ7&^vczbHI`U-fjdw z7QCSQhhaqt0ILMvj!l*D#=SkVssiqD8SEukzN=UccvS^_JEVX)iO>{q#jCJ916;{W z0;V69Dd0bAQo#G%3~QiGim>k9{S3O-Q@}9?tRZN(qHNVPE{ID73=Q0(H#&Q^_CNuP z85YD%MTg0?3y1TqVru-xo;Rc?wQ9h6x5a-If_ctgD4D7)K>=F~x`?neGRZOzc+WaY z@Iu}oTym#L@Y`yhRq`_9n_~u;A{p>4QdJi);;2wa61{mQeq8~VgDyr6xD0=2)4l9f zDwl%=2dl9ngFO(f9bu|~|EK3^1|3_0(`mqGPrr5e^{K(5M`fG3d@$jI z#LaJRLipa)VX+e~eivhF(8|OzAoX=vy(>}6G8EcpF$69+3VjfF({CS>(% zyM+4|R=KgbP)mRp1SpR)8&5W0(!1lc!0qGB&*`Q_@ zl8|wT`DSjqfF)i9n5J@pUq4+Vg}5g&a-22%{{becTm7uA6;j|o8VW9ucpxtTxS5Kxf!o{5Wf>|Uy7$Pjvu7kL zclh$*%UAcbQusdP)IU?eRfM9slkMRb4@bQ$no4+msDlQA`%wDa*VPx(y?tF#`#z=c zeagn)L)*k}WBvN|?c3J3D|_Yg{5Gw`k`vp2U?hRtRRKS2^+ISkQYwVy$^jg5z)S$c zb`o^3=>fRQu8Za81zP2D>(+-`3myn&W3BZe6;OoSqX1s3-A6?4+9-HY|Gc&(p2MsL zwk1K}YOR)gc1Zj=Oyn&Aj7S0lzbJwRYKypO00yX?-mt^UG03c(wHGTHY30Ro#&Iix z&SNx##h?p4h%{%kueo3q{8YhmzynnCBhE8Y~osR9-N(}`p4IuN+TMkM2} z0QmCh!}F#j zlP4q{HB?EefWZzap{?Rw#Bdj_7ofX6NH z?7~NS{L$C0l%rU-SGQ&*#qW{;m^T{BM6g^p0AYQUis43rV3oo8D7}UI7@npS*3RIX zz_!*Pfq~x_ zx-HWR+@W63FDijM)C_vZ4zGc>E~M>Jhb2ki6u?piJq#y~;rEW20)7b_g3#NQ#V_sM zv9V|48$Z~xlg+^TF*}J~P3vkER>gC`zk~v|5VVl@c<2MRp$izI;1d~a9#ThJwBP`K zacyl#i30X%NJ`-9K3Bl3!`f$kz%c|330$QKhgD&9lC^mRp!F|xXQk&2C}3?8Rs~$q zzpL${e|PWM{O;M_i?1J6_Fg{y-sL;Jn{uPA>dJwU2NgrGsb4S{bQS<#v-&L03j@J@ zHteX+_zT>|;%CkN^7s(zt8inzQg<7Hri=Wsmgr?myW&`93rY`CFygqDR6ijMT|_-} zY0u1jcQlkJx7)0|sL=cX4vJ8~<(7i*H@|!98ycQ1Vdh|-HN>2$YZC=~cI|(6Ek2sa z-nGwX5R4_g4Y$e`?GaX6=cF3C#~@mOLx$B=v9uX|LSD06fk%@GVm@E4V0`NVTW4_l+|4dS>bC}46o@33YGR?)?;Rqs_o4p;?n z%mJ@80K*E)C9p)GkDR^y-r*^=X_iT>{g+<{fCGMwz*oPyiPm4%KF@N`2%_7bwN;gwRq7Jc%^&HW8z^3Y`yJJPS`~>ti*4i z;l0}G`qp)72BKL#ywEIYGta0sV>k1*>D%2)?yhB?8h#G^I`G5P_$#ziwWLVuo>xr8 zn~iuMDfu;Dh0$Z<>qkNF-uwC^ocm}&kL2nj1GL^JC-F!n??)dw%{{RF^*wu7?d$OF z%fjC!C9tf^*vs8=!NI*Rjl=wyir{6tzSpRIN&Fgu5tL@K7&}EU@GBPKkiSiq9Owjo zqP<9(`@MDN!JP|^-;aMG2)-mW(W-%OocZ`nRsq|9k)urtcqgYZ#5oMfIAB%4Ls0|2 z`YNX}Bn1BK6ds^998kcBK(`GAFKEV~hhPNlC0Hte9l(|YuDY0X3EbJi_;aV#LGNrr z@OF4XZ+{5@eh&(mRnUTA1@OOm`G=~2!QY+AVCC<2JbB9xwn$g7S79OftLbM-U>%?k zs-UTWZ&LwRnFj9CPgiBfa2OzAeZj$t5^-k_MI#2?lmp%e{tkfRl)ayQ=0(s%@FxP` ze~v|1tyYMo8D00GgbL2LpbD!)I)PhztUXx$p!0ZGxwWum|J9pPB5+s${N8Ed@8zqz zHvRvsy>Dz(cbe}xv5^ZKEU?Ptq`}y@>ek>gjyJ$%DhY%uhCgv70Sc0EqqqZcn&56T zO0wXjb)&faf?D5Ez)nYTJSEh;2xx%NlVxONwTl-kubjC;?vCB`iejF(Y9bQAiA;L)=B8uWT#mAG#2H(@<=`{lugVG6uydrV)=K1rzanW9#(!O+8IIUT;%Gky!Ta7yMG;n)rceM z6tIm;Ql*=S`EOJufwLT-_>ELSuXZDWF%j5mpb5Y`-#hgZ^OvsIs4e3#=A~9=e<}An zL0aO=v0EH`7UQUYh?QOaLsh`pr0&cwRHB-2#o=M=s6GETOj^l$Wq`>{$2; zv7hGtBKma$_kv_?h~27XE_Ud?k3wS?c$GUvz5^5&b-m9=z*$*|!*quZFq-cd?-%(Y zQNZ_|g!bv;F#fzS3kj^q{e_TNgfJgf3e!=#WpRvC_yo{bSIY)_{PIGxJT5DKr6yYR zuge18h7-pv4nE(Q0DhUizxw!B%wD?e`xKpJT$5iPhqti-8=ZoHNH>fQ5d@?|L1G{= z;7@lqjFwh9r9q@i8b)`Bgdj0#Bt%KcXV1I6-hF@0ea`QEzt?r8M~!y*46p{s;sNrv z+#*6jro*sSId^o@AQcu1D1P>q;Y_$JG*6V;y{8EEY#oGJPo{?xsS80?oaIafP?oT( z9#vuq7f}SXJk;;8&dma&m-C4vvK+;g>!^ot0HZaKwWt;*ym-#0r@kiRk3p{dE#_3u z$ZP8vE{KKB$V7JPz8&n!Yz5BBiJX($J+)QX7%K4Ih2^=33XoI>Ij6~2A&9Sj z8#B5-4OP9j|I>KbP;9dE4+EViDOVue0nYm=Cnfy0T^e zpmU?Tm|nuRq|Lw2o!@9qr7y%E{`E(EvU7+L^K10q#X&|v#szb8eh*d{jgtzt14*Hr zXjG2!IhH;nr#yLN20*t=H@xkYX>XfcY;18!st4S=n;%mzo%ivET)^H+C5^sj;^}}@ z=1FE9%r)L$Oml@H^i{E#tRk6FJaL;9CjduzKVTiu-2Y zqKAx1GKOizru_IOdhx4X*ph?qCcJWeUE}bQxVuZb!Pu`&af|n(F4&tbZQPM>EL|XZ zg$gq9D)U1*Rj;LWBYDM$p~yHi4{;yerr>v(RHPTV+#D>ChcSODGbR+NJ`Q7*iP?+0<`rN;qIaC9{% z8<-E)8qVfQKnZ8cvZ2)>Ti^zK|5dQcTB6^)!8zc8UctRJFb;(XW82R-6;#5sM)SmR zNmO&A#g+4matg0m@NNan8SB_WJK#xmR9ju2W)~}v*_aan89c(i9y> zeE%_9;`XJ^<5+NyG5C3Ee336$YP4Df9HVhv`2$BXCn;mQ!NyMcv*5Knp*p0AFejRh z8t>h=x#Gck>n-QOUm)FYS{P$KAl95m`(L1Nyg8`Kpuf_<=Cv;?tM$2Zk`y0rN^0_K zccnMifLeT2gI_eK=W^k8+_JcxUpmd>*$z*Uh2i3z>*zXDSDGIx{+-aEa$QZaILR~A z${FsqyM_DhW~^}OSNG}Ow!rt~H>rP*n`(n**nh!Q?hf~} z3Q>C?A}7<#p0ju7H&@S9%$D%ND!BG<{8D<2JK6BdBGFnNGJ9_e(-kcJ`+dxcNUzoT z28!GwBGxNNdq8FeVdsKSRpUK4uRv#w2=x~eR>`j&=T#lSurqY));6av$v_Ui5BQtxb!%R!@m58W^ z$QnR0C^6QHu$`x8rzc@b_(uQYO0B+?Iq{X`a6t)`i3!;bGcFgF?AazDm3{88tA|Yfi3+JFzAncGLAQZ_(WX{QEiKJEDBsHJwkWE-xGVFl`w&( zu?6$^?F>9zm^J#7IK1y_`JkD|KnuOg?>W-c4&rZ<#KdM>stIMv+X~_&&#yi#veRoA&ljj+_&CQ~CQZx$GzU7A2{7t1T>+GZBd#iqXq$JS7?HPyY zI?_m0Acs|zJRP;0(FS*wM1)}R`-sDfx2tV0O8^VkHq|XUkwec!moo>F<_b)?@>l@< zMQymJcoZs?QY^&s$YKDEBy#kMJj$T9CL@uRKWYloV02KVdQ*d#d_S~F8(EiT$YzeR zy6Y#@JYt+0dmaqm225O*M5$2>ab>Y`=Cz-_PPl%n=9Y0XAJ}$iMWK(+P`9}GXp>gs z!HD>~T8SRe11H%4y;M~j>m6-OB|_=i6UxSMU^llfFM}^gcbdwMr;?Pm zL;kzpQ$2A-O-cT-Q+{(Bw~v`{VGv`hpMK*J$L(#)~|$fnlo%%aiC=Nfa}vL5ztpZ1)UUj(m-+xa-_aq#y$Uv z{3AVw-Ct)iS3K|ijOzI&amM#YSg=jGCJ>US8mNOJ)rrs&spn@JHi*JxZ zsw9!$Ef7>9BrLZdND<&V1GH;j(PQ3^c%4UpN9GKH%2-H(HVk=jQJXrwE%4J`8qR{uH|e^z{E-N!F5YP@MQ zkr{0%(sF@4h#-M`NWXdv@YdpmId!*!lc-lhjyEHsabvw%)I;+$#dae|$Fr_Y<~K?u zc;kX*u=^F%&Fq`vv9@&Jba!Zz1}W=Xo66q5l9-vqa4**e@0)?|7g6ja7_fZ-pJJ9! z7du5@@{AYxM@<6Mpsit5YFkb;L!RI!mU*V}Qm+OUMV3sB?=fQdt8gj8LKMInvGZCa zZwWPIL5qVwZ;$_lvZjlH%gGub{vT;6)>QjsRBgX;N?He+3kUM@DZ7@*>#K#|&oAHZu}g1;M@#7Uq#fV_{PdI^g!+7GN!u@l!4uX zY)k8Jl`u~Mnp{5{cg>zx^}h$BIbQLvN6nFPVul%$lnWGr(N>6ea+f!j%h--9LZI{f z6(JHTclj2Y{vT=(Z=6c5<%>QSvVy~zq`D3P;=%IkOJQ-80nr$Pl>$BBxHM9}F0ueC zqXol!iI(4s?f9}PD3{wJ>x)F7hd1v(yQBAT?pMl~+-9;)Ta4~jxbIZRvz_OuSreZZ zQ?ynZ0rOqYO4FhMjQ|-YJ4}sf7B;oRi``Mpkv2`d?3;dc^5qncJF@;*6;uh@QP+IJ zj?get(-u+@f^Nx^ZEYAP|A<_ZS%eMV_?dJ)2#BW~uIj&QT z?u01YnTDMtrXk_lt+eF6csH+RNB4uuQ@fe{N!;Lp(;uI&*i+GYW=HOrfyf~?}~*72DPu(6X$JB$i??w_6>$lS_qZi$@8M#By-e!AZ^kBiQWH_ zFuNDv2%a*etw?pFGE#Mm()t9(jcO$B*GBqmqsLS|a2qT8<%z$G;yn{PfcNQQ6td16 zY3XAm!ADee`sSh{bCQg_I%pRmlub3h1IgD6@&}dsk3^Iw-zkBa5`hkf<1vC`kZHR= z^Yqh(SMFMW4#x8MN_iR#&iPzlz!qf={#3};hUpVe5^y}idCG}%J6T$)yiZF%jLW$- z73?#VCq%KLN6zDUE7$Xk!dXFSn5p{J6-B+%EBuhP_^vMXC%yMM(SO8NO#ZG$ z-M4%G@j0x;q#g&6Y|tvNe|I64<#oC9qVqXN7Yo>=Z`ILsH}W8Bs_8^4Bs<@%E|r*=t07KE49oqarP7T z5JyQ2cKsDQFG4K|g;Uk_3mmK`dWC}5h9oiJcx?ok%ve2R2(Kivr^=pbxQaT{+zWut z>naJkA&YcQlv1l~$cTfAU|J75^zNeBm*x_o{*ox#no3F7hYXo9Nd)pdf7 zXSr=-<@%$;T?6V}zi!O5esontJTS9AtqM8yOc1w1$K9wrIjmXh zlijjWL|{~R&l5k0R?f8_V{*yFiN005;>bU7RnVSJvEdBLWrSp3iDY)q|nC5!hcPK z?Mg)HSfd(X%`^_-u5^PcZTP(A=RU%B?0V?eEFH0I>%j1(w>u`4$4Cp*yvHsC;pLyA z#c?J7hTj&7%{3V|(O9_^%jiKJZoQn84Xb-W3U9K1>a`EeqDOscsm0D}pN@StXnH1R z%1M`v9IPxT9ib0G%MnYwDfgP`Cd#Bd$@zcH#WKJbd{R;0*kV;maQ1sW(ZnS4afhf| zoD$9hpUE!!C#di7I5huXnv&Vq?u;E$g)g@Cf!C*No#x)AzNm1C2F~F#u2XkHpR?3| zfN%GH8PcYfs4i9xWA1x+WF?t#Rn)N#H~=v0apH2TwH`*CdZ*(@;j8YxTRMjvnB6sl z$!7upu`Q|N_O7Dc`r+Sfk#%c@Wk_%lq$h(ZWPeIv2bt~_q}5eV8n&=RND;9fa0%|| z3@#_nbf_ba!?lLq?hG&EX@G-6rf&QItNt*mmd1r{duH0&r-4 zJzCggSrI@H!Uev21%P9TFHlv@qU(h2yYU^AXx?Lw@Z*;}CrQX5b9{DyPv)a0N%Lk- zl-J|g(?>LD%FyGzJ^VN5js!bG8|BLc>)^&%i!!&AsDiHNXQU(=G-Ed2 zM{=zHc54|z8tW4@bFl>HUw<@&GNBs$bkDL4T!vWNaaLFSn&K=-rGbrtlW&fHjJyl~ z`0{G<86+3WKj%SOoXjvh3Lmh55k2>i z#)}igcu|HP+OR%MX{m`?vru16{Zpgr(KVG`p2%NozAO66u%QrB>0bl<`NM+2l%`fr z-mcigC{p&Rg!k*!LP>C*my-%}XvK%VXL1B6Sx>;)DBz=nOm7TQ{`XS-4@-HBfFxLx zgv?iNT@pV|5+A8sg6n3pXL5g%`!necarEEvp0BAbXIv(3u?DtkEpb$BX1r2|{3i5? zZc3$B{o@vY%vvUHXK>=?Yb7?BXhFh>+nFEp3U{&ll-w9$v5T!NtKgAR?%1eKf~RkD zeeib9UHnJyjy6;h__i@$8%PudOmqPq94lr!B1%#ZIG)x}4P#7Bw^NWH=a zXy^^l0(x8+uUwomnzJaXU`ia)+stiLOExYs{^5A-btx>Hpz0HFKmW=7SaiYHXmDu7 zw`-%fK~(xPc!%z2RavVPM|7=Xf}#fa8HjJ}&1US(&Fz%UXTbau?5R-{Vtisf^X#fxjn$~McBhr# zNDH7&X=Go}@wuEym5llt#r2H<5?FLOkjLK~rVq}?r*!@4p$ha+aE+kxG^NRX^j3`% zGjjc&TzZiB>5vM8Ckq?hyB_I8Amdn9tJUW}B|WB;7FNw{s+0oJ1tGK$L-Sbdqju;Z zxud@gI=cF46fPi~Q8Y72!_R4nrwS9Vohtd3u;1K7?(N5f{NiRBV00Hu|oZD~2l!D%AB!HCs=Z)m8o) z1;qI8)P6EMz@NIZsPxyB3Gdv4?qE*smtOm~J_#CvR;0QY{!G<^HgOSWKc2!z(al9B zLvzCusN%deUg%_S%i6*Vr>EGT`v24V{-g54o{Fu*AmXvK3VRnzsjl3QPFBVmt$jq_ zf81jDTn7pDAPDx;Ic*kEVBkhX4Y(bO=IjNbhKDZVO*)1E-IMiuKP<_8x-}&+l@tSJ zpKY=-izZiS{XuCxt)VbsxnCK-bRV%b5pIujnasDIDi%3He0<*;rgkZEEV*i^Xp?n^ zH39ghhW@qh0pG?wgr_UE3Gtp=(D`hV;JyB62)dPMxLeZrId~KIa4mo@+ax_lr*Is2 zl{Z)f8-Fgy+u2@=%Y-B3$|jgfpGT5@(p+Rc1h@ zIHo~8B_X>AYO{7?nAtGCd8#}?@F-pO_HRC;o|~&@L@(C$@g1l$Q%`=LIu6JI%B(S$ zmQE__RIdF)QV!tYPJ8+j%aAfI(=_zqV6Qj*bgLhsekg>67#AL2hc-LMpa|ILw7=Yl zVHQe*&=}`RgC0VpKQtXp2Vx-}DBvT|0GlcR))L$zUdi%(d;d|^Rtv%Rq=z-sN}7Dt zaPeEiJ{m3M|4j#<@67S?`B)?Z|N1mUjAIXL)SR?8iLaaMA|R%R7>uyVAdCq4641QX zc~sjm6zKqd+d^t_ro2|)`pY6B1*!Tf&=f4f5V-s7v=tKj!%EuxRV@q|tBFl3xSZw^ zmVNh3oUe7wz>|9IFD?K$PE+H(*y1E)V#tkV9LlQ=w&Oy=UQmQoKr>0R)EH&%PxBQ3 zNk?j8jj=(=afM5-liA?Ig+B^Vqt{u9e?W)%#9QWP=y9bv23I?~!Ew;e zQdQd9e5`|zhA>wFe;ME?u;);l`%q$Qes<0Mn-Fc--1-R^KvARJ;)Vjop9z?Gg2Riv zMQIM_>bQ{udHcXrBokdO8Av@e^%Y3{1i_Q`WSY=m zx8KzEdL358F%@klnFoaI6Y%OY-J8{C?&XQ!rpsBhgKP+!i*A(O-1la1$Cad|U?(F~ zQNIl~Thtz8;*U0W4Y(sF_F;SKzdwbEvp+KBQ8F`Oyr%J@3UABL#Effd5}<2otb&r3 zUrYg~4N0@3g%UjkiL zW}Zr7hJ$dg>d~>}XGh{tM`!wJ$7XI`8uMly7O1mSaJsLEnt_d z*enZC#7cOC7fvVVGf}+GuX=K|;Z#=RWd`fMreJs)QV`|y#P`BR_I|p!E@KgPRo zGGlYIA$Wn)v=bBQ=@QjHDOLgeKPms0`AF~gowc(6`jR-v>mBG9wgmbHn!fqwP`TX}vJ6FI&ln2rM^esw2*K^V{7!Q|%@U#$(s z_E=5~Rhv>V#mB_fl{m)tY7H`f@^7>)AC9bf-q70l(WDCICBToj*!phE@^aOI&);)Z z;DBWA@~MlLo21UN4~6B4biP#Wz(D`DpI@P0o5393xGeam`wNSX{(#H#=U6DZ)d;EK}jl#idkqeoW_jLI}r+R~&9(1^&UU%+Bv0x)Folu&9Tm5ui9Y2>l1U zx+mkTEwBIXlK8k}e@lg{PMTfZCUE--P$%Jb)}c*UAndupsE4i=yqzVAuX}-+;^r9B zGm5}o6`2GO-|J&#*hMIk+rwd(o)G)p%4r>%Z2V69v^0x9%S*Fe4&RPL_3{MZVOTs; zjvM8M-b{WYl}J@x=Oa&3{CFAGBM`NC^+Eyhc$DCYjvmAWPNX5L3Jv6_D>P2V;Sano z(lvL3U1{|;^l z-wevlZ|`1aIOo0$BAW#+V?XzIkH(F;iVc0fiD{TEfzQ ziNGpiniGrkU=v!z0KoKF-S1*Ouh7OJdozDFkZ+`x>Z}{la;C0XZ7{>9-emSo-M*Eq z6sE8gw{KYL&{c(zOl^8li5|1f&w&r}SG3JDGJ$!n#ndmGHWHQE8!MjD@ELUfr}sNSdC@t^9v|XhmN@B~9x1Ir8Vj1*{@iV>g12=;WgId8{Ov!J=|@L`pR9s$`xn1G z`nkIV0iMu*E-{39n9b6=_X<6Zx#~b)rMY9x94!Hgqs|8V%%1UqyXHsNZB~%1?5-R^u%_1S-Q^#VxWb7IB+thA|0~Znxb2=@_g|%X*`v$OJrbLllg7nK0oS z(_+~B!x6tMRP!IUEN;H`EHoWJPo9}hNt*jv50Wb)&U^Z^4#Z2=N67xt4l0N*r<{wU zW%;^*nTE_(EiSwpYc~MFJCGlys>fA+W&F!FtTv@{>@U1k{D}R>4O*i~idk;qiw2V z_7C`9ke>ey8v4(~ZNAh1l&`#y?+C{hO-AqA$Z7ObU!#p9o5y%C4vg|37Vu=c4Gd}0 zO`mg>Ows#>G9o=OF{YvoT19@@AI(T5Gp$<9%{+Zd{_G>k#zYz>D$D4DW|q%?9k!YL zIMr4PVh)Jlszb1|TYUq~$c_+8YkWDrB)3&$ZCx9$^3~T^M@oSwX2j)j@@fh-0lr^* zrQVlq(>RPjZWCRL(q2`(OjR6g-So`$|LeU?ls?N&BmqkbXV#D19#y(Q}W~RHE zH2hz;OjW?~WyN(LhB$2%n)vnSC4o#rSSbWE$|1$S&^OT$0s| z4b@;HAo~Ya9r!_Uzfx1p<7qvm1vCiSvz6$Tly-9NRudT|!1l@tpAAl`p+#>CVHZwp zneGYtn0^_ApB+Bq2AR-Dh$Oly?nRs_l18aiv^ggF#I>JyR<)o-;I2Fq9o<&(Sj{u9bc;6ip~ z7_pxnUP>s+dSYvgTlwnv?KM-eb-x~WVtIa74ctikI~m3O)k5i94| z)LMAvpnwcMv@cC^(Jt)O>%z+QR5!kZnpK_ymbC9d@61tD#)hdj_zL5HUl=~A!Pm26 z%;mdz+oSvWw|+W3kN?J~w|0$pC*r(Lwm=y#qjRh%Ak-o-9542M;Fpii+@);@-%Y?M zU4FXRC!H?`O>05#f*v%2MjpO=JaxNxbi|`t7qe+Vyq?6!slXCl`%x@!C~CO;P9KmOi9D$_KfmP&}D zB5YZ_dg1n<2E@TKM=-vH(gWV$p9MqB9l%ob(Oa=~O|Wc0l_KJ|D^2$2f{@$Bhrm5Q z(8))eB|-X8USAst+#d=E2wDO@yCk2>0`a$>_Mb%q`IyIJ^|2(ZgEg=B+ zey&oz45c3jKCFi5K@MNKx)iGCB*6-q78ECe5BH1;ka#;n$}pwGmeucSPU!Q9pKBk> zBDj&GK$}9;;QR3QIJ`a6Ji|@`pOQ?L`1H8OKYRzNJVuJ4HdF7tB;2e=m33)<4KM z7bX8}_5mG>DfulMtGm}gQc7!GVP*9mUei~@gYN4|XIfK@0eS1qFo)8jiNxaM0D-ey zNYk?JD|TJw>d;4-0(# zs$NA;64@N4M3!!c39c48&fqW6$EkaRh9Km2zlM~6@5wakB#5eDnWAt44DmW{XW}l| z6A90ga-&E#roHu}`=uXXOgRvn4m9xEl1jX2@+9(FuTHX9?~8Zp{Y1qr#zi;Y=sbAW}vx zD74@2`ze9a^+(DpDX%!SDzQ{KVV@jqpn^|7hp9z!+s8yngE&%!)KmQBd=Ny; z3kY7dO81CiPpvJz)R>fAuGOu30i7&w>_0v1$EdO9PJ9~-zw3A}gCzR@WOAN>pb@LV zXw(7<;9vxXUmz7+>3a^}O)~~jg~A7*?m<-6_7%E+#tVLgwXkhEt2$?P-IkCSNK2^b zR-TkHzeV|cm^K2ZBs^l!VM>FHzYxwp7@=48LTz$ORer9yjlpak>}|Dmdz3$T z))?HjfWEa@^*Zg3d`Ld|0wNwiK06DHWUPRj?SIxbjurDjXKhXs3elG;ko=>uk7HER zUsGC2chZ; zGt;Wz>#bROR|ON3c=SAz?Rr#+;GPW#La#3uA^`Z}rt&!?k0hPiybHjKl{QBB%e4e> z@H)sBQJy48iR%)9k6(}@SZ3yMZ?m{D883kk&X-9(!l>H3ZPK&1Yl)VYcs!VNr-Oy+ ze|vjF)wT-BG@g};A5J-YVU%?zi(emUAy)8K^hi(K1)I79l-eYE_&cg;Xw%Lcpru7|J@PH1_GQZpc>2> zPHIV zjc+~QN*{I>^K}!}1Mz?iSCjHU7xOzh;gXV#{Qsr^xpUbox?}3iXub~te!TDFNIe@= zfxM^0$z>=q)DXOAs2GgFmFKNkk8fhB9(e~!uPKRl)PH-mq#yHovnd{cusir%Ko#OI zen9F7&{6;@Yy50lwv{;j9KrL3I)#`qT8ryNbN?b}@`Dy#{K^(2?uD{yvoM*@REalJ zn>XusFWt1~0Rp@xo-_(uxplgLXqfNf@isQFY6MBZ2*;UrGhMIg0Hwu;4Uv@QevnRh z3*4n%?mE@@(?q(GvXT;D-n~Lsn{r$W>dLRzH;*vhh@9VAlPiAn6;fpq*nTMAF!4=j|4_vKu_jifC6{|`mO;Z&Jp9R~Uw<>5 z`ce((Z8})VTzvi;;l4cr7w9RI9cZd5j$dWDHQ5>2+S)oH8(L6cI&I=q>8kkNyRR0% z`ZqdRs|I^WS-d>Ov^O;Np8cIyqwbd+dD9FM)?-uwxuXM4ZsZkZ&g zC~WPhO3#=ppxmas*JKRu)oB8?61H<@KKT2#bBf&<;riErYvQC!)ERb_PRydqk=UXK zV6+DGT$j~`U6{@Ya#Z2|hVTW?Bt>c=ctb0Q9kBc}6Kr<7-jFaICm7qfx~n0o=y}c? zYMu5EMCKd=N+-mQTt6zc=I6YBkC)C|rFG%9AXeffkD(@(`yIGho*BgoyJ~qv-1T=8 zTYCt;9^&CM22WlEt%`$t0HV?-n~6jvAr&z7E|Hmtk{~q4=gxu$wYUN{q7{A^%|%u< za~=>A_6ij6FK)3?AI%TdP(%n&8hKH>qLAK0%(flOyqw6qVNxp8cki#Qrodf)fA?&h z1#jeG2@N`8pYnopPMtaX&$vaa+cPet8UcA>v7~V1>vRWwmR~5s$i=rh3xE6CG z3H}tJzL$nm^lg<$sC+fT*((*lEb&my`<{cMV?7dMtrC)2@;&1L!i6PR!l4F8kwRaO z|8#NG3A2m8CPPe_h8Tq6L!YzFS4jR0pZ@C|#4F_>?|WnbsJkcE-Wqi8!=Q?Cs0u-;x!eVXK1#+v#u8FNg0Zz}(1U9E!@X^^gO0*&X z-UPI3(Mne*2>U#sjFH`g;E%0|oFYzHeP;f*W4lPJU^yeDD~|>pS_WEn(|B_IOYRu; z0CkqqS=)J`I09;Uf#(ieBm1KsDZ5&9Uo`m)SYIa4trGf&5zOML%6*GHdV1!mn)Sqt zo9*)R@px~K6BB^(nHVklH5p=qrw)-E@YgxArqpiO&?xD3L7T|R;-a#Vzi$Lw8cIXq zz5iS8CM9S%@to*eV^X|O%6?S3wvWQFlCoXK{rjFBudT&m_kW`K-Hu}&pDOk;Z@q5D z+1o$hmu+|dHxeiJxWjhaS%;OKGWZ>!Z40%Vx13Q+eD6SZZ`4?7_<5lygh=V{SQA2MT7R^;zW?Nge$NpXn%fYUsr@_SkkDuhfH3ilp z_;HF0_{!x#)+;Zj|K;21V?j4J*g1aHIubP;JRe+a9 z-Yj*sPX|)vHKQBRobTn_@dkZ<-t=uLz>@%o0DX%x^WH#RLt^YQoW!muB=TR7EgT)R zEQ+!6;u)jp*z_-o$sxI-kO?t*Ppf$z;;GF3x>?c78b{)kj>!$;F8An1U5~5NmsBNQ zMtxOy9PwoA3s*=nU*^z^u4sZez7jnEzKnnvD_J(LtJtZ!iaD?V*qAh!Y*|CU;uGQ# zFGi(0z99DXxa{@xW5aya9aR@C7dr}H&T84zzWjIRlG zl6Y`fH+4zM`(AA+u6b==GSbCM`ZoL~ePtU#A&GsptmYB!hR3{~k5!pOc-E`-Eu0np z_8E>ft%$X0oGpEEXD$zw#G}7!Q$Z%Yh}KUn1a&*Yd=Im^Fi=lGG5C4`?7UYzVKH5T zb!N_{TdTJw#?Pu{kpE173K#NAd?94`*gRV+ai-$MsAfA{-+8TMH3yfr#r3I=dux)p zn97z+kpI@%JKpkhZ3S5xXu)r%NTJtSgp&y}0Kx z8wPLdWTiK1Z`G6NX&GdcU9$H_)>)O5f}h_WmIPi7WMssoxe8z@Pbs5ajXUS^w6mUV)qS6jrd3lIIy+U z>PNOXExHFQL6M&k_m=HQU_Ft;!iavO5%MN$MS_G3d4wB_s=)r323g)dc?#`9DQw8^ zw(S5%AL3|!5T?b(tHS<$Uhb{%iICn*;Uj&4XG&(2xIS%Km^kbN^S2XCIJJcwu<@|E zyye5E3Y#@=c|ap*CmmUXGb?4c<3dNJ)>8j>k|F>)q`+CiE=>Fjd`z3u-dxseF{>a% zqx$}UvX1GyCU4Qda2~SwU>&0nzQP|X^m9_fvt6%nv&AXV5>pgUmamGRzk<;KuxA7W zp^?2mE!u1w9}P`tntd1P$y(Y!(=Bv53@eBa^~3?}8k31c!Ftofvt2*bO*$PPn<_Y| zO0Rq&K1}=cH#UCGQuT!t%VakV9WTPPa1;|y`HG`7Os(U6f%co$>aS=I%^7-Z*3vjw z(d~~2O|Do{qT_=4t)tMp=N!rP3#+9(%oEm4=0`mr<}JyT_G?ia_98lT?kpXugG&do zbrXa}W1ArqAIKK$D|_upp`E!PQ#d4W9tV3-b|^cVHod5L$v@!^_cV77z8HhM`k&Iv zoO9ga$%O1sSVz(a%{UET1iKh*l8W#{oak%YB25GJ5 zH_-Q&CNU{-|IIyIQh4!0aB1VD&4Z=Q;*#w%dAK=zULc>`z{W6Q-Yy*j9DK{ zxA5>XDqu;y{`G2H4us43rcmJ(#bIPbK{-$Q)`EPa9a$fEU0dy9*5Z(FGD!61Ysu2MkiaQPD2SR&<5$Ms0XwtX#U0k;b|7E8d;X6;htu^aMVwjYx`j=LLPhH9yuDvWTEI$*Ce|RqF$0|GKVq!G z1>-yzeXj6CNpq?|{Op_e-($$V47Q>a_vXbgYcGIr@UInn1$;n_w~AG5-DO=$k3*q- zRVcE+wOVqTTPks0{T_U-zbhbSxWE=+kf&o9FxRKT>`4@QWU`&P3ecv*ZK6t z{vQ%L&@7-O1^6aHTNC)xTVsh=w}#Z!g?O4je(jJ#2A?x*dpkppV3my%NddarIqB=` z3drX*T)W#c;mwzBZGlTk8%harwGx^a9qOh&In_03KO;k}c0V7?kAJR{FfM!d?IiL^ zJ>!!yS+J8G+rtV)YKXzNrFszpJ=z%BS(!cbX$^9)KCfpwWOcr>!Qu6RK zNU-T!3P%#07_V~fw}l>`j~~>6QT1?xgsQZyp(PgN>-~jNWv$_rZt)82YP*@WTr;{i zC}CefBaX(~Z7=!?hc<<3324B`A$2B`>FE^ztr5(zF`ib?(%@G-u?r(4+r zp$P2p)Tu0}1Wat=8&sWW%_QmZAc^H!aF0Y?5ix993?d0X*FzreLjD7^I8a~FR)-zC zTngIe4bE`GBwYL4vDE>%f$XW5?A7?`d$G?{!`y7HaoiZuAEs0Q*(5g=FlN&wh2pFc z@8jyw=sLCG+lX!k22wP`t-Ucw6V~yq`)N}p!1OaJ29^KFm<9gUd$a`Nfr?^De}fiE z==EWyYER!m;Rx@UZlMI)V6A0kK6W`oEq-R9ccQu4?zxmst2=dB&cT8i?mhM#f?N?=M?jP=Yqy9~ zTVp8~7xs5bHYRoXC0k0maKda({(IKC{`^Xnnn>=cbI0}WvUD0Fby#b64gcK$g@c0w zUFH`rY1Rs@hF*8W5A|~bYQ@3@b%4xa*;aR+>d1ye=kw&?jf%;56Dl+?WH5DXt1bl{ zgV=TEp6OKm?G>3nyl&Y*E)K`F*(Nc(oWET$*!RzLVohFx6X!Lz^G9P+1F1G=dHgx8 zg3th0w3%>GJdW=|bipfzko{j^oJ{q)GX4;c%VW)!6^8Df`4K>aKPvSkKMzRafMw#AlGnqw=2(^v87F?o;NA z3SaRfCs}~>o!ub|B)lMuD&(Ztlz*<0oK^Ndr+2aftvgmZ!^lHqsv^_xG)aCz{JX^AL^X)v(nVfNFzQi~1rPF=c5 zs9_D}DTPF1_5lO&>yb}MCztCy6Qp#TWzqbWAruEl z$gfU?H4xpCSS#*-uKkZ*r{~HQLFi8lIedfermSpaP(onu?nm_L8FV8Do%PAXoaN|T z32?6b>>UWv$^n|c=R#YuD98A3D!_oOMYo1>f4 zn?lsHQ%N0kS6)oSWyQpSL=>gI=M&W%ZGl}QI}!8WU;8&(62(SvR6xpQkt~Z*1DAsw z{XfMYETpR@rlxe_^u8@taXjzw0fUj zgf>&s+N;<;_Kke`QIw`j-nO%^&^L0RLRj?;+Pq!dKGv95a)B<_SuGAURPc$lU!oz` zZDCQ{CdK@phX!o;Sa}ViUdQ82KRVJkO@EH44>CVqpg3nnr5O1{zVo8I0N%7?dqJ~e ztmUX?e4aNpn>6@6Oeg_7*wx&hicL!ZoDU1Kpi_Dy`Jc2@AC(Th!Ts5{SE~8iu^F!? z+En!tIw4-$#F0gQ(eCJoUxp)V>p=}GMc{=vJ`(DvgmgWJTjvqPFjr*8?ppopCmU03 zKXZcIzW&(#w-cg&LVn!wBL4{EmqH!(@i4Bd`8(YLe$(zr{V#yj!=E}r0@U>W?-Q<| zlzLfv-tSL}7ARfQq|-mwc=VUL^Wr~8m=2iG8^yozJ9$OX8}24z&tEn z7*y}Ci1}B5`)uF@)813ODlPgWIWm2R3@*&WBvz>vc418~Yrj6tg|u+Bc^zK5iDh0= z`d4!!b>(CqIyo^|+Z{I-@?+E-2%rG;pfutx5|;-Ehu%DMqA#Sim*A+X`7DFvkpI8U zR41UrP@6fpS`wMnJ7eXC#NwUJOo2Yz0c?=KuM>6rGHSrxNVLy;9Cj*=%+kPGg!%R} z5A0i=XOsbfO#HEFV#Jo0Yxt6< zZ<@GwU?qF!+P`790NGSUXujzE^3p*p0WSMyT3{to&j0k>I4HEZg9>b2?KtGFBMZzU*khu->Z^Z$&w(|3U!^e8%-)~xpC zA9!Nbr4iQI)z5^yn1X?pinT}8?+0Aj+4RP_I^~FEJeVcg*wpkDOG`zbX@Zq7w0{j| z(d4yL)|Kyg&ZlDRPuLsBw^1pNBhsBp?2ucFP_&1^?w5Yv2~laqblim>x>bPXE1swN zklR+eC`u`FQK|m{9YNy00AB!p7fbu$Ng4PD1@+|MVnhf(gJ2H$M3Umj`@fdvX06gRd=t4(>blM}%lx0h@a4 z3&PKRp_V4l-RukF&3*X(a4e>*<7n?&E}VT#bHI}T@FV3i0DLT_%454eNlt&=Nf&|+ zH8%dteHP7*egEi-g`oYH4f{`53KX#C?cieB_6GfSaj?AHs+Yy8pmnfJ8+@mgzqFmM zumB!aNjnG#bJH1-R`l4|)YrUgXPU_17>XCPCk&-nvv(Q5B7_$ftQkqGu)Yt1 z(Bs#&3~OBLpv4UO*dzvT#L?a_IbaDvTOTl2LHl_~N?)zPTB3m4Ih|?(IKlFlF0i893c0!wA?S{c zt_GT7(8AwsuLyvjh6W}D{PXkAZlig?Ew67Q>~13o%n0rfJ&e9!R=^uX0ZS28B$*4gfNdk zj4R;0#-NAtY5>joUMOJnA?13GVj7ZfL#jz=3Yac6Z9?Eg4L(xA-`<{X1~8g}`NHa& zI6P!V9Bf4yDUeyDnr((p|E9oOhHEOoZ(Fcw<_&FA?m;{v+87Kb4=jp359JlV8iIBK z=)e&=y^jFQ$x`&o=8oRJ-M!fWY&q6g<#1B_fpQGsCdmO4d~@C>Ba^J9nrHzq<$x0n zb;tp;-F0{O_U?THx!OcV2n^VYPo<`YMgC?0;uTVZrAc7x!Bqv^I_ulZaWfP~5UKszM zdI0x50*QpB$sc;LJ}-Rxj`j8Zz!MJFyiebAaO@|B)TrjCnp7XnC8~^>cwV~PxLx=gmcWp~(h)p&Z|?i~>m-3O7}!@~U6(c_ zXy9?Df5(}=FA;lrVgCO1`Jev$JqED!0dpMs+RpPX1kFXz-V6HiB?Q(SaJwmBEyM~H zuoPkSztBk`= zS41Z|dviToa-GU$ZW0y=EKz8zP{!Ps#0r={jGKD5-`;mLfxz?-KEa3ZAseX|^jA$# zz?ucl520Wxh0#VsE8EzJBUQr-k0atj>NegdUk*N|@Z}vY%a)zC8YOVvC{2OqHXdh` zO{Jt0S*<9ySQS<<6e$S+a|oKNd~=Q66H{jaV5M4CouV|K8H8`s>7!4m6NgS9G#_ZQ zhEAjhCqrn&N^{`s*x7yEhtFvcm2G(kU^flFF!*!ZzL1 zlnBzmk>q+YegQRP^aUYG%R)S6qpGl{S2^ruaE!iEUzPQ?8ya|4@vF)W{XD`3^}(tJuv5TemAmVOv9nf z^zWc3U`Sf(<(>wI1;q$KBlv7iE#l8Iiwj^pTN#zGG=K~lJX-;|N&ns@{N4qB7o;6& zPKvNt1AiZep=BoU$8I14(f8_^mrj28+)K|HfN4y@&kr$xKYRK^k-qCxC*P_^>SVY5^m6Wz-}C& z*kG;lMbO0GH4NZ`Bz|#uM*#dYT9N#H9qC|j_jNJk=g9@@+`#SIN3{)@K4J@)(ZCZK z#E9cJXhSj$OvcMHI1uyyxNf;kSj+uCn7f-|{MMvI?K=3f{7vv--Z%ueHRB{d(THu9 zhNQPhL(p*rob>>PTB}`ic?O_D!`^ zKt)y1l>2S$CSDvquIpy>vq(ouv*d%G&RgbS=%<>~b17%6iX9G2GBU7rm5IzL87;rW|l$fB<}Jvy2rE zx{#s(z1Ec!xvyg$$W8%2wju^#ErNcO0wx5HaXIu93E;cue9BVGd~ObFkd?h6pweFc zR7$O0b^sps)yIX4TK}YFa%5CHWN*4w z*?4y>okG8Z>oC%7>@~vP+R1Z4D^mQ-$>9-(AE(5t--WGgV?}+#ld*-pN@S&u;Sq8G zv!h2!*rn#@VLO%DpJkumjCSft>8sY)pqCgOv6rlV_2JW}kNi=_x_d!5Iz+JSgSm?! z@L)^;^JQ8MMCmfkE}|t`%hYH@3RoOS7`>%wb5D^PEMjvB_0jCo!c9n{a4q4>tT31z zI@IHfJv~@7+oJrHcHln{e&@j8>vN$7h8r|p$5{Z6kB>jNF>&LE#EP7#3Ld+)R7qzm{ZR=_?7+-OD6E4&ZoW5GCvKl6y~3=q6EG^13n4pc#Z?b?v~ zxs#~BstCG2RKWlB2XnwM!fH4EZcryI$pD8rUP(- z*TrBKaBuIU6tIm17W*sK!SyJ|LI~RWfH9;H>0i;uDtl27od<%sB`LxHfV2QuV~vrq zHgfZTK>8;%~cH=lH;HJp1mX)gKL|DR29pk7R8$fS>MKT4=gat=6IyLKNT)Rbkb)5BRmctPhK0k}D>TXPCHs|pwZ{&I6N z)^rqECOAZLAs&y8vf3$rNBV&MFa{IARYh2r`EuskanP6Hdurmi0JsWVAlXfA`@KmO z(&vQC=O%-?AXc8%@=AfvkuyN;XHdY7zBd9LJicuG)dw*O(=t2!$YsQBPkYmhQJ7(> zeTH|~fIG$ruH+a}qNm6*9NfA^o_pFoZ`g=73A2yWllBZU`C>HE>z%H81-k6zb#yN~ zeU(P{QssDw?j6CZo6=tP?joe|p~3Zg$-!%HY5n>^I;@ZH6^~9{XhSR6Q;xDnuT|!?iea2GO8-Lo8$aL_`Fnk@)EW?0%5Mk&+2wkqQg6N$V zL$hhN2cr()EBsff7=LkhR?J<<%oILhvpAV?T_SI+D*Dm_{rhl194Y56sRCyG`-kf? zlTh5S#VXt6oMA~x*Tv&1>N9{ zuzUpiu|NTf7l*o0KGXkUf#5x~9;sUOo9{5)XBF`IKZ4O~49sS6nMOM-_HZnZXG zltJ4lq>YvW7AHzd0n;=F$pO>y{5zBgzVPO=3}CLqV*LvQ6Mf%O1alwoMpnSQiVv2L zj>`ZeGRv?W@ZUoL|CNtG7m&}^0Zz^aI^SAob^2F_3JZv#b*rnOp@89c#Q^TTNQ*}= zrZK1jwt2vk1kMwIsR#@F75Fk>xe2M0a=;gRIukJA&<vB29 zSb7rxjOcH#7!CFcgXtr93x}Z5f{X4Gm|tb+R1NpiHpdGYaeH7{E^?<&^^a$tW9B#Hx4QYTwc5-@|Wc8j|W?3?y_uSgL>zXcaWC zXGj4bQW1RWI2A)*J2OkgF-Xw)MGuT<>TZOcoNlUJ;!W0MbdQWRZt_8HUAj_Y`FlZV z``~xW7C%2K0L|Q0{NiS=^lw>&PjVAKZd-8h2>!` zCFE$?y;O{@Rf?Cj?c!i4fq!T%SIJ~M&YG7eGV$KD{N5!f;8AD(J?-2>3BaRLoK-GO z1Jj5`&rXX+CrRO1-p+CydYUybWsQ}+H;us(e})GB1FbxgkcIGdXy8AH2xj(j7hzKCo>LS-g{3JFb^Yq&O2ef>b#&E8a8ZL5tKnc?q@Ne z2+LZ1+l9nT-+o_)#YY&x(GZpxNRIbe7Jl2 zX5@csamdG@wc@JIXM*uC*Jo)8I+fH4Xwkc@fCCAfjvR;?yiFo|IRecHj?jzb!|KVb zce511YbDj@pOKV`XB1wE2_?&FrmyN=*A>ivTqxk{PvxR)P{QS#iDNZbLorP(SzxV# zHvKCh=vpFhT@1iyNdB?}J|jjPr;hVFK?I(+_m9c})?zsxTj*ao9 z6z(i}beW}89t`B7dvZB^wi2o0(G;E?-YZwVx)8KnGD?>`sof8#xKk_mA$p(`zF=k0 zKTs`h^-8!kUm);k#Gh2wuWJDHgxe#9)|a&q_pk^h!wIyB2?D0^eglXd>_*crI{|1HNuEk(9o) zoSC3B@B;$zOJ{COjDHOLzAXTLmjSF*&;sD|s({z7s$JC}WzfrvL((=RVQ{;6VL2mc zWiU4ZOA*#v7fAv4zu=6p+WRf~>>|9b3#qkQ0q0wt5wtcT0lw{00_|(CI0?MY0?_N= zMAu1}z6zg^7aElOF3a~JYfnKtoio4`z*t=_2C#x<~!2c^3 zVf}r)38|nt;DQxywQCGI@UrX`)=xom=sJ)Z!d+b=`%lZ^sCzg zqJXIf7!k&T1{*2W_mqU7X~CyG_uXzfdUO|u7`e-Qr-+*4)bMuU#0ydf$zGx@iqyUy2u<|#NbYW=rq8xaFrZJ!miFmS>{@+NmB&Gwx zU@w7P`d3{kJ&41=v3fGI%q0b^70~itbDqWg*GlJ*04DzOnzOQQtD27WBMC`?-=zGm zZuqaOv*INuf3#k;$jX|XNmK49IX})lm5WUB`UJ4<@!%TyXO)&PzHHhX%VO}z zw|cM~ep&c79>E5Azl|drms0r%T_poNJvcZWDqvH-%wObo*@SYG2UGA7pq0y^aRgUV z!lZ(sfc5V5Y!HCHd-ofse;4K-N(h>rIM|D0ZcZc64+y>wu0NQ%J~egb@)xvR{_u-4 zWeuF~K<7~$9H+-Sx1IDihFsh(q3V2n+hL{y)+zZ-QTH*4H2w-ak){HO&aQ_Pz zSpoOIU{%oO0xj2mmjPZ*0qbl(!*8d$L9_Z52`mbDmCRxg0X+#byZ? z)P$qp1aQH7LAxsG6Q7?5tSHF}y0cXQEVam7qzJ>1=;L?` z0IVTs636L*?%U|)oVam=8{IBk`0S0(-gx7}8@v#P$JXW;(p$EW=>_qYMDoA@A$H)S zZeDhGlT_Zf?=UZCdF9Jl?oAncvwL$klakMWD!z`ERlqEOsqAWEUu_e6LCXP>wAm)h z055e(_La~%1+chbCDe&S62ci(z)w9<%LfQ0N$NyOQbse)08Z96WPBr1U5vr5BUtwn zNgKk%I82$$)ixw0u<{jfPV?_+O-MaVW2wV3%-S%A$Tu4$bqbPrZde7h4jkk)=P2B( z)mTY=L6G?iucvizJvI#QLC&q>I=GjQ^FvEOiFNN0BQiyu>EdMa zkwLk0L>}Vp5eBfNTaWOI`WLE(?oYtRBl`IIrQ9vWxLK;=>H4MO{ZC!War_R36#jOZ zGvo1SK)39p=7WRP{FNZI1PO@0(7dH170CgYN;(1xTUNm`0!wgwg>>yrX-JX`@GOKc zNnl!OC?nlH!iqS|0ne&Ms%3!xU=r8|p&wkocW>(6y)$RdeE5YfmoHCgGt&FCe!v2l zCGg)!2%2-i&q*6_l>%-=4%mgDof$`{fEB@*j<7TU?TVnGfZytu5Ok~0|F*WrN|cuC z16BnrW|Vo{@-U?KPDul65p)O7VPO8|#EDV_u+ITg5f)o82ek0aloyi7j(N?QMyK8nFm~`Qo!Dd zgA1=H1Wf^GjzHgWWmwB8VDLAK5HwFhVgW1?*v1ha?U63vc1;2&AdcI!ISv!T!gJL+ zE|S@I`0V)jM=kVGFACUI*+e9R1;ext07udoOmL2ljvc?oxIGB=UigghOO*zn-MH|^ zjT^@&#y?t@BXIU?IZCp2_wH9%CLezD>>iTJq?LDyV3u^?d#Cu{ByDy|Zt(C^+2jz$ z5`z(UguhFYz?-*^O-+0Snx)ecXa$EP!3qVemQ#`8*RrV^g3d8>6IKA7&>Cook#ByA zBF->_rjRO$S)z4gvbHuyRF*m{Tk0}PfNXL&bkINqnz2We-JEu#3BwXB2w+YSYY%W! zPa1kyJuH(xjcwU&5In02*t<-+VMnHd4a2=|Dj~kb^wJL-5CAu+=ayH%k?LQqe|DWm zqJC8mN2r3%KJKRha}ic;Lv;@LjQVh(2#cY6?abAySFfoACKfAouTBwir|6-%tGq}+ zTBFieEhx?6cXpw)0R2mQ&TZpCQ2qR%+Wcy~S^|F%D}oM}dDE_vdKL8Ij$jrWU|9B; zusEBEDCAuaC>nx+UuGGdO6z)ld{FlHG6&Zys2zr}%7)XyL{ok_mcIO>)(6~lpxhhm z8%s-7QkCD$@=919l{ZiO>;Jpy8($W~__uDqU2?4K)v?B<(R*ai|5m~K6S}4N^ATD( z%3K!dJ3YvWfU;zFM}@w~?UrOBV~NKf>EY2~_Bv$^EJG?wxIsdgQ@|{OXW__!4y13; zfrJ^uWXa*@60!iEV?#==!y^9Po4b0M7no&%z9-MnCn59q16t|wKHE`31)u-meWGtH z1RYbrYeNP6_+#FQ@-gRxrI}wT#G)H3y;1>;X$-9xhV&nyfLj9v+;29^u^2R_F~q%~ zbsnLML3c_eH0$3^@!^mlG`Ar68faF)+t#ev^`29}5`{iQWV91t%9azr!FbNx_I&H zi+3)1Cuq7UEOElJA}pzbreZ2mw&1T+L9+txNhHh*T8gk}|3|rvEgyZf`=ht_NvGQQ z#DjY`9^4o|d^8s&)^-EHEO859a8@g!X-@#mOypjqLVDoF!8bnpjJf-nz?X&asT%~> zv7K+evhSmHb(CW58Q6Dgd~8g4OX0^u#}C~&e(3n`es}11vK->eq4%gXAJzG6Rlc)3 zm(H4BgjGQ|i4jMjf}gm3ed^YBv8%|SfH*6oZUwtpOlZhj{c+55N)>R>hm=TUqy`ID zdgq&u_B1xI?=!|@XBv_ufYZX2jPTb(IKZ)2!1a>ub^yH# zc~29(DM3#MUP);%mP>y@TqHOSP@?(0ZiyU4)Q90c}3uPj?t@WVU@p_ z)Ud$H7a?dMxI_tH*1s1%k?kkQ8{b>jfl}~0zO)7DgLn}71C1@0T$FjQD&fJBiQd7W z<_deD=X#$9UJn4SFA0osyLV7kFT>J;(a_6+X1TL>u(ZtTkFPhyTXN!cZ#7TX$3w*H zmB#DCMD%*o!0YvWAY=w#!vjVuLZ^8yCQ#ygtIIYfAQ#07f`@ zR*JAh0SD8NN!gCte@qHN58q`9Gl8Yzk4v-!#iNR2SaZx`ZUawWs7TIZp&~h8q=C_m zgmxqfLtnl81(TQI%iGImZcxsbn~|I!2jiDx&>Vt(|NVExh{HnAOUDrgG3e#y0Xx4* zm|>X$7CS89mlZGr_-iQO7y6w9R{Zwk(%K(7QIaNfZAhQ1nr!!l(0LjMyg>zUI}2b` zL8AuC7eTkE0^arBL9K!&CrUQpJoJhHnDuYVHnHDeH_B(XwQMv7TNPG&XHX+10ptP( z0$UEa%80VijxuQO1J26?VRfQxSAJXS*Ea!IRY9K+2~3GzOhZBy^w(SlebKiA#})AB zg}-tmfg|n%MiWwfJ4P;$0;WFT1ea3xsu31So>>9c6PH^)rVce)9^Clk)Pwgv;XTp~ zH^v8Y=|s=I+XG1{!iwyJR@|T?R=`^Xz__0tI7OjmiaE31B}=1mGWR|FO80>S=w_oO znQBh1>z=rA?b;0_{EpvvWd*ih?UZdrAhh97M+Hu0IaF>>yw1U8=&QkOmUYN$; zElo?4!0D##UwyTG3oGBORz)*OBkl3A`F#Q>%fQ;)V9NogeFw04L1#EkOCjiip2oU{ zH03mD1VbGmHkYO-w3ENJ@d$KXoWd)DHl%E<1Ulk4^X4W)GJ{ga4o2|50)NwK%K`VW z0M-z+6>8ZpYIaPvO1_x+dxMj@6ZeJ$)E~N8%mC)-Y zRK9-jf7r4Zs_^y8;CCz%V(}mp%R+#D-xJ5 zOkN2=lLB5Iz~=)6EaM2{1hy(@b)r=Hi!^Y*rhcEXaljZ?*#E*?|A7=R+d`uVtG``! za6H6oRnRBm0A}ro*mJ(!D`1xZ#xNuuM@UiV97|vwhGYQ#Ev%psg?>i>EE1UYuUKNe z{`#|on~kJ?8NM_a7)4m}ll(g@w8r{?|Jf8U3Xz{QE38&&L~3`xRP`hVQNb#Jbr{3{ zZdK4$gC#*|C}7gRUuzMxZK|uFhqw-!5!_qgZr}ZRaiq*!A8>Ea#oo?bZ*=S z`aa-Kr}1~k$KM;L?RzAFx04j?`3e9Q1uUsw*AIM$0sNoThKjJV`7BGBoqOIqeE5}D zULo%CKOgGr0mW7}ClaZ)bobN+Za+WBY2nv!BP=HHYVdjI&OLkfyvYl~Ft5_Q>OEt( z8Z#-8SDwII0{*70KdA}cQAxf};98rO!S?F-oRxl@D2jqf2I{(eRCB;-7k+m5&Gc%}ITxt{hMlG; z;It^Dv@=7G6Q%X&0JBG4AigQ z#GNRIB=Z}IQ@9kWFINTah9Nbs-J}3MDP>p)L6ZVL$p9uCUsLo>NGTR-ucoX3i*H!{ zssLWp>@Nn^@P>!lltk=)CZ*3oRq_L`f5$&r+I$qY|EdJmN?J$XlDXQZTiR*~iD zYU!(a@|?CGHE3b*y}?%ipce9cuV5Mb zMqLSzuh&p?$vFL|tKmOhv$bA{s&Vdbedswz5Pus`SxU2f)z%U0Oyh5IgrPcmr5d3$ z^*LCE!(E~-1NbgLEK|7d%01WiORqC46@E%zp)mS@X}z}~3}*h)fMCNf2cIurrGLib z9>1!A|3>w%5}3a3z3Tv6)dvg!Zwzw4Rnr*$3{uxxkt71GIpBWR2n+?>`N9h>2WTu*eMh zcdtIn5}2ESTQH3Q4DQ%=c1y9FL)QIpByBz!Byl^+Jq#1uRa%$${+)4g$P~K;4~SE~(|iuW}Vu_iob61aEnnHN0%v z{?*!?=`@DK9cu&*y(o>s*n?s`I&hRbf-`B!{<^?zSbe1e*eT##(nPQYpgAT@5$N<< z%7<@n$Z;h!jW$}b;zt~#uH`5+fLklNcZDLjE>pWq5;$qr7}>~xiGE+?fR%v~eolkI zh(I?%{2~BNCFALwgp9MH2ei|;NnvSL6mT}HG51LPI2>o?JBo;3>p;rl@7F5;zTJ~X zJCy2U?<5^0fupbt%l8FG!7v7$28<+do$xoNfH~@nDrhL+lca#BSOZV+EXC@M{v$G1Am%juL#*;)K=luq-J^i+U49;R?<#_MBxX?hdCk&ea~Da>kV`Q z--<_|r2>mHz<+}n^n33vQNZWJDrocpoBp->tH-Pgx?d+QD1@KU)fsW12#W&GZ@usz zBnQkj(XCDZw`(1AYp`^|h$E(eWgM{Pfq53gesILiEb{O&KECj6)XfcBZfZ2#i z-U(|z+@4X41p%Dzq}HO|z=aaU(Dj+lj>F?0zrFieONsZ;--If|pWSt|hg_3-{#val z#bQAej9885CqW-fBUTIzf8lN3Vcc z?4oST@t3VCNr7(Oy!}=;0k}4^y1q^qDuJ$TOe%ltgum$f6$r1;CSc0}*T;~_UeH+? zdbByM8R0Z8+6oNSyIEtdjl(2g?Rypo0p*e&v z3V6-+IRUW5@=g4@%}`(o?L45v(1=)3KDOG816KTM2zpaI1btQdf3IDok1NgrbM%=! zv3R*P@g-I!@01ewDkwaEmHDfmf(+kTVJvrjR$Rf=jZf}X#FIhRd7uBS(!b*(f!}{O zR{YEhhoaBZb=-q@MEM);$!%xZKv3wc=kEG=YOXEK_jJ7 z>^s0-Ho#ggR?h1YE9RC8X-2IUib;8wFX+Mph)U}!(6(0^+-o)8Tq1U4@F&c161P8z zF}C=Nzx^*w|94ek*m_0%BjViOY8}?Mj?3#^MfLwD=(XNulD~veMXu5`z^tIv?>pQ& z0K2<9g<8(@0A@ohim_B2gCT*XC5}Tf6vCMe`Aby{58#M3uJj!@0et=g7Qt2q{r>x` zfxQB*$^ow@QZ`Xn=weOHhtb)#B2!{b%C_1Kq-86<1B7s|bJ6QqWS)zbH zuTsGIA(=*4$Vw8phU=eYFcMwpRVWACnScUL$Q%Z4{{8=1d;i#|?w^Hvp?{m)Q^ZCvXlGS~_-|_tN zZ<_iw?{nVgedh02Os#Kzp~Ws4`d@oGHYqnzGGih~Ht2PtfLlObmcf$7fjQu;WMhaD z;>c%g`y&P~BJ3{GTO6FOm7etwt-#!M`oiHgiysu?GIM7Mb|B0-P~B6leq zt4d~&L;+`^fFmv7xW7$-%T$1V`|5?adCJxXdP(G^l}>3t>JwGK62&3fSoxbnZ8+6b zf8X1Sb8-4fXUx6tK2qrq&Lmef*`|fSCV}1Ng>WFr4qtvoJv^?J<9QkNZs zXaU#D_%E*NfnPL=zK_{>i5g(R3!neljmU-tDz-;#l+*gT+ z`t5`r-XT~z^c;xB{6!R{qa z!d7xebTT=tX&2SHea2UZ5B=)sdG+O2zxPAeeP*Qo@OwXd`MuZQ|8OkLCAaD?1Gd&k z1zPF*wMyGJi$|A)!IwBji~t-^bi%GD+^qxbaCn!pPAAX{4&Obf_*K!&(?^V8J=l0~ z$;ZPGeBF*Fc=Uoff^?(~y0=JkC%k_YRIbbw^;gv=SdVuDD5rh?NW*~+At4I&{FYG5s zx7a$u+FB_&XBf7kfb-RPzY~}*nK_iXL2ZUCJc>YAk4 zw2yGMAedGOFVJmF2~?i5mc$XYVX68}0Vb(gD=!Qkp37?vTo!a>9yZ95@(LK`W*>zy z$t&O{GH@@>(bUnTIW+IQX64HDI{@HZJg7jsWx)DyvXB@H876xGr)}OB@|6^@Y+_;r zOX*kcXbHM0r2tk1oI=rGZCDAnTamo?@#QiWIN}aSSPdiM*tK)qq9n_s1%Kf9&heSATZ( zXG8xk2}G)VFG~e_{4#GP=pH81Yj2k-V16SH>tI048&Ui7k+1!ODZyEGP>Dw&|I3kI=1jrLkH-o@pg&rJ*F>92TB4I7C zv3Ejl=+UXY*LfINL!q4pHU%6CV6kB_fFCWFgarW4_d!^91&bK|6ecXhHMTn;{7tn% zOCy->)^ZXhH-VdF21Ad=|c_MitW0Dc1iZbYs!3ee68jVa*TyYEF{ zq|~1=2Tc5JXuupWH-Tjic(slpDTALnv})*j!KlPw;>Qi0YkvYP`2fwx5FfVBe*0o=$zSOZrM z9=sB|pc{+EkpA*m%U@^{xKpwyGxYXw-V`uV7z#Ke1eOXkV|jj#awl^+=5L1nP|^+k z{?i*M2#Z^{3SgDMP`hcZLboV@6A-`&=>)3St(K+j)$$N)f$5D($qQ+RE`g2{C`IMY zo;r0lHZKrvt9S!2?_uCzYFQp!wP5N3>@>sxLp1&WyUS0XU^97`KuH-Nd3drVgwoE=cpTiBwuYQ~4V=t?Cr8mZ05R z+BvV{(SErT!Eq?yLTuJ~dPV;_{bQKh+AH9=8w8gBy%S*%Sb7uI17-zmg?cv#j4|Ly z33{tjz)W7E@L6_Yk<7jJ>Z_!6|9$A+;k>#=ek(5ep#^tYBLjN?D?sitb`*AZQ4VES zHx#f80JDP?UYlNAhFtwQv_BX1fA^IrV4T-^JcWWT>86*WFXyx?uK1n! zuA{iu$21-kqWTj&c4T!5*y&(HaIbx%0MyE9{!D!=jOHJW9wCsEGdX+ZBr>Uf?ky3~ z%-UXF6Vb)H9q9R;@(N-n7D8YH#Bi6an826fuPXAo&mq0nU-$N$tNx1POJ!({TCkk_ ztqItAI}q2DB!B_I6q(U?Nj%MwW1@`Yb>x+89{{j%TARQey%7O7SD`7V{3ExKLGOQr z1%+}8`l9>{{@&n3EVmQ*Gv@F90Kq4i!21q~0)9mS%nEpWv;<8G*zW_5R-g&MFG=fYr@Zt2rFl#?bC0unYp{ zrpqpd2CYEPg93icDPZ`ZA9)M_eq!IUWzQ^M($GL&tf!xTdg;a`+g2{+5u|zR<}rY= zjsZznVrTZ-kU|Bl+Zc}hw_{!cYZONw3OH|j2z``?31C|c%q8e|B6Gm>Y=P&1Tet?D zAq9M}co3Hn%3tdep``FXk*W1cV2#I;!tulUu{EdhacChQZU%=qyZ+{TLQR=@s zByZ%x;;l!$*nmXec~HLn1TR4XZ{>P|pCM;0?%+q`hx|bk`eFaOk1luHg`WuD%KdEg zrhA&A`u%YD$V&;kAtQ)!%i9J{^_D2=)AyvR9xCa~ncq`t*UP4<6XI zgB~RYX8yC#z}%e+o7=y2@3cf-vj;&(ZBRofd2|>f$8>PAXs|91muE9U_o#uU6`|O-I96K zh~*b6OQ|YACg>-gU)+>q$JVr_0~*Ak3K)OZ<*rx2wo;b=GSjqCoo@N%MZY46nZL6a zELy-_-Pv{eUThu|1d|qK=;j%}q=jem6>lhBfKoJn3KLi(o}~g^UIL3LE5`lHSpB+{ z=ii|qaSr%nl;X6YpoKppSfC0>tzr#-wUfz#hC) z7KH;yww{3$u&WoVRSI~dMMqgsRSM5>X#ryi5)^PO1jTMkO6y3sowl+A;M;z*}oN9d-2r@1670`M#kU`xWX z3N%iXpog};#(_g0jT(39Fz!we+n8k&y#9#(Kw|q57Ra|UiD@Y?vscrfi!{uQGW!RM zRscuvdq@Q^6!6#F2Rs}(A+d(beALM8z69;@rG>n7%S)ezOBV}w7w;MViiO3)T}2y- z-95S?QQK{69lLsmxspFD?~%2S8?c0V7&=jL75;((>flEl>Fs&-%fweHU@JlMQH0-` z+htGR^iSV31$;>vOaP{cjjm$xJ5jD9%)gBL-^ff@bn<^ygShGw*haMUpglD)TuflA zKNEgWjbDZKB?a7nrq~57BKU;#faM?)7R>>_^1!2IgE-ihGtCsRL$FuC_dj{^Q*mNaChvkRI})S#1@ zT0?L?sV1x-3;NAtHV5n|EP^+wYX}{F)r8fU)MbR;1x*D0wO7DwgGN5b7GB#>C^UNo zTnH8Lv0wd9uYiwf3wVrN&@JMAW(H@_14b@1D`1QPJM~L*NY#{swerPn+tyPxej5$l z_V4RI-oL&7@ZvesNf}q?Vh5-MtrMSU0;3g-k)$z;qO_%UXVqfJJw;?xbq6zFc3*lQ^33tiB|5WeF0i)&`tFO;5oTCP%Sm%IkL$3|wrnv)MpG~E-|I0V2l)V)z;CgL^=|25pU+YVJt#4HBi@DDv*8A^@ zoyrU+-l~j$)eU%>pYP_k){?J8{L(wYlRLlsDg|u3E#hxY#2W2WyCN0nn;qqf&^;o7 z2fL#94Q7reCWgb&qi%a~WI-H364s!mbVNMW!z^`eh{^9qvI6v=2w>*#nX9)@gBAe{ z1^l8{z%mL9348)3EFMF8<$>>(?E&*X;Q7%YEVm4p0sQ2XC;$GFlLPl(8EBR=^f&Ky z3OEqJ?UIDW0OrLEN$8YCAM%>rlKM&M+)FHw885=Hrj+XozY zpmh!yHRsx-MqybuxFsk-2Xnwt1&mb;jWmbU$OKMiG6PNl>ox{@S{3jcI)-E|U@>7y zW=U18&H=LzOMAe)A2STg^cmgRsF4wB|^qpUcuWa|R_ zSKx38UEdh5VK4!#%aM$)ZvI!MjZ6S5Z_}RCW`jY61Gt`H8#X*nm&XGDpS1|+>q^TF_`uQK$g#w-{3&*;|QCUdKf`7oNT&H`7OT@1zVAjB|jgE8u`Ha?|&u|0y z47Y%T3N+_%oOsb(SUd(S0RC151l*`4p8Vt=PhJ^#^2verK>t$r z_nQ`UMZC?h03KifH?s?i+rLS-3`zGPIRsa5K_`6_WgY_fUY5Yz2fo)#(2|4&{-O#k z`+xypX#zt4?|Ow4@bY~(h`*Zt%o=#j@#AZj%xhXVZ%)JfIdkSSfno2;vUdQxR7c}l-4)`4a z_%DxzEntd-W&vD1CW%|6oC-&~^r-1P1U*yXZE2$DN zeOh&n@}sMp?%P2rkS*!7H49r3I_=pB0FMRx?RYnQn!!uNt=6WlII!yKz}?wuaK?=| zovHo%FbdBsgJ;W!(BlPp`D8p|q-z$4^?-F9l0Y~XLYN-|yYx}@bLVDbfSF`7rqjV@ zK3ImO^U9OBb7dc}2;fBAL0BR(L;>HoV+L(4&Q(vMt;I;ent>!}bzv1-uBP~>tP<_q zUGh`dC=>x0c~`piNTVlZTVs8_O5m6_f$?#csbT!JTxc|qb1&s`$1ne@K><@1C3?UqwN(cj37rz|w2PXtv_jq4nQ%#1r6uSfh@(gWpH>M>vq+qh zwUq;f*mOEUeSLn2RsLEIcMEYhY&fd4BKST|M|Yrr?HCPU)qUivs}!AwBh>#N z$8Q>EWp6UG_nu#5k3#mi6G>!~%{hA}nb{+fEwVW>3UQQ8F0%LD{O1#LAapUO7QxP|T`>2ba&;{e3EN?VhhT%6qmhzDt1nN&+-(%? znX$*WQW23y-rZMq1_wX%aUlW?$p_gCVU!fDM5F@-=mVx)gU_K0hs&}w@eRoQ zChDl2CVDDA{GW&3=5W(`WCNd+-{Jgu>A>@cHaf#!<}N4kD*h=B0I}TODueapGA|#j z5lxr96hr z+h~IC+>nI0{>L(!C5^1?y!j7LdiQn0@uP(8u^31et!|wI;+jx;g_=(?FjLHDN}i=?|poo%{=Zr$?TW4CH8jBU1t z^l0+Cual6IQ%dcwD{JuXHeQs=+`n5_8RqEe=cIIyJ%(qKjHL#)OdT^Z$II%=5`Z0k z%pwd?KreWX{ks2cvi>yV#_Q1}9L5|CR!t&I?UUIF^k`BIr5k*CIo zkL4AL6O$tdo|cw!CcNSVn>X+*(osCwQUx5(>M3I1|C>s^@}{_^?W$-en3ojOIdP|m z(|IJEd(y`cwUO`^BN^F{ zvM?fTrfd))Vh>1075wl8M<9B?<8Nx6Wfh?wjBd#9vquX z!D*&_Vzx&U7iY=5_x*s28ymGEnd%}-)E)_-JqdpYL6~lN@^d!*-!c8AZTlC8>0h+0 z2Oh+5>B1Ii*q;?H&VfyEU;QK1v!T35UIYcYZ!w#{Ak6XSb6RI zdVN*twP7lh)aYFqRdT*z{o1^K6=qs?skg;5Bpk<45K zRq`7+%1yv>0P`s>IUF<~hkM*3K>V9LAyNTTw0K-M9)cOf%{&6T*mRwDIwGL(FQF@< zt(9H4zZvRJpfv&p21aNh7;1+uT)Si?sMHzErKFDySGHIvAyS>>ZOOOw<=16)O5fyj2!KgKi*~A&*-Y z0ZaIZ66A6Kx)m#VGmcP3Jeda?0&V|9@FmQG&c|Qvz)d+ z?h8Mk(VKpp&C$(zoE!vM2T_^|O@K7?m7Cmh@4BozV^|{z)8bNEzf#E9%@erF<&Jeb z0+#3n>@f@y$I<{?$UW5o_pypQi`*%>8NQ|lmSuTw#gNY1>MhHz(%jr<*v#?9#!4}Y z9vu?F$)t7z>RGV~S>X;Vx$^6lM5^8F@khW-(QoDDOg}M4eCpBM#NQ&b`tp4J`}h=0 zFQmg9u)m;<_vPKxRPkFUK|TlCA1hNN1`nF?P``OC1+8LU^0>7JYBD)VKYp;Itk??| zG}naGT_X!Z6mgN@*bnR^ug~44%;z)R-E`1gS__VRSx@+Iq)Q9XvHFy#^>3mcKMdx& z$BMPX_prJK6LFf6UP#jQNl833fR{})Yrdz0rDa{r6#Q%Q;?vu{UeCOxnsGkPIW2!8 z;wdVff!d9wW8qL52Rq!#HMAV*$B%4|=B_fq4R&UvIB7wy#COWw^@l?Rl*oeMBW!^h z5TC`Vt7ejy!zT<{>00BkZ-wgT(KT)(a;t@v#dIIc57>FKx z4arL7p-1m@4E`Ps(<$SntXeCPa^J22y%~!dtwAkN8;IS`Lh4y<*3A&SI4LwagJ-W)HWjo%l zU)r%#XjYU}J}yMc>jUrsG=vJ;nq^?<4bOqR8_fT`Goo$odM+}S($q~NP?k-Je*KdE z)#3w+3(puO9>nKonj7u%ZGe_fV1ec4(%XbP;c&l#b$1Ksf~7&EsZ1NSG8K9H$1pa( zT<`ONfdNje{!9PQ2Y+YD#zHQy#zI@Z6klJum^Xmatxl3ng~S@SZm9GbwQJ3k{*XLf zyi2I$H{j0cl`a2MON3Z(*%pB;5vTI%hA()2`T5VoN;3|$_pJfWexP0+!WOE1A{M$d zQKGB)xeZL20mJHAMs=08J}=pTmN5LxcFE+yzoJmDvuWd=r>i5|{7}Q0mZXK-+tD0c ziu3%JKv=nCRS4(2tc*a5*cIUmP;a;gKzD!&>LrD{US?R7(#2Z{BCD&X36QKr~l_RS%#4Owp=dN8H`=2bb0 zmlL+^Z#PCcpKsgn5C16MVctmNKyOqnU)+5&y#L?8oG7bzzZgNT550M8F+{XkA`p7x zSPy!Gi;Z`&>g&w&@kWn=cQ1z9nH7-wKzrIE4Lch`dzptwNusMfzdL-Y1Ng;$KK5ZU z=X7B>Vh^C48$(0{!SnxxvK@gKlLT%(+q#^{F6t@KOWrTWW0nTcn%7fvCQvXhWn@7- z4+?fs^npc4U%ym#I`>@XerCHbCWi;(zoDhbe*B*;P-nw8rkngn)4}FOC`VEcB7cEs z1_^)iFQy*7yvtdwbw=`9z_F15BRA!UlW1cGwFcLKdPT=Mb1N3dYt2Zdf8^eQY!x&M4>t}(Kq`6W3A0gN}3=U$NPWpXgt`r`gGbI zHX_$NeRRlXG*#p&O&I*jO;ja`>fpjqyBX5O_2>OaSctp8ADtn>=3OPdx+ zncQiGU0e+-3Ms1oc{X6dI|)^wUZnpUza5+7xVpJaKH@bl&vT$N(DtBWd^%x=KktYk z<26Zev?9lT0=MS@<+r;uqRx_Dk%LUh2~ z2i$)lhcXefO~QO~icnEqQ&+BLH&b(b(T>_doakGHIq>MVsZEavmB@qKACL7gzw$`t zgdvUqWh|A*ZV5F&__F6KZ8NjXfdpey#>5+B_P>yP@t12uM6~|=BI_Owr9A>y(zU~M zC<=+;C!0WgfPp1}gp|xCLEW$JF5n3K)9g8N>7rd)l@>Q=eiPIbOB7i63h|H*;)sV2 zYw5>(ch^fL5|5co1?LSa7)fRp%EA)VMVgWQu%FlTUXyj!t0$@iqv74Mdq7qvRCo*e zfO1S+8b#O+s4E!IA07xQ5;iG5er4G6JWivR^O*=<5(W>?!!V%i<;)wdIoi&p8<6%O zD(Hg_rVFJ1Z<3<%y(_Quyd?zt4TRukz@j!e-Ac9q6jS#NtAt!mv+|tRbXs9$#zOeI zi4zd4wUFea^kQz2>gQQ~pe&6v$BT5d-Om6o84?Bh)@ffikSw#5yEc#!X)ku`B0>Wk z*{1t26F13O%R17@bUq9v>8bb+7?hnWlpMz5eQ#dmmwRvaVrcqh#d+0>Bawc!8qmxT z^bI5;@LknFkx#CpK1)p2Qu>OoP%pxe~``TT=g)Iw@WW}O3@ZmJ0ZH7-|T)=>L$Bb zhBO5VlTrq?r?o2=9T#l;nE3U(bL5w<-=SA~nqen)K49?$w<|C7q+X(2DHHyCgy;u|~9 z%^eP_^?d4*HexvmLI}}KXTwIP185ud=uJu<2;w=h2}Qrd9QgXaj4KT`SL53jh%(a0 zP^kZ~uWY#b9#kI>RN#;1Iun(WP&`c?Lr?2j(S*RU3YA9P34ya@MiI;A82laZpF1H#=u($fn~FatId z1WuKJWEZj-DX~7v@TWpzHLXXTX;cTdJ25^iLX6@^yNF747fP8T@xQnL5I=n>%El3( zJSEKBduX85eH774 z;f@Q5YVR!Nm|&dXJby=`UNicWfZt0e zwX$I{BY3nAr&iN@0tBqG@}@&YEYSk~^lddvidL`9O#)eAGp3@P4aLY-Jd!21UdMNj z2Y&qW`BAnfAAA$6NSBui^3~(bZ6HMSDG#`kn&+T%p@KFDUbW%JZl7N@!|c!v@#h4a_BAACmy69YrP&V+Y!g`YiO>earjquhOAM)_ca$mNRMHHf)An+SCdqr4OnUF@EL7Wk<4!DW zi6J!M#>72I#2ebJ8Rm7eVc2y(g@a(=C7pJMD295bZ7%65{xMPvw?iy!s6aAU4o+Q4 zR;D96KOO!G|uCHNgsE}S=w9`B7xS{vDg+Xzs4 zWb741%v7Ny%%xM^Fdl-3B*R>l1mvRnSvEep@E{K_W>;J`4o?Dp1ytIVsaT=K2&`Or&nvBqH)rp%S?m=Gy3xLcU_OJwW_%nmeml-KQ zOWib76v%02>q~{|#+ffKug_k%I0Dg5V4~PdR0ZNYwj-J9Z#k+gYH|7 z`3OoXLQI3-z_Q9x8ahaR958y{4nGBYS6_DB5v@-f(q|TE5d%7(P5kVQ{@6OaC7ZlS zM}ZekX&s`$Z92q@P(>&LWuWuaKUv<9Y#%IJoDw6XBP3)IA~pLbT3yCe9w}tmQnXSb zlr4ooIM`Da>Oq-VXncz4I$CC1Z|hKbH9J|dIeqqV9gtlFFz73$kfE7rxL|E8+&`+Y zdnKKm=Fz%@!$DvlKTxL!WDSquDsROYV23qsAdO!8-l1Kh#we2Kt zKTtO-I^V?nrYdQ5>_4}Cl9Ex)wn4#=-iGGg?74$OG5V@k3C zQK$QUd)@E`o{wPbZAlJj_9E2J*a}rcf|X^eNh^0~Fn(ZDUS$?TUWK zDUbunP(c{sqWR2Cx;tPK)Jy-Wa=mAR+O6~F7~aiJ3t&yCrGhSF|?;&FS}%(<$f zda@e|2On<^=xmK~2Yfj_XYTe0)l@tK51u&nW=y1=BE!4WegdT{2>CNHC;a3+4K(2y z@(W**p$Ej4Fa>*jV3t??_cX~@Mi}7K2)}UTqljL=t(6up>7rKp20RRv#|YGL=r)T7 zdqve@<^;|Gkq*R9uvGDSevIOM&(cXlOJla=eZngEcZ=Z2wC?buPj6HJU8H;jKfdk4 ze?VNrH7cJ#C8CG%c9aFYUE#zPWo(9oIPk}t-n{_y`RCl=*3Sgdpe{akbBKtnanJxg z+B#gLUS#9`jFs^Gp5`TTCG)4cirCH1^ z?_h~orf#14TxKT1)rH*-49cD1_rg3_(u)6%7jq;h$frIxUeDyPN6epCsMJoFJxHWS zu*kQ~NP3+(ZI8nSpJ-?ZBp5+)s`XR5S43I0ZJZYY{?FMNKuPw`bVPkZ4e$g~(bGP7 zn{6|7SAy{?UqW-BXWm-ZCLnhXy#11U7DXhh3Ba9S%+6#37RvU6PZa6{_*FLXU4%^0Z6urK z4x}@9;riFy;NWKegAe&`^(=J&I@vS@$Owgu1#W^8&abZ&Q&I(5W5w3KJ(NP=dEk!j z8V62Z?dIQ8T-7;-3p5zm=rG54cdK(QT3Eio0w~N4W#T`8JG60cmAT%lKHuqT_8YPO zaw%K3KZrBn4YD8q_lqa>PsPfq)}%sQU%Hc!#+Rum;H`{v^)Q&_8JT$-cVPmi zfv1)5Rtl7ZTlrw-OUAK74(r*Zf4AYyk2T+eer~^LJeMz*ywfX~(0%zBm_TUhn4$+H zLQcy=s}vC+oN~Sbs`nvtdrZ$Js)l7ctIMqE;%476h-H}f@|>QX$f5X*f|AmeJ4ldl()f<4{A%8I`}1%6 zgiLWEJmlWfrZa>cXIfUYu~ock@G#S5AN(#J3=x4JL7IH%*;S0e@R48bctM1lyy|()-CmWVCD()!GD^Ci;;G{ zmNr$$@`YV$k7t0T)1)U#xBn=Y_uoJHql~$!;==}sMxN;VM7Zpy-_`2VarsgZUR0+) z-Ef!?R%>8)tp~YjJCjcj8pad0EhYiMJtB5L9vVfA;-0Nx_!-Q9{ucrKpCI};LmH z7(xZOep5&Q{qqFvois?N0FUf$g`T0^Cp*$8bf0Z6oe86 zFC&L~5$qzoSXCI{4-h~Ux>@VOHiZhu)|Yv&@8^8+7cV-8YGDWWN+8ufb2cr6dYoln zbiT-zUL}fh@;^3Htm4b+No3Uila)s{BcN>_4jLWmb9mOM}0N! zMBX)C(}HS0_6e!|vR+w)LbgHwH}OIEC@&)VKwzktlW3~+nOnRb@Cl5q>U*A?@6qoc zD1-^x+}U099iKfAQ4=;u0Sv+Y_)B#)ouKK8hw)-m9|_B2tMPXz>L&Vi@n0Cd63F0D z4Egkik34wyu>+$TJHu$ve4#qou#zgta*HvvZXRC&EVPkLidzOKb-p?_24OqPzvgRD zWqb`0_BhNIx!G5fiR+sFmwy%-zBJqm2f4qMYq%;51#gQlp2To%+;ZKmFJCUHh2DLa z8G*H!)mi;x|Ffz1gq!0ODBhx3u(wMX-@}{e0h7-L_;UAAN`!;%qMuI0CaqVFXn}QyV`pif`rwZCT=DB>eFy}* z7^L(D=3fcAi1Q%zKU!2Fxk`RWc7x4V^_Eisp(46efj+i0~ zyd)o-Uk{D?>sau3)~iYjSduXelG$t)ZQ0$rxQ}W+(&RvaMqL2Wz4dl#$T(JJdhH%20R7xNtmm!!lhF& zk6!i1Q+aR{*F6lEj6WKpW>!lD`RMjg9mp3W132u;t2p$;kmI{t1doV*-~Rl<`eD*7 znze52XK4?j$X(J$l%e~RQVDQmy0OROaESTYr2|m!wg>cF8qyb6=Iy?p)$Mc5U#oE5 z%P)+n4>@ey@@J#N%ZT>q`cOG`Qbu7uG?w=M2bqIh+=HDrlsXT6Y;!HRk+eKv_7vZM zH_81SIXu+ApQ%IacRt;9d7n=v5b4tEaY&s4JQRwAoPZu8(R~>&6F!O2(u8%E(E$`9 zlq3HuKI1n<;|Sl^wEZuWr46qPs3}bya;F42xM_Vtbdt;lma=#dRcc=IbRTj7q-@*A zk^v?qr4bpkZ~g@^C8(E`l^Dqsl`NjUr(w5XihIyk1lrSUVG&tF?qgsQMumdWYG2u+ zaJ8r2O^EaHrY1aS_qXjJJWAv*t#rz_(y*Am*nTD&sKOpyF#S<;`6CHCi_2CAuv*`} zwZc7PI{MLLXhHa`&bt2_JVD#eS>CbzlV1u)2G=QhM+O6@@k2SNf#Pb@n6qT_06~k^ z-wVf*!xNH!@T(v05yF1YIfuS=HV~sRY38}PWBJQ$D>z;ClsQNqx)Wl?N;Qno?QbJo zMztkwNU?e5tke@?KNJ!!3|27G-F`Gl2h`e-2gzC`T8~R)mfCB%JqrYJJC!3{A)7`^ z*ht!rZ=sn*2U*l2zs^5A%wh|ser2xk*5?+L=d&Oay5g@)TIQ}iGn^R`B&B%N+YqvY zx@+kGqSclUBYI6L2% z1%0so@FJeE=amqoR{rvAq&?H^(d9L$Td^tR@WlT=+Zr_v#t#V?-;NBCpVLISps$9U zjt8$lEzWNBSr$?|OLsqW<*tHch_r~7rg7ky4!YG+;S2GBBEw92oXBzTl#@XBd12_D zqE_w%FKc78G0kK8F*X7rfbwp5cFUl@KSnxi@><(0!%_`!=^-S(yb)*^0q2#bz;d$4h`$&TOn6&bUU#&?pU zjWF$jEFBs;A+-6oT0=v$?qJKr`%cF~^1JNP z@uW9I0mxIJ?0>xNez07n?<+GIi^{DabX6i^X^;ePB% z4X&>6Oe>2|Hzt%PmUjDGI*O%D|i76~;F-OcJ98;=Z&( zO#8IEl`#LM{K@;9UY7XcXE?mAHO7w2-_(%og0ws3fK*T=>}vM>M`V-d$}J-l-K3QG zjR9>GTo#m6`VaQgQO`>S)1(5+7#++Iw_9CQDlMg$Np3d*w!0E=pFUm)B7 zRwHN!m26tDA6pV}ZBU95Njtq5$(ZAZe*8Qi7AGtGod_9`$m_Czx_@n=6%;KWrLq$f z0h(Q^Sd`m|H@e%~UW@55eOn<{eCp=B9?W0&9q8+RXqC5~4koI!MwM^oah*(^SXFEQ zk4)`drFGA1b2`Nd#!?Te76=J|R>aFHmW1;$=nKf!Z*Mp1nAQ61_u!L|MEfzX_!j~m z@6zD{L8KhaAD|x0E`NRU!F&O1qVeMcGad8BS<|!F*&3+c+)Q@iWzKsL3nlcm3X_}v z08SRBq1w|ZAV&9|1-G4g`(0=G8EBlg3`yVf$`EZ7GSxCf7l`(z9~xQImL;%*^?ab> z!K&LaH4?xX@kpsWoD#hhv4ICtcJxf5)3}eqDepxMEv0Fd!!J~Jm4Ce*Pe=lFT{ZJa zbpD|ZK{B4l%y6`UdzV)dA|yr!`;aikJQDe@(RYeQ2X_?~c^m-Fe7>0~EUpXc9; zSI!X1z_*$_dN<7F!UQ%=&gxIJ|P-9UNO+ zdk_zVwP@5l2DuoUw>824m;&!LHCu&nit>!I=e$Ag>}}tW3u`0h)`@FE9;~K!UN@W- zfAeUU*fmc^6Xff`aTl+1h^CGHnN1hwHi%V>y+BlxXnudKdEf+>iF-`rWN9cP6VD{j zfMCP%vY37~$PLj}U`kU9Xz;X+y0_NS z#Ytv&DaA?H{pjtzrnohxY2mU-#>qm4XE+7GnmSy7(cnZ9Dk9t?-n>VrTVpN}93Hm# z;qx|hFMJrm{MY~z=U&S;XL>#Jb_t>@a$NZ8Higm^UB13`m-Fi1V{Khv3L;S+qu%Cm9RiOj|WjE zpOO0a$)2QKR35&m)E*Q@Sy+8VAMgt4@h{#=_14W0E;g?0dT)De=UaSzNl$IaGS}?>Gvb#q1luygR>W+rZ^y*c z+P^^!J4UHS?eBE>Hx%MT)UhJ#o`u`Pigs0osHZi@Di8I3i`F8I$>Vv#sK1LwWz z#Nh^d!gXa3^`HhMvYqQXGcsKN2}F9}qKHisoguhd0I`Jo<6|2HE8%Xm$KZP zc>L*^$t2&%we0E9p<1siby^%q>9{s{Em8TDAoC#f0;tSB_Rc)1mqPy))8OUuhCJj@ zzI1h+Ee2jB9Bu}Zllf3WYZ1>Oulb-tX5^fox$!Z=p`!&ae>`2*-~OeX|J{k@&;c^Rsx)Grc znb~#!!9U>rk#XO=iuvjux{L3RfZYz+ zvkZxT2w5u9$KGVH6IQ`+Z?pT7aT;9q{=JMUG+2EjL6LZY!EJW=9B2%^%DTSG17SIX za_xLH^0M!C#Yph)LLVBZ%)*($8pN(2G*ccEt;1~b*C!P4JJ$ZAygwAKyDvu$RNlm~ z%KPu_`g%L=nm5H8DQte68?lgm%u~KH3d@UuaD&X)ENqKKUvajOf;CKZ;C-&og#V+Y zGfdZW+yh4Zrhsn>n%fifE=Qp=R?=Xl)+1DPIa)frG4?fhg4pEtjTaf{`quLWdqqU4 z-719}L>u2H2az_VA0@u*llp+h#bgHb6^K|CI!Bi4T${nh6PC+pS4a~t@Qm)0D`Jy) zqbPgcGAGcZsK?YPrrlPdMqE&)Cy^Di^&5DaZ~t0v1d*=CNqRr~X%_qE;BT7ak79kW zXja|-l|m4^pHHD3&aI9r#?*oQj0FWC-wLfqP$%^RXj>P0{Xv0R;$KgtRp&~zy82A_ ztF|`&syyNIooJF3^FSMLNXtZN#qV5<5TJ|v&G0;Z=ZwrQ`cJmN=YYj66}5Z1D{LmBSOvB zal~;76D&ly32O95J03bS#IUD#{6=S_K3ZjuZUBMKLtp zjA&!Bz+LQ$2>;sByOD>Z5}p2c4}n>s!rPZ?>cC2(IY%TfOFlNIFrc)|7k^5?cDgU6 zI+Hj%_X&A^2Rme!Hh);;pP3a&R_C+y=QR>tO?sH6W*Gb#i zQN&%m_pSu45Due~C4c6%Cj<4S*0l-3*A6)>37CX=|9kQR-1w0G$Zwoo#nYq?F1^Pc zw_F2peT5NS5e`LI8;QmL6V3aC_@d)L2@mM|7p2GEl19vSsS_|)Zuo}#^|JH^NjasR zaEq}n#!n8Y(2wFN*95-C?b_F9OJO0o1{UA{gVE(7R`(xl!<7K=?1wC?E^D>ApGv#m zH5iP21Ax8Lom*{o_ksr-OiFY#l4+s1c>gzu7` zk*Rm9e{4Iggx9n5RwFiqWLH;vV=Mfmsppg44)6n}b~(Q7n_wDDgU;y9gQrtv+$xcI zcz1=5@%TME=Z6&ij5$i&*<;k^dq0P}G?I;owY(I3E-N<|IHmJnOp2Kh!N~Hmc!}j7 z2igP69``IT$piSrwi0;{&Y^kG&drg6Eb+!y)MWc7TtGYDJ^ZHa^d+GKz zXw*$9=&WAx`b-fIu>a>)l@^JmLpma09CN=|}LA#=d0Q^ianZZ{g2F6>}0DL>?r+ zT*wJrr(supX0Ig?(FgPY@pa|pm{Q}~<;w5L*I23{JQkHesS*_?aZF4LU?38T-!Ou3bO)-KuaAz)Ul_}< z$!c;gKXj^BNdR%v*tZkW7SF2iy28Vrc`Iq=Va-NNCrH;^Nz6gSOvWjuCQaFeFNkFr z_1FmO69+_pppQJ6vew8cO?z>yuEF4>`Vp1D59&^xu59rMpbSyF*cWbnqc+mg8i4$s z7fw^%;?ID>M+9BJlGyZpA1!rLs&8!hv+wnn@?j%+5SOC9O^`#J?ssNJvUv;5Z)m(JC$YF>+2U6B2p`uB5xdLwwjf9AvEIL&yi zO{Y4Af7(YOazCGEzWbk2&(kn|+kc7}e;;kUyz6Ronbv(thz`o1`v&qq9}Ftyu6hXq zNaGAE)66|`oGBNK_xq2O6}5#n-iKY6f<;y`)eodSyKS1ui=j7GJXj6Z9cz9nQfu=X z{F_R6Ea~1mM@a>HldI znlS^lWW4#SS8`c5r*9d#c$p0DtoNTs4h!5K;d9YU{M2h_-DX&de6xp6H8RRxYUmT`0|y%vJw7;b&!^ zc;g)?^B@`Zf$M_GABZp?9?N#;UCuwGpcCTLE`I$0*zr`~yy5KwZ}fIGL{M@gX(s{~ zT8FtV4Shj8vzFqVSI)PEBBl1g@$Q4`r0tE`KRa`&ZYp~ zt(cc{3s`GC{kzW|y;H{w06=}g+oWqBvbsE%g;^&L;N#riWTuNMKa%PfR;xYWUaVWI zwqN9bTr{ z2b^9NMkR9W_hAh|9-Q6%o){fY#NdrCMM;Nu>0no);YItK|AF4R;QMb}SumX5UTCeB z=s~9Tj^@KAeVe-dFV0ikfdwrvqjcy2{C1M{ty@ZZpS2>MAQWBAsA|~yx3W(FNVh+w zDhKv2ZL+$Yweqr!;I}Y@CQ&K53?=9orMz@MM`lMi~ z{d#Fv+3So8BnTLJzkw^4O#$xx^oBU?MYOgXYh4cb4e=cwA$FfKT={bK5xRC1IIK|` zcTv*;+5Yj%lLnY9s@ z*o;EI3fw(17AT|=_A)N8a5fIQ?j5lK=TilD|KtmwBw)Vr;8MgshbF%9?3pfb;pomK zmzjNSfdP2TfVpp;Q)maF!A6nXkR@U5G&knI^4)JPSij(C!B_{$VDm&Xd@L>5nmJHB z?D#oqiqvED#>_LtY()mq8+eVNTuU*UfEXl_BvKCmzH!?!=8o#;WN=&#KP+Prp(ve< zqRw>mpeBO9>rR9(yGUBq9W5t1F0kiv2n#Opm6{%D9?XdRz(R%a6aEY5uJcyG&yKgf zZ9DSjdxkb0a~&FB_k7!Sfz&rQjnE*X8%7ytQEF(;@Z*d;KG}VEubv<#yAy&BTQ5Gx zH^8m_B0d6LJFhe+HOmiptPtq!p^s;+8x&frJ7CGHM4NE1&N zTR_$XKCJ3fXFmpp8`rPtxNwy}6lsN%&_(tOu7A(zzoqjF#gx1&PhrppS`oGxN7PwP zXfOM!I3l@#x`AjY-a+^#vXYd&^`QVQ>Laz2^~cyYeSBL7fV4JSBdND$fmQ=ev&{;;0D^}tq} zMqojQkYS5aYNP)3h>4-(dh1BhIJH*!iB79FS7|bEc&@vZ6KN4Ol>;*HKOH8qzgu0O zs^tKT0sOQflqlwq^0_7*gxQ-Z`qGNXC3W9tD1#vR`ehO5%`}k@+r07kPl_S}8aIut zAK`zJ>@4gpuA&3Y?`MNKWZFKk#m?~%{T=HeQDno3=Vha~C;vm%#653Je4B5t&!_V+#kub%eZGaceWLZkC4 z5SI1omsXk|2A@9!hfqg4?w7SjPWxbk=`OzHb{} zQ=_{ZLApyiq!muq6x0GL7ML!&AoybmEX_ zXM>%#RB^ebB@>dB%u2ET@_D5r9!{k+$Ct}$|E33!k|+_z9S{J^^FZ__%Kq8<~)_)U?$AVO+z45T`@Y-`x{&h!-NbweMqBM6VGpNT$n*WP2dJ zGO|dM>TSPwH#v6#y;4MmhefOjbbTW~iPUd=yRioQa{Nej9r0RcYiPB$&-1a@=VQC) z$FD{^OW*0!9Jg#cMiJbNJ&;fXU^0iq@qr7ohA(nJa_76xLh}BTtuVHxGuUAX?`8PN z7GWw6JP;>AQP3!~T(O;(hWoF=1zy}*jxKqx!KmyeVs-?^&ftMBkWEz#gf2bOeSc%P z`BT`HR49pwRi9??*%SZapgICmCa8V2EFy`&k<_|h=>7iIA||Zf@5y@Y@?UWw1c9we zj3ayvKCNC^dmCV$pGIIiJzU<|bY-km;abCZ-uTIYtW@l?R*{l~pJnu}bd}0MTud3P z3_ZOUZM|dJ7h-Kyb}YBQP4RH_qmj&pc-0?9U2o;epR0mOKfD3k%G*$W}?dT*9EB{4+M--M0NB zjN0{YN)XPvw{ZcO)D7}7X}!8wCgkFDG%SbosNyUzHje>>#yx^ltil1y{68WL!wDH> z?v8zS>06>YZnpSxK7hCdivBck539co%KQ>qek##X)%g<9^7+f`xCiD_nNq?2o`AOv`u;S}NdzJx+f2-?B zGvZZF{~SNtqJ!3lzX~(1rLQ2CEY%Vcr%2KeUeFOHvri0VwU$)7>|GxmYTuh2OPMoI zf*)a=%6kIfLev}g^2)sL@)MhU=`vMvxrEc9@rVqPT@FE9wt(XnU1s%fx}!V;>-Mxc)chTB-M^Xl9yHGEVzf3j)j-+iFJ0BWm9Nn9zOT=aKh3gQ&cImBF zhQHy*4dNPuAy6gOBppx-Ex%YD>7Mhww)Q&)gm9%3g=KjuNe^EAL4lgF0k)ZZNC;sw zfTxSmAY>a9ymicIYu>V)XAIkYe$Bo>UOT>T5^fo-YG!r2-_Q;?|B`Th$3&alQc&HG zj-NA!USjbo?&@U!59{w0Ylz6nA(T?WOj(ff^6ofrjrZa>OXlC?hR^tuE14xKhZ5eX zZl;~L7T}OPdm8+LxTZSae_$oHHX3T01r0n=aeupR^U(LRyC*22U_Z2(*FFOHz#bd(x0W>j_+UQ}JsavI-ce@n zRCc*&D;`>R%YH~lLx(q)Ckojj0i0`G(C(BAI+LD%FOSjN0%iej`Z-l!KtkTa>fKQ_ zpn8EV-N&}!*WP8I0XC#rD7II%uhK<>kn%Ha<%Tvm4m1WeRYI>s=#VZwv!$&6zb)Q+ zjmg#dH{5(OQo;aDcKWA9snSvmqpkzj5OlM5_57L*+1xG1tiOsPyJ&llTE4eI zRZSLg0w$6<{`?I}e!6OIY-z(onvYA}Z=5BR0usr@n=#PH8PI=*65yLg0$bk+-m)VQ zF7iidH%I{B{rWF1WE#z^fgE_J7B_WYXrY?y0ypoSg>@!}uXlQ}^LvZfb!`0Gq~zQf zt1paln%Xcv-hBK%VZM!1HRSx?=1kqwE~gbE%TK;gBbe5uc@i1RMV@%Ik{^zqmSxdV zea>hK@;eB@5lLn^ihx8_35I=C2wgW{%ThGZo-ErgRxFXHpW+fRAe6HhV(*c#5f!dk zzjNU?AZ=G2EMC@0#OV(&*Gz0QI?fo33q+Tpv__D_bp%mI0hjwohgtpGVqhO>U(Lz+BVRrcyxq#E=o3K zclHU(KfK0^?5Cs;Cj+-vZ~-${*RvOxmHn8Thd{>dtC!jpFHdiy><_? zeh`$FT~seIJr>{mx!odDomFcxTTx@&`p(bH@Q4K>EoJs^DLtH!w9nRuHy zt)kc)ik^ii1_?4kZepY>J|e0KgR3o-_*t5Oo+0n$i?Sm4TL>JYq`+lo09t^5EAM6T zqUhguonXKe{}xc~8;#3Fh?vHM50ZS0gJpoQ62O%z(9+1#%V{E(xwFakEFLhmgLYG+*Of|oS%z!W{tQ8n3kf}XU)1O6^3G*7L`qRdJSNmO#2ql zt5_mr(leyT3I)=M04Pc?n2Y#2QyCK+LpZ{x-abmiz7W-TP8zII3|cznF~Dw}|5`B3 z57&9lbLaT0RG;P}uf=(y_SvV)GJf3G+s}?UR80t2+wSAZhWjr*^0Lq3+udh)Lk`z7 z2&INgpDvEKKNhxehK2EWXBX;Nv9BgJv`S-zKo8fh8r^}mk= zrPe^c_r=g()H@Qaz=pc=YQhq0>h28Ll535}D5ovpIKk?Mz2mT-T_jQGTZ2JJ&|$41MRY>-rMTz96g@kCR?86v@9j~Ibf<l)RdddJi7jf1UlRGsBPf2aw0w&jG@+(Z8| zu7Okz+x#yNJ_|2Jed)}w96a37+*7Q)uhRCj`Lposen39+?n^6d?@q5T|CSzK+^@QJ zjE6FF-J!@wjO@iefqeA-`n~bjCaT%@07hq%SrN;qD|DT$&%X47OW(-c0F?Ol3BM^O zMD|9}9bt-@sUf{q?9YQr^q!5@+bjTFF2cBv-_{2HTR>kbjX}cC=>DEf*mi#B$hf_! zChM{RcF9@}U`T@m3NTd!iS4;VF!X9_`RZ6+?Uu0-mD6KnVH{x&p>?+|MTJi>QGjf? zKdYEUpQ_;G!*=PLY{b53b-b~TTQ*Y8pF@bb%4*1CAl>P}E6r+DAT=sa8Q7J57dw)x za&)zFv}w^y>mKIUAHw0WIxRI*n#C9OA%&axJq10!aabk-xG)fr6?3}r%|!DOG^UHt z!--CUUjhhL72ZT_IZApAoC)?MBVnC@Q_xBHDx`jLr8567N6H$r&8OIW2>l*m4uu04 zpNWGfODfJqAyWBn*qDfgF7PgPe^CR^e@YV^C>e~73D*$!BF^T zJd4h&AggfT2mjL#m|`V*AIq*1B=D9qi+zI4-fFDvafmL8Auz53x<`*Rr^8#mDL(>a zC5AZOjQ~TO?ka3Dnd4ax-Vzb4z&%UD(_$pTIR=VQI3uA0On)`(KRSjI#(xY=1 zm%@Fi{N8^m#}iA4$^tP{Tqr9-*OmKcKv1wEG{iayo7N+6{)DngS1nPNb$hZhHWd zyYu<+ZL@+obTk7Satt+KaxzqL7fp_G$L8rkv;dE;nW3(7j?|If3Dg(U0hXou|N5fd z%DVP8Qso<4K6VKs85F+%gMvv^Lk;(uFfA)e^YFWFgO;~Ru2RmHuf_@w!Y|?o8)qZj z%eimq9v0Wa3p~(K{@?but7o?^fDIHGn!0_u&9cLOkKh_UxFYmgGpJ2H8fmDuR*Qm= zBp-h8j{qWAwP*ljVlR84>`ySPHAIBP;QOyhdEc`{;YZHI?d+g-tATVLeRC7wyj-Q| z3OsYq(e+*5Uz8An6?Q7gHZ}>Nc!jY85}y8;-LWwlB9W`^k{0~6-YV{*1$5469XV|q zZ8HGDv?f1H!oE=l&hwdlnk(+O5ovv{Q2LJ{ZRoIw)v^uAaXt35YXcuMC4;US4OPz< z-V5czh&TMVR=CL#oC*+`snsX*R_$_8$$9KzR0#XE!5Pb#B&{Gp;Soy{)H2DkknzB< zfap3QgN+b)Tn4{fld0ON1JwglN$S<#dAuzN$=^MEMRG52KefZLPxGD(EW@msghVs{ zp5@^Q<4bn zC^>Wi2QkqKuo3A5|$hF}%8F7Y5djX003xxi zcXC5Cu~Vc7GD|t-hi&;0vEx#&?K?~#zpmm=<#qViF?+jrZDD%8pLG&-C*-D*?XrTJ zO|37OUUX6|SI<7Yf0J`P?Fu;)EnX6spy;mIc;4C!n$F*Fpmj&?X+j#eF73WPsO_oK z4Av1W{a4Q@%!OU~v-4;7?~gxLrY;W12kQ3SlwZsrWBP1K`e?2@`vN_^HzUQ!Rw&Cu z5rsORng2dtY)R|bUTsop;jP=KmGcCj>A`bz3%5&FS0SSJS)SHjv>Dr4CL$CnV=$vQ zL-&p=LW=2*H6~xnk+DT4!l#3vK2c*Hhuvr{Z7Ddx1!!ebxY-q7uZdhUUi}?>u69bO zm+ms8Ya&|>(ro1>L4AV!>1z^+UwfbrL5=tsO&J1ES%epB^~Oo%9*Cht>P-^?QV||upM#Ntb^SO&6%!T^L2ERU>fS9WdSsHx z*%1|Fp6kyipk9n)#S=vh2v##(Wa=^MEyS1!e$Lv0Dzv4x{>S= zpN@4dvw{9+6S}51rEf4+h7c0c@wxmv=7=we3>6=Zr8+9F==Z)7+9y`|mWZ+q+l?sR zp+53OkYKW~*EtMLNoZgBl_#bTf+orlzVi9M$?1@mWG4E)db#0ZecquWG$qut?;SHf zyydF=I69#5`a|YPsDA{Kk3!g7>6Vq-N9OkKPQMgE@>D8yrXYdDk`)j|I5YR3Pj@>| z=Kr@E0N1in^9Afv=)TyV-PJj;8pp9}kXX;7Q!&@(QDVWWP>OK%D4C z@31?&Qr0oll^6#%6Q1qTRyEh-6t9?Tje0FvK}$<}6AZS`iBMb+ zG{(vI2SkIGMMa_8!2YPW1p1vJmS~gls(d z>{{iN#WyKP9c6||Nw%*1W7P|jYVE0gJ-Gm9wS z_mS92B@s`|q2?P_xvSx_M!J!FZhs@ zVhceIWO7_JN!1Sc@(eCiNDE|>R2OgV`#7J-mwzkY45Ewtb?`=@5&QdT&6Fk8;b&jwo8o5jArvpWh5|4J=p zxPvTj2j38qxSn!kPb-46Ey?!&%~wsK-Jby6DUPk*818II*b>+-HFNfRlV50lsZYE+ zdHw{GSA6rER4`B(CEgG%T@BTAJpKGV;9BKts_KXd>)9Y9r}mxuaK z=1RL)xEDSI?@X?U$oD9dVLmef8Hb7Svkh-yeutI3QUY6$4(TbyEb9 zY9D6S28#oTZ7E`1FHyXtOmt#`C|M_|@v6;d0n{iziZ;dG#_HvwyEp+OWpWX#_MkZd zhDJCxeA@a*?T8-g<}HPmW+x2Ozf+w@TGezXNE^GFFBI9XI>saVsJ_-d@LuY1SxNrK zY&ZQ_G-jhKS%~=+e|J>5)D!SKD`1wSk$baAcgj~j2JK=Ct0}Zqt*nJY(S(AtWDbG$ z7*Q`=L`L?5X_UYGc>+ce&NT$X=~Hj~TIwCrkQ)4r<1TFC0Wu4h$O?OKpg{Yv9sZLu`_RTwK3bS4AcnlU&tmM zZxdTI&T7`W;~^l#;FS7;LxO+nKOTTR)34Wwt9ySc$2Mn$v@CvIKDmO?^?Xof15yZM zFWIBdR^A&y%=5zoT*I@w*lOL z%q$&J8DR4+C_#+2P$ zuZ%BU9`;iX3Tg=`pRQcm_nfKd>>HzD`W}|$b-hmmG-hTcGP&9-*5bs`XX}@osY8sN zoBe)`=$pf{YL@yBmVHZdRyhzsdu{FlfmA)Rk<~%!eHuR!7PuV5{^qYUA${l{EP8Oo z1-U&OEGgggqJ?dD`uQX@EYA&lN#A=g8SZ~Dyo*^Z zBZyHSA%tmR@Q%Y6(n&9zMxiu{ivooAu7(&>XW3+T=ptkyI{@Tl3|jBjH_?;^1Pw4J zna@=2^}TMZ8X^&at>n+82ZYD9{OupDNoKxhrjBsJp-$O;`W6_(J3EhU%J?1|Ohn0M zsR;@S`}oBh!5Afo*mL}*o%#NM$f^mj>NE|X1L1Ow-KS}~5@+VJyo)_Eq?uNWCT*wI z$c|xnDW2goVutOyo*5LqR%rCFvi)%4I`?qr_ML5lsz zrJ-CjMZA7`TCB6kA$aM8CL{1tYn-b#W+U_ic@RU|6^}=WMMKuaNy2juYu#{*j?=x* z{p0$<1<{8w{YlTUCC@L__=Rn0`r9-3V>O-D#Z^l1#yF41z~_suGzvOhn&BVq=$j0d zvssOXFS5ym2vxpBbWN$;{0rGlUdE0t86?6s;+H7K95w#~aZv=-ISffy-URV9vv8Y5 zQoyk|$y>Ab_ZA?c8jA#Mx-86L)H3Dby<^YeQ?tm_^tN`QdoMfR;2dxLw1MFYu{1K1 zR$roGV0a_nzx8vHej8EFbP~Xz1NU1_cW*H5dU0Dy52*%}+(<;37)~^72xGRZ_|fHW z8k{;2wFc1^`pgh*N?rYm08g%0t=p(+W$C5lv6h}l;nZlYGyiH?Kjc-fQ~aaNrt|t2 zOJL~dx!<#0J|7i>7usHb)*nCEy+z-isrXUuXZbB3K`BV9pZeLNcLQxp{K-F3Cw(R! zx6itZ5aMNXIQsY|{N$mteHqD!klG;R;Q%G- zMEm8UlJKnKxRUA5`?M0c02dCJN>^nUR=BHy^Gy4{X1(CnA94AH`U<>DwOd}vP~k%9 z;`%`&)ON3eG~#~ZfL{wY^I-j*2+K!vi#9N@m#HVYM`9SC;OLF(;s8LtGj<=g`s&9% zWpVRHuU^O;x2PW?(xHy3$0V+LT-Y2bk6$`_&@Jsh307RpC1(Ky`OnY}+TWk3)VE>` zPhgS39jvL;!f=h310~d9oMGxVdVo&H@&I|@4StM}JSOGRk znA`R3#suIk7^pB#Q$lu3rSL$4Npdqh?DjhF7Ab*n0xfv46pOLK6+Ea3`!9%t4?|0u zo^W9kRiyMu4d2$EyYXOeH)7OG>5Y-i{aun~)*Wk`5iGR+HIo5T#EYvDxj*R<=oPh4 z2O$1KzMv~HGIeiI2bb9e@=H*7uxD{MM(bC4JaAXGQesoxkC}#n%ZgM+;e%j8)CEe2 z?4q#`Qvn0#GULsNOSM7C%KAu|;~Tf|U@&%Z0_x-H_T@mHE1nj|Liz%c2l+SXJV z?pM_`1|SS8V}Ouhd?Bi_kg${uf;JPK>AkeDlJ>k!5gV_riDA=24r zX#d+N-gvh(qn4BqAq#-muTNgZLUvO`2h*d|YowUoJ6c+SsYSm|dg?^$k#yI9sQG}M zZyCy355y?mc-5k9Lk^#9QLx=lW$PeJM8$2Xn0c)U%yrV)UyMy2yg~4`xd(HS>R`*k z!ijotFX6J>(PiSx7JDs2K5(Y_c?Qss8u5v!YggZYvNFNz)E9Hiy9h!%_C|iR0pb(s z50T``tvrr|nU#XaUXehG>HXH!8mPYg~ zf(Y0nh~FvCC@Mv^^0W(7u{6_4l-#K(048j`NhCaZNq}CPQk66ufXgqC`qp$Q`kPG1 zb+b+cJy^`Wbkl&;-)FzTF1vKb55~}0${cx=56E*BDrUwrN`YTC0R1T9K)4{A?`8K4 zBPb?Jraqvp0Y;{5rHtz9TvP}bj8k$?_SXqwO58ODg~nQ82{2$eS5Xb|Pa&4-v8GwE zjZF>rm?pjppDI<{7)i1c4L|e-&1FmH~UI{Q4!kAa)q) z$*&_n+)DEZhfriGb?Af@hGw1{l?!vg2ZTU_LKhjUy;JXMY4~_6I?^E`==;=uQy|eQ z$>c=}YiS$hwokNA>4<+9n|}On0U`}TuhwnRqd_7VrD}+e3`* zL$KDJAT>+%_Codie+P-Q$1&oc!YK^xWeVRDhy8u^JFFay%6b;PL@qg3F~gY!wKh{w zJ?AYG-yToACIxV|x?ot}Ls1s*rC=uI{(z<*z}w&GkN{?8{F%L0Urqsl400qy_0(XG znhAeEf~wDc_FsDI{3&Wp0&(r0+`I6#)ELXpX1gvdc3LBbGqM810Mj%r>Jr(KRDefN z8pCN-nu-y;#?Zy7qj~AN?(a<+>FUovdQts@MZ`}hm)g;cz*)P~XWQeyd+x5Ys{6TM z(57%EUqN~>g=~ZTQ)~v6C=e_~1~`ISLblc?nk<@2Sp(=~Seo!vlFdHG(ZF`51aV-4 z5h_W;7ltGfjO`7!`uR%^ftJ13$$wSP3?q`(sgOM8*){I}PIi8#f!`hZyI#40B&ws- zV5+>Y{%pWC|FXU)?$t33Ls{F@D`fal*@b`dW@&<^}(ahJS)VXLnd#Pt0@g$(B(hsWw`pZ5T zxG_Y+*G5(UH+3e%E)+z~c)t(c4M)dg?hIW2k^!qKey0V8LnyA!FH%_3^idnk$ae(* zTB52YD(+qZaDyBZvKxv~-3H6u#|%fG+(BTrdM~VjP4tmZN`;VmiJO1m?WLu*dEE3( z#GW0c>~MwgNCXB)!_^B&YKA873lB#Q+=ca-)vL?nSdd>?AP9eT3tHkCVn78MnS_Eo zTEqe@xjkM0%p$Yy6a2%AI5vJmb9~qYMUVU)c_A*O3!wO#-(+BTv<(yS$Py7F2rIk zpam-JJskw)YFq7zy3HfnzdvT4MG&9Hxz!#@pL3liZ5b{-{we=`xX5L%+10mylTMhGX%tT0~?7T6n=jM2T+ zd>3_FaBXXPSUvG)>EDgP#hGosE9bFbMA~CAYUQ5e+)Jei zei7gHFR#Z}Z&U|6%#phk7>z?);&BXc}&xIkcU61Uy9B?$1s4szClAj`#O6%hlv?ShE}T^!ife=rA}MjX>&7lYMAkoL|}oB zOdxzC6>6ScAIohEVQXeWj=~j;=<=EIuY5HQIOoL$M1Emym)aKqIHv)gCMbqKmxSyr zECT4AEW-nsg6B`PF;VCD|F_G7?{>4Dg=u8PXD0U;x=CnOO#bev7Rie;8>8^OdMOPS z8|rn^^`$t)RaP00g6$&s9uZGgeI=WRM7FIJ(7>bS-8QEjksV#g``==$t9(Pg?fCJE z!DV0hOVFWAwPP`Nt!TVRklT^jlJRE#U12b^OFA2s?Kd{7j?#|mUIcpGjhrdmNy33X z5-9v4%#-qm>v%u->9YIZYFiRS-MtsFLge;QrGltQ)Q7ldRkDLlnB+Qb#Dpcx{)Kiu zw=9crPt_+yoHK+Xo?OZ8{kX1Os|7$oxO7s1hb6ZAdjP^^Zo{t0Ha4~ScFrVW0%~l7 z5n>5n`KC9FhTUGqGJ?Q%FPt#iD4)x%kJ6ix$%h5}nprSoc~XDQ0T1o~B>{{iIb7@n zO%w8#N6>RUD)IL81=Y<~<{l8eBcaZEF3fZ6(r2Ik-~E}I#6@5iye{gLOB>fllPg(N z!9_EMQkzc!7)^ZH2i$8p`=$^icRke5)_(6HF|swRsMktPg~w2j@9Nd)BT=^K0Q|!9PA94$7k@@6`=^gI&-w2zL@w37 zwrIH5No)KK@4+xl4v`-DwkIeRTwJ3lDuTVurN4Yxv@Ujoj((i-DDUKZeyfvXk=uR z(XoG@;^NsiDUFbGsF9Q?{6r6^Wn|FJi2^xb>@v&f!C^B4s5$5ujlW=X`0dcO#~0EG z78)^P@isnJ;W!#%2}3MtgdrUmre^*IAVH0TbqVQ`e`XrjYvMK|I#*TzU25I8?`NcRBv4HNN;*1I$W7r`NNxAMzzW#4f($Yt@Zu&_}Vrb>r z4-{j=sAa;-(SOUaV2-#=!5*YhFm=mHzEef-8f#~*dN{(+WM5o2&jO!qa))Xcn14Fc zMv0OTE(P77y(fjrH%j59R1al5!~Xo@&|VfjYV|PHt7G^;oZ@aF#o=wn;1e>JS>R>^ zksD6C7uFrir zJez`B7bz23P~QcEh&(;yz(elxexXJtegoFLU{0Ekjho-uw0Hqf9RVb>1+|lz`N|_X z%gQnRGv8~4@Spn4Kvx(q%+8O!Hfe#|xz?Z`FXkFA_U5v_)8JI0UBf7yya(djT&Wgg zcVjI+U5pYf+%{bf?=M;)VT39>dL{F2O?5Z`{_5B@QWXKhE#_rY8%8QLi38oK7bJI0 z`Wj$w#CVA~ZTZ}LR*_Y8LrkIypOgFq=}Re87(8Rx~^2mFyl|6I*aU!&P>QO#T+FHKLA3g!y3B+*N?mJ ztV!U;V=itl6LRU%p__hfdD;=)3D;ql&r>j(5x4E+eS)1AwaUb(HAx)#UuLB59*4vX z_0siDjRY>fY5ApLxR~nkmA?3I zcZzsH9p3IXVoDm>Ajgc?HR!c|)Irr3xKUpRf}CbRYclLB*a%zX}0_37w? z|L*Qk;GxKTlw|TREDK7u!Wi#`EYM4P7P+n$=2j7P#!dWuwDCTR$Un<(P4jNMSa@t9 z8fj8uVrq-}YS{x0z0YJzf2`YunN)D))!Py-%L39z>9iXLCcc+51d^Dt|EWH!5hr^5gfbs^hYXyng5c_%k~%Cq`p6*z$^u#(aY$(5|2fQQm<wX!@C8N=Rqm8ls)ySVPT4YsQ};I&FD`bq_qPK( z6-ihy^lSW-4e@0MI)OG+-i>6}o*he(#32V9n)sYMB+L>8CC}~d1+2}T2!p{h-uJ@p z{XPuzrEzMayUK)hlLw|DF31Al&pR*TF7Hzq$`m^E@jw^=c5`l{s7EBIF$ob(#50V& z57pqzEdb#1d4;{I3hra*XRnCNTvE3=H0|fI(ZC zC49|U!3;MxZW0Ip?wVV8KM8GSr`3!N6zdZ=$<6?xUv!kd&3q_{`ylB~mhXa5;AjO{ z)^ks!03`NrOxzwvu=&f>%`HFVD2w}sVtp7vvBQmI50ge+bLbH3CjPRY@^|%7?I<28 z8?3q~P5i)vYvC&^uCG)zF9|??Is4z%P?Eg2-SN?}JPF$b95V#K2-VuPj4`^RkLb1sJ`Zs5YIvtJ*7f04f4?hpRP`lCn;Q-%0qZR|1RuLDM?k)wQQ^BtH< z9=iT(K#b&3&pM5vtnze&-?(79qJPWsG_jC9iAc9+lCZ4HtTL6S!$|;rD8Z5%f29=7 zYl%EZxZXsIkv)GqMK$}ezt$}{#>HjqR9WCdOT}ooh@F^Sk;?pd$nx$7Kl`*$M-ibp z6TB=)9)B4}>d)Y1@*1RDl;FfYTN`3&Iv6=gVU7*Nqh_7T4k&(9q%b2BcfcERIQU-1 z_xrFeBOp??k((OU#_p}U#KssFH_keOCXJ4{Rr(N!%|%nA!?A~I-XAmU2Y0B8L+?RV z?;Gl<{bEJ2v+DJs>=(O&{m*h#D!>ImGI9Eoq$comsH?1A@d0(+`A&@SwDRz?S9$ zZFRO7V!A|s54xGM_9Bmr_pVL|AWRTI7uMydBhqTG4d@9hj@jB3J!?<{lU{MG9&Eoh zC47F3{c69cPtf(8oGSsG*`%79hMBrOf1ZL)1coP($}rG?0N|@co!(~{GC1I+1TLFd z+HpF)%9qeRC#g>7tS5j;ykTJwaqa`buD&5KgalMyOmf$KW$-kO%<;meK7(_ouLdnx zV9S|TCb-~VUnu&>O^FTOuIa2{Q5W-JDmO;BwmrVAJ=z`hf<&L+^zhr8TDsrs8haeb z*+U1>x6BglrGrc;!awn!28ud_9CpP4K-Q5JoTj@%nf8H+T3t}K4;`gxIw^v#K%9Os zpDOrjLpG67X|fXrV4`Y|pk}ISZ-*K^!ol$N^>a#*kn^pV?ZB)<{NG>y&a*zUH8FO0 zi@_6|KJm8FLo)vL49wxoR7hm%tQQgRLk>Y|ZtzbPIH;VU(o`l}?_DNhp zoPN9j2VBZuDf{OHP#E3C{Wa#;4zi+K6asre%7S1c)%z1!w+V>`UdmDrd}y*iZ~Pit zL#g1p8RGxUl-1$kj~DL5qaBvSKY(MXcSgmey3%Y&-byYi3xaRIo9Ao%Kwn%67|`MA zQ~VC=-tx~4YAuPFK#|ptaS7DK$S-%i3V2z@UU>pCRgZR7*EZkoP7NEV?%5>vq-(K? zq);G18l}Fq+Ci+_4b*GOFiW%or2+5m8d|e_ZIJ-Tvq~d0>=phx@?tjQ91B11%h{bdXqQG@2^9Ypi)B@hT-o@WGI;PM)c2o$Dvs)A zj zh3Q9D8MW`zr}N?|j3u*^2{*)Pjyk+Efx5;)57N>jJ^(WQe#7Fg7lW*(6N$FB(|UDa z3q<&6$R8#IU`kci*eC7P5$!_c_YKnKm?cspR%B7BD>% zr)qDpW|IMmSYHrtc?_A)h)0pJXmJo;ThOeC^uE-8L6uY^V1EsnSkal`;a6H^({(`sHh%=rgI>Kf zd^*Q7RF>64@1%->fbfu;wLDUl;aKNgO*w|(l=RJb(Q0YO%+>0_aZ-QOqAf*ZZbZl3 z?aa`hFV9nU+Qu6C*c}N6oXNTvEJufAAb1sXM1o?>2XtB2V5zigWe&pnI1q)&XT? zN!;Z>5>^)tKc_Q^7iR|4^?%f*1>$vrXcQXG{5#dOu3vUCk^sNw7CrBg+}bJjIGQ1j zr~Pr)U%8O!bHJNOVK9ed&b-e;;;n!EBQ-}}8Bv%Tj~v_P)ZvzDC4B*~F+QunhY^2f zN47nVi5`j!Snq>k-&!mnr+I*Z($e4;H%=|Ga|Li3hvEjG9hz zvVe?qzqhHI7NNEV#$aNMJvCTPP=Yd|w^&4oO_2E`)IC(vlShb5CRRllCPFvsh;av? z9H2EEiD3nsCqO_&jjM_@VpdDjag4Z55ZQV0BX@8gNCwI)fVWph0P^{^UcP4W|K{c- z!G!N*01I&XR~!T5G+#mU>0A@bMP#32@NxIksDTvX@_-nB1@+q)~4`Fpim+s~?Z>%X(XxgaNZE4`Nf~A6x!4O1$ue zFX*qpIqxt~I!eR4iEW=W3OImxP-+@y0)u**y8qPOLPa9VF#adC{vzM2A17EvltxPU zsW3fiINoWZa5p!yq++{?*7RZ&DS!t)vJY}Ne5@J<$|IqrW8wYz(MgiWhczTdH}hGU zMGo1kD+a)P_rtrt3IONjZ@fLFo;E+BMxVg=6ouWY{B_}AYxR0=X!8!%ct3fx^}b%A zmw3Lou$K7`gAKgkKj#0A{3xLgbHIGY2%!|-lpfK49{>w{=dZioJjd9g(5(3Ekyx?F zrMOc_orw*wY3jNt9&pC2UQ~_JpvAR?|CMCT^0dK75}a}N;h}&UG5i?tSQUIo(L`=7 zM#wZV1h+s?7DE)Y|8YI1Z13CsVzYY+PhO3l35eWge;e~V;I*6~G5?f(VrW%xV^zY0 zN3X4heIQA}uXLuDZ*IhPoswl6Yp-1Pd*u$~Ur1Mhly_*Z=yXuWY$vQ>iDQp6+O=}f;xgznt$scs(Yj$64Scqy6{tM ztC0@OK%6U>3X;^EBDTy83O!E`jCe~TGy}l7abwu;w^fU5JuTAJ9ME=uV%6|gBd#*; zq0H8)0=)f4Mzpgi2n@7)&)n3i&d3CrZL#Nu3Mi!juF423+mvYPAH@V{Dg2UYV3fP5 zn6L(9NJRxOYw1_GU|f4ip=W0eLRtXWjdgO7eE8q#3;%Pu4EuUV;m6ueQ4^+o7~{D2 zRlQ$Yqs`>77f&fD4J2MPvu*Ye=cJ^pynBESI^JMy^S;IR2j1>_i<`Vj7pbcKK^JMK zsu$rxY9z52l_AknZD?$7L@0heUjsxJa^>=>v7eUlX@9gRKYn~gR_m2CX@N9}(H-|Ac5jZ7ItsXV~fYl`Rt^@tXisUBdaQi+v{1i{j^uS8X z5n!S}=?n*9UcyAAaaAs%7|qF8^5+xtMjRVA&wwCcEra4aR|AOr~?&_u$|Prq!~ zXI#1!+M;e^0vwL%VIis0k-GM2f9>7~v0WfOUy@ENTG%<|Yua}N*lSHzyW{Zf@bmr3H=RXK zFhO6!d)nstX5Kea5##F>?01b+fV!l~7nRq?mVXc16(3E| zGkV4E`%v9D-oW6Sckd!8gA9%1=%Y^#Gb=_>etyy;cNxW^@)JJg&=uaCPk>xp>ffZlU`ef~ru%oYm5VZHzwc1Sy`|TgJiJtDjt`qTw zaV4EDB6{iBGjzPsw^+i~k#d7gLc7rub{9NcYf;8Z+cP^cEgDh}2}EmLa2<(6SkIJK zSjeAW#z#;Hpd27t~gMGm1T_$^n>{*y4I?^a_ZUAHb@ivAMNqs1y+WWIEjSSkyh zFeS(25*4hLPH8h$H%sCuvP(a~Qkbz#R97T>kU}Dm1WaQdq@a{T?1^bqXnJLIGrz5L z%YXi7zDgAP1C6~StCvf}!s(;%lX8Q2Hnpn}?Y4z&%lY?)>MZ98Y1(=w3g`}K#Rbya z;hwH56?ajxns5d3{&`t}5w}~YElj7oa&h{HXpOY`_wDj`(y!XwbcP9|K*1Gw{%$xu z&8{3w9tL++cvgC_v5771?8u1VxM$|(FT-K03#yZKoz+@Z?NcZjntW5K(#Cbmf6ct@ zE&r0MBY(&cccjV>YAN_Ximo!CskRNT7z{?&NTsAsE{;qI&Gq9qpC= zeu~IN$@NLJFN0IQIA09#M)TR!06IJ=6ju+IbEX*i8{rIH_N?Y%d1j2(3(w*eamF>i zeo_f>y^RZu2h`=TqWo+yWJ!6fP9|M7O{7#``^H|ALs9dcmbfY$M<;0_t4x#_H=v)1 z&Yveb7a&Y#GwBqiouBok*h32CiIi_%7SK?K$-m>>I%No`9m31Ob9Ir~oLMpIgIpk# z1<8A5b!jzW*g1tgZZw93Qa=NM=A$qWqfx0~L8jwlVt&MHeB&^p7)h8-U5dZ1;&YGi zlvfr8hrNpUSCDOZUudhKFE@z}sgngb-(0(O#ZkQHA-8f#xVc`M{4fD{5|N_X#4Dr6 zgkxxBB&K76|G&{8D{}KO;v1lOa2V zx4voZt{nEryI|+Dy^~k z3V=V5ebzOKh@pQ}SqYVTm!a$Nq+4bne?KPzIJ+J9#N}H(1qCg_$RlW^V%nh-F!dtf zYZ)#`FjsZKK@8SUGapH05Fxlnv1J5VQ7V8L4}b?~+M#Q1aJt~)oCRDC*`6xm3+D;D zNHYrmE8&;&!~N!X&~@_e`O*nR365P8W~E!#&H1~h!P`6yf6K=1;y_5kt|r}xH;e}e zUKy;?*@Oo$j!mI)wden8OwPUE(oZU40G&BLEpNQGl6#wj({LBGCZh93J$;g|-mzen zLQ9v>D6_|YjS5sZ(bRejLtKl^(#Y2R-wNd69Y)YjZqJFIkL_`+;dL##urFdKYVu5U z+@#&WPgjP}-jrJ%3{oLC=I;b7FwM752^#ZY&qdIUstQfdwVfUATjl^Q{lJ<*{W&_r zgJ2+q8jZLfR!IedNhfB&_@W(8UZvj*%!e=D;Yb!^%Avh~xOm(~CBWH?);yG!TE-w} zrgfOma}#mPDjH%Eu$z@zizmnOA}Luli+8MD-@CiKA{?&I^UX$LM6!#4{s)K{4Za}q zUEMzN*dzh{C&jSqM)EG{NlCjruDOC<+AmJJFSB4{z34oEj(#*yhuc9V;Wgy5n5$#& zFdUHlkMq&j2{c)omj`tb`UcH-DI{B`@4FC(KbSwF+e z4d$7Tx-+}MOq;@Ra}kTq!U4rEdlJ^>*>M#Q1+aH=+iP@F=c{xZiVy_e=R5^+&)ywg zf(YFJZs~%plsoLuhlS)`jMJ}ANkGn*<=l(h4TmmgA9vyN>-(11mBn}MXaT}S2yw=W zK(WBOMhR;d$KP7d`W!i9)(z-IJP&y;`z&H@_KTz3wLnY+R)ZoA91 z`Mc_zQ2D4m!1Ia>qGzIjF7G>#_&a>+{f8$aP7;4DT84M_6hh?uF2{IKvSfbwbw56b zU+hxo9`CxiU0zTetFCK5bf-wCReQ!!cO&3hh4atK#s0iQ3*P*0>su{Vf7<;!WDbNk z@&7JBprJ?QU4e+r8M-2OrEcKJa=r-n8W`XFmeaSkVB?m;>z&z)B5pcSzul-n%W9JZ?0iku|t;O4K1*T_!z#ueMcm-IQ;6+ zRy?wWoCV4LaSb|r!@J5t0D_DF$}axKi9~x^432JYHXQ()Dp)IMrwU?ws?dUTq96NB6Awx3xN6 zH}{p>8w%DW^PjyDkuJFSn+eTJQPQO>5FX0g+uN{s%_k%7?~Xb1x6rzUc$eJW43R}& zFmLvc)t1rGWCGDIPf;CgOo}2mo4<8#sk_c?%Ze3*z4~>X@Rd7Vo>HUX5yQ6H_(rfD z4${Lg+p8!S`_&aSo_ChzX130bkV9Jb$e-77tXf(uHNA8>Y8rIPJj3K4UK@3pFq%a5 zW=gZD!teZGN^J=0StSaM3Pfa%!+v&Q(4IQ~et+dp85VJSsNq0_I(*pX#*C8mtfV4EZ zl|0u)mD=x&;m6*|CkooDFOcR!wTPI(&D%wUuHKrOVnp-d3SA1i+;W8J@v|lwa|3-y z&sS1K--u4g<5PqB0!gZrVYHcS9OQe*^|(t;S?UAP{xGI>%-=Fme83gOlDr2A?yb+| zWa#rF|2JtkHT=@>U00s)$dnWkYWlXi@%3S|L;%%rUI67Y4w8pE>UL~Q9iRPUA;EKN zNmY-9i}TUUEb!c4otpj&719G2Jqcf9_T=E^UZmS(uBCMIN%g@VQ*L@wVhOq@jOH!6 z{77G^fzT44o+VXOX0d+;MZ3bawQT%p@J9TQtsrRS4I1#6{nZ%bt5zPU1H~^7mvl3Bxcg$4#|%0Grzo zL~;EL%#_DZu(K)FD4a@eY*jHhX3!v`%PIy3>Q#+0 z8OWWX`IE*@#dv>PCT-qe)0NRcG#EF1Veyzh6{xCEoPm`L%C~&e3~0@tOHBo8aB;D# zVkRX4Y$WZ!?7z9#fc(O{GquyQz?Z?mW?f;Bs@N*b*z`rLn)VvfAZfID5Djh@(qKVb( zcRJ%5ibWY0r;6#1hz-sndr4p%Ctb0X$d^R&SJrO>Hd7^t4yn<||AdB*)WGy!Y1`r- z>?MFU^?iS$Cuo24@OxIbN)pI#6NJIcwE&XqKkMNsUXj=R!Fw=(F!gN9k}}d9$?M=- z2}4TasobE;Gjk3ScwPuf+K$f+wm5+v7Ks1%dvb~Rf@4H@_L;mDJh`G-+gLbqCbQwUlY!!^(!3@^uW?OQlv>f3u-O9N2_;aTi zSe1qps^H>orb%s076*!<*p7@nef6&6Bh=-(`dt_+g5U)}=X``vLR!BzqIzX9)>9>l zS8T|e&!u;uX+HzWs-I4MAd0CUQAH$YeqlxK+y3l+=|kJ+6%hTUBiLKR+y81p>QIj=pb|(dmyDx0gP-Mbcb$NAJe?p1tI^0K%snRv00g z{g@93{ZMw_psxCD3mlt6WOs6R$Q9(}&oA_eRTE718B0&mh*C zvsELIHm$_Y$MH6~Lav5X)q=b#U?ly+2YILOj+L2%OvZrSO#f5Y38gzFI00mkO zTepMrOQD-P{N&f89kf`JySe)fQQpiidT4x!A*0!1Kc6XtRH!L-(JDRP%w7}dDA_O- z1^&*%ZbT0K9~f1zzmD0Fx@O=~X{7+aN4lo27E`yLvFc6bh&^pMUA2h476IcY)LcN> z(>F`&yElBxP30=X2P#(EY`jW)r5!0WOmk~=k3x2nlm>#2;-OC+Kn1D5^`gUtdt+gK zBl4?0Jh;1MD=$aCRcgWItekO+Yt`JmUa+N~w8&1kG_tWc7uejheT;a{FM$d_vXAt+lv0^F**HpsS0Qf>W6|*k-&=(Jvk8c=i9Yc zYA@+#=jY8np;hL@Tulo-;JHi~S~LcjA_%QEK#;q`SgIel@sj_FjWL_;Ht-+C zN@Xz(Lg?i;=$h#XceAXBQvO;1nwIg2ZAt;@0J#S_u$Os}NPL@o9Nq2H`62|`32Mzcdv~iXDIdT?GNeiT zYs&rTnLN0n1F0({B(;mJ-bo7Fdre11xQ?+-eaFU{3jS4F)BDaA-nH0J4yz$DpwBJe zB*(Z^GCJ-Wz0Z!O8(96&^7n1`3E8sQhg-nQi6{;}Ox#-TF@r5u;Fy!xnHviA<|CzV4k? zY49}-LJAAyj_rILe%HzB+~CMrElh3^O}S`j{zKjrusf~C9-WvLZBwuL+r=@^JfiS* zkkoQSw_@8Ci5$?S*LP4(G4MI9>ffJ&kNF=A3Oh)5e)t4J)m8kmmvo$3kE7QwcW%3` zg5!YMGmJM)MuFhPC@qS|icG_@t}6MYL`=*3`lIcDO|SKQm)Lkz4$I7F$z?@q@(Vu7 z0NU+mduD94YeV?HQ?a``I?tFoBN12tk{%WRLU5S3+Rz!71qIuv!V!nR-XE})C6@di z=WkwLZmT`L(9w_LOk}wkvbqVinoT`tfCy79yNKIiYXCDx2F6i34jRS2fZ5;m0C)Bn z(sRaLNAIUwO3_Kts!RW6V8n{-l<2q`#=e&)zq@wbnM*T0Af4)y=%+Eb z)_Bbs>!H=(laT0jFp(xc`5Rh94)a0>{i~r~R_)2};>TH%O#yCwIU_)pivg5{paZ<%Ml5(!8 zB6mbKdf%~ySv-6<7VktU7jh=PCj_8cN*6Feyy_p3aJ)(E=Hi}mkD#*9%Pb)I3_sO~ z1ddVBofrTAOFJzGdJDFHZ zrrf5wRq>-ueLBzDT%eS9-Kslrw23&Y+5JJ3%BenE8OWR@;q$ zyUv<5W&8gm-xdksNyTH#Z$eO+9fcGGto?)(g2mSxe zV*_mSXqkIReamF#&dvtv#c#1hBZAX`T&wd9X>n6;V0#7G#$*hwNL{0(AYP7*cXk7I z-!4?=I4Y&h{UtO@6{Nw(f>1*QTqe7K(HV&+A+E08;8N3qH$@~-jNwOuMc#>W7b&lR zU+gqj`zOxoIB$+WkrS4hH%dK1>w#0I7inBdQnZ{;F8}FVK;)XCBl4cYq*Bz*e}>zM z?Bkcek)+f%qy$`s*OuJYKd!xyO}Q1H&yl8(TCzQsdmM!mVvmM|NqX0txh!Ud zWv;I-Ry2L1fNYofHg!F1=KW_6o~1GIp5$)3${vH4c!{&}!^;ltYtjPEOD(_<*Z^j= z3`7Xs3IuM4(o{ac_0u~>`nlj7!mmH(k$kqodG*KTOi(LhYq)5^D;0+3O34DFnNJkJ z#Phq7h47djeK5|*dk6PgEm4lR0DM5yVWRGtrV^iToM}pDT2V@a5cF1<&Il(9XI_Jy)IBh>mGQ8MU=OVxm}BQj(G;U#CfVs$_oFQeffN$fv+r0_IwDDz<= zi$PHs>BrSQ&hH&3N3zDB;-dnR4L=R)ujjbj6P&@j$Wxtv**!H_wE0FhguFJtm!soqF?<-A)<8BY4*;zXc49=C6o%EfCdK_5AkeBB>{gE?UPHc&>G43 z(10*nW-exCK|Zp>;~vL>dR&VNgUa0gL#E0oynjCBKotN(*G+8laWhSWffKU?ft!yn zY-zVm`d)I>v6cKtoU(GAUQq`uz34RAQ`@41&QJi+FpLtUPrgC3w^pY~>c26nKika+ zZ&iaS`Di*%;t)B<)*5TIlu^+gUl@@H{3$BL`t7}o0$BH_h{?~dWK?Lu+4Wv`=}{~PUpSHF_a)B$m@d9*xq!F+ zjFUN#vHtv>s5`xofSeY-p7I}tm6o0D@q|3T9E#$P=ps(1#rC-G!q)HSzcZU-hCIrC z7bIBLXV{juGL_E>2=ku7H9$ z9bo6Hd-0IZOFab|3oD*Wp_JhcF1SDvBCWpscNCo@8B;#X-l|E4ENDFK=q9rB-xgO! zW!w;GD=X?l4)YN>Vh(MaEraLF2ZFErn$)bYG+}Arx?w@&?rH<{d+idLZ+(3}(bO`( z%LA!lFU7{6q}b6%&lWA64Mso2x2a?Li58t>vthWLh!Y%uL*86jIP$o2UAe;v%#5z^ zzDL2ho*B4#Gtr5wlL*Ap5P_SdS|o&03g2WTL)M{#39gN?N`1%tKBug#FN$f(AEfU6 zk9c4M@Haf?gWIyxp+{S9K1cy4Bz;CHxGbc_CiF(<8=uj4T9%&Gy_)*)kgr5Mx?nQo zcFFxQol_4ttLEyw*2(9GtAI;;J)%$rn4N1vtN;kZ8 zJoq>0pJmDY`zi`Gcy#5be~E_pu7JgqK73D1(o zb2LX$!9GN>5U7^m2m$1WxEUroDZIV{coUKQ{`&dWGd9mYeRYEROCIFgUz2Q?W?8oe zKN6*VXGI(YH(6D$kE&*q;o)|tMNMGKAapr}TryDJ4;IISImsPv_x-WuYVBsKQyjv6$ln7Xk2P7)!c19mI@Y>N{ z_Zj64tsW)zlJzGUqLS0zc!ntnxl)_)jwnmUd8$ z0pW}r?mkT#ME4sK#hw9T5~z7lvM!zJ>lUI`Ki>ssx2P7c{+j7^f6_(UOB$xs#L`3n zLdje;92hxfS#k%U%l8xI9&458FcC}g{U^)4hoK~ZL}uJ7%&|9LYX5KPj8}Ky`HH`P z_}`?7TJRA|zaVir_9>@+yF6z{(viJjRA!f~N3Rc);dDuHC@UJiU!pXoOY@ z8~gLy3^vW#6z`CtY+C==5!sy2+PN5AKF{IL1#E|b#rmXOmm%6)(Q^9u9$qbG;P+7A zEu%uA4BL=lhKb68zeo9l>R)X%eR0k-I&iQlowOuM2}~uhHOLeHDwp#s$uW3=@r?;n zV$e3AGkO6h{WZV$q=;*ORgI|pcjm5F$z7W)4pxapkA7fC6>%N<@!vGvZvD<%8Vq;< zonb`JgcqknIY5^kU2vbhrL03^fQXtY0FkloU)FLxP@xtg^HZ~ny|JjbeT?>lcpu*5 zk;}a9{8JE+D(*vWdMR2qzfNkyzUZ*Zo8BV+$FB&O(g3=e_JyplL7~Gpf+|-Gzh~}J zo`?Fg&2kHq=AR@TI~yz?a8sWrJp)uHnlD5Qu2*yUE@`@!rXjC&7?pfefM$MXx+7U7`WrVy)pVBO}NVZ}40i->d-epAkgDA*U_5n%kQZ$@1r9>$Ipp z5zT;r{xKf}RwKgnu81R!|Hi|8caCOYe~(`fW-teAvGUa|QFJl?@PvVuMN(MzZyKtxQWDVo6dT{}P$}o5UTxRP_t}~&wUjbNflRjmd`o5r$>YdcttTi@SI-N8LbFLc4%-c_Q3Dz5iTUTz+4Q+(L#{o#fG zp@;7oY!xohw#dqRW0T(`__`^}oa1e!1NNcs{ZTzV86*OEFMPK8?2KgwE5Kx3U#US> zQ-6To8igZ(#5eo8SN~Lc?(ndmjmHDYSb;m=d>_ryfKTYXpN_QS>4q{@?Te`~OQ85c z%nb>G33J)fBzGJg&n#-VoTlNYT4}|4G1}ycZ309D$pU?Fe~2_=B-JD;LHF_eYZ5X= z3xg>eot1`Rl_kyrI}DcfH}Qb^*%Ox4ueK;f_Vr>d>J_E-)Mg1{jz4~rF$eR;{v-juw{=A<6>{S zI&NdXENeW1BMAOlA#RI?-j9sw>2Q0&>i|M+7b5DYTTFwSsQ?WOWpw2a6v3Q&zfK1<<|Nc>(;y)({~(HoP;%qYo1 z=%^QS#o@IF(RLOst#4u85^cp+!NwTqB?;uOn-s;nR$fbaNQ{w3SmPjRPwvVH^q@`!EwO;2X?3TtYegtYLu2}(YJ1s ziEn!mz2D51G#~#xK7XT4c7IdCdn8fthYw8Y}>%W$9n?3xyG$61W1R`p;e8?RC<4PL``X4_p(5&X52bks9BphshTinKJ?W zAHw-|f&K8)j0f#8c|chyaQ1!myK4THcSNLZ?;!|xGXTUZyC`Cnv+4SGS&uvJ(XyEd z4ON(9?T;b5Zm|Pf5?n!P1&w*R8{S6@15qNUlV-z?AD}`^FDCwe$7fzAKDc*VZ^?uH z*``*n4Mo_A{-PA0abdg@E`+xb=+8FQak6L1X9wDQ#m;=#wX3ynWG=ObfaIoQ?;E~Y zGE+ZIa>BXRys${=_T4D7?*a6z1CZ0U#G8&mdzFcs;LyV2yB-!eYXFP;zxwO zHjbU-O8n>6BS1)=4lYTNY)FPzb@fU$JY3tky3H}l6mVI~YsUq%NmhRaVL?8h;_JL- zqh*4-;V$lSgSDv=;s3q(lYM7P*^($4WOaNr`{7K^%L#hLM{z?EFpzLEv4!j6nZCEd z?DbdtxMbc?=f2N(y%^gfF%-Ii2fjeGZeQPFQPO1yAG}R8wIjUbH6n;|A5ez7$)CPv z+CCIJNF1|LdA%-d6^s*p`u#~-Okl3m3uhVX$v39DTtl1Lv~HhgZzFU{exQ$qL-U4* zfI4}%Xo-$Qp#ftedID6jc$=c`N}yl?h2zHT|EWa+k^~as4=FB*lPAMv1f?)bq>yc6 znyv6Zas*b2)Y8dVqR9zbP!2SI5FR~E`t?+{xW0vb{TA?M)FSZCXovG^_sVQ2;TAYq zO_B4nz=FM6jRGPC_G?(}7dtcCBx~i5x$=Bj%?Z2t^YZzm7j;;* zp;17{5EvhImO%yRe)a5uVRno>odjM9jq|pSol*Re-p@0|(Ro2<<9J<_jCQ--_*`|g zc{D8Vz^075&wXSKb!6d#|I5GLR!q6yX2nA|K;pIHuf+5}q4}=KJuR(msa2W16C@ya zpH6xEoOP+R^zLQ(Adq;kmP4eXCnlvmjE5E+cIl!(`j_GG>fczccu$Bc#S1AG@1{uL zvWWRJr&PNLH7!IVAf|jAY^}dO0axrVAU>h!7+IU%Z^NvLGs{a@;GdH2`pASIp1W(JXwn z7!P@8H=>?`w$-@bYd1>XIw;Y|Trivb~d@nZ^w`KL-;Oya8T**+ukCivR$}cP#A0IsN-20rMX3faIpZ6u@zZA?X zmu2vA31xzu_&f1hF22W1d>Rra?rAc@{3KbAbTPH7K}nfgR1qk$*p&v>ZEq_JAF3|y z|5$<0L<$_9_T8D7vm6jyD^*(D#4h@EcCsc67z~IF%Sh}G)5oep$HiR#?jHTSi(Py& zN)EUYeIkWYo=3}MX}MMkLjI1u9(U_C&w|L(GlK7C6fZoNQ5fUYtXNGIwY~Xv0LRh( zLs`taRJ=}wjQ0Kew>>lVfxp6=A+_)W*NJGqS#DM;d~OE(A4T$ySZW<@s&x|2^9=;1 zMIjg;Zt(5BkqNpxL1NtZ&+Kfa{9^n2Bc6XM^cyA?#A3+qLx7QGCW(?v-eT%28dFvh zI~afm9L53+0FwG(>AeE@k$9=6{dX?ufZ9++SNUR6YK68sSdWs#zFFkXRwhN^(y1a~ zQU#@Q>-yhJNeiB8ByQUU-5ue{v&l4VeO_*K~ZWpvqY zK*4OVI`3blobe9LH@Z@>OcfAU4vn_gq+K>U0-(9%ot{9Oiglf8jFEQMGX4jb;|5$R zq!;%~Lx)(rb3Yd60eK)gtX)rv8AKdN+70o{eyKK*>svddsX$7Fma_^$fqOj15f<^h zTkH%a{75ZI>rNGy`<{D)x3MOwwucHAUv`|IR!7c@RGkV4X89+(G8Gt}z9~a^lShbp zfcihf*`Fa!DaZ;kp-(7nbkub0ZoFet@yS%Lv}KRxn0}q!83#f;EAoKm<+dc5pGUE3 zq+hi>)~B5B<8Hb2NwRpq?b}k9ftYUSf^Zh6)!SMSoOlDhhUOSLF6>Uf?|F?L%3@MGG$9F#a* z6B;?JBhsiuo}!(coT!K4Mw zthLQy${yiCQ`r3f%}$$i~VFF6TfFQ9D8d4;tqe_S#^2|udyv7TXp^6 zckcJ?G>hKyO<5v!F9NKtZc> z1)r;Ht~33O=`Aemo}KLuusT#^I53sllii1UsWYvWzJs{~4PIZZBoi@ru04HSjB%5xCl%BRaH z?x&(>3M1h+Ev+HGA?q^-yr(V=MuD#Nc>G-Q56ATCi{u6r z-!B>$k}8vl#CzS#M$D0f6pT>LNQm!2`HO}9!N#hej!OGRSsSYiPb13mcW5JD%6(phl+rwUoJ zZLF=e=qS2V=V8f*UMpPajGBMU4Oyttf$HMk&d%bLNs_rjgpP_${u{C+U`1L za}{zb4ylVc-I5P?eM~d_^D?j=EWg9>#s5D^sCcskL^I;}8MI*2Tg?sdR~0Z+8n^A4 zaSyynO9<2oZ@OB@0=|JwQLFH|%S;`M{vH@p>sIk6)I?rZElxX1$;4eMhLJ>$pcygO z_JCQq-1R#tcZpKcs=-Pg#sA2Vihjb6|1$j(Ho%F> z(g6rC6wZ5)ESWyV9|%%$Yg?N@E19xHJ}T~U0t~&90^t$R#2aB7KL;G4G;jybr#2I9 zXr5DaEi8eTkhJ?;zk|*L56jwRv6kA_sQ+Sl)9g$)E8(YRWx<}5_>5toeubY04~62- zg(TJbZ(d9-t7?E@?JxMi$RuKomU1PqweG`98pTzmF>Ox8t!G*Qo7LB}nZ?R=)W-C* zkMVSOe#MX)NZ^sd*vu;-;#jJQ#+3ZZ3&sV3+-raaY<8!TKte~eh-;Ls7zFxvNmbB1 zw~v+^YE881q!$Z96JE+8&|{2;@zkuXUJ`@0C(Xb%$Qes54jt!8HK`vN=T81P9OImR znMk20Mc^0Iwk!Dy4>|}lS9s@SYko$#VqI|mNE_0FJpCpnV%PZvl*n$5&3VAvJ^Sb$ z-bb?h6@k8o5zWl3*oj^LT&&`RfU6yj45!xhlrhtlhTQx8mw1$qvo`e5EYoGv*;!o{ zcGteYIA^~(h`(3-+2{VJPqD9pkKN~VrKVQJ*LGc)E;rVi4i02DKA4HJ|Hb*6nDxiL z;s_UfJ@)8P`cFp6MK@^~-7blsMpx=wfE?eq_;=ELTl#}&W^aZKNeoJEF<-}C#GDNG zwi!sU8@o}|{W&LaV=~+n32*fNp7hJJv%6{OD1c(uFu->)Ui|9nDB$16tGn62F-uRy zQzHnjMZ3hARO~ldBq<

mvcRZ^Y;SAI`UhzxHcIGw~@M3Gqq24-ZaTbn#(wSBYN7a9T(tL81gHY`UDzE+e zyGMeAvx@8GVUYlVJH=;t4g5%B%4c?8`dN;3`bLxTQXiIBWRB_7^yK}PVMD5C(lW&x ze@>9U0&liYKMCtULAsRe2hP}0P3PXAr0=pC|JwHx>oET=Q-{!V?`@D~myeq`?=te6 z?Ml@_{{TJH*CfsO>@QQ$C)Jkm)?y^ARhf;3_NBeJUaaAQK0Tl5nb*ns9{*W$TD|s9 zA=^)4JDRa*?J3VG$)578dhm$2@W;B5fQAQXJJKz0+_(k*?;rgy3@liP-}#;2!-^Fv z^L*$8oPZNB@33&;LY#Km=~%gP6(%OeaqG>uV)yR7Si5!&makmuM<=-7VF%VTiOsv% z*|G?JNd3O~p`?z^^P3-O^J}DQZxm{t>|n|i#$!8^ShG4eh~ph2H_c7vIh@t0`EygamapF3XgtGV7=xr-Jh}!pNE=n z$^l2m0@mnr?7E-N<5$+B5|SkOgjttM+oMvoLvBB1iFMoDC_rNSmZiz1BBTT)QF&}V z!RoR30J8>SrcbtiL0OaW%iqor)i1(xPb%%~=HyQUaFtg1F!9tb$cNQgnTOdA?$zwq zz;qGMnl}5Ou=YC$WI^>HoDcbPNo}6(zXhRXglGDiOc3QuYd4{Se#JCn|0oUvBXfIa zg?Q|@@tG0|=sZ!RIzKR3tX|cf&ZD)j*e$J3^_*?kCJ-uT)_8=9otgh&KBH>iF;{*% zkIV&cTRtxnb69_}Ozoih3+>Ude2vag=tVHPftvOg^UtQwPW2f_?F5dV;t5_KQ)*wV zcsu`fx!TCE{eTLo3WOlVyek7wMN8Hjf<`B>-4ffG(XyKAj*J!0DZkX~IV!#bUv2oX zE9CWQK20Nkc_&Hvx80SRoFd#}i?nlnPS@Gdt|%#=XJi?~cYbQSGh+I*#ZVG6-HM=C zb&@XT&Npm%1oEYS0uJk9p4EpXE0Es)%fRX9k(XnD-ae$OuR(g%n}D9)pewf&6V!E> z?T31&`1SjK!_`AuO{nQB>l4Cz#A_SW@wr_Ta=mun(9TaAUULKFizyfxEZm9KYT2Gq zh6wA;0oMyz`jrsA^jw|Ha?X;Bg4`V{L7xgCobnN{mAQh*$~Y5}Ru9Tl&+tWDGo^+TS?rnn0> zKFA;+s0?2ez!$&pZOqIx@zR(46wW&9Y>gH=0Vm)DbQMGdmtA%_-tmri;mRwYk2`O@ z3!nJNKV$QwTXnI-{5}1USVX9L1a(m!)fOWT!#FSMFVy!3Jn5N#GJM19u%iG7CiKv>H*I?|EB#c{JRE~ zmQB@NE|pdBSw|wI!^Nz^bQdE;rjTBL&~)afV>Jw=rJxvqs_APFgk(GW#e@HxeVanb-Bj@w*^W zERQ{(P9bkaqngK9cK#_VbMP9K_7>_)May#5#0rji->g}x$ zq^ci5qd^@*+YjLUx(>{*{m$%wY0lD?ztT77$RAVFH7oE`%g<4U-_>`9@>am)@iY{! zeeIMQ-Iyxr&S|B-$l%kS27Ws5T~E!wRnlC%V-+x4)HMOyny{59=*|syF!WBo=TIqI zDV}!`D)&vUv5hH9O}@(gHhpr(%-cMp^_G=!L@sXU9ECzDh23A` za-MqKO5f?W;jMVS;grZzq@38c-u{drFqx})GStgPyct+R_587%E$A&N9qr1|NJI9B zMD1*1JHx5Mot3BE)@~gy$aYY}JFceJ$`|5|f4#`~eAzDlD#E1{n+VD;(-~k&w6t8EIb)X-Zw&M9pKM6_CB1?uRHfPCr zLmGbk0E&BWttlZuqtG`Zdgtf0?6hDJFtC7Jt9;4yR8ax#P)zVD-bHe=a6v2hiai}A ze>MkRL>eJ~s?ZK}0y)KY##a!zX<6soc)Rv~81o0b`M35wwR-D%hDD{b&=LfB|oz;YnqVXMD1Ps;{$%Ew>r-vCMD2ioDtY2SND0`G&@vsmgiM z3NoH;LCpue$x?6kQsP?AV!A-?T|KZrX6`D&m*YTr=eolPjE2Ys0?ju{61C z!oV{2EwxS4DQ`kq8ow0E$4eI-2?=GH&kPPkviw9Q{t*P-*bVY#bth1Ewo9_;&1AYI z|CB)hkIc==Q-#q!xHfMU^Egipuatu#95bmF6X_e&w}Vjw0tGyr?Wj8sEV#V_NwTCnOT< zt|-xP9q93^r;)CuKy`)G>Qi2=&9u(yb$e<=AMd)J*6+`C-I*V?nu?x+JczQ-@d2yL zY0~0p=l%N4fHzb@p`tdZXQ*EXFq%gLEB<7!=YmeVC=SMNOpoz)N4j$q;ALGqb#W|5 zO1e{x*nuIyriUQk{46ljh1W=zu0VR-+kwSP>UXlNDTdwG?jefJ`*j_8F-9+T(l<;Q zb%pzl&ibXkGJ+s=Jj+$^;eK5ngFF(=fj^pF!h%M?Cwon-22r|rw^A*0I8N-n5|*?Z zg}#PWZ5Mtj_^>M#@zzd;kM$WoGoi`UVHDSX7}&o%6qsK~C!c}xP4Dt+0g$4dJ`TQP zI|r2Fk7B3M9vqB7tscKL{Tn|4-@zV}yP0z*c%CV0=K6M zl6KGm57U>S$aezei}{$x`ne6!k~0I*;YS}%GH%JVMOkPRfr z!*$S+fRk3~&@N?iek)Lun~fUu z=C$*Fz*GOGey!K)Gcwt}?NZ4FdInx!FvN;2#rhqQd5e(DUsvVT+_l#Y&5k@roxiHQ zkl??pYR^{d&vkhtKFW)vKE(3RAr=zb(d4pxb$zBFzqEC1?O(1RmNmSHz0p|3oBbp} zM)zXiQ-1?Fe2|&uA6loJg{il^x7yy3r)Vdj4w7~7fP=M5(1$^pfC4u4WnSiQM7s#h zTWgm@J{-?4$>@mU1nd_qWzkeX829S56F7e7cGWjp87UL-VHh>M$`y%bJ8m{x_|cE< z!7sn(_i);2r{fc!_)N#S!U;G5C!qUq+pRa_FaGjRv1!wz_>JFrFW&H`*J5~h$o-J@ z%;?H3QB?4M1~#Tdyx~OW1rq&Gb|;yzr*(e|KkC4-md}gTnB}*8oRp&hV>1h+Uy7o# z5GY_Bq6Ksa%TFsx*OxO$CO_|1AaUG9g+&4x@nNGTKcGhm2>_cHgX5>o4{Y9Jk&U|6 zqW5xklFjvHwUH@nzY<85_6quq{F_|$@hUvF8AQ1%S{5B2odr{bF&OwrJcfhjpdiLy zkr$V&I8?S|{Ks)Fh@3O4ep5=22K8$Nq4}h!jjs~sNznO_&yVZ+Wxc7Tk*Ut6m8ZSM zd2M~Hj7?#Q+YRz!t1F5MadymZp!nENO)Y*Hh^kfR4Mh;h?JM>N-p*gL9a)}cAM17Icd`TB&Pu3sPR~%u;oru_CxMQ1wV-#%1lwM$l+X2-^d8!YN5>}t0TvGp zRv4_RHAs*WaM!JnAASpH#TC1Gg5|4_e&!v(!bJe+ghcBV5sZx=RDNj>W;QBm79grq zDi6E)zLwC!V`Evr2GvQ5miI|UL{A+-Ix-2_y;ILnZZ;vi9s^oU%r=`S7c4~Insu6g z!62~mB&0<{KyM$%puiXF%XI|2EzAL~hmoO06}*Aj1%eeM;)R1_Xl`%kv9U>r0K-Fr z*(`&7rc%$|lEa69BZnY6wgRaMOdSFC?8=99jw}HdFV*@+mSMbi5z?X|3@;wG<_va~ z+pEfLt(|Ik<=58A$`%BoJN}lp(StNJ3`l{|u?a{a zSUfxg96pGi&-`!5=sr>+y}@BL-}p{o#hQvJfuHPK|Lk@F~5hDAcrTB4j)4C*j7lZ2{dORySDrJ^J(!? zU}PB}z+__qT1%H>=%h8OAJ%6k>++(y2wU-Mtk>}7cObyXi2C(T{>=T5Yd5#0L9#kX zKhqt8gtLh)pY_kU_%4SgfkTrhc5YQ&w3?7z+ab*+67_MUU;S?X>a|EqSE5`r0_p8_ z1+?jz9P3&t?U-^Ya2y()L?OWN$RPO(axrIY@C1%;RKDiVug9e_L6Fq17X?N}h5*rD zp*G%J*M^q@a%d98#3*F|m{jL|CKKq_ znZFRh_?Y_Nq2VFT#d?`5`)dN-B4G1K6Gs{Gvv9}9CLj&T{Mss@sP0m!^6HtcHlHTH zl*n zo}_x4;Cy{(c*w^R*Bfj+w6@9elS7|pWkqX++HRnJ)&5ksYIAy4FtkpNbH=|Jg^p_` zp7A?|1YpSZ%XZ~>T$W}J+HQJH-q;4Vcx`-(0OMn0@+L;2eNb}Ou~E-83MNL!AdLb;!y~Fsn;%eLxlH@~T2*0q zEuhs3CP&AV-{BF@BRsRZsLdllyLTYk?JGzyIW`UmFf^icV}5HJEf>(4a;+ZnEmEQD zw`=7qWl=3ZSA~&oydBSm&)Fl!=ys+*IjZ(BG(1uj@F_wkJi#g2q5XP4ca>GTE*DJApnBK!wcIp)Y`NSYHbqJvGGHI0E@ZE=1{A@4eIyn{MPAq zs{d#p_NQ8KY~o&cvSN|5Wn4%~B_JHdH{Q_n(r z-CKa3o;LZ$CJzGu77r~9Mn;HoyV$&6(Mi;s8mrn_t=(Yl5nyHp*myr=(?h`S9l+tq zC%))o(CE?QJ5D_V>8uNZ)$6$HM+VG!L?AyEJ428!wr|8!Av!o!^^sUkf_{CmJSF|{ zw@2=WY<>{fvs2HHezNV}+Y5{=MLO+lm3i4JjfCj5Luxyr2<_nQs+VJvY&YkZvOP_a z!~mm)gA)|P*&=b|*V~PSk#xcG;rk#DZ-6|u1(-Vg#P!|Vi?nhL(gl|xopTY;=;e)C ze@SfPJ8ndA^Y?(+{DweUz8a%1e>2j;LEV(0-V;<DYY ztgjAzE`rf(+YHQ$Y!K2F%_rldkR6*KJ2n9a_Cm%E=mPr__z}+3(~H#C4_UGtC<@4q z&3Quv0Ez;w7rYYXWiQg}O25lVy_3U-huZ&Cj`bhhqQPp%5`U;JpZ{!%U4$qykmPRi)>#$ zIHY7uCqN3_1Vz{k4tC@qW@!N*{1_1Ih+vz4Zf6FGk*|jjp|R-!6p!2o*|}vN^sx-( zx--x^|G6kvoCGu)de7`Ssl0W&FrLS$hY>Cc=1H`*Gi3|@rRqbhUj)4*(k+VM*LhF$ zqS&(&jeBoHWAlTM$x)zr?0o}8fi%1n&GVjv<^|6~8d>%Xi6jo!P#stFOPoSzk3df& z>4M%3EYg}VPQN}ftedM5677}Uft2{bm9ihRhqT;ZfN}cC;h{lhtJ=30uZs~Qg$y62 z6XWBP5I`4~Dzariyl>wD{J|f506)5I1Ag;2--p+{=5-<82{-{K;CMpfYrVh!`~L;s z{`S`a2!823KaV%Q`E@vX{W|vdJj_%O^e?vPrU?EK+e{=B+?)b1G^~CnaZxn;Q}-;P z9)3x@E`a1eBCPp!(@QYSpaXr*G$NYM35LaChY;y%biqZ;bIZ`); z)!3LAn?NGJ4i$z7DMzAK{lCfp1phCphm>?P7^1uh+0c~Z3)iHTQn)lV#qlcv<71OR zAsS(B1hsa(v?5dhZ!7(*E~8k{uK*abcC$!VXV?}=tMe5|$mc(}XEmsPbunzXLe6Ci z#{rmqd1(PsX>G>($cNj5=^>XQl-X#Bjx{y9k>4i+Vt+_@a!mD`7m4f8LCR|5*B_!f6gV1mDmEYK z6M`y%`bv6sKE$-u<2iX{JoMc_`SP8;8q{|JJK-z0{Pm_hO^f(6IR?4?CdkGOz@f?R$=uh6bjn#s zSH1*Two>~y2UmsMPF4B5%5C_pM9n;wMR z@dL=NZBKrosuMH{q&24@UG-9=HKzhL`4)j3*bVvCzi8xWoi`DbSG)wR=U=0{@j<_B z?W6?FOdY(x(s3tPv@6@qBJ+T%A2&xI+gxnXCiLTE4y)4Zb*Z}X#VWGkd~hm&AehyLiI=W zX(p=Y&;*LRZbq@;PROB&_I92xX!IbhUWe9mUx3zW=jkRN^x>e=i0XLsp&?m&8j&DC zrC&$?9l(@Lyyr7ImY@AGC5;>}?z$O`4R>^1ALZ(k(R%I+(K_RNplEdP)5>T4nLzU~ zl7-m>;dRYY`5=4#J`;(>XBmEFdVO|)&T6yjvRSq;>QSMt3|7I15+>^sDV`MWb@`7_^sdi0~|Uu zi$DCs|B3U@yMTpGzzH}3PY0A`iTgI(hu`~8zm5I-_o1h!2fz64x8j|@@{79Q=#jbV z>-_o7#@XopjA(EHDXD)^f0^ASpB4bar{HhxxmCc|ZcTOX8EPu2Dgidtp*`0Q$5=X1Ao;ty~`FT=Mr&q7HEv@t2 zmcdEI_B&b%_?g7@+k{NBy>y4{Rp^v)t`rpLs02IQw@y>Pf3)-{aJ1~9gJqbbKn4AH z<=WAeYHPO*D@P6ipZh3qU~kAVPq1{0oyl0Zu}OocV|28 zZh{D~d=<)XkGa- zq=AK@c{vF-I||KSu_MgUuY-MgQnN3hhJ!wQ6gxJd=en;vE&53P187}%Ia*g&Azhc?4RrvEi{{WURU+$$(zzH}3PY+B@P2unV{%`QfPktN# z&^OSJfAcHvz&qdbR`d-FxGPAgzH#M?V$o+re`uSzTujdRV~ixW?|NImYgAG2OOhij z1C@llFlGcqr2U04P&S@%tJf?@yx|1o-$djW0l0{jaSnC=PdT#RG(R2%C4}QPDhe@v z&Hw8cW5i|WB`GH)Uu1OgDx1lM&-!H~GZ=L$kw2}|vm?(NY^V|rTTvq^+{m>kPRFS% zzeb`mdl`rd?Vt$q8W=&My#6Sc-J=Y8Syf18gNujOjP>8<$n5&@C>3^8IF;Az$rqhN zMU*SG^i89nSP}|nWfL9lI;0flWkDSv&oRs015y4_wn2o}u3VXpk0@&c?nE5t8j8+L?PE|f=DWjrRRm#^{KshXqG@077# zfY)>CtlsVdv~g^o_2u7```B;-VfwV)qJt@B&Tbi>V znVe5w!N$xqWc#LE@q9s9BAszAWC4%YAn4$3JFM;CKD>@Mag#d3y597P?Q~!-*4oZF!FiOiw``*#LR)ZeYbpdMt(QB8#>`1Z(GZx#z;y;IS?wdb-My1OHiJBNkzALm%j>~ z@UnLoifccJ;@;cy`NG|XvIRMO5HfxMn4YTNZ36*No^>HGVEr(umXq};!zK+N;aO&m zPwE5hci_zK)QodKv?dj!8}RCFknJCNA|nU(q4BNHqw(E;fo$D){Pfrf$`W#54~lzk zMe)c6q-86Cp~XOyPXH_&M0xu8D7J6v{5rk}(wbAzeB--Nu3AfVRQ+AztEU;kq%2W9 z`T%;q@KH4G_<^3U`*c97iDKtgH17N%ev~v2g=>zwl8s?zr)3(}z$WJ$L>9I5dIQ+S4G7p6d8aIfQblzf+lVt}yN= zvx{8Ne)T*0$;AFDGt=n3?#t-==CvsH@Af|SlL~2M8D`JF3;_L0vdEllKPe@Mzvh~4 z%JC^bW0zdl|{;L{?fMSv|^9>Zrp^Hr=}yAE%BVuUL*V&pwS2eClKTOycP4MI$rS z59z!u;?)lY_u=D>`d6jPMYeDTD`39L>Q(=ujz#Y)ALbKDR=+92&!7dzd-Hqd<@6kR z$0zky;24mSG9EoFSZ{uQR{B=b{X5?ziO+{6BZ~DmI5e+{V0uz2+5anLB^kJ&4wc^! ziCLOXkQeEx7iFV_B#jt`=@*KJy*XryqI!ouT}e^hB&hl|`eaJcw&-qum|v5u`#45g zZ4!?^;Jwnym~7rYfge<5*ZU>w2zF!h9Mhk*Gir;!DREPwszQje%CA{8pYg_VI?Ru?ZOEg`t8_toP6Z{|%*RVs?)@Rk#eBX_!#An`TQCfC1Gp`)`Leu^li$Gmt>!l? zh^6g&wX9WW2H&|p!%C#tiB2baLEpy;vA&q9y|vY*w>n%$IF{>r`MItwEvPT&k5Iqv z2z5qXUm=AslDN(-I&c%yjv(22A6tPcL~R_)^^<}cUysLHU5rlk%TY4SRo**7Edx8h z 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 z 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