diff --git a/.editorconfig b/.editorconfig index 1cd498f..589f7f2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,15 +1,19 @@ -# EditorConfig is awesome: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -[*] -indent_style = tab -indent_size = 4 -end_of_line = crlf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.rs] -indent_style = space +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = tab +indent_size = 4 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_style = space + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/build-lua.yml b/.github/workflows/build-lua.yml new file mode 100644 index 0000000..c0e85ae --- /dev/null +++ b/.github/workflows/build-lua.yml @@ -0,0 +1,45 @@ +name: Build Lua +on: + workflow_dispatch: + push: + paths: + - '.github/workflows/build-lua.yml' + +jobs: + build-lua: + name: Get Steam Runtime Compatible Lua + runs-on: ubuntu-latest + steps: + - name: Download in Steam Runtime container + uses: addnab/docker-run-action@v3 + with: + image: registry.gitlab.steamos.cloud/steamrt/scout/sdk + options: -v ${{ github.workspace }}:/work + run: | + set -ex + apt-get update + apt-get install -y curl + + WORKDIR="/tmp/lua-build" + mkdir -p "$WORKDIR" + cd "$WORKDIR" + + # Download pre-built library + curl -L "https://master.dl.sourceforge.net/project/luabinaries/5.4.2/Linux%20Libraries/lua-5.4.2_Linux313_64_lib.tar.gz?viasf=1" -o lua.tar.gz + + # Extract + tar xf lua.tar.gz + + # Copy library + mkdir -p /work/artifacts + cp liblua54.so /work/artifacts/ + + # Test the library + ldd /work/artifacts/liblua54.so || echo "Library dependencies not found!" + + - name: Upload Lua library + uses: actions/upload-artifact@v4 + with: + name: steamrt-lua + path: artifacts/liblua54.so + if-no-files-found: error diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml new file mode 100644 index 0000000..42a8aa7 --- /dev/null +++ b/.github/workflows/build-rust.yml @@ -0,0 +1,62 @@ +name: Build Rust Library +on: + workflow_dispatch: + push: + paths: + - '.github/workflows/build-rust.yml' + - 'lib/**' + +jobs: + build-rust: + name: Build Rust in Steam Runtime + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build in Steam Runtime container + uses: addnab/docker-run-action@v3 + with: + image: registry.gitlab.steamos.cloud/steamrt/scout/sdk + options: -v ${{ github.workspace }}:/work + run: | + set -ex + + # Debug system info with focus on glibc + echo "System information:" + uname -a + ldd --version + strings /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC_ | sort -V + + apt-get update + apt-get install -y curl build-essential gcc-9 g++-9 wget + + # Download old rustup-init binary directly + mkdir -p ~/.cargo + wget -O rustup-init "https://static.rust-lang.org/rustup/archive/1.21.1/x86_64-unknown-linux-gnu/rustup-init" + chmod +x rustup-init + + # Install Rust 1.63 (known to support old glibc) + RUSTUP_HOME=~/.rustup CARGO_HOME=~/.cargo ./rustup-init -y --no-modify-path --default-toolchain 1.63.0 + export PATH="$HOME/.cargo/bin:$PATH" + + # Verify Rust version and target + rustc --version + rustc -vV + + # Use gcc-9 for linking + export CC=gcc-9 + export CXX=g++-9 + + cd /work/lib + cargo build --release + + # Copy built library + mkdir -p /work/artifacts + cp target/release/libdmi.so /work/artifacts/ + + - name: Upload Rust library + uses: actions/upload-artifact@v4 + with: + name: steamrt-rust + path: artifacts/libdmi.so + if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69a434b..7e81eb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,111 +1,157 @@ -name: CI -on: - push: - branches: - - main - - dev - tags: - - 'v*' - paths: - - '.cargo/**' - - '.github/workflows/**' - - 'lib/**' - - 'scripts/**' - - 'tools/**' - - 'package.json' - pull_request: - branches: - - main - paths: - - '.cargo/**' - - '.github/workflows/**' - - 'lib/**' - - 'scripts/**' - - 'tools/**' - - 'package.json' -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2 - with: - workspaces: './lib -> target' - - name: Run rustfmt - working-directory: lib - run: | - cargo fmt -- --check - - name: Run clippy - working-directory: lib - run: | - cargo clippy --locked -- -D warnings - - name: Check release version - if: startsWith(github.ref, 'refs/tags/v') - working-directory: lib - run: | - cargo test --locked --test version - build: - needs: lint - strategy: - matrix: - os: [ windows-latest, ubuntu-latest, macos-latest ] - include: - - os: windows-latest - name: Windows - rust-target: x86_64-pc-windows-msvc - - os: ubuntu-latest - name: Linux - rust-target: x86_64-unknown-linux-gnu - - os: macos-latest - name: macOS - rust-target: x86_64-apple-darwin - runs-on: ${{ matrix.os }} - name: Build - ${{ matrix.name }} - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - target: ${{ matrix.rust-target }} - - uses: Swatinem/rust-cache@v2 - with: - workspaces: './lib -> target' - - name: Run cargo tests - working-directory: lib - run: | - cargo test --target ${{ matrix.rust-target }} --locked --test dmi - - name: Run cargo build - working-directory: lib - run: | - cargo build --target ${{ matrix.rust-target }} --locked --release - - name: Run build script - run: | - python tools/build.py --ci ${{ matrix.rust-target }} - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.name }} - path: | - dist/* - if-no-files-found: error - release: - needs: build - name: Release - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v') - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - path: dist - merge-multiple: true - - uses: softprops/action-gh-release@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - draft: true - prerelease: false - files: | - dist/* +name: CI +on: + push: + branches: + - main + - dev + tags: + - "v*" + paths: + - ".cargo/**" + - ".github/workflows/**" + - "lib/**" + - "scripts/**" + - "tools/**" + - "package.json" + pull_request: + branches: + - main + paths: + - ".cargo/**" + - ".github/workflows/**" + - "lib/**" + - "scripts/**" + - "tools/**" + - "package.json" +jobs: + lint: + name: Lint + runs-on: ubuntu-latest # we need to build this in the hellish registry.gitlab.steamos.cloud/steamrt/scout/sdk because of too new of glibc + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: "./lib -> target" + + - name: Run rustfmt + working-directory: lib + run: | + cargo fmt -- --check + + - name: Run clippy + working-directory: lib + run: | + cargo clippy --locked -- -D warnings + + - name: Check release version + if: startsWith(github.ref, 'refs/tags/v') + working-directory: lib + run: | + cargo test --locked --test version + + build: + needs: lint + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + include: + - os: windows-latest + name: Windows + rust-target: x86_64-pc-windows-msvc + - os: ubuntu-latest + name: Linux + rust-target: x86_64-unknown-linux-gnu + - os: macos-latest + name: macOS + rust-target: x86_64-apple-darwin + runs-on: ${{ matrix.os }} + name: Build - ${{ matrix.name }} + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + target: ${{ matrix.rust-target }} + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: "./lib -> target" + + - name: Download Steam Runtime libraries + if: matrix.os == 'ubuntu-latest' + uses: dawidd6/action-download-artifact@v8 + with: + workflow: | + build-lua.yml + build-rust.yml + name: | + steamrt-lua + steamrt-rust + path: to-copy + search_artifacts: true + + - name: Verify library downloads + if: matrix.os == 'ubuntu-latest' + run: | + echo "Current directory contents:" + ls -la + echo "to-copy directory contents:" + ls -la to-copy || echo "to-copy directory not found!" + + - name: Run cargo tests + working-directory: lib + run: | + cargo test --target ${{ matrix.rust-target }} --locked --test dmi + + - name: Run cargo build + if: matrix.os != 'ubuntu-latest' + working-directory: lib + run: | + cargo build --target ${{ matrix.rust-target }} --locked --release + + - name: Run build script + run: | + python tools/build.py --ci ${{ matrix.rust-target }} + + - name: Set executable permissions + if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' + run: | + if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then + chmod +x lib/target/${{ matrix.rust-target }}/release/libdmi.so + else + chmod +x lib/target/${{ matrix.rust-target }}/release/libdmi.dylib + fi + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.name }} + path: | + dist/* + if-no-files-found: error + + release: + needs: build + name: Release + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + draft: true + prerelease: false + files: | + dist/* diff --git a/README.md b/README.md index c0fab05..3111723 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,93 @@ -> [!NOTE] -> This project has been taken under stewardship of the SS13 org to provide updates and allow for community support, as the original creator [is no longer maintaining it](https://github.com/Seefaaa/aseprite-dmi). - -# DMI Editor for Aseprite - -This project is a DMI (BYOND's Dream Maker icon files) editor extension for Aseprite, a popular pixel art tool. It is written in Rust and Lua and aims to enhance the Aseprite experience by providing tools for editing and managing DMI files. - -## Download - -The latest version of this extension is available for download from the [Releases](https://github.com/spacestation13/aseprite-dmi/releases) page on the project's GitHub repository. - -The plugin will also prompt you to download an update when a new version is released. - -## Usage - -Once the project has been downloaded or built, the extension can be added to Aseprite by dragging and dropping it into the application or by selecting the 'Add Extension' button in the 'Edit > Preferences > Extensions' menu. - -DMI files can now be opened in Aseprite in the same way as any other file format. You will need to change the open file dialog filter to 'All Files'. - -### Creating New Files - -New files can be created via the following pathway: `File > DMI Editor > New DMI File`. - -### Changing Iconstate Properties - -The state properties, including the state name, can be modified by right clicking on the state or by clicking on the text below the state in the editor. - -### Copy and Paste - -Right-clicking on the state will bring up the context menu. The context menu allows the user to copy the state to the clipboard, which can then be pasted at a later stage. Right click on an empty space within the editor to paste the copied state. The states are copied in the JSON format, with PNG images, which are base64-encoded, included for the frames. - -### Frames and Delays - -In Aseprite's timeline, new frames can be added and delays between frames can be modified. - -### Expand, Resize, Crop - -The DMI file may be expanded, resized, or cropped via the `File > DMI Editor` menu. It should be noted that the active sprite must be a DMI iconstate in order to utilise these commands. - -### Plugin Preferences -Under the `File > DMI Editor` menu, there is an `Preferences` menu which contains various options: - -- **Auto Overwrite**: Automatically overwrites the source DMI file when saving an iconstate. -- **Auto Flatten** *(Enabled by Default)*: Automatically flattens layers downward into directional layers when saving an iconstate, allowing you to fully use Aseprite layers. - -## Building the Project - -### Requirements - -- [Rust](https://www.rust-lang.org/) -- [Python](https://www.python.org/) (build script) - -To build the project, run `tools/build.py` Python script. - -### Releasing - -Push a tag like `v1.0.8` via `git`, after changing the Cargo.toml and package.json files. - -## LICENSE - -**GPLv3**, for more details see the [LICENSE](./LICENSE). - -Originally created by [Seefaaa](https://github.com/Seefaaa) at https://github.com/Seefaaa/aseprite-dmi. +> [!NOTE] +> This project has been taken under stewardship of the SS13 org to provide updates and allow for community support, as the original creator [is no longer maintaining it](https://github.com/Seefaaa/aseprite-dmi). + +# DMI Editor for Aseprite + +This project is a DMI (BYOND's Dream Maker icon files) editor extension for Aseprite, a popular pixel art tool. It is written in Rust and Lua and aims to enhance the Aseprite experience by providing tools for editing and managing DMI files. + +## Download + +The latest version of this extension is available for download from the [Releases](https://github.com/spacestation13/aseprite-dmi/releases) page on the project's GitHub repository. + +The plugin will also prompt you to download an update when a new version is released. + +### Additional Instructions for Linux and macOS + +To use this project, you need to have Lua 5.4 installed on your system. + +#### Linux + + +rust 1.63 required due to scout being ancient + +`rustup install 1.63.0` +`rustup override set 1.63.0` + +For Debian-based distributions, you can install Lua 5.4 using the following command: + +```sh +apt install lua5.4 +``` + +Otherwise, follow the relevant instructions for your distribution to install Lua 5.4. + +#### macOS + +For macOS, you can install Lua 5.4 using Homebrew. First, ensure you have Homebrew installed, then run: + +```sh +brew install lua +``` + +Make sure Lua 5.4 is correctly installed by running `lua -v` in your terminal, which should display the version information. + +## Usage + +Once the project has been downloaded or built, the extension can be added to Aseprite by dragging and dropping it into the application or by selecting the 'Add Extension' button in the 'Edit > Preferences > Extensions' menu. + +DMI files can now be opened in Aseprite in the same way as any other file format. You will need to change the open file dialog filter to 'All Files'. + +### Creating New Files + +New files can be created via the following pathway: `File > DMI Editor > New DMI File`. + +### Changing Iconstate Properties + +The state properties, including the state name, can be modified by right clicking on the state or by clicking on the text below the state in the editor. + +### Copy and Paste + +Right-clicking on the state will bring up the context menu. The context menu allows the user to copy the state to the clipboard, which can then be pasted at a later stage. Right click on an empty space within the editor to paste the copied state. The states are copied in the JSON format, with PNG images, which are base64-encoded, included for the frames. + +### Frames and Delays + +In Aseprite's timeline, new frames can be added and delays between frames can be modified. + +### Expand, Resize, Crop + +The DMI file may be expanded, resized, or cropped via the `File > DMI Editor` menu. It should be noted that the active sprite must be a DMI iconstate in order to utilise these commands. + +### Plugin Preferences +Under the `File > DMI Editor` menu, there is an `Preferences` menu which contains various options: + +- **Auto Overwrite**: Automatically overwrites the source DMI file when saving an iconstate. +- **Auto Flatten** *(Enabled by Default)*: Automatically flattens layers downward into directional layers when saving an iconstate, allowing you to fully use Aseprite layers. + +## Building the Project + +### Requirements + +- [Rust](https://www.rust-lang.org/) +- [Python](https://www.python.org/) (build script) + +To build the project, run `tools/build.py` Python script. + +### Releasing + +Push a tag like `v1.0.8` via `git`, after changing the Cargo.toml and package.json files. + +## LICENSE + +**GPLv3**, for more details see the [LICENSE](./LICENSE). + +Originally created by [Seefaaa](https://github.com/Seefaaa) at https://github.com/Seefaaa/aseprite-dmi. diff --git a/lib/Cargo.lock b/lib/Cargo.lock index 0937c53..a4c1ad4 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "addr2line" @@ -17,6 +17,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anyhow" version = "1.0.95" @@ -25,18 +31,19 @@ checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "arboard" -version = "3.4.1" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" +checksum = "a2041f1943049c7978768d84e6d0fd95de98b76d6c4727b09e78ec253d29fa58" dependencies = [ "clipboard-win", "core-graphics", "image", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc", + "objc-foundation", + "objc_id", "parking_lot", + "thiserror 1.0.69", "windows-sys 0.48.0", "x11rb", ] @@ -53,25 +60,32 @@ version = "1.1.0" dependencies = [ "anyhow", "arboard", - "base64", + "base64 0.22.1", + "bstr 1.6.2", + "fdeflate", + "hashbrown", + "home", "image", "mlua", "native-dialog", "png", + "raw-window-handle", "reqwest", "serde", "serde_json", "sysinfo", "thiserror 2.0.11", + "tinystr", + "tokio", + "tokio-util", + "tower", "webbrowser", + "writeable", + "yoke", + "zerofrom", + "zerovec", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.4.0" @@ -93,6 +107,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -128,11 +148,21 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.3" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] +name = "bstr" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" dependencies = [ "memchr", + "regex-automata", "serde", ] @@ -149,10 +179,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] -name = "byteorder-lite" -version = "0.1.0" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -220,6 +250,12 @@ dependencies = [ "objc", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -314,6 +350,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + [[package]] name = "dirs-next" version = "2.0.0" @@ -343,7 +385,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -369,12 +411,11 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.4.5" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" dependencies = [ "serde", - "typeid", ] [[package]] @@ -401,9 +442,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" -version = "0.3.7" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" dependencies = [ "simd-adler32", ] @@ -424,6 +465,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "foreign-types" version = "0.3.2" @@ -451,7 +498,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -482,7 +529,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -517,7 +563,6 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", - "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -554,15 +599,15 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.4.7" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", + "futures-util", "http", "indexmap", "slab", @@ -573,24 +618,35 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "home" -version = "0.5.11" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] name = "http" -version = "1.2.0" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -599,24 +655,12 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "futures-util", "http", - "http-body", "pin-project-lite", ] @@ -626,194 +670,47 @@ version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" -version = "1.5.2" +version = "0.14.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" dependencies = [ "bytes", "futures-channel", + "futures-core", "futures-util", "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" -dependencies = [ - "futures-util", - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", + "socket2", "tokio", - "tokio-rustls", "tower-service", + "tracing", + "want", ] [[package]] name = "hyper-tls" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "http-body-util", "hyper", - "hyper-util", "native-tls", "tokio", "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", ] [[package]] @@ -829,22 +726,33 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "279259b0ac81c89d11c290495fdcfa96ea3643b7df311c138b6fe8ca5237f0f8" dependencies = [ - "icu_normalizer", - "icu_properties", + "idna_mapping", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna_mapping" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5422cc5bc64289a77dbb45e970b86b5e9a04cb500abc7240505aedc1bf40f38" +dependencies = [ + "unicode-joining-type", ] [[package]] name = "image" -version = "0.25.5" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" dependencies = [ "bytemuck", - "byteorder-lite", + "byteorder", + "color_quant", "num-traits", "png", "tiff", @@ -941,12 +849,6 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" -[[package]] -name = "litemap" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" - [[package]] name = "lock_api" version = "0.4.12" @@ -1002,53 +904,41 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] name = "mlua" -version = "0.10.2" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea43c3ffac2d0798bd7128815212dd78c98316b299b7a902dabef13dc7b6b8d" +checksum = "0bb37b0ba91f017aa7ca2b98ef99496827770cd635b4a932a6047c5b4bbe678e" dependencies = [ - "bstr", - "either", + "bstr 0.2.17", + "cc", "erased-serde", - "mlua-sys", "mlua_derive", "num-traits", - "parking_lot", + "once_cell", + "pkg-config", "rustc-hash", "serde", - "serde-value", -] - -[[package]] -name = "mlua-sys" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63a11d485edf0f3f04a508615d36c7d50d299cf61a7ee6d3e2530651e0a31771" -dependencies = [ - "cc", - "cfg-if", - "pkg-config", ] [[package]] name = "mlua_derive" -version = "0.10.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870d71c172fcf491c6b5fb4c04160619a2ee3e5a42a1402269c66bcbf1dd4deb" +checksum = "b9214e60d3cf1643013b107330fcd374ccec1e4ba1eef76e7e5da5e8202e71c0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1076,9 +966,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -1125,6 +1015,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "objc" version = "0.2.7" @@ -1161,46 +1061,6 @@ dependencies = [ "objc2-encode", ] -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.8.0", - "block2", - "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -1219,31 +1079,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - [[package]] name = "objc_id" version = "0.1.1" @@ -1270,9 +1105,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -1291,7 +1126,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1312,15 +1147,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "parking_lot" version = "0.12.3" @@ -1401,9 +1227,12 @@ dependencies = [ [[package]] name = "raw-window-handle" -version = "0.5.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a" +dependencies = [ + "cty", +] [[package]] name = "rayon" @@ -1445,26 +1274,28 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" + [[package]] name = "reqwest" -version = "0.12.12" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", - "http-body-util", "hyper", - "hyper-rustls", "hyper-tls", - "hyper-util", "ipnet", "js-sys", "log", @@ -1473,36 +1304,17 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", - "tower", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", -] - -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", + "winreg", ] [[package]] @@ -1513,9 +1325,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" @@ -1530,45 +1342,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "rustls" -version = "0.23.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" -dependencies = [ - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.19" @@ -1637,16 +1410,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_derive" version = "1.0.217" @@ -1655,7 +1418,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1719,29 +1482,17 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" -version = "2.0.96" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -1749,58 +1500,29 @@ dependencies = [ ] [[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.1" +name = "syn" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", - "syn", + "unicode-ident", ] [[package]] name = "sysinfo" -version = "0.33.1" +version = "0.29.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" dependencies = [ + "cfg-if", "core-foundation-sys", "libc", - "memchr", "ntapi", + "once_cell", "rayon", - "windows", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.8.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", + "winapi", ] [[package]] @@ -1843,7 +1565,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1854,7 +1576,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1870,27 +1592,42 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef" dependencies = [ "displaydoc", - "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.43.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", "libc", "mio", + "num_cpus", "pin-project-lite", "socket2", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -1903,21 +1640,11 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", @@ -1928,17 +1655,13 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1959,6 +1682,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] @@ -1979,10 +1703,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "typeid" -version = "1.0.2" +name = "unicode-bidi" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" @@ -1991,10 +1715,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" [[package]] -name = "untrusted" -version = "0.9.0" +name = "unicode-joining-type" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "22f8cb47ccb8bc750808755af3071da4a10dcd147b68fc874b7ae4b12543f6f5" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] [[package]] name = "url" @@ -2007,12 +1740,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -2082,7 +1809,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.96", "wasm-bindgen-shared", ] @@ -2117,7 +1844,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2218,89 +1945,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-registry" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" -dependencies = [ - "windows-result 0.2.0", - "windows-strings", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -2516,16 +2160,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "write16" -version = "1.0.0" +name = "winreg" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] [[package]] name = "writeable" -version = "0.5.5" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "60e49e42bdb1d5dc76f4cd78102f8f0714d32edfa3efb82286eb0f0b1fc0da0f" [[package]] name = "x11rb" @@ -2546,73 +2193,26 @@ checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "yoke" -version = "0.7.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "1848075a23a28f9773498ee9a0f2cf58fcbad4f8c0ccf84a210ab33c6ae495de" dependencies = [ "serde", "stable_deref_trait", - "yoke-derive", "zerofrom", ] -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerofrom" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "df54d76c3251de27615dfcce21e636c172dafb2549cd7fd93e21c66f6ca6bea2" [[package]] name = "zerovec" -version = "0.10.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "f3412f49402c32fffcc98fa861dc496eaa777442c5a5fc1e8d33d0fbb53cb0d2" dependencies = [ - "yoke", "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", ] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index a829d91..635b629 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,24 +1,39 @@ -[package] -name = "aseprite-dmi" -version = "1.1.0" -edition = "2021" -repository = "https://github.com/spacestation13/aseprite-dmi" - -[lib] -name = "dmi" -crate-type = ["cdylib", "lib"] - -[dependencies] -anyhow = "1.0" -arboard = "3.4" -base64 = "0.22.1" -image = { version = "0.25.5", default-features = false, features = ["png"] } -mlua = { version = "0.10.2", features = ["module", "lua54", "serialize"] } -native-dialog = "0.7.0" -png = "0.17.16" -reqwest = { version = "0.12.12", features = ["blocking", "json"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -sysinfo = "0.33.1" -thiserror = "2.0" -webbrowser = "1.0" +[package] +name = "aseprite-dmi" +version = "1.1.0" +edition = "2021" +rust-version = "1.63" +repository = "https://github.com/spacestation13/aseprite-dmi" + +[lib] +name = "dmi" +crate-type = ["cdylib", "lib"] + +[dependencies] +anyhow = "1.0" +arboard = "=3.3.2" +base64 = "0.22.1" +image = { version = "=0.24.9", default-features = false, features = ["png"] } +mlua = { version = "=0.8.10", features = ["module", "lua54", "serialize"] } +native-dialog = "0.7.0" +png = "0.17.16" +reqwest = { version = "=0.11.18", features = ["blocking", "json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sysinfo = "=0.29.11" +thiserror = "2.0" +webbrowser = "1.0" + +bstr = "=1.6.2" +fdeflate = "=0.3.5" +tower = "=0.4.13" +tokio = "=1.38.1" +tokio-util = "=0.7.11" +zerovec = "=0.8.1" +yoke = "=0.7.1" +zerofrom = "=0.1.2" +hashbrown = "=0.15.0" +home = "=0.5.5" +tinystr = "=0.7.1" +raw-window-handle = "=0.5.0" +writeable = "=0.5.2" diff --git a/lib/src/dmi.rs b/lib/src/dmi.rs index 00694f0..15498e0 100644 --- a/lib/src/dmi.rs +++ b/lib/src/dmi.rs @@ -1,643 +1,643 @@ -use base64::{engine::general_purpose, Engine as _}; -use image::{imageops, ImageBuffer, Rgba}; -use image::{DynamicImage, ImageReader}; -use png::{Compression, Decoder, Encoder}; -use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; -use std::ffi::OsStr; -use std::fs::{create_dir_all, remove_dir_all, File}; -use std::io::{BufWriter, Cursor, Read as _, Write as _}; -use std::path::Path; -use thiserror::Error; - -use crate::utils::{find_directory, image_to_base64, optimal_size}; - -const DMI_VERSION: &str = "4.0"; - -#[derive(Debug)] -pub struct Dmi { - pub name: String, - pub width: u32, - pub height: u32, - pub states: Vec, -} - -impl Dmi { - pub fn new(name: String, width: u32, height: u32) -> Dmi { - Dmi { - name, - width, - height, - states: Vec::new(), - } - } - pub fn set_metadata(&mut self, metadata: String) -> DmiResult<()> { - let mut lines = metadata.lines(); - - if lines.next().ok_or(DmiError::MissingMetadataHeader)? != "# BEGIN DMI" { - return Err(DmiError::MissingMetadataHeader); - } - - if lines.next().ok_or(DmiError::InvalidMetadataVersion)? - != format!("version = {}", DMI_VERSION) - { - return Err(DmiError::InvalidMetadataVersion); - } - - for line in lines { - if line == "# END DMI" { - break; - } - - let mut split = line.trim().split(" = "); - let (key, value) = ( - split.next().ok_or(DmiError::MissingMetadataValue)?, - split.next().ok_or(DmiError::MissingMetadataValue)?, - ); - - match key { - "width" => self.width = value.parse()?, - "height" => self.height = value.parse()?, - "state" => self.states.push(State::new(value.trim_matches('"').into())), - "dirs" => { - self.states - .last_mut() - .ok_or(DmiError::OutOfOrderStateInfo)? - .dirs = value.parse()?; - } - "frames" => { - self.states - .last_mut() - .ok_or(DmiError::OutOfOrderStateInfo)? - .frame_count = value.parse()?; - } - "delay" => { - self.states - .last_mut() - .ok_or(DmiError::OutOfOrderStateInfo)? - .delays = value - .split(',') - .map(|delay| delay.parse()) - .collect::>()?; - } - "loop" => { - self.states - .last_mut() - .ok_or(DmiError::OutOfOrderStateInfo)? - .loop_ = value.parse()?; - } - "rewind" => { - self.states - .last_mut() - .ok_or(DmiError::OutOfOrderStateInfo)? - .rewind = value == "1"; - } - "movement" => { - self.states - .last_mut() - .ok_or(DmiError::OutOfOrderStateInfo)? - .movement = value == "1"; - } - "hotspot" => { - self.states - .last_mut() - .ok_or(DmiError::OutOfOrderStateInfo)? - .hotspots - .push(value.into()); - } - _ => return Err(DmiError::UnknownMetadataKey), - } - } - - Ok(()) - } - pub fn get_metadata(&self) -> String { - let mut string = String::new(); - string.push_str("# BEGIN DMI\n"); - string.push_str(format!("version = {}\n", DMI_VERSION).as_str()); - string.push_str(format!("\twidth = {}\n", self.width).as_str()); - string.push_str(format!("\theight = {}\n", self.height).as_str()); - for state in self.states.iter() { - string.push_str(format!("state = \"{}\"\n", state.name).as_str()); - string.push_str(format!("\tdirs = {}\n", state.dirs).as_str()); - string.push_str(format!("\tframes = {}\n", state.frame_count).as_str()); - if !state.delays.is_empty() { - let delays = state - .delays - .iter() - .map(|delay| delay.to_string()) - .collect::>() - .join(","); - string.push_str(format!("\tdelay = {}\n", delays).as_str()) - }; - if state.loop_ > 0 { - string.push_str(format!("\tloop = {}\n", state.loop_).as_str()) - }; - if state.rewind { - string.push_str(format!("\trewind = {}\n", state.rewind as u32).as_str()) - }; - if state.movement { - string.push_str(format!("\tmovement = {}\n", state.movement as u32).as_str()) - }; - if !state.hotspots.is_empty() { - for hotspot in state.hotspots.iter() { - string.push_str(format!("\thotspot = {}\n", hotspot).as_str()); - } - } - } - string.push_str("# END DMI\n"); - string - } - pub fn open

(path: P) -> DmiResult - where - P: AsRef, - { - let decoder = Decoder::new(File::open(&path)?); - let reader = decoder.read_info()?; - let chunk = reader - .info() - .compressed_latin1_text - .first() - .ok_or(DmiError::MissingZTXTChunk)?; - let metadata = chunk.get_text()?; - - let mut dmi = Self::new( - path.as_ref().file_stem().unwrap().to_str().unwrap().into(), - 32, - 32, - ); - - dmi.set_metadata(metadata)?; - - let mut reader = ImageReader::open(&path)?; - reader.set_format(image::ImageFormat::Png); - - let mut image = reader.decode()?; - let grid_width = image.width() / dmi.width; - - let mut index = 0; - for state in dmi.states.iter_mut() { - let frame_count = state.frame_count as usize; - if !state.delays.is_empty() { - let delay_count = state.delays.len(); - match delay_count.cmp(&frame_count) { - Ordering::Less => { - let last_delay = *state.delays.last().unwrap(); - let additional_delays = vec![last_delay; frame_count - delay_count]; - state.delays.extend(additional_delays); - } - Ordering::Greater => { - state.delays.truncate(frame_count); - } - _ => {} - } - } else if state.frame_count > 1 { - state.delays = vec![1.; frame_count]; - } - - for _ in 0..state.frame_count { - for _ in 0..state.dirs { - let image = image.crop( - dmi.width * (index % grid_width), - dmi.height * (index / grid_width), - dmi.width, - dmi.height, - ); - if image.width() != dmi.width || image.height() != dmi.height { - return Err(DmiError::ImageSizeMismatch); - } - state.frames.push(image); - index += 1; - } - } - } - - Ok(dmi) - } - pub fn save

(&self, path: P) -> DmiResult<()> - where - P: AsRef, - { - let total_frames = self - .states - .iter() - .map(|state| state.frames.len() as u32) - .sum::() as usize; - - let (sqrt, width, height) = optimal_size(total_frames, self.width, self.height); - - let mut image_buffer = ImageBuffer::new(width, height); - - let mut index: u32 = 0; - for state in self.states.iter() { - for frame in state.frames.iter() { - let (x, y) = ( - (index as f32 % sqrt) as u32 * self.width, - (index as f32 / sqrt) as u32 * self.height, - ); - imageops::replace(&mut image_buffer, frame, x as i64, y as i64); - index += 1; - } - } - - if let Some(parent) = path.as_ref().parent() { - if !parent.exists() { - create_dir_all(parent)?; - } - } - - let mut writer = BufWriter::new(File::create(path)?); - let mut encoder = Encoder::new(&mut writer, width, height); - - encoder.set_compression(Compression::Best); - encoder.set_color(png::ColorType::Rgba); - encoder.set_depth(png::BitDepth::Eight); - - encoder.add_ztxt_chunk("Description".to_string(), self.get_metadata())?; - - let mut writer = encoder.write_header()?; - - writer.write_image_data(&image_buffer)?; - - Ok(()) - } - pub fn to_serialized

(&self, path: P, exact_path: bool) -> DmiResult - where - P: AsRef, - { - let mut path = path.as_ref().to_path_buf(); - - if !exact_path { - path = find_directory(path.join(&self.name)); - } - - if path.exists() { - remove_dir_all(&path)?; - } - - create_dir_all(&path)?; - - let mut states = Vec::new(); - - for state in self.states.iter() { - states.push(state.to_serialized(&path)?); - } - - Ok(SerializedDmi { - name: self.name.clone(), - width: self.width, - height: self.height, - states, - temp: path.to_str().unwrap().to_string(), - }) - } - pub fn from_serialized(serialized: SerializedDmi) -> DmiResult { - if !Path::new(&serialized.temp).exists() { - return Err(DmiError::DirDoesNotExist); - } - - let mut states = Vec::new(); - - for state in serialized.states { - states.push(State::from_serialized(state, &serialized.temp)?); - } - - Ok(Self { - name: serialized.name, - width: serialized.width, - height: serialized.height, - states, - }) - } - pub fn resize(&mut self, width: u32, height: u32, method: image::imageops::FilterType) { - self.width = width; - self.height = height; - for state in self.states.iter_mut() { - state.resize(width, height, method); - } - } - pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) { - self.width = width; - self.height = height; - for state in self.states.iter_mut() { - state.crop(x, y, width, height); - } - } - pub fn expand(&mut self, x: u32, y: u32, width: u32, height: u32) { - self.width = width; - self.height = height; - for state in self.states.iter_mut() { - state.expand(x, y, width, height); - } - } -} - -#[derive(Debug)] -pub struct State { - pub name: String, - pub dirs: u32, - pub frames: Vec, - pub frame_count: u32, - pub delays: Vec, - pub loop_: u32, - pub rewind: bool, - pub movement: bool, - pub hotspots: Vec, -} - -impl State { - fn new(name: String) -> Self { - State { - name, - dirs: 1, - frames: Vec::new(), - frame_count: 0, - delays: Vec::new(), - loop_: 0, - rewind: false, - movement: false, - hotspots: Vec::new(), - } - } - pub fn new_blank(name: String, width: u32, height: u32) -> Self { - let mut state = Self::new(name); - state.frames.push(DynamicImage::new_rgba8(width, height)); - state.frame_count = 1; - state - } - pub fn to_serialized

(&self, path: P) -> DmiResult - where - P: AsRef, - { - let path = Path::new(&path); - - if !path.exists() { - create_dir_all(path)?; - } - - let frame_key: String; - - { - let mut index = 1u32; - let mut path = path.join(".bytes"); - loop { - let frame_key_ = format!("{}.{}", self.name, index); - path.set_file_name(format!("{frame_key_}.0.bytes")); - if !path.exists() { - frame_key = frame_key_; - break; - } - index += 1; - } - } - - let mut index: u32 = 0; - for frame in 0..self.frame_count { - for direction in 0..self.dirs { - let image = &self.frames[(frame * self.dirs + direction) as usize]; - let path = Path::new(&path).join(format!("{frame_key}.{index}.bytes")); - save_image_as_bytes(image, &path)?; - index += 1; - } - } - - Ok(SerializedState { - name: self.name.clone(), - dirs: self.dirs, - frame_key, - frame_count: self.frame_count, - delays: self.delays.clone(), - loop_: self.loop_, - rewind: self.rewind, - movement: self.movement, - hotspots: self.hotspots.clone(), - }) - } - pub fn from_serialized

(serialized: SerializedState, path: P) -> DmiResult - where - P: AsRef, - { - let mut frames = Vec::new(); - - for frame in 0..(serialized.frame_count * serialized.dirs) { - let path = Path::new(&path).join(format!("{}.{}.bytes", serialized.frame_key, frame)); - let image = load_image_from_bytes(path)?; - frames.push(image); - } - - Ok(Self { - name: serialized.name, - dirs: serialized.dirs, - frames, - frame_count: serialized.frame_count, - delays: serialized.delays, - loop_: serialized.loop_, - rewind: serialized.rewind, - movement: serialized.movement, - hotspots: serialized.hotspots, - }) - } - pub fn into_clipboard(self) -> DmiResult { - let frames = self - .frames - .iter() - .map(image_to_base64) - .collect::, _>>()?; - - Ok(ClipboardState { - name: self.name, - dirs: self.dirs, - frames, - delays: self.delays, - loop_: self.loop_, - rewind: self.rewind, - movement: self.movement, - hotspots: self.hotspots, - }) - } - pub fn from_clipboard(state: ClipboardState, width: u32, height: u32) -> DmiResult { - let mut frames = Vec::new(); - - for frame in state.frames.iter() { - let base64 = frame - .split(',') - .nth(1) - .ok_or_else(|| DmiError::MissingData)?; - let image_data = general_purpose::STANDARD.decode(base64)?; - let reader = ImageReader::with_format(Cursor::new(image_data), image::ImageFormat::Png); - let mut image = reader.decode()?; - - if image.width() != width || image.height() != height { - image = image.resize(width, height, imageops::FilterType::Nearest); - } - - frames.push(image); - } - - let frame_count = state.frames.len() as u32 / state.dirs; - - Ok(Self { - name: state.name, - dirs: state.dirs, - frames, - frame_count, - delays: state.delays, - loop_: state.loop_, - rewind: state.rewind, - movement: state.movement, - hotspots: state.hotspots, - }) - } - pub fn resize(&mut self, width: u32, height: u32, method: imageops::FilterType) { - for frame in self.frames.iter_mut() { - *frame = frame.resize_exact(width, height, method); - } - } - pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) { - for frame in self.frames.iter_mut() { - *frame = frame.crop(x, y, width, height); - } - } - pub fn expand(&mut self, x: u32, y: u32, width: u32, height: u32) { - for frame in self.frames.iter_mut() { - let mut bottom = DynamicImage::new_rgba8(width, height); - imageops::replace(&mut bottom, frame, x as i64, y as i64); - *frame = bottom; - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct SerializedDmi { - pub name: String, - pub width: u32, - pub height: u32, - pub states: Vec, - pub temp: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct SerializedState { - pub name: String, - pub dirs: u32, - pub frame_key: String, - pub frame_count: u32, - pub delays: Vec, - pub loop_: u32, - pub rewind: bool, - pub movement: bool, - pub hotspots: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ClipboardState { - pub name: String, - pub dirs: u32, - pub frames: Vec, - pub delays: Vec, - pub loop_: u32, - pub rewind: bool, - pub movement: bool, - pub hotspots: Vec, -} - -type DmiResult = Result; - -#[derive(Error, Debug)] -#[error(transparent)] -pub enum DmiError { - Anyhow(#[from] anyhow::Error), - Io(#[from] std::io::Error), - Image(#[from] image::ImageError), - PngDecoding(#[from] png::DecodingError), - PngEncoding(#[from] png::EncodingError), - ParseInt(#[from] std::num::ParseIntError), - ParseFloat(#[from] std::num::ParseFloatError), - DecodeError(#[from] base64::DecodeError), - #[error("Missing data")] - MissingData, - #[error("Missing ZTXT chunk")] - MissingZTXTChunk, - #[error("Missing metadata header")] - MissingMetadataHeader, - #[error("Invalid metadata version")] - InvalidMetadataVersion, - #[error("Missing metadata value")] - MissingMetadataValue, - #[error("State info out of order")] - OutOfOrderStateInfo, - #[error("Unknown metadata key")] - UnknownMetadataKey, - #[error("Failed to find available directory")] - ImageSizeMismatch, - #[error("Failed to find available directory")] - FindDirError, - #[error("Directory does not exist")] - DirDoesNotExist, -} - -fn save_image_as_bytes>(image: &DynamicImage, path: P) -> DmiResult<()> { - let mut bytes = Vec::new(); - - let width = image.width().to_string(); - let height = image.height().to_string(); - let width_bytes = width.as_bytes(); - let height_bytes = height.as_bytes(); - - bytes.extend_from_slice(width_bytes); - bytes.push(0x0A); - bytes.extend_from_slice(height_bytes); - bytes.push(0x0A); - - for pixel in image.to_rgba8().pixels() { - bytes.push(pixel[0]); - bytes.push(pixel[1]); - bytes.push(pixel[2]); - bytes.push(pixel[3]); - } - - let mut writer = BufWriter::new(File::create(path)?); - writer.write_all(&bytes)?; - - Ok(()) -} - -fn load_image_from_bytes>(path: P) -> DmiResult { - let mut bytes = Vec::new(); - - let mut file = File::open(path)?; - file.read_to_end(&mut bytes)?; - - let mut width = String::new(); - let mut height = String::new(); - let mut index = 0; - - while bytes[index] != 0x0A { - width.push(bytes[index] as char); - index += 1; - } - - index += 1; - - while bytes[index] != 0x0A { - height.push(bytes[index] as char); - index += 1; - } - - index += 1; - - let width = width.parse()?; - let height = height.parse()?; - - let mut image_buffer: ImageBuffer, Vec> = ImageBuffer::new(width, height); - - for pixel in image_buffer.pixels_mut() { - pixel[0] = bytes[index]; - pixel[1] = bytes[index + 1]; - pixel[2] = bytes[index + 2]; - pixel[3] = bytes[index + 3]; - index += 4; - } - - Ok(DynamicImage::ImageRgba8(image_buffer)) -} +use base64::{engine::general_purpose, Engine as _}; +use image::io::Reader; +use image::{imageops, ImageBuffer, Rgba}; +use image::DynamicImage; +use png::{Compression, Decoder, Encoder}; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::fs::{create_dir_all, remove_dir_all, File}; +use std::io::{BufWriter, Cursor, Read as _, Write as _}; +use std::path::Path; +use thiserror::Error; + +use crate::utils::{find_directory, image_to_base64, optimal_size}; + +const DMI_VERSION: &str = "4.0"; + +#[derive(Debug)] +pub struct Dmi { + pub name: String, + pub width: u32, + pub height: u32, + pub states: Vec, +} + +impl Dmi { + pub fn new(name: String, width: u32, height: u32) -> Dmi { + Dmi { + name, + width, + height, + states: Vec::new(), + } + } + pub fn set_metadata(&mut self, metadata: String) -> DmiResult<()> { + let mut lines = metadata.lines(); + + if lines.next().ok_or(DmiError::MissingMetadataHeader)? != "# BEGIN DMI" { + return Err(DmiError::MissingMetadataHeader); + } + + if lines.next().ok_or(DmiError::InvalidMetadataVersion)? + != format!("version = {}", DMI_VERSION) + { + return Err(DmiError::InvalidMetadataVersion); + } + + for line in lines { + if line == "# END DMI" { + break; + } + + let mut split = line.trim().split(" = "); + let (key, value) = ( + split.next().ok_or(DmiError::MissingMetadataValue)?, + split.next().ok_or(DmiError::MissingMetadataValue)?, + ); + + match key { + "width" => self.width = value.parse()?, + "height" => self.height = value.parse()?, + "state" => self.states.push(State::new(value.trim_matches('"').into())), + "dirs" => { + self.states + .last_mut() + .ok_or(DmiError::OutOfOrderStateInfo)? + .dirs = value.parse()?; + } + "frames" => { + self.states + .last_mut() + .ok_or(DmiError::OutOfOrderStateInfo)? + .frame_count = value.parse()?; + } + "delay" => { + self.states + .last_mut() + .ok_or(DmiError::OutOfOrderStateInfo)? + .delays = value + .split(',') + .map(|delay| delay.parse()) + .collect::>()?; + } + "loop" => { + self.states + .last_mut() + .ok_or(DmiError::OutOfOrderStateInfo)? + .loop_ = value.parse()?; + } + "rewind" => { + self.states + .last_mut() + .ok_or(DmiError::OutOfOrderStateInfo)? + .rewind = value == "1"; + } + "movement" => { + self.states + .last_mut() + .ok_or(DmiError::OutOfOrderStateInfo)? + .movement = value == "1"; + } + "hotspot" => { + self.states + .last_mut() + .ok_or(DmiError::OutOfOrderStateInfo)? + .hotspots + .push(value.into()); + } + _ => return Err(DmiError::UnknownMetadataKey), + } + } + + Ok(()) + } + pub fn get_metadata(&self) -> String { + let mut string = String::new(); + string.push_str("# BEGIN DMI\n"); + string.push_str(format!("version = {}\n", DMI_VERSION).as_str()); + string.push_str(format!("\twidth = {}\n", self.width).as_str()); + string.push_str(format!("\theight = {}\n", self.height).as_str()); + for state in self.states.iter() { + string.push_str(format!("state = \"{}\"\n", state.name).as_str()); + string.push_str(format!("\tdirs = {}\n", state.dirs).as_str()); + string.push_str(format!("\tframes = {}\n", state.frame_count).as_str()); + if !state.delays.is_empty() { + let delays = state + .delays + .iter() + .map(|delay| delay.to_string()) + .collect::>() + .join(","); + string.push_str(format!("\tdelay = {}\n", delays).as_str()) + }; + if state.loop_ > 0 { + string.push_str(format!("\tloop = {}\n", state.loop_).as_str()) + }; + if state.rewind { + string.push_str(format!("\trewind = {}\n", state.rewind as u32).as_str()) + }; + if state.movement { + string.push_str(format!("\tmovement = {}\n", state.movement as u32).as_str()) + }; + if !state.hotspots.is_empty() { + for hotspot in state.hotspots.iter() { + string.push_str(format!("\thotspot = {}\n", hotspot).as_str()); + } + } + } + string.push_str("# END DMI\n"); + string + } + pub fn open

(path: P) -> DmiResult + where + P: AsRef, + { + let decoder = Decoder::new(File::open(&path)?); + let reader = decoder.read_info()?; + let chunk = reader + .info() + .compressed_latin1_text + .first() + .ok_or(DmiError::MissingZTXTChunk)?; + let metadata = chunk.get_text()?; + + let mut dmi = Self::new( + path.as_ref().file_stem().unwrap().to_str().unwrap().into(), + 32, + 32, + ); + + dmi.set_metadata(metadata)?; + + let mut reader = Reader::open(&path)?; + reader.set_format(image::ImageFormat::Png); + + let mut image = reader.decode()?; + let grid_width = image.width() / dmi.width; + + let mut index = 0; + for state in dmi.states.iter_mut() { + let frame_count = state.frame_count as usize; + if !state.delays.is_empty() { + let delay_count = state.delays.len(); + match delay_count.cmp(&frame_count) { + Ordering::Less => { + let last_delay = *state.delays.last().unwrap(); + let additional_delays = vec![last_delay; frame_count - delay_count]; + state.delays.extend(additional_delays); + } + Ordering::Greater => { + state.delays.truncate(frame_count); + } + _ => {} + } + } else if state.frame_count > 1 { + state.delays = vec![1.; frame_count]; + } + + for _ in 0..state.frame_count { + for _ in 0..state.dirs { + let image = image.crop( + dmi.width * (index % grid_width), + dmi.height * (index / grid_width), + dmi.width, + dmi.height, + ); + if image.width() != dmi.width || image.height() != dmi.height { + return Err(DmiError::ImageSizeMismatch); + } + state.frames.push(image); + index += 1; + } + } + } + + Ok(dmi) + } + pub fn save

(&self, path: P) -> DmiResult<()> + where + P: AsRef, + { + let total_frames = self + .states + .iter() + .map(|state| state.frames.len() as u32) + .sum::() as usize; + + let (sqrt, width, height) = optimal_size(total_frames, self.width, self.height); + + let mut image_buffer = ImageBuffer::new(width, height); + + let mut index: u32 = 0; + for state in self.states.iter() { + for frame in state.frames.iter() { + let (x, y) = ( + (index as f32 % sqrt) as u32 * self.width, + (index as f32 / sqrt) as u32 * self.height, + ); + imageops::replace(&mut image_buffer, frame, x as i64, y as i64); + index += 1; + } + } + + if let Some(parent) = path.as_ref().parent() { + if !parent.exists() { + create_dir_all(parent)?; + } + } + + let mut writer = BufWriter::new(File::create(path)?); + let mut encoder = Encoder::new(&mut writer, width, height); + + encoder.set_compression(Compression::Best); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + + encoder.add_ztxt_chunk("Description".to_string(), self.get_metadata())?; + + let mut writer = encoder.write_header()?; + + writer.write_image_data(&image_buffer)?; + + Ok(()) + } + pub fn to_serialized

(&self, path: P, exact_path: bool) -> DmiResult + where + P: AsRef, + { + let mut path = path.as_ref().to_path_buf(); + + if !exact_path { + path = find_directory(path.join(&self.name)); + } + + if path.exists() { + remove_dir_all(&path)?; + } + + create_dir_all(&path)?; + + let mut states = Vec::new(); + + for state in self.states.iter() { + states.push(state.to_serialized(&path)?); + } + + Ok(SerializedDmi { + name: self.name.clone(), + width: self.width, + height: self.height, + states, + temp: path.to_str().unwrap().to_string(), + }) + } + pub fn from_serialized(serialized: SerializedDmi) -> DmiResult { + if !Path::new(&serialized.temp).exists() { + return Err(DmiError::DirDoesNotExist); + } + + let mut states = Vec::new(); + + for state in serialized.states { + states.push(State::from_serialized(state, &serialized.temp)?); + } + + Ok(Self { + name: serialized.name, + width: serialized.width, + height: serialized.height, + states, + }) + } + pub fn resize(&mut self, width: u32, height: u32, method: image::imageops::FilterType) { + self.width = width; + self.height = height; + for state in self.states.iter_mut() { + state.resize(width, height, method); + } + } + pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) { + self.width = width; + self.height = height; + for state in self.states.iter_mut() { + state.crop(x, y, width, height); + } + } + pub fn expand(&mut self, x: u32, y: u32, width: u32, height: u32) { + self.width = width; + self.height = height; + for state in self.states.iter_mut() { + state.expand(x, y, width, height); + } + } +} + +#[derive(Debug)] +pub struct State { + pub name: String, + pub dirs: u32, + pub frames: Vec, + pub frame_count: u32, + pub delays: Vec, + pub loop_: u32, + pub rewind: bool, + pub movement: bool, + pub hotspots: Vec, +} + +impl State { + fn new(name: String) -> Self { + State { + name, + dirs: 1, + frames: Vec::new(), + frame_count: 0, + delays: Vec::new(), + loop_: 0, + rewind: false, + movement: false, + hotspots: Vec::new(), + } + } + pub fn new_blank(name: String, width: u32, height: u32) -> Self { + let mut state = Self::new(name); + state.frames.push(DynamicImage::new_rgba8(width, height)); + state.frame_count = 1; + state + } + pub fn to_serialized

(&self, path: P) -> DmiResult + where + P: AsRef, + { + let path = Path::new(&path); + + if !path.exists() { + create_dir_all(path)?; + } + + let frame_key: String; + + { + let mut index = 1u32; + let mut path = path.join(".bytes"); + loop { + let frame_key_ = format!("{}.{}", self.name, index); + path.set_file_name(format!("{frame_key_}.0.bytes")); + if !path.exists() { + frame_key = frame_key_; + break; + } + index += 1; + } + } + + let mut index: u32 = 0; + for frame in 0..self.frame_count { + for direction in 0..self.dirs { + let image = &self.frames[(frame * self.dirs + direction) as usize]; + let path = Path::new(&path).join(format!("{frame_key}.{index}.bytes")); + save_image_as_bytes(image, &path)?; + index += 1; + } + } + + Ok(SerializedState { + name: self.name.clone(), + dirs: self.dirs, + frame_key, + frame_count: self.frame_count, + delays: self.delays.clone(), + loop_: self.loop_, + rewind: self.rewind, + movement: self.movement, + hotspots: self.hotspots.clone(), + }) + } + pub fn from_serialized

(serialized: SerializedState, path: P) -> DmiResult + where + P: AsRef, + { + let mut frames = Vec::new(); + + for frame in 0..(serialized.frame_count * serialized.dirs) { + let path = Path::new(&path).join(format!("{}.{}.bytes", serialized.frame_key, frame)); + let image = load_image_from_bytes(path)?; + frames.push(image); + } + + Ok(Self { + name: serialized.name, + dirs: serialized.dirs, + frames, + frame_count: serialized.frame_count, + delays: serialized.delays, + loop_: serialized.loop_, + rewind: serialized.rewind, + movement: serialized.movement, + hotspots: serialized.hotspots, + }) + } + pub fn into_clipboard(self) -> DmiResult { + let frames = self + .frames + .iter() + .map(image_to_base64) + .collect::, _>>()?; + + Ok(ClipboardState { + name: self.name, + dirs: self.dirs, + frames, + delays: self.delays, + loop_: self.loop_, + rewind: self.rewind, + movement: self.movement, + hotspots: self.hotspots, + }) + } + pub fn from_clipboard(state: ClipboardState, width: u32, height: u32) -> DmiResult { + let mut frames = Vec::new(); + + for frame in state.frames.iter() { + let base64 = frame + .split(',') + .nth(1) + .ok_or_else(|| DmiError::MissingData)?; + let image_data = general_purpose::STANDARD.decode(base64)?; + let reader = Reader::with_format(Cursor::new(image_data), image::ImageFormat::Png); + let mut image = reader.decode()?; + + if image.width() != width || image.height() != height { + image = image.resize(width, height, imageops::FilterType::Nearest); + } + + frames.push(image); + } + + let frame_count = state.frames.len() as u32 / state.dirs; + + Ok(Self { + name: state.name, + dirs: state.dirs, + frames, + frame_count, + delays: state.delays, + loop_: state.loop_, + rewind: state.rewind, + movement: state.movement, + hotspots: state.hotspots, + }) + } + pub fn resize(&mut self, width: u32, height: u32, method: imageops::FilterType) { + for frame in self.frames.iter_mut() { + *frame = frame.resize_exact(width, height, method); + } + } + pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) { + for frame in self.frames.iter_mut() { + *frame = frame.crop(x, y, width, height); + } + } + pub fn expand(&mut self, x: u32, y: u32, width: u32, height: u32) { + for frame in self.frames.iter_mut() { + let mut bottom = DynamicImage::new_rgba8(width, height); + imageops::replace(&mut bottom, frame, x as i64, y as i64); + *frame = bottom; + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SerializedDmi { + pub name: String, + pub width: u32, + pub height: u32, + pub states: Vec, + pub temp: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SerializedState { + pub name: String, + pub dirs: u32, + pub frame_key: String, + pub frame_count: u32, + pub delays: Vec, + pub loop_: u32, + pub rewind: bool, + pub movement: bool, + pub hotspots: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ClipboardState { + pub name: String, + pub dirs: u32, + pub frames: Vec, + pub delays: Vec, + pub loop_: u32, + pub rewind: bool, + pub movement: bool, + pub hotspots: Vec, +} + +type DmiResult = Result; + +#[derive(Error, Debug)] +#[error(transparent)] +pub enum DmiError { + Anyhow(#[from] anyhow::Error), + Io(#[from] std::io::Error), + Image(#[from] image::ImageError), + PngDecoding(#[from] png::DecodingError), + PngEncoding(#[from] png::EncodingError), + ParseInt(#[from] std::num::ParseIntError), + ParseFloat(#[from] std::num::ParseFloatError), + DecodeError(#[from] base64::DecodeError), + #[error("Missing data")] + MissingData, + #[error("Missing ZTXT chunk")] + MissingZTXTChunk, + #[error("Missing metadata header")] + MissingMetadataHeader, + #[error("Invalid metadata version")] + InvalidMetadataVersion, + #[error("Missing metadata value")] + MissingMetadataValue, + #[error("State info out of order")] + OutOfOrderStateInfo, + #[error("Unknown metadata key")] + UnknownMetadataKey, + #[error("Failed to find available directory")] + ImageSizeMismatch, + #[error("Failed to find available directory")] + FindDirError, + #[error("Directory does not exist")] + DirDoesNotExist, +} + +fn save_image_as_bytes>(image: &DynamicImage, path: P) -> DmiResult<()> { + let mut bytes = Vec::new(); + + let width = image.width().to_string(); + let height = image.height().to_string(); + let width_bytes = width.as_bytes(); + let height_bytes = height.as_bytes(); + + bytes.extend_from_slice(width_bytes); + bytes.push(0x0A); + bytes.extend_from_slice(height_bytes); + bytes.push(0x0A); + + for pixel in image.to_rgba8().pixels() { + bytes.push(pixel[0]); + bytes.push(pixel[1]); + bytes.push(pixel[2]); + bytes.push(pixel[3]); + } + + let mut writer = BufWriter::new(File::create(path)?); + writer.write_all(&bytes)?; + + Ok(()) +} + +fn load_image_from_bytes>(path: P) -> DmiResult { + let mut bytes = Vec::new(); + + let mut file = File::open(path)?; + file.read_to_end(&mut bytes)?; + + let mut width = String::new(); + let mut height = String::new(); + let mut index = 0; + + while bytes[index] != 0x0A { + width.push(bytes[index] as char); + index += 1; + } + + index += 1; + + while bytes[index] != 0x0A { + height.push(bytes[index] as char); + index += 1; + } + + index += 1; + + let width = width.parse()?; + let height = height.parse()?; + + let mut image_buffer: ImageBuffer, Vec> = ImageBuffer::new(width, height); + + for pixel in image_buffer.pixels_mut() { + pixel[0] = bytes[index]; + pixel[1] = bytes[index + 1]; + pixel[2] = bytes[index + 2]; + pixel[3] = bytes[index + 3]; + index += 4; + } + + Ok(DynamicImage::ImageRgba8(image_buffer)) +} diff --git a/lib/src/errors.rs b/lib/src/errors.rs index c6180cd..18c992f 100644 --- a/lib/src/errors.rs +++ b/lib/src/errors.rs @@ -1,27 +1,27 @@ -use mlua::ExternalError as _; - -pub enum ExternalError { - Arboard(arboard::Error), - Serde(serde_json::Error), -} - -impl mlua::ExternalError for ExternalError { - fn into_lua_err(self) -> mlua::Error { - match self { - Self::Arboard(err) => err.into_lua_err(), - Self::Serde(err) => err.into_lua_err(), - } - } -} - -impl From for mlua::Error { - fn from(error: ExternalError) -> Self { - error.into_lua_err() - } -} - -impl From for mlua::Error { - fn from(error: crate::dmi::DmiError) -> Self { - error.into_lua_err() - } -} +use mlua::ExternalError as _; + +pub enum ExternalError { + Arboard(arboard::Error), + Serde(serde_json::Error), +} + +impl mlua::ExternalError for ExternalError { + fn to_lua_err(self) -> mlua::Error { + match self { + Self::Arboard(err) => err.to_lua_err(), + Self::Serde(err) => err.to_lua_err(), + } + } +} + +impl From for mlua::Error { + fn from(error: ExternalError) -> Self { + error.to_lua_err() + } +} + +impl From for mlua::Error { + fn from(error: crate::dmi::DmiError) -> Self { + error.to_lua_err() + } +} diff --git a/lib/src/lua.rs b/lib/src/lua.rs index 7c7b51e..dd1b3db 100644 --- a/lib/src/lua.rs +++ b/lib/src/lua.rs @@ -1,354 +1,355 @@ -use mlua::prelude::*; -use std::cmp::Ordering; -use std::ffi::OsStr; -use std::fs::{self, read_dir, remove_dir_all}; -use std::path::Path; - -use crate::dmi::*; -use crate::errors::ExternalError; -use crate::macros::safe; -use crate::utils::check_latest_version; - -#[mlua::lua_module(name = "dmi_module")] -fn module(lua: &Lua) -> LuaResult { - let exports = lua.create_table()?; - - exports.set("new_file", lua.create_function(safe!(new_file))?)?; - exports.set("open_file", lua.create_function(safe!(open_file))?)?; - exports.set("save_file", lua.create_function(safe!(save_file))?)?; - exports.set("new_state", lua.create_function(safe!(new_state))?)?; - exports.set("copy_state", lua.create_function(safe!(copy_state))?)?; - exports.set("paste_state", lua.create_function(safe!(paste_state))?)?; - exports.set("resize", lua.create_function(safe!(resize))?)?; - exports.set("crop", lua.create_function(safe!(crop))?)?; - exports.set("expand", lua.create_function(safe!(expand))?)?; - exports.set("overlay_color", lua.create_function(overlay_color)?)?; - exports.set("remove_dir", lua.create_function(safe!(remove_dir))?)?; - exports.set("exists", lua.create_function(exists)?)?; - exports.set("check_update", lua.create_function(check_update)?)?; - exports.set("open_repo", lua.create_function(safe!(open_repo))?)?; - exports.set("instances", lua.create_function(instances)?)?; - exports.set("save_dialog", lua.create_function(safe!(save_dialog))?)?; - - Ok(exports) -} - -fn new_file( - lua: &Lua, - (name, width, height, temp): (String, u32, u32, String), -) -> LuaResult { - let dmi = Dmi::new(name, width, height).to_serialized(temp, false)?; - let table = dmi.into_lua_table(lua)?; - - Ok(table) -} - -fn open_file(lua: &Lua, (filename, temp): (String, String)) -> LuaResult { - if !Path::new(&filename).is_file() { - Err("File does not exist".to_string()).into_lua_err()? - } - - let dmi = Dmi::open(filename)?.to_serialized(temp, false)?; - let table: LuaTable = dmi.into_lua_table(lua)?; - - Ok(table) -} - -fn save_file(_: &Lua, (dmi, filename): (LuaTable, String)) -> LuaResult { - let dmi = SerializedDmi::from_lua_table(dmi)?; - let dmi = Dmi::from_serialized(dmi)?; - dmi.save(filename)?; - - Ok(LuaValue::Nil) -} - -fn new_state(lua: &Lua, (width, height, temp): (u32, u32, String)) -> LuaResult { - if !Path::new(&temp).exists() { - Err("Temp directory does not exist".to_string()).into_lua_err()? - } - - let state = State::new_blank(String::new(), width, height).to_serialized(temp)?; - let table = state.into_lua_table(lua)?; - - Ok(table) -} - -fn copy_state(_: &Lua, (state, temp): (LuaTable, String)) -> LuaResult { - if !Path::new(&temp).exists() { - Err("Temp directory does not exist".to_string()).into_lua_err()? - } - - let state = SerializedState::from_lua_table(state)?; - let state = State::from_serialized(state, temp)?.into_clipboard()?; - let state = serde_json::to_string(&state).map_err(ExternalError::Serde)?; - - let mut clipboard = arboard::Clipboard::new().map_err(ExternalError::Arboard)?; - clipboard.set_text(state).map_err(ExternalError::Arboard)?; - - Ok(LuaValue::Nil) -} - -fn paste_state(lua: &Lua, (width, height, temp): (u32, u32, String)) -> LuaResult { - if !Path::new(&temp).exists() { - Err("Temp directory does not exist".to_string()).into_lua_err()? - } - - let mut clipboard = arboard::Clipboard::new().map_err(ExternalError::Arboard)?; - let state = clipboard.get_text().map_err(ExternalError::Arboard)?; - let state = serde_json::from_str::(&state).map_err(ExternalError::Serde)?; - let state = State::from_clipboard(state, width, height)?.to_serialized(temp)?; - let table = state.into_lua_table(lua)?; - - Ok(table) -} - -fn resize( - _: &Lua, - (dmi, width, height, method): (LuaTable, u32, u32, String), -) -> LuaResult { - let dmi = SerializedDmi::from_lua_table(dmi)?; - - let temp = dmi.temp.clone(); - let method = match method.as_str() { - "nearest" => image::imageops::FilterType::Nearest, - "triangle" => image::imageops::FilterType::Triangle, - "catmullrom" => image::imageops::FilterType::CatmullRom, - "gaussian" => image::imageops::FilterType::Gaussian, - "lanczos3" => image::imageops::FilterType::Lanczos3, - _ => unreachable!(), - }; - - let mut dmi = Dmi::from_serialized(dmi)?; - dmi.resize(width, height, method); - dmi.to_serialized(temp, true)?; - - Ok(LuaValue::Nil) -} - -fn crop( - _: &Lua, - (dmi, x, y, width, height): (LuaTable, u32, u32, u32, u32), -) -> LuaResult { - let dmi = SerializedDmi::from_lua_table(dmi)?; - let temp = dmi.temp.clone(); - - let mut dmi = Dmi::from_serialized(dmi)?; - dmi.crop(x, y, width, height); - dmi.to_serialized(temp, true)?; - - Ok(LuaValue::Nil) -} - -fn expand( - _: &Lua, - (dmi, x, y, width, height): (LuaTable, u32, u32, u32, u32), -) -> LuaResult { - let dmi = SerializedDmi::from_lua_table(dmi)?; - let temp = dmi.temp.clone(); - - let mut dmi = Dmi::from_serialized(dmi)?; - dmi.expand(x, y, width, height); - dmi.to_serialized(temp, true)?; - - Ok(LuaValue::Nil) -} - -fn overlay_color( - _: &Lua, - (r, g, b, width, height, bytes): (u8, u8, u8, u32, u32, LuaMultiValue), -) -> LuaResult { - use image::{imageops, EncodableLayout, ImageBuffer, Rgba}; - - let mut buf = Vec::new(); - for byte in bytes { - if let LuaValue::Integer(byte) = byte { - buf.push(byte as u8); - } - } - - if let Some(top) = ImageBuffer::from_vec(width, height, buf) { - let mut bottom = ImageBuffer::from_pixel(width, height, Rgba([r, g, b, 255])); - imageops::overlay(&mut bottom, &top, 0, 0); - - let bytes = bottom - .as_bytes() - .iter() - .map(|byte| LuaValue::Integer(*byte as i64)) - .collect(); - - return Ok(LuaMultiValue::from_vec(bytes)); - } - - Ok(LuaMultiValue::from_vec(vec![LuaValue::Nil])) -} - -fn remove_dir(_: &Lua, (path, soft): (String, bool)) -> LuaResult { - let path = Path::new(&path); - - if path.is_dir() { - if !soft { - remove_dir_all(path)?; - } else if read_dir(path)?.next().is_none() { - fs::remove_dir(path)?; - } - } - - Ok(LuaValue::Nil) -} - -fn exists(_: &Lua, path: String) -> LuaResult { - let path = Path::new(&path); - - Ok(path.exists()) -} - -fn save_dialog( - _: &Lua, - (title, filename, location): (String, String, String), -) -> LuaResult { - let dialog = native_dialog::FileDialog::new() - .set_title(&title) - .set_filename(&filename) - .set_location(&location) - .add_filter("dmi files", &["dmi"]); - - if let Ok(Some(file)) = dialog.show_save_single_file() { - if let Some(file) = file.to_str() { - return Ok(file.to_string()); - } - } - - Ok(String::new()) -} - -fn instances(_: &Lua, _: ()) -> LuaResult { - let mut system = sysinfo::System::new(); - let refresh_kind = - sysinfo::ProcessRefreshKind::nothing().with_exe(sysinfo::UpdateKind::OnlyIfNotSet); - system.refresh_processes_specifics(sysinfo::ProcessesToUpdate::All, true, refresh_kind); - - Ok(system.processes_by_name(OsStr::new("aseprite")).count()) -} - -fn check_update(_: &Lua, (): ()) -> LuaResult { - let version = check_latest_version(); - - if let Ok(Ordering::Less) = version { - return Ok(true); - } - - Ok(false) -} - -fn open_repo(_: &Lua, path: Option) -> LuaResult { - let url = if let Some(path) = path { - format!("{}/{}", env!("CARGO_PKG_REPOSITORY"), path) - } else { - env!("CARGO_PKG_REPOSITORY").to_string() - }; - - if webbrowser::open(&url).is_err() { - return Err("Failed to open browser".to_string()).into_lua_err(); - } - - Ok(LuaValue::Nil) -} - -trait IntoLuaTable { - fn into_lua_table(self, lua: &Lua) -> LuaResult; -} - -trait FromLuaTable { - type Result; - fn from_lua_table(table: LuaTable) -> LuaResult; -} - -impl IntoLuaTable for SerializedState { - fn into_lua_table(self, lua: &Lua) -> LuaResult { - let table = lua.create_table()?; - - table.set("name", self.name)?; - table.set("dirs", self.dirs)?; - table.set("frame_key", self.frame_key)?; - table.set("frame_count", self.frame_count)?; - table.set("delays", self.delays)?; - table.set("loop", self.loop_)?; - table.set("rewind", self.rewind)?; - table.set("movement", self.movement)?; - table.set("hotspots", self.hotspots)?; - - Ok(table) - } -} - -impl IntoLuaTable for SerializedDmi { - fn into_lua_table(self, lua: &Lua) -> LuaResult { - let table = lua.create_table()?; - let mut states = Vec::new(); - - for state in self.states.into_iter() { - let table = state.into_lua_table(lua)?; - states.push(table); - } - - table.set("name", self.name)?; - table.set("width", self.width)?; - table.set("height", self.height)?; - table.set("states", states)?; - table.set("temp", self.temp)?; - - Ok(table) - } -} - -impl FromLuaTable for SerializedState { - type Result = SerializedState; - fn from_lua_table(table: LuaTable) -> LuaResult { - let name = table.get::("name")?; - let dirs = table.get::("dirs")?; - let frame_key = table.get::("frame_key")?; - let frame_count = table.get::("frame_count")?; - let delays = table.get::>("delays")?; - let loop_ = table.get::("loop")?; - let rewind = table.get::("rewind")?; - let movement = table.get::("movement")?; - let hotspots = table.get::>("hotspots")?; - - Ok(SerializedState { - name, - dirs, - frame_key, - frame_count, - delays, - loop_, - rewind, - movement, - hotspots, - }) - } -} - -impl FromLuaTable for SerializedDmi { - type Result = SerializedDmi; - fn from_lua_table(table: LuaTable) -> LuaResult { - let name = table.get::("name")?; - let width = table.get::("width")?; - let height = table.get::("height")?; - let states_table = table.get::>("states")?; - let temp = table.get::("temp")?; - - let mut states = Vec::new(); - - for table in states_table { - states.push(SerializedState::from_lua_table(table)?); - } - - Ok(SerializedDmi { - name, - width, - height, - states, - temp, - }) - } -} +use mlua::prelude::*; +use std::cmp::Ordering; +use std::fs::{self, read_dir, remove_dir_all}; +use std::path::Path; + +use sysinfo::SystemExt; + +use crate::dmi::*; +use crate::errors::ExternalError; +use crate::macros::safe; +use crate::utils::check_latest_version; + +#[mlua::lua_module] +fn dmi_module(lua: &Lua) -> LuaResult { + let exports = lua.create_table()?; + + exports.set("new_file", lua.create_function(safe!(new_file))?)?; + exports.set("open_file", lua.create_function(safe!(open_file))?)?; + exports.set("save_file", lua.create_function(safe!(save_file))?)?; + exports.set("new_state", lua.create_function(safe!(new_state))?)?; + exports.set("copy_state", lua.create_function(safe!(copy_state))?)?; + exports.set("paste_state", lua.create_function(safe!(paste_state))?)?; + exports.set("resize", lua.create_function(safe!(resize))?)?; + exports.set("crop", lua.create_function(safe!(crop))?)?; + exports.set("expand", lua.create_function(safe!(expand))?)?; + exports.set("overlay_color", lua.create_function(overlay_color)?)?; + exports.set("remove_dir", lua.create_function(safe!(remove_dir))?)?; + exports.set("exists", lua.create_function(exists)?)?; + exports.set("check_update", lua.create_function(check_update)?)?; + exports.set("open_repo", lua.create_function(safe!(open_repo))?)?; + exports.set("instances", lua.create_function(instances)?)?; + exports.set("save_dialog", lua.create_function(safe!(save_dialog))?)?; + + Ok(exports) +} + +fn new_file( + lua: &Lua, + (name, width, height, temp): (String, u32, u32, String), +) -> LuaResult { + let dmi = Dmi::new(name, width, height).to_serialized(temp, false)?; + let table = dmi.into_lua_table(lua)?; + + Ok(table) +} + +fn open_file(lua: &Lua, (filename, temp): (String, String)) -> LuaResult { + if !Path::new(&filename).is_file() { + Err("File does not exist".to_string()).to_lua_err()? + } + + let dmi = Dmi::open(filename)?.to_serialized(temp, false)?; + let table: LuaTable = dmi.into_lua_table(lua)?; + + Ok(table) +} + +fn save_file<'lua>(_: &'lua Lua, (dmi, filename): (LuaTable, String)) -> LuaResult> { + let dmi = SerializedDmi::from_lua_table(dmi)?; + let dmi = Dmi::from_serialized(dmi)?; + dmi.save(filename)?; + + Ok(LuaValue::Nil) +} + +fn new_state(lua: &Lua, (width, height, temp): (u32, u32, String)) -> LuaResult { + if !Path::new(&temp).exists() { + Err("Temp directory does not exist".to_string()).to_lua_err()? + } + + let state = State::new_blank(String::new(), width, height).to_serialized(temp)?; + let table = state.into_lua_table(lua)?; + + Ok(table) +} + +fn copy_state<'lua>(_: &'lua Lua, (state, temp): (LuaTable, String)) -> LuaResult> { + if !Path::new(&temp).exists() { + Err("Temp directory does not exist".to_string()).to_lua_err()? + } + + let state = SerializedState::from_lua_table(state)?; + let state = State::from_serialized(state, temp)?.into_clipboard()?; + let state = serde_json::to_string(&state).map_err(ExternalError::Serde)?; + + let mut clipboard = arboard::Clipboard::new().map_err(ExternalError::Arboard)?; + clipboard.set_text(state).map_err(ExternalError::Arboard)?; + + Ok(LuaValue::Nil) +} + +fn paste_state(lua: &Lua, (width, height, temp): (u32, u32, String)) -> LuaResult { + if !Path::new(&temp).exists() { + Err("Temp directory does not exist".to_string()).to_lua_err()? + } + + let mut clipboard = arboard::Clipboard::new().map_err(ExternalError::Arboard)?; + let state = clipboard.get_text().map_err(ExternalError::Arboard)?; + let state = serde_json::from_str::(&state).map_err(ExternalError::Serde)?; + let state = State::from_clipboard(state, width, height)?.to_serialized(temp)?; + let table = state.into_lua_table(lua)?; + + Ok(table) +} + +fn resize<'lua>( + _: &Lua, + (dmi, width, height, method): (LuaTable, u32, u32, String), +) -> LuaResult> { + let dmi = SerializedDmi::from_lua_table(dmi)?; + + let temp = dmi.temp.clone(); + let method = match method.as_str() { + "nearest" => image::imageops::FilterType::Nearest, + "triangle" => image::imageops::FilterType::Triangle, + "catmullrom" => image::imageops::FilterType::CatmullRom, + "gaussian" => image::imageops::FilterType::Gaussian, + "lanczos3" => image::imageops::FilterType::Lanczos3, + _ => unreachable!(), + }; + + let mut dmi = Dmi::from_serialized(dmi)?; + dmi.resize(width, height, method); + dmi.to_serialized(temp, true)?; + + Ok(LuaValue::Nil) +} + +fn crop<'lua>( + _: &Lua, + (dmi, x, y, width, height): (LuaTable, u32, u32, u32, u32), +) -> LuaResult> { + let dmi = SerializedDmi::from_lua_table(dmi)?; + let temp = dmi.temp.clone(); + + let mut dmi = Dmi::from_serialized(dmi)?; + dmi.crop(x, y, width, height); + dmi.to_serialized(temp, true)?; + + Ok(LuaValue::Nil) +} + +fn expand<'lua>( + _: &Lua, + (dmi, x, y, width, height): (LuaTable, u32, u32, u32, u32), +) -> LuaResult> { + let dmi = SerializedDmi::from_lua_table(dmi)?; + let temp = dmi.temp.clone(); + + let mut dmi = Dmi::from_serialized(dmi)?; + dmi.expand(x, y, width, height); + dmi.to_serialized(temp, true)?; + + Ok(LuaValue::Nil) +} + +fn overlay_color<'lua>( + _: &Lua, + (r, g, b, width, height, bytes): (u8, u8, u8, u32, u32, LuaMultiValue), +) -> LuaResult> { + use image::{imageops, EncodableLayout, ImageBuffer, Rgba}; + + let mut buf = Vec::new(); + for byte in bytes { + if let LuaValue::Integer(byte) = byte { + buf.push(byte as u8); + } + } + + if let Some(top) = ImageBuffer::from_vec(width, height, buf) { + let mut bottom = ImageBuffer::from_pixel(width, height, Rgba([r, g, b, 255])); + imageops::overlay(&mut bottom, &top, 0, 0); + + let bytes = bottom + .as_bytes() + .iter() + .map(|byte| LuaValue::Integer(*byte as i64)) + .collect(); + + return Ok(LuaMultiValue::from_vec(bytes)); + } + + Ok(LuaMultiValue::from_vec(vec![LuaValue::Nil])) +} + +fn remove_dir(_: &Lua, (path, soft): (String, bool)) -> LuaResult { + let path = Path::new(&path); + + if path.is_dir() { + if !soft { + remove_dir_all(path)?; + } else if read_dir(path)?.next().is_none() { + fs::remove_dir(path)?; + } + } + + Ok(LuaValue::Nil) +} + +fn exists(_: &Lua, path: String) -> LuaResult { + let path = Path::new(&path); + + Ok(path.exists()) +} + +fn save_dialog( + _: &Lua, + (title, filename, location): (String, String, String), +) -> LuaResult { + let dialog = native_dialog::FileDialog::new() + .set_title(&title) + .set_filename(&filename) + .set_location(&location) + .add_filter("dmi files", &["dmi"]); + + if let Ok(Some(file)) = dialog.show_save_single_file() { + if let Some(file) = file.to_str() { + return Ok(file.to_string()); + } + } + + Ok(String::new()) +} + +fn instances(_: &Lua, _: ()) -> LuaResult { + let mut system = sysinfo::System::new(); + let refresh_kind = + sysinfo::ProcessRefreshKind::everything(); + system.refresh_processes_specifics(refresh_kind); + + Ok(system.processes_by_name("aseprite").count()) +} + +fn check_update(_: &Lua, (): ()) -> LuaResult { + let version = check_latest_version(); + + if let Ok(Ordering::Less) = version { + return Ok(true); + } + + Ok(false) +} + +fn open_repo(_: &Lua, path: Option) -> LuaResult { + let url = if let Some(path) = path { + format!("{}/{}", env!("CARGO_PKG_REPOSITORY"), path) + } else { + env!("CARGO_PKG_REPOSITORY").to_string() + }; + + if webbrowser::open(&url).is_err() { + return Err("Failed to open browser".to_string()).to_lua_err(); + } + + Ok(LuaValue::Nil) +} + +trait IntoLuaTable { + fn into_lua_table(self, lua: &Lua) -> LuaResult; +} + +trait FromLuaTable { + type Result; + fn from_lua_table(table: LuaTable) -> LuaResult; +} + +impl IntoLuaTable for SerializedState { + fn into_lua_table(self, lua: &Lua) -> LuaResult { + let table = lua.create_table()?; + + table.set("name", self.name)?; + table.set("dirs", self.dirs)?; + table.set("frame_key", self.frame_key)?; + table.set("frame_count", self.frame_count)?; + table.set("delays", self.delays)?; + table.set("loop", self.loop_)?; + table.set("rewind", self.rewind)?; + table.set("movement", self.movement)?; + table.set("hotspots", self.hotspots)?; + + Ok(table) + } +} + +impl IntoLuaTable for SerializedDmi { + fn into_lua_table(self, lua: &Lua) -> LuaResult { + let table = lua.create_table()?; + let mut states = Vec::new(); + + for state in self.states.into_iter() { + let table = state.into_lua_table(lua)?; + states.push(table); + } + + table.set("name", self.name)?; + table.set("width", self.width)?; + table.set("height", self.height)?; + table.set("states", states)?; + table.set("temp", self.temp)?; + + Ok(table) + } +} + +impl FromLuaTable for SerializedState { + type Result = SerializedState; + fn from_lua_table(table: LuaTable) -> LuaResult { + let name = table.get::<&str, String>("name")?; + let dirs = table.get::<&str, u32>("dirs")?; + let frame_key = table.get::<&str, String>("frame_key")?; + let frame_count = table.get::<&str, u32>("frame_count")?; + let delays = table.get::<&str, Vec>("delays")?; + let loop_ = table.get::<&str, u32>("loop")?; + let rewind = table.get::<&str, bool>("rewind")?; + let movement = table.get::<&str, bool>("movement")?; + let hotspots = table.get::<&str, Vec>("hotspots")?; + + Ok(SerializedState { + name, + dirs, + frame_key, + frame_count, + delays, + loop_, + rewind, + movement, + hotspots, + }) + } +} + +impl FromLuaTable for SerializedDmi { + type Result = SerializedDmi; + fn from_lua_table(table: LuaTable) -> LuaResult { + let name = table.get::<&str, String>("name")?; + let width = table.get::<&str, u32>("width")?; + let height = table.get::<&str, u32>("height")?; + let states_table = table.get::<&str, Vec>("states")?; + let temp = table.get::<&str, String>("temp")?; + + let mut states = Vec::new(); + + for table in states_table { + states.push(SerializedState::from_lua_table(table)?); + } + + Ok(SerializedDmi { + name, + width, + height, + states, + temp, + }) + } +} diff --git a/lib/src/macros.rs b/lib/src/macros.rs index 923eeee..35c724c 100644 --- a/lib/src/macros.rs +++ b/lib/src/macros.rs @@ -1,25 +1,25 @@ -use mlua::prelude::*; - -pub fn safe_lua_function<'lua, A, R, F>( - lua: &'lua Lua, - func: F, - multi: A, -) -> LuaResult<(Option, Option)> -where - A: FromLuaMulti, - R: IntoLuaMulti, - F: Fn(&'lua Lua, A) -> LuaResult, -{ - match func(lua, multi) { - Ok(r) => Ok((Some(r), None)), - Err(err) => Ok((None, Some(err.to_string()))), - } -} - -macro_rules! safe { - ($func:ident) => { - |lua, args| $crate::macros::safe_lua_function(lua, $func, args) - }; -} - -pub(crate) use safe; +use mlua::prelude::*; + +pub fn safe_lua_function<'lua, A, R, F>( + lua: &'lua Lua, + func: F, + multi: A, +) -> LuaResult<(Option, Option)> +where + A: FromLuaMulti<'lua>, + R: ToLuaMulti<'lua>, + F: Fn(&'lua Lua, A) -> LuaResult, +{ + match func(lua, multi) { + Ok(r) => Ok((Some(r), None)), + Err(err) => Ok((None, Some(err.to_string()))), + } +} + +macro_rules! safe { + ($func:ident) => { + |lua, args| $crate::macros::safe_lua_function(lua, $func, args) + }; +} + +pub(crate) use safe; diff --git a/package.json b/package.json index a680086..3f9e831 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,35 @@ -{ - "name": "asprite-dmi", - "displayName": "DMI Editor", - "description": "DMI Editor Extension for Aseprite", - "version": "1.1.0", - "contributors": [ - { - "name": "Seefaaa", - "url": "https://github.com/Seefaaa" - }, - { - "name": "Space Station 13 Community", - "url": "https://github.com/spacestation13" - } - ], - "publisher": "Space Station 13 Community", - "license": "GPL-3.0", - "categories": [ "Scripts" ], - "contributes": { - "scripts": [ - { "path": "./scripts/classes/cache.lua" }, - { "path": "./scripts/classes/preferences.lua" }, - { "path": "./scripts/classes/editor/_editor.lua" }, - { "path": "./scripts/classes/editor/rendering.lua" }, - { "path": "./scripts/classes/editor/state.lua" }, - { "path": "./scripts/classes/statesprite.lua" }, - { "path": "./scripts/classes/widget.lua" }, - { "path": "./scripts/functions/other.lua" }, - { "path": "./scripts/functions/string.lua" }, - { "path": "./scripts/functions/table.lua" }, - { "path": "./scripts/constants.lua" }, - { "path": "./scripts/main.lua" } - ] - } -} +{ + "name": "asprite-dmi", + "displayName": "DMI Editor", + "description": "DMI Editor Extension for Aseprite", + "version": "1.1.0", + "contributors": [ + { + "name": "Seefaaa", + "url": "https://github.com/Seefaaa" + }, + { + "name": "Space Station 13 Community", + "url": "https://github.com/spacestation13" + } + ], + "publisher": "Space Station 13 Community", + "license": "GPL-3.0", + "categories": [ "Scripts" ], + "contributes": { + "scripts": [ + { "path": "./scripts/classes/cache.lua" }, + { "path": "./scripts/classes/preferences.lua" }, + { "path": "./scripts/classes/editor/_editor.lua" }, + { "path": "./scripts/classes/editor/rendering.lua" }, + { "path": "./scripts/classes/editor/state.lua" }, + { "path": "./scripts/classes/statesprite.lua" }, + { "path": "./scripts/classes/widget.lua" }, + { "path": "./scripts/functions/other.lua" }, + { "path": "./scripts/functions/string.lua" }, + { "path": "./scripts/functions/table.lua" }, + { "path": "./scripts/constants.lua" }, + { "path": "./scripts/main.lua" } + ] + } +} diff --git a/scripts/constants.lua b/scripts/constants.lua index e28bead..a5aa16e 100644 --- a/scripts/constants.lua +++ b/scripts/constants.lua @@ -1,13 +1,25 @@ -------------------- CONSTANTS ------------------- - -DIRECTION_NAMES = { "South", "North", "East", "West", "Southeast", "Southwest", "Northeast", "Northwest" } -DIALOG_NAME = "DMI Editor" -TEMP_NAME = "aseprite-dmi" -LUA_LIB = app.fs.pathSeparator ~= "/" and "lua54" or nil -DMI_LIB = app.fs.pathSeparator ~= "/" and "dmi" or "libdmi" -TEMP_DIR = app.fs.joinPath(app.fs.tempPath, TEMP_NAME) - -COMMON_STATE = { - normal = { part = "sunken_normal", color = "button_normal_text" }, - hot = { part = "sunken_focused", color = "button_hot_text" }, -} --[[@as WidgetState]] +------------------- CONSTANTS ------------------- + +DIRECTION_NAMES = { "South", "North", "East", "West", "Southeast", "Southwest", "Northeast", "Northwest" } +DIALOG_NAME = "DMI Editor" +TEMP_NAME = "aseprite-dmi" + +-- OS-specific library extensions and names +local function get_lib_info() + if app.os.windows then + return "lua54", "dmi" + elseif app.os.macos then + return "lua54", "libdmi.dylib" + else -- Linux + return "lua54", "libdmi.so" + end +end + +LUA_LIB, DMI_LIB = get_lib_info() + +TEMP_DIR = app.fs.joinPath(app.fs.tempPath, TEMP_NAME) + +COMMON_STATE = { + normal = { part = "sunken_normal", color = "button_normal_text" }, + hot = { part = "sunken_focused", color = "button_hot_text" }, +} --[[@as WidgetState]] diff --git a/scripts/functions/other.lua b/scripts/functions/other.lua index 2affef5..1e17f24 100644 --- a/scripts/functions/other.lua +++ b/scripts/functions/other.lua @@ -1,54 +1,54 @@ ---- @diagnostic disable: lowercase-global - ---- Finds the first transparent color in the given image. ---- @param image Image The image to search for transparent color. ---- @return Color color The first transparent color found in the image, or a fully transparent black color if no transparent color is found. -function transparent_color(image) - for it in image:pixels() do - local color = Color(it()) - if color.alpha == 0 then - if color.index == 0 then - return color - end - end - end - return Color(0) -end - ---- Function to load image from bytes file. ---- Thanks to `Astropulse` for sharing [this](https://community.aseprite.org/t/loading-ui-images-for-graphicscontext-elements-at-lightning-speed/21128) article. ---- @param file string The path to the file. ---- @return Image image The image loaded from the file. -function load_image_bytes(file) - local file = io.open(file, "rb") - - assert(file, "File not found") - - local width = tonumber(file:read("*line")) - local height = tonumber(file:read("*line")) - local bytes = file:read("*a") - - file:close() - - assert(width and height and bytes, "Invalid file") - - image = Image(width, height) - image.bytes = bytes - - return image -end - ---- Function to save image to bytes file. ---- @param image Image The image to save. ---- @param file string The path to the file. -function save_image_bytes(image, file) - local file = io.open(file, "wb") - - assert(file, "File not found") - - file:write(image.width .. "\n") - file:write(image.height .. "\n") - file:write(image.bytes) - - file:close() -end +--- @diagnostic disable: lowercase-global + +--- Finds the first transparent color in the given image. +--- @param image Image The image to search for transparent color. +--- @return Color color The first transparent color found in the image, or a fully transparent black color if no transparent color is found. +function transparent_color(image) + for it in image:pixels() do + local color = Color(it()) + if color.alpha == 0 then + if color.index == 0 then + return color + end + end + end + return Color(0) +end + +--- Function to load image from bytes file. +--- Thanks to `Astropulse` for sharing [this](https://community.aseprite.org/t/loading-ui-images-for-graphicscontext-elements-at-lightning-speed/21128) article. +--- @param file string The path to the file. +--- @return Image image The image loaded from the file. +function load_image_bytes(file) + local file = io.open(file, "rb") + + assert(file, "File not found") + + local width = tonumber(file:read("*line")) + local height = tonumber(file:read("*line")) + local bytes = file:read("*a") + + file:close() + + assert(width and height and bytes, "Invalid file") + + image = Image(width, height) + image.bytes = bytes + + return image +end + +--- Function to save image to bytes file. +--- @param image Image The image to save. +--- @param file string The path to the file. +function save_image_bytes(image, file) + local file = io.open(file, "wb") + + assert(file, "File not found") + + file:write(image.width .. "\n") + file:write(image.height .. "\n") + file:write(image.bytes) + + file:close() +end diff --git a/scripts/main.lua b/scripts/main.lua index f0da76a..f9fd3e9 100644 --- a/scripts/main.lua +++ b/scripts/main.lua @@ -1,289 +1,328 @@ ---- @diagnostic disable: lowercase-global - ---- After command listener. ---- @type number|nil -local after_listener = nil - ---- Before command listener. ---- @type number|nil -local before_listener = nil - ---- Aseprite is exiting. -local exiting = false - ---- Open editors. ---- @type Editor[] -open_editors = {} - ---- Lib module. ---- @type LibDmi -libdmi = nil - ---- Tracks if we're doing a no-editor DMI open -local opening_dmi_noeditor = false - ---- Initializes the plugin. Called when the plugin is loaded. ---- @param plugin Plugin The plugin object. -function init(plugin) - if app.apiVersion < 27 then - return app.alert("This script requires Aseprite v1.3.3 or above") - end - - if not app.isUIAvailable then - return - end - - -- Initialize Preferences - Preferences.initialize(plugin) - - after_listener = app.events:on("aftercommand", function(ev) - if ev.name == "OpenFile" then - -- Skip DMI editor if coming from Raw Open command - if app.sprite and app.sprite.filename:ends_with(".dmi") and not opening_dmi_noeditor then - local filename = app.sprite.filename - app.command.CloseFile { ui = false } - - loadlib(plugin.path) - - Editor.new(DIALOG_NAME, filename) - end - -- Reset the flag after handling the OpenFile event - opening_dmi_noeditor = false - elseif ev.name == "Exit" then - exiting = true - end - end) - - before_listener = app.events:on("beforecommand", function(ev) - if ev.name == "Exit" then - local stopped = false - if #open_editors > 0 then - local editors = table.clone(open_editors) --[[@as Editor[] ]] - for _, editor in ipairs(editors) do - if not editor:close(false) and not stopped then - stopped = true - ev.stopPropagation() - end - end - end - end - end) - - local is_state_sprite = function() - for _, editor in ipairs(open_editors) do - for _, sprite in ipairs(editor.open_sprites) do - if app.sprite == sprite.sprite then - return sprite - end - end - end - return nil - end - - plugin:newMenuSeparator { - group = "file_import", - } - - plugin:newMenuGroup { - id = "dmi_editor", - title = DIALOG_NAME, - group = "file_import", - } - - plugin:newCommand { - id = "dmi_new_file", - title = "New DMI File", - group = "dmi_editor", - onclick = function() - Editor.new_file(plugin.path) - end, - } - - plugin:newCommand { - id = "dmi_raw_open", - title = "Open DMI (No Editor - Will Delete DMI Metadata!!)", - group = "dmi_editor", - onclick = function() - opening_dmi_noeditor = true - app.command.OpenFile() - end, - } - - plugin:newMenuSeparator { - group = "dmi_editor", - } - - plugin:newCommand { - id = "dmi_expand", - title = "Expand", - group = "dmi_editor", - onclick = function() - local state_sprite = is_state_sprite() - if state_sprite then - state_sprite.editor:expand() - end - end, - onenabled = function() - return is_state_sprite() and true or false - end, - } - - plugin:newCommand { - id = "dmi_resize", - title = "Resize", - group = "dmi_editor", - onclick = function() - local state_sprite = is_state_sprite() - if state_sprite then - state_sprite.editor:resize() - end - end, - onenabled = function() - return is_state_sprite() and true or false - end, - } - - plugin:newCommand { - id = "dmi_crop", - title = "Crop", - group = "dmi_editor", - onclick = function() - local state_sprite = is_state_sprite() - if state_sprite then - state_sprite.editor:crop() - end - end, - onenabled = function() - return is_state_sprite() and true or false - end, - } - - plugin:newMenuSeparator { - group = "dmi_editor", - } - - plugin:newCommand { - id = "dmi_preferences", - title = "Preferences", - group = "dmi_editor", - onclick = function() - Preferences.show(plugin) - end, - } - - plugin:newCommand { - id = "dmi_report_issue", - title = "Report Issue", - group = "dmi_editor", - onclick = function() - loadlib(plugin.path) - libdmi.open_repo("issues") - end, - } - - plugin:newCommand { - id = "dmi_releases", - title = "Releases", - group = "dmi_editor", - onclick = function() - loadlib(plugin.path) - libdmi.open_repo("releases") - end, - } -end - ---- Exits the plugin. Called when the plugin is removed or Aseprite is closed. ---- @param plugin Plugin The plugin object. -function exit(plugin) - if not exiting and libdmi then - print( - "To uninstall the extension, re-open the Aseprite without using the extension and try again.\nThis happens beacuse once the library (dll) is loaded, it cannot be unloaded.\n") - return - end - if after_listener then - app.events:off(after_listener) - after_listener = nil - end - if before_listener then - app.events:off(before_listener) - before_listener = nil - end - if #open_editors > 0 then - local editors = table.clone(open_editors) --[[@as Editor[] ]] - for _, editor in ipairs(editors) do - editor:close(false, true) - end - end - if libdmi then - libdmi.remove_dir(TEMP_DIR, true) - if libdmi.exists(TEMP_DIR) and libdmi.instances() == 1 then - libdmi.remove_dir(TEMP_DIR, false) - end - libdmi = nil - end -end - ---- Loads the DMI library. ---- @param plugin_path string Path where the extension is installed. -function loadlib(plugin_path) - if not libdmi then - if app.fs.pathSeparator ~= "/" then - package.loadlib(app.fs.joinPath(plugin_path, LUA_LIB --[[@as string]]), "") - else - package.cpath = package.cpath .. ";?.dylib" - end - libdmi = package.loadlib(app.fs.joinPath(plugin_path, DMI_LIB), "luaopen_dmi_module")() - general_check() - end -end - ---- General checks. -function general_check() - if libdmi.check_update() then - update_popup() - end -end - ---- Shows the update alert popup. -function update_popup() - local dialog = Dialog { - title = "Update Available", - } - - dialog:label { - focus = true, - text = "An update is available for " .. DIALOG_NAME .. ".", - } - - dialog:newrow() - - dialog:label { - text = "Would you like to download it now?", - } - - dialog:newrow() - - dialog:label { - text = "Pressing \"OK\" will open the releases page in your browser.", - } - - dialog:canvas { height = 1 } - - dialog:button { - focus = true, - text = "&OK", - onclick = function() - libdmi.open_repo("issues") - dialog:close() - end, - } - - dialog:button { - text = "&Later", - onclick = function() - dialog:close() - end, - } - - dialog:show() -end +--- @diagnostic disable: lowercase-global + +--- After command listener. +--- @type number|nil +local after_listener = nil + +--- Before command listener. +--- @type number|nil +local before_listener = nil + +--- Aseprite is exiting. +local exiting = false + +--- Open editors. +--- @type Editor[] +open_editors = {} + +--- Lib module. +--- @type LibDmi +libdmi = nil + +--- Tracks if we're doing a no-editor DMI open +local opening_dmi_noeditor = false + +--- Initializes the plugin. Called when the plugin is loaded. +--- @param plugin Plugin The plugin object. +function init(plugin) + if app.apiVersion < 27 then + return app.alert("This script requires Aseprite v1.3.3 or above") + end + + if not app.isUIAvailable then + return + end + + -- Initialize Preferences + Preferences.initialize(plugin) + + after_listener = app.events:on("aftercommand", function(ev) + if ev.name == "OpenFile" then + -- Skip DMI editor if coming from Raw Open command + if app.sprite and app.sprite.filename:ends_with(".dmi") and not opening_dmi_noeditor then + local filename = app.sprite.filename + app.command.CloseFile { ui = false } + loadlib(plugin.path) + + Editor.new(DIALOG_NAME, filename) + end + -- Reset the flag after handling the OpenFile event + opening_dmi_noeditor = false + elseif ev.name == "Exit" then + exiting = true + end + end) + + before_listener = app.events:on("beforecommand", function(ev) + if ev.name == "Exit" then + local stopped = false + if #open_editors > 0 then + local editors = table.clone(open_editors) --[[@as Editor[] ]] + for _, editor in ipairs(editors) do + if not editor:close(false) and not stopped then + stopped = true + ev.stopPropagation() + end + end + end + end + end) + + local is_state_sprite = function() + for _, editor in ipairs(open_editors) do + for _, sprite in ipairs(editor.open_sprites) do + if app.sprite == sprite.sprite then + return sprite + end + end + end + return nil + end + + plugin:newMenuSeparator { + group = "file_import", + } + + plugin:newMenuGroup { + id = "dmi_editor", + title = DIALOG_NAME, + group = "file_import", + } + + plugin:newCommand { + id = "dmi_new_file", + title = "New DMI File", + group = "dmi_editor", + onclick = function() + Editor.new_file(plugin.path) + end, + } + + plugin:newCommand { + id = "dmi_raw_open", + title = "Open DMI (No Editor - Will Delete DMI Metadata!!)", + group = "dmi_editor", + onclick = function() + opening_dmi_noeditor = true + app.command.OpenFile() + end, + } + + plugin:newMenuSeparator { + group = "dmi_editor", + } + + plugin:newCommand { + id = "dmi_expand", + title = "Expand", + group = "dmi_editor", + onclick = function() + local state_sprite = is_state_sprite() + if state_sprite then + state_sprite.editor:expand() + end + end, + onenabled = function() + return is_state_sprite() and true or false + end, + } + + plugin:newCommand { + id = "dmi_resize", + title = "Resize", + group = "dmi_editor", + onclick = function() + local state_sprite = is_state_sprite() + if state_sprite then + state_sprite.editor:resize() + end + end, + onenabled = function() + return is_state_sprite() and true or false + end, + } + + plugin:newCommand { + id = "dmi_crop", + title = "Crop", + group = "dmi_editor", + onclick = function() + local state_sprite = is_state_sprite() + if state_sprite then + state_sprite.editor:crop() + end + end, + onenabled = function() + return is_state_sprite() and true or false + end, + } + + plugin:newMenuSeparator { + group = "dmi_editor", + } + + plugin:newCommand { + id = "dmi_preferences", + title = "Preferences", + group = "dmi_editor", + onclick = function() + Preferences.show(plugin) + end, + } + + plugin:newCommand { + id = "dmi_report_issue", + title = "Report Issue", + group = "dmi_editor", + onclick = function() + loadlib(plugin.path) + libdmi.open_repo("issues") + end, + } + + plugin:newCommand { + id = "dmi_releases", + title = "Releases", + group = "dmi_editor", + onclick = function() + loadlib(plugin.path) + libdmi.open_repo("releases") + end, + } +end + +--- Exits the plugin. Called when the plugin is removed or Aseprite is closed. +--- @param plugin Plugin The plugin object. +function exit(plugin) + if not exiting and libdmi then + print( + "To uninstall the extension, re-open the Aseprite without using the extension and try again.\nThis happens beacuse once the library (dll) is loaded, it cannot be unloaded.\n") + return + end + if after_listener then + app.events:off(after_listener) + after_listener = nil + end + if before_listener then + app.events:off(before_listener) + before_listener = nil + end + if #open_editors > 0 then + local editors = table.clone(open_editors) --[[@as Editor[] ]] + for _, editor in ipairs(editors) do + editor:close(false, true) + end + end + if libdmi then + libdmi.remove_dir(TEMP_DIR, true) + if libdmi.exists(TEMP_DIR) and libdmi.instances() == 1 then + libdmi.remove_dir(TEMP_DIR, false) + end + libdmi = nil + end +end + +--- Loads the DMI library. +--- @param plugin_path string Path where the extension is installed. +function loadlib(plugin_path) + if not app.os.windows then + -- Update library path based on OS + if app.os.macos then + package.cpath = package.cpath .. ";?.dylib" + LUA_LIB = "liblua54.dylib" + else + package.cpath = package.cpath .. ";?.so" + LUA_LIB = "liblua54.so" + end + end + + -- Load Lua library + if LUA_LIB then + local lua_path = app.fs.joinPath(plugin_path, LUA_LIB) + local success, err = package.loadlib(lua_path, "") + if not success then + -- Get detailed library info using ldd and nm + local log_file = app.fs.joinPath(plugin_path, "lua_library_error.log") + local ldd_cmd = string.format('ldd %q > %q', lua_path, log_file) + local nm_cmd = string.format('nm -D %q >> %q', lua_path, log_file) + + os.execute(ldd_cmd) + os.execute('echo "\nExported symbols:" >> ' .. string.format('%q', log_file)) + os.execute(nm_cmd) + os.execute('echo "\nError: " >> ' .. string.format('%q', log_file)) + os.execute(string.format('echo %q >> %q', err or "unknown error", log_file)) + + app.alert { + title = "Lua Library Error", + text = "Failed to load Lua library. Check the log file at " .. log_file .. " for details", + } + return + end + end + + -- Load DMI library + local dmi_path = app.fs.joinPath(plugin_path, DMI_LIB) + local success, lib = package.loadlib(dmi_path, "luaopen_dmi_module")() + if not success then + app.alert { + title = "DMI Library Error", + text = "Failed to load DMI library: " .. (lib or "unknown error"), + } + return + end + + libdmi = lib() + general_check() +end + + +--- General checks. +function general_check() + if libdmi.check_update() then + update_popup() + end +end + +--- Shows the update alert popup. +function update_popup() + local dialog = Dialog { + title = "Update Available", + } + + dialog:label { + focus = true, + text = "An update is available for " .. DIALOG_NAME .. ".", + } + + dialog:newrow() + + dialog:label { + text = "Would you like to download it now?", + } + + dialog:newrow() + + dialog:label { + text = "Pressing \"OK\" will open the releases page in your browser.", + } + + dialog:canvas { height = 1 } + + dialog:button { + focus = true, + text = "&OK", + onclick = function() + libdmi.open_repo("issues") + dialog:close() + end, + } + + dialog:button { + text = "&Later", + onclick = function() + dialog:close() + end, + } + + dialog:show() +end diff --git a/scripts/types.lua b/scripts/types.lua index c637141..e601e8c 100644 --- a/scripts/types.lua +++ b/scripts/types.lua @@ -1,833 +1,848 @@ ---- --- SOME TYPES ARE MISSING AND SOME ARE NOT CORRECTLY DEFINED --- AND I'M JUST COMPLETING THE TYPES IN NEED ---- - ---- @diagnostic disable: lowercase-global - -------------------- NAMESPACES ------------------- - ---- @type app ---- @diagnostic disable-next-line: missing-fields -app = {} - ---- @type json ---- @diagnostic disable-next-line: missing-fields -json = {} - -------------------- CONSTRUCTORS ------------------- - ---- @class Point.Params ---- @field x number ---- @field y number - ---- Creates a new Point object. ---- @return Point point ---- @overload fun(x: number, y: number): Point ---- @overload fun(otherPoint: Point): Point ---- @overload fun(tbl: Point.Params): Point ---- @overload fun(tbl: number[]): Point -function Point() - return {} -end - ---- @class Color.ParamsIndex ---- @field index number - ---- @class Color.ParamsRGB ---- @field red number ---- @field green number ---- @field blue number ---- @field alpha number - ---- @class Color.ParamsRGB2 ---- @field r number ---- @field g number ---- @field b number ---- @field a number - ---- Creates a new Color object. ---- @return Color color ---- @overload fun(r: number, g: number, b: number, a: number): Color ---- @overload fun(index: number): Color ---- @overload fun(params: Color.ParamsIndex|Color.ParamsRGB|Color.ParamsRGB2): Color ---- @todo HSVA HSLA GRAY -function Color() - return {} -end - ---- @class Rectangle.Params ---- @field x number ---- @field y number ---- @field width number ---- @field height number - ---- @class Rectangle.Params2 ---- @field x number ---- @field y number ---- @field w number ---- @field h number - ---- Creates a new Rectangle object. ---- @return Rectangle rectangle ---- @overload fun(otherRectangle: Rectangle): Rectangle ---- @overload fun(x: number, y: number, width: number, height: number): Rectangle ---- @overload fun(params: Rectangle.Params|Rectangle.Params2|(number)[]): Rectangle -function Rectangle() - return {} -end - ---- @class Size.Params ---- @field width number ---- @field height number - ---- @class Size.Params2 ---- @field w number ---- @field h number - ---- Creates a new Size object. ---- @return Size size ---- @overload fun(width: number, height: number): Size ---- @overload fun(otherSize: Size): Size ---- @overload fun(params: Size.Params|Size.Params2|(number)[]): Size -function Size() - return {} -end - ---- @class Image.Params ---- @field fromFile string - ---- Creates a new Image object. ---- @return Image image ---- @overload fun(width: number, height: number, colorMode?: ColorMode): Image ---- @overload fun(spec: ImageSpec): Image ---- @overload fun(sprite: Sprite): Image ---- @overload fun(otherImage: Image): Image ---- @overload fun(otherImage: Image, rectangle: Rectangle): Image ---- @overload fun(params: Image.Params): Image -function Image() - return {} -end - ---- @class ImageSpec.Params ---- @field width number ---- @field height number ---- @field colorMode ColorMode ---- @field transparentColor number - ---- Creates a new ImageSpec object. ---- @return ImageSpec spec ---- @overload fun(otherImageSpec: ImageSpec): ImageSpec ---- @overload fun(params: ImageSpec.Params): ImageSpec -function ImageSpec() - return {} -end - ---- @class Dialog.Params ---- @field title string ---- @field notitlebar? boolean ---- @field parent? Dialog ---- @field onclose? function - ---- Creates a new Dialog object. ---- @return Dialog dialog ---- @overload fun(title: string): Dialog ---- @overload fun(params: Dialog.Params): Dialog -function Dialog() - return {} -end - ---- @class Sprite.Params ---- @field fromFile string - ---- Creates a new Sprite object. ---- @return Sprite sprite ---- @overload fun(width: number, height: number, colorMode?: ColorMode): Sprite ---- @overload fun(spec: ImageSpec): Sprite ---- @overload fun(otherSprite: Sprite): Sprite ---- @overload fun(params: Sprite.Params): Sprite -function Sprite() - return {} -end - ---- @class ColorSpace.Params ---- @field sRGB boolean - ---- @class ColorSpace.Params2 ---- @field fromFile string - ---- Creates a new ColorSpace object. ---- @return ColorSpace colorSpace ---- @overload fun(): ColorSpace ---- @overload fun(params: ColorSpace.Params|ColorSpace.Params2): ColorSpace -function ColorSpace() - return {} -end - ---- @class Palette.Params ---- @field fromFile string - ---- @class Palette.Params2 ---- @field fromResource string - ---- Creates a new Palette object. ---- @return Palette palette ---- @overload fun(): Palette ---- @overload fun(otherPalette: Palette): Palette ---- @overload fun(numberOfColors: number): Palette ---- @overload fun(params: Palette.Params|Palette.Params2): Palette -function Palette() - return {} -end - ---- @class WebSocket.Params ---- @field url string ---- @field deflate boolean ---- @field onreceive Websocket.Listener ---- @field minconnectwait? number ---- @field maxconnectwait? number - ---- @alias Websocket.Listener fun(data: any|nil, error: string|nil) - ---- Creates a new WebSocket object. ---- @return WebSocket webSocket ---- @overload fun(params: WebSocket.Params): WebSocket -function WebSocket() - return {} -end - -------------------- ENUMS ------------------- - -ColorMode = { - RGB = 1, - GRAY = 2, - INDEXED = 3, - TILEMAP = 4, -} - -MouseButton = { - LEFT = 1, - MIDDLE = 2, - RIGHT = 3, - X1 = 4, - X2 = 5, -} - -BlendMode = { - NORMAL = 0, - SRC = 1, - MULTIPLY = 2, - SCREEN = 3, - OVERLAY = 4, - DARKEN = 5, - LIGHTEN = 6, - COLOR_DODGE = 7, - COLOR_BURN = 8, - HARD_LIGHT = 9, - SOFT_LIGHT = 10, - DIFFERENCE = 11, - EXCLUSION = 12, - HSL_HUE = 13, - HSL_SATURATION = 14, - HSL_COLOR = 15, - HSL_LUMINOSITY = 16, - ADDION = 17, - SUBTRACT = 18, - DIVIDE = 19, -} - -WebSocketMessageType = { - TEXT = "TEXT", - BINARY = "BINARY", - OPEN = "OPEN", - CLOSE = "CLOSE", - PING = "PING", - PONG = "PONG", - FRAGMENT = "FRAGMENT", -} - -------------------- TYPES ------------------- - ---- @class app ---- @field apiVersion number ---- @field version any TODO ---- @field isUIAvailable boolean ---- @field alert (fun(text: string): number)|(fun(params: app.alert.Params): number) ---- @field transaction fun(name?: string, callback: function) ---- @field sprite Sprite ---- @field sprites (Sprite)[] ---- @field frame Frame|number ---- @field site any TODO ---- @field range any TODO ---- @field image Image ---- @field layer Layer ---- @field tag Tag ---- @field tool any TODO ---- @field brush any TODO ---- @field editor Editor ---- @field window any TODO ---- @field command table ---- @field pixelColor app.pixelColor ---- @field fgColor Color ---- @field bgColor Color ---- @field params table ---- @field events app.events ---- @field theme app.theme ---- @field fs app.fs - ---- @class app.pixelColor ---- @field rgba fun(r: number, g: number, b: number, a: number): number - ---- @class app.events ---- @field on fun(self: app.events, name: string, callback: function): number ---- @field off fun(self: app.events, id: number) - ---- @class app.theme ---- @field color table - ---- @class app.fs ---- @field pathSeparator string ---- @field filePath fun(filename: string): string ---- @field fileName fun(filename: string): string ---- @field fileExtension fun(filename: string): string ---- @field fileTitle fun(filename: string): string ---- @field filePathAndTitle fun(filename: string): string ---- @field normalizePath fun(path: string): string ---- @field joinPath fun(path: string, ...: string): string ---- @field currentPath string ---- @field appPath string ---- @field tempPath string ---- @field userDocsPath string ---- @field userConfigPath string ---- @field isFile fun(path: string): boolean ---- @field isDirectory fun(path: string): boolean ---- @field fileSize fun(filename: string): number ---- @field listFiles fun(path: string): table ---- @field makeDirectory fun(path: string): boolean ---- @field makeAllDirectories fun(path: string): boolean ---- @field removeDirectory fun(path: string): boolean - ---- @class app.alert.Params ---- @field title? string ---- @field text? string|(string)[] ---- @field buttons? string|(string)[] - ---- @class json ---- Decodes a json string and returns a table. ---- @field decode fun(str: string): table ---- Encodes the given table as json. ---- @field encode fun(tbl: table): string - ---- @class Plugin ---- @field name string Name of the extension. ---- @field path string Path where the extension is installed. ---- @field preferences table It's a Lua table where you can load/save any kind of Lua value here and they will be saved/restored automatically on each session. ---- @field newCommand fun(self: Plugin, params: Plugin.CommandParams) Creates a new command that can be associated to keyboard shortcuts and it's added in the app menu in the specific `"group"`. Groups are defined in the `gui.xml` file inside the `` element. ---- @field newMenuGroup fun(self: Plugin, params: Plugin.MenuGroupParams) Creates a new menu item which will contain a submenu grouping several plugin commands. ---- @field newMenuSeparator fun(self: Plugin, params: Plugin.MenuSeparatorParams) Creates a menu separator in the given menu group, useful to separate several Plugin:newCommand. - ---- @class Plugin.CommandParams ---- @field id string ID to identify this new command in `Plugin:newCommand{ id=id, ... }` calls to add several keyboard shortcuts to the same command. ---- @field title string Title of the new menu item. ---- @field group string In which existent group we should add this new menu item. Existent app groups are defined in the `gui.xml` file inside the `` element. ---- @field onclick function Function to be called when the command is executed (clicked or an associated keyboard shortcut pressed). ---- @field enabled? fun(): boolean Optional function to know if the command should be available (enabled or disabled). It should return true if the command can be executed right now. If this function is not specified the command will be always available to be executed by the user. - ---- @class Plugin.MenuGroupParams ---- @field id string ID to identify this new menu group in `Plugin:newCommand{ ..., group=id, ... }` calls to add several command/menu items as elements of this group submenu. ---- @field title string Title of the new menu group. ---- @field group string In which existent group we should add this new menu item. Existent app groups are defined in the `gui.xml` file inside the `` element. - ---- @class Plugin.MenuSeparatorParams ---- @field group string In which existent group we should add this new menu item. - ---- @class Image: table ---- @field clone fun(self: Image): Image ---- @field id number ---- @field version number ---- @field width number ---- @field height number ---- @field bounds Rectangle ---- @field colorMode ColorMode ---- @field spec ImageSpec ---- @field cel Cel ---- @field bytes string ---- @field rowStride number ---- @field bytesPerPixel number ---- @field clear function ---- @field drawPixel function ---- @field getPixel function ---- @field drawImage function ---- @field drawSprite function ---- @field isEqual function ---- @field isEmpty function ---- @field isPlain function ---- @field pixels function ---- @field saveAs (fun(self: Image, filename: string))|(fun(self: Image, params: Image.SaveParams)) ---- @field resize function - ---- @class Image.SaveParams ---- @field filename string ---- @field palette Palette - ---- @class Sprite: table ---- @field width number ---- @field height number ---- @field bounds Rectangle ---- @field gridBounds Rectangle ---- @field pixelRatio Size ---- @field selection Selection ---- @field filename string ---- @field isModified boolean ---- @field colorMode ColorMode ---- @field spec ImageSpec ---- @field frames (Frame)[] ---- @field palettes (Palette)[] ---- @field layers (Layer)[] ---- @field cels (Cel)[] ---- @field tags (Tag)[] ---- @field slices (Slice)[] ---- @field backgroundLayer Layer ---- @field transparentColor number ---- @field color Color ---- @field data string ---- @field properties table ---- @field resize function ---- @field crop function ---- @field saveAs fun(self: Sprite, filename: string) ---- @field saveCopyAs function ---- @field close function ---- @field loadPalette function ---- @field setPalette fun(self: Sprite, palette: Palette) ---- @field assignColorSpace function ---- @field convertColorSpace function ---- @field newLayer fun(self: Sprite): Layer ---- @field newGroup localecategory ---- @field deleteLayer (fun(self: Sprite, layer: Layer))|(fun(self: Sprite, layerName: string))) ---- @field newFrame (fun(self: Sprite, frameNumber: number): Frame)|(fun(self: Sprite, frame: Frame): Frame)) ---- @field newEmptyFrame fun(self: Sprite, frameNumber: number): Frame ---- @field deleteFrame function ---- @field newCel function ---- @field deleteCel function ---- @field newTag function ---- @field deleteTag function ---- @field newSlice function ---- @field deleteSlice function ---- @field newTileset function ---- @field deleteTileset function ---- @field newTile function ---- @field deleteTile function ---- @field flatten function ---- @field events table ---- @field tileManagementPlugin table - ---- @class Color: table ---- @field red number ---- @field green number ---- @field blue number ---- @field alpha number ---- @field hsvHue number ---- @field hsvSaturation number ---- @field hsvValue number ---- @field hslHue number ---- @field hslSaturation number ---- @field hslLightness number ---- @field hue number ---- @field saturation number ---- @field value number ---- @field lightness number ---- @field index number ---- @field gray string ---- @field rgbaPixel PixelColor ---- @field grayPixel PixelColor - ---- @class Rectangle: table ---- @field x number ---- @field y number ---- @field width number ---- @field height number ---- @field w number ---- @field h number ---- @field origin Point ---- @field size Size ---- @field isEmpty boolean ---- @field contains fun(self: Rectangle, otherRectangle: Rectangle): boolean ---- @field intersects fun(self: Rectangle, otherRectangle: Rectangle): boolean ---- @field intersect fun(self: Rectangle, otherRectangle: Rectangle): Rectangle ---- @field union fun(self: Rectangle, otherRectangle: Rectangle): Rectangle - ---- @class Layer: table ---- @field sprite Sprite ---- @field name string ---- @field opacity number ---- @field blendMode BlendMode ---- @field layers (Layer)[]|nil ---- @field parent Sprite|Layer|nil ---- @field stackIndex number ---- @field isImage boolean ---- @field isGroup boolean ---- @field isTilemap boolean ---- @field isTransparent boolean ---- @field isBackground boolean ---- @field isEditable boolean ---- @field isVisible boolean ---- @field isContinuous boolean ---- @field isCollapsed boolean ---- @field isExpanded boolean ---- @field isReference boolean ---- @field cels (Cel)[] ---- @field color Color ---- @field data string ---- @field properties table ---- @field cel fun(self: Layer, frameNumber: number): Cel|nil ---- @field tileset Tileset - ---- @class ImageSpec: table ---- @field colorMode ColorMode ---- @field width number ---- @field height number ---- @field colorSpace ColorSpace ---- @field transparentColor number - ---- @class Frame: table ---- @field sprite Sprite ---- @field frameNumber number ---- @field duration number ---- @field previous Frame|nil ---- @field next Frame|nil - ---- @class Point: table ---- @field x number ---- @field y number - ---- @class ButtonParams ---- @field id string ---- @field label? string ---- @field text? string ---- @field selected? boolean ---- @field focus? boolean ---- @field onclick? function - ---- @class Dialog: table ---- @field data table ---- @field bounds Rectangle ---- @field button fun(self: Dialog, params: Dialog.ButtonParams) ---- @field canvas fun(self: Dialog, params: Dialog.CanvasParams) ---- @field file fun(self: Dialog, params: Dialog.FileParams) ---- @field label fun(self: Dialog, params: Dialog.LabelParams) ---- @field separator fun(self: Dialog, params?: Dialog.SeparatorParams) ---- @field entry fun(self: Dialog, params: Dialog.EntryParams) ---- @field combobox fun(self: Dialog, params: Dialog.ComboboxParams) ---- @field number fun(self: Dialog, params: Dialog.NumberParams) ---- @field check fun(self: Dialog, params: Dialog.CheckParams) ---- @field repaint fun(self: Dialog) ---- @field show fun(self: Dialog, params?: Dialog.ShowParams) ---- @field close fun(self: Dialog) ---- @field modify fun(self: Dialog, params: Dialog.ModifyParams) ---- @field newrow fun(self: Dialog, params?: Dialog.NewRowParams) ---- @field slider fun(self: Dialog, params: Dialog.SliderParams) - ---- @class Dialog.SeparatorParams ---- @field id? string ---- @field text? string - ---- @class Dialog.ButtonParams ---- @field id? string ---- @field label? string ---- @field text? string ---- @field selected? boolean ---- @field focus? boolean ---- @field onclick? function - ---- @class Dialog.CanvasParams ---- @field id? string ---- @field width? number ---- @field height? number ---- @field onpaint? fun(ev: Dialog.CanvasEvent) ---- @field onmousedown? function ---- @field onmouseup? function ---- @field onmousemove? function ---- @field onwheel? function - ---- @class Dialog.CanvasEvent ---- @field context GraphicsContext - ---- @class Dialog.FileParams ---- @field id? string ---- @field filetypes? string[] ---- @field load? boolean ---- @field save? boolean ---- @field onchange? function - ---- @class Dialog.LabelParams ---- @field id? string The unique identifier for the label. ---- @field label? string The label to be displayed. ---- @field text? string The text associated with the label. - ---- @class Dialog.EntryParams ---- @field id? string The unique identifier for the entry. ---- @field label? string The label to be displayed. ---- @field text? string The text associated with the entry. ---- @field focus? boolean Whether the entry should be focused or not. ---- @field onchange? function The function to be called when the entry text changes. - ---- @class Dialog.ComboboxParams ---- @field id? string The unique identifier for the combobox. ---- @field label? string The label to be displayed. ---- @field option? string The default option to be selected. ---- @field options? string[] The options to be displayed. ---- @field onchange? function The function to be called when the selected option changes. - ---- @class Dialog.NumberParams ---- @field id? string The unique identifier for the number. ---- @field label? string The label to be displayed. ---- @field text? string The text associated with the number. ---- @field decimals? number The number of decimals to be displayed. ---- @field onchange? function The function to be called when the number changes. - ---- @class Dialog.CheckParams ---- @field id? string The unique identifier for the check. ---- @field label? string The label to be displayed. ---- @field text? string The text associated with the check. ---- @field selected? boolean Whether the check should be selected or not. ---- @field onclick? function The function to be called when the check is clicked. - ---- @class Dialog.ShowParams ---- @field wait? boolean ---- @field bounds? Rectangle - ---- @class Dialog.ModifyParams: table ---- @field id string ---- @field title? string - ---- @class Dialog.NewRowParams ---- @field always? boolean - ---- @class Dialog.SliderParams ---- @field id? string ---- @field label? string ---- @field min number ---- @field max number ---- @field value number ---- @field onchange? function ---- @field onrelease? function - ---- @class Cel: table ---- @field sprite Sprite ---- @field layer Layer ---- @field frame Frame ---- @field frameNumber number ---- @field image Image ---- @field bounds Rectangle ---- @field position Point ---- @field opacity number ---- @field zIndex number ---- @field color Color ---- @field data string ---- @field properties table - ---- @class Size: table ---- @field width number ---- @field height number ---- @field w number ---- @field h number ---- @field union fun(self: Size, otherSize: Size): Size - ---- @class Selection: table ---- @field bounds Rectangle ---- @field origin Point ---- @field isEmpty boolean ---- @field deselect fun(self: Selection) ---- @field selectAll fun(self: Selection) ---- @field add (fun(self: Selection, rectangle: Rectangle))|(fun(self: Selection, otherSelection: Selection)) ---- @field subtract (fun(self: Selection, rectangle: Rectangle))|(fun(self: Selection, otherSelection: Selection)) ---- @field intersect (fun(self: Selection, rectangle: Rectangle))|(fun(self: Selection, otherSelection: Selection)) ---- @field contains (fun(self: Selection, point: Point): boolean)|(fun(self: Selection, x: number, y: number): boolean) - ---- @class Tileset: table ---- @field name string ---- @field grid unknown ---- @field baseIndex number ---- @field color Color ---- @field data string ---- @field properties table ---- @field tile fun(self: Tileset, index: number): Tile ---- @field getTile fun(self: Tileset, index: number): Image - ---- @class Tile: table ---- @field index number ---- @field image Image ---- @field color Color ---- @field data string ---- @field properties table - ---- @class ColorSpace: table ---- @field name string - ---- @class Tag: table ---- @field sprite Sprite ---- @field fromFrame Frame ---- @field toFrame Frame ---- @field frames number ---- @field name string ---- @field aniDir AniDir ---- @field color Color ---- @field repeats number ---- @field data string ---- @field properties table - ---- @class Slice: table ---- @field bounds Rectangle ---- @field center Rectangle ---- @field color Color ---- @field data string ---- @field properties table ---- @field name string ---- @field pivot Point ---- @field sprite Sprite - ---- @class Palette: table ---- @field resize fun(self: Palette, colors: number) ---- @field getColor fun(self: Palette, index: number): Color ---- @field setColor fun(self: Palette, index: number, color: Color) ---- @field frame Frame ---- @field saveAs fun(self: Palette, filename: string) - ---- @class GraphicsContext: table ---- @field width number ---- @field height number ---- @field antialias boolean ---- @field color Color ---- @field strokeWidth number ---- @field blendMode BlendMode ---- @field opacity number ---- @field theme app.theme ---- @field save fun(self: GraphicsContext) ---- @field restore fun(self: GraphicsContext) ---- @field clip fun(self: GraphicsContext) ---- @field strokeRect fun(self: GraphicsContext, rectangle: Rectangle) ---- @field fillRect fun(self: GraphicsContext, rectangle: Rectangle) ---- @field fillText fun(self: GraphicsContext, text: string, x: number, y: number) ---- @field measureText fun(self: GraphicsContext, text: string): Size ---- @field drawImage (fun(self: GraphicsContext, image: Image, x: number, y: number))|(fun(self: GraphicsContext, image: Image, srcRect: Rectangle, dstRect: Rectangle))|(fun(self: GraphicsContext, image: Image, srcX: number, srcY: number, srcWidth: number, srcHeight: number, dstX: number, dstY: number, dstWidth: number, dstHeight: number)) ---- @field drawThemeImage (fun(self: GraphicsContext, partId: string, x: number, y: number))|(fun(self: GraphicsContext, partId: string, point: Point)) ---- @field drawThemeRect (fun(self: GraphicsContext, partId: string, rectangle: Rectangle))|(fun(self: GraphicsContext, partId: string, x: number, y: number, width: number, height: number)) ---- @field beginPath fun(self: GraphicsContext) ---- @field closePath fun(self: GraphicsContext) ---- @field moveTo fun(self: GraphicsContext, x: number, y: number) ---- @field lineTo fun(self: GraphicsContext, x: number, y: number) ---- @field cubicTo fun(self: GraphicsContext, cx1: number, cy1: number, cx2: number, cy2: number, x: number, y: number) ---- @field oval fun(self: GraphicsContext, rectangle: Rectangle) ---- @field rect fun(self: GraphicsContext, rectangle: Rectangle) ---- @field roundedRect (fun(self: GraphicsContext, rectangle: Rectangle, radius: number))|(fun(self: GraphicsContext, rectangle: Rectangle, radiusX: number, radiusY: number)) ---- @field stroke fun(self: GraphicsContext) ---- @field fill fun(self: GraphicsContext) - ---- @class WebSocket: table ---- @field url string Address of the server. Read-only, the url is specified when creating the websocket. ---- @field connect fun(self: WebSocket) Try connecting to the server. After a successful connection, `onreceive` function will be called with message type `WebSocketMessageType.OPEN`. When the server or network breaks the connection, the client tries reconnecting automatically. ---- @field close fun(self: WebSocket) Disconnects from the server. After a disconnect, `onreceive` function will be called with message type `WebSocketMessageType.CLOSE`. ---- @field sendText fun(self: WebSocket, ...: string) Sends a text message to the server. If multiple strings are passed, they will be joined together. ---- @field sendBinary fun(self: WebSocket, ...: string) Sends a binary message to the server. If multiple strings are passed, they will be joined together. Lua makes no distinction between character and byte strings, but the websocket protocol does label them. ---- @field ping fun(self: WebSocket, str: string) Sends a very short ping message to the server. There's a limit to the length of data that can be sent. It's sometimes used to prevent the connection from timing out and closing. A standard compliant server will reply to every "ping" message with a "pong". Client pongs are sent automatically, and there's no need to control that. - ---- @class MouseEvent ---- @field x number ---- @field y number ---- @field button MouseButton ---- @field pressure unknown ---- @field deltaX? number ---- @field deltaY? number ---- @field altKey boolean ---- @field metaKey boolean ---- @field ctrlKey boolean ---- @field shiftKey boolean ---- @field spaceKey boolean - ---- @alias PixelColor number - ---- @class MouseButton ---- @field LEFT number ---- @field MIDDLE number ---- @field RIGHT number ---- @field X1 number ---- @field X2 number - ---- @alias ColorMode ----| 0 ----| 1 ----| 2 ----| 3 - ---- @alias BlendMode ----| 0 ----| 1 ----| 2 ----| 3 ----| 4 ----| 5 ----| 6 ----| 7 ----| 8 ----| 9 ----| 10 ----| 11 ----| 12 ----| 13 ----| 14 ----| 15 ----| 16 ----| 17 ----| 18 ----| 19 - ---- @alias AniDir ----| 0 ----| 1 ----| 2 ----| 3 - ---- @alias WebSocketMessageType ----| 'TEXT' ----| 'BINARY' ----| 'OPEN' ----| 'CLOSE' ----| 'PING' ----| 'PONG' ----| 'FRAGMENT' - ---- @class LibDmi: table ---- @field new_file fun(name: string, width: number, height: number, temp: string): Dmi?, string? Creates a new DMI file. If fails, returns nil and an error message. ---- @field open_file fun(path: string, temp: string): Dmi?, string? Opens a DMI file. If fails, returns nil and an error message. ---- @field save_file fun(dmi: Dmi, filename: string): nil, string? Saves the DMI file. If fails, returns an error message. ---- @field new_state fun(width: number, height: number, temp: string): State?, string? Creates a new state. If fails, returns nil and an error message. ---- @field copy_state fun(state: State, temp: string): nil, string? Copies the state to the clipboard. If fails, returns an error message. ---- @field paste_state fun(width: number, height: number, temp: string): State?, string? Pastes the state from the clipboard. If fails, returns nil and an error message. ---- @field resize fun(dmi: Dmi, width: number, height: number, medhod: string): nil, string? Resizes the DMI file. If fails, returns an error message. ---- @field crop fun(dmi: Dmi, x: number, y: number, width: number, height: number): nil, string? Crops the DMI file. If fails, returns an error message. ---- @field expand fun(dmi: Dmi, x: number, y: number, width: number, height: number): nil, string? Expands the DMI file size. If fails, returns an error message. ---- @field overlay_color fun(r: number, g: number, b: number, width: number, height: number, ...: number): ...: number|nil Overlays the given bytes of an image on a plain color. ---- @field remove_dir fun(path: string, soft: boolean): nil, string? Removes a directory. If fails, returns an error message. ---- @field exists fun(path: string): boolean Returns true if the path points at an existing entity. ---- @field check_update fun(): boolean Return true if there is an update available. ---- @field instances fun(): number?, string? Return the number of Aseprite instances running. ---- @field save_dialog fun(title: string, filename: string, location: string): string?, string? Shows a save dialog. Returns the path of the file to save or empty string if the user cancels the dialog. ---- @field open_repo fun(path?: string): nil, string? Opens the repository in the default browser. If fails, returns an error message. - ---- @class Dmi: table ---- @field name string The name of the DMI file. ---- @field width number The width of the DMI file. ---- @field height number The height of the DMI file. ---- @field states (State)[] The states of the DMI file. ---- @field temp string The temporary directory where images of states are stored. - ---- @class State: table ---- @field name string The name of the state. ---- @field dirs 1|4|8 The number of directions in the state. ---- @field frame_key string The frame key of the state used in the temporary directory. ---- @field frame_count number The number of frames in the state. ---- @field delays (number)[] The delays of the state. ---- @field loop number How many times the state loops. ---- @field rewind boolean Whether the state rewinds or not. ---- @field movement boolean Whether the state is a movement state or not. ---- @field hotspots (string)[] The hotspots of the state. +--- +-- SOME TYPES ARE MISSING AND SOME ARE NOT CORRECTLY DEFINED +-- AND I'M JUST COMPLETING THE TYPES IN NEED +--- + +--- @diagnostic disable: lowercase-global + +------------------- NAMESPACES ------------------- + +--- @type app +--- @diagnostic disable-next-line: missing-fields +app = {} + +--- @type json +--- @diagnostic disable-next-line: missing-fields +json = {} + +---@type unknown +local undefined + +------------------- CONSTRUCTORS ------------------- + +--- @class Point.Params +--- @field x number +--- @field y number + +--- Creates a new Point object. +--- @return Point point +--- @overload fun(x: number, y: number): Point +--- @overload fun(otherPoint: Point): Point +--- @overload fun(tbl: Point.Params): Point +--- @overload fun(tbl: number[]): Point +function Point() + return {} +end + +--- @class Color.ParamsIndex +--- @field index number + +--- @class Color.ParamsRGB +--- @field red number +--- @field green number +--- @field blue number +--- @field alpha number + +--- @class Color.ParamsRGB2 +--- @field r number +--- @field g number +--- @field b number +--- @field a number + +--- Creates a new Color object. +--- @return Color color +--- @overload fun(r: number, g: number, b: number, a: number): Color +--- @overload fun(index: number): Color +--- @overload fun(params: Color.ParamsIndex|Color.ParamsRGB|Color.ParamsRGB2): Color +--- @todo HSVA HSLA GRAY +function Color() + return {} +end + +--- @class Rectangle.Params +--- @field x number +--- @field y number +--- @field width number +--- @field height number + +--- @class Rectangle.Params2 +--- @field x number +--- @field y number +--- @field w number +--- @field h number + +--- Creates a new Rectangle object. +--- @return Rectangle rectangle +--- @overload fun(otherRectangle: Rectangle): Rectangle +--- @overload fun(x: number, y: number, width: number, height: number): Rectangle +--- @overload fun(params: Rectangle.Params|Rectangle.Params2|(number)[]): Rectangle +function Rectangle() + return {} +end + +--- @class Size.Params +--- @field width number +--- @field height number + +--- @class Size.Params2 +--- @field w number +--- @field h number + +--- Creates a new Size object. +--- @return Size size +--- @overload fun(width: number, height: number): Size +--- @overload fun(otherSize: Size): Size +--- @overload fun(params: Size.Params|Size.Params2|(number)[]): Size +function Size() + return {} +end + +--- @class Image.Params +--- @field fromFile string + +--- Creates a new Image object. +--- @return Image image +--- @overload fun(width: number, height: number, colorMode?: ColorMode): Image +--- @overload fun(spec: ImageSpec): Image +--- @overload fun(sprite: Sprite): Image +--- @overload fun(otherImage: Image): Image +--- @overload fun(otherImage: Image, rectangle: Rectangle): Image +--- @overload fun(params: Image.Params): Image +function Image() + return {} +end + +--- @class ImageSpec.Params +--- @field width number +--- @field height number +--- @field colorMode ColorMode +--- @field transparentColor number + +--- Creates a new ImageSpec object. +--- @return ImageSpec spec +--- @overload fun(otherImageSpec: ImageSpec): ImageSpec +--- @overload fun(params: ImageSpec.Params): ImageSpec +function ImageSpec() + return {} +end + +--- @class Dialog.Params +--- @field title string +--- @field notitlebar? boolean +--- @field parent? Dialog +--- @field onclose? function + +--- Creates a new Dialog object. +--- @return Dialog dialog +--- @overload fun(title: string): Dialog +--- @overload fun(params: Dialog.Params): Dialog +function Dialog() + return {} +end + +--- @class Sprite.Params +--- @field fromFile string + +--- Creates a new Sprite object. +--- @return Sprite sprite +--- @overload fun(width: number, height: number, colorMode?: ColorMode): Sprite +--- @overload fun(spec: ImageSpec): Sprite +--- @overload fun(otherSprite: Sprite): Sprite +--- @overload fun(params: Sprite.Params): Sprite +function Sprite() + return {} +end + +--- @class ColorSpace.Params +--- @field sRGB boolean + +--- @class ColorSpace.Params2 +--- @field fromFile string + +--- Creates a new ColorSpace object. +--- @return ColorSpace colorSpace +--- @overload fun(): ColorSpace +--- @overload fun(params: ColorSpace.Params|ColorSpace.Params2): ColorSpace +function ColorSpace() + return {} +end + +--- @class Palette.Params +--- @field fromFile string + +--- @class Palette.Params2 +--- @field fromResource string + +--- Creates a new Palette object. +--- @return Palette palette +--- @overload fun(): Palette +--- @overload fun(otherPalette: Palette): Palette +--- @overload fun(numberOfColors: number): Palette +--- @overload fun(params: Palette.Params|Palette.Params2): Palette +function Palette() + return {} +end + +--- @class WebSocket.Params +--- @field url string +--- @field deflate boolean +--- @field onreceive Websocket.Listener +--- @field minconnectwait? number +--- @field maxconnectwait? number + +--- @alias Websocket.Listener fun(data: any|nil, error: string|nil) + +--- Creates a new WebSocket object. +--- @return WebSocket webSocket +--- @overload fun(params: WebSocket.Params): WebSocket +function WebSocket() + return {} +end + +------------------- ENUMS ------------------- + +ColorMode = { + RGB = 1, + GRAY = 2, + INDEXED = 3, + TILEMAP = 4, +} + +MouseButton = { + LEFT = 1, + MIDDLE = 2, + RIGHT = 3, + X1 = 4, + X2 = 5, +} + +BlendMode = { + NORMAL = 0, + SRC = 1, + MULTIPLY = 2, + SCREEN = 3, + OVERLAY = 4, + DARKEN = 5, + LIGHTEN = 6, + COLOR_DODGE = 7, + COLOR_BURN = 8, + HARD_LIGHT = 9, + SOFT_LIGHT = 10, + DIFFERENCE = 11, + EXCLUSION = 12, + HSL_HUE = 13, + HSL_SATURATION = 14, + HSL_COLOR = 15, + HSL_LUMINOSITY = 16, + ADDION = 17, + SUBTRACT = 18, + DIVIDE = 19, +} + +WebSocketMessageType = { + TEXT = "TEXT", + BINARY = "BINARY", + OPEN = "OPEN", + CLOSE = "CLOSE", + PING = "PING", + PONG = "PONG", + FRAGMENT = "FRAGMENT", +} + +------------------- TYPES ------------------- + +--- @class app +--- @field apiVersion number +--- @field version any TODO +--- @field isUIAvailable boolean +--- @field alert (fun(text: string): number)|(fun(params: app.alert.Params): number) +--- @field transaction fun(name?: string, callback: function) +--- @field sprite Sprite +--- @field sprites (Sprite)[] +--- @field frame Frame|number +--- @field site any TODO +--- @field range any TODO +--- @field image Image +--- @field layer Layer +--- @field tag Tag +--- @field tool any TODO +--- @field brush any TODO +--- @field editor Editor +--- @field window any TODO +--- @field command table +--- @field pixelColor app.pixelColor +--- @field fgColor Color +--- @field bgColor Color +--- @field params table +--- @field events app.events +--- @field theme app.theme +--- @field fs app.fs +--- @field os app.os + +--- @class app.pixelColor +--- @field rgba fun(r: number, g: number, b: number, a: number): number + +--- @class app.events +--- @field on fun(self: app.events, name: string, callback: function): number +--- @field off fun(self: app.events, id: number) + +--- @class app.theme +--- @field color table + +--- @class app.fs +--- @field pathSeparator string +--- @field filePath fun(filename: string): string +--- @field fileName fun(filename: string): string +--- @field fileExtension fun(filename: string): string +--- @field fileTitle fun(filename: string): string +--- @field filePathAndTitle fun(filename: string): string +--- @field normalizePath fun(path: string): string +--- @field joinPath fun(path: string, ...: string): string +--- @field currentPath string +--- @field appPath string +--- @field tempPath string +--- @field userDocsPath string +--- @field userConfigPath string +--- @field isFile fun(path: string): boolean +--- @field isDirectory fun(path: string): boolean +--- @field fileSize fun(filename: string): number +--- @field listFiles fun(path: string): table +--- @field makeDirectory fun(path: string): boolean +--- @field makeAllDirectories fun(path: string): boolean +--- @field removeDirectory fun(path: string): boolean + +--- @class app.os +--- @field name string Returns the platform name. It can be `Windows`, `macOS`, or `Linux`. +-- --- @field version Version Returns a `Version` with the Windows or macOS version. It's just `0.0.0` on Linux. +--- @field fullName string Returns the full platform name with its version. On Linux returns the distribution name with its specific version. Some examples: `Windows NT 10.0.22631`, `macOS 14.4.1`, `Pop!_OS 22.04 LTS`, etc. +--- @field windows boolean Returns `true` if we are running in the windows platform. +--- @field macos boolean Returns `true` if we are running in the macos platform. +--- @field linux boolean Returns `true` if we are running in the linux platform. +--- @field x64 boolean Returns `true` if we are running in the x64 platform. +--- @field x86 boolean Returns `true` if we are running in the x86 platform. +--- @field arm64 boolean Returns `true` if we are running in the arm64 platform. + +--- @class app.alert.Params +--- @field title? string +--- @field text? string|(string)[] +--- @field buttons? string|(string)[] + +--- @class json +--- Decodes a json string and returns a table. +--- @field decode fun(str: string): table +--- Encodes the given table as json. +--- @field encode fun(tbl: table): string + +--- @class Plugin +--- @field name string Name of the extension. +--- @field path string Path where the extension is installed. +--- @field preferences table It's a Lua table where you can load/save any kind of Lua value here and they will be saved/restored automatically on each session. +--- @field newCommand fun(self: Plugin, params: Plugin.CommandParams) Creates a new command that can be associated to keyboard shortcuts and it's added in the app menu in the specific `"group"`. Groups are defined in the `gui.xml` file inside the `` element. +--- @field newMenuGroup fun(self: Plugin, params: Plugin.MenuGroupParams) Creates a new menu item which will contain a submenu grouping several plugin commands. +--- @field newMenuSeparator fun(self: Plugin, params: Plugin.MenuSeparatorParams) Creates a menu separator in the given menu group, useful to separate several Plugin:newCommand. + +--- @class Plugin.CommandParams +--- @field id string ID to identify this new command in `Plugin:newCommand{ id=id, ... }` calls to add several keyboard shortcuts to the same command. +--- @field title string Title of the new menu item. +--- @field group string In which existent group we should add this new menu item. Existent app groups are defined in the `gui.xml` file inside the `` element. +--- @field onclick function Function to be called when the command is executed (clicked or an associated keyboard shortcut pressed). +--- @field enabled? fun(): boolean Optional function to know if the command should be available (enabled or disabled). It should return true if the command can be executed right now. If this function is not specified the command will be always available to be executed by the user. + +--- @class Plugin.MenuGroupParams +--- @field id string ID to identify this new menu group in `Plugin:newCommand{ ..., group=id, ... }` calls to add several command/menu items as elements of this group submenu. +--- @field title string Title of the new menu group. +--- @field group string In which existent group we should add this new menu item. Existent app groups are defined in the `gui.xml` file inside the `` element. + +--- @class Plugin.MenuSeparatorParams +--- @field group string In which existent group we should add this new menu item. + +--- @class Image: table +--- @field clone fun(self: Image): Image +--- @field id number +--- @field version number +--- @field width number +--- @field height number +--- @field bounds Rectangle +--- @field colorMode ColorMode +--- @field spec ImageSpec +--- @field cel Cel +--- @field bytes string +--- @field rowStride number +--- @field bytesPerPixel number +--- @field clear function +--- @field drawPixel function +--- @field getPixel function +--- @field drawImage function +--- @field drawSprite function +--- @field isEqual function +--- @field isEmpty function +--- @field isPlain function +--- @field pixels function +--- @field saveAs (fun(self: Image, filename: string))|(fun(self: Image, params: Image.SaveParams)) +--- @field resize function + +--- @class Image.SaveParams +--- @field filename string +--- @field palette Palette + +--- @class Sprite: table +--- @field width number +--- @field height number +--- @field bounds Rectangle +--- @field gridBounds Rectangle +--- @field pixelRatio Size +--- @field selection Selection +--- @field filename string +--- @field isModified boolean +--- @field colorMode ColorMode +--- @field spec ImageSpec +--- @field frames (Frame)[] +--- @field palettes (Palette)[] +--- @field layers (Layer)[] +--- @field cels (Cel)[] +--- @field tags (Tag)[] +--- @field slices (Slice)[] +--- @field backgroundLayer Layer +--- @field transparentColor number +--- @field color Color +--- @field data string +--- @field properties table +--- @field resize function +--- @field crop function +--- @field saveAs fun(self: Sprite, filename: string) +--- @field saveCopyAs function +--- @field close function +--- @field loadPalette function +--- @field setPalette fun(self: Sprite, palette: Palette) +--- @field assignColorSpace function +--- @field convertColorSpace function +--- @field newLayer fun(self: Sprite): Layer +--- @field newGroup localecategory +--- @field deleteLayer (fun(self: Sprite, layer: Layer))|(fun(self: Sprite, layerName: string))) +--- @field newFrame (fun(self: Sprite, frameNumber: number): Frame)|(fun(self: Sprite, frame: Frame): Frame)) +--- @field newEmptyFrame fun(self: Sprite, frameNumber: number): Frame +--- @field deleteFrame function +--- @field newCel function +--- @field deleteCel function +--- @field newTag function +--- @field deleteTag function +--- @field newSlice function +--- @field deleteSlice function +--- @field newTileset function +--- @field deleteTileset function +--- @field newTile function +--- @field deleteTile function +--- @field flatten function +--- @field events table +--- @field tileManagementPlugin table + +--- @class Color: table +--- @field red number +--- @field green number +--- @field blue number +--- @field alpha number +--- @field hsvHue number +--- @field hsvSaturation number +--- @field hsvValue number +--- @field hslHue number +--- @field hslSaturation number +--- @field hslLightness number +--- @field hue number +--- @field saturation number +--- @field value number +--- @field lightness number +--- @field index number +--- @field gray string +--- @field rgbaPixel PixelColor +--- @field grayPixel PixelColor + +--- @class Rectangle: table +--- @field x number +--- @field y number +--- @field width number +--- @field height number +--- @field w number +--- @field h number +--- @field origin Point +--- @field size Size +--- @field isEmpty boolean +--- @field contains fun(self: Rectangle, otherRectangle: Rectangle): boolean +--- @field intersects fun(self: Rectangle, otherRectangle: Rectangle): boolean +--- @field intersect fun(self: Rectangle, otherRectangle: Rectangle): Rectangle +--- @field union fun(self: Rectangle, otherRectangle: Rectangle): Rectangle + +--- @class Layer: table +--- @field sprite Sprite +--- @field name string +--- @field opacity number +--- @field blendMode BlendMode +--- @field layers (Layer)[]|nil +--- @field parent Sprite|Layer|nil +--- @field stackIndex number +--- @field isImage boolean +--- @field isGroup boolean +--- @field isTilemap boolean +--- @field isTransparent boolean +--- @field isBackground boolean +--- @field isEditable boolean +--- @field isVisible boolean +--- @field isContinuous boolean +--- @field isCollapsed boolean +--- @field isExpanded boolean +--- @field isReference boolean +--- @field cels (Cel)[] +--- @field color Color +--- @field data string +--- @field properties table +--- @field cel fun(self: Layer, frameNumber: number): Cel|nil +--- @field tileset Tileset + +--- @class ImageSpec: table +--- @field colorMode ColorMode +--- @field width number +--- @field height number +--- @field colorSpace ColorSpace +--- @field transparentColor number + +--- @class Frame: table +--- @field sprite Sprite +--- @field frameNumber number +--- @field duration number +--- @field previous Frame|nil +--- @field next Frame|nil + +--- @class Point: table +--- @field x number +--- @field y number + +--- @class ButtonParams +--- @field id string +--- @field label? string +--- @field text? string +--- @field selected? boolean +--- @field focus? boolean +--- @field onclick? function + +--- @class Dialog: table +--- @field data table +--- @field bounds Rectangle +--- @field button fun(self: Dialog, params: Dialog.ButtonParams) +--- @field canvas fun(self: Dialog, params: Dialog.CanvasParams) +--- @field file fun(self: Dialog, params: Dialog.FileParams) +--- @field label fun(self: Dialog, params: Dialog.LabelParams) +--- @field separator fun(self: Dialog, params?: Dialog.SeparatorParams) +--- @field entry fun(self: Dialog, params: Dialog.EntryParams) +--- @field combobox fun(self: Dialog, params: Dialog.ComboboxParams) +--- @field number fun(self: Dialog, params: Dialog.NumberParams) +--- @field check fun(self: Dialog, params: Dialog.CheckParams) +--- @field repaint fun(self: Dialog) +--- @field show fun(self: Dialog, params?: Dialog.ShowParams) +--- @field close fun(self: Dialog) +--- @field modify fun(self: Dialog, params: Dialog.ModifyParams) +--- @field newrow fun(self: Dialog, params?: Dialog.NewRowParams) +--- @field slider fun(self: Dialog, params: Dialog.SliderParams) + +--- @class Dialog.SeparatorParams +--- @field id? string +--- @field text? string + +--- @class Dialog.ButtonParams +--- @field id? string +--- @field label? string +--- @field text? string +--- @field selected? boolean +--- @field focus? boolean +--- @field onclick? function + +--- @class Dialog.CanvasParams +--- @field id? string +--- @field width? number +--- @field height? number +--- @field onpaint? fun(ev: Dialog.CanvasEvent) +--- @field onmousedown? function +--- @field onmouseup? function +--- @field onmousemove? function +--- @field onwheel? function + +--- @class Dialog.CanvasEvent +--- @field context GraphicsContext + +--- @class Dialog.FileParams +--- @field id? string +--- @field filetypes? string[] +--- @field load? boolean +--- @field save? boolean +--- @field onchange? function + +--- @class Dialog.LabelParams +--- @field id? string The unique identifier for the label. +--- @field label? string The label to be displayed. +--- @field text? string The text associated with the label. + +--- @class Dialog.EntryParams +--- @field id? string The unique identifier for the entry. +--- @field label? string The label to be displayed. +--- @field text? string The text associated with the entry. +--- @field focus? boolean Whether the entry should be focused or not. +--- @field onchange? function The function to be called when the entry text changes. + +--- @class Dialog.ComboboxParams +--- @field id? string The unique identifier for the combobox. +--- @field label? string The label to be displayed. +--- @field option? string The default option to be selected. +--- @field options? string[] The options to be displayed. +--- @field onchange? function The function to be called when the selected option changes. + +--- @class Dialog.NumberParams +--- @field id? string The unique identifier for the number. +--- @field label? string The label to be displayed. +--- @field text? string The text associated with the number. +--- @field decimals? number The number of decimals to be displayed. +--- @field onchange? function The function to be called when the number changes. + +--- @class Dialog.CheckParams +--- @field id? string The unique identifier for the check. +--- @field label? string The label to be displayed. +--- @field text? string The text associated with the check. +--- @field selected? boolean Whether the check should be selected or not. +--- @field onclick? function The function to be called when the check is clicked. + +--- @class Dialog.ShowParams +--- @field wait? boolean +--- @field bounds? Rectangle + +--- @class Dialog.ModifyParams: table +--- @field id string +--- @field title? string + +--- @class Dialog.NewRowParams +--- @field always? boolean + +--- @class Dialog.SliderParams +--- @field id? string +--- @field label? string +--- @field min number +--- @field max number +--- @field value number +--- @field onchange? function +--- @field onrelease? function + +--- @class Cel: table +--- @field sprite Sprite +--- @field layer Layer +--- @field frame Frame +--- @field frameNumber number +--- @field image Image +--- @field bounds Rectangle +--- @field position Point +--- @field opacity number +--- @field zIndex number +--- @field color Color +--- @field data string +--- @field properties table + +--- @class Size: table +--- @field width number +--- @field height number +--- @field w number +--- @field h number +--- @field union fun(self: Size, otherSize: Size): Size + +--- @class Selection: table +--- @field bounds Rectangle +--- @field origin Point +--- @field isEmpty boolean +--- @field deselect fun(self: Selection) +--- @field selectAll fun(self: Selection) +--- @field add (fun(self: Selection, rectangle: Rectangle))|(fun(self: Selection, otherSelection: Selection)) +--- @field subtract (fun(self: Selection, rectangle: Rectangle))|(fun(self: Selection, otherSelection: Selection)) +--- @field intersect (fun(self: Selection, rectangle: Rectangle))|(fun(self: Selection, otherSelection: Selection)) +--- @field contains (fun(self: Selection, point: Point): boolean)|(fun(self: Selection, x: number, y: number): boolean) + +--- @class Tileset: table +--- @field name string +--- @field grid unknown +--- @field baseIndex number +--- @field color Color +--- @field data string +--- @field properties table +--- @field tile fun(self: Tileset, index: number): Tile +--- @field getTile fun(self: Tileset, index: number): Image + +--- @class Tile: table +--- @field index number +--- @field image Image +--- @field color Color +--- @field data string +--- @field properties table + +--- @class ColorSpace: table +--- @field name string + +--- @class Tag: table +--- @field sprite Sprite +--- @field fromFrame Frame +--- @field toFrame Frame +--- @field frames number +--- @field name string +--- @field aniDir AniDir +--- @field color Color +--- @field repeats number +--- @field data string +--- @field properties table + +--- @class Slice: table +--- @field bounds Rectangle +--- @field center Rectangle +--- @field color Color +--- @field data string +--- @field properties table +--- @field name string +--- @field pivot Point +--- @field sprite Sprite + +--- @class Palette: table +--- @field resize fun(self: Palette, colors: number) +--- @field getColor fun(self: Palette, index: number): Color +--- @field setColor fun(self: Palette, index: number, color: Color) +--- @field frame Frame +--- @field saveAs fun(self: Palette, filename: string) + +--- @class GraphicsContext: table +--- @field width number +--- @field height number +--- @field antialias boolean +--- @field color Color +--- @field strokeWidth number +--- @field blendMode BlendMode +--- @field opacity number +--- @field theme app.theme +--- @field save fun(self: GraphicsContext) +--- @field restore fun(self: GraphicsContext) +--- @field clip fun(self: GraphicsContext) +--- @field strokeRect fun(self: GraphicsContext, rectangle: Rectangle) +--- @field fillRect fun(self: GraphicsContext, rectangle: Rectangle) +--- @field fillText fun(self: GraphicsContext, text: string, x: number, y: number) +--- @field measureText fun(self: GraphicsContext, text: string): Size +--- @field drawImage (fun(self: GraphicsContext, image: Image, x: number, y: number))|(fun(self: GraphicsContext, image: Image, srcRect: Rectangle, dstRect: Rectangle))|(fun(self: GraphicsContext, image: Image, srcX: number, srcY: number, srcWidth: number, srcHeight: number, dstX: number, dstY: number, dstWidth: number, dstHeight: number)) +--- @field drawThemeImage (fun(self: GraphicsContext, partId: string, x: number, y: number))|(fun(self: GraphicsContext, partId: string, point: Point)) +--- @field drawThemeRect (fun(self: GraphicsContext, partId: string, rectangle: Rectangle))|(fun(self: GraphicsContext, partId: string, x: number, y: number, width: number, height: number)) +--- @field beginPath fun(self: GraphicsContext) +--- @field closePath fun(self: GraphicsContext) +--- @field moveTo fun(self: GraphicsContext, x: number, y: number) +--- @field lineTo fun(self: GraphicsContext, x: number, y: number) +--- @field cubicTo fun(self: GraphicsContext, cx1: number, cy1: number, cx2: number, cy2: number, x: number, y: number) +--- @field oval fun(self: GraphicsContext, rectangle: Rectangle) +--- @field rect fun(self: GraphicsContext, rectangle: Rectangle) +--- @field roundedRect (fun(self: GraphicsContext, rectangle: Rectangle, radius: number))|(fun(self: GraphicsContext, rectangle: Rectangle, radiusX: number, radiusY: number)) +--- @field stroke fun(self: GraphicsContext) +--- @field fill fun(self: GraphicsContext) + +--- @class WebSocket: table +--- @field url string Address of the server. Read-only, the url is specified when creating the websocket. +--- @field connect fun(self: WebSocket) Try connecting to the server. After a successful connection, `onreceive` function will be called with message type `WebSocketMessageType.OPEN`. When the server or network breaks the connection, the client tries reconnecting automatically. +--- @field close fun(self: WebSocket) Disconnects from the server. After a disconnect, `onreceive` function will be called with message type `WebSocketMessageType.CLOSE`. +--- @field sendText fun(self: WebSocket, ...: string) Sends a text message to the server. If multiple strings are passed, they will be joined together. +--- @field sendBinary fun(self: WebSocket, ...: string) Sends a binary message to the server. If multiple strings are passed, they will be joined together. Lua makes no distinction between character and byte strings, but the websocket protocol does label them. +--- @field ping fun(self: WebSocket, str: string) Sends a very short ping message to the server. There's a limit to the length of data that can be sent. It's sometimes used to prevent the connection from timing out and closing. A standard compliant server will reply to every "ping" message with a "pong". Client pongs are sent automatically, and there's no need to control that. + +--- @class MouseEvent +--- @field x number +--- @field y number +--- @field button MouseButton +--- @field pressure unknown +--- @field deltaX? number +--- @field deltaY? number +--- @field altKey boolean +--- @field metaKey boolean +--- @field ctrlKey boolean +--- @field shiftKey boolean +--- @field spaceKey boolean + +--- @alias PixelColor number + +--- @class MouseButton +--- @field LEFT number +--- @field MIDDLE number +--- @field RIGHT number +--- @field X1 number +--- @field X2 number + +--- @alias ColorMode +---| 0 +---| 1 +---| 2 +---| 3 + +--- @alias BlendMode +---| 0 +---| 1 +---| 2 +---| 3 +---| 4 +---| 5 +---| 6 +---| 7 +---| 8 +---| 9 +---| 10 +---| 11 +---| 12 +---| 13 +---| 14 +---| 15 +---| 16 +---| 17 +---| 18 +---| 19 + +--- @alias AniDir +---| 0 +---| 1 +---| 2 +---| 3 + +--- @alias WebSocketMessageType +---| 'TEXT' +---| 'BINARY' +---| 'OPEN' +---| 'CLOSE' +---| 'PING' +---| 'PONG' +---| 'FRAGMENT' + +--- @class LibDmi: table +--- @field new_file fun(name: string, width: number, height: number, temp: string): Dmi?, string? Creates a new DMI file. If fails, returns nil and an error message. +--- @field open_file fun(path: string, temp: string): Dmi?, string? Opens a DMI file. If fails, returns nil and an error message. +--- @field save_file fun(dmi: Dmi, filename: string): nil, string? Saves the DMI file. If fails, returns an error message. +--- @field new_state fun(width: number, height: number, temp: string): State?, string? Creates a new state. If fails, returns nil and an error message. +--- @field copy_state fun(state: State, temp: string): nil, string? Copies the state to the clipboard. If fails, returns an error message. +--- @field paste_state fun(width: number, height: number, temp: string): State?, string? Pastes the state from the clipboard. If fails, returns nil and an error message. +--- @field resize fun(dmi: Dmi, width: number, height: number, medhod: string): nil, string? Resizes the DMI file. If fails, returns an error message. +--- @field crop fun(dmi: Dmi, x: number, y: number, width: number, height: number): nil, string? Crops the DMI file. If fails, returns an error message. +--- @field expand fun(dmi: Dmi, x: number, y: number, width: number, height: number): nil, string? Expands the DMI file size. If fails, returns an error message. +--- @field overlay_color fun(r: number, g: number, b: number, width: number, height: number, ...: number): ...: number|nil Overlays the given bytes of an image on a plain color. +--- @field remove_dir fun(path: string, soft: boolean): nil, string? Removes a directory. If fails, returns an error message. +--- @field exists fun(path: string): boolean Returns true if the path points at an existing entity. +--- @field check_update fun(): boolean Return true if there is an update available. +--- @field instances fun(): number?, string? Return the number of Aseprite instances running. +--- @field save_dialog fun(title: string, filename: string, location: string): string?, string? Shows a save dialog. Returns the path of the file to save or empty string if the user cancels the dialog. +--- @field open_repo fun(path?: string): nil, string? Opens the repository in the default browser. If fails, returns an error message. + +--- @class Dmi: table +--- @field name string The name of the DMI file. +--- @field width number The width of the DMI file. +--- @field height number The height of the DMI file. +--- @field states (State)[] The states of the DMI file. +--- @field temp string The temporary directory where images of states are stored. + +--- @class State: table +--- @field name string The name of the state. +--- @field dirs 1|4|8 The number of directions in the state. +--- @field frame_key string The frame key of the state used in the temporary directory. +--- @field frame_count number The number of frames in the state. +--- @field delays (number)[] The delays of the state. +--- @field loop number How many times the state loops. +--- @field rewind boolean Whether the state rewinds or not. +--- @field movement boolean Whether the state is a movement state or not. +--- @field hotspots (string)[] The hotspots of the state. diff --git a/tools/build.py b/tools/build.py index 167373b..828a4f9 100755 --- a/tools/build.py +++ b/tools/build.py @@ -1,127 +1,147 @@ -#!/usr/bin/python -import os -import shutil -import subprocess -import urllib.request -import urllib.error -import zipfile - -EXTENSION_NAME = "aseprite-dmi" -LIBRARY_NAME = "dmi" -TARGET = "debug" -CI = False - -import sys -args = sys.argv[1:] - -if "--release" in args: - TARGET = "release" -elif "--ci" in args: - try: - index = args.index("--ci") - TARGET = args[index + 1] - CI = True - except IndexError: - print("Error: Please provide a target name after --ci flag.") - sys.exit(1) - -working_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") -os.chdir(working_dir) - -if not CI: - try: - rust_version_output = subprocess.check_output(["rustc", "--version"]).decode() - except FileNotFoundError: - print("Error: Rust is not installed.") - sys.exit(1) - os.chdir(os.path.join(working_dir, "lib")) - try: - print("Building main library...") - if TARGET == "debug": - subprocess.run(["cargo", "build"], check=True) - else: - subprocess.run(["cargo", "build", "--release"], check=True) - except subprocess.CalledProcessError: - print("Error: lib build failed. Please check for errors.") - sys.exit(1) - -os.chdir(working_dir) - -win = sys.platform.startswith('win') -if win: - library_extension = ".dll" - library_prefix = "" -elif sys.platform.startswith('darwin'): - library_extension = ".dylib" - library_prefix = "lib" -else: - library_extension = ".so" - library_prefix = "lib" - -library_source = os.path.join("lib", "target", TARGET if not CI else os.path.join(TARGET, "release"), f"{library_prefix}{LIBRARY_NAME}{library_extension}") - -if not os.path.exists(library_source): - print("Error: lib was not built. Please check for errors.") - sys.exit(1) - -if win: - lua_library = f"{library_prefix}lua54{library_extension}" - if not os.path.exists(lua_library): - print("Lua library not found. Downloading...") - zip_path = os.path.join(working_dir, "lua54.zip") - try: - url = "https://netix.dl.sourceforge.net/project/luabinaries/5.4.2/Windows%20Libraries/Dynamic/lua-5.4.2_Win64_dllw6_lib.zip" - urllib.request.urlretrieve(url, zip_path) - except urllib.error.URLError as e: - if os.path.exists(zip_path): - os.remove(zip_path) - print(f"Could not download lua library. Please check your internet connection and try again.") - print(f"Error details: {e}") - sys.exit(1) - else: - with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extract(lua_library, working_dir) - os.remove(zip_path) - -dist_dir = os.path.join(working_dir, "dist") -unzipped_dir = os.path.join(dist_dir, "unzipped") - -if os.path.exists(dist_dir): - shutil.rmtree(dist_dir) - -os.makedirs(dist_dir) -os.makedirs(unzipped_dir) - -shutil.copy("package.json", unzipped_dir) -shutil.copy("LICENSE", unzipped_dir) -shutil.copy("README.md", unzipped_dir) -shutil.copy(library_source, unzipped_dir) - -if win: - shutil.copy(lua_library, unzipped_dir) - -shutil.copytree(os.path.join("scripts"), os.path.join(unzipped_dir, "scripts")) - -if CI: - if TARGET.find("windows") != -1: - target_name = "-windows" - elif TARGET.find("linux") != -1: - target_name = "-linux" - elif TARGET.find("darwin") != -1: - target_name = "-macos" -else: - target_name = "" - -zip_path = os.path.join(dist_dir, f"{EXTENSION_NAME}{target_name}.zip") -with zipfile.ZipFile(zip_path, "w") as zipf: - for root, dirs, files in os.walk(unzipped_dir): - for file in files: - zipf.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), unzipped_dir)) - -extension_path = os.path.join(dist_dir, f"{EXTENSION_NAME}{target_name}.aseprite-extension") -if os.path.exists(extension_path): - os.remove(extension_path) - -shutil.copy(zip_path, extension_path) - -print("Build completed successfully.") +#!/usr/bin/python +import os +import shutil +import subprocess +import urllib.request +import urllib.error +import zipfile + +EXTENSION_NAME = "aseprite-dmi" +LIBRARY_NAME = "dmi" +TARGET = "debug" +CI = False + +import sys +args = sys.argv[1:] + +if "--release" in args: + TARGET = "release" +elif "--ci" in args: + try: + index = args.index("--ci") + TARGET = args[index + 1] + CI = True + except IndexError: + print("Error: Please provide a target name after --ci flag.") + sys.exit(1) + +working_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") +os.chdir(working_dir) + +if not CI: + try: + rust_version_output = subprocess.check_output(["rustc", "--version"]).decode() + except FileNotFoundError: + print("Error: Rust is not installed.") + sys.exit(1) + os.chdir(os.path.join(working_dir, "lib")) + try: + print("Building main library...") + if TARGET == "debug": + subprocess.run(["cargo", "build"], check=True) + else: + subprocess.run(["cargo", "build", "--release"], check=True) + except subprocess.CalledProcessError: + print("Error: lib build failed. Please check for errors.") + sys.exit(1) + +os.chdir(working_dir) + +win = sys.platform.startswith('win') +if win: + library_extension = ".dll" + library_prefix = "" +elif sys.platform.startswith('darwin'): + library_extension = ".dylib" + library_prefix = "lib" +else: + library_extension = ".so" + library_prefix = "lib" + +library_source = os.path.join("lib", "target", TARGET if not CI else os.path.join(TARGET, "release"), f"{library_prefix}{LIBRARY_NAME}{library_extension}") + +dist_dir = os.path.join(working_dir, "dist") +unzipped_dir = os.path.join(dist_dir, "unzipped") + +if not os.path.exists(library_source): + print("Error: lib was not built. Please check for errors.") + sys.exit(1) + +if win: + lua_library = f"{library_prefix}lua54{library_extension}" + if not os.path.exists(lua_library): + print("Lua library not found. Downloading...") + zip_path = os.path.join(working_dir, "lua54.zip") + try: + url = "https://netix.dl.sourceforge.net/project/luabinaries/5.4.2/Windows%20Libraries/Dynamic/lua-5.4.2_Win64_dllw6_lib.zip" + urllib.request.urlretrieve(url, zip_path) + except urllib.error.URLError as e: + if os.path.exists(zip_path): + os.remove(zip_path) + print(f"Could not download lua library. Please check your internet connection and try again.") + print(f"Error details: {e}") + sys.exit(1) + else: + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extract(lua_library, working_dir) + os.remove(zip_path) +elif CI and sys.platform.startswith('linux'): + # For CI Linux builds, Lua library should already be in dist/unzipped + lua_library = f"{library_prefix}lua54{library_extension}" + + # Check to-copy directory + to_copy_path = os.path.join(working_dir, "to-copy") + lua_path = os.path.join(to_copy_path, lua_library) + + if not os.path.exists(lua_path): + print("Error: Steam Runtime Lua library not found") + sys.exit(1) +elif not CI and sys.platform.startswith('linux'): + print("Warning: On Linux, the Lua library must be built in Steam Runtime.") + print("Please run the build-lua workflow in GitHub Actions to get the correct library.") + sys.exit(1) + +if os.path.exists(dist_dir): + shutil.rmtree(dist_dir) + +os.makedirs(dist_dir) +os.makedirs(unzipped_dir) + +shutil.copy("package.json", unzipped_dir) +shutil.copy("LICENSE", unzipped_dir) +shutil.copy("README.md", unzipped_dir) +shutil.copy(library_source, unzipped_dir) + +if win or (CI and sys.platform.startswith('linux')): + # On Windows, copy from working dir + # On Linux CI, it's in to-copy + if win: + shutil.copy(lua_library, unzipped_dir) + else: + shutil.copy(os.path.join(working_dir, "to-copy", lua_library), unzipped_dir) + +shutil.copytree(os.path.join("scripts"), os.path.join(unzipped_dir, "scripts")) + +if CI: + if TARGET.find("windows") != -1: + target_name = "-windows" + elif TARGET.find("linux") != -1: + target_name = "-linux" + elif TARGET.find("darwin") != -1: + target_name = "-macos" +else: + target_name = "" + +zip_path = os.path.join(dist_dir, f"{EXTENSION_NAME}{target_name}.zip") +with zipfile.ZipFile(zip_path, "w") as zipf: + for root, dirs, files in os.walk(unzipped_dir): + for file in files: + zipf.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), unzipped_dir)) + +extension_path = os.path.join(dist_dir, f"{EXTENSION_NAME}{target_name}.aseprite-extension") +if os.path.exists(extension_path): + os.remove(extension_path) + +shutil.copy(zip_path, extension_path) + +print("Build completed successfully.")