├── .github └── workflows │ ├── mean_bean_ci.yml │ └── mean_bean_deploy.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── ci ├── build.bash ├── common.bash └── set_rust_version.bash └── src ├── cli.rs ├── exclude.rs ├── main.rs └── sort.rs /.github/workflows/mean_bean_ci.yml: -------------------------------------------------------------------------------- 1 | name: Mean Bean CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | # This job downloads and stores `cross` as an artifact, so that it can be 11 | # redownloaded across all of the jobs. Currently this copied pasted between 12 | # `ci.yml` and `deploy.yml`. Make sure to update both places when making 13 | # changes. 14 | install-cross: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: XAMPPRocky/get-github-release@v1 18 | id: cross 19 | with: 20 | owner: rust-embedded 21 | repo: cross 22 | matches: ${{ matrix.platform }} 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | - uses: actions/upload-artifact@v3 25 | with: 26 | name: cross-${{ matrix.platform }} 27 | path: ${{ steps.cross.outputs.install_path }} 28 | strategy: 29 | matrix: 30 | platform: [linux-musl, apple-darwin] 31 | 32 | windows: 33 | runs-on: windows-latest 34 | # Windows technically doesn't need this, but if we don't block windows on it 35 | # some of the windows jobs could fill up the concurrent job queue before 36 | # one of the install-cross jobs has started, so this makes sure all 37 | # artifacts are downloaded first. 38 | needs: install-cross 39 | steps: 40 | - uses: actions/checkout@v3 41 | - run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }} 42 | shell: bash 43 | - run: ci/build.bash cargo ${{ matrix.target }} 44 | shell: bash 45 | 46 | strategy: 47 | fail-fast: true 48 | matrix: 49 | channel: [stable, beta, nightly] 50 | target: 51 | # MSVC 52 | - i686-pc-windows-msvc 53 | - x86_64-pc-windows-msvc 54 | # GNU: You typically only need to test Windows GNU if you're 55 | # specifically targeting it, and it can cause issues with some 56 | # dependencies if you're not so it's disabled by self. 57 | # - i686-pc-windows-gnu 58 | # - x86_64-pc-windows-gnu 59 | 60 | macos: 61 | runs-on: macos-latest 62 | needs: install-cross 63 | steps: 64 | - uses: actions/checkout@v3 65 | - uses: actions/download-artifact@v3 66 | with: 67 | name: cross-apple-darwin 68 | path: /usr/local/bin/ 69 | 70 | - run: chmod +x /usr/local/bin/cross 71 | 72 | - run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }} 73 | - run: ci/build.bash cross ${{ matrix.target }} 74 | # Only test on macOS platforms since we can't simulate iOS. 75 | 76 | strategy: 77 | fail-fast: true 78 | matrix: 79 | channel: [stable, beta, nightly] 80 | target: 81 | # macOS 82 | - x86_64-apple-darwin 83 | # iOS 84 | - aarch64-apple-ios 85 | - x86_64-apple-ios 86 | 87 | linux: 88 | runs-on: ubuntu-latest 89 | needs: install-cross 90 | steps: 91 | - uses: actions/checkout@v3 92 | - name: Download Cross 93 | uses: actions/download-artifact@v3 94 | with: 95 | name: cross-linux-musl 96 | path: /tmp/ 97 | - run: chmod +x /tmp/cross 98 | - run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }} 99 | - run: ci/build.bash /tmp/cross ${{ matrix.target }} 100 | # These targets have issues with being tested so they are disabled 101 | # by default. You can try disabling to see if they work for 102 | # your project. 103 | 104 | strategy: 105 | fail-fast: true 106 | matrix: 107 | channel: [stable, beta, nightly] 108 | target: 109 | # Linux 110 | - aarch64-unknown-linux-gnu 111 | - aarch64-unknown-linux-musl 112 | - arm-unknown-linux-gnueabi 113 | - arm-unknown-linux-gnueabihf 114 | - arm-unknown-linux-musleabi 115 | - arm-unknown-linux-musleabihf 116 | - armv5te-unknown-linux-musleabi 117 | - armv7-unknown-linux-gnueabihf 118 | - armv7-unknown-linux-musleabihf 119 | - i586-unknown-linux-gnu 120 | - i586-unknown-linux-musl 121 | - i686-unknown-linux-gnu 122 | - i686-unknown-linux-musl 123 | - powerpc-unknown-linux-gnu 124 | - powerpc64le-unknown-linux-gnu 125 | - s390x-unknown-linux-gnu 126 | - x86_64-unknown-linux-gnu 127 | - x86_64-unknown-linux-musl 128 | -------------------------------------------------------------------------------- /.github/workflows/mean_bean_deploy.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Mean Bean Deploy 8 | env: 9 | BIN: pixelsort 10 | 11 | jobs: 12 | # This job downloads and stores `cross` as an artifact, so that it can be 13 | # redownloaded across all of the jobs. Currently this copied pasted between 14 | # `ci.yml` and `deploy.yml`. Make sure to update both places when making 15 | # changes. 16 | install-cross: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: XAMPPRocky/get-github-release@v1 20 | id: cross 21 | with: 22 | owner: rust-embedded 23 | repo: cross 24 | matches: ${{ matrix.platform }} 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | - uses: actions/upload-artifact@v3 27 | with: 28 | name: cross-${{ matrix.platform }} 29 | path: ${{ steps.cross.outputs.install_path }} 30 | strategy: 31 | matrix: 32 | platform: [linux-musl, apple-darwin] 33 | 34 | windows: 35 | runs-on: windows-latest 36 | needs: install-cross 37 | strategy: 38 | matrix: 39 | target: 40 | # MSVC 41 | - i686-pc-windows-msvc 42 | - x86_64-pc-windows-msvc 43 | # GNU 44 | # - i686-pc-windows-gnu 45 | # - x86_64-pc-windows-gnu 46 | steps: 47 | - uses: actions/checkout@v3 48 | # FIXME: Hack around thinLTO being broken. 49 | - run: echo "RUSTFLAGS=-Clto=fat" >> $GITHUB_ENV 50 | - run: bash ci/set_rust_version.bash stable ${{ matrix.target }} 51 | - run: bash ci/build.bash cargo ${{ matrix.target }} RELEASE 52 | # We're using using a fork of `actions/create-release` that detects 53 | # whether a release is already available or not first. 54 | - uses: XAMPPRocky/create-release@v1.0.2 55 | id: create_release 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | tag_name: ${{ github.ref }} 60 | release_name: ${{ github.ref }} 61 | # Draft should **always** be false. GitHub doesn't provide a way to 62 | # get draft releases from its API, so there's no point using it. 63 | draft: false 64 | prerelease: true 65 | - uses: actions/upload-release-asset@v1 66 | id: upload-release-asset 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | with: 70 | upload_url: ${{ steps.create_release.outputs.upload_url }} 71 | asset_path: target/${{ matrix.target }}/release/${{ env.BIN }}.exe 72 | asset_name: ${{ env.BIN }}-${{ matrix.target }}.exe 73 | asset_content_type: application/zip 74 | 75 | macos: 76 | runs-on: macos-latest 77 | needs: install-cross 78 | strategy: 79 | matrix: 80 | target: 81 | # macOS 82 | - x86_64-apple-darwin 83 | # iOS 84 | # - aarch64-apple-ios 85 | # - armv7-apple-ios 86 | # - armv7s-apple-ios 87 | # - i386-apple-ios 88 | # - x86_64-apple-ios 89 | steps: 90 | - uses: actions/checkout@v3 91 | - uses: actions/download-artifact@v3 92 | with: 93 | name: cross-apple-darwin 94 | path: /usr/local/bin/ 95 | - run: chmod +x /usr/local/bin/cross 96 | 97 | - run: ci/set_rust_version.bash stable ${{ matrix.target }} 98 | - run: ci/build.bash cross ${{ matrix.target }} RELEASE 99 | - run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }} 100 | - uses: XAMPPRocky/create-release@v1.0.2 101 | id: create_release 102 | env: 103 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 104 | with: 105 | tag_name: ${{ github.ref }} 106 | release_name: ${{ github.ref }} 107 | draft: false 108 | prerelease: true 109 | - uses: actions/upload-release-asset@v1 110 | id: upload-release-asset 111 | env: 112 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 113 | with: 114 | upload_url: ${{ steps.create_release.outputs.upload_url }} 115 | asset_path: ${{ env.BIN }}.tar.gz 116 | asset_name: ${{ env.BIN }}-${{ matrix.target }}.tar.gz 117 | asset_content_type: application/gzip 118 | 119 | linux: 120 | runs-on: ubuntu-latest 121 | needs: install-cross 122 | strategy: 123 | fail-fast: false 124 | matrix: 125 | target: 126 | # WASM, off by default as most rust projects aren't compatible yet. 127 | # - wasm32-unknown-emscripten 128 | # Linux 129 | - aarch64-unknown-linux-gnu 130 | - arm-unknown-linux-gnueabi 131 | - armv7-unknown-linux-gnueabihf 132 | - i686-unknown-linux-gnu 133 | - i686-unknown-linux-musl 134 | - mips-unknown-linux-gnu 135 | - mips64-unknown-linux-gnuabi64 136 | - mips64el-unknown-linux-gnuabi64 137 | - mipsel-unknown-linux-gnu 138 | - powerpc64-unknown-linux-gnu 139 | - powerpc64le-unknown-linux-gnu 140 | - s390x-unknown-linux-gnu 141 | - x86_64-unknown-linux-gnu 142 | - x86_64-unknown-linux-musl 143 | # Android 144 | - arm-linux-androideabi 145 | - armv7-linux-androideabi 146 | # Solaris 147 | - sparcv9-sun-solaris 148 | steps: 149 | - uses: actions/checkout@v3 150 | - uses: actions/download-artifact@v3 151 | with: 152 | name: cross-linux-musl 153 | path: /tmp/ 154 | - run: chmod +x /tmp/cross 155 | 156 | - run: ci/set_rust_version.bash stable ${{ matrix.target }} 157 | - run: ci/build.bash /tmp/cross ${{ matrix.target }} RELEASE 158 | - run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }} 159 | - uses: XAMPPRocky/create-release@v1.0.2 160 | id: create_release 161 | env: 162 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 163 | with: 164 | tag_name: ${{ github.ref }} 165 | release_name: ${{ github.ref }} 166 | draft: false 167 | prerelease: false 168 | - name: Upload Release Asset 169 | id: upload-release-asset 170 | uses: actions/upload-release-asset@v1 171 | env: 172 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 173 | with: 174 | upload_url: ${{ steps.create_release.outputs.upload_url }} 175 | asset_path: ${{ env.BIN }}.tar.gz 176 | asset_name: ${{ env.BIN }}-${{ matrix.target }}.tar.gz 177 | asset_content_type: application/gzip 178 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.png 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "anstream" 13 | version = "0.6.11" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 16 | dependencies = [ 17 | "anstyle", 18 | "anstyle-parse", 19 | "anstyle-query", 20 | "anstyle-wincon", 21 | "colorchoice", 22 | "utf8parse", 23 | ] 24 | 25 | [[package]] 26 | name = "anstyle" 27 | version = "1.0.5" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" 30 | 31 | [[package]] 32 | name = "anstyle-parse" 33 | version = "0.2.3" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 36 | dependencies = [ 37 | "utf8parse", 38 | ] 39 | 40 | [[package]] 41 | name = "anstyle-query" 42 | version = "1.0.2" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 45 | dependencies = [ 46 | "windows-sys 0.52.0", 47 | ] 48 | 49 | [[package]] 50 | name = "anstyle-wincon" 51 | version = "3.0.2" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 54 | dependencies = [ 55 | "anstyle", 56 | "windows-sys 0.52.0", 57 | ] 58 | 59 | [[package]] 60 | name = "autocfg" 61 | version = "1.1.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 64 | 65 | [[package]] 66 | name = "bitflags" 67 | version = "1.3.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 70 | 71 | [[package]] 72 | name = "bitflags" 73 | version = "2.4.2" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 76 | 77 | [[package]] 78 | name = "bytemuck" 79 | version = "1.14.1" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9" 82 | 83 | [[package]] 84 | name = "byteorder" 85 | version = "1.5.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 88 | 89 | [[package]] 90 | name = "cfg-if" 91 | version = "1.0.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 94 | 95 | [[package]] 96 | name = "clap" 97 | version = "4.4.18" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" 100 | dependencies = [ 101 | "clap_builder", 102 | ] 103 | 104 | [[package]] 105 | name = "clap_builder" 106 | version = "4.4.18" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" 109 | dependencies = [ 110 | "anstream", 111 | "anstyle", 112 | "clap_lex", 113 | "strsim", 114 | "terminal_size", 115 | ] 116 | 117 | [[package]] 118 | name = "clap_lex" 119 | version = "0.6.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 122 | 123 | [[package]] 124 | name = "color_quant" 125 | version = "1.1.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 128 | 129 | [[package]] 130 | name = "colorchoice" 131 | version = "1.0.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 134 | 135 | [[package]] 136 | name = "crc32fast" 137 | version = "1.3.2" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 140 | dependencies = [ 141 | "cfg-if", 142 | ] 143 | 144 | [[package]] 145 | name = "errno" 146 | version = "0.3.8" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 149 | dependencies = [ 150 | "libc", 151 | "windows-sys 0.52.0", 152 | ] 153 | 154 | [[package]] 155 | name = "fdeflate" 156 | version = "0.3.4" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" 159 | dependencies = [ 160 | "simd-adler32", 161 | ] 162 | 163 | [[package]] 164 | name = "flate2" 165 | version = "1.0.28" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" 168 | dependencies = [ 169 | "crc32fast", 170 | "miniz_oxide", 171 | ] 172 | 173 | [[package]] 174 | name = "getrandom" 175 | version = "0.2.12" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 178 | dependencies = [ 179 | "cfg-if", 180 | "libc", 181 | "wasi", 182 | ] 183 | 184 | [[package]] 185 | name = "image" 186 | version = "0.24.8" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23" 189 | dependencies = [ 190 | "bytemuck", 191 | "byteorder", 192 | "color_quant", 193 | "jpeg-decoder", 194 | "num-traits", 195 | "png", 196 | ] 197 | 198 | [[package]] 199 | name = "jpeg-decoder" 200 | version = "0.3.1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 203 | 204 | [[package]] 205 | name = "libc" 206 | version = "0.2.153" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 209 | 210 | [[package]] 211 | name = "linux-raw-sys" 212 | version = "0.4.13" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 215 | 216 | [[package]] 217 | name = "miniz_oxide" 218 | version = "0.7.2" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 221 | dependencies = [ 222 | "adler", 223 | "simd-adler32", 224 | ] 225 | 226 | [[package]] 227 | name = "num-traits" 228 | version = "0.2.17" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 231 | dependencies = [ 232 | "autocfg", 233 | ] 234 | 235 | [[package]] 236 | name = "pixelsort" 237 | version = "0.1.1" 238 | dependencies = [ 239 | "clap", 240 | "image", 241 | "rand", 242 | ] 243 | 244 | [[package]] 245 | name = "png" 246 | version = "0.17.11" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a" 249 | dependencies = [ 250 | "bitflags 1.3.2", 251 | "crc32fast", 252 | "fdeflate", 253 | "flate2", 254 | "miniz_oxide", 255 | ] 256 | 257 | [[package]] 258 | name = "ppv-lite86" 259 | version = "0.2.17" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 262 | 263 | [[package]] 264 | name = "rand" 265 | version = "0.8.5" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 268 | dependencies = [ 269 | "libc", 270 | "rand_chacha", 271 | "rand_core", 272 | ] 273 | 274 | [[package]] 275 | name = "rand_chacha" 276 | version = "0.3.1" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 279 | dependencies = [ 280 | "ppv-lite86", 281 | "rand_core", 282 | ] 283 | 284 | [[package]] 285 | name = "rand_core" 286 | version = "0.6.4" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 289 | dependencies = [ 290 | "getrandom", 291 | ] 292 | 293 | [[package]] 294 | name = "rustix" 295 | version = "0.38.31" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" 298 | dependencies = [ 299 | "bitflags 2.4.2", 300 | "errno", 301 | "libc", 302 | "linux-raw-sys", 303 | "windows-sys 0.52.0", 304 | ] 305 | 306 | [[package]] 307 | name = "simd-adler32" 308 | version = "0.3.7" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 311 | 312 | [[package]] 313 | name = "strsim" 314 | version = "0.10.0" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 317 | 318 | [[package]] 319 | name = "terminal_size" 320 | version = "0.3.0" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" 323 | dependencies = [ 324 | "rustix", 325 | "windows-sys 0.48.0", 326 | ] 327 | 328 | [[package]] 329 | name = "utf8parse" 330 | version = "0.2.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 333 | 334 | [[package]] 335 | name = "wasi" 336 | version = "0.11.0+wasi-snapshot-preview1" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 339 | 340 | [[package]] 341 | name = "windows-sys" 342 | version = "0.48.0" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 345 | dependencies = [ 346 | "windows-targets 0.48.5", 347 | ] 348 | 349 | [[package]] 350 | name = "windows-sys" 351 | version = "0.52.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 354 | dependencies = [ 355 | "windows-targets 0.52.0", 356 | ] 357 | 358 | [[package]] 359 | name = "windows-targets" 360 | version = "0.48.5" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 363 | dependencies = [ 364 | "windows_aarch64_gnullvm 0.48.5", 365 | "windows_aarch64_msvc 0.48.5", 366 | "windows_i686_gnu 0.48.5", 367 | "windows_i686_msvc 0.48.5", 368 | "windows_x86_64_gnu 0.48.5", 369 | "windows_x86_64_gnullvm 0.48.5", 370 | "windows_x86_64_msvc 0.48.5", 371 | ] 372 | 373 | [[package]] 374 | name = "windows-targets" 375 | version = "0.52.0" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 378 | dependencies = [ 379 | "windows_aarch64_gnullvm 0.52.0", 380 | "windows_aarch64_msvc 0.52.0", 381 | "windows_i686_gnu 0.52.0", 382 | "windows_i686_msvc 0.52.0", 383 | "windows_x86_64_gnu 0.52.0", 384 | "windows_x86_64_gnullvm 0.52.0", 385 | "windows_x86_64_msvc 0.52.0", 386 | ] 387 | 388 | [[package]] 389 | name = "windows_aarch64_gnullvm" 390 | version = "0.48.5" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 393 | 394 | [[package]] 395 | name = "windows_aarch64_gnullvm" 396 | version = "0.52.0" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 399 | 400 | [[package]] 401 | name = "windows_aarch64_msvc" 402 | version = "0.48.5" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 405 | 406 | [[package]] 407 | name = "windows_aarch64_msvc" 408 | version = "0.52.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 411 | 412 | [[package]] 413 | name = "windows_i686_gnu" 414 | version = "0.48.5" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 417 | 418 | [[package]] 419 | name = "windows_i686_gnu" 420 | version = "0.52.0" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 423 | 424 | [[package]] 425 | name = "windows_i686_msvc" 426 | version = "0.48.5" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 429 | 430 | [[package]] 431 | name = "windows_i686_msvc" 432 | version = "0.52.0" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 435 | 436 | [[package]] 437 | name = "windows_x86_64_gnu" 438 | version = "0.48.5" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 441 | 442 | [[package]] 443 | name = "windows_x86_64_gnu" 444 | version = "0.52.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 447 | 448 | [[package]] 449 | name = "windows_x86_64_gnullvm" 450 | version = "0.48.5" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 453 | 454 | [[package]] 455 | name = "windows_x86_64_gnullvm" 456 | version = "0.52.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 459 | 460 | [[package]] 461 | name = "windows_x86_64_msvc" 462 | version = "0.48.5" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 465 | 466 | [[package]] 467 | name = "windows_x86_64_msvc" 468 | version = "0.52.0" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 471 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pixelsort" 3 | version = "0.1.1" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = { version = "4.4.18", features = ["cargo", "wrap_help"] } 10 | image = { version = "0.24.8", default-features = false, features = ["png", "jpeg"] } 11 | rand = "0.8.5" 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024-present Void-ux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PixelSort 2 | 3 | PixelSort is a customisable CLI program that manipulates images through sorting columns or rows of pixels, producing unique "glitchy" yet beautiful effects. 4 | 5 | ## Showcase 6 | ![showcase image](https://cdn.overseer-bot.net/file/the-void/screenshots/output.png) 7 | 8 | *This image was produced with the default pixelsort arguments* 9 | 10 | ## Features 11 | 12 | Before delving into the features, it is worth understanding how pixelsort works. Essentially, pixels are taken either by columns (vertically) or by rows (horizontally) and sorted. Certain pixels can be excluded from the sort (so their position is never moved) through certain critera, for example, if a pixel is bright enough, it won't be moved. 13 | 14 | - Pixelsort is **very fast**, programmed in Rust with attention to programmatic design choice. 15 | - Pixelsort is generally very customisable, with different pixel exclusion algorithms allowing user-definable thresholds. 16 | - Pixelsort supports many different formats, such as **png**, **webp** and **jpeg**. 17 | 18 | ## Installation 19 | 20 | Currently only manual installation is supported while pixelsort is pre-release. 21 | 22 | ### Manual 23 | 24 | #### Downloading 25 | 26 | You can download pre-built binaries over on the [releases page](https://github.com/Void-ux/pixelsort/releases). 27 | 28 | #### Building 29 | 30 | Feel free to build and install from the source code directly (this requires the latest Rust compiler). 31 | ```shell 32 | cargo install --git https://github.com/Void-ux/pixelsort.git pixelsort 33 | ``` 34 | 35 | ### Usage 36 | 37 | Pixelsort is relatively simple to use, simply run: 38 | ```shell 39 | $ pixelsort image.png 40 | ``` 41 | 42 | This will apply the default arguments. You can customize pixelsort using arguments, for example: 43 | 44 | ```shell 45 | $ pixelsort image.png -r 90 --exclude random_exclude --sort saturation -o sorted_image.png 46 | ``` 47 | 48 | Which will: 49 | 50 | 1. Sort the pixels horizontally instead of vertically. 51 | 2. Exclude pixels randomly, meaning that each pixel will have a 20% chance of not "moving". 52 | 3. Sort the pixels based by their saturation in ascending order. So the "blandest" pixels will be at the top of the column. 53 | 4. Output the sorted image as "sorted_image.png" in the current directory. 54 | 55 | For more information on arguments/options, run 56 | ```shell 57 | $ pixelsort --help 58 | ``` 59 | -------------------------------------------------------------------------------- /ci/build.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Script for building your rust projects. 3 | set -e 4 | 5 | source ci/common.bash 6 | 7 | # $1 {path} = Path to cross/cargo executable 8 | CROSS=$1 9 | # $1 {string} = 10 | TARGET_TRIPLE=$2 11 | # $3 {boolean} = Whether or not building for release or not. 12 | RELEASE_BUILD=$3 13 | 14 | required_arg "$CROSS" 'CROSS' 15 | required_arg "$TARGET_TRIPLE" '' 16 | 17 | if [ -z "$RELEASE_BUILD" ]; then 18 | $CROSS build --target "$TARGET_TRIPLE" 19 | $CROSS build --target "$TARGET_TRIPLE" --all-features 20 | else 21 | $CROSS build --target "$TARGET_TRIPLE" --all-features --release 22 | fi 23 | -------------------------------------------------------------------------------- /ci/common.bash: -------------------------------------------------------------------------------- 1 | required_arg() { 2 | if [ -z "$1" ]; then 3 | echo "Required argument $2 missing" 4 | exit 1 5 | fi 6 | } 7 | -------------------------------------------------------------------------------- /ci/set_rust_version.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | rustup default "$1" 4 | rustup target add "$2" 5 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, builder::{ArgPredicate, OsStr}, value_parser, ArgMatches}; 2 | 3 | pub struct Cli { 4 | #[allow(dead_code)] 5 | matches: ArgMatches, 6 | pub input_file: String, 7 | pub output_file: String, 8 | pub exclude_algorithm: String, 9 | pub lower_threshold: f32, 10 | pub upper_threshold: f32, 11 | pub sort_algorithm: String, 12 | pub rotate: u16 13 | } 14 | 15 | impl Cli { 16 | pub fn from_args() -> Self { 17 | let matches = clap::Command::new("pixelsort") 18 | .about("Add unique, glitchy effects to your images by sorting pixels") 19 | .arg(arg!( "The file path of the image to pixel sort")) 20 | .arg( 21 | arg!( 22 | -o --output [name] "The file path to output to" 23 | ) 24 | .default_value("output.png") 25 | ) 26 | .arg( 27 | arg!( 28 | -e --exclude [value] "Determines which pixels to exclude from sorting" 29 | ) 30 | .value_parser(["lightness_threshold", "saturation_threshold", "hue_threshold", "random_exclude"]) 31 | .default_value("lightness_threshold") 32 | ) 33 | .arg( 34 | arg!( 35 | --lower_threshold [value] "Excludes pixels lower than this HSL value, e.g. excludes pixels darker than 10%" 36 | ) 37 | .value_parser(value_parser!(f32)) 38 | .default_value("0.25") 39 | .default_value_if("exclude", ArgPredicate::Equals(OsStr::from("random_exclude")), Some("0")) 40 | ) 41 | .arg( 42 | arg!( 43 | --upper_threshold [value] "Excludes pixels higher than this HSL value, e.g. excludes pixels more saturated than 60%" 44 | ) 45 | .value_parser(value_parser!(f32)) 46 | .default_value("0.8") 47 | .default_value_ifs([ 48 | ("sort", "saturation", Some("0.6")), 49 | ("exclude", "random_exclude", Some("5")) 50 | ]) 51 | ) 52 | .arg( 53 | arg!( 54 | -s --sort [value] "The pixel sorting algorithm to use" 55 | ) 56 | .value_parser(["lightness", "saturation", "hue"]) 57 | .default_value("lightness") 58 | ) 59 | .arg( 60 | arg!( 61 | -r --rotate [value] "Amount to rotate the image by before processing" 62 | ) 63 | .value_parser(["0", "90", "180", "270"]) 64 | .default_value("0") 65 | ) 66 | .get_matches(); 67 | 68 | let input_file = matches.get_one::("name").unwrap().to_owned(); 69 | let output_file = matches.get_one::("output").unwrap().to_owned(); 70 | let exclude_algorithm = matches.get_one::("exclude").unwrap().to_owned(); 71 | let lower_threshold = *matches.get_one::("lower_threshold").unwrap(); 72 | let upper_threshold = *matches.get_one::("upper_threshold").unwrap(); 73 | let sort_algorithm = matches.get_one::("sort").unwrap().to_owned(); 74 | let rotate = matches.get_one::("rotate").unwrap().parse::().unwrap(); 75 | 76 | Cli { 77 | matches, 78 | input_file, 79 | output_file, 80 | exclude_algorithm, 81 | lower_threshold, 82 | upper_threshold, 83 | sort_algorithm, 84 | rotate 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/exclude.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | use image::Rgb; 3 | 4 | pub fn random_exclude(pixels: Vec>, sort_func: fn(&Rgb) -> f32, lower: f32, upper: f32) -> Vec>> { 5 | let mut chunks: Vec>> = vec![]; 6 | 7 | let mut group = vec![]; 8 | for i in pixels { 9 | // could store this; computed twice 10 | let num = rand::thread_rng().gen_range(lower as usize..upper as usize); 11 | if num == 0 { 12 | group.sort_by_key(|i| (sort_func(i) * 100.0) as u32); 13 | group.push(i); 14 | chunks.push(group.clone()); 15 | group.clear(); 16 | } else { 17 | group.push(i); 18 | } 19 | } 20 | 21 | chunks 22 | } 23 | 24 | pub fn hsl_exclude( 25 | pixels: Vec>, 26 | sort_func: fn(&Rgb) -> f32, 27 | exclude_func: fn(&Rgb) -> f32, 28 | lower: f32, 29 | upper: f32, 30 | ) -> Vec>> { 31 | let mut chunks: Vec>> = vec![]; 32 | 33 | let mut group = vec![]; 34 | for i in pixels { 35 | // could store this; computed twice 36 | let val = exclude_func(&i); 37 | if val < lower || val > upper { 38 | group.sort_by_key(|i| (sort_func(i) * 100.0) as u32); 39 | group.push(i); 40 | chunks.push(group.clone()); 41 | group.clear(); 42 | } else { 43 | group.push(i); 44 | } 45 | } 46 | 47 | chunks 48 | } 49 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod exclude; 2 | mod sort; 3 | mod cli; 4 | 5 | use std::error::Error; 6 | 7 | use image::{GenericImage, GenericImageView, Pixel, Rgb}; 8 | 9 | use crate::exclude::{hsl_exclude, random_exclude}; 10 | use crate::sort::{hue, saturation, luminance}; 11 | use crate::cli::Cli; 12 | 13 | 14 | fn get_hsl_func(func_name: &str) -> fn(pixel: &Rgb) -> f32 { 15 | match func_name { 16 | "lightness" | "lightness_threshold" => luminance, 17 | "saturation" | "saturation_threshold" => saturation, 18 | "hue" | "hue_threshold" => hue, 19 | _ => panic!("Unknown HSL function name: {}", func_name), 20 | } 21 | } 22 | 23 | fn main() -> Result<(), Box> { 24 | let cli = Cli::from_args(); 25 | 26 | let mut source = image::open(cli.input_file).unwrap(); 27 | source = match cli.rotate { 28 | 0 => source, 29 | 90 => source.rotate90(), 30 | 180 => source.rotate180(), 31 | 270 => source.rotate270(), 32 | _ => source, 33 | }; 34 | let dims = source.dimensions(); 35 | let mut target = source.clone(); 36 | 37 | for x in 0..dims.0 { 38 | let mut col = vec![]; 39 | for y in 0..dims.1 { 40 | let pixel = source.get_pixel(x, y).to_rgb(); 41 | col.push(pixel); 42 | } 43 | 44 | let grouped_cols = match cli.exclude_algorithm.as_str() { 45 | "lightness_threshold" | "saturation_threshold" | "hue_threshold" => hsl_exclude( 46 | col, 47 | get_hsl_func(cli.sort_algorithm.as_str()), 48 | get_hsl_func(cli.exclude_algorithm.as_str()), 49 | cli.lower_threshold, 50 | cli.upper_threshold, 51 | ), 52 | "random_exclude" => random_exclude( 53 | col, 54 | get_hsl_func(cli.sort_algorithm.as_str()), 55 | cli.lower_threshold, 56 | cli.upper_threshold, 57 | ), 58 | _ => panic!("Unknown pixel exclusion algorithm"), 59 | }; 60 | for (c, i) in grouped_cols.concat().iter().enumerate() { 61 | target.put_pixel(x, c as u32, i.to_rgba()) 62 | } 63 | } 64 | 65 | target = match cli.rotate { 66 | 0 => target, 67 | 90 => target.rotate270(), 68 | 180 => target.rotate180(), 69 | 270 => target.rotate90(), 70 | _ => target, 71 | }; 72 | target 73 | .save(cli.output_file) 74 | .expect("Something went wrong with saving the file..."); 75 | 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /src/sort.rs: -------------------------------------------------------------------------------- 1 | use image::Rgb; 2 | 3 | pub fn luminance(pixel: &Rgb) -> f32 { 4 | (pixel.0.iter().max().unwrap().to_owned() as f32 / 255.0 5 | + pixel.0.iter().min().unwrap().to_owned() as f32 / 255.0) 6 | / 2.0 7 | } 8 | 9 | pub fn saturation(pixel: &Rgb) -> f32 { 10 | let min = pixel.0.iter().min().unwrap().to_owned() as f32; 11 | let max = pixel.0.iter().max().unwrap().to_owned() as f32; 12 | 13 | // no saturation 14 | if min == max { 15 | return 0.0; 16 | } 17 | // different formula if luminance > 50% 18 | if (min + max) / 2.0 > 0.5 { 19 | (max - min) / (max + min) 20 | } else { 21 | (max - min) / (2.0 - max - min) 22 | } 23 | } 24 | 25 | pub fn hue(pixel: &Rgb) -> f32 { 26 | let min = pixel.0.iter().min().unwrap().to_owned() as i32; 27 | let max = pixel.0.iter().max().unwrap().to_owned() as i32; 28 | if max - min == 0 { 29 | return 0.0; 30 | } 31 | 32 | let r = pixel.0[0] as i32; 33 | let g = pixel.0[1] as i32; 34 | let b = pixel.0[2] as i32; 35 | 36 | let mut _hue: i32; 37 | if r == max { 38 | _hue = g - b / (max - min); 39 | } else if g == max { 40 | _hue = 2 + (b - r) / (max - min); 41 | } else { 42 | _hue = 4 + (b - g) / (max - min); 43 | } 44 | 45 | _hue *= 60; 46 | if _hue < 0 { 47 | _hue += 360; 48 | } 49 | (_hue / 360) as f32 50 | } 51 | --------------------------------------------------------------------------------