├── .flake8 ├── .github └── workflows │ └── lint.yml ├── .gitignore ├── .pre-commit-config.yaml ├── README.md ├── docs └── KTX section.png ├── poetry.lock ├── pyproject.toml ├── tests ├── __init__.py └── test_polygon.py └── xcoder ├── __init__.py ├── __main__.py ├── bytestream.py ├── config.py ├── console.py ├── exceptions ├── __init__.py └── tool_not_found.py ├── features ├── __init__.py ├── csv │ ├── __init__.py │ ├── compress.py │ ├── decompress.py │ └── update.py ├── cut_sprites.py ├── directories.py ├── files.py ├── initialization.py ├── ktx.py ├── place_sprites.py ├── sc │ ├── __init__.py │ ├── decode.py │ └── encode.py └── update │ ├── __init__.py │ ├── check.py │ └── download.py ├── images.py ├── languages ├── en-EU.json ├── ru-RU.json ├── ua-UA.json └── zh-CN.json ├── localization.py ├── main_menu.py ├── math ├── __init__.py ├── point.py ├── polygon.py └── rect.py ├── matrices ├── __init__.py ├── color_transform.py ├── matrix2x3.py └── matrix_bank.py ├── menu.py ├── objects ├── __init__.py ├── movie_clip │ ├── __init__.py │ ├── movie_clip.py │ └── movie_clip_frame.py ├── plain_object.py ├── renderable │ ├── __init__.py │ ├── display_object.py │ ├── renderable_factory.py │ ├── renderable_movie_clip.py │ └── renderable_shape.py ├── shape │ ├── __init__.py │ ├── region.py │ └── shape.py └── texture.py ├── pixel_utils.py ├── pvr_tex_tool.py ├── swf.py └── xcod.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = E203, W503 4 | exclude = 5 | updates 6 | venv 7 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint and test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{matrix.os}} 8 | strategy: 9 | matrix: 10 | python: ["3.11", "3.12"] 11 | os: ["ubuntu-latest", "windows-latest"] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python ${{matrix.python}} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{matrix.python}} 19 | 20 | - name: Install poetry 21 | run: | 22 | pip install pipx 23 | pipx install poetry 24 | 25 | - name: Install dependencies 26 | run: | 27 | poetry install 28 | 29 | - name: Linting code by ruff 30 | run: | 31 | poetry run ruff check . 32 | 33 | - name: Check types by pyright 34 | run: | 35 | poetry run pyright . 36 | 37 | - name: Run unit-tests 38 | run: poetry run python -m unittest 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE project files 2 | /.idea/ 3 | /venv/ 4 | 5 | # Script folders 6 | /updates/ 7 | /logs/ 8 | /CSV/ 9 | /TEX/ 10 | /SC/ 11 | 12 | # Configuration files 13 | xcoder/config.json 14 | 15 | # Python compiled files 16 | *.pyc 17 | 18 | # PVRTexToolCLI 19 | PVRTexToolCLI* 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - repo: https://github.com/PyCQA/isort 7 | rev: 6.0.1 8 | hooks: 9 | - id: isort 10 | args: ['--profile','black'] 11 | - repo: https://github.com/charliermarsh/ruff-pre-commit 12 | rev: v0.11.6 13 | hooks: 14 | # Run the linter. 15 | - id: ruff 16 | args: [ --fix ] 17 | # Run the formatter. 18 | - id: ruff-format 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XCoder - easy to use modding tool 2 | 3 | A multiplatform modding tool designed for ANY Supercell game. 4 | 5 | ## About 6 | 7 | Effortlessly work with Supercell\`s game files! XCoder offers support for SC and CSV 8 | formats across all Supercell games. 9 | 10 | ### Features 11 | 12 | - SC compile / decompile 13 | - Compression and decompression 14 | 15 | ### Installation and Usage 16 | 17 | **Windows**: 18 | 19 | 1. Download Python 3.10 or newer version from 20 | the [official page](https://www.python.org/downloads/) 21 | 2. Install Python. While Installing, enable such parameters as "Add Python to 22 | PATH", "Install pip", "Install py launcher", "Associate files with Python" and "Add 23 | Python to environment variables" 24 | 25 | **Linux**: 26 | 27 | 1. Open Terminal and install Python using: 28 | ```sh 29 | sudo apt-get update && sudo apt-get install python3 python3-poetry 30 | ``` 31 | 32 | **Common steps**: 33 | 34 | 1. Download XCoder from 35 | the [releases page](https://github.com/xcoder-tool/XCoder/releases) and extract it 36 | 2. Go to the extracted directory and install required modules by executing: 37 | ```sh 38 | poetry install 39 | ``` 40 | 3. Run program with 41 | ```sh 42 | poetry run python -m xcoder 43 | ``` 44 | 45 | ### How to enable KTX section 46 | 47 | ![KTX section demo](docs/KTX%20section.png) 48 | 49 | **Supercell also uses KTX textures in new versions of the games, so it is advisable to 50 | perform this step.** 51 | 52 | To enable the KTX module, you need to get the "PVRTexToolCLI" binary from the official 53 | site: https://developer.imaginationtech.com/pvrtextool/. 54 | 55 | Then it is necessary to put CLI in "bin/" folder in the main script folder or add it to 56 | your PATH environment variable. 57 | 58 | ### Planned Features 59 | 60 | - CSV updating 61 | 62 | ## Credits 63 | 64 | XCoder is based on the original [XCoder](https://github.com/MasterDevX/xcoder), 65 | developed by [MasterDevX](https://github.com/MasterDevX).
66 | Special thanks to [spiky_Spike](https://github.com/spiky-s) for their contributions. 67 | -------------------------------------------------------------------------------- /docs/KTX section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcoder-tool/XCoder/43ea81b1a06de4a19c5ed1b2732164bc3de464c9/docs/KTX section.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "24.10.0" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, 12 | {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, 13 | {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, 14 | {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, 15 | {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, 16 | {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, 17 | {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, 18 | {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, 19 | {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, 20 | {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, 21 | {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, 22 | {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, 23 | {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, 24 | {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, 25 | {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, 26 | {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, 27 | {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, 28 | {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, 29 | {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, 30 | {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, 31 | {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, 32 | {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, 33 | ] 34 | 35 | [package.dependencies] 36 | click = ">=8.0.0" 37 | mypy-extensions = ">=0.4.3" 38 | packaging = ">=22.0" 39 | pathspec = ">=0.9.0" 40 | platformdirs = ">=2" 41 | 42 | [package.extras] 43 | colorama = ["colorama (>=0.4.3)"] 44 | d = ["aiohttp (>=3.10)"] 45 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 46 | uvloop = ["uvloop (>=0.15.2)"] 47 | 48 | [[package]] 49 | name = "cffi" 50 | version = "1.17.1" 51 | description = "Foreign Function Interface for Python calling C code." 52 | optional = false 53 | python-versions = ">=3.8" 54 | groups = ["main"] 55 | markers = "platform_python_implementation == \"PyPy\"" 56 | files = [ 57 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 58 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 59 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 60 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 61 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 62 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 63 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 64 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 65 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 66 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 67 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 68 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 69 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 70 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 71 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 72 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 73 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 74 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 75 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 76 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 77 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 78 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 79 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 80 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 81 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 82 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 83 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 84 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 85 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 86 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 87 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 88 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 89 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 90 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 91 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 92 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 93 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 94 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 95 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 96 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 97 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 98 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 99 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 100 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 101 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 102 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 103 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 104 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 105 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 106 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 107 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 108 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 109 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 110 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 111 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 112 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 113 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 114 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 115 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 116 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 117 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 118 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 119 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 120 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 121 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 122 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 123 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 124 | ] 125 | 126 | [package.dependencies] 127 | pycparser = "*" 128 | 129 | [[package]] 130 | name = "cfgv" 131 | version = "3.4.0" 132 | description = "Validate configuration and produce human readable error messages." 133 | optional = false 134 | python-versions = ">=3.8" 135 | groups = ["dev"] 136 | files = [ 137 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 138 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 139 | ] 140 | 141 | [[package]] 142 | name = "click" 143 | version = "8.1.8" 144 | description = "Composable command line interface toolkit" 145 | optional = false 146 | python-versions = ">=3.7" 147 | groups = ["dev"] 148 | files = [ 149 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 150 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 151 | ] 152 | 153 | [package.dependencies] 154 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 155 | 156 | [[package]] 157 | name = "colorama" 158 | version = "0.4.6" 159 | description = "Cross-platform colored terminal text." 160 | optional = false 161 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 162 | groups = ["main", "dev"] 163 | files = [ 164 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 165 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 166 | ] 167 | 168 | [[package]] 169 | name = "distlib" 170 | version = "0.3.9" 171 | description = "Distribution utilities" 172 | optional = false 173 | python-versions = "*" 174 | groups = ["dev"] 175 | files = [ 176 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 177 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 178 | ] 179 | 180 | [[package]] 181 | name = "filelock" 182 | version = "3.18.0" 183 | description = "A platform independent file lock." 184 | optional = false 185 | python-versions = ">=3.9" 186 | groups = ["dev"] 187 | files = [ 188 | {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, 189 | {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, 190 | ] 191 | 192 | [package.extras] 193 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 194 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] 195 | typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] 196 | 197 | [[package]] 198 | name = "identify" 199 | version = "2.6.9" 200 | description = "File identification library for Python" 201 | optional = false 202 | python-versions = ">=3.9" 203 | groups = ["dev"] 204 | files = [ 205 | {file = "identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150"}, 206 | {file = "identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf"}, 207 | ] 208 | 209 | [package.extras] 210 | license = ["ukkonen"] 211 | 212 | [[package]] 213 | name = "loguru" 214 | version = "0.7.3" 215 | description = "Python logging made (stupidly) simple" 216 | optional = false 217 | python-versions = "<4.0,>=3.5" 218 | groups = ["main"] 219 | files = [ 220 | {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, 221 | {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, 222 | ] 223 | 224 | [package.dependencies] 225 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 226 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 227 | 228 | [package.extras] 229 | dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] 230 | 231 | [[package]] 232 | name = "mypy-extensions" 233 | version = "1.0.0" 234 | description = "Type system extensions for programs checked with the mypy type checker." 235 | optional = false 236 | python-versions = ">=3.5" 237 | groups = ["dev"] 238 | files = [ 239 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 240 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 241 | ] 242 | 243 | [[package]] 244 | name = "nodeenv" 245 | version = "1.9.1" 246 | description = "Node.js virtual environment builder" 247 | optional = false 248 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 249 | groups = ["dev"] 250 | files = [ 251 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 252 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 253 | ] 254 | 255 | [[package]] 256 | name = "packaging" 257 | version = "24.2" 258 | description = "Core utilities for Python packages" 259 | optional = false 260 | python-versions = ">=3.8" 261 | groups = ["dev"] 262 | files = [ 263 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 264 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 265 | ] 266 | 267 | [[package]] 268 | name = "pathspec" 269 | version = "0.12.1" 270 | description = "Utility library for gitignore style pattern matching of file paths." 271 | optional = false 272 | python-versions = ">=3.8" 273 | groups = ["dev"] 274 | files = [ 275 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 276 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 277 | ] 278 | 279 | [[package]] 280 | name = "pillow" 281 | version = "11.2.1" 282 | description = "Python Imaging Library (Fork)" 283 | optional = false 284 | python-versions = ">=3.9" 285 | groups = ["main"] 286 | files = [ 287 | {file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"}, 288 | {file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"}, 289 | {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"}, 290 | {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"}, 291 | {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"}, 292 | {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"}, 293 | {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"}, 294 | {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"}, 295 | {file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"}, 296 | {file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"}, 297 | {file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"}, 298 | {file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"}, 299 | {file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"}, 300 | {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"}, 301 | {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"}, 302 | {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"}, 303 | {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"}, 304 | {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"}, 305 | {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"}, 306 | {file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"}, 307 | {file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"}, 308 | {file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"}, 309 | {file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"}, 310 | {file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"}, 311 | {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"}, 312 | {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"}, 313 | {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"}, 314 | {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"}, 315 | {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"}, 316 | {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"}, 317 | {file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"}, 318 | {file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"}, 319 | {file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"}, 320 | {file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"}, 321 | {file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"}, 322 | {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"}, 323 | {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"}, 324 | {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"}, 325 | {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"}, 326 | {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"}, 327 | {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"}, 328 | {file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"}, 329 | {file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"}, 330 | {file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"}, 331 | {file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"}, 332 | {file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"}, 333 | {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"}, 334 | {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"}, 335 | {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"}, 336 | {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"}, 337 | {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"}, 338 | {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"}, 339 | {file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"}, 340 | {file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"}, 341 | {file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"}, 342 | {file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"}, 343 | {file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"}, 344 | {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"}, 345 | {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"}, 346 | {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"}, 347 | {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"}, 348 | {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"}, 349 | {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"}, 350 | {file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"}, 351 | {file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"}, 352 | {file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"}, 353 | {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"}, 354 | {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"}, 355 | {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"}, 356 | {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"}, 357 | {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"}, 358 | {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"}, 359 | {file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"}, 360 | {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"}, 361 | {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"}, 362 | {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"}, 363 | {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"}, 364 | {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"}, 365 | {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"}, 366 | {file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"}, 367 | {file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"}, 368 | ] 369 | 370 | [package.extras] 371 | docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] 372 | fpx = ["olefile"] 373 | mic = ["olefile"] 374 | test-arrow = ["pyarrow"] 375 | tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] 376 | typing = ["typing-extensions ; python_version < \"3.10\""] 377 | xmp = ["defusedxml"] 378 | 379 | [[package]] 380 | name = "platformdirs" 381 | version = "4.3.7" 382 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 383 | optional = false 384 | python-versions = ">=3.9" 385 | groups = ["dev"] 386 | files = [ 387 | {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, 388 | {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, 389 | ] 390 | 391 | [package.extras] 392 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 393 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] 394 | type = ["mypy (>=1.14.1)"] 395 | 396 | [[package]] 397 | name = "pre-commit" 398 | version = "3.8.0" 399 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 400 | optional = false 401 | python-versions = ">=3.9" 402 | groups = ["dev"] 403 | files = [ 404 | {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, 405 | {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, 406 | ] 407 | 408 | [package.dependencies] 409 | cfgv = ">=2.0.0" 410 | identify = ">=1.0.0" 411 | nodeenv = ">=0.11.1" 412 | pyyaml = ">=5.1" 413 | virtualenv = ">=20.10.0" 414 | 415 | [[package]] 416 | name = "pycparser" 417 | version = "2.22" 418 | description = "C parser in Python" 419 | optional = false 420 | python-versions = ">=3.8" 421 | groups = ["main"] 422 | markers = "platform_python_implementation == \"PyPy\"" 423 | files = [ 424 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 425 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 426 | ] 427 | 428 | [[package]] 429 | name = "pylzham" 430 | version = "0.1.3" 431 | description = "Python 3 Wrapper for LZHAM Codec" 432 | optional = false 433 | python-versions = "*" 434 | groups = ["main"] 435 | files = [ 436 | {file = "pylzham-0.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:fe877259db418c4a1b11e89a8a972628cac364040de00b3b83d859b93264f236"}, 437 | {file = "pylzham-0.1.3.tar.gz", hash = "sha256:af4828d4b0e158a938550d9299e5da82bdc9fbb375fe6e3da429012839fbade2"}, 438 | ] 439 | 440 | [[package]] 441 | name = "pyright" 442 | version = "1.1.399" 443 | description = "Command line wrapper for pyright" 444 | optional = false 445 | python-versions = ">=3.7" 446 | groups = ["dev"] 447 | files = [ 448 | {file = "pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b"}, 449 | {file = "pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b"}, 450 | ] 451 | 452 | [package.dependencies] 453 | nodeenv = ">=1.6.0" 454 | typing-extensions = ">=4.1" 455 | 456 | [package.extras] 457 | all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] 458 | dev = ["twine (>=3.4.1)"] 459 | nodejs = ["nodejs-wheel-binaries"] 460 | 461 | [[package]] 462 | name = "pyyaml" 463 | version = "6.0.2" 464 | description = "YAML parser and emitter for Python" 465 | optional = false 466 | python-versions = ">=3.8" 467 | groups = ["dev"] 468 | files = [ 469 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 470 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 471 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 472 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 473 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 474 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 475 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 476 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 477 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 478 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 479 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 480 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 481 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 482 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 483 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 484 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 485 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 486 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 487 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 488 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 489 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 490 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 491 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 492 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 493 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 494 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 495 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 496 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 497 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 498 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 499 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 500 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 501 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 502 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 503 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 504 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 505 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 506 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 507 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 508 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 509 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 510 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 511 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 512 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 513 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 514 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 515 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 516 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 517 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 518 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 519 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 520 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 521 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 522 | ] 523 | 524 | [[package]] 525 | name = "ruff" 526 | version = "0.11.6" 527 | description = "An extremely fast Python linter and code formatter, written in Rust." 528 | optional = false 529 | python-versions = ">=3.7" 530 | groups = ["dev"] 531 | files = [ 532 | {file = "ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1"}, 533 | {file = "ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de"}, 534 | {file = "ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a"}, 535 | {file = "ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193"}, 536 | {file = "ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e"}, 537 | {file = "ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308"}, 538 | {file = "ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55"}, 539 | {file = "ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc"}, 540 | {file = "ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2"}, 541 | {file = "ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6"}, 542 | {file = "ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2"}, 543 | {file = "ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03"}, 544 | {file = "ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b"}, 545 | {file = "ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9"}, 546 | {file = "ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287"}, 547 | {file = "ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e"}, 548 | {file = "ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79"}, 549 | {file = "ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79"}, 550 | ] 551 | 552 | [[package]] 553 | name = "sc-compression" 554 | version = "0.6.5" 555 | description = "SC Compression" 556 | optional = false 557 | python-versions = ">=3.5" 558 | groups = ["main"] 559 | files = [ 560 | {file = "sc_compression-0.6.5-py3-none-any.whl", hash = "sha256:a10c713e0c2bd2cd5f485c99beaad2e9e8c9e20d0e13eb8c2d8ad24ce64773a4"}, 561 | {file = "sc_compression-0.6.5.tar.gz", hash = "sha256:d45da0d64a8b8b9cbadf3354bcf2211e980096fd750217dfd07cfd4563bb127a"}, 562 | ] 563 | 564 | [package.dependencies] 565 | zstandard = "*" 566 | 567 | [[package]] 568 | name = "typing-extensions" 569 | version = "4.13.2" 570 | description = "Backported and Experimental Type Hints for Python 3.8+" 571 | optional = false 572 | python-versions = ">=3.8" 573 | groups = ["dev"] 574 | files = [ 575 | {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, 576 | {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, 577 | ] 578 | 579 | [[package]] 580 | name = "virtualenv" 581 | version = "20.30.0" 582 | description = "Virtual Python Environment builder" 583 | optional = false 584 | python-versions = ">=3.8" 585 | groups = ["dev"] 586 | files = [ 587 | {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, 588 | {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, 589 | ] 590 | 591 | [package.dependencies] 592 | distlib = ">=0.3.7,<1" 593 | filelock = ">=3.12.2,<4" 594 | platformdirs = ">=3.9.1,<5" 595 | 596 | [package.extras] 597 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 598 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] 599 | 600 | [[package]] 601 | name = "win32-setctime" 602 | version = "1.2.0" 603 | description = "A small Python utility to set file creation time on Windows" 604 | optional = false 605 | python-versions = ">=3.5" 606 | groups = ["main"] 607 | markers = "sys_platform == \"win32\"" 608 | files = [ 609 | {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, 610 | {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, 611 | ] 612 | 613 | [package.extras] 614 | dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] 615 | 616 | [[package]] 617 | name = "zstandard" 618 | version = "0.23.0" 619 | description = "Zstandard bindings for Python" 620 | optional = false 621 | python-versions = ">=3.8" 622 | groups = ["main"] 623 | files = [ 624 | {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, 625 | {file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, 626 | {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc"}, 627 | {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573"}, 628 | {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391"}, 629 | {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e"}, 630 | {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd"}, 631 | {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4"}, 632 | {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea"}, 633 | {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2"}, 634 | {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9"}, 635 | {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a"}, 636 | {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0"}, 637 | {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c"}, 638 | {file = "zstandard-0.23.0-cp310-cp310-win32.whl", hash = "sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813"}, 639 | {file = "zstandard-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4"}, 640 | {file = "zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e"}, 641 | {file = "zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23"}, 642 | {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a"}, 643 | {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db"}, 644 | {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2"}, 645 | {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca"}, 646 | {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c"}, 647 | {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e"}, 648 | {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5"}, 649 | {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48"}, 650 | {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c"}, 651 | {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003"}, 652 | {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78"}, 653 | {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473"}, 654 | {file = "zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160"}, 655 | {file = "zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0"}, 656 | {file = "zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094"}, 657 | {file = "zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8"}, 658 | {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1"}, 659 | {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072"}, 660 | {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20"}, 661 | {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373"}, 662 | {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db"}, 663 | {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772"}, 664 | {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105"}, 665 | {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba"}, 666 | {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd"}, 667 | {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a"}, 668 | {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90"}, 669 | {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35"}, 670 | {file = "zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d"}, 671 | {file = "zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b"}, 672 | {file = "zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9"}, 673 | {file = "zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a"}, 674 | {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2"}, 675 | {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5"}, 676 | {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f"}, 677 | {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed"}, 678 | {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea"}, 679 | {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847"}, 680 | {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171"}, 681 | {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840"}, 682 | {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690"}, 683 | {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b"}, 684 | {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057"}, 685 | {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33"}, 686 | {file = "zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd"}, 687 | {file = "zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b"}, 688 | {file = "zstandard-0.23.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ef3775758346d9ac6214123887d25c7061c92afe1f2b354f9388e9e4d48acfc"}, 689 | {file = "zstandard-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4051e406288b8cdbb993798b9a45c59a4896b6ecee2f875424ec10276a895740"}, 690 | {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2d1a054f8f0a191004675755448d12be47fa9bebbcffa3cdf01db19f2d30a54"}, 691 | {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f83fa6cae3fff8e98691248c9320356971b59678a17f20656a9e59cd32cee6d8"}, 692 | {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32ba3b5ccde2d581b1e6aa952c836a6291e8435d788f656fe5976445865ae045"}, 693 | {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f146f50723defec2975fb7e388ae3a024eb7151542d1599527ec2aa9cacb152"}, 694 | {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bfe8de1da6d104f15a60d4a8a768288f66aa953bbe00d027398b93fb9680b26"}, 695 | {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:29a2bc7c1b09b0af938b7a8343174b987ae021705acabcbae560166567f5a8db"}, 696 | {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61f89436cbfede4bc4e91b4397eaa3e2108ebe96d05e93d6ccc95ab5714be512"}, 697 | {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53ea7cdc96c6eb56e76bb06894bcfb5dfa93b7adcf59d61c6b92674e24e2dd5e"}, 698 | {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:a4ae99c57668ca1e78597d8b06d5af837f377f340f4cce993b551b2d7731778d"}, 699 | {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:379b378ae694ba78cef921581ebd420c938936a153ded602c4fea612b7eaa90d"}, 700 | {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:50a80baba0285386f97ea36239855f6020ce452456605f262b2d33ac35c7770b"}, 701 | {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:61062387ad820c654b6a6b5f0b94484fa19515e0c5116faf29f41a6bc91ded6e"}, 702 | {file = "zstandard-0.23.0-cp38-cp38-win32.whl", hash = "sha256:b8c0bd73aeac689beacd4e7667d48c299f61b959475cdbb91e7d3d88d27c56b9"}, 703 | {file = "zstandard-0.23.0-cp38-cp38-win_amd64.whl", hash = "sha256:a05e6d6218461eb1b4771d973728f0133b2a4613a6779995df557f70794fd60f"}, 704 | {file = "zstandard-0.23.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa014d55c3af933c1315eb4bb06dd0459661cc0b15cd61077afa6489bec63bb"}, 705 | {file = "zstandard-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7f0804bb3799414af278e9ad51be25edf67f78f916e08afdb983e74161b916"}, 706 | {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb2b1ecfef1e67897d336de3a0e3f52478182d6a47eda86cbd42504c5cbd009a"}, 707 | {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:837bb6764be6919963ef41235fd56a6486b132ea64afe5fafb4cb279ac44f259"}, 708 | {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1516c8c37d3a053b01c1c15b182f3b5f5eef19ced9b930b684a73bad121addf4"}, 709 | {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48ef6a43b1846f6025dde6ed9fee0c24e1149c1c25f7fb0a0585572b2f3adc58"}, 710 | {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11e3bf3c924853a2d5835b24f03eeba7fc9b07d8ca499e247e06ff5676461a15"}, 711 | {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2fb4535137de7e244c230e24f9d1ec194f61721c86ebea04e1581d9d06ea1269"}, 712 | {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8c24f21fa2af4bb9f2c492a86fe0c34e6d2c63812a839590edaf177b7398f700"}, 713 | {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8c86881813a78a6f4508ef9daf9d4995b8ac2d147dcb1a450448941398091c9"}, 714 | {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe3b385d996ee0822fd46528d9f0443b880d4d05528fd26a9119a54ec3f91c69"}, 715 | {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:82d17e94d735c99621bf8ebf9995f870a6b3e6d14543b99e201ae046dfe7de70"}, 716 | {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c7c517d74bea1a6afd39aa612fa025e6b8011982a0897768a2f7c8ab4ebb78a2"}, 717 | {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fd7e0f1cfb70eb2f95a19b472ee7ad6d9a0a992ec0ae53286870c104ca939e5"}, 718 | {file = "zstandard-0.23.0-cp39-cp39-win32.whl", hash = "sha256:43da0f0092281bf501f9c5f6f3b4c975a8a0ea82de49ba3f7100e64d422a1274"}, 719 | {file = "zstandard-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:f8346bfa098532bc1fb6c7ef06783e969d87a99dd1d2a5a18a892c1d7a643c58"}, 720 | {file = "zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09"}, 721 | ] 722 | 723 | [package.dependencies] 724 | cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""} 725 | 726 | [package.extras] 727 | cffi = ["cffi (>=1.11)"] 728 | 729 | [metadata] 730 | lock-version = "2.1" 731 | python-versions = "^3.11" 732 | content-hash = "f36130b9e24a7d5d31c83c5d4a4673624d05c5b24b3b2fda45fdb8389e5ceabd" 733 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "xcoder" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Danila "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | sc-compression = "0.6.5" 11 | colorama = "0.4.6" 12 | pylzham = "^0.1.3" 13 | zstandard = "^0.23.0" 14 | pillow = "~11.2.1" 15 | loguru = "0.7.3" 16 | 17 | 18 | [tool.poetry.group.dev.dependencies] 19 | pyright = "^1.1.376" 20 | black = "^24.8.0" 21 | pre-commit = "^3.8.0" 22 | ruff = "^0.11.2" 23 | 24 | [build-system] 25 | requires = ["poetry-core"] 26 | build-backend = "poetry.core.masonry.api" 27 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcoder-tool/XCoder/43ea81b1a06de4a19c5ed1b2732164bc3de464c9/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_polygon.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from xcoder.math import Point, PointOrder, Polygon, get_polygon_point_order 4 | 5 | 6 | def create_polygon_from_tuple(*polygon: tuple[float, float]) -> Polygon: 7 | return [Point(x, y) for x, y in polygon] 8 | 9 | 10 | class PolygonTestCase(TestCase): 11 | def test_clockwise_square(self): 12 | polygon = create_polygon_from_tuple( 13 | (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0) 14 | ) 15 | 16 | self.assertEqual(PointOrder.CLOCKWISE, get_polygon_point_order(polygon)) 17 | 18 | def test_clockwise_real1(self): 19 | polygon = create_polygon_from_tuple( 20 | (4.0, 4.0), (5.0, -2.0), (-1.0, -4.0), (-6.0, 0.0), (-2.0, 5.0), (0.0, 1.0) 21 | ) 22 | 23 | self.assertEqual(PointOrder.CLOCKWISE, get_polygon_point_order(polygon)) 24 | 25 | def test_clockwise_real2(self): 26 | polygon = create_polygon_from_tuple( 27 | (160.0, -73.0), 28 | (89.0, -73.0), 29 | (89.0, -10.0), 30 | (143.0, 10.0), 31 | (156.0, 10.0), 32 | (160.0, -46.0), 33 | ) 34 | 35 | self.assertEqual(PointOrder.CLOCKWISE, get_polygon_point_order(polygon)) 36 | 37 | def test_counter_clockwise_real1(self): 38 | polygon = create_polygon_from_tuple( 39 | (5.0, 0.0), (6.0, 4.0), (4.0, 5.0), (1.0, 5.0), (1.0, 0.0) 40 | ) 41 | 42 | self.assertEqual(PointOrder.COUNTER_CLOCKWISE, get_polygon_point_order(polygon)) 43 | 44 | def test_counter_clockwise_real2(self): 45 | polygon = create_polygon_from_tuple( 46 | (0.0, 0.0), (11.0, 0.0), (0.0, 10.0), (10.0, 10.0) 47 | ) 48 | 49 | self.assertEqual(PointOrder.COUNTER_CLOCKWISE, get_polygon_point_order(polygon)) 50 | 51 | def test_counter_clockwise_real3(self): 52 | polygon = create_polygon_from_tuple( 53 | (20.0, -73.0), 54 | (91.0, -73.0), 55 | (91.0, -10.0), 56 | (37.0, 10.0), 57 | (24.0, 10.0), 58 | (20.0, -46.0), 59 | ) 60 | 61 | self.assertEqual(PointOrder.COUNTER_CLOCKWISE, get_polygon_point_order(polygon)) 62 | -------------------------------------------------------------------------------- /xcoder/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import sys 4 | 5 | from loguru import logger 6 | 7 | is_windows = platform.system() == "Windows" 8 | null_output = f"{'nul' if is_windows else '/dev/null'} 2>&1" 9 | 10 | 11 | def run(command: str, output_path: str = null_output): 12 | return os.system(f"{command} > {output_path}") 13 | 14 | 15 | if is_windows: 16 | with logger.catch(): 17 | try: 18 | import colorama 19 | 20 | colorama.init() 21 | except Exception as e: 22 | logger.exception(e) 23 | 24 | def clear(): 25 | os.system("cls") 26 | 27 | else: 28 | 29 | def clear(): 30 | os.system("clear") 31 | 32 | 33 | logger.remove() 34 | logger.add( 35 | "./logs/info/{time:YYYY-MM-DD}.log", 36 | format="[{time:HH:mm:ss}] [{level}]: {message}", 37 | encoding="utf8", 38 | level="INFO", 39 | ) 40 | logger.add( 41 | "./logs/errors/{time:YYYY-MM-DD}.log", 42 | format="[{time:HH:mm:ss}] [{level}]: {message}", 43 | backtrace=True, 44 | diagnose=True, 45 | encoding="utf8", 46 | level="ERROR", 47 | ) 48 | logger.add(sys.stdout, format="[{level}] {message}", level="INFO") 49 | -------------------------------------------------------------------------------- /xcoder/__main__.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from xcoder.config import config 4 | from xcoder.localization import locale 5 | from xcoder.main_menu import check_auto_update, check_files_updated, menu, refill_menu 6 | 7 | try: 8 | from loguru import logger 9 | except ImportError: 10 | raise RuntimeError("Please, install loguru using pip") 11 | 12 | from xcoder import clear 13 | from xcoder.features.initialization import initialize 14 | 15 | 16 | def main(): 17 | if not config.initialized: 18 | config.change_language(locale.change()) 19 | 20 | if not config.initialized: 21 | initialize(True) 22 | exit() 23 | 24 | check_auto_update() 25 | check_files_updated() 26 | 27 | refill_menu() 28 | 29 | while True: 30 | handler = menu.choice() 31 | if handler is not None: 32 | start_time = time.time() 33 | with logger.catch(): 34 | handler() 35 | logger.opt(colors=True).info( 36 | f"{locale.done % (time.time() - start_time)}" 37 | ) 38 | input(locale.to_continue) 39 | clear() 40 | 41 | 42 | if __name__ == "__main__": 43 | try: 44 | main() 45 | except KeyboardInterrupt: 46 | logger.info("Exit.") 47 | -------------------------------------------------------------------------------- /xcoder/bytestream.py: -------------------------------------------------------------------------------- 1 | import io 2 | import struct 3 | from typing import Literal 4 | 5 | 6 | class Reader: 7 | def __init__( 8 | self, 9 | initial_buffer: bytes = b"", 10 | endian: Literal["little", "big"] = "little", 11 | ): 12 | self._internal_reader = io.BytesIO(initial_buffer) 13 | 14 | self.endian: Literal["little", "big"] = endian 15 | self.endian_sign: Literal["<", ">"] = "<" if endian == "little" else ">" 16 | 17 | def seek(self, position: int) -> None: 18 | self._internal_reader.seek(position) 19 | 20 | def tell(self) -> int: 21 | return self._internal_reader.tell() 22 | 23 | def read(self, size: int) -> bytes: 24 | return self._internal_reader.read(size) 25 | 26 | def read_uchar(self) -> int: 27 | return struct.unpack("B", self.read(1))[0] 28 | 29 | def read_char(self) -> int: 30 | return struct.unpack("b", self.read(1))[0] 31 | 32 | def read_ushort(self) -> int: 33 | return struct.unpack(f"{self.endian_sign}H", self.read(2))[0] 34 | 35 | def read_short(self) -> int: 36 | return struct.unpack(f"{self.endian_sign}h", self.read(2))[0] 37 | 38 | def read_uint(self) -> int: 39 | return struct.unpack(f"{self.endian_sign}I", self.read(4))[0] 40 | 41 | def read_int(self) -> int: 42 | return struct.unpack(f"{self.endian_sign}i", self.read(4))[0] 43 | 44 | def read_twip(self) -> float: 45 | return self.read_int() / 20 46 | 47 | def read_string(self) -> str: 48 | length = self.read_uchar() 49 | if length != 0xFF: 50 | return self.read(length).decode() 51 | return "" 52 | 53 | 54 | class Writer(io.BytesIO): 55 | def __init__(self, endian: Literal["little", "big"] = "little"): 56 | super().__init__() 57 | self._endian: Literal["little", "big"] = endian 58 | 59 | def write_int(self, integer: int, length: int = 1, signed: bool = False): 60 | self.write(integer.to_bytes(length, self._endian, signed=signed)) 61 | 62 | def write_ubyte(self, integer: int): 63 | self.write_int(integer) 64 | 65 | def write_byte(self, integer: int): 66 | self.write_int(integer, signed=True) 67 | 68 | def write_uint16(self, integer: int): 69 | self.write_int(integer, 2) 70 | 71 | def write_int16(self, integer: int): 72 | self.write_int(integer, 2, True) 73 | 74 | def write_uint32(self, integer: int): 75 | self.write_int(integer, 4) 76 | 77 | def write_int32(self, integer: int): 78 | self.write_int(integer, 4, True) 79 | 80 | def write_string(self, string: str | None = None): 81 | if string is None: 82 | self.write_byte(0xFF) 83 | return 84 | 85 | encoded = string.encode() 86 | self.write_byte(len(encoded)) 87 | self.write(encoded) 88 | -------------------------------------------------------------------------------- /xcoder/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | from typing import LiteralString 5 | 6 | _LIBRARY_DIRECTORY = Path(__file__).parent 7 | 8 | 9 | class Config: 10 | DEFAULT_LANGUAGE: LiteralString = "en-EU" 11 | 12 | REPO_OWNER: LiteralString = "xcoder-tool" 13 | REPO_NAME: LiteralString = "xcoder" 14 | 15 | config_path = _LIBRARY_DIRECTORY / "config.json" 16 | 17 | def __init__(self): 18 | self.config_items = ( 19 | "initialized", 20 | "repo_owner", 21 | "repo_name", 22 | "version", 23 | "language", 24 | "has_update", 25 | "last_update", 26 | "auto_update", 27 | "should_render_movie_clips", 28 | ) 29 | 30 | self.initialized: bool = False 31 | self.repo_owner: str = Config.REPO_OWNER 32 | self.repo_name: str = Config.REPO_NAME 33 | self.version = None 34 | self.language: str = Config.DEFAULT_LANGUAGE 35 | self.has_update: bool = False 36 | self.last_update: int = -1 37 | self.auto_update: bool = False 38 | self.should_render_movie_clips: bool = False 39 | 40 | self.load() 41 | 42 | def toggle_auto_update(self) -> None: 43 | self.auto_update = not self.auto_update 44 | self.dump() 45 | 46 | def change_language(self, language: str) -> None: 47 | self.language = language 48 | self.dump() 49 | 50 | def load(self) -> None: 51 | if os.path.isfile(self.config_path): 52 | with open(self.config_path) as config_file: 53 | config_data = json.load(config_file) 54 | for key, value in config_data.items(): 55 | setattr(self, key, value) 56 | 57 | def dump(self) -> None: 58 | with open(self.config_path, "w") as config_file: 59 | json.dump( 60 | {item: getattr(self, item) for item in self.config_items}, config_file 61 | ) 62 | 63 | def get_repo_url(self) -> str: 64 | return f"https://github.com/{self.repo_owner}/{self.repo_name}" 65 | 66 | 67 | config = Config() 68 | -------------------------------------------------------------------------------- /xcoder/console.py: -------------------------------------------------------------------------------- 1 | class Console: 2 | previous_percentage: int = -1 3 | 4 | @classmethod 5 | def progress_bar(cls, message: str, current: int, total: int) -> None: 6 | percentage = (current + 1) * 100 // total 7 | if percentage == cls.previous_percentage: 8 | return 9 | 10 | print(f"\r[{percentage}%] {message}", end="") 11 | 12 | if percentage == 100: 13 | print() 14 | cls.previous_percentage = -1 15 | else: 16 | cls.previous_percentage = percentage 17 | 18 | @staticmethod 19 | def ask_integer(message: str): 20 | while True: 21 | try: 22 | return int(input(f"[????] {message}: ")) 23 | except ValueError: 24 | pass 25 | 26 | @staticmethod 27 | def question(message: str) -> bool: 28 | while True: 29 | answer = input(f"[????] {message} [Y/n] ").lower() 30 | if not answer: 31 | return True 32 | 33 | if answer in "ny": 34 | break 35 | 36 | return bool("ny".index(answer)) 37 | 38 | 39 | if __name__ == "__main__": 40 | Console.ask_integer("Please, type any integer") 41 | 42 | for i in range(1000): 43 | Console.progress_bar("Test progress bar", i, 1000) 44 | -------------------------------------------------------------------------------- /xcoder/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | from xcoder.exceptions.tool_not_found import ToolNotFoundException 2 | 3 | __all__ = ["ToolNotFoundException"] 4 | -------------------------------------------------------------------------------- /xcoder/exceptions/tool_not_found.py: -------------------------------------------------------------------------------- 1 | class ToolNotFoundException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /xcoder/features/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcoder-tool/XCoder/43ea81b1a06de4a19c5ed1b2732164bc3de464c9/xcoder/features/__init__.py -------------------------------------------------------------------------------- /xcoder/features/csv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcoder-tool/XCoder/43ea81b1a06de4a19c5ed1b2732164bc3de464c9/xcoder/features/csv/__init__.py -------------------------------------------------------------------------------- /xcoder/features/csv/compress.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from loguru import logger 4 | from sc_compression import compress 5 | 6 | from xcoder.localization import locale 7 | 8 | 9 | def compress_csv(): 10 | from sc_compression.signatures import Signatures 11 | 12 | folder = "./CSV/In-Decompressed" 13 | folder_export = "./CSV/Out-Compressed" 14 | 15 | for file in os.listdir(folder): 16 | if file.endswith(".csv"): 17 | try: 18 | with open(f"{folder}/{file}", "rb") as f: 19 | file_data = f.read() 20 | 21 | with open(f"{folder_export}/{file}", "wb") as f: 22 | f.write(compress(file_data, Signatures.LZMA)) 23 | except Exception as exception: 24 | logger.exception( 25 | locale.error 26 | % ( 27 | exception.__class__.__module__, 28 | exception.__class__.__name__, 29 | exception, 30 | ) 31 | ) 32 | 33 | print() 34 | -------------------------------------------------------------------------------- /xcoder/features/csv/decompress.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from loguru import logger 4 | from sc_compression import decompress 5 | 6 | from xcoder.localization import locale 7 | 8 | 9 | def decompress_csv(): 10 | folder = "./CSV/In-Compressed" 11 | folder_export = "./CSV/Out-Decompressed" 12 | 13 | for file in os.listdir(folder): 14 | if file.endswith(".csv"): 15 | try: 16 | with open(f"{folder}/{file}", "rb") as f: 17 | file_data = f.read() 18 | 19 | with open(f"{folder_export}/{file}", "wb") as f: 20 | f.write(decompress(file_data)[0]) 21 | except Exception as exception: 22 | logger.exception( 23 | locale.error 24 | % ( 25 | exception.__class__.__module__, 26 | exception.__class__.__name__, 27 | exception, 28 | ) 29 | ) 30 | 31 | print() 32 | -------------------------------------------------------------------------------- /xcoder/features/csv/update.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from loguru import logger 4 | from sc_compression import compress 5 | 6 | from xcoder.localization import locale 7 | 8 | 9 | def update_csv(): 10 | from sc_compression.signatures import Signatures 11 | 12 | input_folder = "./CSV/In-Old" 13 | export_folder = "./CSV/Out-Updated" 14 | 15 | for file in os.listdir(input_folder): 16 | if file.endswith(".csv"): 17 | try: 18 | with open(f"{input_folder}/{file}", "rb") as f: 19 | file_data = f.read() 20 | 21 | with open(f"{export_folder}/{file}", "wb") as f: 22 | f.write(compress(file_data, Signatures.LZMA)) 23 | except Exception as exception: 24 | logger.exception( 25 | locale.error 26 | % ( 27 | exception.__class__.__module__, 28 | exception.__class__.__name__, 29 | exception, 30 | ) 31 | ) 32 | 33 | print() 34 | -------------------------------------------------------------------------------- /xcoder/features/cut_sprites.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from xcoder.config import config 5 | from xcoder.console import Console 6 | from xcoder.localization import locale 7 | from xcoder.matrices import Matrix2x3 8 | from xcoder.objects.renderable.renderable_factory import create_renderable_from_plain 9 | from xcoder.swf import SupercellSWF 10 | 11 | 12 | def render_objects(swf: SupercellSWF, output_folder: Path): 13 | os.makedirs(output_folder / "overwrite", exist_ok=True) 14 | os.makedirs(output_folder / "shapes", exist_ok=True) 15 | os.makedirs(output_folder / "movie_clips", exist_ok=True) 16 | 17 | shapes_count = len(swf.shapes) 18 | 19 | swf.xcod_writer.write_uint16(shapes_count) 20 | for shape_index in range(shapes_count): 21 | shape = swf.shapes[shape_index] 22 | 23 | regions_count = len(shape.regions) 24 | swf.xcod_writer.write_uint16(shape.id) 25 | swf.xcod_writer.write_uint16(regions_count) 26 | for region_index in range(regions_count): 27 | region = shape.regions[region_index] 28 | 29 | swf.xcod_writer.write_ubyte(region.texture_index) 30 | swf.xcod_writer.write_ubyte(region.get_point_count()) 31 | 32 | for i in range(region.get_point_count()): 33 | swf.xcod_writer.write_uint16(int(region.get_u(i))) 34 | swf.xcod_writer.write_uint16(int(region.get_v(i))) 35 | 36 | for shape_index in range(shapes_count): 37 | shape = swf.shapes[shape_index] 38 | 39 | Console.progress_bar( 40 | locale.cut_sprites_process % (shape_index + 1, shapes_count), 41 | shape_index, 42 | shapes_count, 43 | ) 44 | 45 | rendered_shape = create_renderable_from_plain(swf, shape).render(Matrix2x3()) 46 | rendered_shape.save(f"{output_folder}/shapes/{shape.id}.png") 47 | 48 | regions_count = len(shape.regions) 49 | for region_index in range(regions_count): 50 | region = shape.regions[region_index] 51 | 52 | rendered_region = region.get_image() 53 | rendered_region.save(f"{output_folder}/shape_{shape.id}_{region_index}.png") 54 | 55 | if config.should_render_movie_clips: 56 | movie_clips_skipped = 0 57 | movie_clip_count = len(swf.movie_clips) 58 | for movie_clip_index in range(movie_clip_count): 59 | movie_clip = swf.movie_clips[movie_clip_index] 60 | 61 | rendered_movie_clip = create_renderable_from_plain(swf, movie_clip).render( 62 | Matrix2x3() 63 | ) 64 | if sum(rendered_movie_clip.size) >= 2: 65 | clip_name = movie_clip.export_name or movie_clip.id 66 | rendered_movie_clip.save(f"{output_folder}/movie_clips/{clip_name}.png") 67 | else: 68 | # For debug: 69 | # logger.warning(f'MovieClip {movie_clip.id} cannot be rendered.') 70 | movie_clips_skipped += 1 71 | 72 | Console.progress_bar( 73 | "Rendering movie clips (%d/%d). Skipped count: %d" 74 | % (movie_clip_index + 1, movie_clip_count, movie_clips_skipped), 75 | movie_clip_index, 76 | movie_clip_count, 77 | ) 78 | -------------------------------------------------------------------------------- /xcoder/features/directories.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | IO_TYPES = ("In", "Out") 5 | 6 | SC_FILE_TYPES = ("Compressed", "Decompressed", "Sprites") 7 | CSV_FILE_TYPES = ("Compressed", "Decompressed") 8 | TEXTURE_FILE_TYPES = ("KTX", "PNG") 9 | 10 | 11 | def create_directories(): 12 | for io_type in IO_TYPES: 13 | for filetype in SC_FILE_TYPES: 14 | os.makedirs(f"SC/{io_type}-{filetype}", exist_ok=True) 15 | 16 | for filetype in CSV_FILE_TYPES: 17 | os.makedirs(f"CSV/{io_type}-{filetype}", exist_ok=True) 18 | 19 | for filetype in TEXTURE_FILE_TYPES: 20 | os.makedirs(f"TEX/{io_type}-{filetype}", exist_ok=True) 21 | 22 | 23 | def clear_directories(): 24 | for io_type in IO_TYPES: 25 | for filetype in SC_FILE_TYPES: 26 | _recreate_directory(f"SC/{io_type}-{filetype}") 27 | 28 | for filetype in CSV_FILE_TYPES: 29 | _recreate_directory(f"CSV/{io_type}-{filetype}") 30 | 31 | for filetype in TEXTURE_FILE_TYPES: 32 | _recreate_directory(f"TEX/{io_type}-{filetype}") 33 | 34 | 35 | def _recreate_directory(directory): 36 | if os.path.isdir(directory): 37 | shutil.rmtree(directory) 38 | os.makedirs(directory, exist_ok=True) 39 | -------------------------------------------------------------------------------- /xcoder/features/files.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from loguru import logger 4 | from sc_compression import compress, decompress 5 | from sc_compression.signatures import Signatures 6 | 7 | from xcoder.localization import locale 8 | 9 | 10 | def write_sc( 11 | output_filename: os.PathLike | str, 12 | buffer: bytes, 13 | signature: Signatures, 14 | version: int | None = None, 15 | ): 16 | with open(output_filename, "wb") as file_out: 17 | file_out.write(compress(buffer, signature, version)) # type: ignore 18 | 19 | 20 | def open_sc(input_filename: os.PathLike | str) -> tuple[bytes, Signatures, int]: 21 | with open(input_filename, "rb") as f: 22 | file_data = f.read() 23 | 24 | try: 25 | if b"START" in file_data: 26 | file_data = file_data[: file_data.index(b"START")] 27 | 28 | return decompress(file_data) 29 | except TypeError: 30 | logger.info(locale.decompression_error) 31 | exit(1) 32 | -------------------------------------------------------------------------------- /xcoder/features/initialization.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | from loguru import logger 4 | 5 | from xcoder import clear 6 | from xcoder.config import config 7 | from xcoder.features.directories import create_directories 8 | from xcoder.features.update.check import get_tags 9 | from xcoder.localization import locale 10 | 11 | 12 | @logger.catch() 13 | def initialize(first_init=False): 14 | if first_init: 15 | clear() 16 | 17 | logger.info(locale.detected_os % platform.system()) 18 | logger.info(locale.installing) 19 | 20 | logger.info(locale.crt_workspace) 21 | create_directories() 22 | logger.info(locale.verifying) 23 | 24 | config.initialized = True 25 | config.version = get_tags(config.repo_owner, config.repo_name)[0]["name"][1:] 26 | config.dump() 27 | 28 | if first_init: 29 | input(locale.to_continue) 30 | -------------------------------------------------------------------------------- /xcoder/features/ktx.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from loguru import logger 5 | 6 | from xcoder.localization import locale 7 | from xcoder.pvr_tex_tool import convert_ktx_to_png, convert_png_to_ktx 8 | 9 | IN_PNG_PATH = Path("./TEX/In-PNG") 10 | IN_KTX_PATH = Path("./TEX/In-KTX") 11 | OUT_PNG_PATH = Path("./TEX/Out-PNG") 12 | OUT_KTX_PATH = Path("./TEX/Out-KTX") 13 | 14 | 15 | def convert_png_textures_to_ktx(): 16 | input_folder = IN_PNG_PATH 17 | output_folder = OUT_KTX_PATH 18 | 19 | for file in os.listdir(input_folder): 20 | if not file.endswith(".png"): 21 | continue 22 | 23 | png_filepath = input_folder / file 24 | if not os.path.isfile(png_filepath): 25 | continue 26 | 27 | logger.info(locale.collecting_inf % file) 28 | convert_png_to_ktx(png_filepath, output_folder=output_folder) 29 | 30 | 31 | def convert_ktx_textures_to_png(): 32 | input_folder = IN_KTX_PATH 33 | output_folder = OUT_PNG_PATH 34 | 35 | for file in os.listdir(input_folder): 36 | if not file.endswith(".ktx"): 37 | continue 38 | 39 | ktx_filepath = input_folder / file 40 | if not os.path.isfile(ktx_filepath): 41 | continue 42 | 43 | logger.info(locale.collecting_inf % file) 44 | convert_ktx_to_png(ktx_filepath, output_folder=output_folder) 45 | -------------------------------------------------------------------------------- /xcoder/features/place_sprites.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from PIL import Image 5 | 6 | from xcoder.console import Console 7 | from xcoder.images import create_filled_polygon_image, get_format_by_pixel_type 8 | from xcoder.localization import locale 9 | from xcoder.math.polygon import get_rect 10 | from xcoder.xcod import FileInfo 11 | 12 | MASK_COLOR = 255 13 | 14 | 15 | def place_sprites( 16 | file_info: FileInfo, folder: Path, overwrite: bool = False 17 | ) -> list[Image.Image]: 18 | files_to_overwrite = os.listdir(folder / ("overwrite" if overwrite else "")) 19 | texture_files = os.listdir(folder / "textures") 20 | 21 | sheets = [] 22 | for i in range(len(file_info.sheets)): 23 | sheet_info = file_info.sheets[i] 24 | 25 | sheets.append( 26 | Image.open(f"{folder}/textures/{texture_files[i]}") 27 | if overwrite 28 | else Image.new( 29 | get_format_by_pixel_type(sheet_info.pixel_type), sheet_info.size 30 | ) 31 | ) 32 | 33 | shapes_count = len(file_info.shapes) 34 | for shape_index, shape_info in enumerate(file_info.shapes): 35 | Console.progress_bar( 36 | locale.place_sprites_process % (shape_index + 1, shapes_count), 37 | shape_index, 38 | shapes_count, 39 | ) 40 | 41 | for region_index, region_info in enumerate(shape_info.regions): 42 | texture_width = sheets[region_info.texture_id].width 43 | texture_height = sheets[region_info.texture_id].height 44 | 45 | filename = f"shape_{shape_info.id}_{region_index}.png" 46 | if filename not in files_to_overwrite: 47 | continue 48 | 49 | rect = get_rect(region_info.points) 50 | 51 | img_mask = create_filled_polygon_image( 52 | "L", texture_width, texture_height, region_info.points, MASK_COLOR 53 | ) 54 | 55 | if rect.width == 0 or rect.height == 0: 56 | if rect.height != 0: 57 | for _y in range(int(rect.height)): 58 | img_mask.putpixel( 59 | (int(rect.right - 1), int(rect.top + _y - 1)), MASK_COLOR 60 | ) 61 | rect.right += 1 62 | elif rect.width != 0: 63 | for _x in range(int(rect.width)): 64 | img_mask.putpixel( 65 | (int(rect.left + _x - 1), int(rect.bottom - 1)), MASK_COLOR 66 | ) 67 | rect.bottom += 1 68 | else: 69 | img_mask.putpixel( 70 | (int(rect.right - 1), int(rect.bottom - 1)), MASK_COLOR 71 | ) 72 | rect.right += 1 73 | rect.bottom += 1 74 | 75 | x = int(rect.left) 76 | y = int(rect.top) 77 | width = int(rect.width) 78 | height = int(rect.height) 79 | bbox = int(rect.left), int(rect.top), int(rect.right), int(rect.bottom) 80 | 81 | region_image = Image.open( 82 | f"{folder}{'/overwrite' if overwrite else ''}/{filename}" 83 | ).convert("RGBA") 84 | 85 | sheets[region_info.texture_id].paste( 86 | Image.new("RGBA", (width, height)), (x, y), img_mask.crop(bbox) 87 | ) 88 | sheets[region_info.texture_id].paste(region_image, (x, y), region_image) 89 | print() 90 | 91 | return sheets 92 | -------------------------------------------------------------------------------- /xcoder/features/sc/__init__.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from pathlib import Path 3 | 4 | from loguru import logger 5 | from PIL import Image 6 | 7 | from xcoder.bytestream import Writer 8 | from xcoder.console import Console 9 | from xcoder.features.files import write_sc 10 | from xcoder.images import get_byte_count_by_pixel_type, save_texture, split_image 11 | from xcoder.localization import locale 12 | from xcoder.xcod import FileInfo 13 | 14 | 15 | def compile_sc( 16 | output_folder: Path, 17 | file_info: FileInfo, 18 | sheets: list[Image.Image], 19 | ): 20 | sc = Writer() 21 | 22 | for picture_index in range(len(sheets)): 23 | sheet_info = file_info.sheets[picture_index] 24 | sheet = sheets[picture_index] 25 | 26 | file_type = sheet_info.file_type 27 | pixel_type = sheet_info.pixel_type 28 | 29 | if sheet.size != sheet_info.size: 30 | logger.info( 31 | locale.illegal_size 32 | % (sheet_info.width, sheet_info.height, sheet.width, sheet.height) 33 | ) 34 | 35 | if Console.question(locale.resize_qu): 36 | logger.info(locale.resizing) 37 | sheet = sheet.resize(sheet_info.size, Image.Resampling.LANCZOS) 38 | 39 | width, height = sheet.size 40 | pixel_size = get_byte_count_by_pixel_type(pixel_type) 41 | 42 | file_size = width * height * pixel_size + 5 43 | 44 | logger.info( 45 | locale.about_sc.format( 46 | filename=file_info.name, 47 | index=picture_index, 48 | pixel_type=pixel_type, 49 | width=width, 50 | height=height, 51 | ) 52 | ) 53 | 54 | sc.write(struct.pack(" Path: 100 | objects_output_folder = output_folder / base_name 101 | if objects_output_folder.exists(): 102 | shutil.rmtree(objects_output_folder) 103 | objects_output_folder.mkdir(parents=True) 104 | return objects_output_folder 105 | 106 | 107 | def _save_textures(swf: SupercellSWF, textures_output: Path, base_name: str) -> None: 108 | os.makedirs(textures_output, exist_ok=True) 109 | for texture_index, texture in enumerate(swf.textures): 110 | assert texture.image is not None 111 | texture.image.save(textures_output / f"{base_name}_{texture_index}.png") 112 | 113 | 114 | def _save_meta_file( 115 | swf: SupercellSWF, 116 | objects_output_folder: Path, 117 | base_name: str, 118 | signature: Signatures, 119 | ) -> None: 120 | writer = Writer() 121 | writer.write(b"XCOD") 122 | writer.write_string(signature.name) 123 | writer.write_ubyte(len(swf.textures)) 124 | writer.write(swf.xcod_writer.getvalue()) 125 | 126 | with open(objects_output_folder / f"{base_name}.xcod", "wb") as file: 127 | file.write(writer.getvalue()) 128 | -------------------------------------------------------------------------------- /xcoder/features/sc/encode.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from loguru import logger 5 | from PIL import Image 6 | 7 | from xcoder.features.place_sprites import place_sprites 8 | from xcoder.features.sc import compile_sc 9 | from xcoder.localization import locale 10 | from xcoder.xcod import parse_info 11 | 12 | OUT_COMPRESSED_PATH = Path("./SC/Out-Compressed") 13 | IN_DECOMPRESSED_PATH = Path("./SC/In-Decompressed") 14 | IN_SPRITES_PATH = Path("./SC/In-Sprites/") 15 | 16 | 17 | def encode_textures_only(): 18 | input_folder = IN_DECOMPRESSED_PATH 19 | output_folder = OUT_COMPRESSED_PATH 20 | 21 | for folder in os.listdir(input_folder): 22 | textures_input_folder = input_folder / folder 23 | 24 | if not os.path.isdir(textures_input_folder): 25 | continue 26 | 27 | xcod_path = _ensure_metadata_exists(textures_input_folder, folder) 28 | if xcod_path is None: 29 | continue 30 | 31 | file_info = parse_info(xcod_path, False) 32 | sheets = _load_sheets(textures_input_folder) 33 | compile_sc(output_folder, file_info, sheets) 34 | 35 | 36 | def collect_objects_and_encode(overwrite: bool = False) -> None: 37 | input_folder = IN_SPRITES_PATH 38 | output_folder = OUT_COMPRESSED_PATH 39 | 40 | for folder in os.listdir(input_folder): 41 | objects_input_folder = input_folder / folder 42 | 43 | if not os.path.isdir(objects_input_folder): 44 | continue 45 | 46 | xcod_path = _ensure_metadata_exists(objects_input_folder, folder) 47 | if xcod_path is None: 48 | continue 49 | 50 | file_info = parse_info(xcod_path, True) 51 | sheets = place_sprites(file_info, objects_input_folder, overwrite) 52 | compile_sc(output_folder, file_info, sheets) 53 | 54 | 55 | def _ensure_metadata_exists(input_folder: Path, file: str) -> Path | None: 56 | metadata_file_name = f"{file}.xcod" 57 | metadata_file_path = input_folder / metadata_file_name 58 | 59 | if not os.path.exists(metadata_file_path): 60 | logger.error(locale.file_not_found % metadata_file_name) 61 | print() 62 | return None 63 | 64 | return metadata_file_path 65 | 66 | 67 | def _load_sheets(input_folder: Path) -> list[Image.Image]: 68 | files = [] 69 | for i in os.listdir(input_folder): 70 | if i.endswith(".png"): 71 | files.append(i) 72 | files.sort() 73 | 74 | if not files: 75 | raise RuntimeError(locale.dir_empty % input_folder.name) 76 | return [Image.open(input_folder / file) for file in files] 77 | -------------------------------------------------------------------------------- /xcoder/features/update/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcoder-tool/XCoder/43ea81b1a06de4a19c5ed1b2732164bc3de464c9/xcoder/features/update/__init__.py -------------------------------------------------------------------------------- /xcoder/features/update/check.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from loguru import logger 5 | 6 | from xcoder import run 7 | from xcoder.config import config 8 | from xcoder.features.update.download import download_update 9 | from xcoder.localization import locale 10 | 11 | 12 | def get_run_output(command: str): 13 | import tempfile 14 | 15 | temp_filename = tempfile.mktemp(".temp") 16 | 17 | del tempfile 18 | run(command, temp_filename) 19 | 20 | with open(temp_filename) as f: 21 | file_data = f.read() 22 | 23 | os.remove(temp_filename) 24 | 25 | return file_data 26 | 27 | 28 | def get_pip_info(outdated: bool = False) -> list: 29 | output = get_run_output( 30 | f"pip --disable-pip-version-check list {'-o' if outdated else ''}" 31 | ) 32 | output = output.splitlines() 33 | output = output[2:] 34 | packages = [package.split() for package in output] 35 | 36 | return packages 37 | 38 | 39 | def get_tags(owner: str, repo: str): 40 | api_url = "https://api.github.com" 41 | 42 | import urllib.request 43 | 44 | tags = json.loads( 45 | urllib.request.urlopen(api_url + f"/repos/{owner}/{repo}/tags").read().decode() 46 | ) 47 | tags = [ 48 | {key: v for key, v in tag.items() if key in ["name", "zipball_url"]} 49 | for tag in tags 50 | ] 51 | 52 | return tags 53 | 54 | 55 | def check_update(): 56 | tags = get_tags(config.repo_owner, config.repo_name) 57 | 58 | if len(tags) > 0: 59 | latest_tag = tags[0] 60 | latest_tag_name = latest_tag["name"][1:] # clear char 'v' at string start 61 | 62 | logger.info(locale.check_update) 63 | if config.version != latest_tag_name: 64 | logger.error(locale.not_latest) 65 | 66 | logger.info(locale.update_downloading) 67 | download_update(latest_tag["zipball_url"]) 68 | -------------------------------------------------------------------------------- /xcoder/features/update/download.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from loguru import logger 5 | 6 | from xcoder.config import config 7 | from xcoder.localization import locale 8 | 9 | 10 | def download_update(zip_url: str) -> None: 11 | if not os.path.exists("updates"): 12 | os.mkdir("updates") 13 | 14 | try: 15 | import urllib.request 16 | 17 | with open("updates/update.zip", "wb") as f: 18 | f.write(urllib.request.urlopen(zip_url).read()) 19 | 20 | import zipfile 21 | 22 | with zipfile.ZipFile("updates/update.zip") as zf: 23 | zf.extractall("updates/") 24 | zf.close() 25 | 26 | folder_name = f' "{zf.namelist()[0]}"' 27 | logger.opt(colors=True).info( 28 | f"{locale.update_done % folder_name}" 29 | ) 30 | config.has_update = True 31 | config.last_update = int(time.time()) 32 | config.dump() 33 | input(locale.to_continue) 34 | exit() 35 | except ImportError as exception: 36 | logger.exception(exception) 37 | -------------------------------------------------------------------------------- /xcoder/images.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import TYPE_CHECKING 3 | 4 | from PIL import Image, ImageDraw 5 | 6 | if TYPE_CHECKING: 7 | from PIL._imaging import PixelAccess # type: ignore[reportPrivateImportUsage] 8 | 9 | from xcoder.bytestream import Reader, Writer 10 | from xcoder.console import Console 11 | from xcoder.localization import locale 12 | from xcoder.math.point import Point 13 | from xcoder.matrices import Matrix2x3 14 | from xcoder.pixel_utils import get_pixel_encode_function, get_raw_mode 15 | 16 | CHUNK_SIZE = 32 17 | 18 | 19 | def load_image_from_buffer( 20 | pixel_type: int, width: int, height: int, pixel_buffer: Reader 21 | ) -> Image.Image: 22 | raw_mode = get_raw_mode(pixel_type) 23 | bytes_per_pixel = get_byte_count_by_pixel_type(pixel_type) 24 | 25 | return Image.frombuffer( 26 | get_format_by_pixel_type(pixel_type), 27 | (width, height), 28 | pixel_buffer.read(width * height * bytes_per_pixel), 29 | "raw", 30 | raw_mode, 31 | 0, 32 | 1, 33 | ) 34 | 35 | 36 | def join_image( 37 | pixel_type: int, width: int, height: int, pixel_buffer: Reader 38 | ) -> Image.Image: 39 | mode = get_format_by_pixel_type(pixel_type) 40 | bytes_per_pixel = get_byte_count_by_pixel_type(pixel_type) 41 | image = Image.new(mode, (width, height)) 42 | 43 | chunk_count_x = math.ceil(width / CHUNK_SIZE) 44 | chunk_count_y = math.ceil(height / CHUNK_SIZE) 45 | chunk_count = chunk_count_x * chunk_count_y 46 | 47 | raw_mode = get_raw_mode(pixel_type) 48 | 49 | for chunk_index in range(chunk_count): 50 | chunk_x = (chunk_index % chunk_count_x) * CHUNK_SIZE 51 | chunk_y = (chunk_index // chunk_count_x) * CHUNK_SIZE 52 | 53 | chunk_width = min(width - chunk_x, CHUNK_SIZE) 54 | chunk_height = min(height - chunk_y, CHUNK_SIZE) 55 | 56 | sub_image = Image.frombuffer( 57 | mode, 58 | (chunk_width, chunk_height), 59 | pixel_buffer.read(bytes_per_pixel * chunk_width * chunk_height), 60 | "raw", 61 | raw_mode, 62 | 0, 63 | 1, 64 | ) 65 | 66 | image.paste(sub_image, (chunk_x, chunk_y)) 67 | 68 | Console.progress_bar(locale.join_pic, chunk_index, chunk_count) 69 | 70 | return image 71 | 72 | 73 | def _add_pixel( 74 | image: "PixelAccess", pixel_index: int, width: int, color: tuple 75 | ) -> None: 76 | image[pixel_index % width, int(pixel_index / width)] = color 77 | 78 | 79 | def split_image(image: Image.Image) -> Image.Image: 80 | width, height = image.size 81 | 82 | chunk_count_x = math.ceil(width / CHUNK_SIZE) 83 | chunk_count_y = math.ceil(height / CHUNK_SIZE) 84 | chunk_count = chunk_count_x * chunk_count_y 85 | 86 | split_image_buffers = [] 87 | 88 | for chunk_index in range(chunk_count): 89 | chunk_x = (chunk_index % chunk_count_x) * CHUNK_SIZE 90 | chunk_y = (chunk_index // chunk_count_x) * CHUNK_SIZE 91 | 92 | chunk_width = min(width - chunk_x, CHUNK_SIZE) 93 | chunk_height = min(height - chunk_y, CHUNK_SIZE) 94 | 95 | chunk = image.crop( 96 | (chunk_x, chunk_y, chunk_x + chunk_width, chunk_y + chunk_height) 97 | ) 98 | 99 | split_image_buffers.append(chunk.tobytes("raw")) 100 | 101 | Console.progress_bar(locale.split_pic, chunk_index, chunk_count) 102 | 103 | return Image.frombuffer( 104 | image.mode, image.size, b"".join(split_image_buffers), "raw" 105 | ) 106 | 107 | 108 | def get_byte_count_by_pixel_type(pixel_type: int) -> int: 109 | if pixel_type in (0, 1): 110 | return 4 111 | elif pixel_type in (2, 3, 4, 6): 112 | return 2 113 | elif pixel_type == 10: 114 | return 1 115 | raise Exception(locale.unknown_pixel_type % pixel_type) 116 | 117 | 118 | def get_format_by_pixel_type(pixel_type: int) -> str: 119 | if pixel_type in (0, 1, 2, 3): 120 | return "RGBA" 121 | elif pixel_type == 4: 122 | return "RGB" 123 | elif pixel_type == 6: 124 | return "LA" 125 | elif pixel_type == 10: 126 | return "L" 127 | 128 | raise Exception(locale.unknown_pixel_type % pixel_type) 129 | 130 | 131 | def save_texture(writer: Writer, image: Image.Image, pixel_type: int) -> None: 132 | raw_mode = get_raw_mode(pixel_type) 133 | encode_pixel = get_pixel_encode_function(pixel_type) 134 | 135 | width, height = image.size 136 | 137 | pixels = image.getdata() 138 | 139 | # Some packers for raw_encoder are absent 140 | # https://github.com/python-pillow/Pillow/blob/58e48745cc7b6c6f7dd26a50fe68d1a82ea51562/src/encode.c#L337 141 | # https://github.com/python-pillow/Pillow/blob/main/src/libImaging/Pack.c#L668 142 | if raw_mode != image.mode and encode_pixel is not None: 143 | for y in range(height): 144 | for x in range(width): 145 | # noinspection PyTypeChecker 146 | writer.write(encode_pixel(pixels[y * width + x])) 147 | 148 | Console.progress_bar(locale.writing_pic, y, height) 149 | 150 | return 151 | 152 | writer.write(image.tobytes("raw", raw_mode, 0, 1)) 153 | Console.progress_bar(locale.writing_pic, height - 1, height) 154 | 155 | 156 | def transform_image( 157 | image: Image.Image, scale_x: float, scale_y: float, angle: float, x: float, y: float 158 | ) -> Image.Image: 159 | im_orig = image 160 | image = Image.new("RGBA", im_orig.size, (255, 255, 255, 255)) 161 | image.paste(im_orig) 162 | 163 | w, h = image.size 164 | angle = -angle 165 | 166 | cos_theta = math.cos(angle) 167 | sin_theta = math.sin(angle) 168 | 169 | scaled_w, scaled_h = w * scale_x, h * scale_y 170 | 171 | scaled_rotated_w = int( 172 | math.ceil(math.fabs(cos_theta * scaled_w) + math.fabs(sin_theta * scaled_h)) 173 | ) 174 | scaled_rotated_h = int( 175 | math.ceil(math.fabs(sin_theta * scaled_w) + math.fabs(cos_theta * scaled_h)) 176 | ) 177 | 178 | translated_w = int(math.ceil(scaled_rotated_w + math.fabs(x))) 179 | translated_h = int(math.ceil(scaled_rotated_h + math.fabs(y))) 180 | if x > 0: 181 | x = 0 182 | if y > 0: 183 | y = 0 184 | 185 | cx = w / 2.0 186 | cy = h / 2.0 187 | translate_x = scaled_rotated_w / 2.0 - x 188 | translate_y = scaled_rotated_h / 2.0 - y 189 | 190 | a = cos_theta / scale_x 191 | b = sin_theta / scale_x 192 | c = cx - translate_x * a - translate_y * b 193 | d = -sin_theta / scale_y 194 | e = cos_theta / scale_y 195 | f = cy - translate_x * d - translate_y * e 196 | 197 | return image.transform( 198 | (translated_w, translated_h), 199 | Image.Transform.AFFINE, 200 | (a, b, c, d, e, f), 201 | resample=Image.Resampling.BILINEAR, 202 | ) 203 | 204 | 205 | def translate_image(image, x: float, y: float) -> Image.Image: 206 | w, h = image.size 207 | 208 | translated_w = int(math.ceil(w + math.fabs(x))) 209 | translated_h = int(math.ceil(h + math.fabs(y))) 210 | if x > 0: 211 | x = 0 212 | if y > 0: 213 | y = 0 214 | 215 | return image.transform( 216 | (translated_w, translated_h), 217 | Image.Transform.AFFINE, 218 | (1, 0, -x, 0, 1, -y), 219 | resample=Image.Resampling.BILINEAR, 220 | ) 221 | 222 | 223 | def transform_image_by_matrix(image: Image.Image, matrix: Matrix2x3): 224 | new_width = abs(int(matrix.apply_x(image.width, image.height))) 225 | new_height = abs(int(matrix.apply_y(image.width, image.height))) 226 | 227 | return image.transform( 228 | (new_width, new_height), 229 | Image.Transform.AFFINE, 230 | (matrix.a, matrix.b, matrix.x, matrix.c, matrix.d, matrix.y), 231 | resample=Image.Resampling.BILINEAR, 232 | ) 233 | 234 | 235 | def create_filled_polygon_image( 236 | mode: str, width: int, height: int, polygon: list[Point], color: int 237 | ) -> Image.Image: 238 | mask_image = Image.new(mode, (width, height), 0) 239 | drawable_image = ImageDraw.Draw(mask_image) 240 | drawable_image.polygon([point.as_tuple() for point in polygon], fill=color) 241 | 242 | return mask_image 243 | -------------------------------------------------------------------------------- /xcoder/languages/en-EU.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "English", 3 | "xcoder_header": "XCoder | Version: %s | by Vorono4ka", 4 | "detected_os": "Detected %s OS.", 5 | "installing": "Installing required modules...", 6 | "update_downloading": "Update downloading...", 7 | "crt_workspace": "Creating workspace directories...", 8 | "verifying": "Verifying installation...", 9 | "installed": "%s was successfully installed!", 10 | "update_done": "The update was downloaded successfully! Please move all files from directory %s to script directory", 11 | "not_installed": "%s wasn't installed!", 12 | "clear_qu": "Are you sure you want to clear the dirs?", 13 | "done": "Completed in %.2f seconds!", 14 | "done_qu": "Done?", 15 | "choice": "Your choice: ", 16 | "to_continue": "Press Enter to continue...", 17 | "experimental": "Experimental feature", 18 | 19 | "sc_label": "SC", 20 | "decode_sc": "Decode SC", 21 | "encode_sc": "Encode SC", 22 | "decode_by_parts": "Decode SC to sprites", 23 | "encode_by_parts": "Encode SC from sprites", 24 | "overwrite_by_parts": "Encode SC from sprites", 25 | "decode_sc_description": "Converts SC to PNG", 26 | "encode_sc_description": "Converts PNG to SC", 27 | "decode_by_parts_description": "Converts SC in PNG and cuts texture to sprites", 28 | "encode_by_parts_description": "Puts sprites on the texture and converts PNG in SC", 29 | "overwrite_by_parts_description": "Do the same as the previous but consider the \"overwrite\" folder", 30 | 31 | "csv_label": "CSV", 32 | "decompress_csv": "Decompress the CSV", 33 | "compress_csv": "Compress the CSV", 34 | "decompress_csv_description": "Decompresses spreadsheet file", 35 | "compress_csv_description": "Compresses spreadsheet file", 36 | 37 | "ktx_label": "KTX", 38 | "ktx_from_png_label": "PNG2KTX", 39 | "png_from_ktx_label": "KTX2PNG", 40 | "ktx_from_png_description": "Converts PNG to KTX", 41 | "png_from_ktx_description": "Converts KTX to PNG", 42 | 43 | "other_features_label": "OTHER", 44 | "check_update": "Check for updates", 45 | "reinit": "Repeat init", 46 | "change_language": "Select another language", 47 | "clear_directories": "Clear workspace dirs", 48 | "toggle_update_auto_checking": "Toggle update auto checking", 49 | "exit": "Exit", 50 | "version": "Current version: %s", 51 | "reinit_description": "If something went wrong", 52 | "change_lang_description": "Current lang: %s", 53 | "clean_dirs_description": "Clear In and Out dirs", 54 | 55 | "not_latest": "Not the latest version installed", 56 | "collecting_inf": "File: %s. Collecting information...", 57 | "about_sc": "About texture. Filename: {filename} ({index}), Pixel type: {pixel_type}, Size: {width}x{height}", 58 | "decompression_error": "Error while decompressing! Trying to decode as is...", 59 | "skip_not_installed": "%s isn't installed! Reinitialize", 60 | "detected_comp": "%s compression detected", 61 | "unknown_pixel_type": "Unknown pixel type: %s", 62 | "crt_pic": "Creating picture...", 63 | "join_pic": "Joining picture...", 64 | "png_save": "Saving to png...", 65 | "saved": "Saving completed!", 66 | "illegal_size": "Illegal image size! Expected %sx%s but we got %sx%s", 67 | "resize_qu": "Would you like to resize an image?", 68 | "resizing": "Resizing...", 69 | "split_pic": "Splitting picture...", 70 | "writing_pic": "Writing pixels...", 71 | "compressing_with": "Compressing texture with %s...", 72 | "compression_error": "Compression failed", 73 | "compression_done": "Compression done!", 74 | "dir_empty": "Dir '%s' is empty!", 75 | "texture_not_found": "Texture file for '%s' not found!", 76 | "file_not_found": "File '%s' not found!", 77 | "cut_sprites_process": "Cutting sprites... (%d/%d)", 78 | "place_sprites_process": "Placing sprites... (%d/%d)", 79 | "not_implemented": "This feature will be added in future updates.\nYou can follow XCoder updates here: {repo_url}", 80 | "error": "ERROR! (%s.%s: %s)", 81 | "e1sc1": "Overwrite SC sprites", 82 | "cgl": "Changelog:\n%s", 83 | "upd_av": "\nUpdate is available!\nVersion: %s\n", 84 | "upd_qu": "Do you want to update?", 85 | "upd": "Updating...", 86 | "upd_ck": "Checking for updates...", 87 | "bkp": "Backing up current version...", 88 | "stp": "Installing...", 89 | "margin_qu": "Consider edge margins?", 90 | "enabled": "Enabled", 91 | "disabled": "Disabled", 92 | 93 | "install_to_unlock": "Install '%s' to unlock more functions!" 94 | } 95 | -------------------------------------------------------------------------------- /xcoder/languages/ru-RU.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Russian (Русский)", 3 | "xcoder_header": "XCoder | Версия: %s | От: Vorono4ka", 4 | "detected_os": "Обнаружена система %s.", 5 | "installing": "Установка необходимых модулей...", 6 | "update_downloading": "Загрузка обновления...", 7 | "crt_workspace": "Создаются рабочие папки...", 8 | "verifying": "Проверка установки...", 9 | "installed": "%s успешно установлен!", 10 | "update_done": "Обновление успешно загружено! Пожалуйста, переместите все файлы из каталога %s в каталог скрипта", 11 | "not_installed": "%s не установлен!", 12 | "clear_qu": "Вы действительно хотите очистить папки?", 13 | "done": "Выполнено за %.2f секунд!", 14 | "done_qu": "Сделано?", 15 | "choice": "ВАШЕ ДЕЙСТВИЕ: ", 16 | "to_continue": "Нажмите Enter для продолжения...", 17 | "experimental": "Экспериментальная функция", 18 | 19 | "sc_label": "SC - Графика", 20 | "decode_sc": "Декодировать SC", 21 | "encode_sc": "Закодировать SC", 22 | "decode_by_parts": "Декодировать SC на части", 23 | "encode_by_parts": "Закодировать SC по частям", 24 | "overwrite_by_parts": "Закодировать SC по частям", 25 | "decode_sc_description": "Конвертирует SC в PNG", 26 | "encode_sc_description": "Конвертирует PNG в SC", 27 | "decode_by_parts_description": "Конвертирует SC в PNG и разрезает текстуру на части", 28 | "encode_by_parts_description": "Собирает части текстуры и конвертирует PNG в SC", 29 | "overwrite_by_parts_description": "Повторяет действие предыдущей функции, но учитывает папку 'overwrite'", 30 | 31 | "csv_label": "CSV - Таблицы", 32 | "decompress_csv": "Разжать CSV", 33 | "compress_csv": "Сжать CSV", 34 | "decompress_csv_description": "Разжимает файл таблиц", 35 | "compress_csv_description": "Сжимает файл таблиц", 36 | 37 | "ktx_label": "KTX", 38 | "ktx_from_png_label": "PNG2KTX", 39 | "png_from_ktx_label": "KTX2PNG", 40 | "ktx_from_png_description": "Конвертирует PNG в KTX", 41 | "png_from_ktx_description": "Конвертирует KTX в PNG", 42 | 43 | "other_features_label": "ПРОЧЕЕ", 44 | "check_update": "Проверить обновления", 45 | "reinit": "Повторная инициализация", 46 | "change_language": "Выбрать другой язык", 47 | "clear_directories": "Очистить рабочие папки", 48 | "toggle_update_auto_checking": "Переключить проверку обновлений", 49 | "exit": "Выход", 50 | "version": "Версия: %s", 51 | "reinit_description": "Если что-то пошло не так", 52 | "change_lang_description": "Текущий язык: %s", 53 | "clean_dirs_description": "Очистить In и Out папки", 54 | 55 | "not_latest": "Установлена не последняя версия", 56 | "collecting_inf": "Файл: %s. Сбор информации...", 57 | "about_sc": "О текстуре. Файл: %s (%d), Тип пикселя: %d, Размеры: %dx%d", 58 | "decompression_error": "Ошибка распаковки! Проведите повторную инициализацию!", 59 | "skip_not_installed": "%s не установлен! Пропускаем...", 60 | "detected_comp": "Обнаружено сжатие: %s", 61 | "unknown_pixel_type": "Неизвестный подтип: %s", 62 | "crt_pic": "Создаём картинку...", 63 | "join_pic": "Соединяем картинку...", 64 | "png_save": "Сохраняем в png...", 65 | "saved": "Сохранение прошло успешно!", 66 | "illegal_size": "Размер картинки не совпадает с оригиналом! Ожидалось %sx%s, но мы получили %sx%s", 67 | "resize_qu": "Хотите изменить размер?", 68 | "resizing": "Изменяем размер...", 69 | "split_pic": "Разделяем картинку...", 70 | "writing_pic": "Конвертируем пиксели...", 71 | "compressing_with": "Сохраняем с применением %s сжатия...", 72 | "compression_error": "Сжатие не удалось", 73 | "compression_done": "Сжатие прошло успешно!", 74 | "dir_empty": "Папка '%s' пуста!", 75 | "texture_not_found": "Текстуры для '%s' не найдена!", 76 | "file_not_found": "Файл '%s' не найден!", 77 | "cut_sprites_process": "Вырезаем спрайты... (%d/%d)", 78 | "place_sprites_process": "Ставим спрайты на место... (%d/%d)", 79 | "not_implemented": "Данная возможность будет добавлена в будущих обновлениях.\nЗа обновлениями XCoder вы можете следить здесь: {repo_url}", 80 | "error": "ОШИБКА! (%s.%s: %s)", 81 | "e1sc1": "Перезапись спрайтов", 82 | "cgl": "Список изменений: \n%s", 83 | "upd_av": "\nДоступно обновление!\nВерсия: %s\n", 84 | "upd_qu": "Обновить?", 85 | "upd": "Обновляем...", 86 | "upd_ck": "Проверка обновлений...", 87 | "bkp": "Копирование текущей версии...", 88 | "stp": "Установка...", 89 | "margin_qu": "Учитывать отступы от краев?", 90 | "enabled": "Включено", 91 | "disabled": "Выключено", 92 | 93 | "install_to_unlock": "Установите '%s' чтобы открыть больше функций!" 94 | } 95 | -------------------------------------------------------------------------------- /xcoder/languages/ua-UA.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Українська", 3 | "xcoder_header": "XCoder | Версія: %s | by Vorono4ka", 4 | "detected_os": "Виявлено %s ОС.", 5 | "installing": "Встановлюємо потрібні модулі...", 6 | "update_downloading": "Оновлення завантажується...", 7 | "crt_workspace": "Створюємо робочі папки...", 8 | "verifying": "Перевіряємо інсталяцію...", 9 | "installed": "%s був удало інстальований!", 10 | "update_done": "Оновлення було вдало завантажено! Будь ласка, перемістіть усі файли з %s до папки скрипту", 11 | "not_installed": "%s Не встановився!", 12 | "clear_qu": "Ти впевнений, що хочеш очистити робочі папки?", 13 | "done": "Виконано за %.2f секунди!", 14 | "done_qu": "Готово?", 15 | "choice": "Ваш вибір: ", 16 | "to_continue": "Натисніть Enter, щоб продовжити...", 17 | "experimental": "Експерементальна функція", 18 | 19 | "sc_label": "SC", 20 | "decode_sc": "Розпакувати SC", 21 | "encode_sc": "Запакувати SC", 22 | "decode_by_parts": "Розпакувати SC до спрайтів", 23 | "encode_by_parts": "Запакувати SC до спрайтів", 24 | "overwrite_by_parts": "Запакувати SC із спрайтів", 25 | "decode_sc_description": "Конвертує SC у PNG", 26 | "encode_sc_description": "Конвертує PNG у SC", 27 | "decode_by_parts_description": "Розпаковує SC у PNG і вирізає текстуру на спрайти", 28 | "encode_by_parts_description": "Запаковує спрайти на текстуру та перетворює PNG у SC", 29 | "overwrite_by_parts_description": "Робить те саме, що минуле, але враховує \"overwrite\" папку", 30 | 31 | "csv_label": "CSV", 32 | "decompress_csv": "Розпакувати CSV", 33 | "compress_csv": "Запакуати CSV", 34 | "decompress_csv_description": "Розпакувати CSV", 35 | "compress_csv_description": "Запакувати CSV", 36 | 37 | "ktx_label": "KTX", 38 | "ktx_from_png_label": "PNG2KTX", 39 | "png_from_ktx_label": "KTX2PNG", 40 | "ktx_from_png_description": "Конвертує PNG у KTX", 41 | "png_from_ktx_description": "Конвертує KTX у PNG", 42 | 43 | "other_features_label": "ІНШЕ", 44 | "check_update": "Шукати оновлення", 45 | "reinit": "Повторити налаштування", 46 | "change_language": "Вибрати іншу мову", 47 | "clear_directories": "Очистити робочі папки", 48 | "toggle_update_auto_checking": "Увімк/Вимк авто оновлення", 49 | "exit": "Вийти", 50 | "version": "версія: %s", 51 | "reinit_description": "Якщо щось пішло не так", 52 | "change_lang_description": "Мова: %s", 53 | "clean_dirs_description": "Очистити папки і файли в них", 54 | 55 | "not_latest": "Встановлена не остання версія", 56 | "collecting_inf": "Файл: %s. Збираємо інформацію...", 57 | "about_sc": "Про текстуру. Назва файлу: {filename} ({index}), Тип пікселів: {pixel_type}, Величина: {width}x{height}", 58 | "decompression_error": "Помилка при розпакуванні! Намагаємся розпакувати як є...", 59 | "skip_not_installed": "%s не встановлений, повторіть налаштування", 60 | "detected_comp": "Виявлено компресію: %s", 61 | "unknown_pixel_type": "Невідомий вид пікселів: %s", 62 | "crt_pic": "Створюємо зоображеня...", 63 | "join_pic": "З'єднуємо зоображення...", 64 | "png_save": "Зберігаємо PNG...", 65 | "saved": "Збереження Виконано!", 66 | "illegal_size": "Неможливий розмір картинки! Очікували %sx%s але отримали %sx%s", 67 | "resize_qu": "Ти хочеш змінити розмір картинки?", 68 | "resizing": "Змінюємо розмір...", 69 | "split_pic": "Розділюємо зоображення...", 70 | "writing_pic": "Записуємо пікселі...", 71 | "compressing_with": "Запаковуємо з %s...", 72 | "compression_error": "Запаковування не вдалося", 73 | "compression_done": "Запаковування виконане!", 74 | "dir_empty": "Папка '%s' порожня!", 75 | "texture_not_found": "Текстури для '%s' не знайдено!", 76 | "file_not_found": "Файл '%s' не знайдено!", 77 | "cut_sprites_process": "Обрізаємо спрайти... (%d/%d)", 78 | "place_sprites_process": "Вставляємо спрайти... (%d/%d)", 79 | "not_implemented": "Ця функція буде додана у наступних оновленнях.\nТи можеш сладкувати за оновленнями тут: {repo_url}", 80 | "error": "Помилка! (%s.%s: %s)", 81 | "e1sc1": "Переписати SC спрайти", 82 | "cgl": "Список змін:\n%s", 83 | "upd_av": "\nЗнайдено оновлення!\nВерсія: %s\n", 84 | "upd_qu": "Хочеш оновити?", 85 | "upd": "Триває оновлення...", 86 | "upd_ck": "Шукаємо оновлення...", 87 | "bkp": "Створюємо бекап теперішньої версії...", 88 | "stp": "Встановлюємо...", 89 | "margin_qu": "Враховувати бокові відступи?", 90 | "enabled": "Включено", 91 | "disabled": "Виключено", 92 | 93 | "install_to_unlock": "Встанови '%s' щоб розблокувати більше функцій!" 94 | } 95 | -------------------------------------------------------------------------------- /xcoder/languages/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "简体中文", 3 | "xcoder_header": "XCoder | 版本: %s | Vorono4ka制作 | OrangePig汉化", 4 | "detected_os": "检测到 %s 操作系统。", 5 | "installing": "正在安装所需模块...", 6 | "update_downloading": "更新下载中...", 7 | "crt_workspace": "正在创建工作空间目录...", 8 | "verifying": "正在验证安装...", 9 | "installed": "%s 安装成功!", 10 | "update_done": "更新下载成功!请将所有文件从目录%s移动到脚本目录", 11 | "not_installed": "%s 未安装!", 12 | "clear_qu": "确定要清空目录吗?", 13 | "done": "已完成,用时 %.2f 秒!", 14 | "done_qu": "完成?", 15 | "choice": "你的选择(输入数字后回车):", 16 | "to_continue": "按回车键继续...", 17 | "experimental": "实验性功能", 18 | 19 | "sc_label": "SC 文件(一般为 UI 或特效贴图)", 20 | "decode_sc": "SC 转 PNG", 21 | "encode_sc": "PNG 转 SC", 22 | "decode_by_parts": "SC 转 精灵图", 23 | "encode_by_parts": "精灵图 转 SC", 24 | "overwrite_by_parts": "精灵图 转 SC(覆盖)", 25 | "decode_sc_description": "将 SC 转换为一整张大的 PNG", 26 | "encode_sc_description": "将一整张大的 PNG 转换为 SC", 27 | "decode_by_parts_description": "将 SC 转换为 PNG 并按照成分切割", 28 | "encode_by_parts_description": "将一张张独立的精灵图组合好并转换为 SC", 29 | "overwrite_by_parts_description": "同上,但会 \"覆盖\" 文件夹", 30 | 31 | "csv_label": "CSV 文件(一般为各种数据)", 32 | "decompress_csv": "解压 CSV", 33 | "compress_csv": "压缩 CSV", 34 | "decompress_csv_description": "解压电子表格文件", 35 | "compress_csv_description": "压缩电子表格文件", 36 | 37 | "ktx_label": "KTX 文件(一般为模型贴图)", 38 | "ktx_from_png_label": "PNG 转 KTX", 39 | "png_from_ktx_label": "KTX 转 PNG", 40 | "ktx_from_png_description": "将 PNG 转换为 KTX", 41 | "png_from_ktx_description": "将 KTX 转换为 PNG", 42 | 43 | "other_features_label": "其他选项", 44 | "check_update": "检查更新", 45 | "reinit": "重新初始化", 46 | "change_language": "切换语言", 47 | "clear_directories": "清空工作空间目录", 48 | "toggle_update_auto_checking": "切换自动检查更新", 49 | "exit": "退出", 50 | "version": "当前版本:%s", 51 | "reinit_description": "如果出现问题,请重新初始化", 52 | "change_lang_description": "当前语言:%s", 53 | "clean_dirs_description": "清空输入和输出目录", 54 | 55 | "not_latest": "安装的不是最新版本", 56 | "collecting_inf": "文件:%s。正在收集信息...", 57 | "about_sc": "关于纹理。文件名:{filename} ({index}),像素类型:{pixel_type},尺寸:{width}x{height}", 58 | "decompression_error": "解压时出错!尝试直接解码...", 59 | "skip_not_installed": "%s 未安装!请重新初始化", 60 | "detected_comp": "检测到 %s 压缩", 61 | "unknown_pixel_type": "未知像素类型:%s", 62 | "crt_pic": "正在创建图片...", 63 | "join_pic": "正在合并图片...", 64 | "png_save": "正在保存为 png...", 65 | "saved": "保存完成!", 66 | "xcod_not_found": "文件 '%s.xcod' 不存在!", 67 | "illegal_size": "非法图片尺寸!预期为 %sx%s,但得到的是 %sx%s", 68 | "resize_qu": "是否需要调整图片尺寸?", 69 | "resizing": "正在调整大小...", 70 | "split_pic": "正在切割图片...", 71 | "writing_pic": "正在写入像素...", 72 | "compressing_with": "使用 %s 压缩纹理...", 73 | "compression_error": "压缩失败", 74 | "compression_done": "压缩完成!", 75 | "dir_empty": "目录 '%s' 为空!", 76 | "not_found": "文件 '%s' 未找到!", 77 | "cut_sprites_process": "正在裁剪精灵图... (%d/%d)", 78 | "place_sprites_process": "正在放置精灵图... (%d/%d)", 79 | "not_implemented": "此功能将在未来更新中添加。\n你可以在此处关注 XCoder 更新:{repo_url}", 80 | "error": "错误!(%s.%s: %s)", 81 | "e1sc1": "覆盖 SC 精灵图", 82 | "cgl": "更新日志:\n%s", 83 | "upd_av": "\n有可用更新!\n版本:%s\n", 84 | "upd_qu": "你要更新吗?", 85 | "upd": "正在更新...", 86 | "upd_ck": "正在检查更新...", 87 | "bkp": "正在备份当前版本...", 88 | "stp": "正在安装...", 89 | "margin_qu": "考虑边缘空白吗?", 90 | "enabled": "已启用", 91 | "disabled": "已禁用", 92 | 93 | "install_to_unlock": "安装 '%s' 以解锁更多功能!" 94 | } 95 | -------------------------------------------------------------------------------- /xcoder/localization.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | 5 | from xcoder.config import config 6 | 7 | _DEFAULT_STRING = "NO LOCALE" 8 | _LIBRARY_DIRECTORY = Path(__file__).parent 9 | _LOCALES_DIRECTORY = _LIBRARY_DIRECTORY / "languages" 10 | 11 | 12 | class Locale: 13 | def __init__(self): 14 | self.xcoder_header: str = _DEFAULT_STRING 15 | self.detected_os: str = _DEFAULT_STRING 16 | self.installing: str = _DEFAULT_STRING 17 | self.update_downloading: str = _DEFAULT_STRING 18 | self.crt_workspace: str = _DEFAULT_STRING 19 | self.verifying: str = _DEFAULT_STRING 20 | self.installed: str = _DEFAULT_STRING 21 | self.update_done: str = _DEFAULT_STRING 22 | self.not_installed: str = _DEFAULT_STRING 23 | self.clear_qu: str = _DEFAULT_STRING 24 | self.done: str = _DEFAULT_STRING 25 | self.done_qu: str = _DEFAULT_STRING 26 | self.choice: str = _DEFAULT_STRING 27 | self.to_continue: str = _DEFAULT_STRING 28 | self.experimental: str = _DEFAULT_STRING 29 | 30 | self.sc_label: str = _DEFAULT_STRING 31 | self.decode_sc: str = _DEFAULT_STRING 32 | self.encode_sc: str = _DEFAULT_STRING 33 | self.decode_by_parts: str = _DEFAULT_STRING 34 | self.encode_by_parts: str = _DEFAULT_STRING 35 | self.overwrite_by_parts: str = _DEFAULT_STRING 36 | self.decode_sc_description: str = _DEFAULT_STRING 37 | self.encode_sc_description: str = _DEFAULT_STRING 38 | self.decode_by_parts_description: str = _DEFAULT_STRING 39 | self.encode_by_parts_description: str = _DEFAULT_STRING 40 | self.overwrite_by_parts_description: str = _DEFAULT_STRING 41 | 42 | self.csv_label: str = _DEFAULT_STRING 43 | self.decompress_csv: str = _DEFAULT_STRING 44 | self.compress_csv: str = _DEFAULT_STRING 45 | self.decompress_csv_description: str = _DEFAULT_STRING 46 | self.compress_csv_description: str = _DEFAULT_STRING 47 | 48 | self.ktx_label: str = _DEFAULT_STRING 49 | self.ktx_from_png_label: str = _DEFAULT_STRING 50 | self.png_from_ktx_label: str = _DEFAULT_STRING 51 | self.ktx_from_png_description: str = _DEFAULT_STRING 52 | self.png_from_ktx_description: str = _DEFAULT_STRING 53 | 54 | self.other_features_label: str = _DEFAULT_STRING 55 | self.check_update: str = _DEFAULT_STRING 56 | self.reinit: str = _DEFAULT_STRING 57 | self.change_language: str = _DEFAULT_STRING 58 | self.clear_directories: str = _DEFAULT_STRING 59 | self.toggle_update_auto_checking: str = _DEFAULT_STRING 60 | self.exit: str = _DEFAULT_STRING 61 | self.version: str = _DEFAULT_STRING 62 | self.reinit_description: str = _DEFAULT_STRING 63 | self.change_lang_description: str = _DEFAULT_STRING 64 | self.clean_dirs_description: str = _DEFAULT_STRING 65 | 66 | self.not_latest: str = _DEFAULT_STRING 67 | self.collecting_inf: str = _DEFAULT_STRING 68 | self.about_sc: str = _DEFAULT_STRING 69 | self.skip_not_installed: str = _DEFAULT_STRING 70 | self.decompression_error: str = _DEFAULT_STRING 71 | self.detected_comp: str = _DEFAULT_STRING 72 | self.unknown_pixel_type: str = _DEFAULT_STRING 73 | self.crt_pic: str = _DEFAULT_STRING 74 | self.join_pic: str = _DEFAULT_STRING 75 | self.png_save: str = _DEFAULT_STRING 76 | self.saved: str = _DEFAULT_STRING 77 | self.illegal_size: str = _DEFAULT_STRING 78 | self.resize_qu: str = _DEFAULT_STRING 79 | self.resizing: str = _DEFAULT_STRING 80 | self.split_pic: str = _DEFAULT_STRING 81 | self.writing_pic: str = _DEFAULT_STRING 82 | self.compressing_with: str = _DEFAULT_STRING 83 | self.compression_error: str = _DEFAULT_STRING 84 | self.compression_done: str = _DEFAULT_STRING 85 | self.dir_empty: str = _DEFAULT_STRING 86 | self.texture_not_found: str = _DEFAULT_STRING 87 | self.file_not_found: str = _DEFAULT_STRING 88 | self.cut_sprites_process: str = _DEFAULT_STRING 89 | self.place_sprites_process: str = _DEFAULT_STRING 90 | self.not_implemented: str = _DEFAULT_STRING 91 | self.error: str = _DEFAULT_STRING 92 | 93 | self.e1sc1: str = _DEFAULT_STRING 94 | self.cgl: str = _DEFAULT_STRING 95 | self.upd_av: str = _DEFAULT_STRING 96 | self.upd_qu: str = _DEFAULT_STRING 97 | self.upd: str = _DEFAULT_STRING 98 | self.upd_ck: str = _DEFAULT_STRING 99 | self.bkp: str = _DEFAULT_STRING 100 | self.stp: str = _DEFAULT_STRING 101 | 102 | self.enabled: str = _DEFAULT_STRING 103 | self.disabled: str = _DEFAULT_STRING 104 | 105 | self.install_to_unlock: str = _DEFAULT_STRING 106 | 107 | def load(self, language: str): 108 | language_filepath = _LOCALES_DIRECTORY / (language + ".json") 109 | default_locale_filepath = _LOCALES_DIRECTORY / ( 110 | config.DEFAULT_LANGUAGE + ".json" 111 | ) 112 | 113 | loaded_locale = {} 114 | if os.path.exists(language_filepath): 115 | loaded_locale = json.load(open(language_filepath, encoding="utf-8")) # Any 116 | english_locale = json.load(open(default_locale_filepath)) # English 117 | 118 | for key in self.__dict__: 119 | if key in loaded_locale: 120 | setattr(self, key, loaded_locale[key]) 121 | continue 122 | setattr(self, key, english_locale[key]) 123 | 124 | def change(self): 125 | language_files = os.listdir(_LOCALES_DIRECTORY) 126 | 127 | for file_index, language_file in enumerate(language_files): 128 | language_path = _LOCALES_DIRECTORY / language_file 129 | language_name = json.load(open(language_path, encoding="utf-8"))["name"] 130 | 131 | print(f"{file_index + 1} - {language_name}") 132 | 133 | language_index = input("\n>>> ") 134 | try: 135 | language_index = int(language_index) - 1 136 | if language_index >= 0: 137 | if language_index < len(language_files): 138 | language = ".".join(language_files[language_index].split(".")[:-1]) 139 | self.load(language) 140 | 141 | return language 142 | except ValueError: 143 | pass 144 | 145 | return self.change() 146 | 147 | 148 | locale = Locale() 149 | locale.load(config.language) 150 | -------------------------------------------------------------------------------- /xcoder/main_menu.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from loguru import logger 4 | 5 | from xcoder import clear 6 | from xcoder.config import config 7 | from xcoder.console import Console 8 | from xcoder.features.directories import clear_directories 9 | from xcoder.features.initialization import initialize 10 | from xcoder.features.update.check import check_update, get_tags 11 | from xcoder.localization import locale 12 | from xcoder.menu import Menu 13 | 14 | menu = Menu() 15 | 16 | 17 | def check_auto_update(): 18 | if config.auto_update and time.time() - config.last_update > 60 * 60 * 24 * 7: 19 | check_update() 20 | config.last_update = int(time.time()) 21 | config.dump() 22 | 23 | 24 | def check_files_updated(): 25 | if config.has_update: 26 | logger.opt(colors=True).info(f"{locale.update_done % ''}") 27 | if Console.question(locale.done_qu): 28 | latest_tag = get_tags(config.repo_owner, config.repo_name)[0] 29 | latest_tag_name = latest_tag["name"][1:] 30 | 31 | config.has_update = False 32 | config.version = latest_tag_name 33 | config.last_update = int(time.time()) 34 | config.dump() 35 | else: 36 | exit() 37 | 38 | 39 | # noinspection PyUnresolvedReferences 40 | @logger.catch() 41 | def refill_menu(): 42 | menu.categories.clear() 43 | 44 | sc_category = Menu.Category(0, locale.sc_label) 45 | ktx_category = Menu.Category(1, locale.ktx_label) 46 | csv_category = Menu.Category(2, locale.csv_label) 47 | other = Menu.Category(10, locale.other_features_label) 48 | 49 | menu.add_category(sc_category) 50 | menu.add_category(ktx_category) 51 | menu.add_category(csv_category) 52 | menu.add_category(other) 53 | 54 | try: 55 | import sc_compression 56 | 57 | del sc_compression 58 | except ImportError: 59 | logger.warning(locale.install_to_unlock % "sc-compression") 60 | else: 61 | from xcoder.features.csv.compress import compress_csv 62 | from xcoder.features.csv.decompress import decompress_csv 63 | 64 | try: 65 | import PIL 66 | 67 | del PIL 68 | except ImportError: 69 | logger.warning(locale.install_to_unlock % "PILLOW") 70 | else: 71 | from xcoder.features.sc.decode import ( 72 | decode_and_render_objects, 73 | decode_textures_only, 74 | ) 75 | from xcoder.features.sc.encode import ( 76 | collect_objects_and_encode, 77 | encode_textures_only, 78 | ) 79 | 80 | sc_category.add( 81 | Menu.Item( 82 | name=locale.decode_sc, 83 | description=locale.decode_sc_description, 84 | handler=decode_textures_only, 85 | ) 86 | ) 87 | sc_category.add( 88 | Menu.Item( 89 | name=locale.encode_sc, 90 | description=locale.encode_sc_description, 91 | handler=encode_textures_only, 92 | ) 93 | ) 94 | sc_category.add( 95 | Menu.Item( 96 | name=locale.decode_by_parts, 97 | description=locale.decode_by_parts_description, 98 | handler=decode_and_render_objects, 99 | ) 100 | ) 101 | sc_category.add( 102 | Menu.Item( 103 | name=locale.encode_by_parts, 104 | description=locale.encode_by_parts_description, 105 | handler=collect_objects_and_encode, 106 | ) 107 | ) 108 | sc_category.add( 109 | Menu.Item( 110 | name=locale.overwrite_by_parts, 111 | description=locale.overwrite_by_parts_description, 112 | handler=lambda: collect_objects_and_encode(True), 113 | ) 114 | ) 115 | 116 | from xcoder.features.ktx import ( 117 | convert_ktx_textures_to_png, 118 | convert_png_textures_to_ktx, 119 | ) 120 | from xcoder.pvr_tex_tool import can_use_pvr_tex_tool 121 | 122 | if can_use_pvr_tex_tool(): 123 | ktx_category.add( 124 | Menu.Item( 125 | name=locale.png_from_ktx_label, 126 | description=locale.png_from_ktx_description, 127 | handler=convert_ktx_textures_to_png, 128 | ) 129 | ) 130 | ktx_category.add( 131 | Menu.Item( 132 | name=locale.ktx_from_png_label, 133 | description=locale.ktx_from_png_description, 134 | handler=convert_png_textures_to_ktx, 135 | ) 136 | ) 137 | 138 | csv_category.add( 139 | Menu.Item( 140 | name=locale.decompress_csv, 141 | description=locale.decompress_csv_description, 142 | handler=decompress_csv, 143 | ) 144 | ) 145 | csv_category.add( 146 | Menu.Item( 147 | name=locale.compress_csv, 148 | description=locale.compress_csv_description, 149 | handler=compress_csv, 150 | ) 151 | ) 152 | 153 | other.add( 154 | Menu.Item( 155 | name=locale.check_update, 156 | description=locale.version % config.version, 157 | handler=check_update, 158 | ) 159 | ) 160 | other.add( 161 | Menu.Item( 162 | name=locale.reinit, 163 | description=locale.reinit_description, 164 | handler=lambda: (initialize(), refill_menu()), 165 | ) 166 | ) 167 | other.add( 168 | Menu.Item( 169 | name=locale.change_language, 170 | description=locale.change_lang_description % config.language, 171 | handler=lambda: (config.change_language(locale.change()), refill_menu()), 172 | ) 173 | ) 174 | other.add( 175 | Menu.Item( 176 | name=locale.clear_directories, 177 | description=locale.clean_dirs_description, 178 | handler=lambda: ( 179 | clear_directories() if Console.question(locale.clear_qu) else -1 180 | ), 181 | ) 182 | ) 183 | other.add( 184 | Menu.Item( 185 | name=locale.toggle_update_auto_checking, 186 | description=locale.enabled if config.auto_update else locale.disabled, 187 | handler=lambda: (config.toggle_auto_update(), refill_menu()), 188 | ) 189 | ) 190 | other.add(Menu.Item(name=locale.exit, handler=lambda: (clear(), exit()))) 191 | -------------------------------------------------------------------------------- /xcoder/math/__init__.py: -------------------------------------------------------------------------------- 1 | from .point import Point 2 | from .polygon import PointOrder, Polygon, get_polygon_point_order 3 | 4 | __all__ = ["Point", "PointOrder", "Polygon", "get_polygon_point_order"] 5 | -------------------------------------------------------------------------------- /xcoder/math/point.py: -------------------------------------------------------------------------------- 1 | class Point: 2 | def __init__(self, x: float = 0, y: float = 0): 3 | self.x: float = x 4 | self.y: float = y 5 | 6 | def __eq__(self, other): 7 | if isinstance(other, Point): 8 | return self.x == other.x and self.y == other.y 9 | return False 10 | 11 | def __mul__(self, other): 12 | if isinstance(other, int): 13 | self.x *= other 14 | self.y *= other 15 | if isinstance(other, float): 16 | self.x *= other 17 | self.y *= other 18 | 19 | return self 20 | 21 | def __add__(self, other): 22 | if isinstance(other, Point): 23 | self.x += other.x 24 | self.y += other.y 25 | return self 26 | 27 | def __sub__(self, other): 28 | return self + -other 29 | 30 | def __neg__(self): 31 | self.x *= -1 32 | self.y *= -1 33 | return self 34 | 35 | def __repr__(self): 36 | return str(self.as_tuple()) 37 | 38 | def as_tuple(self) -> tuple[float, float]: 39 | return self.x, self.y 40 | -------------------------------------------------------------------------------- /xcoder/math/polygon.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from math import atan2, degrees 3 | from typing import TypeAlias 4 | 5 | from xcoder.math.point import Point 6 | from xcoder.math.rect import Rect 7 | from xcoder.matrices import Matrix2x3 8 | 9 | Polygon: TypeAlias = list[Point] 10 | 11 | 12 | class PointOrder(IntEnum): 13 | CLOCKWISE = 0 14 | COUNTER_CLOCKWISE = 1 15 | 16 | 17 | def get_polygon_sum_of_edges(polygon: Polygon) -> float: 18 | """ 19 | Mostly like signed area, but two times bigger and more accurate with signs. 20 | 21 | https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order/1165943#1165943 22 | 23 | :param polygon: 24 | :return: 25 | """ 26 | points_sum = 0 27 | for i in range(len(polygon)): 28 | p1 = polygon[i] 29 | p2 = polygon[(i + 1) % len(polygon)] 30 | 31 | points_sum += (p2.x - p1.x) * (p1.y + p2.y) 32 | return points_sum 33 | 34 | 35 | def get_polygon_point_order(polygon: Polygon) -> PointOrder | None: 36 | sum_of_edges = get_polygon_sum_of_edges(polygon) 37 | if sum_of_edges > 0: 38 | return PointOrder.CLOCKWISE 39 | elif sum_of_edges < 0: 40 | return PointOrder.COUNTER_CLOCKWISE 41 | 42 | return None 43 | 44 | 45 | def compare_polygons( 46 | polygon1: Polygon, 47 | polygon2: Polygon, 48 | ) -> tuple[float, bool]: 49 | """Calculates rotation and if polygon is mirrored. 50 | 51 | :param polygon1: shape polygon 52 | :param polygon2: sheet polygon 53 | :return: rotation degrees, is polygon mirrored 54 | """ 55 | 56 | polygon1_order = get_polygon_point_order(polygon1) 57 | polygon2_order = get_polygon_point_order(polygon2) 58 | 59 | mirroring = polygon1_order != polygon2_order 60 | 61 | dx = (polygon1[1].x - polygon1[0].x) * (-1 if mirroring else 1) 62 | dy = polygon1[1].y - polygon1[0].y 63 | du = polygon2[1].x - polygon2[0].x 64 | dv = polygon2[1].y - polygon2[0].y 65 | 66 | # Solution from https://stackoverflow.com/a/21484228/14915825 67 | angle_radians = atan2(dy, dx) - atan2(dv, du) 68 | angle = degrees(angle_radians) 69 | 70 | return angle, mirroring 71 | 72 | 73 | def get_rect(polygon: list[Point]) -> Rect: 74 | """Calculates polygon bounds and returns rect. 75 | 76 | :param polygon: polygon points 77 | :return: Rect object 78 | """ 79 | 80 | rect = Rect(left=100000, top=100000, right=-100000, bottom=-100000) 81 | 82 | for point in polygon: 83 | rect.add_point(point.x, point.y) 84 | 85 | return rect 86 | 87 | 88 | def apply_matrix(polygon: Polygon, matrix: Matrix2x3 | None = None) -> Polygon: 89 | """Applies affine matrix to the given polygon. If matrix is none, returns points. 90 | 91 | :param polygon: polygon points 92 | :param matrix: Affine matrix 93 | """ 94 | 95 | if matrix is None: 96 | return polygon 97 | 98 | return [ 99 | Point( 100 | matrix.apply_x(point.x, point.y), 101 | matrix.apply_y(point.x, point.y), 102 | ) 103 | for point in polygon 104 | ] 105 | -------------------------------------------------------------------------------- /xcoder/math/rect.py: -------------------------------------------------------------------------------- 1 | from typing import Self 2 | 3 | 4 | class Rect: 5 | def __init__( 6 | self, *, left: float = 0, top: float = 0, right: float = 0, bottom: float = 0 7 | ): 8 | self.left: float = left 9 | self.top: float = top 10 | self.right: float = right 11 | self.bottom: float = bottom 12 | 13 | @property 14 | def width(self) -> float: 15 | return self.right - self.left 16 | 17 | @property 18 | def height(self) -> float: 19 | return self.bottom - self.top 20 | 21 | def as_tuple(self) -> tuple[float, float, float, float]: 22 | return self.left, self.top, self.right, self.bottom 23 | 24 | def add_point(self, x: float, y: float): 25 | if x < self.left: 26 | self.left = x 27 | if x > self.right: 28 | self.right = x 29 | if y < self.top: 30 | self.top = y 31 | if y > self.bottom: 32 | self.bottom = y 33 | 34 | def merge_bounds(self, other: Self): 35 | if other.left < self.left: 36 | self.left = other.left 37 | if other.right > self.right: 38 | self.right = other.right 39 | if other.top < self.top: 40 | self.top = other.top 41 | if other.bottom > self.bottom: 42 | self.bottom = other.bottom 43 | 44 | def __str__(self): 45 | return f"Rect{self.left, self.top, self.right, self.bottom}" 46 | -------------------------------------------------------------------------------- /xcoder/matrices/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["MatrixBank", "Matrix2x3", "ColorTransform"] 2 | 3 | from .color_transform import ColorTransform 4 | from .matrix2x3 import Matrix2x3 5 | from .matrix_bank import MatrixBank 6 | -------------------------------------------------------------------------------- /xcoder/matrices/color_transform.py: -------------------------------------------------------------------------------- 1 | class ColorTransform: 2 | pass 3 | -------------------------------------------------------------------------------- /xcoder/matrices/matrix2x3.py: -------------------------------------------------------------------------------- 1 | from math import atan2, cos, degrees, hypot, sin 2 | from typing import Self 3 | 4 | from xcoder.bytestream import Reader 5 | 6 | DEFAULT_MULTIPLIER = 1024 7 | PRECISE_MULTIPLIER = 65535 8 | 9 | 10 | class Matrix2x3: 11 | """ 12 | self matrix looks like: 13 | [a c x] 14 | [b d y] 15 | """ 16 | 17 | def __init__(self, matrix: Self | None = None): 18 | self.a: float = 1 19 | self.b: float = 0 20 | self.c: float = 0 21 | self.d: float = 1 22 | self.x: float = 0 23 | self.y: float = 0 24 | 25 | if matrix is not None: 26 | self.a = matrix.a 27 | self.b = matrix.b 28 | self.c = matrix.c 29 | self.d = matrix.d 30 | self.x = matrix.x 31 | self.y = matrix.y 32 | 33 | def load(self, reader: Reader, tag: int): 34 | divider: int 35 | if tag == 8: 36 | divider = DEFAULT_MULTIPLIER 37 | elif tag == 36: 38 | divider = PRECISE_MULTIPLIER 39 | else: 40 | raise ValueError(f"Unsupported matrix tag: {tag}") 41 | 42 | self.a = reader.read_int() / divider 43 | self.b = reader.read_int() / divider 44 | self.c = reader.read_int() / divider 45 | self.d = reader.read_int() / divider 46 | self.x = reader.read_twip() 47 | self.y = reader.read_twip() 48 | 49 | def apply_x(self, x: float, y: float): 50 | return x * self.a + y * self.c + self.x 51 | 52 | def apply_y(self, x: float, y: float): 53 | return y * self.d + x * self.b + self.y 54 | 55 | def multiply(self, matrix: Self) -> Self: 56 | a = (self.a * matrix.a) + (self.b * matrix.c) 57 | b = (self.a * matrix.b) + (self.b * matrix.d) 58 | c = (self.d * matrix.d) + (self.c * matrix.b) 59 | d = (self.d * matrix.c) + (self.c * matrix.a) 60 | x = matrix.apply_x(self.x, self.y) 61 | y = matrix.apply_y(self.x, self.y) 62 | 63 | self.a = a 64 | self.b = b 65 | self.d = c 66 | self.c = d 67 | self.x = x 68 | self.y = y 69 | 70 | return self 71 | 72 | def get_angle_radians(self) -> float: 73 | theta = atan2(self.b, self.a) 74 | return theta 75 | 76 | def get_angle(self) -> float: 77 | return degrees(self.get_angle_radians()) 78 | 79 | def get_scale(self) -> tuple[float, float]: 80 | scale_x = hypot(self.a, self.b) 81 | 82 | theta = self.get_angle_radians() 83 | sin_theta = sin(theta) 84 | 85 | scale_y = self.d / cos(theta) if abs(sin_theta) <= 0.01 else self.c / sin_theta 86 | 87 | return scale_x, scale_y 88 | 89 | def __str__(self): 90 | return f"Matrix2x3{self.a, self.b, self.c, self.d, self.x, self.y}" 91 | -------------------------------------------------------------------------------- /xcoder/matrices/matrix_bank.py: -------------------------------------------------------------------------------- 1 | from xcoder.matrices.color_transform import ColorTransform 2 | from xcoder.matrices.matrix2x3 import Matrix2x3 3 | 4 | 5 | class MatrixBank: 6 | def __init__(self): 7 | self._matrices: list[Matrix2x3] = [] 8 | self._color_transforms: list[ColorTransform] = [] 9 | 10 | def init(self, matrix_count: int, color_transform_count: int): 11 | self._matrices = [] 12 | for i in range(matrix_count): 13 | self._matrices.append(Matrix2x3()) 14 | 15 | self._color_transforms = [] 16 | for i in range(color_transform_count): 17 | self._color_transforms.append(ColorTransform()) 18 | 19 | def get_matrix(self, index: int) -> Matrix2x3: 20 | return self._matrices[index] 21 | 22 | def get_color_transform(self, index: int) -> ColorTransform: 23 | return self._color_transforms[index] 24 | -------------------------------------------------------------------------------- /xcoder/menu.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import textwrap 3 | import typing 4 | 5 | import colorama 6 | 7 | from xcoder.config import config 8 | from xcoder.localization import locale 9 | 10 | 11 | def print_feature( 12 | feature_id: int, name: str, description: str | None = None, console_width: int = -1 13 | ): 14 | text = f" {feature_id} {name}" 15 | if description: 16 | text += " " * (console_width // 2 - len(text)) + ": " + description 17 | 18 | print(textwrap.fill(text, console_width)) 19 | 20 | 21 | def print_category(text: str, background_width: int = 10): 22 | print( 23 | colorama.Back.GREEN 24 | + colorama.Fore.BLACK 25 | + text 26 | + " " * (background_width - len(text)) 27 | + colorama.Style.RESET_ALL 28 | ) 29 | 30 | 31 | class Menu: 32 | class Item: 33 | def __init__( 34 | self, 35 | *, 36 | name: str, 37 | handler: typing.Callable, 38 | description: str | None = None, 39 | ): 40 | self.name: str = name 41 | self.description: str | None = description 42 | self.handler: typing.Callable = handler 43 | 44 | class Category: 45 | def __init__(self, _id: int, name: str): 46 | self.id = _id 47 | self.name = name 48 | self.items = [] 49 | 50 | def item(self, name: str, description: str | None = None): 51 | def wrapper(handler: typing.Callable): 52 | self.add(Menu.Item(name=name, handler=handler, description=description)) 53 | 54 | return wrapper 55 | 56 | def add(self, item): 57 | self.items.append(item) 58 | 59 | def __init__(self): 60 | self.categories = [] 61 | 62 | def add_category(self, category): 63 | self.categories.append(category) 64 | return category 65 | 66 | def choice(self): 67 | console_width = shutil.get_terminal_size().columns 68 | print( 69 | ( 70 | colorama.Back.BLACK 71 | + colorama.Fore.GREEN 72 | + locale.xcoder_header % config.version 73 | + colorama.Style.RESET_ALL 74 | ).center(console_width + 12) 75 | ) 76 | print(config.get_repo_url().center(console_width - 1)) 77 | self._print_divider_line(console_width) 78 | 79 | for category in self.categories: 80 | if len(category.items) == 0: 81 | continue 82 | 83 | print_category(category.name) 84 | for item_index in range(len(category.items)): 85 | item = category.items[item_index] 86 | print_feature( 87 | category.id * 10 + item_index + 1, 88 | item.name, 89 | item.description, 90 | console_width, 91 | ) 92 | self._print_divider_line(console_width) 93 | 94 | choice = input(locale.choice) 95 | try: 96 | choice = int(choice) - 1 97 | if choice < 0: 98 | return None 99 | except ValueError: 100 | return None 101 | self._print_divider_line(console_width) 102 | 103 | category_id = choice // 10 104 | item_index = choice % 10 105 | for category in self.categories: 106 | if category.id == category_id: 107 | if len(category.items) > item_index: 108 | item = category.items[item_index] 109 | return item.handler 110 | break 111 | return None 112 | 113 | @staticmethod 114 | def _print_divider_line(console_width: int) -> None: 115 | print((console_width - 1) * "-") 116 | -------------------------------------------------------------------------------- /xcoder/objects/__init__.py: -------------------------------------------------------------------------------- 1 | from .movie_clip import MovieClip 2 | from .shape import Shape 3 | from .texture import SWFTexture 4 | 5 | __all__ = ["Shape", "MovieClip", "SWFTexture"] 6 | -------------------------------------------------------------------------------- /xcoder/objects/movie_clip/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["MovieClip", "MovieClipFrame"] 2 | 3 | from xcoder.objects.movie_clip.movie_clip import MovieClip 4 | from xcoder.objects.movie_clip.movie_clip_frame import MovieClipFrame 5 | -------------------------------------------------------------------------------- /xcoder/objects/movie_clip/movie_clip.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from ..plain_object import PlainObject 6 | from .movie_clip_frame import MovieClipFrame 7 | 8 | if TYPE_CHECKING: 9 | from xcoder.swf import SupercellSWF 10 | 11 | 12 | CACHE = {} 13 | 14 | 15 | class MovieClip(PlainObject): 16 | def __init__(self): 17 | super().__init__() 18 | 19 | self.id = -1 20 | self.export_name: str | None = None 21 | self.fps: int = 30 22 | self.frame_count: int = 0 23 | self.frames: list[MovieClipFrame] = [] 24 | self.frame_elements: list[tuple[int, int, int]] = [] 25 | self.blends: list[int] = [] 26 | self.binds: list[int] = [] 27 | self.matrix_bank_index: int = 0 28 | 29 | def load(self, swf: SupercellSWF, tag: int): 30 | assert swf.reader is not None 31 | 32 | self.id = swf.reader.read_ushort() 33 | 34 | self.fps = swf.reader.read_char() 35 | self.frame_count = swf.reader.read_ushort() 36 | 37 | if tag in (3, 14): 38 | pass 39 | else: 40 | if tag == 49: 41 | swf.reader.read_char() # unknown 42 | 43 | transforms_count = swf.reader.read_uint() 44 | 45 | for i in range(transforms_count): 46 | child_index = swf.reader.read_ushort() 47 | matrix_index = swf.reader.read_ushort() 48 | color_transform_index = swf.reader.read_ushort() 49 | 50 | self.frame_elements.append( 51 | (child_index, matrix_index, color_transform_index) 52 | ) 53 | 54 | binds_count = swf.reader.read_ushort() 55 | 56 | for i in range(binds_count): 57 | bind_id = swf.reader.read_ushort() # bind_id 58 | self.binds.append(bind_id) 59 | 60 | if tag in (12, 35, 49): 61 | for i in range(binds_count): 62 | blend = swf.reader.read_char() # blend 63 | self.blends.append(blend) 64 | 65 | for i in range(binds_count): 66 | swf.reader.read_string() # bind_name 67 | 68 | elements_used = 0 69 | 70 | while True: 71 | frame_tag = swf.reader.read_uchar() 72 | frame_length = swf.reader.read_int() 73 | 74 | if frame_tag == 0: 75 | break 76 | 77 | if frame_tag == 11: 78 | frame = MovieClipFrame() 79 | frame.load(swf.reader) 80 | frame.set_elements( 81 | self.frame_elements[ 82 | elements_used : elements_used + frame.get_elements_count() 83 | ] 84 | ) 85 | self.frames.append(frame) 86 | 87 | elements_used += frame.get_elements_count() 88 | elif frame_tag == 41: 89 | self.matrix_bank_index = swf.reader.read_uchar() 90 | else: 91 | swf.reader.read(frame_length) 92 | -------------------------------------------------------------------------------- /xcoder/objects/movie_clip/movie_clip_frame.py: -------------------------------------------------------------------------------- 1 | from xcoder.bytestream import Reader 2 | 3 | 4 | class MovieClipFrame: 5 | def __init__(self): 6 | self._elements_count: int = 0 7 | self._label: str | None = None 8 | 9 | self._elements: list[tuple[int, int, int]] = [] 10 | 11 | def load(self, reader: Reader) -> None: 12 | self._elements_count = reader.read_short() 13 | self._label = reader.read_string() 14 | 15 | def get_elements_count(self) -> int: 16 | return self._elements_count 17 | 18 | def set_elements(self, elements: list[tuple[int, int, int]]) -> None: 19 | self._elements = elements 20 | 21 | def get_elements(self) -> list[tuple[int, int, int]]: 22 | return self._elements 23 | 24 | def get_element(self, index: int) -> tuple[int, int, int]: 25 | return self._elements[index] 26 | -------------------------------------------------------------------------------- /xcoder/objects/plain_object.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from xcoder.swf import SupercellSWF 8 | 9 | 10 | class PlainObject(ABC): 11 | @abstractmethod 12 | def load(self, swf: SupercellSWF, tag: int): ... 13 | -------------------------------------------------------------------------------- /xcoder/objects/renderable/__init__.py: -------------------------------------------------------------------------------- 1 | from xcoder.objects.renderable.renderable_factory import create_renderable_from_plain 2 | 3 | __all__ = ["create_renderable_from_plain"] 4 | -------------------------------------------------------------------------------- /xcoder/objects/renderable/display_object.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | 5 | from PIL import Image 6 | 7 | from xcoder.math.rect import Rect 8 | from xcoder.matrices import ColorTransform, Matrix2x3 9 | 10 | 11 | class DisplayObject(ABC): 12 | def __init__(self): 13 | self._matrix = Matrix2x3() 14 | self._color_transform = ColorTransform() 15 | 16 | @abstractmethod 17 | def calculate_bounds(self, matrix: Matrix2x3) -> Rect: ... 18 | 19 | @abstractmethod 20 | def render(self, matrix: Matrix2x3) -> Image.Image: ... 21 | 22 | def set_matrix(self, matrix: Matrix2x3): 23 | self._matrix = matrix 24 | 25 | def set_color_transform(self, color_transform: ColorTransform): 26 | self._color_transform = color_transform 27 | -------------------------------------------------------------------------------- /xcoder/objects/renderable/renderable_factory.py: -------------------------------------------------------------------------------- 1 | from xcoder.objects import Shape 2 | from xcoder.objects.movie_clip.movie_clip import MovieClip 3 | from xcoder.objects.plain_object import PlainObject 4 | from xcoder.objects.renderable.display_object import DisplayObject 5 | from xcoder.objects.renderable.renderable_movie_clip import RenderableMovieClip 6 | from xcoder.objects.renderable.renderable_shape import RenderableShape 7 | from xcoder.swf import SupercellSWF 8 | 9 | 10 | def create_renderable_from_plain( 11 | swf: SupercellSWF, plain_object: PlainObject 12 | ) -> DisplayObject: 13 | if isinstance(plain_object, Shape): 14 | return RenderableShape(plain_object) 15 | if isinstance(plain_object, MovieClip): 16 | children = [] 17 | 18 | for bind_id in plain_object.binds: 19 | bind_object = swf.get_display_object(bind_id) 20 | 21 | display_object = None 22 | if bind_object is not None: 23 | display_object = create_renderable_from_plain(swf, bind_object) 24 | 25 | children.append(display_object) 26 | 27 | return RenderableMovieClip.create_from_plain(swf, plain_object, children) 28 | 29 | raise Exception(f"Unsupported object type: {plain_object}") 30 | -------------------------------------------------------------------------------- /xcoder/objects/renderable/renderable_movie_clip.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from PIL import Image 6 | 7 | from xcoder.math.rect import Rect 8 | from xcoder.matrices import ColorTransform, Matrix2x3, MatrixBank 9 | from xcoder.objects.movie_clip.movie_clip import MovieClip 10 | from xcoder.objects.movie_clip.movie_clip_frame import MovieClipFrame 11 | from xcoder.objects.renderable.display_object import DisplayObject 12 | 13 | if TYPE_CHECKING: 14 | from xcoder.swf import SupercellSWF 15 | 16 | 17 | class RenderableMovieClip(DisplayObject): 18 | def __init__(self): 19 | super().__init__() 20 | 21 | self._id = -1 22 | self._export_name: str | None = None 23 | self._fps: int = 30 24 | self._frame_count: int = 0 25 | self._frames: list[MovieClipFrame] = [] 26 | self._frame_elements: list[tuple[int, int, int]] = [] 27 | self._blends: list[int] = [] 28 | self._binds: list[int] = [] 29 | self._matrix_bank: MatrixBank | None = None 30 | 31 | self._children: list[DisplayObject] = [] 32 | self._frame_children: list[DisplayObject] = [] 33 | 34 | @staticmethod 35 | def create_from_plain( 36 | swf: SupercellSWF, movie_clip: MovieClip, children: list[DisplayObject] 37 | ) -> RenderableMovieClip: 38 | clip = RenderableMovieClip() 39 | 40 | clip._id = movie_clip.id 41 | clip._matrix_bank = swf.get_matrix_bank(movie_clip.matrix_bank_index) 42 | 43 | clip._export_name = movie_clip.export_name 44 | clip._fps = movie_clip.fps 45 | clip._frame_count = movie_clip.frame_count 46 | clip._frames = movie_clip.frames 47 | clip._frame_elements = movie_clip.frame_elements 48 | clip._blends = movie_clip.blends 49 | clip._binds = movie_clip.binds 50 | clip._children = children 51 | 52 | clip.set_frame(0) 53 | 54 | return clip 55 | 56 | def render(self, matrix: Matrix2x3) -> Image.Image: 57 | matrix_multiplied = Matrix2x3(self._matrix) 58 | matrix_multiplied.multiply(matrix) 59 | 60 | bounds = self.calculate_bounds(matrix) 61 | 62 | image = Image.new("RGBA", (int(bounds.width), int(bounds.height))) 63 | 64 | for child in self._frame_children: 65 | rendered_child = child.render(matrix_multiplied) 66 | child_bounds = child.calculate_bounds(matrix_multiplied) 67 | 68 | x = int(child_bounds.left - bounds.left) 69 | y = int(child_bounds.top - bounds.top) 70 | 71 | image.paste(rendered_child, (x, y), rendered_child) 72 | 73 | return image 74 | 75 | def calculate_bounds(self, matrix: Matrix2x3) -> Rect: 76 | matrix_multiplied = Matrix2x3(self._matrix) 77 | matrix_multiplied.multiply(matrix) 78 | 79 | rect = Rect() 80 | 81 | for child in self._frame_children: 82 | rect.merge_bounds(child.calculate_bounds(matrix_multiplied)) 83 | 84 | rect = Rect( 85 | left=round(rect.left), 86 | top=round(rect.top), 87 | right=round(rect.right), 88 | bottom=round(rect.bottom), 89 | ) 90 | 91 | return rect 92 | 93 | def set_frame(self, frame_index: int): 94 | assert self._matrix_bank is not None 95 | 96 | self._frame_children = [] 97 | 98 | frame = self._frames[frame_index] 99 | for child_index, matrix_index, color_transform_index in frame.get_elements(): 100 | matrix = Matrix2x3() 101 | if matrix_index != 0xFFFF: 102 | matrix = self._matrix_bank.get_matrix(matrix_index) 103 | 104 | color_transform = ColorTransform() 105 | if color_transform_index != 0xFFFF: 106 | color_transform = self._matrix_bank.get_color_transform( 107 | color_transform_index 108 | ) 109 | 110 | child = self._children[child_index] 111 | if child is None: 112 | continue 113 | 114 | child.set_matrix(matrix) 115 | child.set_color_transform(color_transform) 116 | 117 | self._frame_children.append(child) 118 | -------------------------------------------------------------------------------- /xcoder/objects/renderable/renderable_shape.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from PIL import Image 4 | 5 | from xcoder.math.rect import Rect 6 | from xcoder.matrices import Matrix2x3 7 | from xcoder.objects import Shape 8 | from xcoder.objects.renderable.display_object import DisplayObject 9 | 10 | 11 | class RenderableShape(DisplayObject): 12 | def __init__(self, shape: Shape): 13 | super().__init__() 14 | 15 | self._id = shape.id 16 | self._regions = shape.regions 17 | 18 | def render(self, matrix: Matrix2x3) -> Image.Image: 19 | matrix_multiplied = Matrix2x3(self._matrix) 20 | matrix_multiplied.multiply(matrix) 21 | 22 | bounds = self.calculate_bounds(matrix) 23 | 24 | image = Image.new("RGBA", (int(bounds.width), int(bounds.height))) 25 | 26 | for region in self._regions: 27 | rendered_region = region.render(matrix_multiplied) 28 | region_bounds = region.calculate_bounds(matrix_multiplied) 29 | 30 | x = int(region_bounds.left - bounds.left) 31 | y = int(region_bounds.top - bounds.top) 32 | 33 | image.paste(rendered_region, (x, y), rendered_region) 34 | 35 | return image 36 | 37 | def calculate_bounds(self, matrix: Matrix2x3) -> Rect: 38 | matrix_multiplied = Matrix2x3(self._matrix) 39 | matrix_multiplied.multiply(matrix) 40 | 41 | rect = Rect() 42 | 43 | for region in self._regions: 44 | rect.merge_bounds(region.calculate_bounds(matrix_multiplied)) 45 | 46 | rect = Rect( 47 | left=round(rect.left), 48 | top=round(rect.top), 49 | right=round(rect.right), 50 | bottom=round(rect.bottom), 51 | ) 52 | 53 | return rect 54 | -------------------------------------------------------------------------------- /xcoder/objects/shape/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Shape", "Region"] 2 | 3 | from .region import Region 4 | from .shape import Shape 5 | -------------------------------------------------------------------------------- /xcoder/objects/shape/region.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from PIL import Image 6 | 7 | from xcoder.images import create_filled_polygon_image 8 | from xcoder.math.point import Point 9 | from xcoder.math.polygon import apply_matrix, compare_polygons, get_rect 10 | from xcoder.math.rect import Rect 11 | from xcoder.matrices import Matrix2x3 12 | 13 | if TYPE_CHECKING: 14 | from xcoder.objects import SWFTexture 15 | from xcoder.swf import SupercellSWF 16 | 17 | 18 | class Region: 19 | def __init__(self): 20 | self.texture_index: int = 0 21 | 22 | self._texture: SWFTexture | None = None 23 | self._point_count = 0 24 | self._xy_points: list[Point] = [] 25 | self._uv_points: list[Point] = [] 26 | 27 | self._cache_image: Image.Image | None = None 28 | 29 | def load(self, swf: SupercellSWF, tag: int): 30 | assert swf.reader is not None 31 | 32 | self.texture_index = swf.reader.read_uchar() 33 | 34 | self._texture = swf.textures[self.texture_index] 35 | 36 | self._point_count = 4 37 | if tag != 4: 38 | self._point_count = swf.reader.read_uchar() 39 | 40 | self._xy_points: list[Point] = [Point() for _ in range(self._point_count)] 41 | self._uv_points: list[Point] = [Point() for _ in range(self._point_count)] 42 | 43 | for i in range(self._point_count): 44 | x = swf.reader.read_int() / 20 45 | y = swf.reader.read_int() / 20 46 | 47 | self._xy_points[i] = Point(x, y) 48 | 49 | for i in range(self._point_count): 50 | if tag == 4: 51 | u = swf.reader.read_ushort() * 0xFFFF / self._texture.width 52 | v = swf.reader.read_ushort() * 0xFFFF / self._texture.height 53 | else: 54 | u = swf.reader.read_ushort() * self._texture.width / 0xFFFF 55 | v = swf.reader.read_ushort() * self._texture.height / 0xFFFF 56 | 57 | self._uv_points[i] = Point(u, v) * (0.5 if swf.use_lowres_texture else 1) 58 | 59 | def render(self, matrix: Matrix2x3) -> Image.Image: 60 | transformed_points = apply_matrix(self._xy_points, matrix) 61 | 62 | rect = self.calculate_bounds(matrix) 63 | width, height = max(int(rect.width), 1), max(int(rect.height), 1) 64 | 65 | rendered_region = self.get_image() 66 | if rendered_region.width + rendered_region.height <= 2: 67 | fill_color: int = rendered_region.getpixel((0, 0)) # type: ignore 68 | 69 | return create_filled_polygon_image( 70 | rendered_region.mode, width, height, transformed_points, fill_color 71 | ) 72 | 73 | self.rotation, self.is_mirrored = compare_polygons( 74 | transformed_points, self._uv_points 75 | ) 76 | 77 | rendered_region = rendered_region.rotate(-self.rotation, expand=True) 78 | if self.is_mirrored: 79 | rendered_region = rendered_region.transpose(Image.Transpose.FLIP_LEFT_RIGHT) 80 | 81 | return rendered_region.resize((width, height), Image.Resampling.BILINEAR) 82 | 83 | def get_image(self) -> Image.Image: 84 | # Note: it's 100% safe and very helpful for rendering movie clips 85 | if self._cache_image is not None: 86 | return self._cache_image 87 | 88 | assert self._texture is not None and self._texture.image is not None 89 | 90 | rect = get_rect(self._uv_points) 91 | 92 | width = max(int(rect.width), 1) 93 | height = max(int(rect.height), 1) 94 | if width + height <= 2: # The same speed as without this return 95 | return Image.new( 96 | self._texture.image.mode, 97 | (1, 1), 98 | color=self._texture.image.getpixel((int(rect.left), int(rect.top))), 99 | ) 100 | 101 | mask_image = create_filled_polygon_image( 102 | "L", self._texture.width, self._texture.height, self._uv_points, 0xFF 103 | ) 104 | 105 | rendered_region = Image.new(self._texture.image.mode, (width, height)) 106 | rendered_region.paste( 107 | self._texture.image.crop(rect.as_tuple()), 108 | (0, 0), 109 | mask_image.crop(rect.as_tuple()), 110 | ) 111 | rendered_region = rendered_region.convert("RGBA") 112 | 113 | self._cache_image = rendered_region 114 | 115 | return rendered_region 116 | 117 | def get_point_count(self): 118 | return self._point_count 119 | 120 | def get_uv(self, index: int): 121 | return self._uv_points[index] 122 | 123 | def get_u(self, index: int): 124 | return self._uv_points[index].x 125 | 126 | def get_v(self, index: int): 127 | return self._uv_points[index].y 128 | 129 | def get_xy(self, index: int): 130 | return self._xy_points[index] 131 | 132 | def get_x(self, index: int): 133 | return self._xy_points[index].x 134 | 135 | def get_y(self, index: int): 136 | return self._xy_points[index].y 137 | 138 | def calculate_bounds(self, matrix: Matrix2x3 | None = None) -> Rect: 139 | rect = get_rect(apply_matrix(self._xy_points, matrix)) 140 | rect = Rect( 141 | left=round(rect.left), 142 | top=round(rect.top), 143 | right=round(rect.right), 144 | bottom=round(rect.bottom), 145 | ) 146 | return rect 147 | -------------------------------------------------------------------------------- /xcoder/objects/shape/shape.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from xcoder.objects.plain_object import PlainObject 6 | from xcoder.objects.shape import Region 7 | 8 | if TYPE_CHECKING: 9 | from xcoder.swf import SupercellSWF 10 | 11 | 12 | class Shape(PlainObject): 13 | def __init__(self): 14 | super().__init__() 15 | 16 | self.id = 0 17 | self.regions: list[Region] = [] 18 | 19 | def load(self, swf: SupercellSWF, tag: int): 20 | assert swf.reader is not None 21 | 22 | self.id = swf.reader.read_ushort() 23 | 24 | swf.reader.read_ushort() # regions_count 25 | if tag == 18: 26 | swf.reader.read_ushort() # point_count 27 | 28 | while True: 29 | region_tag = swf.reader.read_char() 30 | region_length = swf.reader.read_uint() 31 | 32 | if region_tag == 0: 33 | return 34 | elif region_tag in (4, 17, 22): 35 | region = Region() 36 | region.load(swf, region_tag) 37 | self.regions.append(region) 38 | else: 39 | swf.reader.read(region_length) 40 | -------------------------------------------------------------------------------- /xcoder/objects/texture.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | import zstandard 6 | from PIL import Image 7 | 8 | from xcoder.bytestream import Reader 9 | from xcoder.images import join_image, load_image_from_buffer 10 | from xcoder.pvr_tex_tool import get_image_from_ktx_data 11 | 12 | if TYPE_CHECKING: 13 | from xcoder.swf import SupercellSWF 14 | 15 | 16 | class SWFTexture: 17 | def __init__(self): 18 | self.width = 0 19 | self.height = 0 20 | 21 | self.pixel_type = -1 22 | 23 | self.image: Image.Image | None = None 24 | 25 | def load(self, swf: SupercellSWF, tag: int, has_texture: bool): 26 | assert swf.reader is not None 27 | 28 | khronos_texture_length = 0 29 | khronos_texture_filename = None 30 | if tag == 45: 31 | khronos_texture_length = swf.reader.read_int() 32 | elif tag == 47: 33 | khronos_texture_filename = swf.reader.read_string() 34 | 35 | self.pixel_type = swf.reader.read_char() 36 | self.width, self.height = (swf.reader.read_ushort(), swf.reader.read_ushort()) 37 | 38 | if not has_texture: 39 | return 40 | 41 | khronos_texture_data = None 42 | if tag == 45: 43 | # noinspection PyUnboundLocalVariable 44 | khronos_texture_data = swf.reader.read(khronos_texture_length) 45 | elif tag == 47: 46 | assert khronos_texture_filename is not None 47 | with open(swf.filepath.parent / khronos_texture_filename, "rb") as file: 48 | decompressor = zstandard.ZstdDecompressor() 49 | khronos_texture_data = decompressor.decompress(file.read()) 50 | 51 | if khronos_texture_data is not None: 52 | self.image = get_image_from_ktx_data(khronos_texture_data).resize( 53 | (self.width, self.height), Image.Resampling.LANCZOS 54 | ) 55 | return 56 | 57 | self.image = self._load_texture(swf.reader, tag) 58 | 59 | def _load_texture(self, reader: Reader, tag: int) -> Image.Image: 60 | if tag in (27, 28, 29): 61 | return join_image(self.pixel_type, self.width, self.height, reader) 62 | 63 | return load_image_from_buffer(self.pixel_type, self.width, self.height, reader) 64 | -------------------------------------------------------------------------------- /xcoder/pixel_utils.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from typing import Callable, Literal, TypeAlias 3 | 4 | from xcoder.localization import locale 5 | 6 | PixelChannels: TypeAlias = tuple[int, ...] 7 | EncodeFunction: TypeAlias = Callable[[PixelChannels], bytes] 8 | RawMode: TypeAlias = Literal["RGBA", "RGBA;4B", "RGBA;15", "BGR;16", "LA", "L"] 9 | 10 | 11 | def get_raw_mode(pixel_type: int) -> RawMode: 12 | if pixel_type in _raw_modes: 13 | return _raw_modes[pixel_type] 14 | 15 | raise Exception(locale.unknown_pixel_type % pixel_type) 16 | 17 | 18 | def get_pixel_encode_function(pixel_type: int) -> EncodeFunction | None: 19 | return _encode_functions.get(pixel_type, None) 20 | 21 | 22 | def get_channel_count_by_pixel_type(pixel_type: int) -> int: 23 | if pixel_type == 4: 24 | return 3 25 | elif pixel_type == 6: 26 | return 2 27 | elif pixel_type == 10: 28 | return 1 29 | return 4 30 | 31 | 32 | def _write_rgba8(pixel: PixelChannels) -> bytes: 33 | return struct.pack("4B", *pixel) 34 | 35 | 36 | def _write_rgba4(pixel: PixelChannels) -> bytes: 37 | r, g, b, a = pixel 38 | return struct.pack("> 4 | b >> 4 << 4 | g >> 4 << 8 | r >> 4 << 12) 39 | 40 | 41 | def _write_rgb5a1(pixel: PixelChannels) -> bytes: 42 | r, g, b, a = pixel 43 | return struct.pack("> 7 | b >> 3 << 1 | g >> 3 << 6 | r >> 3 << 11) 44 | 45 | 46 | # TODO: rewrite with numpy https://qna.habr.com/q/298153 47 | # rgb888 = numpy.asarray(Image.open(filename)) 48 | # # check that image have 3 color components, each of 8 bits 49 | # assert rgb888.shape[-1] == 3 and rgb888.dtype == numpy.uint8 50 | # r5 = (rgb888[..., 0] >> 3 & 0x1f).astype(numpy.uint16) 51 | # g6 = (rgb888[..., 1] >> 2 & 0x3f).astype(numpy.uint16) 52 | # b5 = (rgb888[..., 2] >> 3 & 0x1f).astype(numpy.uint16) 53 | # rgb565 = r5 << 11 | g6 << 5 | b5 54 | # return rgb565.tobytes() 55 | def _write_rgb565(pixel: PixelChannels) -> bytes: 56 | r, g, b = pixel 57 | return struct.pack("> 3 | g >> 2 << 5 | r >> 3 << 11) 58 | 59 | 60 | _encode_functions: dict[int, EncodeFunction] = { 61 | 2: _write_rgba4, 62 | 3: _write_rgb5a1, 63 | 4: _write_rgb565, 64 | } 65 | 66 | # here is a problem with these names https://github.com/python-pillow/Pillow/pull/8158 67 | _raw_modes: dict[int, RawMode] = { 68 | 0: "RGBA", 69 | 1: "RGBA", 70 | 2: "RGBA;4B", # ABGR;4 71 | 3: "RGBA;15", # ABGR;1555 72 | 4: "BGR;16", # RGB;565 73 | 6: "LA", 74 | 10: "L", 75 | } 76 | -------------------------------------------------------------------------------- /xcoder/pvr_tex_tool.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from pathlib import Path 4 | 5 | from PIL import Image 6 | 7 | from xcoder import run 8 | from xcoder.exceptions import ToolNotFoundException 9 | 10 | _main_dir = Path(__file__).parent 11 | _color_space = "sRGB" 12 | _format = "ETC1,UBN,lRGB" 13 | _quality = "etcfast" 14 | 15 | 16 | # Note: a solution from 17 | # https://stackoverflow.com/questions/11210104/check-if-a-program-exists-from-a-python-script 18 | def _get_executable_path( 19 | *paths: os.PathLike[str] | str, 20 | ) -> os.PathLike[str] | str | None: 21 | from shutil import which 22 | 23 | for path in paths: 24 | # Fix of https://github.com/xcoder-tool/XCoder/issues/22 25 | executable_path = which(path) 26 | if executable_path is not None: 27 | return path 28 | 29 | return None 30 | 31 | 32 | _cli_name = "PVRTexToolCLI" 33 | _cli_path = _get_executable_path(_main_dir / f"bin/{_cli_name}", _cli_name) 34 | 35 | 36 | def can_use_pvr_tex_tool() -> bool: 37 | return _cli_path is not None 38 | 39 | 40 | # noinspection PyTypeChecker 41 | def get_image_from_ktx_data(data: bytes) -> Image.Image: 42 | with tempfile.NamedTemporaryFile(delete=False, suffix=".ktx") as tmp: 43 | tmp.write(data) 44 | 45 | try: 46 | image = get_image_from_ktx(Path(tmp.name)) 47 | finally: 48 | os.remove(tmp.name) 49 | 50 | return image 51 | 52 | 53 | # noinspection PyTypeChecker 54 | def get_image_from_ktx(filepath: Path) -> Image.Image: 55 | png_filepath = convert_ktx_to_png(filepath) 56 | image_open = Image.open(png_filepath) 57 | 58 | try: 59 | return image_open.copy() 60 | finally: 61 | image_open.close() 62 | os.remove(png_filepath) 63 | 64 | 65 | def convert_ktx_to_png(filepath: Path, output_folder: Path | None = None) -> Path: 66 | _ensure_tool_installed() 67 | 68 | output_filepath = filepath.with_suffix(".png") 69 | if output_folder is not None: 70 | output_filepath = output_folder / output_filepath.name 71 | 72 | run( 73 | f"{_cli_path} -noout -ics {_color_space} -i {filepath!s} -d {output_filepath!s}" 74 | ) 75 | 76 | return output_filepath 77 | 78 | 79 | def convert_png_to_ktx(filepath: Path, output_folder: Path | None = None) -> Path: 80 | _ensure_tool_installed() 81 | 82 | output_filepath = filepath.with_suffix(".ktx") 83 | if output_folder is not None: 84 | output_filepath = output_folder / output_filepath.name 85 | 86 | run( 87 | f"{_cli_path} -f {_format} -q {_quality} -i {filepath!s} -o {output_filepath!s}" 88 | ) 89 | 90 | return output_filepath 91 | 92 | 93 | def _ensure_tool_installed(): 94 | if can_use_pvr_tex_tool(): 95 | return 96 | 97 | raise ToolNotFoundException("PVRTexTool not found.") 98 | -------------------------------------------------------------------------------- /xcoder/swf.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from loguru import logger 5 | from sc_compression import Signatures 6 | 7 | from xcoder.bytestream import Reader, Writer 8 | from xcoder.features.files import open_sc 9 | from xcoder.localization import locale 10 | from xcoder.matrices.matrix_bank import MatrixBank 11 | from xcoder.objects import MovieClip, Shape, SWFTexture 12 | from xcoder.objects.plain_object import PlainObject 13 | 14 | DEFAULT_HIGHRES_SUFFIX = "_highres" 15 | DEFAULT_LOWRES_SUFFIX = "_lowres" 16 | 17 | 18 | class SupercellSWF: 19 | TEXTURES_TAGS = (1, 16, 28, 29, 34, 19, 24, 27, 45, 47) 20 | SHAPES_TAGS = (2, 18) 21 | MOVIE_CLIPS_TAGS = (3, 10, 12, 14, 35, 49) 22 | 23 | TEXTURE_EXTENSION = "_tex.sc" 24 | 25 | def __init__(self): 26 | self.filename: str | None = None 27 | self.reader: Reader | None = None 28 | 29 | self.use_lowres_texture: bool = False 30 | 31 | self.shapes: list[Shape] = [] 32 | self.movie_clips: list[MovieClip] = [] 33 | self.textures: list[SWFTexture] = [] 34 | 35 | self.xcod_writer = Writer("big") 36 | 37 | self._filepath: Path | None = None 38 | self._uncommon_texture_path: os.PathLike | str | None = None 39 | 40 | self._lowres_suffix: str = DEFAULT_LOWRES_SUFFIX 41 | self._highres_suffix: str = DEFAULT_HIGHRES_SUFFIX 42 | 43 | self._use_uncommon_texture: bool = False 44 | 45 | self._shape_count: int = 0 46 | self._movie_clip_count: int = 0 47 | self._texture_count: int = 0 48 | self._text_field_count: int = 0 49 | 50 | self._export_count: int = 0 51 | self._export_ids: list[int] = [] 52 | self._export_names: list[str] = [] 53 | 54 | self._matrix_banks: list[MatrixBank] = [] 55 | self._matrix_bank: MatrixBank | None = None 56 | 57 | def load(self, filepath: str | os.PathLike) -> tuple[bool, Signatures]: 58 | self._filepath = Path(filepath) 59 | 60 | texture_loaded, signature = self._load_internal( 61 | self._filepath, self._filepath.name.endswith("_tex.sc") 62 | ) 63 | 64 | if not texture_loaded: 65 | if self._use_uncommon_texture: 66 | assert self._uncommon_texture_path is not None 67 | texture_loaded, signature = self._load_internal( 68 | self._uncommon_texture_path, True 69 | ) 70 | else: 71 | texture_path = str(self._filepath)[:-3] + SupercellSWF.TEXTURE_EXTENSION 72 | texture_loaded, signature = self._load_internal(texture_path, True) 73 | 74 | return texture_loaded, signature 75 | 76 | def _load_internal( 77 | self, filepath: os.PathLike | str, is_texture_file: bool 78 | ) -> tuple[bool, Signatures]: 79 | self.filename = os.path.basename(filepath) 80 | 81 | logger.info(locale.collecting_inf % self.filename) 82 | 83 | decompressed_data, signature, version = open_sc(filepath) 84 | 85 | if signature.name != Signatures.NONE: 86 | logger.info(locale.detected_comp % signature.name.upper()) 87 | print() 88 | 89 | self.reader = Reader(decompressed_data) 90 | del decompressed_data 91 | 92 | if not is_texture_file: 93 | self._shape_count = self.reader.read_ushort() 94 | self._movie_clip_count = self.reader.read_ushort() 95 | self._texture_count = self.reader.read_ushort() 96 | self._text_field_count = self.reader.read_ushort() 97 | 98 | matrix_count = self.reader.read_ushort() 99 | color_transformation_count = self.reader.read_ushort() 100 | 101 | self._matrix_bank = MatrixBank() 102 | self._matrix_bank.init(matrix_count, color_transformation_count) 103 | self._matrix_banks.append(self._matrix_bank) 104 | 105 | self.shapes = [_class() for _class in [Shape] * self._shape_count] 106 | self.movie_clips = [ 107 | _class() for _class in [MovieClip] * self._movie_clip_count 108 | ] 109 | self.textures = [_class() for _class in [SWFTexture] * self._texture_count] 110 | 111 | self.reader.read_uint() 112 | self.reader.read_char() 113 | 114 | self._export_count = self.reader.read_ushort() 115 | 116 | self._export_ids = [] 117 | for _ in range(self._export_count): 118 | self._export_ids.append(self.reader.read_ushort()) 119 | 120 | self._export_names = [] 121 | for _ in range(self._export_count): 122 | self._export_names.append(self.reader.read_string()) 123 | 124 | loaded = self._load_tags(is_texture_file) 125 | 126 | for i in range(self._export_count): 127 | export_id = self._export_ids[i] 128 | export_name = self._export_names[i] 129 | 130 | movie_clip = self.get_display_object( 131 | export_id, export_name, raise_error=True 132 | ) 133 | 134 | if isinstance(movie_clip, MovieClip): 135 | movie_clip.export_name = export_name 136 | 137 | return loaded, signature 138 | 139 | def _load_tags(self, is_texture_file: bool) -> bool: 140 | assert self.reader is not None 141 | 142 | has_texture = True 143 | 144 | texture_id = 0 145 | movie_clips_loaded = 0 146 | shapes_loaded = 0 147 | matrices_loaded = 0 148 | 149 | while True: 150 | tag = self.reader.read_char() 151 | length = self.reader.read_uint() 152 | 153 | if tag == 0: 154 | return has_texture 155 | elif tag in SupercellSWF.TEXTURES_TAGS: 156 | # this is done to avoid loading the data file 157 | # (although it does not affect the speed) 158 | if is_texture_file and texture_id >= len(self.textures): 159 | self.textures.append(SWFTexture()) 160 | 161 | texture = self.textures[texture_id] 162 | texture.load(self, tag, has_texture) 163 | 164 | if has_texture: 165 | logger.info( 166 | locale.about_sc.format( 167 | filename=self.filename, 168 | index=texture_id, 169 | pixel_type=texture.pixel_type, 170 | width=texture.width, 171 | height=texture.height, 172 | ) 173 | ) 174 | 175 | self.xcod_writer.write_ubyte(tag) 176 | self.xcod_writer.write_ubyte(texture.pixel_type) 177 | self.xcod_writer.write_uint16(texture.width) 178 | self.xcod_writer.write_uint16(texture.height) 179 | texture_id += 1 180 | elif tag in SupercellSWF.SHAPES_TAGS: 181 | self.shapes[shapes_loaded].load(self, tag) 182 | shapes_loaded += 1 183 | elif tag in SupercellSWF.MOVIE_CLIPS_TAGS: # MovieClip 184 | self.movie_clips[movie_clips_loaded].load(self, tag) 185 | movie_clips_loaded += 1 186 | elif tag == 8 or tag == 36: # Matrix 187 | assert self._matrix_bank is not None 188 | self._matrix_bank.get_matrix(matrices_loaded).load(self.reader, tag) 189 | matrices_loaded += 1 190 | elif tag == 26: 191 | has_texture = False 192 | elif tag == 30: 193 | self._use_uncommon_texture = True 194 | highres_texture_path = ( 195 | str(self._filepath)[:-3] 196 | + self._highres_suffix 197 | + SupercellSWF.TEXTURE_EXTENSION 198 | ) 199 | lowres_texture_path = ( 200 | str(self._filepath)[:-3] 201 | + self._lowres_suffix 202 | + SupercellSWF.TEXTURE_EXTENSION 203 | ) 204 | 205 | self._uncommon_texture_path = highres_texture_path 206 | if not os.path.exists(highres_texture_path) and os.path.exists( 207 | lowres_texture_path 208 | ): 209 | self._uncommon_texture_path = lowres_texture_path 210 | self.use_lowres_texture = True 211 | elif tag == 42: 212 | matrix_count = self.reader.read_ushort() 213 | color_transformation_count = self.reader.read_ushort() 214 | 215 | self._matrix_bank = MatrixBank() 216 | self._matrix_bank.init(matrix_count, color_transformation_count) 217 | self._matrix_banks.append(self._matrix_bank) 218 | 219 | matrices_loaded = 0 220 | else: 221 | self.reader.read(length) 222 | 223 | def get_display_object( 224 | self, target_id: int, name: str | None = None, *, raise_error: bool = False 225 | ) -> PlainObject | None: 226 | for shape in self.shapes: 227 | if shape.id == target_id: 228 | return shape 229 | 230 | for movie_clip in self.movie_clips: 231 | if movie_clip.id == target_id: 232 | return movie_clip 233 | 234 | if raise_error: 235 | exception_text = ( 236 | f"Unable to find some DisplayObject id {target_id}, {self.filename}" 237 | ) 238 | if name is not None: 239 | exception_text += f" needed by export name {name}" 240 | 241 | raise ValueError(exception_text) 242 | return None 243 | 244 | def get_matrix_bank(self, index: int) -> MatrixBank: 245 | return self._matrix_banks[index] 246 | 247 | @property 248 | def filepath(self) -> Path: 249 | assert self._filepath is not None 250 | return self._filepath 251 | -------------------------------------------------------------------------------- /xcoder/xcod.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from dataclasses import dataclass 5 | from pathlib import Path 6 | 7 | from loguru import logger 8 | from sc_compression import Signatures 9 | 10 | from xcoder.bytestream import Reader 11 | from xcoder.localization import locale 12 | from xcoder.math.point import Point 13 | 14 | 15 | @dataclass 16 | class SheetInfo: 17 | file_type: int 18 | pixel_type: int 19 | size: tuple[int, int] 20 | 21 | @property 22 | def width(self) -> int: 23 | return self.size[0] 24 | 25 | @property 26 | def height(self) -> int: 27 | return self.size[1] 28 | 29 | 30 | @dataclass 31 | class RegionInfo: 32 | texture_id: int 33 | points: list[Point] 34 | 35 | 36 | @dataclass 37 | class ShapeInfo: 38 | id: int 39 | regions: list[RegionInfo] 40 | 41 | 42 | @dataclass 43 | class FileInfo: 44 | name: str 45 | signature: Signatures 46 | signature_version: int | None 47 | sheets: list[SheetInfo] 48 | shapes: list[ShapeInfo] 49 | 50 | 51 | def parse_info(metadata_file_path: Path, has_detailed_info: bool) -> FileInfo: 52 | logger.info(locale.collecting_inf % metadata_file_path.name) 53 | print() 54 | 55 | with open(metadata_file_path, "rb") as file: 56 | reader = Reader(file.read(), "big") 57 | 58 | ensure_magic_known(reader) 59 | 60 | file_info = FileInfo( 61 | os.path.splitext(metadata_file_path.name)[0], Signatures.NONE, None, [], [] 62 | ) 63 | parse_base_info(file_info, reader) 64 | 65 | if has_detailed_info: 66 | parse_detailed_info(file_info, reader) 67 | 68 | return file_info 69 | 70 | 71 | def parse_base_info(file_info: FileInfo, reader: Reader) -> None: 72 | file_info.signature = Signatures.SC 73 | file_info.signature_version = 1 if reader.read_string() == "LZMA" else 3 74 | 75 | sheets_count = reader.read_uchar() 76 | for i in range(sheets_count): 77 | file_type = reader.read_uchar() 78 | pixel_type = reader.read_uchar() 79 | width = reader.read_ushort() 80 | height = reader.read_ushort() 81 | 82 | file_info.sheets.append(SheetInfo(file_type, pixel_type, (width, height))) 83 | 84 | 85 | def parse_detailed_info(file_info: FileInfo, reader: Reader) -> None: 86 | shapes_count = reader.read_ushort() 87 | for shape_index in range(shapes_count): 88 | shape_id = reader.read_ushort() 89 | 90 | regions = [] 91 | 92 | regions_count = reader.read_ushort() 93 | for region_index in range(regions_count): 94 | texture_id, points_count = reader.read_uchar(), reader.read_uchar() 95 | 96 | points = [ 97 | Point(reader.read_ushort(), reader.read_ushort()) 98 | for _ in range(points_count) 99 | ] 100 | 101 | regions.append(RegionInfo(texture_id, points)) 102 | 103 | file_info.shapes.append(ShapeInfo(shape_id, regions)) 104 | 105 | 106 | def ensure_magic_known(reader: Reader) -> None: 107 | magic = reader.read(4) 108 | if magic != b"XCOD": 109 | raise IOError("Unknown file MAGIC: " + magic.hex()) 110 | --------------------------------------------------------------------------------